1. 排行榜悬浮窗关闭后小按钮消失问题 - 修复面板居中模式关闭时按钮位置错误 - 保存并恢复按钮的居中状态 2. 首页tag按钮按素材数量排序 - 添加按素材数量降序排序逻辑 3. 上传弹窗输入框浅色模式文字不可见 - 添加 text-slate-900 确保浅色模式下文字可见 - 添加 placeholder 样式确保占位符正确显示
293 lines
8.3 KiB
TypeScript
293 lines
8.3 KiB
TypeScript
/**
|
||
* 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<string, SelectionValue>
|
||
/** 是否同步到URL参数 */
|
||
syncToUrl?: boolean
|
||
/** URL参数前缀 */
|
||
urlParamPrefix?: string
|
||
/** 选择变化回调 */
|
||
onChange?: (selections: Record<string, SelectionValue>) => void
|
||
/** 防抖延迟(毫秒) */
|
||
debounceMs?: number
|
||
}
|
||
|
||
interface UseFilterSelectionsReturn {
|
||
/** 当前选择值 */
|
||
selections: Record<string, string | string[]>
|
||
/** 设置单个字段的选择值 */
|
||
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<NodeJS.Timeout | null>(null)
|
||
const onChangeRef = useRef(onChange)
|
||
onChangeRef.current = onChange
|
||
|
||
// 从URL参数初始化选择值
|
||
const getInitialSelectionsFromUrl = useCallback((): Record<string, string | string[]> => {
|
||
if (!syncToUrl || !searchParams) return {}
|
||
|
||
const selections: Record<string, string | string[]> = {}
|
||
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<string, string | string[]> = {}
|
||
|
||
// 先应用传入的初始值
|
||
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<Record<string, string | string[]>>(
|
||
mergedInitialSelections
|
||
)
|
||
|
||
// 同步到URL
|
||
const syncSelectionsToUrl = useCallback((newSelections: Record<string, string | string[]>) => {
|
||
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<string, string | string[]>) => {
|
||
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
|