feat: 前端UI标题栏优化与配置更新
- 更新.gitignore,添加frontend/.next/忽略 - 优化RecordModal、index、recurring、search、stats页面标题栏样式 - 新增frontend/lib/utils.js工具函数 - 更新useCategories.js钩子 - 添加代码优化计划文档 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+33
-27
@@ -1,12 +1,13 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRecord, getCategories } from '../lib/api';
|
||||
import { DEFAULT_CATEGORIES } from '../lib/categories';
|
||||
import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRecord } from '../lib/api';
|
||||
import AppShell from '../components/AppShell';
|
||||
import { Icon } from '../components/icons';
|
||||
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';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
@@ -22,7 +23,14 @@ export default function Home() {
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [formError, setFormError] = useState('');
|
||||
const [categories, setCategories] = useState(DEFAULT_CATEGORIES);
|
||||
const { categories, error: categoriesError } = useCategories();
|
||||
|
||||
// 显示分类加载错误
|
||||
useEffect(() => {
|
||||
if (categoriesError) {
|
||||
showToast('分类加载失败', { tone: 'error' });
|
||||
}
|
||||
}, [categoriesError]);
|
||||
const [formData, setFormData] = useState({
|
||||
type: 'expense',
|
||||
amount: '',
|
||||
@@ -40,9 +48,6 @@ export default function Home() {
|
||||
loadData();
|
||||
}, [router, currentMonth, typeFilter]);
|
||||
|
||||
const getMonthStr = (date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
|
||||
const formatMoney = (num) => (num || 0).toFixed(2);
|
||||
const formatMonth = (date) => `${date.getFullYear()}年${date.getMonth() + 1}月`;
|
||||
|
||||
const resetForm = () => ({
|
||||
type: 'expense',
|
||||
@@ -53,27 +58,26 @@ export default function Home() {
|
||||
note: '',
|
||||
});
|
||||
|
||||
// Memoized category lookup map for O(1) icon retrieval
|
||||
const categoryIconMap = useMemo(() => {
|
||||
const icons = {};
|
||||
for (const type of ['expense', 'income']) {
|
||||
for (const cat of categories[type] || []) {
|
||||
icons[`${type}:${cat.name}`] = cat.icon;
|
||||
}
|
||||
}
|
||||
return icons;
|
||||
}, [categories]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [recordsData, statsData, categoriesData] = await Promise.all([
|
||||
const [recordsData, statsData] = await Promise.all([
|
||||
getRecords(getMonthStr(currentMonth), typeFilter),
|
||||
getStats(getMonthStr(currentMonth)),
|
||||
getCategories(),
|
||||
]);
|
||||
setRecords(recordsData);
|
||||
setStats(statsData);
|
||||
// 合并默认分类和自定义分类
|
||||
setCategories({
|
||||
expense: [
|
||||
...DEFAULT_CATEGORIES.expense,
|
||||
...(categoriesData.expense || []).filter(c => !DEFAULT_CATEGORIES.expense.some(d => d.name === c.name))
|
||||
],
|
||||
income: [
|
||||
...DEFAULT_CATEGORIES.income,
|
||||
...(categoriesData.income || []).filter(c => !DEFAULT_CATEGORIES.income.some(d => d.name === c.name))
|
||||
],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast(err.message, { tone: 'error' });
|
||||
@@ -106,14 +110,14 @@ export default function Home() {
|
||||
};
|
||||
|
||||
const openEditModal = (record) => {
|
||||
const [datePart, timePart] = record.date.split(' ');
|
||||
const { datePart, timePart } = parseDateTime(record.date);
|
||||
setEditingRecord(record);
|
||||
setFormData({
|
||||
type: record.type,
|
||||
amount: record.amount.toString(),
|
||||
category: record.category,
|
||||
date: datePart,
|
||||
time: timePart || '00:00',
|
||||
time: timePart,
|
||||
note: record.note || '',
|
||||
});
|
||||
setFormError('');
|
||||
@@ -146,14 +150,16 @@ export default function Home() {
|
||||
};
|
||||
|
||||
if (editingRecord) {
|
||||
const updated = { ...editingRecord, ...data, amount: parseFloat(data.amount) };
|
||||
await updateRecord(editingRecord.id, data);
|
||||
setRecords(prev => prev.map(r => r.id === editingRecord.id ? updated : r));
|
||||
showToast('记录已更新', { tone: 'success' });
|
||||
} else {
|
||||
await createRecord(data);
|
||||
const newRecord = await createRecord(data);
|
||||
setRecords(prev => [...prev, newRecord]);
|
||||
showToast('记录已添加', { tone: 'success' });
|
||||
}
|
||||
closeModal();
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setFormError(err.message);
|
||||
showToast(err.message, { tone: 'error' });
|
||||
@@ -174,9 +180,9 @@ export default function Home() {
|
||||
setDeleting(true);
|
||||
try {
|
||||
await deleteRecord(editingRecord.id);
|
||||
setRecords(prev => prev.filter(r => r.id !== editingRecord.id));
|
||||
closeModal();
|
||||
showToast('记录已删除', { tone: 'success' });
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
setFormError(err.message);
|
||||
showToast(err.message, { tone: 'error' });
|
||||
@@ -187,7 +193,7 @@ export default function Home() {
|
||||
|
||||
const groupedRecords = useMemo(() => {
|
||||
return records.reduce((groups, record) => {
|
||||
const date = record.date.split(' ')[0];
|
||||
const date = getDatePart(record.date);
|
||||
if (!groups[date]) groups[date] = [];
|
||||
groups[date].push(record);
|
||||
return groups;
|
||||
@@ -293,11 +299,11 @@ export default function Home() {
|
||||
<span className="day-expense">支出 {formatMoney(getDayExpense(date))}</span>
|
||||
</div>
|
||||
{groupedRecords[date].map((record) => {
|
||||
const cat = categories[record.type]?.find((item) => item.name === record.category) || { icon: '📝' };
|
||||
const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝';
|
||||
return (
|
||||
<button key={record.id} type="button" className="record-item" onClick={() => openEditModal(record)}>
|
||||
<div className="record-left">
|
||||
<div className="record-icon">{cat.icon}</div>
|
||||
<div className="record-icon">{icon}</div>
|
||||
<div className="record-info">
|
||||
<div className="record-category">{record.category}</div>
|
||||
{record.note && <div className="record-note">{record.note}</div>}
|
||||
|
||||
Reference in New Issue
Block a user