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
@@ -118,6 +118,7 @@
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@visible-change="onDatePickerVisibleChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -130,6 +131,7 @@
|
||||
size="small"
|
||||
clearable
|
||||
style="width: 100%"
|
||||
@visible-change="onDatePickerVisibleChange"
|
||||
/>
|
||||
</div>
|
||||
<p class="field-tip">闭区间:含起止当天</p>
|
||||
@@ -154,7 +156,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Filter, Delete, Close, Check } from '@element-plus/icons-vue'
|
||||
import {
|
||||
@@ -192,6 +194,9 @@
|
||||
const draftMax = ref('')
|
||||
const draftFrom = ref('')
|
||||
const draftTo = ref('')
|
||||
/** 日期面板 teleport 到 body,点选日期会被 popover 当成外部点击;短暂抑制关闭 */
|
||||
const holdPopoverOpen = ref(false)
|
||||
let holdPopoverTimer: ReturnType<typeof setTimeout> | undefined
|
||||
|
||||
const typeHint = computed(() => {
|
||||
if (props.type === 'text') return '文本:搜索并勾选本页出现的值'
|
||||
@@ -212,7 +217,7 @@
|
||||
function resetDraftFromModel() {
|
||||
const m = props.modelValue
|
||||
if (props.type === 'text' || props.type === 'enum') {
|
||||
const s = (m || { selected: [], keyword: '' }) as TextColumnFilterState
|
||||
const s = (m || { text: '', selected: [], keyword: '' }) as TextColumnFilterState
|
||||
draftSelected.value = [...(s.selected || [])]
|
||||
draftKeyword.value = s.keyword || ''
|
||||
return
|
||||
@@ -232,12 +237,35 @@
|
||||
}
|
||||
}
|
||||
|
||||
function onDatePickerVisibleChange(open: boolean) {
|
||||
if (holdPopoverTimer !== undefined) {
|
||||
clearTimeout(holdPopoverTimer)
|
||||
holdPopoverTimer = undefined
|
||||
}
|
||||
if (open) {
|
||||
holdPopoverOpen.value = true
|
||||
return
|
||||
}
|
||||
// 选中日期后,同一轮点击会触发 popover 的 click-outside,延迟释放抑制
|
||||
holdPopoverOpen.value = true
|
||||
holdPopoverTimer = setTimeout(() => {
|
||||
holdPopoverOpen.value = false
|
||||
holdPopoverTimer = undefined
|
||||
}, 200)
|
||||
}
|
||||
|
||||
function onVisibleChange(next: boolean) {
|
||||
if (!next && holdPopoverOpen.value) return
|
||||
if (next) resetDraftFromModel()
|
||||
visible.value = next
|
||||
}
|
||||
|
||||
function close() {
|
||||
holdPopoverOpen.value = false
|
||||
if (holdPopoverTimer !== undefined) {
|
||||
clearTimeout(holdPopoverTimer)
|
||||
holdPopoverTimer = undefined
|
||||
}
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
@@ -263,6 +291,7 @@
|
||||
function buildState(): ColumnFilterState {
|
||||
if (props.type === 'text' || props.type === 'enum') {
|
||||
return {
|
||||
text: '',
|
||||
selected: [...draftSelected.value],
|
||||
keyword: draftKeyword.value
|
||||
}
|
||||
@@ -321,6 +350,13 @@
|
||||
if (visible.value) resetDraftFromModel()
|
||||
}
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (holdPopoverTimer !== undefined) {
|
||||
clearTimeout(holdPopoverTimer)
|
||||
holdPopoverTimer = undefined
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -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