272 lines
34 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>