This commit is contained in:
zlwu committed 2026-08-19 09:14:37 +08:00
1 parent c3bd2c00ae
commit a52fe77e34
39 files changed
+609647 -9238

No files matched your search

@@ -0,0 +1,262 @@
import { computed, reactive, type Ref, type ComputedRef } from 'vue'
export type ColumnFilterType = 'text' | 'enum' | 'number' | 'datetime'
export type NumberFilterOp = 'between' | 'eq' | 'gt' | 'gte' | 'lt' | 'lte'
export interface ColumnFilterEnumOption {
value: string
label: string
}
export interface ColumnFilterDef {
prop: string
label: string
type: ColumnFilterType
/** 枚举固定选项;未提供时从当前页去重 */
enumOptions?: ColumnFilterEnumOption[]
/** 自定义取值(展示值 / 比较值) */
getValue?: (row: any) => unknown
}
export interface TextColumnFilterState {
selected: string[]
keyword: string
}
export interface NumberColumnFilterState {
op: NumberFilterOp
value: string
min: string
max: string
}
export interface DatetimeColumnFilterState {
from: string
to: string
}
export type ColumnFilterState =
| TextColumnFilterState
| NumberColumnFilterState
| DatetimeColumnFilterState
function parseDateMs(value: unknown): number | null {
const text = String(value ?? '').trim()
if (!text) return null
const t = Date.parse(text.replace(/-/g, '/'))
return Number.isNaN(t) ? null : t
}
function startOfDayMs(yyyyMmDd: string): number | null {
return parseDateMs(`${yyyyMmDd} 00:00:00`)
}
function endOfDayMs(yyyyMmDd: string): number | null {
return parseDateMs(`${yyyyMmDd} 23:59:59`)
}
function defaultState(type: ColumnFilterType): ColumnFilterState {
if (type === 'number') {
return { op: 'between', value: '', min: '', max: '' }
}
if (type === 'datetime') {
return { from: '', to: '' }
}
return { selected: [], keyword: '' }
}
export function isColumnFilterActive(
type: ColumnFilterType,
state?: ColumnFilterState | null
): boolean {
if (!state) return false
if (type === 'text' || type === 'enum') {
const s = state as TextColumnFilterState
return Array.isArray(s.selected) && s.selected.length > 0
}
if (type === 'number') {
const s = state as NumberColumnFilterState
if (s.op === 'between') {
return (s.min !== '' && s.min != null) || (s.max !== '' && s.max != null)
}
return s.value !== '' && s.value != null
}
if (type === 'datetime') {
const s = state as DatetimeColumnFilterState
return !!(s.from || s.to)
}
return false
}
function resolveRawValue(row: any, def: ColumnFilterDef): unknown {
if (def.getValue) return def.getValue(row)
return row?.[def.prop]
}
function resolveCompareKey(row: any, def: ColumnFilterDef): string {
const raw = resolveRawValue(row, def)
if (raw == null) return ''
return String(raw)
}
function matchOne(row: any, def: ColumnFilterDef, state: ColumnFilterState): boolean {
if (!isColumnFilterActive(def.type, state)) return true
if (def.type === 'text' || def.type === 'enum') {
const s = state as TextColumnFilterState
const key = resolveCompareKey(row, def)
return s.selected.includes(key)
}
if (def.type === 'number') {
const s = state as NumberColumnFilterState
const val = Number(resolveRawValue(row, def))
if (Number.isNaN(val)) return false
if (s.op === 'eq') return s.value !== '' && val === Number(s.value)
if (s.op === 'gt') return s.value !== '' && val > Number(s.value)
if (s.op === 'gte') return s.value !== '' && val >= Number(s.value)
if (s.op === 'lt') return s.value !== '' && val < Number(s.value)
if (s.op === 'lte') return s.value !== '' && val <= Number(s.value)
const minOk = s.min === '' || s.min == null || val >= Number(s.min)
const maxOk = s.max === '' || s.max == null || val <= Number(s.max)
return minOk && maxOk
}
if (def.type === 'datetime') {
const s = state as DatetimeColumnFilterState
const t = parseDateMs(resolveRawValue(row, def))
if (t == null) return false
const fromOk =
!s.from ||
(() => {
const from = startOfDayMs(s.from)
return from != null && t >= from
})()
const toOk =
!s.to ||
(() => {
const to = endOfDayMs(s.to)
return to != null && t <= to
})()
return fromOk && toOk
}
return true
}
export function useClientColumnFilters(
sourceRows: Ref<any[]> | ComputedRef<any[]>,
columnDefs: ColumnFilterDef[]
) {
const defMap = new Map(columnDefs.map((d) => [d.prop, d]))
const filters = reactive<Record<string, ColumnFilterState>>({})
const filteredRows = computed(() => {
const list = sourceRows.value || []
const activeProps = Object.keys(filters).filter((prop) => {
const def = defMap.get(prop)
return def && isColumnFilterActive(def.type, filters[prop])
})
if (!activeProps.length) return list
return list.filter((row) =>
activeProps.every((prop) => {
const def = defMap.get(prop)!
return matchOne(row, def, filters[prop])
})
)
})
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 getFilterState(prop: string): ColumnFilterState {
const def = defMap.get(prop)
if (!def) return defaultState('text')
if (!filters[prop]) {
filters[prop] = defaultState(def.type)
}
return filters[prop]
}
function setFilterState(prop: string, state: ColumnFilterState | null) {
const def = defMap.get(prop)
if (!def) return
if (!state || !isColumnFilterActive(def.type, state)) {
delete filters[prop]
return
}
filters[prop] = JSON.parse(JSON.stringify(state))
}
function clearFilter(prop: string) {
delete filters[prop]
}
function clearAllFilters() {
Object.keys(filters).forEach((k) => delete filters[k])
}
function isFilterActive(prop: string): boolean {
const def = defMap.get(prop)
if (!def) return false
return isColumnFilterActive(def.type, filters[prop])
}
/** 当前页去重可选值(含计数) */
function collectUniqueOptions(
prop: string
): Array<{ value: string; label: string; count: number }> {
const def = defMap.get(prop)
if (!def) return []
if (def.type === 'enum' && def.enumOptions?.length) {
const counts = new Map<string, number>()
for (const row of sourceRows.value || []) {
const key = resolveCompareKey(row, def)
counts.set(key, (counts.get(key) || 0) + 1)
}
return def.enumOptions.map((opt) => ({
value: opt.value,
label: opt.label,
count: counts.get(opt.value) || 0
}))
}
const map = new Map<string, { label: string; count: number }>()
for (const row of sourceRows.value || []) {
const key = resolveCompareKey(row, def)
const label = key || '(空)'
const prev = map.get(key)
if (prev) prev.count += 1
else map.set(key, { label, count: 1 })
}
return [...map.entries()]
.map(([value, meta]) => ({ value, label: meta.label, count: meta.count }))
.sort((a, b) => a.label.localeCompare(b.label, 'zh-CN'))
}
function getColumnDef(prop: string): ColumnFilterDef | undefined {
return defMap.get(prop)
}
return {
filters,
filteredRows,
activeFilterProps,
hasActiveFilters,
getFilterState,
setFilterState,
clearFilter,
clearAllFilters,
isFilterActive,
collectUniqueOptions,
getColumnDef
}
}