- 新增 useSearch Hook 封装搜索逻辑 - 实现 300ms 防抖搜索避免频繁请求 - 支持角色筛选切换即时刷新 - 支持分页加载和结果缓存 - 提供完整的搜索状态管理
291 lines
6.4 KiB
TypeScript
291 lines
6.4 KiB
TypeScript
'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,
|
||
};
|
||
}
|