|
|
@@ -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
|
|
|
+ }
|
|
|
+}
|