feat(web-admin): add useServerColumnFilters for list column queries.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
36afab503a
commit
e492cedab9
3 files changed
+220
-8
No files matched your search
@@ -19,9 +19,12 @@ export interface ColumnFilterDef {
|
||||
getValue?: (row: any) => unknown
|
||||
}
|
||||
|
||||
/** 文本列:服务端模糊 / 客户端也可只用 text */
|
||||
export interface TextColumnFilterState {
|
||||
selected: string[]
|
||||
keyword: string
|
||||
text: string
|
||||
/** @deprecated 旧客户端勾选;服务端 text 不用 */
|
||||
selected?: string[]
|
||||
keyword?: string
|
||||
}
|
||||
|
||||
export interface NumberColumnFilterState {
|
||||
@@ -63,7 +66,8 @@ function defaultState(type: ColumnFilterType): ColumnFilterState {
|
||||
if (type === 'datetime') {
|
||||
return { from: '', to: '' }
|
||||
}
|
||||
return { selected: [], keyword: '' }
|
||||
// text / enum 共用形状;enum 主要用 selected,text 主要用 text
|
||||
return { text: '', selected: [] }
|
||||
}
|
||||
|
||||
export function isColumnFilterActive(
|
||||
@@ -71,7 +75,12 @@ export function isColumnFilterActive(
|
||||
state?: ColumnFilterState | null
|
||||
): boolean {
|
||||
if (!state) return false
|
||||
if (type === 'text' || type === 'enum') {
|
||||
if (type === 'text') {
|
||||
const s = state as TextColumnFilterState
|
||||
if (String(s.text || '').trim()) return true
|
||||
return Array.isArray(s.selected) && s.selected.length > 0
|
||||
}
|
||||
if (type === 'enum') {
|
||||
const s = state as TextColumnFilterState
|
||||
return Array.isArray(s.selected) && s.selected.length > 0
|
||||
}
|
||||
@@ -103,10 +112,20 @@ function resolveCompareKey(row: any, def: ColumnFilterDef): string {
|
||||
function matchOne(row: any, def: ColumnFilterDef, state: ColumnFilterState): boolean {
|
||||
if (!isColumnFilterActive(def.type, state)) return true
|
||||
|
||||
if (def.type === 'text' || def.type === 'enum') {
|
||||
if (def.type === 'text') {
|
||||
const s = state as TextColumnFilterState
|
||||
const text = String(s.text || '').trim()
|
||||
if (text) {
|
||||
return String(resolveRawValue(row, def) ?? '').includes(text)
|
||||
}
|
||||
const key = resolveCompareKey(row, def)
|
||||
return Array.isArray(s.selected) && s.selected.includes(key)
|
||||
}
|
||||
|
||||
if (def.type === 'enum') {
|
||||
const s = state as TextColumnFilterState
|
||||
const key = resolveCompareKey(row, def)
|
||||
return s.selected.includes(key)
|
||||
return Array.isArray(s.selected) && s.selected.includes(key)
|
||||
}
|
||||
|
||||
if (def.type === 'number') {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import { computed, reactive, watch, type Ref } from 'vue'
|
||||
import {
|
||||
isColumnFilterActive,
|
||||
type ColumnFilterDef,
|
||||
type ColumnFilterState,
|
||||
type DatetimeColumnFilterState,
|
||||
type NumberColumnFilterState,
|
||||
type TextColumnFilterState
|
||||
} from '@/composables/useClientColumnFilters'
|
||||
|
||||
/**
|
||||
* 服务端列条件:管理 state、toQueryParams、藏列清条件。
|
||||
* toQueryParams 本页(报销申请)YAGNI 写死映射 applyDateFrom / amountOp / statusIn 等;
|
||||
* 日后推广可在 ColumnFilterDef 增加可选 queryKey。
|
||||
*/
|
||||
export function useServerColumnFilters(
|
||||
columnDefs: ColumnFilterDef[],
|
||||
options?: {
|
||||
initialFilters?: Record<string, ColumnFilterState | null>
|
||||
onChange?: () => void
|
||||
}
|
||||
) {
|
||||
const defMap = new Map(columnDefs.map((d) => [d.prop, d]))
|
||||
const filters = reactive<Record<string, ColumnFilterState>>({})
|
||||
|
||||
function applyInitial() {
|
||||
const init = options?.initialFilters || {}
|
||||
Object.keys(filters).forEach((k) => delete filters[k])
|
||||
for (const [prop, state] of Object.entries(init)) {
|
||||
const def = defMap.get(prop)
|
||||
if (!def || !state || !isColumnFilterActive(def.type, state)) continue
|
||||
filters[prop] = JSON.parse(JSON.stringify(state))
|
||||
}
|
||||
}
|
||||
applyInitial()
|
||||
|
||||
const activeFilterProps = computed(() =>
|
||||
Object.keys(filters).filter((prop) => {
|
||||
const def = defMap.get(prop)
|
||||
return def && isColumnFilterActive(def.type, filters[prop])
|
||||
})
|
||||
)
|
||||
const hasActiveFilters = computed(() => activeFilterProps.value.length > 0)
|
||||
|
||||
function setFilterState(prop: string, state: ColumnFilterState | null) {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return
|
||||
if (!state || !isColumnFilterActive(def.type, state)) {
|
||||
delete filters[prop]
|
||||
} else {
|
||||
filters[prop] = JSON.parse(JSON.stringify(state))
|
||||
}
|
||||
options?.onChange?.()
|
||||
}
|
||||
|
||||
function clearFilter(prop: string) {
|
||||
if (!(prop in filters)) return
|
||||
delete filters[prop]
|
||||
options?.onChange?.()
|
||||
}
|
||||
|
||||
function clearAllFilters() {
|
||||
const had = Object.keys(filters).length > 0
|
||||
Object.keys(filters).forEach((k) => delete filters[k])
|
||||
if (had) options?.onChange?.()
|
||||
}
|
||||
|
||||
function resetToInitial() {
|
||||
applyInitial()
|
||||
options?.onChange?.()
|
||||
}
|
||||
|
||||
function isFilterActive(prop: string) {
|
||||
const def = defMap.get(prop)
|
||||
if (!def) return false
|
||||
return isColumnFilterActive(def.type, filters[prop])
|
||||
}
|
||||
|
||||
/** 隐藏列:清该列条件;若确实清过则触发 onChange */
|
||||
function onColumnHidden(prop: string) {
|
||||
if (!(prop in filters)) return
|
||||
delete filters[prop]
|
||||
options?.onChange?.()
|
||||
}
|
||||
|
||||
function toQueryParams(): Record<string, string> {
|
||||
const q: Record<string, string> = {}
|
||||
for (const prop of Object.keys(filters)) {
|
||||
const def = defMap.get(prop)
|
||||
const state = filters[prop]
|
||||
if (!def || !isColumnFilterActive(def.type, state)) continue
|
||||
|
||||
if (def.type === 'datetime') {
|
||||
// 本页仅 applyTime → applyDateFrom / applyDateTo
|
||||
const s = state as DatetimeColumnFilterState
|
||||
if (s.from) q.applyDateFrom = s.from
|
||||
if (s.to) q.applyDateTo = s.to
|
||||
continue
|
||||
}
|
||||
if (def.type === 'number') {
|
||||
// 本页仅 amount → amountOp / amountMin|amountMax|amount
|
||||
const s = state as NumberColumnFilterState
|
||||
q.amountOp = s.op || 'between'
|
||||
if (s.op === 'between') {
|
||||
if (s.min !== '' && s.min != null) q.amountMin = String(s.min)
|
||||
if (s.max !== '' && s.max != null) q.amountMax = String(s.max)
|
||||
} else if (s.value !== '' && s.value != null) {
|
||||
q.amount = String(s.value)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (def.type === 'enum') {
|
||||
// 本页仅 status → statusIn
|
||||
const s = state as TextColumnFilterState
|
||||
if (s.selected?.length) q.statusIn = s.selected.join(',')
|
||||
continue
|
||||
}
|
||||
// text:prop 直接作为 query key(applyCode/title/expenseName/currentNode)
|
||||
const s = state as TextColumnFilterState
|
||||
const text = String(s.text || '').trim()
|
||||
if (text) q[prop] = text
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听可见列:从有→无时 onColumnHidden。
|
||||
* visibleProps: Ref<string[]> 当前可见的业务列 prop 列表。
|
||||
*/
|
||||
function watchVisibleColumns(visibleProps: Ref<string[]>) {
|
||||
watch(
|
||||
visibleProps,
|
||||
(next, prev) => {
|
||||
if (!prev) return
|
||||
const nextSet = new Set(next || [])
|
||||
for (const prop of prev) {
|
||||
if (!nextSet.has(prop)) onColumnHidden(prop)
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
filters,
|
||||
hasActiveFilters,
|
||||
activeFilterProps,
|
||||
setFilterState,
|
||||
clearFilter,
|
||||
clearAllFilters,
|
||||
resetToInitial,
|
||||
isFilterActive,
|
||||
onColumnHidden,
|
||||
toQueryParams,
|
||||
watchVisibleColumns
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user