feat: 统计页面添加分类筛选与搜索优化
- 统计页面添加分类多选筛选功能 - 点击分类可跳转到搜索页面查看详情 - 搜索页面添加 URL 同步与自动搜索 - 提取 useCategories hook 消除重复代码 - 修复自定义分类图标显示问题 - 添加 useMemo 优化计算性能 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b5f9a238bb
commit
a89d4f2402
@@ -1,17 +1,18 @@
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
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';
|
||||
import { useCategories } from '../hooks/useCategories';
|
||||
|
||||
export default function Search() {
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
const confirm = useConfirm();
|
||||
const { categories } = useCategories();
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [results, setResults] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -35,6 +36,28 @@ export default function Search() {
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
// 检查 URL 参数,自动搜索
|
||||
useEffect(() => {
|
||||
if (router.isReady && router.query.q) {
|
||||
const q = decodeURIComponent(router.query.q);
|
||||
setKeyword(q);
|
||||
// 自动搜索
|
||||
const doSearch = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await searchRecords(q);
|
||||
setResults(data);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
showToast(err.message, { tone: 'error' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
doSearch();
|
||||
}
|
||||
}, [router.isReady, router.query.q]);
|
||||
|
||||
const handleSearch = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!keyword.trim()) {
|
||||
@@ -42,6 +65,9 @@ export default function Search() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新 URL
|
||||
router.push(`/search?q=${encodeURIComponent(keyword)}`, undefined, { shallow: true });
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await searchRecords(keyword);
|
||||
@@ -142,6 +168,7 @@ export default function Search() {
|
||||
const formatMoney = (num) => (num || 0).toFixed(2);
|
||||
|
||||
const groupedResults = useMemo(() => {
|
||||
if (!Array.isArray(results)) return {};
|
||||
return results.reduce((groups, record) => {
|
||||
const date = record.date.split(' ')[0];
|
||||
if (!groups[date]) groups[date] = [];
|
||||
@@ -156,6 +183,7 @@ export default function Search() {
|
||||
);
|
||||
|
||||
const { totalIncome, totalExpense, totalBalance } = useMemo(() => {
|
||||
if (!Array.isArray(results)) return { totalIncome: 0, totalExpense: 0, totalBalance: 0 };
|
||||
let income = 0;
|
||||
let expense = 0;
|
||||
for (const record of results) {
|
||||
@@ -165,16 +193,16 @@ export default function Search() {
|
||||
return { totalIncome: income, totalExpense: expense, totalBalance: income - expense };
|
||||
}, [results]);
|
||||
|
||||
// O(1) category icon lookup map
|
||||
// O(1) category icon lookup map (includes custom categories)
|
||||
const categoryIconMap = useMemo(() => {
|
||||
const icons = {};
|
||||
for (const type of ['expense', 'income']) {
|
||||
for (const cat of DEFAULT_CATEGORIES[type]) {
|
||||
for (const cat of categories[type]) {
|
||||
icons[`${type}:${cat.name}`] = cat.icon;
|
||||
}
|
||||
}
|
||||
return icons;
|
||||
}, []);
|
||||
}, [categories]);
|
||||
|
||||
const actions = (
|
||||
<button type="button" className="icon-button" aria-label="设置" onClick={() => router.push('/settings')}>
|
||||
@@ -209,16 +237,16 @@ export default function Search() {
|
||||
|
||||
{results.length > 0 ? (
|
||||
<>
|
||||
<div className="search-summary">
|
||||
<div className="search-stat">
|
||||
<div className="stats-grid stats-grid--hero">
|
||||
<div className="stat-card">
|
||||
<span className="label">收入</span>
|
||||
<span className="value income">+{formatMoney(totalIncome)}</span>
|
||||
</div>
|
||||
<div className="search-stat">
|
||||
<div className="stat-card">
|
||||
<span className="label">支出</span>
|
||||
<span className="value expense">-{formatMoney(totalExpense)}</span>
|
||||
</div>
|
||||
<div className="search-stat">
|
||||
<div className="stat-card stat-card--balance">
|
||||
<span className="label">结余</span>
|
||||
<span className="value">{formatMoney(totalBalance)}</span>
|
||||
</div>
|
||||
@@ -283,7 +311,7 @@ export default function Search() {
|
||||
submitting={submitting}
|
||||
deleting={deleting}
|
||||
editingRecord={editingRecord}
|
||||
categories={DEFAULT_CATEGORIES}
|
||||
categories={categories}
|
||||
/>
|
||||
</AppShell>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user