/** * useFilterSelections Hook * * 筛选选择状态管理Hook,优化了以下功能: * - 支持单选和多选模式 * - 使用防抖技术避免频繁更新 * - 支持URL参数同步 * - 支持批量清除 * * @created 2024-12-13 */ import { useState, useCallback, useMemo, useRef, useEffect } from 'react' import { useSearchParams, useRouter } from 'next/navigation' type SelectionValue = string | string[] | null interface UseFilterSelectionsOptions { /** 初始选择值 */ initialSelections?: Record /** 是否同步到URL参数 */ syncToUrl?: boolean /** URL参数前缀 */ urlParamPrefix?: string /** 选择变化回调 */ onChange?: (selections: Record) => void /** 防抖延迟(毫秒) */ debounceMs?: number } interface UseFilterSelectionsReturn { /** 当前选择值 */ selections: Record /** 设置单个字段的选择值 */ setSelection: (fieldKey: string, value: SelectionValue) => void /** 切换选择值(用于单选模式) */ toggleSelection: (fieldKey: string, value: string) => void /** 添加选择值(用于多选模式) */ addSelection: (fieldKey: string, value: string) => void /** 移除选择值(用于多选模式) */ removeSelection: (fieldKey: string, value: string) => void /** 清除单个字段的选择 */ clearSelection: (fieldKey: string) => void /** 清除所有选择 */ clearAllSelections: () => void /** 是否有任何选择 */ hasSelections: boolean /** 选择数量 */ selectionCount: number /** 获取字段的选择值(始终返回数组) */ getSelectionValues: (fieldKey: string) => string[] /** 检查值是否被选中 */ isSelected: (fieldKey: string, value: string) => boolean } /** * 筛选选择状态管理Hook */ export function useFilterSelections( options: UseFilterSelectionsOptions = {} ): UseFilterSelectionsReturn { const { initialSelections = {}, syncToUrl = false, urlParamPrefix = 'filter_', onChange, debounceMs = 100, } = options const router = useRouter() const searchParams = useSearchParams() const debounceTimerRef = useRef(null) const onChangeRef = useRef(onChange) onChangeRef.current = onChange // 从URL参数初始化选择值 const getInitialSelectionsFromUrl = useCallback((): Record => { if (!syncToUrl || !searchParams) return {} const selections: Record = {} searchParams.forEach((value, key) => { if (key.startsWith(urlParamPrefix)) { const fieldKey = key.slice(urlParamPrefix.length) // 支持多值(逗号分隔) const values = value.split(',').filter(Boolean) if (values.length === 1) { selections[fieldKey] = values[0] } else if (values.length > 1) { selections[fieldKey] = values } } }) return selections }, [searchParams, syncToUrl, urlParamPrefix]) // 合并初始值 const mergedInitialSelections = useMemo(() => { const urlSelections = getInitialSelectionsFromUrl() const merged: Record = {} // 先应用传入的初始值 Object.entries(initialSelections).forEach(([key, value]) => { if (value !== null) { merged[key] = value } }) // URL参数优先级更高 Object.entries(urlSelections).forEach(([key, value]) => { merged[key] = value }) return merged }, [initialSelections, getInitialSelectionsFromUrl]) const [selections, setSelections] = useState>( mergedInitialSelections ) // 同步到URL const syncSelectionsToUrl = useCallback((newSelections: Record) => { if (!syncToUrl) return const params = new URLSearchParams(searchParams?.toString() || '') // 清除旧的筛选参数 const keysToDelete: string[] = [] params.forEach((_, key) => { if (key.startsWith(urlParamPrefix)) { keysToDelete.push(key) } }) keysToDelete.forEach(key => params.delete(key)) // 添加新的筛选参数 Object.entries(newSelections).forEach(([fieldKey, value]) => { if (value) { const paramValue = Array.isArray(value) ? value.join(',') : value params.set(`${urlParamPrefix}${fieldKey}`, paramValue) } }) const newUrl = params.toString() ? `?${params.toString()}` : window.location.pathname router.replace(newUrl, { scroll: false }) }, [router, searchParams, syncToUrl, urlParamPrefix]) // 防抖更新 const debouncedUpdate = useCallback((newSelections: Record) => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current) } debounceTimerRef.current = setTimeout(() => { onChangeRef.current?.(newSelections) syncSelectionsToUrl(newSelections) }, debounceMs) }, [debounceMs, syncSelectionsToUrl]) // 清理定时器 useEffect(() => { return () => { if (debounceTimerRef.current) { clearTimeout(debounceTimerRef.current) } } }, []) const setSelection = useCallback((fieldKey: string, value: SelectionValue) => { setSelections(prev => { const next = { ...prev } if (value === null || (Array.isArray(value) && value.length === 0)) { delete next[fieldKey] } else { next[fieldKey] = value } debouncedUpdate(next) return next }) }, [debouncedUpdate]) const toggleSelection = useCallback((fieldKey: string, value: string) => { setSelections(prev => { const next = { ...prev } const current = prev[fieldKey] if (current === value) { // 取消选择 delete next[fieldKey] } else { // 选择新值 next[fieldKey] = value } debouncedUpdate(next) return next }) }, [debouncedUpdate]) const addSelection = useCallback((fieldKey: string, value: string) => { setSelections(prev => { const next = { ...prev } const current = prev[fieldKey] if (!current) { next[fieldKey] = [value] } else if (Array.isArray(current)) { if (!current.includes(value)) { next[fieldKey] = [...current, value] } } else { if (current !== value) { next[fieldKey] = [current, value] } } debouncedUpdate(next) return next }) }, [debouncedUpdate]) const removeSelection = useCallback((fieldKey: string, value: string) => { setSelections(prev => { const next = { ...prev } const current = prev[fieldKey] if (!current) return prev if (Array.isArray(current)) { const filtered = current.filter(v => v !== value) if (filtered.length === 0) { delete next[fieldKey] } else if (filtered.length === 1) { next[fieldKey] = filtered[0] } else { next[fieldKey] = filtered } } else if (current === value) { delete next[fieldKey] } debouncedUpdate(next) return next }) }, [debouncedUpdate]) const clearSelection = useCallback((fieldKey: string) => { setSelection(fieldKey, null) }, [setSelection]) const clearAllSelections = useCallback(() => { setSelections({}) debouncedUpdate({}) }, [debouncedUpdate]) const getSelectionValues = useCallback((fieldKey: string): string[] => { const value = selections[fieldKey] if (!value) return [] return Array.isArray(value) ? value : [value] }, [selections]) const isSelected = useCallback((fieldKey: string, value: string): boolean => { const values = getSelectionValues(fieldKey) return values.includes(value) }, [getSelectionValues]) const hasSelections = useMemo(() => Object.keys(selections).length > 0, [selections]) const selectionCount = useMemo(() => { return Object.values(selections).reduce((count, value) => { if (Array.isArray(value)) { return count + value.length } return count + 1 }, 0) }, [selections]) return { selections, setSelection, toggleSelection, addSelection, removeSelection, clearSelection, clearAllSelections, hasSelections, selectionCount, getSelectionValues, isSelected, } } export default useFilterSelections