@@ -299,117 +320,19 @@ export default function Home() {
- {modalOpen && (
-
-
e.stopPropagation()}>
-
-
-
{editingRecord ? '编辑记录' : '新增记录'}
- {editingRecord ?
更新账目
: null}
-
-
-
- {formError &&
{formError}
}
-
-
-
- )}
+
);
}
diff --git a/frontend/pages/search.js b/frontend/pages/search.js
index a217552..966d790 100644
--- a/frontend/pages/search.js
+++ b/frontend/pages/search.js
@@ -1,17 +1,33 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useMemo } from 'react';
import { useRouter } from 'next/router';
-import { isLoggedIn, searchRecords } from '../lib/api';
+import { isLoggedIn, searchRecords, updateRecord, deleteRecord } 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';
export default function Search() {
const router = useRouter();
const { showToast } = useToast();
+ const confirm = useConfirm();
const [keyword, setKeyword] = useState('');
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [editingRecord, setEditingRecord] = useState(null);
+ const [formData, setFormData] = useState({
+ type: 'expense',
+ amount: '',
+ category: '',
+ date: '',
+ time: '00:00',
+ note: '',
+ });
+ const [formError, setFormError] = useState('');
+ const [submitting, setSubmitting] = useState(false);
+ const [deleting, setDeleting] = useState(false);
useEffect(() => {
if (!isLoggedIn()) {
@@ -38,19 +54,127 @@ export default function Search() {
}
};
+ const openEditModal = (record) => {
+ const [datePart, timePart] = record.date.split(' ');
+ setEditingRecord(record);
+ setFormData({
+ type: record.type,
+ amount: record.amount.toString(),
+ category: record.category,
+ date: datePart,
+ time: timePart || '00:00',
+ note: record.note || '',
+ });
+ setFormError('');
+ setModalOpen(true);
+ };
+
+ const closeModal = () => {
+ if (submitting || deleting) return;
+ setModalOpen(false);
+ setEditingRecord(null);
+ setFormError('');
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ if (!formData.amount || !formData.category || !formData.date) {
+ setFormError('请填写完整信息');
+ return;
+ }
+
+ setSubmitting(true);
+ setFormError('');
+ try {
+ const data = {
+ type: formData.type,
+ amount: parseFloat(formData.amount),
+ category: formData.category,
+ date: `${formData.date} ${formData.time || '00:00'}`,
+ note: formData.note,
+ };
+
+ // 先调用 API 保存到数据库
+ await updateRecord(editingRecord.id, data);
+
+ // 直接更新本地状态,避免重新获取
+ const updatedResults = results.map(r =>
+ r.id === editingRecord.id
+ ? { ...r, ...data, amount: parseFloat(data.amount), date: data.date }
+ : r
+ );
+ setResults(updatedResults);
+ showToast('记录已更新', { tone: 'success' });
+ closeModal();
+ } catch (err) {
+ setFormError(err.message);
+ showToast(err.message, { tone: 'error' });
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const handleDelete = async () => {
+ if (!editingRecord) return;
+ const shouldDelete = await confirm({
+ title: '删除这条记录?',
+ description: '删除后将无法恢复,请确认是否继续。',
+ confirmText: '删除',
+ });
+ if (!shouldDelete) return;
+
+ setDeleting(true);
+ try {
+ await deleteRecord(editingRecord.id);
+ // 直接从本地状态移除,避免重新获取
+ const filteredResults = results.filter(r => r.id !== editingRecord.id);
+ setResults(filteredResults);
+ showToast('记录已删除', { tone: 'success' });
+ closeModal();
+ } catch (err) {
+ setFormError(err.message);
+ showToast(err.message, { tone: 'error' });
+ } finally {
+ setDeleting(false);
+ }
+ };
+
const formatMoney = (num) => (num || 0).toFixed(2);
- const groupedResults = results.reduce((groups, record) => {
- const date = record.date.split(' ')[0];
- if (!groups[date]) groups[date] = [];
- groups[date].push(record);
- return groups;
- }, {});
+ const groupedResults = useMemo(() => {
+ return results.reduce((groups, record) => {
+ const date = record.date.split(' ')[0];
+ if (!groups[date]) groups[date] = [];
+ groups[date].push(record);
+ return groups;
+ }, {});
+ }, [results]);
- const sortedDates = Object.keys(groupedResults).sort((a, b) => b.localeCompare(a));
- const totalIncome = results.filter((record) => record.type === 'income').reduce((sum, record) => sum + record.amount, 0);
- const totalExpense = results.filter((record) => record.type === 'expense').reduce((sum, record) => sum + record.amount, 0);
- const totalBalance = totalIncome - totalExpense;
+ const sortedDates = useMemo(
+ () => Object.keys(groupedResults).sort((a, b) => b.localeCompare(a)),
+ [groupedResults]
+ );
+
+ const { totalIncome, totalExpense, totalBalance } = useMemo(() => {
+ let income = 0;
+ let expense = 0;
+ for (const record of results) {
+ if (record.type === 'income') income += record.amount;
+ else expense += record.amount;
+ }
+ return { totalIncome: income, totalExpense: expense, totalBalance: income - expense };
+ }, [results]);
+
+ // O(1) category icon lookup map
+ const categoryIconMap = useMemo(() => {
+ const icons = {};
+ for (const type of ['expense', 'income']) {
+ for (const cat of DEFAULT_CATEGORIES[type]) {
+ icons[`${type}:${cat.name}`] = cat.icon;
+ }
+ }
+ return icons;
+ }, []);
const actions = (
{groupedResults[date].map((record) => {
- const categories = DEFAULT_CATEGORIES[record.type];
- const cat = categories.find((item) => item.name === record.category) || { icon: '📝' };
+ const icon = categoryIconMap[`${record.type}:${record.category}`] || '📝';
return (
-