Files
EmailBill/.opencode/skills/pancli-implement/SKILL.cn.md

1198 lines
30 KiB
Markdown
Raw Normal View History

2026-02-11 13:00:01 +08:00
---
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 开发团队