All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 4m27s
Docker Build & Deploy / Deploy to Production (push) Successful in 7s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 1s
1198 lines
30 KiB
Markdown
1198 lines
30 KiB
Markdown
---
|
|
name: pancli-implement
|
|
description: 根据 pancli 设计图(.pen 文件)进行前后端完整实施的专业技能,确保高度还原设计,数据表支持,交互确认
|
|
license: MIT
|
|
compatibility: Requires .pen design files, Entity layer, WebApi controllers, Vue 3 frontend
|
|
metadata:
|
|
author: EmailBill Development Team
|
|
version: "1.0.0"
|
|
generatedBy: opencode
|
|
lastUpdated: "2026-02-03"
|
|
source: "基于 .pans/v2.pen 设计实施流程"
|
|
---
|
|
|
|
# pancli-implement - 设计图实施技能
|
|
|
|
> 根据 pancli 设计图(.pen 文件)进行前后端完整实施,确保高度还原设计稿,包括数据表确认、API 开发、前端组件实现和交互验证。
|
|
|
|
## 何时使用此技能
|
|
|
|
**总是使用此技能当:**
|
|
- 需要根据 .pen 设计文件实施前后端功能
|
|
- 将设计图转换为可运行的代码
|
|
- 需要确认数据表结构是否支持设计需求
|
|
- 实施过程中需要询问用户确认交互细节
|
|
- 高度还原设计稿的视觉效果和交互体验
|
|
|
|
**触发条件:**
|
|
- 用户提到 "实施设计图"、"根据设计实现"、"按照 pancli"
|
|
- 任务涉及从 .pen 文件到代码的转换
|
|
- 需要同时开发前端和后端
|
|
|
|
## 核心实施原则
|
|
|
|
### 0. **模块化架构:复杂页面拆分**
|
|
|
|
**重要:** 对于复杂页面(如日历、统计概览、仪表板),必须采用**模块化架构**:
|
|
|
|
**目录结构规范:**
|
|
```
|
|
views/
|
|
calendarV2/ # 页面目录
|
|
Index.vue # 主页面(编排器)
|
|
modules/ # 模块目录
|
|
Calendar.vue # 日历模块
|
|
Stats.vue # 统计模块
|
|
TransactionList.vue # 交易列表模块
|
|
```
|
|
|
|
**模块化原则:**
|
|
|
|
1. **高内聚模块**
|
|
- 每个模块是独立的功能单元
|
|
- 模块内部管理自己的数据和状态
|
|
- 模块自己调用 API 获取数据
|
|
- 模块自己处理内部事件
|
|
|
|
2. **Index.vue 作为编排器**
|
|
- **只负责布局和事件编排**
|
|
- **不调用 API,不管理业务数据**
|
|
- 接收用户交互(如日期选择)
|
|
- 通过 props 传递必要参数给模块
|
|
- 通过事件监听模块的输出
|
|
- 协调模块间的通信
|
|
|
|
3. **模块间通信**
|
|
- 父 → 子:通过 props 传递(只传必要参数,如 `selectedDate`)
|
|
- 子 → 父:通过 emit 发送事件(如 `@transaction-click`)
|
|
- 兄弟模块:通过父组件中转事件
|
|
|
|
4. **不使用 TypeScript**
|
|
- 项目使用纯 JavaScript + Vue 3 Composition API
|
|
- **不要**在 `<script setup>` 添加 `lang="ts"`
|
|
- 使用 JSDoc 注释代替类型定义
|
|
|
|
**示例:calendarV2 模块化架构**
|
|
|
|
```vue
|
|
<!-- Index.vue - 编排器(仅负责布局和事件协调) -->
|
|
<template>
|
|
<div class="page-container-flex">
|
|
<!-- 头部 -->
|
|
<header class="header">
|
|
<button @click="changeMonth(-1)">上一月</button>
|
|
<h1>{{ currentMonth }}</h1>
|
|
<button @click="changeMonth(1)">下一月</button>
|
|
</header>
|
|
|
|
<!-- 可滚动内容 -->
|
|
<div class="scroll-content">
|
|
<!-- 日历模块:自己查询日历数据 -->
|
|
<CalendarModule
|
|
:current-date="currentDate"
|
|
:selected-date="selectedDate"
|
|
@day-click="onDayClick"
|
|
/>
|
|
|
|
<!-- 统计模块:自己查询统计数据 -->
|
|
<StatsModule
|
|
:selected-date="selectedDate"
|
|
/>
|
|
|
|
<!-- 交易列表模块:自己查询交易数据 -->
|
|
<TransactionListModule
|
|
:selected-date="selectedDate"
|
|
@transaction-click="onTransactionClick"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import CalendarModule from './modules/Calendar.vue'
|
|
import StatsModule from './modules/Stats.vue'
|
|
import TransactionListModule from './modules/TransactionList.vue'
|
|
|
|
const router = useRouter()
|
|
|
|
// Index 只管理日期状态(共享参数)
|
|
const currentDate = ref(new Date())
|
|
const selectedDate = ref(new Date())
|
|
|
|
const currentMonth = computed(() => {
|
|
return `${currentDate.value.getFullYear()}年${currentDate.value.getMonth() + 1}月`
|
|
})
|
|
|
|
// Index 只负责事件编排
|
|
const onDayClick = (day) => {
|
|
selectedDate.value = new Date(day.date)
|
|
}
|
|
|
|
const onTransactionClick = (txn) => {
|
|
router.push({ path: '/transaction-detail', query: { id: txn.id } })
|
|
}
|
|
|
|
const changeMonth = (offset) => {
|
|
const newDate = new Date(currentDate.value)
|
|
newDate.setMonth(newDate.getMonth() + offset)
|
|
currentDate.value = newDate
|
|
}
|
|
|
|
// ❌ Index 不调用 API
|
|
// ❌ Index 不管理业务数据(如交易列表、统计数据)
|
|
// ✅ Index 只管理共享的状态(如日期)
|
|
// ✅ Index 只负责路由跳转和事件编排
|
|
</script>
|
|
```
|
|
|
|
```vue
|
|
<!-- modules/Stats.vue - 高内聚模块 -->
|
|
<template>
|
|
<div class="stats-module">
|
|
<h2>{{ selectedDateFormatted }}</h2>
|
|
<div class="stats-card">
|
|
<div class="stats-item">
|
|
<span>当日支出</span>
|
|
<div class="value">¥{{ expense.toFixed(2) }}</div>
|
|
</div>
|
|
<div class="stats-item">
|
|
<span>当日收入</span>
|
|
<div class="value">¥{{ income.toFixed(2) }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch, computed } from 'vue'
|
|
import { getTransactionsByDate } from '@/api/transactionRecord'
|
|
|
|
const props = defineProps({
|
|
selectedDate: Date // 从父组件接收必要参数
|
|
})
|
|
|
|
// ✅ 模块自己管理数据
|
|
const expense = ref(0)
|
|
const income = ref(0)
|
|
const loading = ref(false)
|
|
|
|
// ✅ 模块自己调用 API
|
|
const fetchStats = async (date) => {
|
|
try {
|
|
loading.value = true
|
|
const dateKey = formatDate(date)
|
|
const response = await getTransactionsByDate(dateKey)
|
|
|
|
if (response.success) {
|
|
expense.value = response.data
|
|
.filter(t => t.type === 0)
|
|
.reduce((sum, t) => sum + t.amount, 0)
|
|
|
|
income.value = response.data
|
|
.filter(t => t.type === 1)
|
|
.reduce((sum, t) => sum + t.amount, 0)
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// ✅ 模块监听 props 变化并重新加载数据
|
|
watch(() => props.selectedDate, async (newDate) => {
|
|
if (newDate) {
|
|
await fetchStats(newDate)
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// ✅ 模块内部工具函数
|
|
const formatDate = (date) => {
|
|
const y = date.getFullYear()
|
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
const d = String(date.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${d}`
|
|
}
|
|
|
|
const selectedDateFormatted = computed(() => {
|
|
const y = props.selectedDate.getFullYear()
|
|
const m = props.selectedDate.getMonth() + 1
|
|
const d = props.selectedDate.getDate()
|
|
return `${y}年${m}月${d}日`
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* 模块自己的样式 */
|
|
.stats-module {
|
|
padding: 24px;
|
|
}
|
|
/* ... */
|
|
</style>
|
|
```
|
|
|
|
```vue
|
|
<!-- modules/TransactionList.vue - 高内聚模块 -->
|
|
<template>
|
|
<div class="transaction-list-module">
|
|
<div class="header">
|
|
<h2>交易记录</h2>
|
|
<span class="badge">{{ transactions.length }} Items</span>
|
|
</div>
|
|
|
|
<van-loading v-if="loading" />
|
|
<div v-else-if="transactions.length === 0" class="empty">
|
|
当天暂无交易
|
|
</div>
|
|
<div v-else class="list">
|
|
<div
|
|
v-for="txn in transactions"
|
|
:key="txn.id"
|
|
class="item"
|
|
@click="handleClick(txn)"
|
|
>
|
|
<!-- 交易卡片内容 -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch } from 'vue'
|
|
import { getTransactionsByDate } from '@/api/transactionRecord'
|
|
|
|
const props = defineProps({
|
|
selectedDate: Date
|
|
})
|
|
|
|
// ✅ 向父组件发送事件
|
|
const emit = defineEmits(['transactionClick'])
|
|
|
|
// ✅ 模块自己管理数据
|
|
const transactions = ref([])
|
|
const loading = ref(false)
|
|
|
|
// ✅ 模块自己调用 API
|
|
const fetchTransactions = async (date) => {
|
|
try {
|
|
loading.value = true
|
|
const dateKey = formatDate(date)
|
|
const response = await getTransactionsByDate(dateKey)
|
|
|
|
if (response.success) {
|
|
transactions.value = response.data.map(txn => ({
|
|
id: txn.id,
|
|
name: txn.reason || '未知交易',
|
|
amount: formatAmount(txn.amount, txn.type),
|
|
time: formatTime(txn.occurredAt),
|
|
type: txn.type
|
|
}))
|
|
}
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
// ✅ 模块监听 props 变化
|
|
watch(() => props.selectedDate, async (newDate) => {
|
|
if (newDate) {
|
|
await fetchTransactions(newDate)
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// ✅ 模块处理内部事件
|
|
const handleClick = (txn) => {
|
|
// 向父组件发送事件,由父组件决定跳转逻辑
|
|
emit('transactionClick', txn)
|
|
}
|
|
|
|
// ✅ 模块内部工具函数
|
|
const formatDate = (date) => { /* ... */ }
|
|
const formatAmount = (amount, type) => { /* ... */ }
|
|
const formatTime = (datetime) => { /* ... */ }
|
|
</script>
|
|
```
|
|
|
|
**模块化的好处:**
|
|
- ✅ **关注点分离**:Index 只管布局和编排,模块专注业务逻辑
|
|
- ✅ **独立维护**:修改某个模块不影响其他模块
|
|
- ✅ **可复用性**:模块可以在其他页面复用
|
|
- ✅ **可测试性**:每个模块可以独立测试
|
|
- ✅ **代码清晰**:职责明确,代码结构清晰
|
|
|
|
**反模式(不要这样做):**
|
|
```vue
|
|
<!-- ❌ 错误:Index.vue 包含所有逻辑 -->
|
|
<template>
|
|
<div>
|
|
<!-- 日历 -->
|
|
<div class="calendar">
|
|
<!-- 几百行日历代码 -->
|
|
</div>
|
|
|
|
<!-- 统计 -->
|
|
<div class="stats">
|
|
<!-- 统计代码 -->
|
|
</div>
|
|
|
|
<!-- 交易列表 -->
|
|
<div class="transactions">
|
|
<!-- 交易列表代码 -->
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
// ❌ 所有 API 调用都在 Index.vue
|
|
const calendarData = ref([])
|
|
const statsData = ref({})
|
|
const transactions = ref([])
|
|
|
|
const fetchCalendarData = async () => { /* ... */ }
|
|
const fetchStatsData = async () => { /* ... */ }
|
|
const fetchTransactions = async () => { /* ... */ }
|
|
|
|
// ❌ Index.vue 变得又长又难维护
|
|
</script>
|
|
```
|
|
|
|
**什么时候需要模块化?**
|
|
- 页面有 3 个以上独立功能区域
|
|
- 每个区域有自己的数据查询需求
|
|
- 页面代码超过 300 行
|
|
- 功能区域可能被其他页面复用
|
|
|
|
**什么时候不需要模块化?**
|
|
- 简单页面(如登录页、详情页)
|
|
- 只有一个主要功能
|
|
- 代码少于 200 行
|
|
- 无明显的功能分区
|
|
|
|
### 1. **数据优先,提前确认**
|
|
|
|
在开始编码前,**必须先确认数据表结构**:
|
|
|
|
**检查清单:**
|
|
- [ ] 读取 .pen 设计文件,理解所有数据需求
|
|
- [ ] 检查现有 Entity 层,确认是否支持设计需求
|
|
- [ ] 如果需要新增字段/表/枚举值,**先询问用户确认**
|
|
- [ ] 列出所有数据变更,等待用户批准后再继续
|
|
|
|
**示例确认流程:**
|
|
```markdown
|
|
## 数据表结构确认
|
|
|
|
根据设计图分析,需要以下数据支持:
|
|
|
|
### 现有实体:
|
|
✅ TransactionRecord - 支持交易记录
|
|
✅ BudgetRecord - 支持预算管理
|
|
✅ TransactionCategory - 支持分类
|
|
|
|
### 需要变更:
|
|
1. **TransactionCategory 表新增字段**
|
|
- 字段名: IconName (string)
|
|
- 用途: 存储 lucide 图标名称(如 "utensils"、"shopping-cart")
|
|
- 原因: 设计图使用图标名称渲染,现有 Icon 字段存储 SVG 不适合
|
|
|
|
2. **BudgetRecord 新增计算字段**
|
|
- 字段名: ProgressPercentage (decimal, 计算属性)
|
|
- 用途: 当前支出占预算的百分比
|
|
- 原因: 设计图显示进度条需要
|
|
|
|
### 是否批准以上变更?如不批准,请提供替代方案。
|
|
```
|
|
|
|
**不得做:**
|
|
- ❌ 未经确认直接修改 Entity 层
|
|
- ❌ 假设字段存在而不检查
|
|
- ❌ 跳过数据表确认直接实施前端
|
|
|
|
### 2. **交互细节,逐一确认**
|
|
|
|
设计图无法表达所有交互逻辑,**必须向用户确认**:
|
|
|
|
**需要确认的交互:**
|
|
1. **按钮点击行为**
|
|
- 跳转到哪个页面?
|
|
- 弹出什么对话框?
|
|
- 触发什么 API?
|
|
|
|
2. **周期切换逻辑**(如周/月/年)
|
|
- 周:本周一到周日?还是最近7天?
|
|
- 月:自然月?还是最近30天?
|
|
- 年:当年1-12月?还是最近12个月?
|
|
|
|
3. **数据计算逻辑**
|
|
- 环比百分比如何计算?
|
|
- 趋势图数据范围?
|
|
- 空状态显示什么?
|
|
|
|
4. **跳转目标**
|
|
- "查看全部"跳转到哪里?
|
|
- "管理预算"跳转到哪个页面?
|
|
- 通知按钮打开什么?
|
|
|
|
**示例确认:**
|
|
```markdown
|
|
## 交互行为确认
|
|
|
|
### 1. 顶部通知按钮
|
|
设计图:bell 图标按钮
|
|
**问题:** 点击后跳转到哪里?
|
|
- A. 消息中心(MessageView.vue)
|
|
- B. 通知列表(新页面)
|
|
- C. 弹出通知面板
|
|
|
|
### 2. 周期切换(周/月/年)
|
|
设计图:三个 segment 切换按钮
|
|
**问题:** 周期定义?
|
|
- 周:本周一到周日 or 最近7天
|
|
- 月:当前自然月 or 最近30天
|
|
- 年:当年1-12月 or 最近12个月
|
|
|
|
### 3. 核心指标徽章(如 "-15%")
|
|
设计图:显示百分比变化
|
|
**问题:** 对比哪个周期?
|
|
- A. 与上一周期对比(上周/上月/去年)
|
|
- B. 与同期对比(去年同周/去年同月)
|
|
- C. 与预算目标对比
|
|
|
|
请明确以上交互逻辑。
|
|
```
|
|
|
|
### 3. **高度还原设计稿**
|
|
|
|
**视觉还原标准:**
|
|
- 严格按照设计图的间距、字号、颜色、圆角
|
|
- 使用设计图中的实际字体(如 DM Sans, Bricolage Grotesque, JetBrains Mono)
|
|
- 匹配卡片阴影、边框样式
|
|
- 复现设计图的布局层级
|
|
|
|
**对照检查:**
|
|
```typescript
|
|
// 设计图:
|
|
// - padding: 24px
|
|
// - gap: 16px
|
|
// - cornerRadius: 12px
|
|
// - fontSize: 14px
|
|
// - fontWeight: 600
|
|
|
|
// 实现:
|
|
<div class="metrics-card">
|
|
<style scoped>
|
|
.metrics-card {
|
|
padding: 24px; /* ✅ 匹配设计 */
|
|
gap: 16px; /* ✅ 匹配设计 */
|
|
border-radius: 12px; /* ✅ 匹配设计 */
|
|
font-size: 14px; /* ✅ 匹配设计 */
|
|
font-weight: 600; /* ✅ 匹配设计 */
|
|
}
|
|
</style>
|
|
</div>
|
|
```
|
|
|
|
**字体映射:**
|
|
| 设计图字体 | CSS 实现 |
|
|
|-----------|---------|
|
|
| Bricolage Grotesque | `font-family: 'Bricolage Grotesque', system-ui, sans-serif` |
|
|
| DM Sans | `font-family: 'DM Sans', -apple-system, sans-serif` |
|
|
| JetBrains Mono | `font-family: 'JetBrains Mono', 'SF Mono', monospace` |
|
|
| Newsreader | `font-family: 'Newsreader', Georgia, serif` |
|
|
|
|
### 4. **前后端同步开发**
|
|
|
|
**开发顺序:**
|
|
```
|
|
1. 确认数据表 → 2. 开发后端 API → 3. 实施前端组件 → 4. 集成测试 → 5. 视觉验证
|
|
```
|
|
|
|
**后端开发标准:**
|
|
- 在 `Entity/` 中新增/修改实体
|
|
- 在 `Repository/` 中添加数据访问方法
|
|
- 在 `Service/` 中实现业务逻辑
|
|
- 在 `WebApi/Controllers/` 中创建 API 端点
|
|
- 编写 xUnit 测试验证逻辑
|
|
|
|
**前端开发标准:**
|
|
- 在 `Web/src/views/` 中创建/修改页面组件
|
|
- 在 `Web/src/components/` 中提取可复用组件
|
|
- 在 `Web/src/api/` 中定义 API 客户端
|
|
- 使用 Vue 3 Composition API + `<script setup>`
|
|
- 使用 Vant UI 组件库
|
|
- 严格遵循设计图的样式(SCSS)
|
|
|
|
### 5. **测试与验证**
|
|
|
|
**验证清单:**
|
|
- [ ] 后端测试通过(`dotnet test`)
|
|
- [ ] 前端构建通过(`pnpm build`)
|
|
- [ ] API 返回数据正确
|
|
- [ ] 页面显示与设计图一致
|
|
- [ ] 交互行为符合确认的逻辑
|
|
- [ ] 亮色/暗色主题都正常
|
|
- [ ] 空状态、加载状态、错误状态处理
|
|
|
|
## 实施工作流程
|
|
|
|
### 阶段 1: 设计分析与数据确认
|
|
|
|
```typescript
|
|
// 1. 读取设计文件
|
|
pencil_batch_get(
|
|
filePath: ".pans/v2.pen",
|
|
nodeIds: ["jF3SD"], // 设计图根节点 ID
|
|
readDepth: 3
|
|
)
|
|
|
|
// 2. 分析数据需求
|
|
// - 列出所有显示的数据字段
|
|
// - 检查现有 Entity 是否支持
|
|
// - 列出需要新增的字段/表
|
|
|
|
// 3. 读取现有实体
|
|
glob("Entity/*.cs")
|
|
read("Entity/TransactionRecord.cs")
|
|
read("Entity/BudgetRecord.cs")
|
|
read("Entity/TransactionCategory.cs")
|
|
|
|
// 4. 向用户确认数据变更
|
|
// (生成确认清单,等待用户批准)
|
|
```
|
|
|
|
### 阶段 2: 交互逻辑确认
|
|
|
|
```markdown
|
|
## 交互确认清单
|
|
|
|
根据设计图,以下交互需要确认:
|
|
|
|
### 1. [按钮/链接名称]
|
|
**位置:** [设计图位置描述]
|
|
**问题:** [需要确认的问题]
|
|
**选项:**
|
|
- A. [选项1]
|
|
- B. [选项2]
|
|
- C. [其他]
|
|
|
|
### 2. [数据计算逻辑]
|
|
**显示内容:** [设计图显示的数据]
|
|
**问题:** [计算逻辑或数据来源]
|
|
**选项:**
|
|
- A. [选项1]
|
|
- B. [选项2]
|
|
|
|
请逐一明确以上交互。
|
|
```
|
|
|
|
### 阶段 3: 后端开发
|
|
|
|
**3.1 实体层变更(如需要)**
|
|
```csharp
|
|
// Entity/TransactionCategory.cs
|
|
namespace Entity;
|
|
|
|
/// <summary>
|
|
/// 交易分类
|
|
/// </summary>
|
|
public class TransactionCategory : BaseEntity
|
|
{
|
|
/// <summary>
|
|
/// 分类名称
|
|
/// </summary>
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 交易类型(支出/收入)
|
|
/// </summary>
|
|
public TransactionType Type { get; set; }
|
|
|
|
/// <summary>
|
|
/// 图标名称(lucide 图标,如 "utensils", "shopping-cart")
|
|
/// </summary>
|
|
public string? IconName { get; set; }
|
|
|
|
// 现有 Icon 字段保留用于自定义 SVG
|
|
[Column(StringLength = -1)]
|
|
public string? Icon { get; set; }
|
|
}
|
|
```
|
|
|
|
**3.2 Repository 扩展(如需要)**
|
|
```csharp
|
|
// Repository/ITransactionRepository.cs
|
|
public interface ITransactionRepository
|
|
{
|
|
Task<List<WeeklyStatisticsDto>> GetWeeklyStatisticsAsync(
|
|
int year, int weekNumber);
|
|
|
|
Task<ComparisonDto> GetComparisonDataAsync(
|
|
int year, int month, PeriodType periodType);
|
|
}
|
|
```
|
|
|
|
**3.3 Controller API**
|
|
```csharp
|
|
// WebApi/Controllers/TransactionRecordController.cs
|
|
|
|
/// <summary>
|
|
/// 获取周统计数据
|
|
/// </summary>
|
|
[HttpGet("GetWeeklyStatistics")]
|
|
public async Task<BaseResponse<WeeklyStatisticsDto>> GetWeeklyStatistics(
|
|
[FromQuery] int year,
|
|
[FromQuery] int weekNumber)
|
|
{
|
|
var data = await _repository.GetWeeklyStatisticsAsync(year, weekNumber);
|
|
return BaseResponse<WeeklyStatisticsDto>.Success(data);
|
|
}
|
|
```
|
|
|
|
**3.4 测试**
|
|
```csharp
|
|
// WebApi.Test/TransactionStatisticsTest.cs
|
|
[Fact]
|
|
public async Task GetWeeklyStatistics_本周_返回正确数据()
|
|
{
|
|
// Arrange
|
|
var testData = CreateTestWeeklyData();
|
|
_repo.GetWeeklyStatisticsAsync(2026, 5).Returns(testData);
|
|
|
|
// Act
|
|
var result = await _controller.GetWeeklyStatistics(2026, 5);
|
|
|
|
// Assert
|
|
result.Success.Should().BeTrue();
|
|
result.Data.TotalExpense.Should().Be(1248.50m);
|
|
result.Data.TransactionCount.Should().Be(127);
|
|
}
|
|
```
|
|
|
|
### 阶段 4: 前端开发
|
|
|
|
**4.1 API 客户端**
|
|
```javascript
|
|
// Web/src/api/statistics.js
|
|
|
|
/**
|
|
* 获取周统计数据
|
|
* @param {Object} params - 查询参数
|
|
* @param {number} params.year - 年份
|
|
* @param {number} params.weekNumber - 周数(1-53)
|
|
* @returns {Promise<{success: boolean, data: Object}>}
|
|
*/
|
|
export const getWeeklyStatistics = (params) => {
|
|
return request({
|
|
url: '/TransactionRecord/GetWeeklyStatistics',
|
|
method: 'get',
|
|
params
|
|
})
|
|
}
|
|
```
|
|
|
|
**4.2 Vue 组件实现**
|
|
```vue
|
|
<!-- Web/src/views/StatisticsOverview.vue -->
|
|
<template>
|
|
<div class="statistics-overview">
|
|
<!-- 按照设计图的结构实现 -->
|
|
<van-nav-bar placeholder>
|
|
<template #title>
|
|
<div class="nav-title">2026年</div>
|
|
</template>
|
|
<template #right>
|
|
<van-icon name="bell-o" size="20" @click="handleNotification" />
|
|
</template>
|
|
</van-nav-bar>
|
|
|
|
<div class="content-wrapper">
|
|
<!-- 周期切换 -->
|
|
<div class="period-section">
|
|
<div class="segment-control">
|
|
<div
|
|
class="segment-item"
|
|
:class="{ active: period === 'week' }"
|
|
@click="period = 'week'"
|
|
>
|
|
周
|
|
</div>
|
|
<div
|
|
class="segment-item"
|
|
:class="{ active: period === 'month' }"
|
|
@click="period = 'month'"
|
|
>
|
|
月
|
|
</div>
|
|
<div
|
|
class="segment-item"
|
|
:class="{ active: period === 'year' }"
|
|
@click="period = 'year'"
|
|
>
|
|
年
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 核心指标 -->
|
|
<div class="metrics-section">
|
|
<h3 class="section-header">核心指标</h3>
|
|
<div class="metrics-row">
|
|
<!-- 总支出卡片 -->
|
|
<div class="metric-card">
|
|
<div class="card-header">
|
|
<span class="label">总支出</span>
|
|
<span class="badge expense">-15%</span>
|
|
</div>
|
|
<div class="value">¥{{ formatMoney(stats.totalExpense) }}</div>
|
|
<div class="description">较上期减少</div>
|
|
</div>
|
|
|
|
<!-- 交易笔数卡片 -->
|
|
<div class="metric-card">
|
|
<div class="card-header">
|
|
<span class="label">交易笔数</span>
|
|
<span class="badge success">+8</span>
|
|
</div>
|
|
<div class="value">{{ stats.transactionCount }}</div>
|
|
<div class="chart-bars">
|
|
<!-- 7天趋势小图 -->
|
|
<div
|
|
v-for="(bar, index) in stats.weeklyBars"
|
|
:key="index"
|
|
class="bar"
|
|
:style="{ height: bar.height + 'px' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
### 4.2 模块化开发示例 (High Cohesion)
|
|
|
|
**A. 编排层 (Orchestrator)**
|
|
`src/views/Statistics/Index.vue`
|
|
- **职责**:只负责布局结构和子模块引用,**不**包含具体业务逻辑或 API 调用。
|
|
- **结构**:干净的 `<template>` 包含各个 Modules。
|
|
|
|
```vue
|
|
<template>
|
|
<div class="statistics-view">
|
|
<van-nav-bar title="统计概览" left-arrow @click-left="onClickLeft" />
|
|
|
|
<div class="content-wrapper">
|
|
<!-- 模块 1: 核心指标卡片 -->
|
|
<MetricsCard />
|
|
|
|
<!-- 模块 2: 趋势图表 -->
|
|
<TrendChart />
|
|
|
|
<!-- 模块 3: 分类占比 -->
|
|
<CategoryPie />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { useRouter } from 'vue-router'
|
|
// 导入子模块
|
|
import MetricsCard from './modules/MetricsCard.vue'
|
|
import TrendChart from './modules/TrendChart.vue'
|
|
import CategoryPie from './modules/CategoryPie.vue'
|
|
|
|
const router = useRouter()
|
|
const onClickLeft = () => router.back()
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.statistics-view {
|
|
min-height: 100vh;
|
|
background: var(--van-background-2);
|
|
}
|
|
|
|
.content-wrapper {
|
|
padding: 16px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
</style>
|
|
```
|
|
|
|
**B. 业务模块 (Business Module)**
|
|
`src/views/Statistics/modules/MetricsCard.vue`
|
|
- **职责**:独立负责特定功能区域的 UI 和数据获取。
|
|
- **原则**:自包含 (Self-contained) - 自己 import api,自己 fetch data。
|
|
|
|
```vue
|
|
<template>
|
|
<div class="metrics-card">
|
|
<!-- 周期切换 -->
|
|
<div class="segment-control">
|
|
<div
|
|
v-for="p in periods"
|
|
:key="p.value"
|
|
class="segment-item"
|
|
:class="{ active: currentPeriod === p.value }"
|
|
@click="currentPeriod = p.value"
|
|
>
|
|
{{ p.label }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 数据展示 -->
|
|
<div class="metrics-content">
|
|
<div class="label">总支出</div>
|
|
<div class="value">¥{{ formatMoney(stats.totalExpense) }}</div>
|
|
|
|
<div class="chart-bars">
|
|
<div
|
|
v-for="(bar, index) in stats.weeklyBars"
|
|
:key="index"
|
|
class="bar"
|
|
:style="{ height: bar.height + 'px' }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, watch } from 'vue'
|
|
import { getWeeklyStatistics } from '@/api/statistics' // 模块自己请求数据
|
|
|
|
const periods = [
|
|
{ label: '周', value: 'week' },
|
|
{ label: '月', value: 'month' }
|
|
]
|
|
const currentPeriod = ref('week')
|
|
const stats = ref({ totalExpense: 0, weeklyBars: [] })
|
|
|
|
const fetchData = async () => {
|
|
const res = await getWeeklyStatistics({ period: currentPeriod.value })
|
|
if (res.success) {
|
|
stats.value = res.data
|
|
}
|
|
}
|
|
|
|
// 监听周期变化自动刷新
|
|
watch(currentPeriod, fetchData)
|
|
|
|
onMounted(fetchData)
|
|
|
|
const formatMoney = (val) => Number(val).toFixed(2)
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.metrics-card {
|
|
background: #fff;
|
|
border-radius: 12px;
|
|
padding: 16px;
|
|
|
|
// 严格按照设计图实现内部样式
|
|
.value {
|
|
font-family: 'JetBrains Mono', monospace;
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
margin: 12px 0;
|
|
}
|
|
|
|
// 暗色模式适配
|
|
@media (prefers-color-scheme: dark) {
|
|
background: #1c1c1e;
|
|
.value { color: #fff; }
|
|
}
|
|
}
|
|
</style>
|
|
```
|
|
|
|
### 阶段 5: 集成测试与验证
|
|
|
|
**5.1 运行测试**
|
|
```bash
|
|
# 后端测试
|
|
dotnet test WebApi.Test/WebApi.Test.csproj
|
|
|
|
# 前端构建
|
|
cd Web
|
|
pnpm lint
|
|
pnpm build
|
|
```
|
|
|
|
**5.2 视觉验证**
|
|
```typescript
|
|
// 使用 pencil 截图对比
|
|
pencil_get_screenshot(nodeId: "jF3SD") // 设计图截图
|
|
|
|
// 在浏览器中截图实际效果
|
|
// 对比以下方面:
|
|
// - 间距是否一致(padding, gap)
|
|
// - 字号是否匹配(fontSize)
|
|
// - 颜色是否准确(fill, textColor)
|
|
// - 圆角是否正确(cornerRadius)
|
|
// - 布局是否对齐(layout, justifyContent, alignItems)
|
|
```
|
|
|
|
**5.3 交互验证**
|
|
- [ ] 周期切换按钮工作正常
|
|
- [ ] 核心指标数据正确显示
|
|
- [ ] 徽章百分比计算准确
|
|
- [ ] 点击卡片跳转正确
|
|
- [ ] 空状态显示合理
|
|
- [ ] 加载状态流畅
|
|
- [ ] 错误处理友好
|
|
|
|
## 避免的反模式
|
|
|
|
### ❌ 不要这样做:
|
|
|
|
**1. 未经确认修改数据表**
|
|
```csharp
|
|
// ❌ 直接新增字段
|
|
public class TransactionCategory : BaseEntity
|
|
{
|
|
public string IconName { get; set; } // ❌ 未经用户确认
|
|
}
|
|
```
|
|
|
|
**2. 猜测交互逻辑**
|
|
```vue
|
|
// ❌ 假设跳转目标
|
|
<van-icon @click="router.push('/notifications')" />
|
|
<!-- ❌ 未确认通知按钮跳转到哪里 -->
|
|
```
|
|
|
|
**3. 不按设计图样式**
|
|
```scss
|
|
// 设计图: padding: 24px
|
|
.card {
|
|
padding: 20px; // ❌ 不匹配设计
|
|
}
|
|
```
|
|
|
|
**4. 跳过测试**
|
|
```bash
|
|
# ❌ 不运行测试直接提交
|
|
git add .
|
|
git commit -m "完成实施"
|
|
```
|
|
|
|
**5. 忽略暗色模式**
|
|
```scss
|
|
.card {
|
|
background: #FFFFFF; // ❌ 只实现亮色模式
|
|
}
|
|
```
|
|
|
|
### ✅ 应该这样做:
|
|
|
|
**1. 数据变更先确认**
|
|
```markdown
|
|
## 数据表变更请求
|
|
|
|
需要在 TransactionCategory 新增以下字段:
|
|
- IconName (string): 存储图标名称
|
|
- 用途: 前端渲染 lucide 图标
|
|
|
|
是否批准?
|
|
```
|
|
|
|
**2. 交互逻辑先确认**
|
|
```markdown
|
|
## 交互确认
|
|
|
|
通知按钮点击后:
|
|
- A. 跳转到 MessageView
|
|
- B. 弹出通知列表
|
|
- C. 其他
|
|
|
|
请明确选择。
|
|
```
|
|
|
|
**3. 严格按照设计图**
|
|
```scss
|
|
// 设计图: padding: 24px, gap: 16px, borderRadius: 12px
|
|
.card {
|
|
padding: 24px; // ✅ 匹配设计
|
|
gap: 16px; // ✅ 匹配设计
|
|
border-radius: 12px; // ✅ 匹配设计
|
|
}
|
|
```
|
|
|
|
**4. 完整测试**
|
|
```bash
|
|
# ✅ 运行所有测试
|
|
dotnet test
|
|
cd Web && pnpm lint && pnpm build
|
|
```
|
|
|
|
**5. 支持暗色模式**
|
|
```scss
|
|
.card {
|
|
background: #FFFFFF;
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
background: #18181B; // ✅ 暗色适配
|
|
}
|
|
}
|
|
```
|
|
|
|
## 委派与任务管理
|
|
|
|
**使用此技能时:**
|
|
|
|
```typescript
|
|
delegate_task(
|
|
category: "visual-engineering",
|
|
load_skills: ["pancli-implement", "frontend-ui-ux"],
|
|
description: "根据 v2.pen 设计图实施统计概览页面",
|
|
prompt: `
|
|
## 任务
|
|
根据 .pans/v2.pen 中的统计概览设计图(节点 ID: jF3SD)实施完整的前后端功能
|
|
|
|
## 预期结果
|
|
- [ ] Entity 层变更(如需要,经用户确认)
|
|
- [ ] 后端 API 开发并通过测试
|
|
- [ ] 前端页面高度还原设计图
|
|
- [ ] 交互逻辑符合用户确认
|
|
- [ ] 亮色/暗色主题都正常
|
|
- [ ] 所有测试通过(dotnet test, pnpm build)
|
|
|
|
## 必需工具
|
|
- pencil_batch_get: 读取设计文件
|
|
- pencil_get_screenshot: 设计图截图
|
|
- read: 读取现有代码
|
|
- glob: 查找文件
|
|
- edit/write: 修改/新增代码
|
|
- bash: 运行测试
|
|
|
|
## 必须做
|
|
1. **数据确认阶段**
|
|
- 读取设计文件分析数据需求
|
|
- 检查现有 Entity 是否支持
|
|
- 如需变更,生成确认清单并等待用户批准
|
|
|
|
2. **交互确认阶段**
|
|
- 列出所有不确定的交互逻辑
|
|
- 生成确认清单并等待用户明确
|
|
|
|
3. **后端开发**
|
|
- 按用户批准修改 Entity
|
|
- 实现 Repository 数据访问
|
|
- 创建 Controller API
|
|
- 编写 xUnit 测试并通过
|
|
|
|
4. **前端开发**
|
|
- **复杂页面强制模块化**: 采用 Index.vue + modules/ 结构
|
|
- Index.vue 仅编排,modules 负责数据获取
|
|
- 定义 API 客户端(src/api/)
|
|
- 严格按照设计图样式(间距、字号、颜色、圆角)
|
|
- 使用设计图中的实际字体
|
|
- 支持亮色/暗色主题
|
|
|
|
5. **验证阶段**
|
|
- 运行后端测试:dotnet test
|
|
- 运行前端构建:pnpm lint && pnpm build
|
|
- 视觉对比设计图截图
|
|
- 验证交互逻辑
|
|
|
|
## 不得做
|
|
- ❌ 未经确认直接修改 Entity
|
|
- ❌ 猜测交互逻辑而不询问
|
|
- ❌ 禁止复杂页面写成单文件组件 (Monolithic)
|
|
- ❌ 不按设计图样式随意实现
|
|
- ❌ 跳过测试
|
|
- ❌ 忽略暗色模式
|
|
- ❌ 使用占位数据而不调用真实 API
|
|
|
|
## 上下文
|
|
- 设计文件: .pans/v2.pen
|
|
- 设计节点 ID: jF3SD (Statistics - Overview - Light)
|
|
- 现有页面: Web/src/views/StatisticsView.vue
|
|
- 现有 API: Web/src/api/statistics.js
|
|
- Entity 层: Entity/*.cs
|
|
- Controller 层: WebApi/Controllers/*.cs
|
|
|
|
## 实施流程
|
|
1. 数据确认 → 等待批准
|
|
2. 交互确认 → 等待明确
|
|
3. 后端开发 → 测试通过
|
|
4. 前端开发 → 视觉验证
|
|
5. 集成测试 → 全部通过
|
|
`,
|
|
run_in_background: false
|
|
)
|
|
```
|
|
|
|
## 快速参考
|
|
|
|
### 数据表确认模板
|
|
|
|
```markdown
|
|
## 数据表结构确认
|
|
|
|
### 现有实体分析
|
|
✅ [实体名] - 支持 [功能]
|
|
❌ [实体名] - 缺少 [字段]
|
|
|
|
### 需要变更
|
|
1. **[表名] 新增字段**
|
|
- 字段名: [字段名] ([类型])
|
|
- 用途: [用途说明]
|
|
- 原因: [为什么需要]
|
|
|
|
2. **[表名] 修改字段**
|
|
- 字段名: [字段名]
|
|
- 变更: [原类型] → [新类型]
|
|
- 原因: [为什么修改]
|
|
|
|
### 是否批准?
|
|
```
|
|
|
|
### 交互确认模板
|
|
|
|
```markdown
|
|
## 交互逻辑确认
|
|
|
|
### 1. [交互元素名称]
|
|
**设计位置:** [描述]
|
|
**问题:** [需要确认的问题]
|
|
**选项:**
|
|
- A. [选项1]
|
|
- B. [选项2]
|
|
- C. 其他:[请说明]
|
|
|
|
### 2. [数据计算逻辑]
|
|
**设计显示:** [设计图显示的内容]
|
|
**问题:** [计算逻辑或数据来源]
|
|
**选项:**
|
|
- A. [选项1]
|
|
- B. [选项2]
|
|
|
|
请逐一明确。
|
|
```
|
|
|
|
### 样式映射表
|
|
|
|
| 设计图属性 | CSS 属性 | 示例 |
|
|
|-----------|---------|------|
|
|
| padding | padding | `padding: 24px` |
|
|
| gap | gap | `gap: 16px` |
|
|
| cornerRadius | border-radius | `border-radius: 12px` |
|
|
| fill | background | `background: #FFFFFF` |
|
|
| textColor | color | `color: #1A1A1A` |
|
|
| fontSize | font-size | `font-size: 14px` |
|
|
| fontWeight | font-weight | `font-weight: 600` |
|
|
| fontFamily | font-family | `font-family: 'DM Sans', sans-serif` |
|
|
| width: "fill_container" | width: 100% | `width: 100%` |
|
|
| height: "hug_contents" | height: auto | `height: auto` |
|
|
| layout: "vertical" | flex-direction: column | `flex-direction: column` |
|
|
| layout: "horizontal" | flex-direction: row | `flex-direction: row` |
|
|
| justifyContent: "space_between" | justify-content: space-between | `justify-content: space-between` |
|
|
| alignItems: "center" | align-items: center | `align-items: center` |
|
|
|
|
---
|
|
|
|
**版本:** 1.0.0
|
|
**最后更新:** 2026-02-03
|
|
**维护者:** EmailBill 开发团队
|