From 2b44aca254eb0c46f844299b6d595ffff5f5aea1 Mon Sep 17 00:00:00 2001 From: gaoziman <2942894660@qq.com> Date: Wed, 24 Dec 2025 22:50:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(Hook):=20=E6=B7=BB=E5=8A=A0=E6=B6=88?= =?UTF-8?q?=E6=81=AF=E6=90=9C=E7=B4=A2=E7=8A=B6=E6=80=81=E7=AE=A1=E7=90=86?= =?UTF-8?q?=20Hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 useSearch Hook 封装搜索逻辑 - 实现 300ms 防抖搜索避免频繁请求 - 支持角色筛选切换即时刷新 - 支持分页加载和结果缓存 - 提供完整的搜索状态管理 --- src/hooks/useSearch.ts | 290 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 src/hooks/useSearch.ts diff --git a/src/hooks/useSearch.ts b/src/hooks/useSearch.ts new file mode 100644 index 0000000..e1fe19e --- /dev/null +++ b/src/hooks/useSearch.ts @@ -0,0 +1,290 @@ +'use client'; + +import { useState, useCallback, useRef, useEffect } from 'react'; + +/** + * 搜索结果项类型 + */ +export interface SearchResult { + messageId: string; + conversationId: string; + conversationTitle: string; + role: string; + content: string; + createdAt: string; +} + +/** + * 搜索响应类型 + */ +interface SearchResponse { + success: boolean; + data?: { + results: SearchResult[]; + total: number; + page: number; + limit: number; + totalPages: number; + query: string; + role: string; + }; + error?: string; +} + +/** + * 搜索状态类型 + */ +type SearchStatus = 'idle' | 'loading' | 'success' | 'error'; + +/** + * 搜索角色筛选类型 + */ +export type SearchRole = 'all' | 'user' | 'assistant'; + +/** + * useSearch Hook 返回类型 + */ +interface UseSearchReturn { + // 状态 + query: string; + results: SearchResult[]; + total: number; + page: number; + totalPages: number; + status: SearchStatus; + error: string | null; + role: SearchRole; + + // 操作 + setQuery: (query: string) => void; + setRole: (role: SearchRole) => void; + search: (searchQuery?: string) => Promise; + loadMore: () => Promise; + reset: () => void; +} + +/** + * 消息搜索 Hook + * + * 功能: + * - 防抖搜索(300ms) + * - 角色筛选 + * - 分页加载 + * - 结果缓存 + * - 状态管理 + */ +export function useSearch(): UseSearchReturn { + // 搜索状态 + const [query, setQueryState] = useState(''); + const [results, setResults] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(1); + const [totalPages, setTotalPages] = useState(0); + const [status, setStatus] = useState('idle'); + const [error, setError] = useState(null); + const [role, setRoleState] = useState('all'); + + // 防抖定时器 + const debounceTimerRef = useRef(null); + + // 缓存最后一次搜索的参数,避免重复请求 + const lastSearchRef = useRef<{ query: string; role: SearchRole } | null>(null); + + // AbortController 用于取消请求 + const abortControllerRef = useRef(null); + + /** + * 执行搜索请求 + */ + const doSearch = useCallback(async ( + searchQuery: string, + searchRole: SearchRole, + searchPage: number = 1, + append: boolean = false + ) => { + // 空查询时重置状态 + if (!searchQuery.trim()) { + setResults([]); + setTotal(0); + setPage(1); + setTotalPages(0); + setStatus('idle'); + setError(null); + return; + } + + // 取消之前的请求 + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + abortControllerRef.current = new AbortController(); + + try { + setStatus('loading'); + setError(null); + + const params = new URLSearchParams({ + q: searchQuery.trim(), + role: searchRole, + page: searchPage.toString(), + limit: '20', + }); + + const response = await fetch(`/api/messages/search?${params}`, { + signal: abortControllerRef.current.signal, + }); + + const data: SearchResponse = await response.json(); + + if (!response.ok || !data.success) { + throw new Error(data.error || '搜索失败'); + } + + // 更新状态 + if (append && data.data) { + setResults(prev => [...prev, ...data.data!.results]); + } else if (data.data) { + setResults(data.data.results); + } + + if (data.data) { + setTotal(data.data.total); + setPage(data.data.page); + setTotalPages(data.data.totalPages); + } + setStatus('success'); + + // 更新缓存 + lastSearchRef.current = { query: searchQuery, role: searchRole }; + } catch (err) { + // 忽略取消的请求 + if (err instanceof Error && err.name === 'AbortError') { + return; + } + setStatus('error'); + setError(err instanceof Error ? err.message : '搜索失败'); + } + }, []); + + /** + * 设置搜索关键词(带防抖) + */ + const setQuery = useCallback((newQuery: string) => { + setQueryState(newQuery); + + // 清除之前的定时器 + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + // 空查询立即清空结果 + if (!newQuery.trim()) { + setResults([]); + setTotal(0); + setPage(1); + setTotalPages(0); + setStatus('idle'); + setError(null); + return; + } + + // 设置新的防抖定时器 + debounceTimerRef.current = setTimeout(() => { + doSearch(newQuery, role, 1, false); + }, 300); + }, [role, doSearch]); + + /** + * 设置角色筛选 + */ + const setRole = useCallback((newRole: SearchRole) => { + setRoleState(newRole); + + // 如果有搜索关键词,立即重新搜索 + if (query.trim()) { + // 清除防抖定时器 + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + doSearch(query, newRole, 1, false); + } + }, [query, doSearch]); + + /** + * 手动触发搜索 + */ + const search = useCallback(async (searchQuery?: string) => { + const q = searchQuery ?? query; + if (q.trim()) { + // 清除防抖定时器 + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + await doSearch(q, role, 1, false); + } + }, [query, role, doSearch]); + + /** + * 加载更多结果 + */ + const loadMore = useCallback(async () => { + if (status === 'loading' || page >= totalPages) { + return; + } + + await doSearch(query, role, page + 1, true); + }, [query, role, page, totalPages, status, doSearch]); + + /** + * 重置搜索状态 + */ + const reset = useCallback(() => { + // 清除防抖定时器 + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + // 取消进行中的请求 + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + setQueryState(''); + setResults([]); + setTotal(0); + setPage(1); + setTotalPages(0); + setStatus('idle'); + setError(null); + setRoleState('all'); + lastSearchRef.current = null; + }, []); + + // 清理定时器和请求 + useEffect(() => { + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + }; + }, []); + + return { + query, + results, + total, + page, + totalPages, + status, + error, + role, + setQuery, + setRole, + search, + loadMore, + reset, + }; +}