feat(bx): server-side column filters on reimburse apply list.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-09-06 07:53:28 +08:00
1 parent 1604ffceb1
commit 55cbd1d3d2
2 files changed
+68 -123

No files matched your search

+17 -1
View File
@@ -162,7 +162,23 @@ export const reimburseApi = {
exportApprovedByMe(data: Record<string, any>) {
return bxRequest.post('/reimburses/approved-by-me/export', data, { responseType: 'blob' })
},
my(params: { page: number; pageSize: number; status?: string; keyword?: string } & BxApplyDateFilter) {
my(
params: {
page: number
pageSize: number
status?: string
statusIn?: string
keyword?: string
applyCode?: string
title?: string
expenseName?: string
currentNode?: string
amountOp?: string
amount?: string
amountMin?: string
amountMax?: string
} & BxApplyDateFilter
) {
return bxRequest.get('/reimburses/my', { params })
},
myStats() {
+51 -122
View File
@@ -3,49 +3,6 @@
<template>
<div class="page-container">
<section class="panel">
<div class="filter-block">
<el-form :model="filterForm" class="filter-form" @submit.prevent>
<div class="filter-grid">
<el-form-item label="报销单号">
<el-input v-model="filterForm.keyword" placeholder="请输入报销单号" clearable />
</el-form-item>
<el-form-item label="费用项目">
<el-input v-model="filterForm.expenseName" placeholder="请输入费用项目" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="filterForm.status" placeholder="全部" clearable>
<el-option label="待提交" value="draft" />
<el-option label="审批中" value="pending" />
<el-option label="审批通过" value="approved" />
<el-option label="审批驳回" value="rejected" />
</el-select>
</el-form-item>
<el-form-item label="申请时间">
<el-date-picker
v-model="filterForm.applyDateRange"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
clearable
style="width: 100%"
/>
</el-form-item>
</div>
<div class="filter-actions">
<div class="filter-actions-end">
<el-button class="btn-ghost" :loading="loading" @click="handleReset">
<el-icon><RefreshLeft /></el-icon>重置
</el-button>
<el-button class="btn-main" type="primary" :loading="loading" @click="handleSearch">
<el-icon><Search /></el-icon>搜索
</el-button>
</div>
</div>
</el-form>
</div>
<div class="list-toolbar">
<div class="list-toolbar-row">
<ListQuickSearchInput
@@ -111,8 +68,8 @@
v-model:visible-columns="visibleColumns"
v-model:check-all="checkAll"
:is-indeterminate="isIndeterminate"
@refresh="handleSearch"
@reset-columns="resetToDefault"
@refresh="runListSearch"
@reset-columns="handleResetColumns"
/>
</div>
@@ -125,7 +82,7 @@
table-key="bx-reimburse-apply"
class="list-table"
:height="tableHeight"
:data="filteredRows"
:data="rows"
v-loading="loading"
stripe
@selection-change="onSelectionChange"
@@ -178,7 +135,6 @@
type="text"
:is-active="isFilterActive('applyCode')"
:model-value="filters.applyCode || null"
:options="collectUniqueOptions('applyCode')"
@update:model-value="(v) => setFilterState('applyCode', v)"
/>
</div>
@@ -205,7 +161,6 @@
type="text"
:is-active="isFilterActive('title')"
:model-value="filters.title || null"
:options="collectUniqueOptions('title')"
@update:model-value="(v) => setFilterState('title', v)"
/>
</div>
@@ -229,7 +184,6 @@
type="text"
:is-active="isFilterActive('expenseName')"
:model-value="filters.expenseName || null"
:options="collectUniqueOptions('expenseName')"
@update:model-value="(v) => setFilterState('expenseName', v)"
/>
</div>
@@ -287,7 +241,7 @@
type="enum"
:is-active="isFilterActive('status')"
:model-value="filters.status || null"
:options="collectUniqueOptions('status')"
:options="enumOptionsFor('status')"
@update:model-value="(v) => setFilterState('status', v)"
/>
</div>
@@ -315,7 +269,6 @@
type="text"
:is-active="isFilterActive('currentNode')"
:model-value="filters.currentNode || null"
:options="collectUniqueOptions('currentNode')"
@update:model-value="(v) => setFilterState('currentNode', v)"
/>
</div>
@@ -338,7 +291,7 @@
ref="reimburseApplyStatsRowRef"
storage-key="bx-reimburse-apply-column-stats"
:columns="reimburseApplyStatsRowColumns"
:data="filteredRows"
:data="rows"
:table-wrap-el="reimburseApplyTableWrapRef"
:get-cell-value="getReimburseApplyStatCellValue"
/>
@@ -817,13 +770,10 @@ import ListToolbarEndActions from '@/components/business/list-toolbar-end-action
import ListExportDropdown from '@/components/business/list-export-dropdown/index.vue'
import ListColumnStatsRow from '@/components/business/list-column-stats-row/index.vue'
import ColumnHeaderFilter from '@/components/business/column-header-filter/index.vue'
import { useDbInstantSearch } from '@/composables/useDbInstantSearch'
import { usePersistedListColumns } from '@/composables/usePersistedListColumns'
import { useListTableViewportHeight } from '@/composables/useListTableViewportHeight'
import {
useClientColumnFilters,
type ColumnFilterDef
} from '@/composables/useClientColumnFilters'
import { type ColumnFilterDef } from '@/composables/useClientColumnFilters'
import { useServerColumnFilters } from '@/composables/useServerColumnFilters'
import ListQuickSearchInput from '@/components/business/list-quick-search-input/index.vue'
import { reactive, ref, computed, onMounted, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
@@ -834,8 +784,6 @@ import {
View,
Close,
Delete,
Search,
RefreshLeft,
FolderOpened,
FolderChecked,
Promotion,
@@ -849,7 +797,6 @@ import {
} from '@element-plus/icons-vue'
import type { UploadFile } from 'element-plus'
import { reimburseApi, invoiceWalletApi, attachmentApi } from '@/api/bx'
import { buildApplyDateRangeQuery, type ApplyDateRange } from '@/utils/bx/apply-date-filter'
import { canDeleteByCreator, allRowsDeletableByCreator, CREATOR_DELETE_TIP } from '@/utils/creator-delete'
import { useExpenseItemTree, type ExpenseItem } from '@/hooks/bx/useExpenseItemTree'
import { useApplicantOptions } from '@/hooks/bx/useApplicantOptions'
@@ -908,19 +855,13 @@ const { rows, total, currentPage, pageSize, rangeText, loading, refresh, search,
}
)
const quickSearchKeyword = ref('')
const columnFilterDefs: ColumnFilterDef[] = [
{ prop: 'applyTime', label: '申请时间', type: 'datetime' },
{ prop: 'applyCode', label: '报销单号', type: 'text' },
{ prop: 'title', label: '报销标题', type: 'text' },
{
prop: 'expenseName',
label: '费用项目',
type: 'text',
getValue: (row) => {
const lines = getReimburseExpenseItemLines(row.details, row)
return lines?.length ? lines.join('\n') : row.expenseName || ''
}
},
{ prop: 'expenseName', label: '费用项目', type: 'text' },
{ prop: 'amount', label: '报销金额(元)', type: 'number' },
{
prop: 'status',
@@ -933,23 +874,27 @@ const columnFilterDefs: ColumnFilterDef[] = [
{ value: 'rejected', label: '审批驳回' }
]
},
{
prop: 'currentNode',
label: '当前节点',
type: 'text',
getValue: (row) => workflowNodeLabel(row.currentNode, row.status) || ''
}
{ prop: 'currentNode', label: '当前节点', type: 'text' }
]
const {
filters,
filteredRows,
hasActiveFilters,
setFilterState,
clearAllFilters,
isFilterActive,
collectUniqueOptions
} = useClientColumnFilters(rows, columnFilterDefs)
toQueryParams,
resetToInitial,
watchVisibleColumns
} = useServerColumnFilters(columnFilterDefs, {
initialFilters: {},
onChange: () => runListSearch()
})
function enumOptionsFor(prop: string) {
const def = columnFilterDefs.find((d) => d.prop === prop)
return (def?.enumOptions || []).map((o) => ({ ...o, count: 0 }))
}
const { expenseTree } = useExpenseItemTree()
const { applicantOptions, loadApplicantOptions } = useApplicantOptions()
@@ -969,8 +914,6 @@ function markDisabledExpenseNodes(tree: ExpenseItem[]): ExpenseItem[] {
const expenseTreeForSelect = computed(() => markDisabledExpenseNodes(expenseTree.value))
const quickSearchKeyword = ref('')
const listColumnDefs = [
{ prop: 'applyTime', label: '申请时间' },
{ prop: 'title', label: '报销标题' },
@@ -988,6 +931,20 @@ const {
resetToDefault
} = usePersistedListColumns('bx-reimburse-apply-list-columns', listColumnDefs)
const visibleFilterProps = computed(() => {
const props: string[] = ['applyCode']
for (const def of listColumnDefs) {
if (isColumnVisible(def.prop)) props.push(def.prop)
}
return props
})
watchVisibleColumns(visibleFilterProps)
function handleResetColumns() {
resetToDefault()
resetToInitial()
}
const reimburseApplyTableWrapRef = ref<HTMLElement | null>(null)
const reimburseApplyBelowRef = ref<HTMLElement | null>(null)
const reimburseApplyTableRef = ref<any>(null)
@@ -999,6 +956,18 @@ const { tableHeight, recalc: recalcTableHeight } = useListTableViewportHeight({
tableRef: reimburseApplyTableRef
})
function buildSearchQuery() {
const q: Record<string, string> = { ...toQueryParams() }
const kw = quickSearchKeyword.value.trim()
if (kw) q.keyword = kw
return q
}
function runListSearch() {
search(buildSearchQuery())
nextTick(() => recalcTableHeight())
}
const reimburseApplyStatsRowColumns = computed((): StatsRowColumn[] => {
const cols: StatsRowColumn[] = [
{ key: '__selection', interactive: false },
@@ -1042,13 +1011,6 @@ function onReimburseApplyHeaderDragEnd() {
reimburseApplyStatsRowRef.value?.resync?.()
}
const filterForm = reactive({
keyword: '',
status: '',
expenseName: '',
applyDateRange: null as ApplyDateRange
})
function todayYmd() {
const d = new Date()
const y = d.getFullYear()
@@ -1560,46 +1522,13 @@ onMounted(() => {
refreshList()
})
function buildSearchQuery() {
const q: Record<string, string> = {}
const kw = (quickSearchKeyword.value.trim() || filterForm.keyword.trim())
if (kw) q.keyword = kw
if (filterForm.status) q.status = filterForm.status
if (filterForm.expenseName) q.expenseName = filterForm.expenseName
buildApplyDateRangeQuery(filterForm.applyDateRange, q)
return q
}
function handleSearch() {
quickSearchKeyword.value = ''
search(buildSearchQuery())
nextTick(() => recalcTableHeight())
}
useDbInstantSearch(
() => ({
keyword: filterForm.keyword,
status: filterForm.status,
expenseName: filterForm.expenseName,
applyDateRange: filterForm.applyDateRange
}),
handleSearch
)
function handleReset() {
quickSearchKeyword.value = ''
Object.assign(filterForm, { keyword: '', status: '', expenseName: '', applyDateRange: null })
search()
nextTick(() => recalcTableHeight())
}
function handleQuickSearch() {
search(buildSearchQuery())
runListSearch()
}
function handleQuickSearchClear() {
quickSearchKeyword.value = ''
search(buildSearchQuery())
runListSearch()
}
async function handleToolbarSubmit() {