claude-code-cchui/src/hooks/useSearch.ts
gaoziman 2b44aca254 feat(Hook): 添加消息搜索状态管理 Hook
- 新增 useSearch Hook 封装搜索逻辑
- 实现 300ms 防抖搜索避免频繁请求
- 支持角色筛选切换即时刷新
- 支持分页加载和结果缓存
- 提供完整的搜索状态管理
2025-12-24 22:50:09 +08:00

291 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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,
};
}