feat: 更新文章相关API和组件以增强搜索功能

1. 修改getArticlePagingAPI以支持按标题模糊搜索,更新相关参数名称。
2. 优化搜索组件逻辑,使用防抖机制处理关键词变化,避免过多请求。
3. 更新CommentForm组件的按钮样式,提升用户交互体验。
4. 重构useDebounce钩子,确保最新的函数和等待时间被正确引用。
This commit is contained in:
刘宇阳
2026-07-24 16:07:01 +08:00
parent 2462ed800a
commit 17c8a9f2ea
5 changed files with 76 additions and 46 deletions

View File

@@ -6,8 +6,8 @@ export const getArticleDataAPI = async (id: number, password?: string) => {
return await Request<Article>('GET', `/article${!password ? `/${id}` : `/${id}?password=${password}`}`);
}
// 获取文章列表
export const getArticlePagingAPI = async (params?: Page & { key?: string }) => {
// 获取文章列表title 按标题模糊搜索)
export const getArticlePagingAPI = async (params?: Page & { title?: string }) => {
return await Request<Paginate<Article[]>>('GET', `/article`, {
params: params ?? {}
});

View File

@@ -242,7 +242,7 @@ const CommentForm = ({ articleId }: Props) => {
<Spinner />
</div>
) : (
<button className="w-full h-10 mt-2 text-white rounded-md bg-primary hover:bg-primary/80 active:bg-primary/90 active:scale-95 transition-[scale] text-center cursor-pointer" type="submit">
<button className="w-full h-10 text-white rounded-md bg-primary hover:bg-primary/80 active:bg-primary/90 active:scale-95 transition-[scale] text-center cursor-pointer" type="submit">
发表评论
</button>
)}

View File

@@ -5,7 +5,6 @@ import Link from 'next/link';
import { Modal, TextField, type DisclosureProps } from '@/ThriveUI';
import { getArticlePagingAPI } from '@/api/article';
import { Article } from '@/types/app/article';
import useDebounce from '@/hooks/useDebounce';
import Empty from '../Empty';
interface Props {
@@ -18,29 +17,6 @@ export default ({ disclosure }: Props) => {
const [data, setData] = useState<Paginate<Article[]>>();
const [searchKey, setSearchKey] = useState('');
const getArticleList = async (key: string) => {
if (key.trim().length === 0) {
setData(undefined);
return;
}
const { data } = await getArticlePagingAPI({
key,
pageNum: 1,
pageSize: 10,
});
setData(data);
};
const debouncedFetchArticles = useDebounce(getArticleList, 300);
const onSearchArticle = (e: React.ChangeEvent<HTMLInputElement>) => {
const key = e.target.value;
setSearchKey(key);
debouncedFetchArticles(key);
};
useEffect(() => {
if (!isOpen) {
setData(undefined);
@@ -48,14 +24,49 @@ export default ({ disclosure }: Props) => {
}
}, [isOpen]);
// 关键词变化时防抖请求;清理函数取消过期请求,避免清空后再搜被旧响应覆盖
useEffect(() => {
if (!isOpen) return;
const key = searchKey.trim();
if (!key) {
setData(undefined);
return;
}
let cancelled = false;
const timer = window.setTimeout(async () => {
try {
const { data: result } = await getArticlePagingAPI({
title: key,
pageNum: 1,
pageSize: 10,
});
if (!cancelled) setData(result);
} catch {
if (!cancelled) setData(undefined);
}
}, 300);
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [searchKey, isOpen]);
return (
<Modal open={isOpen} onClose={onClose} title="搜索文章" className="max-w-2xl">
<div className="mb-7">
<TextField type="text" placeholder="请输入文章关键词" value={searchKey} onChange={onSearchArticle} />
<TextField
type="text"
placeholder="请输入文章关键词"
value={searchKey}
onChange={(e) => setSearchKey(e.target.value)}
/>
<div className="mt-4">
{data?.result
? data?.result?.map((item) => (
{data?.result?.length
? data.result.map((item) => (
<Link
key={item.id}
href={`/article/${item.id}`}

View File

@@ -1,16 +1,35 @@
import { useRef } from 'react';
import { useEffect, useRef } from 'react';
export default function useDebounce<T extends (...args: any[]) => any>(func: T, wait: number) {
const timeoutRef = useRef<number | undefined>(null);
/** 返回稳定的防抖函数;始终调用最新的 func避免闭包过期 */
export default function useDebounce<T extends (...args: never[]) => unknown>(func: T, wait: number) {
const funcRef = useRef(func);
funcRef.current = func;
const waitRef = useRef(wait);
const timeoutRef = useRef<number | undefined>(undefined);
return (...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
funcRef.current(...args);
}, wait);
};
useEffect(() => {
funcRef.current = func;
}, [func]);
useEffect(() => {
waitRef.current = wait;
}, [wait]);
useEffect(() => {
return () => {
if (timeoutRef.current !== undefined) clearTimeout(timeoutRef.current);
};
}, []);
const debouncedRef = useRef<T>(null as unknown as T);
if (!debouncedRef.current) {
debouncedRef.current = ((...args: Parameters<T>) => {
if (timeoutRef.current !== undefined) clearTimeout(timeoutRef.current);
timeoutRef.current = window.setTimeout(() => {
timeoutRef.current = undefined;
funcRef.current(...args);
}, waitRef.current);
}) as T;
}
return debouncedRef.current;
}

View File

@@ -6,7 +6,7 @@ import { CACHE_TAGS } from '@/lib/cache-tags';
type ArticlePagingParams = {
pageNum?: number;
pageSize?: number;
key?: string;
title?: string;
};
export async function getArticleCacheAPI(id: number) {
@@ -25,19 +25,19 @@ export async function getArticlePagingCacheAPI(params: ArticlePagingParams = {})
'use cache';
const pageNum = params.pageNum ?? 1;
const pageSize = params.pageSize ?? 8;
const key = params.key ?? '';
const title = params.title ?? '';
cacheLife('blog');
cacheTag(
CACHE_TAGS.articles,
`${CACHE_TAGS.articlesList}-${pageNum}-${pageSize}`,
...(key ? [`${CACHE_TAGS.articlesList}-search-${key}`] : []),
...(title ? [`${CACHE_TAGS.articlesList}-search-${title}`] : []),
);
return getArticlePagingAPI({
pageNum,
pageSize,
...(key ? { key } : {}),
...(title ? { title } : {}),
});
}