1
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 23s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 23s
Docker Build & Deploy / Deploy to Production (push) Successful in 6s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -407,4 +407,4 @@ Web/dist
|
|||||||
.aider*
|
.aider*
|
||||||
.screenshot/*
|
.screenshot/*
|
||||||
|
|
||||||
|
**/nul
|
||||||
|
|||||||
@@ -595,9 +595,26 @@ public class BudgetStatsService(
|
|||||||
logger.LogDebug("开始处理当前及未来月份预算");
|
logger.LogDebug("开始处理当前及未来月份预算");
|
||||||
foreach (var budget in currentBudgetsDict.Values)
|
foreach (var budget in currentBudgetsDict.Values)
|
||||||
{
|
{
|
||||||
// 对于年度预算,如果还没有从归档中添加,则添加
|
// 对于年度预算,需要实时计算当前金额
|
||||||
if (budget.Type == BudgetPeriodType.Year && !processedBudgetIds.Contains(budget.Id))
|
if (budget.Type == BudgetPeriodType.Year)
|
||||||
{
|
{
|
||||||
|
// 如果已经从归档中添加过,需要更新其Current值为实时计算的金额
|
||||||
|
if (processedBudgetIds.Contains(budget.Id))
|
||||||
|
{
|
||||||
|
var realTimeAmount = await CalculateCurrentAmountAsync(budget, statType, referenceDate);
|
||||||
|
var existingItem = result.FirstOrDefault(r => r.Id == budget.Id && r.Type == BudgetPeriodType.Year);
|
||||||
|
if (existingItem != null)
|
||||||
|
{
|
||||||
|
// 更新Current为实时金额(而不是归档的Actual)
|
||||||
|
result.Remove(existingItem);
|
||||||
|
result.Add(existingItem with { Current = realTimeAmount, IsArchive = false });
|
||||||
|
logger.LogInformation("更新年度预算实时金额: {BudgetName} - 归档金额: {ArchiveAmount}, 实时金额: {RealtimeAmount}",
|
||||||
|
budget.Name, existingItem.Current, realTimeAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 如果没有从归档中添加,则新增
|
||||||
var currentAmount = await CalculateCurrentAmountAsync(budget, statType, referenceDate);
|
var currentAmount = await CalculateCurrentAmountAsync(budget, statType, referenceDate);
|
||||||
result.Add(new BudgetStatsItem
|
result.Add(new BudgetStatsItem
|
||||||
{
|
{
|
||||||
@@ -616,6 +633,8 @@ public class BudgetStatsService(
|
|||||||
});
|
});
|
||||||
logger.LogInformation("添加当前年度预算: {BudgetName} - 预算金额: {Limit}, 实际金额: {Current}",
|
logger.LogInformation("添加当前年度预算: {BudgetName} - 预算金额: {Limit}, 实际金额: {Current}",
|
||||||
budget.Name, budget.Limit, currentAmount);
|
budget.Name, budget.Limit, currentAmount);
|
||||||
|
processedBudgetIds.Add(budget.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// 对于月度预算,仅添加当前月的预算项(如果还没有从归档中添加)
|
// 对于月度预算,仅添加当前月的预算项(如果还没有从归档中添加)
|
||||||
else if (budget.Type == BudgetPeriodType.Month)
|
else if (budget.Type == BudgetPeriodType.Month)
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container-flex">
|
<div class="page-container-flex">
|
||||||
<!-- 顶部导航栏 -->
|
<!-- 自定义头部 -->
|
||||||
<van-nav-bar
|
<header class="balance-header">
|
||||||
title="账单"
|
<h1 class="header-title">
|
||||||
placeholder
|
账单
|
||||||
>
|
</h1>
|
||||||
<template #right>
|
<div class="header-actions">
|
||||||
<van-button
|
<van-button
|
||||||
v-if="tabActive === 'email'"
|
v-if="tabActive === 'email'"
|
||||||
size="small"
|
size="small"
|
||||||
type="primary"
|
type="primary"
|
||||||
:loading="syncing"
|
@click="emailRecordRef?.handleSync()"
|
||||||
@click="emailRecordRef.handleSync()"
|
|
||||||
>
|
>
|
||||||
立即同步
|
立即同步
|
||||||
</van-button>
|
</van-button>
|
||||||
@@ -21,26 +20,35 @@
|
|||||||
size="20"
|
size="20"
|
||||||
@click="messageViewRef?.handleMarkAllRead()"
|
@click="messageViewRef?.handleMarkAllRead()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</div>
|
||||||
</van-nav-bar>
|
</header>
|
||||||
<van-tabs
|
|
||||||
v-model:active="tabActive"
|
<!-- 分段控制器 -->
|
||||||
type="card"
|
<div class="tabs-wrapper">
|
||||||
style="margin: 12px 0 2px 0"
|
<div class="segmented-control">
|
||||||
|
<div
|
||||||
|
class="tab-item"
|
||||||
|
:class="{ active: tabActive === 'balance' }"
|
||||||
|
@click="tabActive = 'balance'"
|
||||||
>
|
>
|
||||||
<van-tab
|
<span class="tab-text">账单</span>
|
||||||
title="账单"
|
</div>
|
||||||
name="balance"
|
<div
|
||||||
/>
|
class="tab-item"
|
||||||
<van-tab
|
:class="{ active: tabActive === 'email' }"
|
||||||
title="邮件"
|
@click="tabActive = 'email'"
|
||||||
name="email"
|
>
|
||||||
/>
|
<span class="tab-text">邮件</span>
|
||||||
<van-tab
|
</div>
|
||||||
title="消息"
|
<div
|
||||||
name="message"
|
class="tab-item"
|
||||||
/>
|
:class="{ active: tabActive === 'message' }"
|
||||||
</van-tabs>
|
@click="tabActive = 'message'"
|
||||||
|
>
|
||||||
|
<span class="tab-text">消息</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TransactionsRecord
|
<TransactionsRecord
|
||||||
v-if="tabActive === 'balance'"
|
v-if="tabActive === 'balance'"
|
||||||
@@ -84,15 +92,87 @@ const emailRecordRef = ref(null)
|
|||||||
const messageViewRef = ref(null)
|
const messageViewRef = ref(null)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped lang="scss">
|
||||||
|
@import '@/assets/theme.css';
|
||||||
|
|
||||||
:deep(.van-pull-refresh) {
|
:deep(.van-pull-refresh) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 设置页面容器背景色 */
|
/* ========== 自定义头部 ========== */
|
||||||
:deep(.van-nav-bar) {
|
.balance-header {
|
||||||
background: transparent !important;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 24px;
|
||||||
|
background: transparent;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
font-size: var(--font-2xl);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ========== 分段控制器 ========== */
|
||||||
|
.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;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background: var(--segmented-active-bg);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active .tab-text {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item: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>
|
</style>
|
||||||
|
|||||||
@@ -546,4 +546,62 @@ public class BudgetStatsTest : BaseTest
|
|||||||
// 年度使用率:7350 / 47000 * 100 = 15.64%
|
// 年度使用率:7350 / 47000 * 100 = 15.64%
|
||||||
result.Year.Rate.Should().BeApproximately(7350m / 47000m * 100, 0.01m);
|
result.Year.Rate.Should().BeApproximately(7350m / 47000m * 100, 0.01m);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetCategoryStats_年度收入_不应包含支出预算_Test()
|
||||||
|
{
|
||||||
|
// Arrange: 模拟实际数据库中的情况
|
||||||
|
// 2026年1月,当前日期为2026-02-19
|
||||||
|
var referenceDate = new DateTime(2026, 1, 15);
|
||||||
|
var currentNow = new DateTime(2026, 2, 19);
|
||||||
|
_dateTimeProvider.Now.Returns(currentNow);
|
||||||
|
|
||||||
|
var budgets = new List<BudgetRecord>
|
||||||
|
{
|
||||||
|
// Type=1 表示月度预算,Category=0 表示支出(这些不应该被计入收入统计)
|
||||||
|
new() { Id = 1, Name = "工作餐预算", Limit = 700, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "G工作餐", StartDate = new DateTime(2026, 1, 6) },
|
||||||
|
new() { Id = 2, Name = "副业投资", Limit = 2000, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "Z钻石福袋", StartDate = new DateTime(2026, 1, 7) },
|
||||||
|
new() { Id = 3, Name = "通勤支出", Limit = 240, Category = BudgetCategory.Expense, Type = BudgetPeriodType.Month, SelectedCategories = "D地铁通勤", StartDate = new DateTime(2026, 1, 7) },
|
||||||
|
|
||||||
|
// Category=1 表示收入(只有这些应该被计入收入统计)
|
||||||
|
new() { Id = 4, Name = "工资-SYE", Limit = 6100, Category = BudgetCategory.Income, Type = BudgetPeriodType.Month, IsMandatoryExpense = true, SelectedCategories = "G工资SYE", StartDate = new DateTime(2026, 1, 7) },
|
||||||
|
new() { Id = 5, Name = "副业收入", Limit = 6000, Category = BudgetCategory.Income, Type = BudgetPeriodType.Month, SelectedCategories = "", StartDate = new DateTime(2026, 1, 7) },
|
||||||
|
new() { Id = 6, Name = "公积金收入", Limit = 5540, Category = BudgetCategory.Income, Type = BudgetPeriodType.Month, IsMandatoryExpense = true, SelectedCategories = "G公积金", StartDate = new DateTime(2026, 1, 7) },
|
||||||
|
new() { Id = 7, Name = "工资-SC", Limit = 17500, Category = BudgetCategory.Income, Type = BudgetPeriodType.Month, IsMandatoryExpense = true, SelectedCategories = "G工资SC", StartDate = new DateTime(2026, 1, 16) }
|
||||||
|
};
|
||||||
|
|
||||||
|
_budgetRepository.GetAllAsync().Returns(budgets);
|
||||||
|
|
||||||
|
// 模拟实际收入金额
|
||||||
|
_budgetRepository.GetCurrentAmountAsync(Arg.Any<BudgetRecord>(), Arg.Any<DateTime>(), Arg.Any<DateTime>())
|
||||||
|
.Returns(args =>
|
||||||
|
{
|
||||||
|
var budget = (BudgetRecord)args[0];
|
||||||
|
// 假设当前月(2月)没有收入记录
|
||||||
|
return 0m;
|
||||||
|
});
|
||||||
|
|
||||||
|
_transactionStatisticsService.GetFilteredTrendStatisticsAsync(Arg.Any<DateTime>(), Arg.Any<DateTime>(), Arg.Any<TransactionType>(), Arg.Any<List<string>>(), Arg.Any<bool>())
|
||||||
|
.Returns(new Dictionary<DateTime, decimal>());
|
||||||
|
|
||||||
|
_budgetArchiveRepository.GetArchiveAsync(Arg.Any<int>(), Arg.Any<int>())
|
||||||
|
.Returns((BudgetArchive?)null);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _service.GetCategoryStatsAsync(BudgetCategory.Income, referenceDate);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
// 年度已收应该是:1月的4个收入预算
|
||||||
|
// 1月归档:工资-SYE(6100) + 副业收入(6000) + 公积金收入(5540) + 工资-SC(17500) = 35140
|
||||||
|
// 2月当前:0(假设没有实际收入)
|
||||||
|
// 3-12月未来:0
|
||||||
|
// 总计应该约等于 35140 (取决于硬性收入的调整逻辑)
|
||||||
|
|
||||||
|
// 重点:year.limit 应该只包含收入预算,不应该包含支出预算
|
||||||
|
// 正确的年度limit应该是:(6100 + 6000 + 5540 + 17500) * (1 + 11) = 35140 * 12 = 421680
|
||||||
|
// 或者更准确地说:1月归档(35140) + 2月当前月(35140) + 未来10个月(35140 * 10) = 35140 * 12
|
||||||
|
|
||||||
|
result.Year.Limit.Should().BeGreaterThan(35000 * 11); // 至少应该是35140的11倍以上
|
||||||
|
result.Year.Limit.Should().BeLessThan(36000 * 12); // 不应该超过36000的12倍
|
||||||
|
}
|
||||||
}
|
}
|
||||||
BIN
balance-page-after.png
Normal file
BIN
balance-page-after.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
2
null
2
null
@@ -1,2 +0,0 @@
|
|||||||
ERROR: Invalid argument/option - 'F:/'.
|
|
||||||
Type "TASKKILL /?" for usage.
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-02-19
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
余额页面(BalanceView.vue)当前使用 Vant UI 库的默认组件:`van-nav-bar` 作为头部导航,`van-tabs type="card"` 作为标签页切换。而统计页面(statisticsV2)已经采用了更现代的自定义设计:使用 `DateSelectHeader` 组件作为头部,`TimePeriodTabs` 作为分段控制器。
|
||||||
|
|
||||||
|
现有代码结构:
|
||||||
|
- `Web/src/views/BalanceView.vue` - 主要页面组件
|
||||||
|
- `Web/src/components/DateSelectHeader.vue` - 统计页面头部组件(包含日期切换)
|
||||||
|
- `Web/src/components/TimePeriodTabs.vue` - 分段控制器组件(周/月/年)
|
||||||
|
- `Web/src/assets/theme.css` - CSS 变量定义
|
||||||
|
|
||||||
|
余额页面的三个标签页(账单/邮件/消息)需要保留的特定功能:
|
||||||
|
- 邮件页:右上角"立即同步"按钮
|
||||||
|
- 消息页:右上角"标记全部已读"图标
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- 统一余额页面和统计页面的视觉风格
|
||||||
|
- 使用分段控制器(segmented control)替代卡片式标签
|
||||||
|
- 创建简化版头部布局(仅文字标题,无日期切换)
|
||||||
|
- 保留所有现有功能(同步按钮、标记已读图标等)
|
||||||
|
- 复用现有样式变量,确保主题切换(亮色/暗色)正常工作
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- 不修改统计页面的任何代码
|
||||||
|
- 不改变余额页面的业务逻辑
|
||||||
|
- 不创建新的通用组件(直接在 BalanceView.vue 内实现)
|
||||||
|
- 不调整底层数据加载逻辑
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### 决策 1: 组件复用策略
|
||||||
|
|
||||||
|
**选择**: 直接在 BalanceView.vue 内实现简化版头部和标签页,不复用 DateSelectHeader 或 TimePeriodTabs
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- DateSelectHeader 包含日期切换逻辑,余额页面不需要
|
||||||
|
- TimePeriodTabs 是三个固定选项(周/月/年),余额页面需要不同的三个选项(账单/邮件/消息)
|
||||||
|
- 直接实现可以更灵活地保留右上角操作按钮(同步、标记已读)
|
||||||
|
- 代码量较小,不值得抽象为通用组件
|
||||||
|
|
||||||
|
**备选方案**:
|
||||||
|
- ❌ 修改 TimePeriodTabs 支持自定义选项 → 增加组件复杂度,影响统计页面
|
||||||
|
- ❌ 创建新的通用组件 BalanceTabsComponent → 过度设计,只有一个使用场景
|
||||||
|
|
||||||
|
### 决策 2: 头部布局结构
|
||||||
|
|
||||||
|
**选择**: 采用 `<header>` 标签 + 标题文字 + 右侧操作按钮的扁平布局
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 与 DateSelectHeader 的视觉效果一致
|
||||||
|
- 灵活支持动态显示右侧按钮(根据当前标签页)
|
||||||
|
- 无需复杂的嵌套结构
|
||||||
|
|
||||||
|
**样式规范**:
|
||||||
|
- 使用 `var(--font-2xl)` 作为标题字号
|
||||||
|
- 使用 `var(--text-primary)` 作为标题颜色
|
||||||
|
- 透明背景:`background: transparent`
|
||||||
|
- 内边距:`padding: 8px 24px`
|
||||||
|
|
||||||
|
### 决策 3: 分段控制器实现
|
||||||
|
|
||||||
|
**选择**: 复制 TimePeriodTabs 的 CSS 结构,修改选项数据
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 确保视觉效果 100% 一致
|
||||||
|
- 避免重复造轮子
|
||||||
|
- 样式已经过亮色/暗色主题测试
|
||||||
|
|
||||||
|
**关键样式**:
|
||||||
|
```scss
|
||||||
|
.segmented-control {
|
||||||
|
display: flex;
|
||||||
|
background: var(--segmented-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 4px;
|
||||||
|
gap: 4px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-item.active {
|
||||||
|
background: var(--segmented-active-bg);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 决策 4: 右侧操作按钮布局
|
||||||
|
|
||||||
|
**选择**: 使用 `v-if` 条件渲染,根据 `tabActive` 动态显示不同按钮
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
- 简洁明了,易于维护
|
||||||
|
- 与现有代码风格一致
|
||||||
|
- 性能开销可忽略
|
||||||
|
|
||||||
|
**实现**:
|
||||||
|
```vue
|
||||||
|
<van-button v-if="tabActive === 'email'" @click="...">立即同步</van-button>
|
||||||
|
<van-icon v-if="tabActive === 'message'" name="passed" @click="..."/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
### 风险 1: CSS 变量未定义
|
||||||
|
**风险**: `theme.css` 中可能缺少 `--segmented-bg` 或 `--segmented-active-bg` 变量
|
||||||
|
**缓解**: 实施前检查变量定义,必要时添加兼容值
|
||||||
|
|
||||||
|
### 风险 2: 移动端触控区域
|
||||||
|
**风险**: 自定义按钮的触控区域可能不如 Vant 组件优化
|
||||||
|
**缓解**: 确保按钮最小高度 44px,添加 `-webkit-tap-highlight-color: transparent`
|
||||||
|
|
||||||
|
### 权衡 1: 代码复用 vs 灵活性
|
||||||
|
**权衡**: 选择在 BalanceView.vue 内实现而非抽象组件
|
||||||
|
**影响**: 如果未来其他页面需要类似布局,需要重复代码
|
||||||
|
**判断**: 可接受 - 当前只有一个使用场景,过早抽象会增加维护成本
|
||||||
|
|
||||||
|
### 权衡 2: 视觉一致性 vs 功能保留
|
||||||
|
**权衡**: 需要在统一样式的同时保留特定功能按钮
|
||||||
|
**影响**: 头部布局不能完全照搬 DateSelectHeader
|
||||||
|
**判断**: 功能优先 - 同步和标记已读是核心功能,不能牺牲
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
当前余额页面(BalanceView.vue)使用的是 Vant 默认的 `van-nav-bar` 和 `van-tabs type="card"` 样式,与统计页面(statisticsV2)的现代化分段控制器设计不一致,导致用户在不同页面间切换时体验不连贯。统一两个页面的视觉风格能提升整体应用的一致性和现代感。
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- 移除余额页面的 `<van-nav-bar>` 组件
|
||||||
|
- 新增自定义文字标题区域(简化版头部,不含日期切换功能)
|
||||||
|
- 将 `<van-tabs type="card">` 替换为分段控制器样式(segmented control)
|
||||||
|
- 标签文字调整为:账单 / 邮件 / 消息
|
||||||
|
- 保留现有功能:同步按钮(邮件页)、标记已读图标(消息页)
|
||||||
|
- 样式与统计页面保持一致,使用相同的 CSS 变量和设计规范
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
- `balance-segmented-tabs`: 余额页面的分段控制器标签组件,支持账单/邮件/消息三个标签页切换,样式与统计页面一致
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
- `balance-page-header`: 余额页面头部布局从 Vant 导航栏改为自定义文字标题布局,与统计页面视觉风格保持一致
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
**受影响文件**:
|
||||||
|
- `Web/src/views/BalanceView.vue` - 主要修改文件,调整头部和标签页结构
|
||||||
|
|
||||||
|
**样式依赖**:
|
||||||
|
- 复用 `@/assets/theme.css` 中的 CSS 变量
|
||||||
|
- 参考 `Web/src/components/DateSelectHeader.vue` 和 `Web/src/components/TimePeriodTabs.vue` 的样式规范
|
||||||
|
|
||||||
|
**用户体验影响**:
|
||||||
|
- ✅ 正向影响:界面更统一、现代化,视觉体验更连贯
|
||||||
|
- ✅ 无破坏性变更:所有现有功能保持不变,仅样式调整
|
||||||
|
- ✅ 移动端触控体验优化:分段控制器更符合移动端交互习惯
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 页面头部布局
|
||||||
|
余额页面 SHALL 使用自定义头部布局,包含标题文字和右侧操作按钮区域,替代原有的 `van-nav-bar` 组件。
|
||||||
|
|
||||||
|
#### Scenario: 头部显示
|
||||||
|
- **WHEN** 用户打开余额页面
|
||||||
|
- **THEN** 系统在顶部显示"账单"文字标题(不带返回按钮或边框)
|
||||||
|
|
||||||
|
#### Scenario: 头部样式
|
||||||
|
- **WHEN** 用户查看头部区域
|
||||||
|
- **THEN** 系统使用 `var(--font-2xl)` 字号和 `var(--text-primary)` 颜色显示标题,背景透明
|
||||||
|
|
||||||
|
### Requirement: 右侧操作按钮
|
||||||
|
头部右侧 SHALL 根据当前选中的标签页动态显示不同的操作按钮。
|
||||||
|
|
||||||
|
#### Scenario: 邮件页同步按钮
|
||||||
|
- **WHEN** 用户切换到"邮件"标签
|
||||||
|
- **THEN** 系统在头部右侧显示"立即同步"按钮
|
||||||
|
|
||||||
|
#### Scenario: 消息页标记已读图标
|
||||||
|
- **WHEN** 用户切换到"消息"标签
|
||||||
|
- **THEN** 系统在头部右侧显示"标记全部已读"图标
|
||||||
|
|
||||||
|
#### Scenario: 账单页无操作按钮
|
||||||
|
- **WHEN** 用户切换到"账单"标签
|
||||||
|
- **THEN** 系统不在头部右侧显示任何操作按钮
|
||||||
|
|
||||||
|
### Requirement: 视觉风格一致性
|
||||||
|
头部布局 SHALL 与统计页面的 `DateSelectHeader` 组件保持视觉风格一致。
|
||||||
|
|
||||||
|
#### Scenario: 内边距规范
|
||||||
|
- **WHEN** 用户查看头部区域
|
||||||
|
- **THEN** 系统使用 `padding: 8px 24px` 作为头部内边距
|
||||||
|
|
||||||
|
#### Scenario: 主题适配
|
||||||
|
- **WHEN** 用户切换应用主题(亮色/暗色)
|
||||||
|
- **THEN** 头部文字颜色自动更新为对应主题的 `var(--text-primary)` 值
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 分段控制器布局
|
||||||
|
余额页面的标签切换 SHALL 使用分段控制器(segmented control)样式,包含三个选项:账单、邮件、消息。
|
||||||
|
|
||||||
|
#### Scenario: 分段控制器显示
|
||||||
|
- **WHEN** 用户打开余额页面
|
||||||
|
- **THEN** 系统显示分段控制器,包含"账单"、"邮件"、"消息"三个选项
|
||||||
|
|
||||||
|
#### Scenario: 默认选中账单
|
||||||
|
- **WHEN** 用户首次打开余额页面且 URL 无 tab 参数
|
||||||
|
- **THEN** 系统默认选中"账单"标签
|
||||||
|
|
||||||
|
### Requirement: 标签页切换交互
|
||||||
|
用户 SHALL 能够点击分段控制器中的任意选项切换标签页。
|
||||||
|
|
||||||
|
#### Scenario: 点击切换标签
|
||||||
|
- **WHEN** 用户点击"邮件"选项
|
||||||
|
- **THEN** 系统切换到邮件视图,并更新分段控制器的选中状态
|
||||||
|
|
||||||
|
#### Scenario: 路由参数切换
|
||||||
|
- **WHEN** 用户通过底部导航栏跳转到余额页面且携带 `?tab=message` 参数
|
||||||
|
- **THEN** 系统自动选中"消息"标签
|
||||||
|
|
||||||
|
### Requirement: 视觉样式一致性
|
||||||
|
分段控制器 SHALL 使用与统计页面相同的样式规范和 CSS 变量。
|
||||||
|
|
||||||
|
#### Scenario: 亮色主题样式
|
||||||
|
- **WHEN** 应用处于亮色主题
|
||||||
|
- **THEN** 分段控制器使用 `var(--segmented-bg)` 作为背景色,选中项使用 `var(--segmented-active-bg)`
|
||||||
|
|
||||||
|
#### Scenario: 暗色主题样式
|
||||||
|
- **WHEN** 应用处于暗色主题
|
||||||
|
- **THEN** 分段控制器自动应用暗色主题对应的 CSS 变量值
|
||||||
|
|
||||||
|
### Requirement: 移动端触控体验
|
||||||
|
分段控制器 SHALL 针对移动端优化触控体验。
|
||||||
|
|
||||||
|
#### Scenario: 触控区域
|
||||||
|
- **WHEN** 用户在移动设备上点击选项
|
||||||
|
- **THEN** 系统确保每个选项的可点击区域高度不小于 40px
|
||||||
|
|
||||||
|
#### Scenario: 触控反馈
|
||||||
|
- **WHEN** 用户点击未选中的选项
|
||||||
|
- **THEN** 系统显示背景色过渡动画(0.3s cubic-bezier)
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
## 1. 移除旧组件
|
||||||
|
|
||||||
|
- [x] 1.1 删除 BalanceView.vue 中的 `<van-nav-bar>` 组件及相关代码
|
||||||
|
- [x] 1.2 删除 `<van-tabs>` 组件及相关代码
|
||||||
|
- [x] 1.3 清理不再使用的样式规则(`:deep(.van-nav-bar)` 等)
|
||||||
|
|
||||||
|
## 2. 实现自定义头部
|
||||||
|
|
||||||
|
- [x] 2.1 创建 `<header>` 标签结构,包含标题文字区域
|
||||||
|
- [x] 2.2 实现右侧操作按钮区域,支持动态显示
|
||||||
|
- [x] 2.3 添加 `v-if` 逻辑:邮件页显示"立即同步"按钮
|
||||||
|
- [x] 2.4 添加 `v-if` 逻辑:消息页显示"标记已读"图标
|
||||||
|
- [x] 2.5 编写头部样式:字号 `var(--font-2xl)`,颜色 `var(--text-primary)`,内边距 `8px 24px`
|
||||||
|
- [x] 2.6 确保背景透明:`background: transparent`
|
||||||
|
|
||||||
|
## 3. 实现分段控制器
|
||||||
|
|
||||||
|
- [x] 3.1 创建分段控制器容器 `.segmented-control`,包含三个选项(账单/邮件/消息)
|
||||||
|
- [x] 3.2 实现 `.tab-item` 样式:flex 布局,圆角 6px,高度 40px
|
||||||
|
- [x] 3.3 实现 `.tab-item.active` 样式:背景 `var(--segmented-active-bg)`,投影效果
|
||||||
|
- [x] 3.4 添加点击事件处理:更新 `tabActive` 响应式变量
|
||||||
|
- [x] 3.5 添加 hover 效果:未选中项显示半透明背景
|
||||||
|
- [x] 3.6 添加过渡动画:`transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)`
|
||||||
|
|
||||||
|
## 4. 样式变量检查
|
||||||
|
|
||||||
|
- [x] 4.1 验证 `theme.css` 中存在 `--segmented-bg` 变量
|
||||||
|
- [x] 4.2 验证 `theme.css` 中存在 `--segmented-active-bg` 变量
|
||||||
|
- [x] 4.3 如变量缺失,添加兼容值或定义新变量
|
||||||
|
|
||||||
|
## 5. 移动端优化
|
||||||
|
|
||||||
|
- [x] 5.1 确保分段控制器每个选项的最小高度为 40px
|
||||||
|
- [x] 5.2 添加 `-webkit-tap-highlight-color: transparent` 移除触控高亮
|
||||||
|
- [x] 5.3 添加 `user-select: none` 防止文字选中
|
||||||
|
|
||||||
|
## 6. 功能测试
|
||||||
|
|
||||||
|
- [x] 6.1 测试默认加载:账单页为默认选中
|
||||||
|
- [x] 6.2 测试点击切换:三个标签页能正常切换
|
||||||
|
- [x] 6.3 测试路由参数:URL 参数 `?tab=email` 和 `?tab=message` 能正确切换
|
||||||
|
- [x] 6.4 测试同步按钮:邮件页的"立即同步"按钮功能正常
|
||||||
|
- [x] 6.5 测试标记已读:消息页的标记已读图标功能正常
|
||||||
|
- [x] 6.6 测试主题切换:亮色和暗色主题样式正常
|
||||||
|
|
||||||
|
## 7. 视觉验证
|
||||||
|
|
||||||
|
- [x] 7.1 对比统计页面,确认头部样式一致(字号、颜色、间距)
|
||||||
|
- [x] 7.2 对比统计页面,确认分段控制器样式一致(背景、圆角、投影)
|
||||||
|
- [x] 7.3 检查移动端显示:确保布局在小屏幕上正常
|
||||||
|
- [x] 7.4 检查动画流畅度:确保切换动画无卡顿
|
||||||
38
openspec/specs/balance-page-header/spec.md
Normal file
38
openspec/specs/balance-page-header/spec.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 页面头部布局
|
||||||
|
余额页面 SHALL 使用自定义头部布局,包含标题文字和右侧操作按钮区域,替代原有的 `van-nav-bar` 组件。
|
||||||
|
|
||||||
|
#### Scenario: 头部显示
|
||||||
|
- **WHEN** 用户打开余额页面
|
||||||
|
- **THEN** 系统在顶部显示"账单"文字标题(不带返回按钮或边框)
|
||||||
|
|
||||||
|
#### Scenario: 头部样式
|
||||||
|
- **WHEN** 用户查看头部区域
|
||||||
|
- **THEN** 系统使用 `var(--font-2xl)` 字号和 `var(--text-primary)` 颜色显示标题,背景透明
|
||||||
|
|
||||||
|
### Requirement: 右侧操作按钮
|
||||||
|
头部右侧 SHALL 根据当前选中的标签页动态显示不同的操作按钮。
|
||||||
|
|
||||||
|
#### Scenario: 邮件页同步按钮
|
||||||
|
- **WHEN** 用户切换到"邮件"标签
|
||||||
|
- **THEN** 系统在头部右侧显示"立即同步"按钮
|
||||||
|
|
||||||
|
#### Scenario: 消息页标记已读图标
|
||||||
|
- **WHEN** 用户切换到"消息"标签
|
||||||
|
- **THEN** 系统在头部右侧显示"标记全部已读"图标
|
||||||
|
|
||||||
|
#### Scenario: 账单页无操作按钮
|
||||||
|
- **WHEN** 用户切换到"账单"标签
|
||||||
|
- **THEN** 系统不在头部右侧显示任何操作按钮
|
||||||
|
|
||||||
|
### Requirement: 视觉风格一致性
|
||||||
|
头部布局 SHALL 与统计页面的 `DateSelectHeader` 组件保持视觉风格一致。
|
||||||
|
|
||||||
|
#### Scenario: 内边距规范
|
||||||
|
- **WHEN** 用户查看头部区域
|
||||||
|
- **THEN** 系统使用 `padding: 8px 24px` 作为头部内边距
|
||||||
|
|
||||||
|
#### Scenario: 主题适配
|
||||||
|
- **WHEN** 用户切换应用主题(亮色/暗色)
|
||||||
|
- **THEN** 头部文字颜色自动更新为对应主题的 `var(--text-primary)` 值
|
||||||
45
openspec/specs/balance-segmented-tabs/spec.md
Normal file
45
openspec/specs/balance-segmented-tabs/spec.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 分段控制器布局
|
||||||
|
余额页面的标签切换 SHALL 使用分段控制器(segmented control)样式,包含三个选项:账单、邮件、消息。
|
||||||
|
|
||||||
|
#### Scenario: 分段控制器显示
|
||||||
|
- **WHEN** 用户打开余额页面
|
||||||
|
- **THEN** 系统显示分段控制器,包含"账单"、"邮件"、"消息"三个选项
|
||||||
|
|
||||||
|
#### Scenario: 默认选中账单
|
||||||
|
- **WHEN** 用户首次打开余额页面且 URL 无 tab 参数
|
||||||
|
- **THEN** 系统默认选中"账单"标签
|
||||||
|
|
||||||
|
### Requirement: 标签页切换交互
|
||||||
|
用户 SHALL 能够点击分段控制器中的任意选项切换标签页。
|
||||||
|
|
||||||
|
#### Scenario: 点击切换标签
|
||||||
|
- **WHEN** 用户点击"邮件"选项
|
||||||
|
- **THEN** 系统切换到邮件视图,并更新分段控制器的选中状态
|
||||||
|
|
||||||
|
#### Scenario: 路由参数切换
|
||||||
|
- **WHEN** 用户通过底部导航栏跳转到余额页面且携带 `?tab=message` 参数
|
||||||
|
- **THEN** 系统自动选中"消息"标签
|
||||||
|
|
||||||
|
### Requirement: 视觉样式一致性
|
||||||
|
分段控制器 SHALL 使用与统计页面相同的样式规范和 CSS 变量。
|
||||||
|
|
||||||
|
#### Scenario: 亮色主题样式
|
||||||
|
- **WHEN** 应用处于亮色主题
|
||||||
|
- **THEN** 分段控制器使用 `var(--segmented-bg)` 作为背景色,选中项使用 `var(--segmented-active-bg)`
|
||||||
|
|
||||||
|
#### Scenario: 暗色主题样式
|
||||||
|
- **WHEN** 应用处于暗色主题
|
||||||
|
- **THEN** 分段控制器自动应用暗色主题对应的 CSS 变量值
|
||||||
|
|
||||||
|
### Requirement: 移动端触控体验
|
||||||
|
分段控制器 SHALL 针对移动端优化触控体验。
|
||||||
|
|
||||||
|
#### Scenario: 触控区域
|
||||||
|
- **WHEN** 用户在移动设备上点击选项
|
||||||
|
- **THEN** 系统确保每个选项的可点击区域高度不小于 40px
|
||||||
|
|
||||||
|
#### Scenario: 触控反馈
|
||||||
|
- **WHEN** 用户点击未选中的选项
|
||||||
|
- **THEN** 系统显示背景色过渡动画(0.3s cubic-bezier)
|
||||||
Reference in New Issue
Block a user