flnxi-Wu/hooks/useFilterSelections.ts
WuYongJie 63ec6b7dfb fix: 修复排行榜按钮消失、tag排序和输入框浅色模式问题
1. 排行榜悬浮窗关闭后小按钮消失问题
   - 修复面板居中模式关闭时按钮位置错误
   - 保存并恢复按钮的居中状态

2. 首页tag按钮按素材数量排序
   - 添加按素材数量降序排序逻辑

3. 上传弹窗输入框浅色模式文字不可见
   - 添加 text-slate-900 确保浅色模式下文字可见
   - 添加 placeholder 样式确保占位符正确显示
2025-12-14 21:41:29 +08:00

293 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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