perf: 性能优化与代码质量改进

- 简化ToastProvider组件,移除未使用导入
- 简化Stats页面代码
- 添加React ESLint配置
- 添加数据库索引和API参数验证
- 清理重复文件server.js.backup
- 移除前端未使用的UI代码和复杂样式

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-03-23 16:43:03 +08:00
co-authored by Claude Opus 4.6
parent 3975bbed73
commit 63c97ab31a
9 changed files with 43 additions and 859 deletions
-10
View File
@@ -1,5 +1,3 @@
import { getRecords, getStats, getCategories, getRecurringBills } from './api';
// 全局 SWR 配置
export const swrConfig = {
refreshInterval: 0, // 默认不自动刷新
@@ -11,11 +9,3 @@ export const swrConfig = {
console.error('SWR Error:', error);
},
};
// 数据获取器映射
export const fetchers = {
records: (month, type) => getRecords(month, type),
stats: (month) => getStats(month),
categories: () => getCategories(),
recurring: () => getRecurringBills(),
};
+7
View File
@@ -7,6 +7,13 @@ export const formatMonth = (date) =>
export const formatMoney = (num) => (num || 0).toFixed(2);
// Format date as 'YYYY-MM-DD'
export const formatDate = (date) =>
`${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
// Get current time string 'HH:MM'
export const getCurrentTimeStr = () => new Date().toTimeString().slice(0, 5);
// Parse date-time string 'YYYY-MM-DD HH:MM' into date and time parts
export const parseDateTime = (dateTimeStr) => {
if (!dateTimeStr) return { datePart: '', timePart: '00:00' };
+17 -12
View File
@@ -7,7 +7,7 @@ import RecordModal from '../components/RecordModal';
import { useToast } from '../components/ToastProvider';
import { useConfirm } from '../components/ConfirmProvider';
import { useCategories } from '../hooks/useCategories';
import { getMonthStr, formatMoney, formatMonth, parseDateTime, getDatePart } from '../lib/utils';
import { getMonthStr, formatMoney, formatMonth, parseDateTime, getDatePart, formatDate, getCurrentTimeStr } from '../lib/utils';
export default function Home() {
const router = useRouter();
@@ -31,12 +31,13 @@ export default function Home() {
showToast('分类加载失败', { tone: 'error' });
}
}, [categoriesError]);
const [formData, setFormData] = useState({
type: 'expense',
amount: '',
category: '',
date: new Date().toISOString().split('T')[0],
time: new Date().toTimeString().slice(0, 5),
date: formatDate(new Date()),
time: getCurrentTimeStr(),
note: '',
});
@@ -53,8 +54,8 @@ export default function Home() {
type: 'expense',
amount: '',
category: '',
date: new Date().toISOString().split('T')[0],
time: new Date().toTimeString().slice(0, 5),
date: formatDate(new Date()),
time: getCurrentTimeStr(),
note: '',
});
@@ -206,12 +207,16 @@ export default function Home() {
[groupedRecords]
);
const getDayExpense = (date) => {
const dayRecords = groupedRecords[date] || [];
return dayRecords
.filter((record) => record.type === 'expense')
.reduce((sum, record) => sum + record.amount, 0);
};
const dayExpenses = useMemo(() => {
const expenses = {};
for (const date of Object.keys(groupedRecords)) {
const dayRecords = groupedRecords[date] || [];
expenses[date] = dayRecords
.filter((record) => record.type === 'expense')
.reduce((sum, record) => sum + record.amount, 0);
}
return expenses;
}, [groupedRecords]);
const homeHeaderContent = (
<div className="header-center-container">
@@ -297,7 +302,7 @@ export default function Home() {
<div key={date} className="date-group">
<div className="date-label">
<span>{new Date(date).toLocaleDateString('zh-CN', { month: 'short', day: 'numeric', weekday: 'short' })}</span>
<span className="day-expense">支出 {formatMoney(getDayExpense(date))}</span>
<span className="day-expense">支出 {formatMoney(dayExpenses[date] || 0)}</span>
</div>
{groupedRecords[date].map((record) => {
const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝';
+2 -2
View File
@@ -49,7 +49,7 @@ export default function Search() {
if (router.isReady && router.query.q) {
const q = decodeURIComponent(router.query.q);
// Skip if already have results for this keyword (prevents double API call)
if (keyword === q && results.length > 0) return;
if (keyword === q) return;
setKeyword(q);
// 自动搜索
const doSearch = async () => {
@@ -66,7 +66,7 @@ export default function Search() {
};
doSearch();
}
}, [router.isReady, router.query.q, keyword, results]);
}, [router.isReady, router.query.q, keyword]);
const handleSearch = async (e) => {
e.preventDefault();
+14 -7
View File
@@ -44,13 +44,9 @@ export default function Stats() {
router.push('/login');
return;
}
loadTrendData();
}, [router, period, currentMonth]);
useEffect(() => {
if (!isLoggedIn()) return;
loadCategoryData();
}, [router, currentMonth, categoryType]);
// 并行加载两个数据集
Promise.all([loadTrendData(), loadCategoryData()]);
}, [router, period, currentMonth, categoryType]);
const loadTrendData = async () => {
setTrendLoading(true);
@@ -75,9 +71,20 @@ export default function Stats() {
}
};
useEffect(() => {
return () => {
// 组件卸载时清理 Chart.js 实例
if (trendChartInstance.current) {
trendChartInstance.current.destroy();
trendChartInstance.current = null;
}
};
}, []);
useEffect(() => {
if (trendChartInstance.current) {
trendChartInstance.current.destroy();
trendChartInstance.current = null;
}
if (trendChartRef.current && trendData.length > 0) {
const ctx = trendChartRef.current.getContext('2d');