feat: cache update index and contents

This commit is contained in:
CDN 2025-02-03 13:13:02 +08:00
parent 2daa1b9879
commit be80282695
Signed by: CDN
GPG key ID: 0C656827F9F80080
6 changed files with 217 additions and 158 deletions

View file

@ -11,6 +11,7 @@
},
"dependencies": {
"@ant-design/icons": "^5.6.0",
"@tanstack/react-query": "^5.66.0",
"antd": "^5.23.3",
"clsx": "^2.1.1",
"i18next": "^23.16.8",

18
pnpm-lock.yaml generated
View file

@ -11,6 +11,9 @@ importers:
'@ant-design/icons':
specifier: ^5.6.0
version: 5.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tanstack/react-query':
specifier: ^5.66.0
version: 5.66.0(react@18.3.1)
antd:
specifier: ^5.23.3
version: 5.23.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -772,6 +775,14 @@ packages:
peerDependencies:
tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1'
'@tanstack/query-core@5.66.0':
resolution: {integrity: sha512-J+JeBtthiKxrpzUu7rfIPDzhscXF2p5zE/hVdrqkACBP8Yu0M96mwJ5m/8cPPYQE9aRNvXztXHlNwIh4FEeMZw==}
'@tanstack/react-query@5.66.0':
resolution: {integrity: sha512-z3sYixFQJe8hndFnXgWu7C79ctL+pI0KAelYyW+khaNJ1m22lWrhJU2QrsTcRKMuVPtoZvfBYrTStIdKo+x0Xw==}
peerDependencies:
react: ^18 || ^19
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -2589,6 +2600,13 @@ snapshots:
postcss-selector-parser: 6.0.10
tailwindcss: 3.4.17
'@tanstack/query-core@5.66.0': {}
'@tanstack/react-query@5.66.0(react@18.3.1)':
dependencies:
'@tanstack/query-core': 5.66.0
react: 18.3.1
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.26.7

View file

@ -10,11 +10,25 @@ import UpdateDetailPage from './pages/UpdateDetailPage';
import ContributorsPage from './pages/ContributorsPage';
import AboutPage from './pages/AboutPage';
import iconMap from './utils/iconMap';
import { useQueryClient } from '@tanstack/react-query';
function App() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const socialLinks = t('social.links', { returnObjects: true });
// 在窗口关闭时清除缓存
React.useEffect(() => {
const handleBeforeUnload = () => {
queryClient.clear();
};
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload);
};
}, [queryClient]);
return (
<ThemeProvider>
<Router>

View file

@ -61,52 +61,31 @@ const Pagination: React.FC<PaginationProps> = ({
const Timeline = () => {
const { t, i18n } = useTranslation();
const [searchParams, setSearchParams] = useSearchParams();
const [updates, setUpdates] = useState<Update[]>([]);
const [pagination, setPagination] = useState({
currentPage: 1,
totalPages: 1,
totalItems: 0,
hasNextPage: false,
hasPrevPage: false
});
// 获取当前页码和标签筛选
const currentPage = Math.max(1, parseInt(searchParams.get('page') || '1', 10));
const selectedTags = searchParams.get('tags')?.split(',').filter(Boolean) || [];
const { getPaginatedUpdates } = useUpdates(currentPage);
// 使用 React Query hook
const { getPaginatedUpdates, isLoading, error } = useUpdates(currentPage);
// 获取分页数据
const { updates, pagination } = getPaginatedUpdates(selectedTags);
// 获取所有可用的标签
const availableTags = Object.keys(t('updates.tags', { returnObjects: true }));
// 当页码超出范围时自动调整
useEffect(() => {
const fetchUpdates = async () => {
try {
const result = await getPaginatedUpdates(selectedTags);
setUpdates(result.updates);
setPagination(result.pagination);
// 如果当前页码超出范围,自动调整到最后一页
if (currentPage > result.pagination.totalPages) {
if (pagination.totalPages > 0 && currentPage > pagination.totalPages) {
setSearchParams((prev) => {
prev.set('page', result.pagination.totalPages.toString());
prev.set('page', pagination.totalPages.toString());
return prev;
});
}
} catch (error) {
console.error('Failed to fetch updates:', error);
setUpdates([]);
setPagination({
currentPage: 1,
totalPages: 1,
totalItems: 0,
hasNextPage: false,
hasPrevPage: false
});
}
};
fetchUpdates();
}, [currentPage, selectedTags, i18n.language, getPaginatedUpdates, setSearchParams]);
}, [pagination.totalPages, currentPage, setSearchParams]);
// 处理页码变化
const handlePageChange = (page: number) => {
setSearchParams((prev) => {
prev.set('page', page.toString());
@ -114,7 +93,8 @@ const Timeline = () => {
});
};
const handleTagsChange = (tags: string[]) => {
// 处理标签变化
const handleTagChange = (tags: string[]) => {
setSearchParams((prev) => {
if (tags.length > 0) {
prev.set('tags', tags.join(','));
@ -126,6 +106,14 @@ const Timeline = () => {
});
};
if (error) {
return (
<div className="text-center py-20">
<p className="text-red-500">{t('updates.error')}</p>
</div>
);
}
return (
<div className="container mx-auto px-8 md:px-20 lg:px-32 xl:px-48 2xl:px-64">
<div className="max-w-5xl">
@ -137,7 +125,7 @@ const Timeline = () => {
<TagFilter
availableTags={availableTags}
selectedTags={selectedTags}
onTagsChange={handleTagsChange}
onChange={handleTagChange}
/>
</div>
@ -145,6 +133,12 @@ const Timeline = () => {
{/* Timeline line */}
<div className="absolute left-6 top-0 h-full w-0.5 bg-blue-200 dark:bg-blue-900"></div>
{isLoading ? (
<div className="text-center py-20">
<p>{t('updates.loading')}</p>
</div>
) : updates.length > 0 ? (
<>
<div className="space-y-8">
{updates.map((update) => (
<div key={update.id} className="relative flex items-start group">
@ -171,7 +165,7 @@ const Timeline = () => {
{update.tags.map((tag) => (
<button
key={tag}
onClick={() => handleTagsChange([...selectedTags, tag])}
onClick={() => handleTagChange([...selectedTags, tag])}
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium transition-colors ${
selectedTags.includes(tag)
? 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
@ -188,7 +182,7 @@ const Timeline = () => {
))}
</div>
{pagination.totalPages > 0 && (
{pagination.totalPages > 1 && (
<Pagination
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
@ -197,6 +191,12 @@ const Timeline = () => {
hasPrevPage={pagination.hasPrevPage}
/>
)}
</>
) : (
<div className="text-center py-20">
<p>{t('updates.no_results')}</p>
</div>
)}
</div>
</div>
</div>

View file

@ -3,9 +3,20 @@ import { createRoot } from 'react-dom/client';
import App from './App.tsx';
import './i18n';
import './index.css';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity, // 数据永不过期,除非手动使其失效
},
},
});
createRoot(document.getElementById('root')!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>
);

View file

@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
export interface Update {
id: string;
@ -17,13 +18,10 @@ interface UpdatesIndex {
years: string[];
}
export const useUpdates = (page: number = 1, pageSize: number = 10) => {
const { i18n } = useTranslation();
// 获取更新年份列表
const getUpdateYears = async (): Promise<string[]> => {
const getUpdateYears = async (language: string): Promise<string[]> => {
try {
const response = await fetch(`/data/${i18n.language}/updates.json`);
const response = await fetch(`/data/${language}/updates.json`);
if (!response.ok) {
throw new Error(`Failed to load updates index, status: ${response.status}`);
}
@ -38,9 +36,9 @@ export const useUpdates = (page: number = 1, pageSize: number = 10) => {
};
// 动态获取指定的年度更新
const getYearUpdates = async (yearFile: string): Promise<YearUpdates | null> => {
const getYearUpdates = async (yearFile: string, language: string): Promise<YearUpdates | null> => {
try {
const response = await fetch(`/data/${i18n.language}/updates/${yearFile}`);
const response = await fetch(`/data/${language}/updates/${yearFile}`);
if (!response.ok) {
throw new Error(`Failed to load updates from ${yearFile}, status: ${response.status}`);
}
@ -55,15 +53,15 @@ export const useUpdates = (page: number = 1, pageSize: number = 10) => {
};
// 获取所有更新并按日期排序
const getAllUpdates = async (selectedTags: string[] = []): Promise<Update[]> => {
const getAllUpdates = async (language: string, selectedTags: string[] = []): Promise<Update[]> => {
const allUpdates: Update[] = [];
// 从索引文件获取年份列表
const yearFiles = await getUpdateYears();
const yearFiles = await getUpdateYears(language);
// 加载所有年份的更新
for (const yearFile of yearFiles) {
const yearData = await getYearUpdates(yearFile);
const yearData = await getYearUpdates(yearFile, language);
if (yearData?.updates?.length) {
// 如果指定了标签,只添加包含所有选定标签的更新
const filteredUpdates = selectedTags.length > 0
@ -81,9 +79,23 @@ export const useUpdates = (page: number = 1, pageSize: number = 10) => {
};
// 获取分页的更新列表
const getPaginatedUpdates = async (selectedTags: string[] = []) => {
const allUpdates = await getAllUpdates(selectedTags);
const totalItems = allUpdates.length;
export const useUpdates = (page: number = 1, pageSize: number = 10) => {
const { i18n } = useTranslation();
const { data: allUpdates = [], isLoading, error } = useQuery({
queryKey: ['updates', i18n.language],
queryFn: () => getAllUpdates(i18n.language),
});
const getPaginatedUpdates = (selectedTags: string[] = []) => {
// 过滤标签
const filteredUpdates = selectedTags.length > 0
? allUpdates.filter(update =>
selectedTags.every(tag => update.tags.includes(tag))
)
: allUpdates;
const totalItems = filteredUpdates.length;
// 如果没有更新,返回空结果
if (totalItems === 0) {
@ -99,26 +111,29 @@ export const useUpdates = (page: number = 1, pageSize: number = 10) => {
};
}
// 计算分页信息
const totalPages = Math.ceil(totalItems / pageSize);
const safeCurrentPage = Math.min(Math.max(1, page), totalPages);
const start = (safeCurrentPage - 1) * pageSize;
const end = Math.min(start + pageSize, totalItems);
const updates = allUpdates.slice(start, end);
const normalizedPage = Math.min(Math.max(1, page), totalPages);
const startIndex = (normalizedPage - 1) * pageSize;
const endIndex = Math.min(startIndex + pageSize, totalItems);
return {
updates,
updates: filteredUpdates.slice(startIndex, endIndex),
pagination: {
currentPage: safeCurrentPage,
currentPage: normalizedPage,
totalPages,
totalItems,
hasNextPage: safeCurrentPage < totalPages,
hasPrevPage: safeCurrentPage > 1
hasNextPage: normalizedPage < totalPages,
hasPrevPage: normalizedPage > 1
}
};
};
return { getPaginatedUpdates, getAllUpdates };
return {
getPaginatedUpdates,
isLoading,
error
};
};
export const getUpdateUrl = (update: Update): string => {