diff --git a/docs/superpowers/specs/2026-03-17-stats-category-filter-design.md b/docs/superpowers/specs/2026-03-17-stats-category-filter-design.md new file mode 100644 index 0000000..ccfe2f5 --- /dev/null +++ b/docs/superpowers/specs/2026-03-17-stats-category-filter-design.md @@ -0,0 +1,100 @@ +# 统计界面分类筛选功能设计 + +> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 在统计界面添加分类多选功能,支持按分类筛选查看支出/收入明细 + +**Architecture:** 在现有统计页面基础上,增加分类多选状态管理,在分类明细区域显示选中分类的标签chips,并过滤显示对应分类的数据 + +**Tech Stack:** React (Next.js), Chart.js + +--- + +## 需求概述 + +用户在统计界面可以多选分类,查看特定分类的支出/收入明细。 + +### 界面布局 + +``` +┌─────────────────────────────────┐ +│ ← 2026年3月 → │ +├─────────────────────────────────┤ +│ [周] [月] [年] │ +│ ┌─────────────────────────┐ │ +│ │ 收支趋势折线图 │ │ +│ └─────────────────────────┘ │ +├─────────────────────────────────┤ +│ [支出] [收入] │ +│ ┌─────────────────────────┐ │ +│ │ 支出 ¥3200 │ │ ← 切换到支出时显示支出总额 +│ ├─────────────────────────┤ │ +│ │ 餐饮 ████████ 500 │ │ +│ │ 交通 ████ 300 │ │ ← 分类明细 +│ │ 购物 ██ 150 │ │ +│ └─────────────────────────┘ │ +└─────────────────────────────────┘ +``` + +### 交互逻辑 + +1. **分类类型切换**:点击「支出」/「收入」tab,显示对应类型的总额 + 分类明细 +2. **分类多选**: + - 分类明细区域显示所有分类(可点击) + - 点击某个分类 → 添加到已选列表(显示为chip标签) + - 点击已选分类的 ✕ 按钮 → 从已选列表移除 +3. **数据过滤**: + - 没有选中任何分类 → 显示全部分类 + - 选中部分分类 → 只显示选中的分类 +4. **总额变化**:根据选中的分类,计算并显示对应分类的总金额 + +--- + +## 实现方案 + +### 需要修改的文件 + +- `frontend/pages/stats.js` - 主页面逻辑 + +### 状态管理 + +```javascript +// 新增状态 +const [selectedCategories, setSelectedCategories] = useState([]); // 已选中的分类数组 +``` + +### 逻辑实现 + +1. **分类点击处理**: + - 如果分类已在 selectedCategories 中,忽略 + - 否则添加到 selectedCategories + +2. **移除分类**: + - 从 selectedCategories 中过滤掉对应分类 + +3. **数据过滤**: + - 如果 selectedCategories 为空,显示全部 categoryData + - 否则过滤只显示 selectedCategories 中的分类 + +4. **总额计算**: + - 根据当前过滤后的数据计算总额 + +### UI 修改 + +1. **分类明细区域**: + - 在分类明细列表上方添加「总额显示」(如:支出 ¥3200) + - 将分类名称改为可点击的 chip 样式 + - 已选中的分类显示 ✕ 按钮 + +2. **移除饼图**:按照需求去掉饼图显示 + +--- + +## 验收标准 + +1. 切换「支出」/「收入」tab 时,正确显示对应类型的总额 +2. 点击分类可选中/取消选中,选中的分类显示为 chip 标签 +3. 分类明细只显示选中的分类(如果有选中) +4. 没有选中分类时,显示全部分类 +5. 总额根据选中的分类动态变化 +6. 页面响应流畅,无明显卡顿 diff --git a/frontend/hooks/useCategories.js b/frontend/hooks/useCategories.js new file mode 100644 index 0000000..21630f7 --- /dev/null +++ b/frontend/hooks/useCategories.js @@ -0,0 +1,45 @@ +import { useState, useEffect } from 'react'; +import { getCategories, isLoggedIn } from '../lib/api'; +import { DEFAULT_CATEGORIES } from '../lib/categories'; + +export function useCategories() { + const [categories, setCategories] = useState(DEFAULT_CATEGORIES); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (!isLoggedIn()) { + setLoading(false); + return; + } + + const loadCategories = async () => { + try { + const categoriesData = await getCategories(); + if (categoriesData) { + 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); + } finally { + setLoading(false); + } + }; + + loadCategories(); + }, []); + + return { categories, loading }; +} diff --git a/frontend/pages/recurring.js b/frontend/pages/recurring.js index dd4c8a1..b337c26 100644 --- a/frontend/pages/recurring.js +++ b/frontend/pages/recurring.js @@ -1,16 +1,17 @@ import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; import { isLoggedIn, getRecurringBills, createRecurringBill, updateRecurringBill, deleteRecurringBill, applyRecurringBill } from '../lib/api'; -import { DEFAULT_CATEGORIES } from '../lib/categories'; import AppShell from '../components/AppShell'; import { Icon } from '../components/icons'; import { useToast } from '../components/ToastProvider'; import { useConfirm } from '../components/ConfirmProvider'; +import { useCategories } from '../hooks/useCategories'; export default function Recurring() { const router = useRouter(); const { showToast } = useToast(); const { confirm } = useConfirm(); + const { categories } = useCategories(); const [bills, setBills] = useState([]); const [modalOpen, setModalOpen] = useState(false); const [editingBill, setEditingBill] = useState(null); @@ -197,8 +198,8 @@ export default function Recurring() {