Files
EmailBill/.opencode/skills/pancli-implement/SKILL.md
SunCheng 952c75bf08
All checks were successful
Docker Build & Deploy / Build Docker Image (push) Successful in 54s
Docker Build & Deploy / Deploy to Production (push) Successful in 9s
Docker Build & Deploy / Cleanup Dangling Images (push) Successful in 1s
Docker Build & Deploy / WeChat Notification (push) Successful in 2s
1
2026-02-03 17:56:32 +08:00

1198 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 开发团队