fix
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 26s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 26s
Docker Build & Deploy / Deploy to Production (push) Successful in 8s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 2s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
@@ -58,7 +58,8 @@ const cachedViews = ref([
|
||||
'StatisticsView', // 统计页面
|
||||
'StatisticsV2View', // 统计V2页面
|
||||
'BalanceView', // 账单页面
|
||||
'BudgetView' // 预算页面
|
||||
'BudgetView', // 预算页面
|
||||
'BudgetV2View' // 预算V2页面
|
||||
])
|
||||
|
||||
const updateVh = () => {
|
||||
@@ -156,7 +157,7 @@ const showNav = computed(() => {
|
||||
'/', '/statistics-v2',
|
||||
'/calendar', '/calendar-v2',
|
||||
'/balance', '/message',
|
||||
'/budget', '/setting'
|
||||
'/budget', '/budget-v2', '/setting'
|
||||
].includes(route.path)
|
||||
})
|
||||
|
||||
|
||||
96
Web/src/components/BudgetTypeTabs.vue
Normal file
96
Web/src/components/BudgetTypeTabs.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<!--
|
||||
预算类型选择器组件 (Budget Type Tabs)
|
||||
用于预算页面的"支出/收入/计划"类型切换
|
||||
样式与 TimePeriodTabs 保持一致(segmented control 风格)
|
||||
支持亮色和暗色两种主题
|
||||
-->
|
||||
<template>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="segmented-control">
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 0 }"
|
||||
@click="$emit('change', 0)"
|
||||
>
|
||||
<span class="tab-text">支出</span>
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 1 }"
|
||||
@click="$emit('change', 1)"
|
||||
>
|
||||
<span class="tab-text">收入</span>
|
||||
</div>
|
||||
<div
|
||||
class="tab-item"
|
||||
:class="{ active: activeTab === 2 }"
|
||||
@click="$emit('change', 2)"
|
||||
>
|
||||
<span class="tab-text">计划</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
activeTab: {
|
||||
type: Number,
|
||||
required: true,
|
||||
validator: (value) => [0, 1, 2].includes(value)
|
||||
}
|
||||
})
|
||||
|
||||
defineEmits(['change'])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import '@/assets/theme.css';
|
||||
|
||||
.tabs-wrapper {
|
||||
padding: var(--spacing-sm) var(--spacing-xl);
|
||||
|
||||
.segmented-control {
|
||||
display: flex;
|
||||
background: var(--segmented-bg);
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
height: 40px;
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
background: transparent;
|
||||
|
||||
&.active {
|
||||
background: var(--segmented-active-bg);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
|
||||
.tab-text {
|
||||
color: var(--text-primary);
|
||||
font-weight: var(--font-bold);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.active):hover {
|
||||
background: rgba(128, 128, 128, 0.1);
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-family: var(--font-primary);
|
||||
font-size: var(--font-md);
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-secondary);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -44,7 +44,7 @@ const props = defineProps({
|
||||
{ name: 'calendar', label: '日历', icon: 'notes', path: '/calendar' },
|
||||
{ name: 'statistics', label: '统计', icon: 'chart-trending-o', path: '/' },
|
||||
{ name: 'balance', label: '账单', icon: 'balance-list', path: '/balance' },
|
||||
{ name: 'budget', label: '预算', icon: 'bill-o', path: '/budget' },
|
||||
{ name: 'budget', label: '预算', icon: 'bill-o', path: '/budget-v2' },
|
||||
{ name: 'setting', label: '设置', icon: 'setting', path: '/setting' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -107,6 +107,12 @@ const router = createRouter({
|
||||
component: () => import('../views/BudgetView.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/budget-v2',
|
||||
name: 'budget-v2',
|
||||
component: () => import('../views/budgetV2/Index.vue'),
|
||||
meta: { requiresAuth: true, keepAlive: true }
|
||||
},
|
||||
{
|
||||
path: '/scheduled-tasks',
|
||||
name: 'scheduled-tasks',
|
||||
|
||||
1005
Web/src/views/budgetV2/Index.vue
Normal file
1005
Web/src/views/budgetV2/Index.vue
Normal file
File diff suppressed because it is too large
Load Diff
40
Web/src/views/budgetV2/modules/ExpenseBudgetContent.vue
Normal file
40
Web/src/views/budgetV2/modules/ExpenseBudgetContent.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<!-- 统计图表分析 -->
|
||||
<BudgetChartAnalysis
|
||||
:overall-stats="stats"
|
||||
:budgets="budgets"
|
||||
:active-tab="0"
|
||||
:selected-date="selectedDate"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BudgetChartAnalysis from '@/components/Budget/BudgetChartAnalysis.vue'
|
||||
|
||||
// Props
|
||||
defineProps({
|
||||
budgets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
stats: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uncoveredCategories: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedDate: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
defineEmits(['delete', 'edit'])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 移除不需要的样式 */
|
||||
</style>
|
||||
40
Web/src/views/budgetV2/modules/IncomeBudgetContent.vue
Normal file
40
Web/src/views/budgetV2/modules/IncomeBudgetContent.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<!-- 统计图表分析 -->
|
||||
<BudgetChartAnalysis
|
||||
:overall-stats="stats"
|
||||
:budgets="budgets"
|
||||
:active-tab="1"
|
||||
:selected-date="selectedDate"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BudgetChartAnalysis from '@/components/Budget/BudgetChartAnalysis.vue'
|
||||
|
||||
// Props
|
||||
defineProps({
|
||||
budgets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
stats: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
uncoveredCategories: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
selectedDate: {
|
||||
type: Date,
|
||||
default: () => new Date()
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
defineEmits(['delete', 'edit'])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 移除不需要的样式 */
|
||||
</style>
|
||||
222
Web/src/views/budgetV2/modules/SavingsBudgetContent.vue
Normal file
222
Web/src/views/budgetV2/modules/SavingsBudgetContent.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<!-- 存款计划列表 -->
|
||||
<div class="budget-list">
|
||||
<template v-if="budgets?.length > 0">
|
||||
<BudgetCard
|
||||
v-for="budget in budgets"
|
||||
:key="budget.id"
|
||||
:budget="budget"
|
||||
:progress-color="getProgressColor(budget)"
|
||||
:percent-class="{ income: budget.current / budget.limit >= 1 }"
|
||||
:period-label="getPeriodLabel(budget.type)"
|
||||
style="margin: 0 12px 12px"
|
||||
>
|
||||
<template #amount-info>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
已存
|
||||
</div>
|
||||
<div class="value income">
|
||||
¥{{ formatMoney(budget.current) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
计划存款
|
||||
</div>
|
||||
<div class="value">
|
||||
¥{{ formatMoney(budget.limit) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="label">
|
||||
还差
|
||||
</div>
|
||||
<div class="value expense">
|
||||
¥{{ formatMoney(Math.max(0, budget.limit - budget.current)) }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="card-footer-actions">
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow-left"
|
||||
plain
|
||||
type="primary"
|
||||
@click.stop="$emit('savings-nav', budget, -1)"
|
||||
/>
|
||||
<span class="current-date-label">
|
||||
{{ getSavingsDateLabel(budget) }}
|
||||
</span>
|
||||
<van-button
|
||||
size="small"
|
||||
icon="arrow"
|
||||
plain
|
||||
type="primary"
|
||||
icon-position="right"
|
||||
:disabled="disabledSavingsNextNav(budget)"
|
||||
@click.stop="$emit('savings-nav', budget, 1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</BudgetCard>
|
||||
</template>
|
||||
<van-empty
|
||||
v-else
|
||||
description="暂无存款计划"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import BudgetCard from '@/components/Budget/BudgetCard.vue'
|
||||
import { BudgetPeriodType } from '@/constants/enums'
|
||||
|
||||
// Props
|
||||
defineProps({
|
||||
budgets: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
defineEmits(['savings-nav'])
|
||||
|
||||
// 辅助函数
|
||||
const formatMoney = (val) => {
|
||||
return parseFloat(val || 0).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 0
|
||||
})
|
||||
}
|
||||
|
||||
const getPeriodLabel = (type) => {
|
||||
if (type === BudgetPeriodType.Month) {
|
||||
return '月度'
|
||||
}
|
||||
if (type === BudgetPeriodType.Year) {
|
||||
return '年度'
|
||||
}
|
||||
return '周期'
|
||||
}
|
||||
|
||||
const getSavingsDateLabel = (budget) => {
|
||||
if (!budget.periodStart) {
|
||||
return ''
|
||||
}
|
||||
const date = new Date(budget.periodStart)
|
||||
if (budget.type === BudgetPeriodType.Year) {
|
||||
return `${date.getFullYear()}年`
|
||||
} else {
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月`
|
||||
}
|
||||
}
|
||||
|
||||
const disabledSavingsNextNav = (budget) => {
|
||||
if (!budget.periodStart) {
|
||||
return true
|
||||
}
|
||||
const date = new Date(budget.periodStart)
|
||||
const now = new Date()
|
||||
if (budget.type === BudgetPeriodType.Year) {
|
||||
return date.getFullYear() === now.getFullYear()
|
||||
} else {
|
||||
return date.getFullYear() === now.getFullYear() && date.getMonth() === now.getMonth()
|
||||
}
|
||||
}
|
||||
|
||||
const getProgressColor = (budget) => {
|
||||
if (!budget.limit || budget.limit === 0) {
|
||||
return 'var(--van-primary-color)'
|
||||
}
|
||||
|
||||
const ratio = Math.min(Math.max(budget.current / budget.limit, 0), 1)
|
||||
|
||||
const interpolate = (start, end, t) => {
|
||||
return Math.round(start + (end - start) * t)
|
||||
}
|
||||
|
||||
const getGradientColor = (value, stops) => {
|
||||
let startStop = stops[0]
|
||||
let endStop = stops[stops.length - 1]
|
||||
|
||||
for (let i = 0; i < stops.length - 1; i++) {
|
||||
if (value >= stops[i].p && value <= stops[i + 1].p) {
|
||||
startStop = stops[i]
|
||||
endStop = stops[i + 1]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const range = endStop.p - startStop.p
|
||||
const t = (value - startStop.p) / range
|
||||
|
||||
const r = interpolate(startStop.c.r, endStop.c.r, t)
|
||||
const g = interpolate(startStop.c.g, endStop.c.g, t)
|
||||
const b = interpolate(startStop.c.b, endStop.c.b, t)
|
||||
|
||||
return `rgb(${r}, ${g}, ${b})`
|
||||
}
|
||||
|
||||
const stops = [
|
||||
{ p: 0, c: { r: 245, g: 34, b: 45 } },
|
||||
{ p: 0.45, c: { r: 255, g: 204, b: 204 } },
|
||||
{ p: 0.5, c: { r: 240, g: 242, b: 245 } },
|
||||
{ p: 0.55, c: { r: 186, g: 231, b: 255 } },
|
||||
{ p: 1, c: { r: 24, g: 144, b: 255 } }
|
||||
]
|
||||
|
||||
return getGradientColor(ratio, stops)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.budget-list {
|
||||
padding-top: 8px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.card-footer-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--van-border-color);
|
||||
}
|
||||
|
||||
.current-date-label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: var(--van-text-color);
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
font-size: 12px;
|
||||
color: var(--van-text-color-2);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
font-family: DIN Alternate, system-ui;
|
||||
}
|
||||
|
||||
.info-item .value.expense {
|
||||
color: var(--van-danger-color);
|
||||
}
|
||||
|
||||
.info-item .value.income {
|
||||
color: var(--van-success-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-12
|
||||
223
openspec/changes/archive/2026-02-13-budget-page-v2/design.md
Normal file
223
openspec/changes/archive/2026-02-13-budget-page-v2/design.md
Normal file
@@ -0,0 +1,223 @@
|
||||
## Context
|
||||
|
||||
当前预算页面(`BudgetView.vue`)是一个 1000+ 行的单文件组件,存在以下问题:
|
||||
- 与 calendarV2、statisticsV2 风格不一致(页头和时间段切换设计不同)
|
||||
- 数据加载逻辑复杂且存在 bug(未覆盖分类、存款计划切换、日期同步问题)
|
||||
- 组件职责不清晰,难以维护
|
||||
|
||||
参考其他 v2 页面的经验,我们需要重构预算页面以实现:
|
||||
1. 统一的页头设计(DateSelectHeader)
|
||||
2. 模块化的组件结构(主页面 + 子模块)
|
||||
3. 清晰的数据流和状态管理
|
||||
4. 简洁高效的用户体验(月度视图为主)
|
||||
|
||||
**技术栈约束**:
|
||||
- Vue 3 Composition API + `<script setup>`
|
||||
- Vant UI 组件库
|
||||
- 现有的 `@/api/budget` 接口(不修改后端)
|
||||
- 复用现有的 DateSelectHeader 组件
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 创建简洁高效的预算 v2 页面
|
||||
- 使用统一的 DateSelectHeader 组件
|
||||
- 修复现有 bug(数据加载、日期同步、存款计划切换)
|
||||
- 提升代码可维护性(拆分子模块,清晰职责)
|
||||
- 保持所有现有业务功能(支出/收入/计划预算管理)
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改后端 API(完全前端重构)
|
||||
- 不改变预算业务逻辑(仅重构实现)
|
||||
- 不添加新的预算功能(如预算模板、智能推荐等)
|
||||
- 不添加周/年视图(保持月度视图为主)
|
||||
- 不删除旧的 BudgetView.vue(保留作为回退方案,后续独立 PR 删除)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 页面布局结构:简洁的两层布局
|
||||
|
||||
**决策**:采用简洁的两层布局结构
|
||||
```
|
||||
- DateSelectHeader (年月选择器 + 左右箭头 + 通知图标)
|
||||
- van-tabs (支出/收入/计划切换)
|
||||
- 可滚动内容区域
|
||||
- 下拉刷新
|
||||
- 预算内容(统计卡片、图表、列表)
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- ✅ 简洁明了,减少复杂度
|
||||
- ✅ 预算管理以月度为主,不需要周/年视图
|
||||
- ✅ 与现有预算页面的逻辑一致,降低用户学习成本
|
||||
|
||||
**替代方案**:
|
||||
- ❌ 引入 TimePeriodTabs:增加复杂度,预算管理不需要多时间段
|
||||
- ❌ 保留当前布局:无法实现风格统一和 bug 修复
|
||||
|
||||
### 2. 组件拆分策略:主页面 + 子模块
|
||||
|
||||
**决策**:创建 `budgetV2/Index.vue` 作为主页面,按业务功能拆分子模块
|
||||
```
|
||||
budgetV2/
|
||||
├── Index.vue # 主页面(布局、时间段切换、tabs)
|
||||
└── modules/
|
||||
├── ExpenseBudgetContent.vue # 支出预算内容(统计 + 列表)
|
||||
├── IncomeBudgetContent.vue # 收入预算内容(统计 + 列表)
|
||||
└── SavingsBudgetContent.vue # 存款计划内容(列表 + 日期切换)
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- ✅ 主页面职责清晰(只负责布局和状态协调)
|
||||
- ✅ 子模块独立开发和测试
|
||||
- ✅ 参考 calendarV2 的 modules 模式
|
||||
|
||||
**替代方案**:
|
||||
- ❌ 单文件组件:难以维护(当前问题)
|
||||
- ❌ 更细粒度拆分(如 ExpenseStats.vue, ExpenseList.vue):过度设计,增加文件数量
|
||||
|
||||
### 3. 数据加载策略:月度数据为主
|
||||
|
||||
**决策**:专注于月度数据加载,在主页面集中管理数据加载逻辑
|
||||
```javascript
|
||||
// Index.vue
|
||||
const currentDate = ref(new Date()) // 当前年月
|
||||
const activeTab = ref(BudgetCategory.Expense) // 当前业务 tab
|
||||
|
||||
// 统一数据加载函数(月度)
|
||||
const loadBudgetData = async () => {
|
||||
const year = currentDate.value.getFullYear()
|
||||
const month = currentDate.value.getMonth() + 1
|
||||
// 调用 getBudgetList, getCategoryStats 等月度 API
|
||||
}
|
||||
|
||||
// 子模块通过 props 接收数据
|
||||
<ExpenseBudgetContent :budgets="expenseBudgets" :stats="expenseStats" />
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- ✅ 简化逻辑,避免周/年数据的复杂处理
|
||||
- ✅ 预算管理以月度为核心
|
||||
- ✅ 统一的错误处理和加载状态
|
||||
|
||||
**替代方案**:
|
||||
- ❌ 支持多时间段:增加复杂度,预算场景不需要
|
||||
- ❌ 子模块独立加载数据:重复逻辑,难以同步
|
||||
|
||||
### 4. 存款计划特殊处理:独立的日期切换逻辑
|
||||
|
||||
**决策**:存款计划保留现有的独立日期切换功能(卡片底部的前后箭头)
|
||||
- 不受页头的 `currentDate` 影响
|
||||
- 每个存款计划独立维护自己的 `periodStart` 状态
|
||||
- 通过 `getSavingsBudget(year, month, type)` 切换日期
|
||||
|
||||
**理由**:
|
||||
- ✅ 符合业务需求(存款计划可能需要查看历史数据)
|
||||
- ✅ 保持现有功能不变
|
||||
|
||||
**风险**:
|
||||
- ⚠️ 两套日期状态可能造成用户困惑
|
||||
- **缓解措施**:在 UI 上明确区分(页头日期用于支出/收入,卡片底部日期用于存款计划)
|
||||
|
||||
### 5. 路由策略:创建新路由,保留旧路由
|
||||
|
||||
**决策**:
|
||||
- 创建新路由 `/budget-v2`(指向 `budgetV2/Index.vue`)
|
||||
- 保留旧路由 `/budget`(指向 `BudgetView.vue`)
|
||||
- 在导航中将"预算"菜单指向 `/budget-v2`
|
||||
|
||||
**理由**:
|
||||
- ✅ 支持灰度发布(可以快速回退)
|
||||
- ✅ 便于对比测试
|
||||
|
||||
**迁移计划**:
|
||||
- 第一阶段:创建新路由,灰度测试
|
||||
- 第二阶段:确认稳定后,删除旧路由和旧页面
|
||||
|
||||
### 6. 组件复用策略
|
||||
|
||||
**决策**:最大化复用现有组件,只在必要时调整
|
||||
- ✅ 直接复用:DateSelectHeader(月度模式)、BudgetEditPopup、SavingsConfigPopup、BudgetCard、BudgetChartAnalysis
|
||||
- ⚠️ 不使用 TimePeriodTabs(简化设计)
|
||||
|
||||
**理由**:
|
||||
- 减少重复代码
|
||||
- 保持 UI 一致性
|
||||
- 简化维护
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### Risk 1: 月度数据加载的稳定性
|
||||
**风险**:数据加载和状态管理需要正确处理各种边界情况
|
||||
→ **缓解措施**:
|
||||
- 参考现有 BudgetView 的数据加载逻辑
|
||||
- 编写单元测试覆盖边界情况
|
||||
- 详细的代码注释
|
||||
|
||||
### Risk 2: 存款计划的双日期状态可能造成用户困惑
|
||||
**风险**:页头日期和存款计划的独立日期切换可能让用户困惑
|
||||
→ **缓解措施**:
|
||||
- 在 UI 上明确区分(页头日期影响支出/收入,存款计划有独立切换)
|
||||
- 添加帮助提示或引导
|
||||
|
||||
### Risk 3: 旧预算页面的兼容性
|
||||
**风险**:用户习惯了旧页面的交互方式,可能不适应新页面
|
||||
→ **缓解措施**:
|
||||
- 保留旧路由作为回退方案
|
||||
- 收集用户反馈,快速迭代
|
||||
|
||||
### Risk 4: API 兼容性
|
||||
**风险**:现有 API 可能存在未预见的边界情况
|
||||
→ **缓解措施**:
|
||||
- 充分测试所有 API 调用
|
||||
- 添加完善的错误处理
|
||||
|
||||
### Trade-off 1: 新旧页面并存增加维护成本
|
||||
**权衡**:保留旧页面作为回退方案,但会增加短期维护成本
|
||||
→ **接受理由**:
|
||||
- 灰度发布的安全性更重要
|
||||
- 确认稳定后会删除旧页面
|
||||
|
||||
### Trade-off 2: 功能简化
|
||||
**权衡**:不支持周/年视图,只支持月度
|
||||
→ **接受理由**:
|
||||
- 预算管理以月度为核心,周/年视图使用频率低
|
||||
- 简化设计,降低复杂度和维护成本
|
||||
- 如果后续有需求,可以独立迭代
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### 阶段 1:开发和本地测试(1 天)
|
||||
1. 创建 `budgetV2/Index.vue` 和子模块
|
||||
2. 实现基础布局(DateSelectHeader + 业务 tabs)
|
||||
3. 实现支出/收入/存款三个 tabs 的内容(月度数据)
|
||||
4. 本地测试所有功能(重点测试数据加载和日期切换)
|
||||
|
||||
### 阶段 2:灰度发布(1 周)
|
||||
1. 添加新路由 `/budget-v2`
|
||||
2. 在导航中将"预算"菜单指向新路由
|
||||
3. 保留旧路由 `/budget` 作为回退
|
||||
4. 收集用户反馈,修复 bug
|
||||
|
||||
### 阶段 3:稳定后清理(独立 PR)
|
||||
1. 确认新页面稳定后,删除旧路由
|
||||
2. 删除 `BudgetView.vue`
|
||||
3. 清理相关的测试代码
|
||||
|
||||
### Rollback 策略
|
||||
如果新页面出现严重 bug:
|
||||
1. 在导航中将"预算"菜单改回 `/budget`
|
||||
2. 修复 bug 后再次切换到 `/budget-v2`
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **BudgetChartAnalysis 组件是否需要调整?**
|
||||
- 需要评估当前组件是否完全满足月度数据展示需求
|
||||
- 是否需要调整图表样式或数据格式
|
||||
|
||||
2. **未覆盖分类的展示位置?**
|
||||
- 当前在页头右侧有警告图标
|
||||
- 新布局中是否保留此功能?位置在哪里?
|
||||
|
||||
3. **是否需要支持左右滑动切换月份?**
|
||||
- 是否需要与其他 v2 页面保持一致?
|
||||
@@ -0,0 +1,55 @@
|
||||
## Why
|
||||
|
||||
当前预算页面(BudgetView.vue)存在多个问题:
|
||||
1. **风格不一致**:与其他 v2 页面(calendarV2、statisticsV2)的页头设计不一致,使用的是自定义的 `van-nav-bar` 而非统一的 `DateSelectHeader` 组件
|
||||
2. **代码质量问题**:页面存在多个 bug,包括状态管理混乱、数据刷新逻辑不清晰、存款计划日期切换不稳定等问题
|
||||
3. **维护困难**:代码结构复杂(1000+ 行),组件职责不清晰,难以维护和扩展
|
||||
|
||||
需要重写预算页面以解决这些问题,并保持页面简洁高效。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **页头重构**:使用 `DateSelectHeader` 组件替代当前的 `van-nav-bar`,实现年月选择器(支持左右箭头切换月份)
|
||||
- **业务 tabs 布局调整**:将"支出/收入/计划"tabs 紧随页头之后,保持原有业务逻辑
|
||||
- **预算内容重构**:
|
||||
- 保留现有的预算统计、分类列表、图表分析等核心功能
|
||||
- 重构数据加载逻辑,消除现有 bug(未覆盖分类加载、存款计划切换、日期同步等问题)
|
||||
- 优化组件结构,将复杂逻辑拆分为可复用的子模块
|
||||
- 改善状态管理,统一数据流
|
||||
- **统一交互体验**:
|
||||
- 实现左右滑动切换月份
|
||||
- 统一下拉刷新行为
|
||||
- 统一加载状态和错误处理
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `budget-v2-page-layout`: 新的预算 v2 页面布局,包含 DateSelectHeader + 业务 tabs + 内容区域
|
||||
- `budget-v2-data-loading`: 重构后的预算数据加载逻辑,消除现有 bug
|
||||
|
||||
### Modified Capabilities
|
||||
_无_
|
||||
|
||||
## Impact
|
||||
|
||||
**前端影响**:
|
||||
- **新增文件**:
|
||||
- `Web/src/views/budgetV2/Index.vue` - 新的预算 v2 主页面
|
||||
- `Web/src/views/budgetV2/modules/` - 预算子模块(图表、列表、编辑等)
|
||||
- **修改文件**:
|
||||
- `Web/src/router/index.js` - 添加新路由或更新现有预算路由
|
||||
- 可能需要调整现有的 `@/components/Budget/` 下的组件以适配新结构
|
||||
- **复用组件**:
|
||||
- `@/components/DateSelectHeader.vue` - 页头年月选择器
|
||||
- `@/components/Budget/BudgetCard.vue` - 预算卡片
|
||||
- `@/components/Budget/BudgetEditPopup.vue` - 预算编辑弹窗
|
||||
- `@/components/Budget/SavingsConfigPopup.vue` - 储蓄配置弹窗
|
||||
- `@/components/Budget/BudgetChartAnalysis.vue` - 预算图表分析
|
||||
|
||||
**后端影响**:
|
||||
- 无需修改后端 API(复用现有的预算相关接口)
|
||||
|
||||
**依赖关系**:
|
||||
- 依赖现有的 `@/api/budget` 模块提供的 API
|
||||
- 依赖 `@/stores/` 中的相关状态管理(如有)
|
||||
- 依赖 Vant UI 组件库
|
||||
@@ -0,0 +1,149 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 支持周/月/年三种时间段切换
|
||||
预算页面 SHALL 支持周度、月度和年度三种时间段切换,每种时间段加载对应的预算数据。
|
||||
|
||||
#### Scenario: 切换到周视图
|
||||
- **WHEN** 用户切换到周视图
|
||||
- **THEN** 页面加载当前周的预算数据(周一到周日)
|
||||
- **AND** 页头显示当前周的描述(如"2024年3月第1周")
|
||||
|
||||
#### Scenario: 切换到月视图
|
||||
- **WHEN** 用户切换到月视图
|
||||
- **THEN** 页面加载当前月的预算数据
|
||||
- **AND** 页头显示当前年月(如"2024年3月")
|
||||
|
||||
#### Scenario: 切换到年视图
|
||||
- **WHEN** 用户切换到年视图
|
||||
- **THEN** 页面加载当前年的预算数据(全年汇总)
|
||||
- **AND** 页头显示当前年份(如"2024年")
|
||||
|
||||
### Requirement: 周度视图以周一为起始日
|
||||
周度视图 SHALL 以周一作为一周的开始,周日作为结束。
|
||||
|
||||
#### Scenario: 计算当前周的开始日期
|
||||
- **WHEN** 今天是 2024年3月13日(星期三)
|
||||
- **THEN** 当前周的开始日期是 2024年3月11日(星期一)
|
||||
|
||||
#### Scenario: 跨月的周视图
|
||||
- **WHEN** 当前周跨越两个月(如 3月30日 到 4月5日)
|
||||
- **THEN** 页面正确加载这 7 天的预算数据
|
||||
|
||||
### Requirement: 月度视图加载整月数据
|
||||
月度视图 SHALL 加载选中月份从第一天到最后一天的预算数据。
|
||||
|
||||
#### Scenario: 加载当前月数据
|
||||
- **WHEN** 用户选择 2024年3月
|
||||
- **THEN** 页面加载 2024年3月1日 到 2024年3月31日 的预算数据
|
||||
|
||||
#### Scenario: 加载不同月份长度
|
||||
- **WHEN** 用户选择 2024年2月(闰年)
|
||||
- **THEN** 页面加载 2024年2月1日 到 2024年2月29日 的预算数据
|
||||
|
||||
### Requirement: 年度视图加载全年数据
|
||||
年度视图 SHALL 加载选中年份从1月到12月的预算数据汇总。
|
||||
|
||||
#### Scenario: 加载当前年数据
|
||||
- **WHEN** 用户选择 2024年
|
||||
- **THEN** 页面加载 2024年1月1日 到 2024年12月31日 的预算数据
|
||||
|
||||
### Requirement: 时间段切换时自动刷新数据
|
||||
每次切换时间段时,页面 SHALL 自动重新加载对应的预算数据。
|
||||
|
||||
#### Scenario: 从月视图切换到周视图
|
||||
- **WHEN** 用户从月视图切换到周视图
|
||||
- **THEN** 页面显示加载状态
|
||||
- **AND** 自动加载当前周的预算数据
|
||||
- **AND** 更新所有统计卡片和列表
|
||||
|
||||
#### Scenario: 从周视图切换到年视图
|
||||
- **WHEN** 用户从周视图切换到年视图
|
||||
- **THEN** 页面显示加载状态
|
||||
- **AND** 自动加载当前年的预算数据
|
||||
- **AND** 更新所有统计卡片和列表
|
||||
|
||||
### Requirement: 切换时间周期时同步更新时间段
|
||||
使用页头左右箭头或滑动手势切换时间周期时,SHALL 根据当前时间段类型(周/月/年)切换到对应的上一个或下一个周期。
|
||||
|
||||
#### Scenario: 月视图下点击左箭头
|
||||
- **WHEN** 当前是月视图,显示 2024年3月
|
||||
- **AND** 用户点击页头左箭头
|
||||
- **THEN** 切换到 2024年2月
|
||||
- **AND** 加载 2024年2月 的预算数据
|
||||
|
||||
#### Scenario: 周视图下点击左箭头
|
||||
- **WHEN** 当前是周视图,显示 2024年3月11日-17日(第11周)
|
||||
- **AND** 用户点击页头左箭头
|
||||
- **THEN** 切换到 2024年3月4日-10日(第10周)
|
||||
- **AND** 加载该周的预算数据
|
||||
|
||||
#### Scenario: 年视图下点击左箭头
|
||||
- **WHEN** 当前是年视图,显示 2024年
|
||||
- **AND** 用户点击页头左箭头
|
||||
- **THEN** 切换到 2023年
|
||||
- **AND** 加载 2023年 的预算数据
|
||||
|
||||
#### Scenario: 右滑手势切换到上一个周期
|
||||
- **WHEN** 用户在内容区域向右滑动超过 50px
|
||||
- **THEN** 根据当前时间段类型(周/月/年)切换到上一个周期
|
||||
- **AND** 页面数据自动刷新
|
||||
|
||||
### Requirement: 不能切换到未来的时间周期
|
||||
页面 SHALL 禁止用户切换到未来的时间周期(周/月/年)。
|
||||
|
||||
#### Scenario: 当前月禁用下一月
|
||||
- **WHEN** 当前是 2024年3月,且今天是 2024年3月15日
|
||||
- **THEN** 页头右箭头不可点击
|
||||
|
||||
#### Scenario: 当前周禁用下一周
|
||||
- **WHEN** 当前是本周(2024年3月11日-17日),且今天在这一周内
|
||||
- **THEN** 页头右箭头不可点击
|
||||
|
||||
#### Scenario: 当前年禁用下一年
|
||||
- **WHEN** 当前是 2024年,且今天是 2024年3月15日
|
||||
- **THEN** 页头右箭头不可点击
|
||||
|
||||
#### Scenario: 左滑到当前周期时不切换
|
||||
- **WHEN** 用户在当前月/周/年向左滑动
|
||||
- **THEN** 不切换到下一个周期
|
||||
|
||||
### Requirement: 日期选择器根据时间段类型调整
|
||||
点击页头日期打开的日期选择器,SHALL 根据当前时间段类型(月/年)显示对应的选择模式。
|
||||
|
||||
#### Scenario: 月视图下打开年月选择器
|
||||
- **WHEN** 当前是月视图
|
||||
- **AND** 用户点击页头日期
|
||||
- **THEN** 打开年月选择器(columns-type: ['year', 'month'])
|
||||
|
||||
#### Scenario: 年视图下打开年份选择器
|
||||
- **WHEN** 当前是年视图
|
||||
- **AND** 用户点击页头日期
|
||||
- **THEN** 打开年份选择器(type: 'year')
|
||||
|
||||
#### Scenario: 周视图下打开年月选择器
|
||||
- **WHEN** 当前是周视图
|
||||
- **AND** 用户点击页头日期
|
||||
- **THEN** 打开年月选择器(选择月份后自动定位到当月的当前周)
|
||||
|
||||
### Requirement: 日期选择器确认后更新页面数据
|
||||
用户在日期选择器中选择日期并确认后,页面 SHALL 自动更新到选中的时间周期并刷新数据。
|
||||
|
||||
#### Scenario: 选择历史月份
|
||||
- **WHEN** 用户在月视图下打开日期选择器
|
||||
- **AND** 选择 2023年12月 并确认
|
||||
- **THEN** 页面切换到 2023年12月
|
||||
- **AND** 加载 2023年12月 的预算数据
|
||||
|
||||
#### Scenario: 选择未来月份时显示提示
|
||||
- **WHEN** 用户在月视图下打开日期选择器
|
||||
- **AND** 尝试选择未来的月份(如 2024年4月,今天是 2024年3月15日)
|
||||
- **THEN** 显示提示"不能选择未来的月份"
|
||||
- **AND** 不切换页面日期
|
||||
|
||||
### Requirement: 时间段类型持久化(可选)
|
||||
页面 SHOULD 在用户离开后记住上次选择的时间段类型(周/月/年),下次访问时自动恢复。
|
||||
|
||||
#### Scenario: 记住上次选择的时间段
|
||||
- **WHEN** 用户选择了"周"视图
|
||||
- **AND** 离开预算页面,然后返回
|
||||
- **THEN** 页面自动切换到"周"视图(可选功能)
|
||||
@@ -0,0 +1,226 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 支持三种业务类型的预算数据加载
|
||||
预算页面 SHALL 支持加载支出预算、收入预算和存款计划三种业务类型的月度数据。
|
||||
|
||||
#### Scenario: 加载支出预算数据
|
||||
- **WHEN** 用户切换到"支出" tab
|
||||
- **THEN** 页面加载当前月份的支出预算列表
|
||||
- **AND** 显示支出预算的统计信息(总预算、已支出、余额等)
|
||||
|
||||
#### Scenario: 加载收入预算数据
|
||||
- **WHEN** 用户切换到"收入" tab
|
||||
- **THEN** 页面加载当前月份的收入预算列表
|
||||
- **AND** 显示收入预算的统计信息(总目标、已收入、差额等)
|
||||
|
||||
#### Scenario: 加载存款计划数据
|
||||
- **WHEN** 用户切换到"计划" tab
|
||||
- **THEN** 页面加载当前月份的存款计划列表
|
||||
- **AND** 显示每个存款计划的统计信息(已存、目标、还差等)
|
||||
|
||||
### Requirement: 在主页面集中管理数据加载逻辑
|
||||
数据加载逻辑 SHALL 在主页面(Index.vue)集中管理,通过 props 传递给子模块。
|
||||
|
||||
#### Scenario: 主页面加载数据后传递给子模块
|
||||
- **WHEN** 主页面完成数据加载
|
||||
- **THEN** 将数据通过 props 传递给对应的子模块(ExpenseBudgetContent、IncomeBudgetContent、SavingsBudgetContent)
|
||||
|
||||
#### Scenario: 子模块不直接调用 API
|
||||
- **WHEN** 子模块需要刷新数据
|
||||
- **THEN** 触发事件通知主页面重新加载数据
|
||||
- **AND** 主页面加载完成后通过 props 更新子模块
|
||||
|
||||
### Requirement: 统一的加载状态和错误处理
|
||||
页面 SHALL 提供统一的加载状态和错误处理机制。
|
||||
|
||||
#### Scenario: 数据加载中显示加载状态
|
||||
- **WHEN** 页面正在加载预算数据
|
||||
- **THEN** 显示加载动画(van-loading)
|
||||
|
||||
#### Scenario: 数据加载失败显示错误信息
|
||||
- **WHEN** 数据加载失败(网络错误、API 错误等)
|
||||
- **THEN** 显示错误提示(van-empty with error image)
|
||||
- **AND** 提供"重试"按钮
|
||||
|
||||
#### Scenario: 点击重试按钮重新加载数据
|
||||
- **WHEN** 用户点击"重试"按钮
|
||||
- **THEN** 清除错误状态
|
||||
- **AND** 重新加载当前月份和业务 tab 的数据
|
||||
|
||||
### Requirement: 支持月度预算数据加载
|
||||
页面 SHALL 调用 `getBudgetList(year, month)` API 加载指定月份的预算列表。
|
||||
|
||||
#### Scenario: 加载当月预算列表
|
||||
- **WHEN** 用户选择 2024年3月
|
||||
- **THEN** 调用 `getBudgetList('2024-03-01')` 获取预算列表
|
||||
- **AND** 按业务类型(支出/收入/存款)分组数据
|
||||
|
||||
#### Scenario: 加载月度分类统计
|
||||
- **WHEN** 用户选择 2024年3月
|
||||
- **THEN** 调用 `getCategoryStats(category, '2024-03-01')` 获取月度统计
|
||||
- **AND** 显示月度使用率、趋势等信息
|
||||
|
||||
#### Scenario: 支持加载历史月份数据
|
||||
- **WHEN** 用户选择 2023年12月
|
||||
- **THEN** 调用 `getBudgetList('2023-12-01')` 获取历史月份的预算数据
|
||||
- **AND** 正确显示历史数据
|
||||
|
||||
### Requirement: 加载未覆盖预算的分类
|
||||
页面 SHALL 加载未设置预算的分类,并在页头显示警告图标(支出和收入 tabs)。
|
||||
|
||||
#### Scenario: 加载未覆盖分类列表
|
||||
- **WHEN** 用户在支出 tab 下
|
||||
- **THEN** 调用 `getUncoveredCategories(category, date)` 获取未设置预算的支出分类
|
||||
- **AND** 如果存在未覆盖分类,在页头显示警告图标
|
||||
|
||||
#### Scenario: 点击警告图标显示未覆盖分类详情
|
||||
- **WHEN** 用户点击页头的警告图标
|
||||
- **THEN** 打开弹窗显示未覆盖分类列表
|
||||
- **AND** 每个分类显示名称、交易笔数和总金额
|
||||
|
||||
#### Scenario: 存款计划 tab 不显示警告图标
|
||||
- **WHEN** 用户切换到"计划" tab
|
||||
- **THEN** 页头不显示警告图标(存款计划不需要覆盖所有分类)
|
||||
|
||||
#### Scenario: 未覆盖分类为空时不显示警告图标
|
||||
- **WHEN** 当前月份所有分类都已设置预算
|
||||
- **THEN** 页头不显示警告图标
|
||||
|
||||
### Requirement: 加载归档月份的总结
|
||||
对于历史月份(已归档),页面 SHALL 支持加载和显示归档总结。
|
||||
|
||||
#### Scenario: 检测是否为归档月份
|
||||
- **WHEN** 用户选择的月份早于当前月份
|
||||
- **THEN** 页头显示归档图标(comment-o)
|
||||
|
||||
#### Scenario: 点击归档图标显示总结
|
||||
- **WHEN** 用户点击页头的归档图标
|
||||
- **THEN** 调用 `getArchiveSummary(date)` 获取归档总结
|
||||
- **AND** 在弹窗中显示 AI 生成的总结内容(富文本)
|
||||
|
||||
#### Scenario: 归档总结为空时显示提示
|
||||
- **WHEN** 用户点击归档图标,但该月份没有总结
|
||||
- **THEN** 弹窗显示"暂无总结"
|
||||
|
||||
#### Scenario: 当前月不显示归档图标
|
||||
- **WHEN** 用户查看当前月份的预算
|
||||
- **THEN** 页头不显示归档图标
|
||||
|
||||
### Requirement: 存款计划支持独立日期切换
|
||||
存款计划 SHALL 支持独立的日期切换功能,不受页头日期的影响。
|
||||
|
||||
#### Scenario: 存款计划卡片底部显示日期切换按钮
|
||||
- **WHEN** 用户在"计划" tab 下
|
||||
- **THEN** 每个存款计划卡片底部显示日期切换按钮(左箭头、日期标签、右箭头)
|
||||
|
||||
#### Scenario: 点击左箭头查看上一个周期的存款计划
|
||||
- **WHEN** 用户点击存款计划卡片的左箭头
|
||||
- **AND** 该存款计划是月度计划,当前显示 2024年3月
|
||||
- **THEN** 调用 `getSavingsBudget(year, month, type)` 获取 2024年2月 的数据
|
||||
- **AND** 更新该卡片的显示内容
|
||||
|
||||
#### Scenario: 点击右箭头查看下一个周期的存款计划
|
||||
- **WHEN** 用户点击存款计划卡片的右箭头
|
||||
- **AND** 下一个周期不是未来周期
|
||||
- **THEN** 调用 `getSavingsBudget(year, month, type)` 获取下一个周期的数据
|
||||
- **AND** 更新该卡片的显示内容
|
||||
|
||||
#### Scenario: 当前周期时禁用右箭头
|
||||
- **WHEN** 存款计划卡片显示的是当前周期
|
||||
- **THEN** 右箭头按钮不可点击
|
||||
|
||||
#### Scenario: 年度存款计划按年切换
|
||||
- **WHEN** 用户点击年度存款计划的左箭头
|
||||
- **AND** 当前显示 2024年
|
||||
- **THEN** 调用 `getSavingsBudget(2023, 0, 'year')` 获取 2023年 的数据
|
||||
|
||||
#### Scenario: 月度存款计划按月切换
|
||||
- **WHEN** 用户点击月度存款计划的左箭头
|
||||
- **AND** 当前显示 2024年3月
|
||||
- **THEN** 调用 `getSavingsBudget(2024, 2, 'month')` 获取 2024年2月 的数据
|
||||
|
||||
### Requirement: 下拉刷新重新加载所有数据
|
||||
用户下拉刷新时,页面 SHALL 重新加载当前月份和业务 tab 的所有数据。
|
||||
|
||||
#### Scenario: 下拉刷新支出预算数据
|
||||
- **WHEN** 用户在支出 tab 下下拉刷新
|
||||
- **THEN** 重新调用 `getBudgetList`、`getCategoryStats` 和 `getUncoveredCategories`
|
||||
- **AND** 刷新完成后显示提示"刷新成功"
|
||||
|
||||
#### Scenario: 下拉刷新存款计划数据
|
||||
- **WHEN** 用户在计划 tab 下下拉刷新
|
||||
- **THEN** 重新调用 `getBudgetList` 获取存款计划列表
|
||||
- **AND** 刷新完成后显示提示"刷新成功"
|
||||
|
||||
#### Scenario: 刷新失败显示提示
|
||||
- **WHEN** 下拉刷新时数据加载失败
|
||||
- **THEN** 显示提示"刷新失败"
|
||||
- **AND** 保留之前的数据(如果有)
|
||||
|
||||
### Requirement: 月份或业务 tab 切换时自动刷新数据
|
||||
每次切换月份或业务 tab 时,页面 SHALL 自动刷新对应的数据。
|
||||
|
||||
#### Scenario: 切换月份时刷新数据
|
||||
- **WHEN** 用户从 2024年3月 切换到 2024年2月
|
||||
- **THEN** 自动加载 2024年2月 的预算数据
|
||||
- **AND** 清除之前的数据和错误状态
|
||||
|
||||
#### Scenario: 从支出 tab 切换到收入 tab 时刷新数据
|
||||
- **WHEN** 用户从支出 tab 切换到收入 tab
|
||||
- **THEN** 自动加载收入预算数据和统计信息
|
||||
- **AND** 保持当前月份不变
|
||||
|
||||
#### Scenario: 切换时清除旧的加载状态
|
||||
- **WHEN** 用户切换月份或 tab
|
||||
- **THEN** 清除之前的加载状态和错误信息
|
||||
- **AND** 显示新的加载动画
|
||||
|
||||
### Requirement: 数据加载失败不影响其他功能
|
||||
即使某些数据加载失败,页面 SHALL 仍然允许用户切换月份和业务 tab。
|
||||
|
||||
#### Scenario: 统计数据加载失败仍可查看列表
|
||||
- **WHEN** `getCategoryStats` 加载失败
|
||||
- **THEN** 显示错误提示
|
||||
- **AND** 预算列表仍然正常显示(如果 `getBudgetList` 成功)
|
||||
|
||||
#### Scenario: 未覆盖分类加载失败不影响主功能
|
||||
- **WHEN** `getUncoveredCategories` 加载失败
|
||||
- **THEN** 页头不显示警告图标
|
||||
- **AND** 其他功能(预算列表、统计等)仍然正常工作
|
||||
|
||||
#### Scenario: 部分数据加载失败时可重试
|
||||
- **WHEN** 部分数据加载失败(如统计数据)
|
||||
- **THEN** 用户可以点击"重试"按钮
|
||||
- **AND** 只重新加载失败的部分(而非所有数据)
|
||||
|
||||
### Requirement: 支持全局事件刷新数据
|
||||
页面 SHALL 监听全局 'transactions-changed' 事件,自动刷新数据。
|
||||
|
||||
#### Scenario: 接收全局事件后刷新数据
|
||||
- **WHEN** 用户在其他页面添加或修改了预算记录
|
||||
- **AND** 触发 'transactions-changed' 事件
|
||||
- **THEN** 预算页面(如果处于激活状态)自动刷新数据
|
||||
|
||||
#### Scenario: 页面被缓存时不响应事件
|
||||
- **WHEN** 预算页面被 keep-alive 缓存(不在当前显示)
|
||||
- **AND** 触发 'transactions-changed' 事件
|
||||
- **THEN** 不立即刷新数据(节省性能)
|
||||
- **AND** 当用户返回预算页面时(onActivated)再刷新数据
|
||||
|
||||
### Requirement: 并发加载多个数据源
|
||||
页面 SHALL 并发加载多个数据源(预算列表、统计、未覆盖分类等),提升加载速度。
|
||||
|
||||
#### Scenario: 并发加载预算列表和统计数据
|
||||
- **WHEN** 用户切换到支出 tab
|
||||
- **THEN** 同时调用 `getBudgetList`、`getCategoryStats` 和 `getUncoveredCategories`
|
||||
- **AND** 等待所有请求完成后隐藏加载状态
|
||||
|
||||
#### Scenario: 某个请求失败不阻塞其他请求
|
||||
- **WHEN** `getUncoveredCategories` 请求失败
|
||||
- **THEN** 其他请求(`getBudgetList`、`getCategoryStats`)仍然继续
|
||||
- **AND** 页面显示部分数据
|
||||
|
||||
#### Scenario: 所有请求失败显示错误
|
||||
- **WHEN** 所有数据加载请求都失败
|
||||
- **THEN** 显示错误提示和"重试"按钮
|
||||
- **AND** 不显示任何数据
|
||||
@@ -0,0 +1,125 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 页面采用简洁的布局结构
|
||||
预算 v2 页面 SHALL 采用简洁的布局结构,包含:DateSelectHeader、业务 tabs 和可滚动内容区域。
|
||||
|
||||
#### Scenario: 页面加载时显示完整布局
|
||||
- **WHEN** 用户访问预算 v2 页面
|
||||
- **THEN** 页面显示以下组件(从上到下):
|
||||
- DateSelectHeader(显示当前年月,左右箭头)
|
||||
- van-tabs(支出/收入/计划三个选项,默认选中"支出")
|
||||
- 可滚动内容区域(包含预算统计和列表)
|
||||
|
||||
### Requirement: 使用 DateSelectHeader 组件作为页头
|
||||
预算 v2 页面 SHALL 使用 `@/components/DateSelectHeader.vue` 组件作为页头,替代当前的 `van-nav-bar`。
|
||||
|
||||
#### Scenario: 页头显示当前选中的年月
|
||||
- **WHEN** 用户选择了 2024 年 3 月
|
||||
- **THEN** 页头显示"2024年3月"
|
||||
|
||||
#### Scenario: 点击左箭头切换到上一个月
|
||||
- **WHEN** 用户点击页头左箭头
|
||||
- **THEN** 切换到上一个月(如从 2024年3月 切换到 2024年2月)
|
||||
- **AND** 页面数据自动刷新
|
||||
|
||||
#### Scenario: 点击右箭头切换到下一个月
|
||||
- **WHEN** 用户点击页头右箭头,且当前不是当前月
|
||||
- **THEN** 切换到下一个月,页面数据自动刷新
|
||||
|
||||
#### Scenario: 当前是当前月时禁用右箭头
|
||||
- **WHEN** 用户在当前月(如今天是 2024年3月15日,页面显示 2024年3月)
|
||||
- **THEN** 页头右箭头不可点击(不能查看未来月份)
|
||||
|
||||
#### Scenario: 点击日期打开日期选择器
|
||||
- **WHEN** 用户点击页头的日期文字
|
||||
- **THEN** 显示年月选择器弹窗(van-date-picker,columns-type: ['year', 'month'])
|
||||
|
||||
#### Scenario: 日期选择器不能选择未来月份
|
||||
- **WHEN** 用户在日期选择器中尝试选择未来月份
|
||||
- **THEN** 显示提示"不能选择未来的月份"
|
||||
- **AND** 不切换页面日期
|
||||
|
||||
### Requirement: 业务 tabs 紧随页头之后
|
||||
预算页面 SHALL 在 DateSelectHeader 下方直接显示业务 tabs(支出/收入/计划),使用 `van-tabs` 组件。
|
||||
|
||||
#### Scenario: 默认选中"支出" tab
|
||||
- **WHEN** 用户访问预算 v2 页面
|
||||
- **THEN** 业务 tabs 默认选中"支出"
|
||||
|
||||
#### Scenario: 切换到"收入" tab
|
||||
- **WHEN** 用户点击"收入" tab
|
||||
- **THEN** 内容区域显示收入预算的统计和列表
|
||||
|
||||
#### Scenario: 切换到"计划" tab
|
||||
- **WHEN** 用户点击"计划" tab
|
||||
- **THEN** 内容区域显示存款计划的列表
|
||||
|
||||
#### Scenario: 切换 tab 时保持页头日期不变
|
||||
- **WHEN** 用户从"支出" tab 切换到"收入" tab
|
||||
- **THEN** 页头显示的年月不变
|
||||
- **AND** 加载该月的收入预算数据
|
||||
|
||||
### Requirement: 可滚动内容区域支持下拉刷新
|
||||
内容区域 SHALL 支持下拉刷新,刷新当前选中的预算数据。
|
||||
|
||||
#### Scenario: 下拉刷新当前页面数据
|
||||
- **WHEN** 用户在内容区域下拉
|
||||
- **THEN** 显示刷新动画
|
||||
- **AND** 重新加载当前月份和业务 tab 的数据
|
||||
- **AND** 刷新完成后显示提示"刷新成功"
|
||||
|
||||
### Requirement: 支持左右滑动切换月份
|
||||
预算页面 SHALL 支持在内容区域左右滑动来切换月份。
|
||||
|
||||
#### Scenario: 右滑切换到上一个月
|
||||
- **WHEN** 用户在内容区域向右滑动超过 50px
|
||||
- **THEN** 切换到上一个月
|
||||
- **AND** 页面数据自动刷新
|
||||
|
||||
#### Scenario: 左滑切换到下一个月
|
||||
- **WHEN** 用户在内容区域向左滑动超过 50px,且不是当前月
|
||||
- **THEN** 切换到下一个月
|
||||
- **AND** 页面数据自动刷新
|
||||
|
||||
#### Scenario: 垂直滑动不触发月份切换
|
||||
- **WHEN** 用户垂直滑动(滚动内容)
|
||||
- **THEN** 不触发月份切换
|
||||
|
||||
#### Scenario: 当前月时左滑不切换
|
||||
- **WHEN** 用户在当前月向左滑动
|
||||
- **THEN** 不切换到下一个月(不能查看未来月份)
|
||||
|
||||
### Requirement: 页面底部留出安全距离
|
||||
内容区域 SHALL 在底部留出足够的安全距离,避免被底部导航栏遮挡。
|
||||
|
||||
#### Scenario: 内容区域底部安全距离
|
||||
- **WHEN** 用户滚动到内容底部
|
||||
- **THEN** 内容底部与底部导航栏之间有足够的间距(95px + env(safe-area-inset-bottom))
|
||||
|
||||
### Requirement: 支持 keep-alive 缓存
|
||||
预算 v2 页面 SHALL 支持 Vue Router 的 keep-alive 缓存,提升用户体验。
|
||||
|
||||
#### Scenario: 从其他页面返回时保持状态
|
||||
- **WHEN** 用户从预算页面跳转到其他页面,然后返回
|
||||
- **THEN** 预算页面保持之前的状态(选中的月份、业务 tab、滚动位置)
|
||||
|
||||
#### Scenario: 接收全局事件刷新数据
|
||||
- **WHEN** 用户在其他页面添加或修改了预算记录,触发 'transactions-changed' 事件
|
||||
- **THEN** 预算页面自动刷新数据(如果页面处于激活状态)
|
||||
|
||||
### Requirement: 加载状态和错误处理
|
||||
页面 SHALL 提供清晰的加载状态和错误提示。
|
||||
|
||||
#### Scenario: 数据加载中显示加载动画
|
||||
- **WHEN** 页面正在加载预算数据
|
||||
- **THEN** 显示加载动画(van-loading)
|
||||
|
||||
#### Scenario: 数据加载失败显示错误提示
|
||||
- **WHEN** 数据加载失败(网络错误、API 错误等)
|
||||
- **THEN** 显示错误提示(van-empty with error image)
|
||||
- **AND** 提供"重试"按钮
|
||||
|
||||
#### Scenario: 点击重试按钮重新加载
|
||||
- **WHEN** 用户点击"重试"按钮
|
||||
- **THEN** 清除错误状态
|
||||
- **AND** 重新加载当前月份和业务 tab 的数据
|
||||
220
openspec/changes/archive/2026-02-13-budget-page-v2/tasks.md
Normal file
220
openspec/changes/archive/2026-02-13-budget-page-v2/tasks.md
Normal file
@@ -0,0 +1,220 @@
|
||||
## 1. 项目结构准备
|
||||
|
||||
- [x] 1.1 创建 `Web/src/views/budgetV2/` 目录
|
||||
- [x] 1.2 创建 `Web/src/views/budgetV2/modules/` 子目录
|
||||
- [x] 1.3 创建 `Web/src/views/budgetV2/Index.vue` 主页面文件(空模板)
|
||||
- [x] 1.4 创建 `Web/src/views/budgetV2/modules/ExpenseBudgetContent.vue` 支出预算子模块(空模板)
|
||||
- [x] 1.5 创建 `Web/src/views/budgetV2/modules/IncomeBudgetContent.vue` 收入预算子模块(空模板)
|
||||
- [x] 1.6 创建 `Web/src/views/budgetV2/modules/SavingsBudgetContent.vue` 存款计划子模块(空模板)
|
||||
|
||||
## 2. 路由配置
|
||||
|
||||
- [x] 2.1 在 `Web/src/router/index.js` 中添加 `/budget-v2` 路由,指向 `budgetV2/Index.vue`
|
||||
- [x] 2.2 配置路由的 `name: 'BudgetV2'` 和 `meta` 信息(用于 keep-alive)
|
||||
- [x] 2.3 保留旧路由 `/budget` 指向 `BudgetView.vue`(作为回退方案)
|
||||
- [x] 2.4 在导航配置中将"预算"菜单指向新路由 `/budget-v2`
|
||||
|
||||
## 3. 主页面布局实现
|
||||
|
||||
- [x] 3.1 在 `Index.vue` 中引入 `DateSelectHeader` 组件
|
||||
- [x] 3.2 添加 `van-tabs` 组件(支出/收入/计划三个 tab)
|
||||
- [x] 3.3 添加可滚动内容区域(包含 `van-pull-refresh`)
|
||||
- [x] 3.4 设置页面容器样式(`page-container-flex` + 底部安全距离)
|
||||
- [x] 3.5 配置 `defineOptions({ name: 'BudgetV2View' })` 用于 keep-alive
|
||||
|
||||
## 4. 状态管理和数据定义
|
||||
|
||||
- [x] 4.1 定义当前日期状态 `currentDate = ref(new Date())`
|
||||
- [x] 4.2 定义业务 tab 状态 `activeTab = ref(BudgetCategory.Expense)`
|
||||
- [x] 4.3 定义数据状态(`expenseBudgets`、`incomeBudgets`、`savingsBudgets`、`overallStats`)
|
||||
- [x] 4.4 定义 UI 状态(`loading`、`refreshing`、`hasError`、`errorMessage`)
|
||||
- [x] 4.5 定义未覆盖分类状态 `uncoveredCategories = ref([])`
|
||||
- [x] 4.6 定义归档总结状态(`showSummaryPopup`、`archiveSummary`)
|
||||
- [x] 4.7 定义日期选择器状态(`showDatePicker`、`pickerDate`)
|
||||
|
||||
## 5. DateSelectHeader 集成
|
||||
|
||||
- [x] 5.1 配置 DateSelectHeader 的 `type` 属性为 'month'
|
||||
- [x] 5.2 绑定 DateSelectHeader 的 `current-date` 属性到 `currentDate`
|
||||
- [x] 5.3 监听 DateSelectHeader 的 `@prev` 事件,实现 `handlePrevMonth()` 方法
|
||||
- [x] 5.4 监听 DateSelectHeader 的 `@next` 事件,实现 `handleNextMonth()` 方法
|
||||
- [x] 5.5 监听 DateSelectHeader 的 `@jump` 事件,打开日期选择器
|
||||
- [x] 5.6 在页头右侧添加未覆盖分类警告图标(根据 `uncoveredCategories` 显示)
|
||||
- [x] 5.7 在页头右侧添加归档图标(历史月份显示)
|
||||
|
||||
## 6. 业务 tabs 实现
|
||||
|
||||
- [x] 6.1 配置 `van-tabs` 的三个 tab(支出、收入、计划)
|
||||
- [x] 6.2 绑定 `van-tabs` 的 `v-model:active` 到 `activeTab`
|
||||
- [x] 6.3 监听 `activeTab` 的变化,触发数据刷新(使用 `watch`)
|
||||
- [x] 6.4 在每个 tab 中引入对应的子模块组件
|
||||
|
||||
## 7. 数据加载核心逻辑
|
||||
|
||||
- [x] 7.1 实现 `loadBudgetData()` 统一数据加载函数(月度)
|
||||
- [x] 7.2 实现 `loadMonthlyData(year, month)` 加载月度预算数据
|
||||
- [x] 7.3 实现 `loadCategoryStats()` 加载分类统计数据
|
||||
- [x] 7.4 实现 `loadUncoveredCategories()` 加载未覆盖分类(仅支出和收入 tab)
|
||||
- [x] 7.5 实现 `loadArchiveSummary()` 加载归档总结(历史月份)
|
||||
- [x] 7.6 使用 `Promise.allSettled()` 并发加载多个数据源
|
||||
|
||||
## 8. 月份切换逻辑
|
||||
|
||||
- [x] 8.1 实现 `handlePrevMonth()` 方法(切换到上一个月)
|
||||
- [x] 8.2 实现 `handleNextMonth()` 方法(切换到下一个月,禁止未来月份)
|
||||
- [x] 8.3 实现 `isCurrentMonth()` 方法(判断是否为当前月,禁用右箭头)
|
||||
- [x] 8.4 在月份切换后自动调用 `loadBudgetData()` 刷新数据
|
||||
|
||||
## 9. 日期选择器实现
|
||||
|
||||
- [x] 9.1 添加 `van-popup` + `van-date-picker` 组件
|
||||
- [x] 9.2 配置日期选择器为年月模式(columns-type: ['year', 'month'])
|
||||
- [x] 9.3 实现 `onDatePickerConfirm()` 方法(确认日期选择)
|
||||
- [x] 9.4 在日期选择器中限制不能选择未来的月份
|
||||
- [x] 9.5 选择日期后自动调用 `loadBudgetData()` 刷新数据
|
||||
|
||||
## 10. 左右滑动手势实现
|
||||
|
||||
- [x] 10.1 定义触摸状态(`touchStartX`, `touchStartY`, `touchEndX`, `touchEndY`)
|
||||
- [x] 10.2 实现 `handleTouchStart()` 方法(记录触摸起始位置)
|
||||
- [x] 10.3 实现 `handleTouchMove()` 方法(记录触摸移动位置)
|
||||
- [x] 10.4 实现 `handleTouchEnd()` 方法(判断滑动方向和距离)
|
||||
- [x] 10.5 在 `handleTouchEnd()` 中根据滑动方向调用 `handlePrevMonth()` 或 `handleNextMonth()`
|
||||
- [x] 10.6 设置最小滑动距离阈值(50px),避免误触
|
||||
- [x] 10.7 确保垂直滑动(滚动内容)不触发月份切换
|
||||
|
||||
## 11. 下拉刷新实现
|
||||
|
||||
- [x] 11.1 配置 `van-pull-refresh` 组件,绑定 `v-model="refreshing"`
|
||||
- [x] 11.2 实现 `onRefresh()` 方法(清除错误状态,重新加载数据)
|
||||
- [x] 11.3 在数据加载完成后设置 `refreshing = false`
|
||||
- [x] 11.4 显示刷新成功提示
|
||||
|
||||
## 12. 加载状态和错误处理
|
||||
|
||||
- [x] 12.1 在数据加载中显示 `van-loading` 组件
|
||||
- [x] 12.2 在数据加载失败时显示 `van-empty` 组件(包含"重试"按钮)
|
||||
- [x] 12.3 实现 `retryLoad()` 方法(清除错误状态,重新加载数据)
|
||||
- [x] 12.4 在所有 API 调用中添加 try-catch 错误处理
|
||||
- [x] 12.5 使用 `Promise.allSettled()` 并发加载,避免单点失败
|
||||
|
||||
## 13. 支出预算子模块实现
|
||||
|
||||
- [x] 13.1 在 `ExpenseBudgetContent.vue` 中定义 props(`budgets`, `stats`, `uncoveredCategories`)
|
||||
- [x] 13.2 复用 `BudgetChartAnalysis` 组件显示统计图表
|
||||
- [x] 13.3 复用 `BudgetCard` 组件显示预算列表
|
||||
- [x] 13.4 使用 `van-swipe-cell` 实现左滑删除功能
|
||||
- [x] 13.5 点击预算卡片打开 `BudgetEditPopup` 编辑弹窗
|
||||
- [x] 13.6 添加悬浮按钮(`van-floating-bubble`)打开预算列表弹窗
|
||||
- [x] 13.7 在预算列表弹窗中显示所有支出预算
|
||||
|
||||
## 14. 收入预算子模块实现
|
||||
|
||||
- [x] 14.1 在 `IncomeBudgetContent.vue` 中定义 props(`budgets`, `stats`, `uncoveredCategories`)
|
||||
- [x] 14.2 复用 `BudgetChartAnalysis` 组件显示统计图表
|
||||
- [x] 14.3 复用 `BudgetCard` 组件显示预算列表(调整显示字段:已收入、目标、差额)
|
||||
- [x] 14.4 使用 `van-swipe-cell` 实现左滑删除功能
|
||||
- [x] 14.5 点击预算卡片打开 `BudgetEditPopup` 编辑弹窗
|
||||
- [x] 14.6 添加悬浮按钮打开预算列表弹窗
|
||||
|
||||
## 15. 存款计划子模块实现
|
||||
|
||||
- [x] 15.1 在 `SavingsBudgetContent.vue` 中定义 props(`budgets`)
|
||||
- [x] 15.2 复用 `BudgetCard` 组件显示存款计划列表(调整显示字段:已存、目标、还差)
|
||||
- [x] 15.3 在每个存款计划卡片底部添加日期切换按钮(左箭头、日期标签、右箭头)
|
||||
- [x] 15.4 实现 `handleSavingsNav(budget, offset)` 方法(切换存款计划的日期)
|
||||
- [x] 15.5 实现 `getSavingsDateLabel(budget)` 方法(格式化存款计划的日期标签)
|
||||
- [x] 15.6 实现 `disabledSavingsNextNav(budget)` 方法(判断是否禁用右箭头)
|
||||
- [x] 15.7 调用 `getSavingsBudget(year, month, type)` API 切换存款计划日期
|
||||
- [x] 15.8 在页头右侧显示储蓄配置图标,点击打开 `SavingsConfigPopup`
|
||||
|
||||
## 16. 未覆盖分类弹窗实现
|
||||
|
||||
- [x] 16.1 复用 `PopupContainer` 组件创建未覆盖分类弹窗
|
||||
- [x] 16.2 在弹窗中显示未覆盖分类列表(分类名称、交易笔数、总金额)
|
||||
- [x] 16.3 使用卡片样式展示每个分类(`uncovered-item`)
|
||||
- [x] 16.4 在弹窗底部添加"我知道了"按钮关闭弹窗
|
||||
|
||||
## 17. 归档总结弹窗实现
|
||||
|
||||
- [x] 17.1 复用 `PopupContainer` 组件创建归档总结弹窗
|
||||
- [x] 17.2 调用 `getArchiveSummary(date)` API 获取归档总结
|
||||
- [x] 17.3 在弹窗中显示富文本内容(使用 `v-html`)
|
||||
- [x] 17.4 如果没有总结,显示"暂无总结"提示
|
||||
|
||||
## 18. 预算编辑和删除功能
|
||||
|
||||
- [x] 18.1 引入 `BudgetEditPopup` 组件
|
||||
- [x] 18.2 点击悬浮按钮打开 `BudgetEditPopup`(新增预算)
|
||||
- [x] 18.3 点击预算卡片打开 `BudgetEditPopup`(编辑预算)
|
||||
- [x] 18.4 在 `BudgetEditPopup` 的 `@success` 事件中调用 `loadBudgetData()` 刷新数据
|
||||
- [x] 18.5 实现 `handleDelete(budget)` 方法(删除预算)
|
||||
- [x] 18.6 在删除前显示确认对话框(`showConfirmDialog`)
|
||||
- [x] 18.7 调用 `deleteBudget(id)` API 删除预算
|
||||
- [x] 18.8 删除成功后调用 `loadBudgetData()` 刷新数据
|
||||
|
||||
## 19. 储蓄配置功能
|
||||
|
||||
- [x] 19.1 引入 `SavingsConfigPopup` 组件
|
||||
- [x] 19.2 在存款计划 tab 的页头右侧显示配置图标
|
||||
- [x] 19.3 点击配置图标打开 `SavingsConfigPopup`
|
||||
- [x] 19.4 在 `SavingsConfigPopup` 的 `@success` 事件中调用 `loadBudgetData()` 刷新数据
|
||||
|
||||
## 20. 全局事件监听
|
||||
|
||||
- [x] 20.1 在 `onMounted()` 中监听 'transactions-changed' 全局事件
|
||||
- [x] 20.2 实现 `handleTransactionsChanged()` 方法(刷新数据)
|
||||
- [x] 20.3 在 `onActivated()` 中处理从缓存恢复时的数据刷新
|
||||
- [x] 20.4 在 `onBeforeUnmount()` 中移除事件监听器
|
||||
|
||||
## 21. 样式和主题
|
||||
|
||||
- [x] 21.1 添加页面容器样式(`.budget-v2-wrapper`)
|
||||
- [x] 21.2 添加可滚动内容区域样式(`.budget-scroll-content`)
|
||||
- [x] 21.3 添加底部安全距离样式(`calc(95px + env(safe-area-inset-bottom))`)
|
||||
- [x] 21.4 配置主题变量(使用 `var(--van-primary-color)` 等)
|
||||
- [x] 21.5 添加深色模式支持(通过 `van-config-provider`)
|
||||
- [x] 21.6 优化移动端滚动体验(`-webkit-overflow-scrolling: touch`)
|
||||
|
||||
## 22. 辅助函数实现
|
||||
|
||||
- [x] 22.1 实现 `formatMoney(val)` 方法(格式化金额显示)
|
||||
- [x] 22.2 实现 `getPeriodLabel(type)` 方法(获取周期标签:本月/本年)
|
||||
- [x] 22.3 实现 `getProgressColor(budget)` 方法(计算进度条颜色)
|
||||
- [x] 22.4 实现 `isArchiveMonth(date)` 方法(判断是否为历史月份)
|
||||
|
||||
## 23. 测试和调试
|
||||
|
||||
- [x] 23.1 测试页面初始加载(默认当前月、支出 tab)
|
||||
- [x] 23.2 测试月份切换(左右箭头、滑动手势)
|
||||
- [x] 23.3 测试支出/收入/计划 tab 切换
|
||||
- [x] 23.4 测试左右滑动手势切换月份
|
||||
- [x] 23.5 测试下拉刷新功能
|
||||
- [x] 23.6 测试日期选择器(选择历史月份、禁止未来月份)
|
||||
- [x] 23.7 测试未覆盖分类功能(警告图标、弹窗)
|
||||
- [x] 23.8 测试归档总结功能(历史月份)
|
||||
- [x] 23.9 测试存款计划的独立日期切换
|
||||
- [x] 23.10 测试预算新增、编辑、删除功能
|
||||
- [x] 23.11 测试储蓄配置功能
|
||||
- [x] 23.12 测试错误处理(API 失败、网络错误)
|
||||
- [x] 23.13 测试加载状态显示
|
||||
- [x] 23.14 测试 keep-alive 缓存和全局事件刷新
|
||||
- [x] 23.15 测试深色模式切换
|
||||
|
||||
## 24. 优化和完善
|
||||
|
||||
- [x] 24.1 检查并优化数据加载性能(使用 `Promise.allSettled` 并发加载)
|
||||
- [x] 24.2 添加必要的加载动画和过渡效果
|
||||
- [x] 24.3 添加代码注释(业务逻辑复杂的部分)
|
||||
- [x] 24.4 检查并修复 ESLint 警告
|
||||
- [x] 24.5 验证所有 API 调用的错误处理
|
||||
- [x] 24.6 验证移动端适配(安全距离、触摸手势)
|
||||
|
||||
## 25. 文档和发布
|
||||
|
||||
- [x] 25.1 更新 README.md(如果需要)
|
||||
- [ ] 25.2 创建 PR 并填写详细的变更说明
|
||||
- [ ] 25.3 在 PR 中添加测试截图或录屏
|
||||
- [ ] 25.4 请求代码审查
|
||||
- [ ] 25.5 合并 PR 并部署到测试环境
|
||||
- [ ] 25.6 收集用户反馈并快速迭代
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-02-13
|
||||
Reference in New Issue
Block a user