@ -4,11 +4,24 @@
< 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 > 店铺 < input v -model .trim = " filters.store_name " placeholder = "店铺名称" / > < / label >
< label > 账单项目 < input v -model .trim = " filters.bill_item " placeholder = "账单项目" / > < / 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 >
< label > 店铺
< select v-if = "isDouyin && !filterOptionsError" v-model="filters.store_name" aria-label="店铺筛选" :aria-busy="filterOptionsLoading ? 'true' : 'false'" @change="applyFilters" >
< option value = "" > { { filterOptionsLoading ? '店铺加载中…' : '全部店铺' } } < / option >
< option v-for ="shop in filterOptions.shops" :key="shop" :value ="shop" > {{ shop }} < / option >
< / select >
< input v -else v -model .trim = " filters.store_name " placeholder = "店铺名称" / >
< / label >
< label > 账单项目
< select v-if = "isDouyin && !filterOptionsError" v-model="filters.bill_item" aria-label="账单项目筛选" :aria-busy="filterOptionsLoading ? 'true' : 'false'" @change="applyFilters" >
< option value = "" > { { filterOptionsLoading ? '账单项目加载中…' : '全部账单项目' } } < / option >
< option v-for ="item in filterOptions.bill_items" :key="item" :value ="item" > {{ item }} < / option >
< / select >
< input v -else v -model .trim = " filters.bill_item " placeholder = "账单项目" / >
< / label >
< label class = "tw-date" > 发生时间 < span > < input v-model = "filters.start" type="date" aria-label="开始日期" :max="filters.end || undefined" :aria-invalid="invalidDateRange ? 'true' : 'false'" @change="applyFilters" /><b>至</b><input v-model="filters.end" type="date" aria-label="结束日期" :min="filters.start || undefined" :aria-invalid="invalidDateRange ? 'true' : 'false'" @change="applyFilters" / > < / span > < / label >
< 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" > { { loading ? '查询中…' : '查询' } } < / button > < button class = "tw-button" type = "button" @click ="resetFilters" > 重置 < / button > < / div >
< p v-if = "invalidDateRange" class="tw-filter-error" role="alert" > 结束日期不能早于开始日期 < / p >
< / form >
< section class = "tw-panel" : aria -label = " ` $ { columnsTitle | | ' 账户 ' } 账单明细 ` " >
< div v-if = "loadError" class="tw-alert" role="alert"><span>账单加载失败,请重试</span><button type="button" @click="fetch" > 重新加载 < / button > < / div >
@ -43,18 +56,21 @@ import TableColumnSettings from './TableColumnSettings.vue';
const SORTABLE = [ 'occurred_at' , 'income' , 'expense' , 'balance' , 'record_code' ] ;
export default {
name : 'AccountBills' , components : { TableColumnSettings } , props : { apiBase : { type : String , required : true } , billType : { type : String , required : true } , userKey : { type : [ String , Number ] , required : true } } ,
data ( ) { return { loading : false , loadError : false , rows : [ ] , sourceColumns : [ ] , configuredColumns : [ ] , columnsTitle : '' , sourceGroups : [ ] , summary : { income : 0 , expense : 0 , balance : 0 , count : 0 } , total : 0 , currentPage : 1 , lastPage : 1 , perPage : 20 , filters : { store _name : '' , bill _item : '' , start : '' , end : '' , keyword : '' } , sort: { key : 'occurred_at' , dir : 'desc' } , exportMode : 'filtered-visible' } ; } ,
data ( ) { return { loading : false , loadError : false , rows : [ ] , sourceColumns : [ ] , configuredColumns : [ ] , columnsTitle : '' , sourceGroups : [ ] , summary : { income : 0 , expense : 0 , balance : 0 , count : 0 } , total : 0 , currentPage : 1 , lastPage : 1 , perPage : 20 , filters : { store _name : '' , bill _item : '' , start : '' , end : '' , keyword : '' } , filterOptions: { shops : [ ] , bill _items : [ ] } , filterOptionsLoading : false , filterOptionsError : false , requestSerial : 0 , sort: { key : 'occurred_at' , dir : 'desc' } , exportMode : 'filtered-visible' } ; } ,
computed : {
isDouyin ( ) { return this . billType === 'douyin' ; } ,
invalidDateRange ( ) { return Boolean ( this . filters . start && this . filters . end && this . filters . start > this . filters . end ) ; } ,
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 ( ) { 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 ) ; }
} ,
mounted ( ) { this. fetch ( ) ; } ,
mounted ( ) { if( this . isDouyin ) this . fetchFilterOptions ( ) ; this. fetch ( ) ; } ,
methods : {
async fetch ( ) { this . loading = true ; this . loadError = false ; 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 } ; 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 ) { this . rows = [ ] ; this . total = 0 ; this . summary = { income : 0 , expense : 0 , balance : 0 , count : 0 } ; this . loadError = true ; } finally { this . loading = false ; } } ,
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 ( ) { const requestId = ++ this . requestSerial ; this . loading = true ; this . loadError = false ; 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 } } ) ; if ( requestId !== this . requestSerial ) return ; 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 } ; 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 . 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 ( ) { this. currentPage = 1 ; this . fetch ( ) ; } , resetFilters ( ) { this . filters = { store _name : '' , bill _item : '' , start : '' , end : '' , keyword : '' } ; this . applyFilters ( ) ; } , changePage ( page ) { this . currentPage = page ; this . fetch ( ) ; this . $nextTick ( ( ) => { if ( this . $refs . scroll ) this . $refs . scroll . scrollTop = 0 ; } ) ; } ,
applyFilters ( ) { if( this . invalidDateRange ) return ; this. currentPage = 1 ; this . fetch ( ) ; } , resetFilters ( ) { this . filters = { store _name : '' , bill _item : '' , start : '' , end : '' , keyword : '' } ; 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' ? '↑' : '↓' ) : '↕' ; } ,
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 } )} ` ; } ,
@ -65,7 +81,7 @@ export default {
< / script >
< style scoped >
. table - workspace { padding : 1 rem ; color : # 171717 ; font : 400 .8125 rem / 1.5 "JetBrains Mono" , "Microsoft YaHei" , "PingFang SC" , monospace } . tw - stats { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; gap : .75 rem ; margin - bottom : .75 rem } . tw - stat { padding : .875 rem 1 rem ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff } . tw - label , . tw - note , . tw - count { color : # 595959 ; font - size : .75 rem } . tw - value { margin - top : .375 rem ; font - size : 1.375 rem ; font - weight : 700 ; font - variant - numeric : tabular - nums } . tw - note { margin - top : .1875 rem } . is - income { color : # 53793 e } . is - expense { color : # b80000 } . tw - filter { display : flex ; flex - wrap : wrap ; gap : .75 rem ; margin - bottom : .75 rem ; padding : .875 rem 1 rem ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff } . tw - filter > label { display : flex ; flex - direction : column ; gap : .25 rem ; color : # 595959 ; font - size : .75 rem ; font - weight : 500 } . tw - filter input , . tw - tools select { height : 2.125 rem ; min - width : 10 rem ; padding : 0 .625 rem ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff ; color : # 171717 ; font : 400 .8125 rem / 1 inherit } . tw - filter inpu t: focus , . tw - tools select : focus { border - color : # 856632 ; box - shadow : 0 0 0 3 px rgba ( 133 , 102 , 50 , .2 ) ; outline : none } . tw - filter . tw - keyword { flex : 1 ; min - width : 14 rem } . tw - keyword input { width : 100 % } . tw - date span , . tw - actions , . tw - tools , . tw - page - actions { display : flex ; align - items : center ; gap : .5 rem } . tw - date b { font - weight : 400 } . tw - date input { min - width : 8.5 rem } . 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 : .375 rem ; height : 2.125 rem ; padding : 0 .875 rem ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff ; color : # 171717 ; font : 500 .8125 rem / 1 inherit ; cursor : pointer } . tw - button : hover { border - color : # 856632 ; color : # 856632 } . tw - primary { border - color : # 856632 ; background : # 856632 ; color : # fff } . tw - primary : hover { background : # 6 a5129 ; color : # fff } . tw - button . material - icons - outlined { font - size : 1.125 rem } . tw - panel { overflow : visible ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff } . tw - alert { display : flex ; align - items : center ; justify - content : space - between ; gap : 1 rem ; margin : .75 rem ; padding : .75 rem ; border : 1 px solid # b80000 ; border - radius : .5 rem ; 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 : .75 rem ; min - height : 3.5 rem ; padding : .625 rem .875 rem ; border - bottom : 1 px solid # d8d2c7 } . tw - toolbar strong { margin - right : .625 rem ; font - size : .875 rem } . tw - tools { flex - wrap : wrap ; justify - content : flex - end } . tw - tools > select { min - width : 13.75 rem } . tw - scroll { position : relative ; max - height : min ( 62 vh , 42 rem ) ; overflow : auto ; overscroll - behavior : contain ; scrollbar - gutter : stable } . tw - table { width : max - content ; min - width : 100 % ; border - collapse : separate ; border - spacing : 0 ; font - size : .78125 rem } . tw - table th , . tw - table td { min - width : 7.5 rem ; padding : .5 rem .625 rem ; border - right : 1 px solid # eeeae2 ; border - bottom : 1 px 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 : 2 rem ; text - align : center ; color : # 856632 ; font - size : .75 rem } . tw - columns th { top : 2 rem ; height : 2.25 rem ; text - align : left ; background : # fbfaf7 ; box - shadow : inset 0 - 1 px # 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 : .0625 rem .375 rem ; border - radius : .25 rem ; background : # f7f0de ; color : # 856632 } . tw - empty { padding : 3 rem 1 rem ! important ; text - align : center ! important ; color : # 595959 } . tw - pager { display : flex ; align - items : center ; justify - content : space - between ; gap : .75 rem ; min - height : 3.5 rem ; padding : .625 rem .875 rem ; border - top : 1 px solid # d8d2c7 ; color : # 595959 } . tw - page - actions select { height : 1.875 rem ; border : 1 px solid # d8d2c7 ; border - radius : .375 rem } . tw - page - actions button { width : 1.875 rem ; height : 1.875 rem ; padding : 0 } . tw - page - actions button : disabled { cursor : not - allowed ; opacity : .45 } @ 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 inpu t{ 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 } }
. table - workspace { padding : 1 rem ; color : # 171717 ; font : 400 .8125 rem / 1.5 "JetBrains Mono" , "Microsoft YaHei" , "PingFang SC" , monospace } . tw - stats { display : grid ; grid - template - columns : repeat ( 4 , minmax ( 0 , 1 fr ) ) ; gap : .75 rem ; margin - bottom : .75 rem } . tw - stat { padding : .875 rem 1 rem ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff } . tw - label , . tw - note , . tw - count { color : # 595959 ; font - size : .75 rem } . tw - value { margin - top : .375 rem ; font - size : 1.375 rem ; font - weight : 700 ; font - variant - numeric : tabular - nums } . tw - note { margin - top : .1875 rem } . is - income { color : # 53793 e } . is - expense { color : # b80000 } . tw - filter { display : flex ; flex - wrap : wrap ; gap : .75 rem ; margin - bottom : .75 rem ; padding : .875 rem 1 rem ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff } . tw - filter > label { display : flex ; flex - direction : column ; gap : .25 rem ; color : # 595959 ; font - size : .75 rem ; font - weight : 500 } . tw - filter input , . tw - filter select , . tw - tools select { height : 2.125 rem ; min - width : 10 rem ; padding : 0 .625 rem ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff ; color : # 171717 ; font : 400 .8125 rem / 1 inherit } . tw - filter inpu t: focus , . tw - filter selec t: focus , . tw - tools select : focus { border - color : # 856632 ; box - shadow : 0 0 0 3 px rgba ( 133 , 102 , 50 , .2 ) ; outline : none } . tw - filter . tw - keyword { flex : 1 ; min - width : 14 rem } . tw - keyword input { width : 100 % } . tw - date span , . tw - actions , . tw - tools , . tw - page - actions { display : flex ; align - items : center ; gap : .5 rem } . tw - date b { font - weight : 400 } . tw - date input { min - width : 8.5 rem } . tw - filter- error { grid - column : 1 / - 1 ; margin : 0 ; color : # b80000 ; font - size : .75 rem } . 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 : .375 rem ; height : 2.125 rem ; padding : 0 .875 rem ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff ; color : # 171717 ; font : 500 .8125 rem / 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 : # 6 a5129 ; color : # fff } . tw - button . material - icons - outlined { font - size : 1.125 rem } . tw - panel { overflow : visible ; border : 1 px solid # d8d2c7 ; border - radius : .5 rem ; background : # fff } . tw - alert { display : flex ; align - items : center ; justify - content : space - between ; gap : 1 rem ; margin : .75 rem ; padding : .75 rem ; border : 1 px solid # b80000 ; border - radius : .5 rem ; 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 : .75 rem ; min - height : 3.5 rem ; padding : .625 rem .875 rem ; border - bottom : 1 px solid # d8d2c7 } . tw - toolbar strong { margin - right : .625 rem ; font - size : .875 rem } . tw - tools { flex - wrap : wrap ; justify - content : flex - end } . tw - tools > select { min - width : 13.75 rem } . tw - scroll { position : relative ; max - height : min ( 62 vh , 42 rem ) ; overflow : auto ; overscroll - behavior : contain ; scrollbar - gutter : stable } . tw - table { width : max - content ; min - width : 100 % ; border - collapse : separate ; border - spacing : 0 ; font - size : .78125 rem } . tw - table th , . tw - table td { min - width : 7.5 rem ; padding : .5 rem .625 rem ; border - right : 1 px solid # eeeae2 ; border - bottom : 1 px 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 : 2 rem ; text - align : center ; color : # 856632 ; font - size : .75 rem } . tw - columns th { top : 2 rem ; height : 2.25 rem ; text - align : left ; background : # fbfaf7 ; box - shadow : inset 0 - 1 px # 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 : .0625 rem .375 rem ; border - radius : .25 rem ; background : # f7f0de ; color : # 856632 } . tw - empty { padding : 3 rem 1 rem ! important ; text - align : center ! important ; color : # 595959 } . tw - pager { display : flex ; align - items : center ; justify - content : space - between ; gap : .75 rem ; min - height : 3.5 rem ; padding : .625 rem .875 rem ; border - top : 1 px solid # d8d2c7 ; color : # 595959 } . tw - page - actions select { height : 1.875 rem ; border : 1 px solid # d8d2c7 ; border - radius : .375 rem } . tw - page - actions button { width : 1.875 rem ; height : 1.875 rem ; padding : 0 } . tw - page - actions button : disabled { cursor : not - allowed ; opacity : .45 } @ 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 inpu t, . tw - filter selec t{ 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 { display : grid ; grid - template - columns : minmax ( 10 rem , 1 fr ) minmax ( 10 rem , 1 fr ) auto minmax ( 14 rem , 2 fr ) auto ; align - items : end } . tw - filter > label { min - width : 0 } . tw - actions { align - self : auto ; justify - self : end ; margin - left : 0 } @ media ( max - width : 900 px ) { . tw - filter { grid - template - columns : 1 fr } . tw - actions { justify - self : start } }