店铺收款账户账单:默认账期改本月、日期不再自动查询、导出按钮与文案各页签统一

This commit is contained in:
yueyang 2026-10-08 14:59:56 +08:00
parent 30f69f2af9
commit 9710c3baa7
2 changed files with 16 additions and 9 deletions

View File

@ -112,7 +112,9 @@ 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'});
// 默认账期=本月:不填日期时查全表(几百万行)会很慢,默认落到当月既快又符合日常用法。
const currentMonthRange = () => { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const pad = value => String(value).padStart(2, '0'); const lastDay = new Date(year, month, 0).getDate(); return { start: `${year}-${pad(month)}-01`, end: `${year}-${pad(month)}-${pad(lastDay)}` }; };
const emptyFilters = billType => ({store_name:'',ledger_remark:'',bill_item:'',...currentMonthRange(),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};},
@ -144,7 +146,8 @@ export default {
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();},
// 选日期不自动查询:日期是一次选两个(开始/结束),中途触发会先按半截条件查全表,很慢。改完点「查询」再生效。
changeDate(field,value){const date=value||'';if(this.filters[field]===date)return;this.filters[field]=date;},
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})}万`;},

View File

@ -55,11 +55,11 @@
<div><strong>账单明细</strong><span v-if="!loading" class="tw-count">{{ total }} 条</span></div>
<div class="tw-tools">
<select v-model="exportMode" title="选择导出范围" aria-label="导出范围">
<option value="page-visible">当前页 · 当前显示列</option>
<option value="filtered-visible">全部筛选结果 · 当前显示列</option>
<option value="filtered-all">全部筛选结果 · 全部字段</option>
<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="exporting" @click="exportRows"><span class="material-icons-outlined" aria-hidden="true">download</span>{{ exporting ? '提交中…' : '导出' }}</button>
<button type="button" class="tw-button" :disabled="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="defaultColumns" :user-key="userKey" page-key="store-receiving-bills" tab-key="taobao-alipay" table-key="bill-details" @change="configuredColumns = $event" />
</div>
@ -133,6 +133,9 @@ import 'flatpickr/dist/flatpickr.css';
const dateConfig = { dateFormat: 'Y-m-d', locale: Mandarin, disableMobile: true, allowInput: false, onReady: (_, value, picker) => picker.calendarContainer.classList.add('alipay-bill-calendar') };
// 默认账期=本月:不填日期会查全表(几百万行)非常慢,默认落到当月。
const currentMonthRange = () => { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const pad = value => String(value).padStart(2, '0'); const lastDay = new Date(year, month, 0).getDate(); return { start: `${year}-${pad(month)}-01`, end: `${year}-${pad(month)}-${pad(lastDay)}` }; };
const COLUMNS = [
['记录编码', 'record_code', 'num', '基础信息'], ['原始店铺', 'store_name', 'text', '基础信息'], ['总账备注', 'ledger_remark', 'text', '基础信息'], ['自定义备注', 'custom_remark', 'text', '基础信息'], ['账单项目', 'bill_item', 'tag', '基础信息'],
['支付宝账号', 'alipay_account', 'num', '账户与交易'], ['账务订单号', 'account_order_no', 'num', '账户与交易'], ['业务流水号', 'business_serial_no', 'num', '账户与交易'], ['商户订单号', 'merchant_order_no', 'num', '账户与交易'], ['业务基础订单号', 'business_base_order_no', 'num', '账户与交易'], ['商品名称', 'goods_name', 'text', '账户与交易'], ['对方账号', 'counterparty_account', 'text', '账户与交易'], ['发生时间', 'occurred_at', 'date', '账户与交易'],
@ -145,7 +148,7 @@ const SORTABLE = ['occurred_at', 'income', 'expense', 'balance', 'record_code'];
export default {
name: 'TaobaoAlipayBills', components: { TableColumnSettings, flatPicker, [Select.name]: Select, [Option.name]: Option },
props: { apiBase: { type: String, required: true }, exportBase: { type: String, required: true }, progressBase: { type: String, default: '' }, userKey: { type: [String, Number], required: true } },
data() { return { loading: false, error: '', rows: [], configuredColumns: [], summary: { income: 0, expense: 0, balance: 0, count: 0 }, total: 0, currentPage: 1, lastPage: 1, perPage: 20, filters: { store_name: '', ledger_remark: '', source_channel: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }, filterOptions: { shops: [], bill_items: [], ledger_remarks: [] }, filterOptionsLoading: false, filterOptionsError: false, sort: { key: 'occurred_at', dir: 'desc' }, exportMode: 'filtered-visible', exporting: false, exportPanelOpen: false, exportTasks: [], exportHint: '导出在后台生成,成功后点「下载」;期间可以继续浏览其它页面。', exportTimer: null }; },
data() { return { loading: false, error: '', rows: [], configuredColumns: [], summary: { income: 0, expense: 0, balance: 0, count: 0 }, total: 0, currentPage: 1, lastPage: 1, perPage: 20, filters: { store_name: '', ledger_remark: '', source_channel: '', bill_item: '', business_type: '', ...currentMonthRange(), keyword: '' }, filterOptions: { shops: [], bill_items: [], ledger_remarks: [] }, filterOptionsLoading: false, filterOptionsError: false, sort: { key: 'occurred_at', dir: 'desc' }, exportMode: 'filtered-visible', exporting: false, exportPanelOpen: false, exportTasks: [], exportHint: '导出在后台生成,成功后点「下载」;期间可以继续浏览其它页面。', exportTimer: null }; },
computed: {
defaultColumns() { return COLUMNS; }, visibleColumns() { return this.configuredColumns.filter((column) => column.visible); },
visibleGroups() { return this.visibleColumns.reduce((groups, column) => { 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; }, []); },
@ -161,9 +164,10 @@ export default {
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() { this.loading = true; this.error = ''; try { const response = await window.axios.get(this.apiBase, { params: { ...this.filters, sort: this.sort.key, dir: this.sort.dir, limit: this.perPage, page: this.currentPage } }); const body = response.data || {}; 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 }; } catch (error) { this.rows = []; this.total = 0; this.summary = { income: 0, expense: 0, balance: 0, count: 0 }; this.error = (error.response && error.response.data && error.response.data.message) || '淘系支付宝账单暂时无法读取,请稍后重试。'; } finally { this.loading = false; } },
applyFilters() { if (this.invalidDateRange) return; this.currentPage = 1; this.fetch(); }, resetFilters() { this.filters = { store_name: '', ledger_remark: '', source_channel: '', bill_item: '', business_type: '', start: '', end: '', keyword: '' }; this.applyFilters(); },
applyFilters() { if (this.invalidDateRange) return; this.currentPage = 1; this.fetch(); }, resetFilters() { this.filters = { store_name: '', ledger_remark: '', source_channel: '', bill_item: '', business_type: '', ...currentMonthRange(), keyword: '' }; this.applyFilters(); },
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; this.applyFilters(); },
// 选日期不自动查询:日期是一对(开始/结束),中途触发会先按半截条件查全表,很慢。改完点「查询」再生效。
changeDate(field, value) { const date = value || ''; if (this.filters[field] === date) return; this.filters[field] = date; },
changePage(page) { this.currentPage = page; this.fetch(); this.$nextTick(() => { if (this.$refs.scroll) this.$refs.scroll.scrollTop = 0; }); },
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' ? '↑' : '↓') : '↕'; },
rawValue(row, column) { const value = row[column.field]; return value === null || value === undefined ? '' : value; },