272 lines
34 KiB
Vue
272 lines
34 KiB
Vue
<template>
|
||
<div class="table-workspace">
|
||
<div class="tw-stats">
|
||
<div v-for="stat in stats" :key="stat.label" class="tw-stat"><div class="tw-label">{{ stat.label }}</div><div class="tw-value" :class="stat.className">{{ stat.value }}</div><div class="tw-note">{{ stat.note }}</div></div>
|
||
</div>
|
||
<form class="tw-filter" @submit.prevent="applyFilters">
|
||
<label>店铺
|
||
<el-select v-if="usesMirrorOptions && !filterOptionsError" v-model="filters.store_name" filterable clearable :loading="filterOptionsLoading" placeholder="全部店铺" aria-label="店铺筛选" popper-class="account-bill-filter-options" no-match-text="没有匹配的店铺" no-data-text="暂无店铺" @change="applyFilters">
|
||
<el-option v-for="shop in filterOptions.shops" :key="shop" :label="shop" :value="shop" />
|
||
</el-select>
|
||
<input v-else v-model.trim="filters.store_name" placeholder="店铺名称" />
|
||
</label>
|
||
<label>总账备注
|
||
<el-select v-if="usesMirrorOptions && !filterOptionsError" v-model="filters.ledger_remark" filterable clearable :loading="filterOptionsLoading" placeholder="全部总账备注" aria-label="总账备注筛选" popper-class="account-bill-filter-options" no-match-text="没有匹配的总账备注" no-data-text="暂无总账备注" @change="applyFilters">
|
||
<el-option v-for="remark in filterOptions.ledger_remarks" :key="remark" :label="remark" :value="remark" />
|
||
</el-select>
|
||
<input v-else v-model.trim="filters.ledger_remark" placeholder="总账备注" />
|
||
</label>
|
||
<label>账单项目
|
||
<el-select v-if="usesMirrorOptions && !filterOptionsError" v-model="filters.bill_item" filterable clearable :loading="filterOptionsLoading" placeholder="全部账单项目" aria-label="账单项目筛选" popper-class="account-bill-filter-options" no-match-text="没有匹配的账单项目" no-data-text="暂无账单项目" @change="applyFilters">
|
||
<el-option v-for="item in filterOptions.bill_items" :key="item" :label="item" :value="item" />
|
||
</el-select>
|
||
<input v-else v-model.trim="filters.bill_item" placeholder="账单项目" />
|
||
</label>
|
||
<div class="tw-filter-field tw-date" :class="{ 'is-stable-date': usesStableDateControl }">
|
||
<span class="tw-filter-label">{{ dateFilterLabel }}</span>
|
||
<div class="tw-date-row">
|
||
<span class="tw-date-control has-picker" @click="usesStableDateControl && openDatePicker('startDate')">
|
||
<flat-picker ref="startDate" :value="filters.start" :config="startDateConfig" aria-label="开始日期" placeholder="开始日期" @on-change="(_, value) => changeDate('start', value)" />
|
||
<button v-if="filters.start" type="button" class="tw-date-clear" title="清除开始日期" aria-label="清除开始日期" @click.stop="changeDate('start', '')"><span class="material-icons-outlined" aria-hidden="true">close</span></button>
|
||
<button type="button" class="tw-date-picker" title="选择开始日期" aria-label="选择开始日期" @click.stop="openDatePicker('startDate')"><span class="material-icons-outlined" aria-hidden="true">calendar_today</span></button>
|
||
</span>
|
||
<b>至</b>
|
||
<span class="tw-date-control has-picker" @click="usesStableDateControl && openDatePicker('endDate')">
|
||
<flat-picker ref="endDate" :value="filters.end" :config="endDateConfig" aria-label="结束日期" placeholder="结束日期" @on-change="(_, value) => changeDate('end', value)" />
|
||
<button v-if="filters.end" type="button" class="tw-date-clear" title="清除结束日期" aria-label="清除结束日期" @click.stop="changeDate('end', '')"><span class="material-icons-outlined" aria-hidden="true">close</span></button>
|
||
<button type="button" class="tw-date-picker" title="选择结束日期" aria-label="选择结束日期" @click.stop="openDatePicker('endDate')"><span class="material-icons-outlined" aria-hidden="true">calendar_today</span></button>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<label class="tw-keyword">关键词(记录编码 / 订单 / 备注)<span class="tw-keyword-controls"><input v-model.trim="filters.keyword" placeholder="输入关键词" aria-label="关键词" /></span></label>
|
||
<div class="tw-actions"><button class="tw-button tw-primary" type="submit" :disabled="invalidDateRange">{{ loading ? '查询中…' : '查询' }}</button><button class="tw-button" type="button" @click="resetFilters">重置</button></div>
|
||
<p v-if="invalidDateRange" class="tw-filter-error" role="alert">结束日期不能早于开始日期</p>
|
||
<p v-if="filterOptionsError" class="tw-filter-error" role="alert">筛选选项加载失败,可输入名称查询。<button type="button" class="tw-button" @click="fetchFilterOptions">重试选项</button></p>
|
||
<p v-if="filtersDirty && !loading" class="tw-note" role="status">筛选条件已修改,查询后可导出对应结果。</p>
|
||
</form>
|
||
<section class="tw-panel" :aria-label="`${columnsTitle || '账户'}账单明细`">
|
||
<div v-if="isJdBillTab && sync" class="tw-note">来源:{{ sync.source }} · 已接入月份:{{ (sync.periods || []).join('、') }} · 导入时间:{{ sync.last_succeeded_at }}</div>
|
||
<div v-if="loadError" class="tw-alert" role="alert"><span>账单加载失败,请重试</span><button type="button" @click="fetch">重新加载</button></div>
|
||
<div class="tw-toolbar">
|
||
<div><strong>{{ sourceGroups.length ? '账单明细' : columnsTitle }}</strong><span v-if="!loading" class="tw-count">{{ total }} 条</span></div>
|
||
<div class="tw-tools" v-if="sourceColumns.length">
|
||
<select v-model="exportMode" title="选择导出范围" aria-label="导出范围"><option value="page-visible">当前页({{ rows.length }} 条)· 当前显示列</option><option value="filtered-visible">全部筛选结果({{ total }} 条)· 当前显示列</option><option value="filtered-all">全部筛选结果({{ total }} 条)· 全部字段</option></select>
|
||
<button type="button" class="tw-button" :disabled="!canExport || exporting" @click="exportRows"><span class="material-icons-outlined" aria-hidden="true">download</span>{{ exporting ? '提交中…' : (exportMode === 'page-visible' ? '导出当前页' : '导出全部筛选结果') }}</button>
|
||
<button type="button" class="tw-button" @click="toggleExportPanel"><span class="material-icons-outlined" aria-hidden="true">assignment</span>导出进度</button>
|
||
<table-column-settings :columns="sourceColumns" :user-key="userKey" page-key="store-receiving-bills" :tab-key="billType" table-key="bill-details" @change="configuredColumns = $event" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 导出进度:任务由后台进程生成,这里只看状态并下载(和聚水潭的导出任务一样)。 -->
|
||
<div v-if="exportPanelOpen" class="tw-export-tasks" role="region" aria-label="导出进度">
|
||
<div class="tw-export-tasks__head">
|
||
<strong>导出进度</strong>
|
||
<span class="tw-export-tasks__note">{{ exportHint }}</span>
|
||
<button type="button" class="tw-button" @click="refreshExportTasks">刷新</button>
|
||
<button type="button" class="tw-button" @click="exportPanelOpen = false">收起</button>
|
||
</div>
|
||
<table class="tw-export-tasks__table">
|
||
<thead>
|
||
<tr><th>序号</th><th>类型</th><th>计划时间</th><th>执行时间</th><th>状态</th><th>操作</th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(task, index) in exportTasks" :key="task.token">
|
||
<td>{{ index + 1 }}</td>
|
||
<td>{{ task.type }} · {{ task.tab_label }}</td>
|
||
<td>{{ task.planned_at || '—' }}</td>
|
||
<td>{{ task.finished_at || task.started_at || '—' }}</td>
|
||
<td><span class="tw-export-tasks__status" :class="'is-' + task.status">{{ statusLabel(task) }}</span></td>
|
||
<td><a v-if="task.status === 'done'" :href="downloadUrl(task)" class="tw-export-tasks__link">下载</a><span v-else>—</span></td>
|
||
</tr>
|
||
<tr v-if="!exportTasks.length"><td colspan="6" class="tw-export-tasks__empty">暂无导出任务</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<div ref="scroll" class="tw-scroll">
|
||
<table v-if="!loading && !loadError && configuredColumns.length" class="tw-table">
|
||
<thead>
|
||
<tr v-if="visibleGroups.length" class="tw-groups"><th v-for="(group,index) in visibleGroups" :key="`${group.label}-${index}`" :colspan="group.span" scope="colgroup">{{ group.label }}</th></tr>
|
||
<tr class="tw-columns" :class="{ 'is-single': !visibleGroups.length }"><th v-for="column in visibleColumns" :key="column.key" :class="{ 'is-number': isNumber(column.kind), 'is-sortable': sortableField(column.field) }" scope="col" @click="maybeSort(column)">{{ column.label }} <span v-if="sortableField(column.field)" class="tw-sort" aria-hidden="true">{{ sortMark(column.field) }}</span></th></tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="row in rows" :key="row.id"><td v-for="column in visibleColumns" :key="column.key" :class="cellClass(column)"><span v-if="column.kind === 'tag'" class="tw-tag">{{ displayValue(row,column) }}</span><template v-else>{{ displayValue(row,column) }}</template></td></tr>
|
||
<tr v-if="!rows.length"><td :colspan="visibleColumns.length" class="tw-empty">没有符合条件的账单记录</td></tr>
|
||
</tbody>
|
||
</table>
|
||
<div v-else-if="loading" class="tw-empty" role="status">加载中…</div><div v-else-if="!loadError" class="tw-empty">暂无账单字段</div>
|
||
</div>
|
||
<div v-if="rows.length" class="tw-pager"><div>第 {{ rangeStart }} - {{ rangeEnd }} 条 / 共 {{ total }} 条</div><div class="tw-page-actions"><label>每页 <select v-model.number="perPage" @change="changePage(1)"><option :value="10">10</option><option :value="20">20</option><option :value="50">50</option></select></label><button type="button" :disabled="currentPage<=1" title="第一页" @click="changePage(1)">«</button><button type="button" :disabled="currentPage<=1" title="上一页" @click="changePage(currentPage-1)">‹</button><span>{{ currentPage }} / {{ lastPage }}</span><button type="button" :disabled="currentPage>=lastPage" title="下一页" @click="changePage(currentPage+1)">›</button><button type="button" :disabled="currentPage>=lastPage" title="最后一页" @click="changePage(lastPage)">»</button></div></div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import TableColumnSettings from './TableColumnSettings.vue';
|
||
import { Select, Option } from 'element-ui';
|
||
import flatPicker from 'vue-flatpickr-component';
|
||
import { Mandarin } from 'flatpickr/dist/l10n/zh';
|
||
import 'flatpickr/dist/flatpickr.css';
|
||
const SORTABLE = ['occurred_at','income','expense','balance','record_code'];
|
||
// 京东账单与京喜账单共用同一份列定义与同一套交互,只在数据口径上拆分。
|
||
const JD_BILL_TABS = ['jd-transactions','jingxi'];
|
||
// 时间筛选标签与该页签的时间列名保持一致(其余页签沿用「发生时间」)。
|
||
const DATE_FILTER_LABELS = {'jd-transactions':'费用结算时间','jingxi':'费用结算时间','jd-wallet':'费用结算时间','douyin':'结算时间','vipshop':'结算时间','vipshop-jx':'结算时间'};
|
||
const dateConfig = {dateFormat:'Y-m-d',locale:Mandarin,disableMobile:true,allowInput:false,onReady:(_,value,picker)=>picker.calendarContainer.classList.add('account-bill-calendar')};
|
||
const emptyFilters = billType => ({store_name:'',ledger_remark:'',bill_item:'',start:'',end:'',keyword:'',keyword_match:'contains'});
|
||
export default {
|
||
name:'AccountBills', components:{ TableColumnSettings, flatPicker, [Select.name]:Select, [Option.name]:Option }, props:{ apiBase:{type:String,required:true}, billType:{type:String,required:true}, progressBase:{type:String,default:''}, userKey:{type:[String,Number],required:true} },
|
||
data(){return{loading:false,loadError:false,sync:null,rows:[],sourceColumns:[],configuredColumns:[],columnsTitle:'',sourceGroups:[],summary:{income:0,expense:0,balance:0,count:0},total:0,currentPage:1,lastPage:1,perPage:20,filters:emptyFilters(this.billType),queryFilters:emptyFilters(this.billType),resultFilters:null,filterOptions:{shops:[],bill_items:[],ledger_remarks:[]},filterOptionsLoading:false,filterOptionsError:false,requestSerial:0,sort:{key:'occurred_at',dir:'desc'},exportMode:'filtered-visible',exporting:false,exportPanelOpen:false,exportTasks:[],exportHint:'导出在后台生成,成功后点「下载」;期间可以继续浏览其它页面。',exportTimer:null};},
|
||
computed:{
|
||
isDouyin(){return this.billType==='douyin';},
|
||
companyPrefix(){const matched=(this.apiBase||'').match(/^\/\d+/);return matched?matched[0]:'';},
|
||
tasksUrl(){return this.companyPrefix+'/ecommerce-finance/account-bills/export-tasks';},
|
||
// 费用结算时间标签、来源说明与收支净额统计口径按京东系列页签(京东账单/京喜账单)统一。
|
||
isJdBillTab(){return JD_BILL_TABS.indexOf(this.billType)!==-1;},
|
||
dateFilterLabel(){return DATE_FILTER_LABELS[this.billType]||'发生时间';},
|
||
// Every account-bill tab uses the same explicit picker path. Keeping one
|
||
// control avoids browser-native date inputs appearing on older tabs.
|
||
usesStableDateControl(){return true;},
|
||
usesMirrorOptions(){return ['douyin','pinduoduo','dewu','taobao-wechat','jd-transactions','jingxi','jd-wallet','vipshop','vipshop-jx'].indexOf(this.billType)!==-1;},
|
||
invalidDateRange(){return Boolean(this.filters.start&&this.filters.end&&this.filters.start>this.filters.end);},
|
||
startDateConfig(){return {...dateConfig,maxDate:this.filters.end||null};},
|
||
endDateConfig(){return {...dateConfig,minDate:this.filters.start||null};},
|
||
filtersDirty(){return !this.resultFilters||Object.keys(this.filters).some(key=>this.filters[key]!==this.resultFilters[key]);},
|
||
canExport(){return !this.loading&&!this.loadError&&!this.invalidDateRange&&!this.filtersDirty&&this.total>0&&(this.exportMode==='filtered-all'||this.visibleColumns.length>0);},
|
||
visibleColumns(){return this.configuredColumns.filter(column=>column.visible);},
|
||
visibleGroups(){return this.visibleColumns.reduce((groups,column)=>{if(!column.group)return groups;const last=groups[groups.length-1];if(last&&last.label===column.group)last.span+=1;else groups.push({label:column.group,span:1});return groups;},[]);},
|
||
stats(){const netLabel=JD_BILL_TABS.concat(['vipshop-jx']).indexOf(this.billType)!==-1;return[{label:'收入合计',value:this.moneyWan(this.summary.income),note:`${this.summary.count} 笔交易`,className:'is-income'},{label:'支出合计',value:this.moneyWan(this.summary.expense),note:'按当前筛选',className:'is-expense'},{label:netLabel?'收支净额':(this.billType==='jd-wallet'?'账户余额合计(最新)':'期末余额(最新)'),value:this.moneyWan(this.summary.balance),note:netLabel?'按当前筛选':(this.billType==='jd-wallet'?'筛选结果中每个店铺账户最新一笔':'最新一条记录'),className:''},{label:'交易笔数',value:this.summary.count,note:'当前筛选结果',className:''}];},
|
||
rangeStart(){return this.total===0?0:(this.currentPage-1)*this.perPage+1;},rangeEnd(){return Math.min(this.currentPage*this.perPage,this.total);}
|
||
},
|
||
mounted(){if(this.usesMirrorOptions)this.fetchFilterOptions();this.fetch();},
|
||
methods:{
|
||
async fetchFilterOptions(){this.filterOptionsLoading=true;this.filterOptionsError=false;try{const response=await window.axios.get(`${this.apiBase}/filter-options`);const body=response.data||{};this.filterOptions={shops:Array.isArray(body.shops)?body.shops:[],bill_items:Array.isArray(body.bill_items)?body.bill_items:[],ledger_remarks:Array.isArray(body.ledger_remarks)?body.ledger_remarks:[]};}catch(error){this.filterOptionsError=true;}finally{this.filterOptionsLoading=false;}},
|
||
async fetch(){const requestId=++this.requestSerial;const requestedFilters={...this.queryFilters};this.loading=true;this.loadError=false;try{const response=await window.axios.get(this.apiBase,{params:{...requestedFilters,sort:this.sort.key,dir:this.sort.dir,limit:this.perPage,page:this.currentPage}});if(requestId!==this.requestSerial)return;const body=response.data||{};this.resultFilters=requestedFilters;this.sync=body.sync||null;this.rows=body.data||[];this.total=body.total||0;this.currentPage=body.current_page||1;this.lastPage=body.last_page||1;this.perPage=body.per_page||this.perPage;this.summary=body.summary||{income:0,expense:0,balance:0,count:0};if(body.columns){this.columnsTitle=body.columns.title||'';this.sourceGroups=body.columns.groups||[];this.sourceColumns=this.attachGroups(body.columns.columns||body.columns||[],this.sourceGroups);}}catch(error){if(requestId!==this.requestSerial)return;this.rows=[];this.total=0;this.sync=null;this.summary={income:0,expense:0,balance:0,count:0};this.loadError=true;}finally{if(requestId===this.requestSerial)this.loading=false;}},
|
||
attachGroups(columns,groups){let index=0;return columns.map((column,columnIndex)=>{while(groups[index]&&columnIndex>=groups.slice(0,index+1).reduce((sum,group)=>sum+Number(group.span||0),0))index+=1;return{...column,key:column.field||`column-${columnIndex}`,group:groups[index]?groups[index].label:''};});},
|
||
applyFilters(){if(this.invalidDateRange)return;this.queryFilters={...this.filters};this.currentPage=1;return this.fetch();},resetFilters(){this.filters=emptyFilters(this.billType);return this.applyFilters();},changePage(page){this.currentPage=page;this.fetch();this.$nextTick(()=>{if(this.$refs.scroll)this.$refs.scroll.scrollTop=0;});},
|
||
openDatePicker(refName){const picker=this.$refs[refName];if(picker&&picker.fp)picker.fp.open();},
|
||
changeDate(field,value){const date=value||'';if(this.filters[field]===date)return;this.filters[field]=date;return this.applyFilters();},
|
||
maybeSort(column){if(!this.sortableField(column.field))return;this.sort.dir=this.sort.key===column.field&&this.sort.dir==='desc'?'asc':'desc';this.sort.key=column.field;this.applyFilters();},sortableField(field){return SORTABLE.indexOf(field)!==-1;},sortMark(field){return this.sort.key===field?(this.sort.dir==='asc'?'↑':'↓'):'↕';},
|
||
getValue(row,field){if(!field)return'';if(field.indexOf('payload.')===0){const payload=row.payload||{};const value=payload[field.slice(8)];return value===null||value===undefined?'':value;}const value=row[field];return value===null||value===undefined?'':value;},rawValue(row,column){return this.getValue(row,column.field);},
|
||
displayValue(row,column){const value=this.rawValue(row,column);if(value==='')return'—';if(column.kind==='money_in')return Number(value)>0?`+${this.money(value)}`:'—';if(column.kind==='money_out')return Number(value)>0?`-${this.money(value)}`:'—';if(['balance','money'].indexOf(column.kind)!==-1)return this.money(value);return value;},cellClass(column){return{'is-number':this.isNumber(column.kind),'is-id':column.kind==='num','is-income':column.kind==='money_in','is-expense':column.kind==='money_out'};},isNumber(kind){return['money','money_in','money_out','balance','num'].indexOf(kind)!==-1;},money(value){return`¥${Number(value||0).toLocaleString('zh-CN',{minimumFractionDigits:2,maximumFractionDigits:2})}`;},moneyWan(value){return`¥${(Number(value||0)/10000).toLocaleString('zh-CN',{minimumFractionDigits:2,maximumFractionDigits:2})}万`;},
|
||
exportRows(){
|
||
if(!this.canExport||this.exporting)return;
|
||
if(this.exportMode==='page-visible'){this.downloadCurrentPage();return;}
|
||
const payload={...(this.resultFilters||{}),tab:this.billType,sort:this.sort.key,dir:this.sort.dir,total:Number(this.total||0)};
|
||
if(this.exportMode==='filtered-visible'){payload.columns=this.visibleColumns.map(column=>column.field);}
|
||
this.createExportTask(payload);
|
||
},
|
||
// 导出任务:后台生成,页面只提交任务并到「导出进度」里看状态、下载。
|
||
async createExportTask(payload){
|
||
this.exporting=true;
|
||
this.exportPanelOpen=true;
|
||
this.exportHint='已提交导出任务,正在后台生成…';
|
||
try{
|
||
await window.axios.post(this.tasksUrl,payload);
|
||
this.exportHint='导出在后台生成,成功后点「下载」;期间可以继续浏览其它页面。';
|
||
await this.refreshExportTasks();
|
||
}catch(error){
|
||
this.exportHint=(error.response&&error.response.data&&error.response.data.message)||'提交导出任务失败,请重试。';
|
||
}finally{
|
||
this.exporting=false;
|
||
}
|
||
},
|
||
toggleExportPanel(){
|
||
this.exportPanelOpen=!this.exportPanelOpen;
|
||
if(this.exportPanelOpen){this.refreshExportTasks();}
|
||
},
|
||
async refreshExportTasks(){
|
||
try{
|
||
const response=await window.axios.get(this.tasksUrl,{headers:{Accept:'application/json'}});
|
||
this.exportTasks=(response.data&&response.data.data)||[];
|
||
}catch(error){
|
||
this.exportHint='导出任务列表加载失败,请点「刷新」重试。';
|
||
return;
|
||
}
|
||
this.scheduleExportRefresh();
|
||
},
|
||
scheduleExportRefresh(){
|
||
if(this.exportTimer){window.clearTimeout(this.exportTimer);this.exportTimer=null;}
|
||
if(!this.exportPanelOpen){return;}
|
||
const pending=this.exportTasks.some(task=>task.status==='queued'||task.status==='running');
|
||
if(!pending){return;}
|
||
this.exportTimer=window.setTimeout(()=>this.refreshExportTasks(),3000);
|
||
},
|
||
statusLabel(task){
|
||
return {queued:'排队中',running:'导出中',done:'成功',failed:'失败'}[task.status]||task.status;
|
||
},
|
||
downloadUrl(task){
|
||
return `${this.tasksUrl}/${encodeURIComponent(task.token)}/download`;
|
||
},
|
||
downloadCurrentPage(){const csvRows=[this.visibleColumns.map(column=>column.label),...this.rows.map(row=>this.visibleColumns.map(column=>this.rawValue(row,column)))];const csv=csvRows.map(values=>values.map((value, index) => this.csvCell(value, this.visibleColumns[index])).join(',')).join('\r\n');const link=document.createElement('a');link.href=URL.createObjectURL(new Blob(['\ufeff',csv],{type:'text/csv;charset=utf-8'}));link.download=`${this.billType}_account_bills_page_${this.currentPage}.csv`;link.click();URL.revokeObjectURL(link.href);},csvCell(value,column){const text=String(value===null||value===undefined?'':value);const safe=column.kind==='num'&&/^(?:0\d|\d{12,}$)/.test(text)?`\t${text}`:(/^[=+\-@]/.test(text)?`'${text}`:text);return`"${safe.replace(/"/g,'""')}"`;}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
.table-workspace{padding:1rem;color:#171717;font:400 .8125rem/1.5 "JetBrains Mono","Microsoft YaHei","PingFang SC",monospace}.tw-stats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.375rem;margin-bottom:.375rem}.tw-stat{padding:.375rem .625rem;border:1px solid #d8d2c7;border-radius:.5rem;background:#fff}.tw-label,.tw-note,.tw-count{color:#595959;font-size:.625rem}.tw-value{margin-top:.125rem;font-size:1rem;font-weight:700;font-variant-numeric:tabular-nums}.tw-note{margin-top:.125rem}.is-income{color:#53793e}.is-expense{color:#b80000}.tw-filter{display:flex;flex-wrap:wrap;gap:.375rem;margin-bottom:.375rem;padding:.375rem .625rem;border:1px solid #d8d2c7;border-radius:.5rem;background:#fff}.tw-filter>label,.tw-filter-field{display:flex;flex-direction:column;gap:0;color:#595959;font-size:.625rem;font-weight:500}.tw-filter input,.tw-filter select,.tw-tools select{height:1.75rem;min-width:10rem;padding:0 .625rem;border:1px solid #d8d2c7;border-radius:.5rem;background:#fff;color:#171717;font:400 .8125rem/1 inherit}.tw-filter input:focus,.tw-filter select:focus,.tw-tools select:focus{border-color:#856632;box-shadow:0 0 0 3px rgba(133,102,50,.2);outline:none}.tw-filter .tw-keyword{flex:1;min-width:14rem}.tw-keyword input{width:100%}.tw-date-row,.tw-actions,.tw-tools,.tw-page-actions{display:flex;align-items:center;gap:.5rem}.tw-date-control{display:flex;align-items:stretch}.tw-date-control input{min-width:8.5rem}.tw-date-control.has-picker input{border-radius:.5rem 0 0 .5rem}.tw-date-picker{display:inline-flex;align-items:center;justify-content:center;width:2.125rem;height:2.125rem;padding:0;border:1px solid #d8d2c7;border-left:0;border-radius:0 .5rem .5rem 0;background:#fff;color:#856632;cursor:pointer}.tw-date-picker:hover,.tw-date-picker:focus-visible{background:#f7f0de;color:#6a5129;outline:none}.tw-date-picker:focus-visible{box-shadow:0 0 0 3px rgba(133,102,50,.2)}.tw-date-picker .material-icons-outlined{font-size:1.125rem}.tw-date b{font-weight:400}.tw-filter-error{grid-column:1/-1;margin:0;color:#b80000;font-size:.75rem}.tw-actions{align-self:flex-end;margin-left:auto}.tw-button,.tw-page-actions button{display:inline-flex;align-items:center;justify-content:center;gap:.375rem;height:1.875rem;padding:0 .75rem;border:1px solid #d8d2c7;border-radius:.5rem;background:#fff;color:#171717;font:500 .8125rem/1 inherit;cursor:pointer}.tw-button:hover{border-color:#856632;color:#856632}.tw-button:disabled{cursor:not-allowed;opacity:.55}.tw-primary{border-color:#856632;background:#856632;color:#fff}.tw-primary:hover{background:#6a5129;color:#fff}.tw-button .material-icons-outlined{font-size:1.125rem}.tw-panel{overflow:visible;border:1px solid #d8d2c7;border-radius:.5rem;background:#fff}.tw-alert{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin:.75rem;padding:.75rem;border:1px solid #b80000;border-radius:.5rem;background:#fff4f4;color:#b80000}.tw-alert button{border:0;background:transparent;color:#b80000;font:inherit;font-weight:600;cursor:pointer}.tw-toolbar{display:flex;align-items:center;justify-content:space-between;gap:.75rem;min-height:3.5rem;padding:.625rem .875rem;border-bottom:1px solid #d8d2c7}.tw-toolbar strong{margin-right:.625rem;font-size:.875rem}.tw-tools{flex-wrap:wrap;justify-content:flex-end}.tw-tools>select{min-width:13.75rem}.tw-scroll{position:relative;max-height:min(62vh,42rem);overflow:auto;overscroll-behavior:contain;scrollbar-gutter:stable}.tw-table{width:max-content;min-width:100%;border-collapse:separate;border-spacing:0;font-size:.78125rem}.tw-table th,.tw-table td{min-width:7.5rem;padding:.5rem .625rem;border-right:1px solid #eeeae2;border-bottom:1px solid #eeeae2;white-space:nowrap}.tw-table thead th{position:sticky;z-index:4;background:#f7f0de;color:#171717;font-weight:600}.tw-groups th{top:0;height:2rem;text-align:center;color:#856632;font-size:.75rem}.tw-columns th{top:2rem;height:2.25rem;text-align:left;background:#fbfaf7;box-shadow:inset 0 -1px #d8d2c7}.tw-columns.is-single th{top:0}.tw-columns th.is-sortable{cursor:pointer}.tw-columns th.is-sortable:hover,.tw-sort{color:#856632}.tw-table tbody tr:hover{background:#f7f0de}.tw-table .is-number{text-align:right;font-variant-numeric:tabular-nums}.tw-table .is-id{color:#595959}.tw-tag{display:inline-block;padding:.0625rem .375rem;border-radius:.25rem;background:#f7f0de;color:#856632}.tw-empty{padding:3rem 1rem!important;text-align:center!important;color:#595959}.tw-pager{display:flex;align-items:center;justify-content:space-between;gap:.75rem;min-height:3.5rem;padding:.625rem .875rem;border-top:1px solid #d8d2c7;color:#595959}.tw-page-actions select{height:1.875rem;border:1px solid #d8d2c7;border-radius:.375rem}.tw-page-actions button{width:1.875rem;height:1.875rem;padding:0}.tw-page-actions button:disabled{cursor:not-allowed;opacity:.45}@media(max-width:900px){.table-workspace{padding:.75rem}.tw-stats{grid-template-columns:repeat(2,minmax(0,1fr))}.tw-filter>label,.tw-filter-field,.tw-filter .tw-keyword{width:100%}.tw-filter input,.tw-filter select{width:100%}.tw-date-control{width:100%}.tw-date-control input{min-width:0}.tw-date-control.has-picker input{width:calc(100% - 2.125rem)}.tw-actions{margin-left:0}.tw-toolbar,.tw-pager{align-items:flex-start;flex-direction:column}.tw-tools{justify-content:flex-start}.tw-scroll{max-height:58vh;width:100%}}@media(max-width:520px){.tw-stats{grid-template-columns:1fr}.tw-date-row{align-items:stretch;flex-direction:column}.tw-tools>select{min-width:0;max-width:100%}.tw-page-actions{flex-wrap:wrap}}
|
||
.tw-toolbar { flex-wrap: wrap; }
|
||
.tw-tools { flex: 1 1 24rem; min-width: 0; }
|
||
.tw-export-tasks { padding: .5rem .875rem .75rem; border-bottom: 1px solid #d8d2c7; background: #fffdf8; }
|
||
.tw-export-tasks__head { display: flex; align-items: center; gap: .5rem; margin-bottom: .4rem; }
|
||
.tw-export-tasks__head strong { font-size: .8125rem; }
|
||
.tw-export-tasks__note { flex: 1 1 auto; color: #595959; font-size: .75rem; }
|
||
.tw-export-tasks__table { width: 100%; max-height: 11rem; overflow: auto; font-size: .75rem; border-collapse: separate; border-spacing: 0; display: block; }
|
||
.tw-export-tasks__table thead th { padding: .25rem .5rem; background: #f7f0de; color: #171717; font-weight: 600; text-align: left; white-space: nowrap; }
|
||
.tw-export-tasks__table td { padding: .25rem .5rem; border-bottom: 1px solid #efe8db; white-space: nowrap; }
|
||
.tw-export-tasks__empty { text-align: center; color: #8a8377; }
|
||
.tw-export-tasks__status { padding: .0625rem .375rem; border-radius: .25rem; background: #f2f2f2; color: #595959; }
|
||
.tw-export-tasks__status.is-done { background: #e6f0e0; color: #3f6b2b; }
|
||
.tw-export-tasks__status.is-running { background: #f7f0de; color: #8a6a2f; }
|
||
.tw-export-tasks__status.is-queued { background: #eef1f7; color: #4a5a75; }
|
||
.tw-export-tasks__status.is-failed { background: #fdecec; color: #b80000; }
|
||
.tw-export-tasks__link { color: #856632; font-weight: 600; text-decoration: underline; }
|
||
/* 筛选栏默认一行铺开(店铺 / 总账备注 / 账单项目 / 关键词 / 时间 / 查询),只有容器变窄才折行。 */
|
||
.tw-filter{display:flex;flex-wrap:wrap;align-items:flex-end;gap:.375rem .5rem}.tw-filter>label,.tw-filter>.tw-filter-field{flex:0 1 11rem;min-width:9rem}.tw-filter>.tw-keyword{flex:1 1 12rem;min-width:10rem}.tw-filter>.tw-date{flex:0 1 22rem;min-width:15rem}.tw-filter>.tw-actions{flex:0 0 auto;align-self:flex-end;justify-content:flex-end;margin-left:auto}
|
||
@container(max-width:1024px){.table-workspace .tw-filter>label,.table-workspace .tw-filter>.tw-filter-field{flex:1 1 10rem}.table-workspace .tw-filter>.tw-date{flex:1 1 100%}.table-workspace .tw-filter>.tw-actions{flex:1 1 100%;margin-left:0}}
|
||
@media(max-width:560px){.tw-filter>label,.tw-filter>.tw-filter-field,.tw-filter>.tw-keyword{flex:1 1 100%}.tw-filter>.tw-actions{justify-content:flex-start}}
|
||
.tw-filter ::v-deep .el-select{width:100%;min-width:0}
|
||
.tw-filter ::v-deep .el-select .el-input__inner{height:28px;min-width:0;padding-right:28px;border-color:#d8d2c7;border-radius:8px;color:#171717;font:inherit}
|
||
.tw-filter ::v-deep .el-select .el-input.is-focus .el-input__inner,.tw-filter ::v-deep .el-select .el-input__inner:focus{border-color:#856632}
|
||
.tw-filter ::v-deep .el-input__icon{line-height:28px}
|
||
.tw-filter>.tw-note,.tw-filter>.tw-filter-error{flex:1 1 100%;margin:0}
|
||
.tw-keyword-controls{display:flex;flex-wrap:wrap;gap:4px;min-width:0}
|
||
.tw-keyword-controls>select{min-width:0;width:100%}
|
||
.tw-keyword-controls>input{min-width:0}
|
||
@media(max-width:900px){.tw-tools{flex:0 0 auto;height:auto;width:100%}}
|
||
.table-workspace{container-type:inline-size}
|
||
.tw-date-control{flex:1 1 0;min-width:0;width:auto;max-width:100%}
|
||
.tw-date-control.has-picker input{flex:1;width:0;min-width:0;cursor:pointer;background:#fff}
|
||
.tw-date-picker{flex:none}
|
||
.tw-date-clear{display:flex;align-items:center;justify-content:center;width:1.5rem;flex:none;padding:0;border:1px solid #d8d2c7;border-left:0;border-right:0;background:#fff;color:#595959;cursor:pointer}
|
||
.tw-date-clear .material-icons-outlined{font-size:1rem}
|
||
.tw-date.is-stable-date .tw-date-control{position:relative;overflow:hidden;height:1.75rem;border:1px solid #d8d2c7;border-radius:.5rem;background:#fff;cursor:pointer}
|
||
.tw-date.is-stable-date .tw-date-control:focus-within{border-color:#856632;box-shadow:0 0 0 3px rgba(133,102,50,.2)}
|
||
.tw-date.is-stable-date .tw-date-control.has-picker input{width:100%;height:100%;padding:0 4rem 0 .625rem;border:0;border-radius:0;background:transparent;box-shadow:none;cursor:pointer}
|
||
.tw-date.is-stable-date .tw-date-picker,.tw-date.is-stable-date .tw-date-clear{position:absolute;top:0;height:1.625rem;border-top:0;border-bottom:0}
|
||
.tw-date.is-stable-date .tw-date-picker{right:0;border-right:0;border-radius:0}
|
||
.tw-date.is-stable-date .tw-date-clear{right:2.125rem;border-left:0;z-index:1}
|
||
@container(max-width:860px){
|
||
.table-workspace .tw-date{flex:1 1 100%}
|
||
.table-workspace .tw-keyword-controls{flex-wrap:nowrap}
|
||
.table-workspace .tw-keyword-controls>select{width:auto}
|
||
.table-workspace .tw-keyword-controls>input{flex:1;width:0}
|
||
}
|
||
@container(max-width:560px){
|
||
.table-workspace .tw-date{flex:1 1 100%}
|
||
.table-workspace .tw-keyword-controls{flex-wrap:wrap}
|
||
.table-workspace .tw-keyword-controls>select,.table-workspace .tw-keyword-controls>input{width:100%;flex:auto}
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
.account-bill-calendar{font-family:"Microsoft YaHei",sans-serif;border:1px solid #d8d2c7;border-radius:8px;background:#fff}
|
||
.account-bill-calendar.open{opacity:1}
|
||
.account-bill-calendar .flatpickr-day.selected,.account-bill-calendar .flatpickr-day.selected:hover{background:#856632;border-color:#856632}
|
||
.account-bill-calendar .flatpickr-day.today{border-color:#856632}
|
||
.account-bill-calendar .flatpickr-day:hover,.account-bill-calendar .flatpickr-day:focus{background:#f7f0de;border-color:#d8d2c7}
|
||
.account-bill-filter-options .el-select-dropdown__item.selected{color:#856632}
|
||
.account-bill-filter-options .el-select-dropdown__item.hover,.account-bill-filter-options .el-select-dropdown__item:hover{background:#f7f0de}
|
||
.account-bill-filter-options{max-width:calc(100vw - 32px)}
|
||
.account-bill-filter-options .el-select-dropdown__item{max-width:100%;text-overflow:ellipsis;overflow:hidden}
|
||
</style>
|