提交
This commit is contained in:
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user