From 1f51c330dd163be9eaddd5edd5f1645d01f6699e Mon Sep 17 00:00:00 2001 From: Developer Date: Wed, 18 Mar 2026 16:16:04 +0800 Subject: [PATCH] feat: add SWR for data fetching with optimistic updates --- frontend/hooks/useRecords.js | 54 ++++++++++++++++++++++++++++++++++ frontend/hooks/useStats.js | 57 ++++++++++++++++++++++++++++++++++++ frontend/lib/swr-config.js | 21 +++++++++++++ frontend/package-lock.json | 34 ++++++++++++++++++++- frontend/package.json | 3 +- 5 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 frontend/hooks/useRecords.js create mode 100644 frontend/hooks/useStats.js create mode 100644 frontend/lib/swr-config.js diff --git a/frontend/hooks/useRecords.js b/frontend/hooks/useRecords.js new file mode 100644 index 0000000..36afd35 --- /dev/null +++ b/frontend/hooks/useRecords.js @@ -0,0 +1,54 @@ +import useSWR from 'swr'; +import { getRecords, createRecord, updateRecord, deleteRecord } from '../lib/api'; + +export function useRecords(month, type = 'all') { + const key = month ? ['records', month, type] : null; + + const { data, error, isLoading, mutate } = useSWR( + key, + () => getRecords(month, type), + { + revalidateOnFocus: false, + dedupingInterval: 5000, + } + ); + + // 乐观更新:添加记录 + const addRecord = async (recordData) => { + const newRecord = await createRecord(recordData); + mutate( + (current) => (current ? [...current, newRecord] : [newRecord]), + { revalidate: false } + ); + return newRecord; + }; + + // 乐观更新:更新记录 + const editRecord = async (id, recordData) => { + await updateRecord(id, recordData); + mutate( + (current) => + current?.map((r) => (r.id === id ? { ...r, ...recordData } : r)), + { revalidate: false } + ); + }; + + // 乐观更新:删除记录 + const removeRecord = async (id) => { + await deleteRecord(id); + mutate( + (current) => current?.filter((r) => r.id !== id), + { revalidate: false } + ); + }; + + return { + records: data || [], + isLoading, + error, + mutate, + addRecord, + editRecord, + removeRecord, + }; +} diff --git a/frontend/hooks/useStats.js b/frontend/hooks/useStats.js new file mode 100644 index 0000000..3109ca2 --- /dev/null +++ b/frontend/hooks/useStats.js @@ -0,0 +1,57 @@ +import useSWR from 'swr'; +import { getStats, getTrendStats, getCategoryStats } from '../lib/api'; + +export function useStats(month) { + const key = month ? ['stats', month] : null; + + const { data, error, isLoading, mutate } = useSWR( + key, + () => getStats(month), + { + revalidateOnFocus: false, + } + ); + + return { + stats: data || { income: 0, expense: 0, balance: 0 }, + isLoading, + error, + mutate, + }; +} + +export function useTrendStats(period = 'month', month) { + const key = ['trend', period, month]; + + const { data, error, isLoading } = useSWR( + key, + () => getTrendStats(period, month), + { + revalidateOnFocus: false, + } + ); + + return { + trend: data || [], + isLoading, + error, + }; +} + +export function useCategoryStats(month, type = 'all') { + const key = ['categoryStats', month, type]; + + const { data, error, isLoading } = useSWR( + key, + () => getCategoryStats(month, type), + { + revalidateOnFocus: false, + } + ); + + return { + categories: data || [], + isLoading, + error, + }; +} diff --git a/frontend/lib/swr-config.js b/frontend/lib/swr-config.js new file mode 100644 index 0000000..5213467 --- /dev/null +++ b/frontend/lib/swr-config.js @@ -0,0 +1,21 @@ +import { getRecords, getStats, getCategories, getRecurringBills } from './api'; + +// 全局 SWR 配置 +export const swrConfig = { + refreshInterval: 0, // 默认不自动刷新 + revalidateOnFocus: true, + revalidateOnReconnect: true, + dedupingInterval: 2000, + errorRetryCount: 3, + onError: (error) => { + console.error('SWR Error:', error); + }, +}; + +// 数据获取器映射 +export const fetchers = { + records: (month, type) => getRecords(month, type), + stats: (month) => getStats(month), + categories: () => getCategories(), + recurring: () => getRecurringBills(), +}; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bfc0635..204257a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,7 +11,8 @@ "chart.js": "^4.5.1", "next": "^14.0.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "swr": "^2.4.1" } }, "node_modules/@kurkle/color": { @@ -235,6 +236,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -435,11 +445,33 @@ } } }, + "node_modules/swr": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.1.tgz", + "integrity": "sha512-2CC6CiKQtEwaEeNiqWTAw9PGykW8SR5zZX8MZk6TeAvEAnVS7Visz8WzphqgtQ8v2xz/4Q5K+j+SeMaKXeeQIA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 4e7e601..b93170b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "chart.js": "^4.5.1", "next": "^14.0.0", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "swr": "^2.4.1" } }