@ -9,13 +9,40 @@
< / div >
< form class = "tw-filter" @submit.prevent ="applyFilters" >
< label > 原始店铺 < input v -model .trim = " filters.store_name " placeholder = "如 天猫-亿家达旗舰店" / > < / label >
< label > 原始店铺
< el-select v-if = "!filterOptionsError" v-model="filters.store_name" filterable clearable :loading="filterOptionsLoading" placeholder="全部店铺" aria-label="店铺筛选" popper-class="alipay-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 > 数据源渠道 < input v -model .trim = " filters.source_channel " placeholder = "如 alipayCSV" / > < / label >
< label > 账单项目 < input v -model .trim = " filters.bill_item " placeholder = "如 交易佣金" / > < / label >
< label > 账单项目
< el-select v-if = "!filterOptionsError" v-model="filters.bill_item" filterable clearable :loading="filterOptionsLoading" placeholder="全部账单项目" aria-label="账单项目筛选" popper-class="alipay-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 >
< label > 收支类型 < select v-model = "filters.business_type"><option value="" > 全部类型 < / option > < option > 收入 < / option > < option > 支出 < / option > < / select > < / label >
< label class = "tw-date" > 发生时间 < span > < input v-model = "filters.start" type="date" aria-label="开始日期" /><b>至</b><input v-model="filters.end" type="date" aria-label="结束日期" / > < / span > < / label >
< div class = "tw-filter-field tw-date" >
< span class = "tw-filter-label" > 发生时间 < / span >
< div class = "tw-date-row" >
< span class = "tw-date-control has-picker" @click ="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 ="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" > 关键词 ( 费用 / 订单 / 商品编码 ) < input v -model .trim = " filters.keyword " placeholder = "输入费用名称、订单号或商品编码" / > < / label >
< div class = "tw-actions" > < button class = "tw-button tw-primary" type = "submit" > 查询 < / button > < button class = "tw-button" type = "button" @click ="resetFilters" > 重置 < / button > < / div >
< div class = "tw-actions" > < button class = "tw-button tw-primary" type = "submit" :disabled = "invalidDateRange" > 查询 < / 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 >
< / form >
< section class = "tw-panel" aria -label = " 淘系支付宝账户账单明细 " >
@ -68,6 +95,12 @@
< 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 dateConfig = { dateFormat : 'Y-m-d' , locale : Mandarin , disableMobile : true , allowInput : false , onReady : ( _ , value , picker ) => picker . calendarContainer . classList . add ( 'alipay-bill-calendar' ) } ;
const COLUMNS = [
[ '记录编码' , 'record_code' , 'num' , '基础信息' ] , [ '原始店铺' , 'store_name' , 'text' , '基础信息' ] , [ '自定义备注' , 'custom_remark' , 'text' , '基础信息' ] , [ '账单项目' , 'bill_item' , 'tag' , '基础信息' ] ,
@ -79,19 +112,25 @@ const COLUMNS = [
const SORTABLE = [ 'occurred_at' , 'income' , 'expense' , 'balance' , 'record_code' ] ;
export default {
name : 'TaobaoAlipayBills' , components : { TableColumnSettings } ,
name : 'TaobaoAlipayBills' , components : { TableColumnSettings , flatPicker , [ Select . name ] : Select , [ Option . name ] : Option } ,
props : { apiBase : { type : String , required : true } , exportBase : { type : String , required : true } , 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 : '' , source _channel : '' , bill _item : '' , business _type : '' , start : '' , end : '' , keyword : '' } , sort: { key : 'occurred_at' , dir : 'desc' } , exportMode : 'filtered-visible' } ; } ,
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 : '' , source _channel : '' , bill _item : '' , business _type : '' , start : '' , end : '' , keyword : '' } , filterOptions: { shops : [ ] , bill _items : [ ] } , filterOptionsLoading : false , filterOptionsError : false , sort: { key : 'occurred_at' , dir : 'desc' } , exportMode : 'filtered-visible' } ; } ,
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 ; } , [ ] ) ; } ,
stats ( ) { return [ { label : '收入合计' , value : this . money ( this . summary . income ) , note : ` ${ this . summary . count } 笔交易 ` , className : 'is-income' } , { label : '支出合计' , value : this . money ( this . summary . expense ) , note : '按当前筛选' , className : 'is-expense' } , { label : '收支差额' , value : this . money ( this . summary . balance ) , note : '收入合计 - 支出合计' , 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 ) ; } ,
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 } ; } ,
} ,
mounted ( ) { this . fetch ( ) ; } ,
mounted ( ) { this . fetch FilterOptions( ) ; 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 : [ ] } ; } 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 ( ) { this . currentPage = 1 ; this . fetch ( ) ; } , resetFilters ( ) { this . filters = { store _name : '' , 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 : '' , source _channel : '' , bill _item : '' , business _type : '' , start : '' , end : '' , 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 ( ) ; } ,
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 ; } ,
@ -115,4 +154,38 @@ export default {
@ media ( max - width : 900 px ) { . table - workspace { padding : .75 rem } . tw - stats { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) } . tw - filter > label , . tw - filter . tw - keyword { width : 100 % } . tw - filter input , . tw - filter select { width : 100 % } . tw - date input { min - width : 0 } . 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 : 58 vh ; width : 100 % } } @ media ( max - width : 520 px ) { . tw - stats { grid - template - columns : 1 fr } . tw - date span { 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 24 rem ; min - width : 0 ; }
. tw - filter { grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; }
. tw - filter . tw - date { grid - column : span 2 ; }
. tw - filter : : v - deep . el - select { width : 100 % ; min - width : 0 ; }
. tw - filter : : v - deep . el - select . el - input _ _inner { height : 34 px ; min - width : 0 ; padding - right : 30 px ; border - color : # d8d2c7 ; border - radius : 8 px ; 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 : 34 px ; }
. tw - filter - field { display : flex ; flex - direction : column ; gap : .25 rem ; min - width : 0 ; color : # 595959 ; font - size : .75 rem ; font - weight : 500 ; }
. tw - date - row { display : flex ; align - items : center ; gap : .5 rem ; }
. tw - date - control . has - picker { position : relative ; display : flex ; flex - direction : row ; width : 14 rem ; max - width : 100 % ; height : 2.125 rem ; overflow : hidden ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff ; cursor : pointer ; }
. tw - date - control . has - picker : focus - within { border - color : # 856632 ; box - shadow : 0 0 0 3 px rgba ( 133 , 102 , 50 , .2 ) ; }
. tw - date - control . has - picker : : v - deep input { width : 100 % ; min - width : 0 ; height : 100 % ; padding : 0 4 rem 0 .625 rem ; border : 0 ; border - radius : 0 ; background : transparent ; box - shadow : none ; cursor : pointer ; }
. tw - date - control . tw - date - picker ,
. tw - date - control . tw - date - clear { position : absolute ; top : 0 ; height : 2 rem ; border - top : 0 ; border - bottom : 0 ; }
. tw - date - control . tw - date - picker { right : 0 ; border - right : 0 ; border - radius : 0 ; }
. tw - date - control . tw - date - clear { right : 2.125 rem ; z - index : 1 ; display : flex ; align - items : center ; justify - content : center ; width : 1.5 rem ; padding : 0 ; border - left : 0 ; border - right : 0 ; background : # fff ; color : # 595959 ; cursor : pointer ; }
. tw - date - control . tw - date - clear . material - icons - outlined { font - size : 1 rem ; }
. tw - filter - error { grid - column : 1 / - 1 ; margin : 0 ; color : # b80000 ; font - size : .75 rem ; }
@ media ( max - width : 1100 px ) { . tw - filter { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . tw - filter . tw - date { grid - column : 1 / - 1 ; } }
@ media ( max - width : 600 px ) { . tw - filter { grid - template - columns : minmax ( 0 , 1 fr ) ; } . tw - filter . tw - date { grid - column : auto ; } . tw - date - row { align - items : stretch ; flex - direction : column ; } . tw - date - control . has - picker { width : 100 % ; } }
< / style >
< style >
. alipay - bill - calendar { font - family : "Microsoft YaHei" , sans - serif ; border : 1 px solid # d8d2c7 ; border - radius : 8 px ; background : # fff ; }
. alipay - bill - calendar . flatpickr - day . selected ,
. alipay - bill - calendar . flatpickr - day . selected : hover { background : # 856632 ; border - color : # 856632 ; }
. alipay - bill - calendar . flatpickr - day . today { border - color : # 856632 ; }
. alipay - bill - calendar . flatpickr - day : hover ,
. alipay - bill - calendar . flatpickr - day : focus { background : # f7f0de ; border - color : # d8d2c7 ; }
. alipay - bill - filter - options { max - width : calc ( 100 vw - 32 px ) ; }
. alipay - bill - filter - options . el - select - dropdown _ _item { max - width : 100 % ; overflow : hidden ; text - overflow : ellipsis ; }
. alipay - bill - filter - options . el - select - dropdown _ _item . selected { color : # 856632 ; }
. alipay - bill - filter - options . el - select - dropdown _ _item . hover ,
. alipay - bill - filter - options . el - select - dropdown _ _item : hover { background : # f7f0de ; }
< / style >