From be8028269585a0d27d7a0eaa31adc5b6cf9ef4d5 Mon Sep 17 00:00:00 2001 From: cdn0x12 Date: Mon, 3 Feb 2025 13:13:02 +0800 Subject: [PATCH] feat: cache update index and contents --- package.json | 1 + pnpm-lock.yaml | 18 ++++ src/App.tsx | 14 +++ src/components/Timeline.tsx | 170 ++++++++++++++++++------------------ src/main.tsx | 13 ++- src/utils/updates.ts | 159 ++++++++++++++++++--------------- 6 files changed, 217 insertions(+), 158 deletions(-) diff --git a/package.json b/package.json index a13bd87..09cfa03 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 891cac5..16e3736 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/App.tsx b/src/App.tsx index 5b5a642..697f503 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( diff --git a/src/components/Timeline.tsx b/src/components/Timeline.tsx index 104aad7..e9157e1 100644 --- a/src/components/Timeline.tsx +++ b/src/components/Timeline.tsx @@ -61,52 +61,31 @@ const Pagination: React.FC = ({ const Timeline = () => { const { t, i18n } = useTranslation(); const [searchParams, setSearchParams] = useSearchParams(); - const [updates, setUpdates] = useState([]); - 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) { - setSearchParams((prev) => { - prev.set('page', result.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]); + if (pagination.totalPages > 0 && currentPage > pagination.totalPages) { + setSearchParams((prev) => { + prev.set('page', pagination.totalPages.toString()); + return prev; + }); + } + }, [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 ( +
+

{t('updates.error')}

+
+ ); + } + return (
@@ -137,7 +125,7 @@ const Timeline = () => {
@@ -145,57 +133,69 @@ const Timeline = () => { {/* Timeline line */}
-
- {updates.map((update) => ( -
- {/* Timeline dot */} -
- - {/* Content */} -
- {update.date} - -

- {update.title} - {update.link && ( - - )} -

- -

{update.summary}

-
- {update.tags.map((tag) => ( - - ))} +

+ {update.title} + {update.link && ( + + )} +

+ +

{update.summary}

+
+ {update.tags.map((tag) => ( + + ))} +
+
+
-
-
+ ))}
- ))} -
- {pagination.totalPages > 0 && ( - + {pagination.totalPages > 1 && ( + + )} + + ) : ( +
+

{t('updates.no_results')}

+
)} diff --git a/src/main.tsx b/src/main.tsx index b59339e..679b2c5 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -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( - + + + ); \ No newline at end of file diff --git a/src/utils/updates.ts b/src/utils/updates.ts index e5ab3ef..b292fb6 100644 --- a/src/utils/updates.ts +++ b/src/utils/updates.ts @@ -1,4 +1,5 @@ import { useTranslation } from 'react-i18next'; +import { useQuery } from '@tanstack/react-query'; export interface Update { id: string; @@ -17,73 +18,84 @@ interface UpdatesIndex { years: string[]; } +// 获取更新年份列表 +const getUpdateYears = async (language: string): Promise => { + try { + const response = await fetch(`/data/${language}/updates.json`); + if (!response.ok) { + throw new Error(`Failed to load updates index, status: ${response.status}`); + } + const index: UpdatesIndex = await response.json(); + return index.years || []; + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn(`Failed to load updates index:`, error); + } + return []; + } +}; + +// 动态获取指定的年度更新 +const getYearUpdates = async (yearFile: string, language: string): Promise => { + try { + const response = await fetch(`/data/${language}/updates/${yearFile}`); + if (!response.ok) { + throw new Error(`Failed to load updates from ${yearFile}, status: ${response.status}`); + } + const data: YearUpdates = await response.json(); + return data; + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.warn(`Failed to load updates from ${yearFile}:`, error); + } + return null; + } +}; + +// 获取所有更新并按日期排序 +const getAllUpdates = async (language: string, selectedTags: string[] = []): Promise => { + const allUpdates: Update[] = []; + + // 从索引文件获取年份列表 + const yearFiles = await getUpdateYears(language); + + // 加载所有年份的更新 + for (const yearFile of yearFiles) { + const yearData = await getYearUpdates(yearFile, language); + if (yearData?.updates?.length) { + // 如果指定了标签,只添加包含所有选定标签的更新 + const filteredUpdates = selectedTags.length > 0 + ? yearData.updates.filter(update => + selectedTags.every(tag => update.tags.includes(tag)) + ) + : yearData.updates; + + allUpdates.push(...filteredUpdates); + } + } + + // 按日期降序排序 + return allUpdates.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); +}; + +// 获取分页的更新列表 export const useUpdates = (page: number = 1, pageSize: number = 10) => { const { i18n } = useTranslation(); - // 获取更新年份列表 - const getUpdateYears = async (): Promise => { - try { - const response = await fetch(`/data/${i18n.language}/updates.json`); - if (!response.ok) { - throw new Error(`Failed to load updates index, status: ${response.status}`); - } - const index: UpdatesIndex = await response.json(); - return index.years || []; - } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.warn(`Failed to load updates index:`, error); - } - return []; - } - }; + const { data: allUpdates = [], isLoading, error } = useQuery({ + queryKey: ['updates', i18n.language], + queryFn: () => getAllUpdates(i18n.language), + }); - // 动态获取指定的年度更新 - const getYearUpdates = async (yearFile: string): Promise => { - try { - const response = await fetch(`/data/${i18n.language}/updates/${yearFile}`); - if (!response.ok) { - throw new Error(`Failed to load updates from ${yearFile}, status: ${response.status}`); - } - const data: YearUpdates = await response.json(); - return data; - } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.warn(`Failed to load updates from ${yearFile}:`, error); - } - return null; - } - }; + const getPaginatedUpdates = (selectedTags: string[] = []) => { + // 过滤标签 + const filteredUpdates = selectedTags.length > 0 + ? allUpdates.filter(update => + selectedTags.every(tag => update.tags.includes(tag)) + ) + : allUpdates; - // 获取所有更新并按日期排序 - const getAllUpdates = async (selectedTags: string[] = []): Promise => { - const allUpdates: Update[] = []; - - // 从索引文件获取年份列表 - const yearFiles = await getUpdateYears(); - - // 加载所有年份的更新 - for (const yearFile of yearFiles) { - const yearData = await getYearUpdates(yearFile); - if (yearData?.updates?.length) { - // 如果指定了标签,只添加包含所有选定标签的更新 - const filteredUpdates = selectedTags.length > 0 - ? yearData.updates.filter(update => - selectedTags.every(tag => update.tags.includes(tag)) - ) - : yearData.updates; - - allUpdates.push(...filteredUpdates); - } - } - - // 按日期降序排序 - return allUpdates.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); - }; - - // 获取分页的更新列表 - const getPaginatedUpdates = async (selectedTags: string[] = []) => { - const allUpdates = await getAllUpdates(selectedTags); - const totalItems = allUpdates.length; + 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 => {