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