feat: 删除亲情卡分类并优化搜索功能
- 移除亲情卡支出分类(前端+后端) - 搜索结果添加点击编辑功能 - 提取 RecordModal 组件复用(index.js/search.js) - 添加 useMemo 优化计算性能 - 优化类别查找为 O(1) 映射 - 添加删除分类接口输入验证 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
3347a256b2
commit
b5f9a238bb
+49
-126
@@ -1,9 +1,10 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRecord } from '../lib/api';
|
||||
import { isLoggedIn, getRecords, getStats, createRecord, updateRecord, deleteRecord, getCategories } from '../lib/api';
|
||||
import { DEFAULT_CATEGORIES } from '../lib/categories';
|
||||
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';
|
||||
|
||||
@@ -21,6 +22,7 @@ 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 [formData, setFormData] = useState({
|
||||
type: 'expense',
|
||||
amount: '',
|
||||
@@ -54,12 +56,24 @@ export default function Home() {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [recordsData, statsData] = await Promise.all([
|
||||
const [recordsData, statsData, categoriesData] = 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' });
|
||||
@@ -171,18 +185,26 @@ export default function Home() {
|
||||
}
|
||||
};
|
||||
|
||||
const groupedRecords = records.reduce((groups, record) => {
|
||||
const date = record.date.split(' ')[0];
|
||||
if (!groups[date]) groups[date] = [];
|
||||
groups[date].push(record);
|
||||
return groups;
|
||||
}, {});
|
||||
const groupedRecords = useMemo(() => {
|
||||
return records.reduce((groups, record) => {
|
||||
const date = record.date.split(' ')[0];
|
||||
if (!groups[date]) groups[date] = [];
|
||||
groups[date].push(record);
|
||||
return groups;
|
||||
}, {});
|
||||
}, [records]);
|
||||
|
||||
const sortedDates = Object.keys(groupedRecords).sort((a, b) => b.localeCompare(a));
|
||||
const sortedDates = useMemo(
|
||||
() => Object.keys(groupedRecords).sort((a, b) => b.localeCompare(a)),
|
||||
[groupedRecords]
|
||||
);
|
||||
|
||||
const getDayExpense = (date) => groupedRecords[date]
|
||||
.filter((record) => record.type === 'expense')
|
||||
.reduce((sum, record) => sum + record.amount, 0);
|
||||
const getDayExpense = (date) => {
|
||||
const dayRecords = groupedRecords[date] || [];
|
||||
return dayRecords
|
||||
.filter((record) => record.type === 'expense')
|
||||
.reduce((sum, record) => sum + record.amount, 0);
|
||||
};
|
||||
|
||||
const homeHeaderContent = (
|
||||
<div className="header-center-container">
|
||||
@@ -271,8 +293,7 @@ export default function Home() {
|
||||
<span className="day-expense">支出 {formatMoney(getDayExpense(date))}</span>
|
||||
</div>
|
||||
{groupedRecords[date].map((record) => {
|
||||
const categories = DEFAULT_CATEGORIES[record.type];
|
||||
const cat = categories.find((item) => item.name === record.category) || { icon: '📝' };
|
||||
const cat = categories[record.type]?.find((item) => item.name === record.category) || { icon: '📝' };
|
||||
return (
|
||||
<button key={record.id} type="button" className="record-item" onClick={() => openEditModal(record)}>
|
||||
<div className="record-left">
|
||||
@@ -299,117 +320,19 @@ export default function Home() {
|
||||
<Icon name="plus" size={24} />
|
||||
</button>
|
||||
|
||||
{modalOpen && (
|
||||
<div className="modal-overlay modal-overlay--fullscreen" onClick={closeModal}>
|
||||
<div className="modal modal--fullscreen" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="modal-header">
|
||||
<div>
|
||||
<p className="eyebrow">{editingRecord ? '编辑记录' : '新增记录'}</p>
|
||||
{editingRecord ? <h2>更新账目</h2> : null}
|
||||
</div>
|
||||
<button type="button" className="icon-button icon-button--ghost" aria-label="关闭弹窗" onClick={closeModal}>
|
||||
<Icon name="close" size={18} />
|
||||
</button>
|
||||
</div>
|
||||
{formError && <div className="error-msg">{formError}</div>}
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="type-switch">
|
||||
<button
|
||||
type="button"
|
||||
className={`type-btn expense ${formData.type === 'expense' ? 'active' : ''}`}
|
||||
onClick={() => setFormData({ ...formData, type: 'expense', category: '' })}
|
||||
disabled={submitting || deleting}
|
||||
>
|
||||
支出
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`type-btn income ${formData.type === 'income' ? 'active' : ''}`}
|
||||
onClick={() => setFormData({ ...formData, type: 'income', category: '' })}
|
||||
disabled={submitting || deleting}
|
||||
>
|
||||
收入
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="record-amount">金额</label>
|
||||
<input
|
||||
id="record-amount"
|
||||
type="number"
|
||||
className="amount-input"
|
||||
value={formData.amount}
|
||||
onChange={(e) => setFormData({ ...formData, amount: e.target.value })}
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
required
|
||||
disabled={submitting || deleting}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>分类</label>
|
||||
<div className="category-grid">
|
||||
{DEFAULT_CATEGORIES[formData.type].map((cat) => (
|
||||
<button
|
||||
key={cat.name}
|
||||
type="button"
|
||||
className={`category-btn ${formData.category === cat.name ? 'active' : ''}`}
|
||||
onClick={() => setFormData({ ...formData, category: cat.name })}
|
||||
disabled={submitting || deleting}
|
||||
>
|
||||
<span className="icon">{cat.icon}</span>
|
||||
<span>{cat.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>日期</label>
|
||||
<div className="datetime-picker">
|
||||
<input
|
||||
type="date"
|
||||
value={formData.date}
|
||||
onChange={(e) => setFormData({ ...formData, date: e.target.value })}
|
||||
required
|
||||
disabled={submitting || deleting}
|
||||
/>
|
||||
<input
|
||||
type="time"
|
||||
value={formData.time || '00:00'}
|
||||
onChange={(e) => setFormData({ ...formData, time: e.target.value })}
|
||||
disabled={submitting || deleting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="record-note">备注(可选)</label>
|
||||
<input
|
||||
id="record-note"
|
||||
type="text"
|
||||
value={formData.note}
|
||||
onChange={(e) => setFormData({ ...formData, note: e.target.value })}
|
||||
placeholder="添加备注..."
|
||||
disabled={submitting || deleting}
|
||||
/>
|
||||
</div>
|
||||
<div className="modal-actions">
|
||||
<button type="submit" className="btn btn-primary" disabled={submitting || deleting}>
|
||||
{submitting ? (
|
||||
<span className="button-content">
|
||||
<Icon name="spinner" size={16} className="is-spinning" />
|
||||
保存中...
|
||||
</span>
|
||||
) : '保存'}
|
||||
</button>
|
||||
{editingRecord && (
|
||||
<button type="button" className="btn btn-danger" onClick={handleDelete} disabled={submitting || deleting}>
|
||||
{deleting ? '删除中...' : '删除'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<RecordModal
|
||||
isOpen={modalOpen}
|
||||
onClose={closeModal}
|
||||
onSubmit={handleSubmit}
|
||||
onDelete={handleDelete}
|
||||
formData={formData}
|
||||
setFormData={setFormData}
|
||||
formError={formError}
|
||||
submitting={submitting}
|
||||
deleting={deleting}
|
||||
editingRecord={editingRecord}
|
||||
categories={categories}
|
||||
/>
|
||||
</AppShell>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user