generated from template/astro-template
113 lines
3.7 KiB
TypeScript
113 lines
3.7 KiB
TypeScript
import { query } from '@/modules/query';
|
|
import { QueryMark, Mark } from '@/query/query-mark/query-mark';
|
|
import { useEffect, useRef, useState, useCallback } from 'react';
|
|
import { Tooltip } from '@/components/a/tooltip';
|
|
export const queryMark = new QueryMark({ query: query, markType: 'md' });
|
|
import Fuse from 'fuse.js';
|
|
import { debounce } from 'lodash-es';
|
|
import { SquareArrowOutUpRight } from 'lucide-react';
|
|
|
|
export const App = () => {
|
|
const [list, setList] = useState<Mark[]>([]);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [searchResults, setSearchResults] = useState<Mark[]>([]);
|
|
const fuseRef = useRef<Fuse<Mark>>(null);
|
|
|
|
// 实际执行搜索的函数
|
|
const performSearch = (term: string) => {
|
|
if (!term.trim()) {
|
|
setSearchResults([]);
|
|
return;
|
|
}
|
|
|
|
if (fuseRef.current) {
|
|
const results = fuseRef.current.search(term);
|
|
setSearchResults(results.map((result) => result.item));
|
|
}
|
|
};
|
|
|
|
// 使用 useRef 和 useCallback 创建防抖搜索函数,确保它在组件生命周期中只创建一次
|
|
const debouncedSearchRef = useRef(
|
|
debounce((term: string) => {
|
|
performSearch(term);
|
|
}, 1000), // 300毫秒的防抖延迟
|
|
);
|
|
|
|
useEffect(() => {
|
|
init();
|
|
|
|
// 组件卸载时取消待执行的防抖函数
|
|
return () => {
|
|
debouncedSearchRef.current.cancel();
|
|
};
|
|
}, []);
|
|
|
|
const init = async () => {
|
|
const res = await queryMark.getMarkList();
|
|
if (res.code === 200) {
|
|
const list = res.data?.list || [];
|
|
setList(list!);
|
|
const fuse = new Fuse(list, {
|
|
keys: ['title', 'description', 'tags', 'summary'],
|
|
});
|
|
fuseRef.current = fuse;
|
|
const result = fuse.search('苏生不惑的博客');
|
|
console.log(result);
|
|
}
|
|
};
|
|
|
|
const handleSearch = (term: string) => {
|
|
setSearchTerm(term); // 立即更新输入框的值
|
|
debouncedSearchRef.current(term); // 使用防抖函数进行搜索
|
|
};
|
|
|
|
const onOpen = (item: Mark) => {
|
|
const link = item.link;
|
|
window.open(link);
|
|
};
|
|
|
|
// 确定要显示的列表:有搜索内容时显示搜索结果,否则显示全部
|
|
const displayList = searchTerm.trim() ? searchResults : list;
|
|
|
|
return (
|
|
<div className='w-full h-full overflow-auto'>
|
|
<div className='sticky top-0 p-4 bg-white z-10 shadow-sm'>
|
|
<input
|
|
type='text'
|
|
value={searchTerm}
|
|
onChange={(e) => handleSearch(e.target.value)}
|
|
placeholder='搜索书签...'
|
|
className='w-full p-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300'
|
|
/>
|
|
</div>
|
|
<div className='p-4'>
|
|
{displayList.length === 0 && searchTerm.trim() ? (
|
|
<div className='text-center text-gray-500 my-8'>没有找到匹配的结果</div>
|
|
) : (
|
|
displayList.map((item) => {
|
|
return (
|
|
<div key={item.id} className='p-4 mb-3 border rounded-md hover:shadow-md transition-shadow' title={item.link}>
|
|
<Tooltip title={item.link}>
|
|
<h3 className='text-lg font-semibold mb-1 flex gap-2 items-center'>
|
|
{item.title || '-'}
|
|
|
|
<SquareArrowOutUpRight className='h-5 w-5 cursor-pointer' onClick={() => onOpen(item)} />
|
|
</h3>
|
|
</Tooltip>
|
|
<p className='text-sm text-gray-600 mb-2'>{item.summary}</p>
|
|
<div className='flex flex-wrap gap-1'>
|
|
{item.tags.map((tag, index) => (
|
|
<span key={index} className='px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded-md'>
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|