feat(Hook): 添加消息搜索状态管理 Hook
- 新增 useSearch Hook 封装搜索逻辑 - 实现 300ms 防抖搜索避免频繁请求 - 支持角色筛选切换即时刷新 - 支持分页加载和结果缓存 - 提供完整的搜索状态管理
This commit is contained in:
parent
dcd757e584
commit
2b44aca254
290
src/hooks/useSearch.ts
Normal file
290
src/hooks/useSearch.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user