feat(Hook): 添加消息搜索状态管理 Hook

- 新增 useSearch Hook 封装搜索逻辑
- 实现 300ms 防抖搜索避免频繁请求
- 支持角色筛选切换即时刷新
- 支持分页加载和结果缓存
- 提供完整的搜索状态管理
This commit is contained in:
gaoziman 2025-12-24 22:50:09 +08:00
parent dcd757e584
commit 2b44aca254

290
src/hooks/useSearch.ts Normal file
View File

@ -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<void>;
loadMore: () => Promise<void>;
reset: () => void;
}
/**
* Hook
*
*
* - 300ms
* -
* -
* -
* -
*/
export function useSearch(): UseSearchReturn {
// 搜索状态
const [query, setQueryState] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(0);
const [status, setStatus] = useState<SearchStatus>('idle');
const [error, setError] = useState<string | null>(null);
const [role, setRoleState] = useState<SearchRole>('all');
// 防抖定时器
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
// 缓存最后一次搜索的参数,避免重复请求
const lastSearchRef = useRef<{ query: string; role: SearchRole } | null>(null);
// AbortController 用于取消请求
const abortControllerRef = useRef<AbortController | null>(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,
};
}