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

31 KiB
Raw Blame History

name, description, license, compatibility, metadata
name description license compatibility metadata
pancli-implement 根据 pancli 设计图(.pen 文件)进行前后端完整实施的专业技能,确保高度还原设计,数据表支持,交互确认 MIT Requires .pen design files, Entity layer, WebApi controllers, Vue 3 frontend
author version generatedBy lastUpdated source
EmailBill Development Team 1.0.0 opencode 2026-02-03 基于 .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 模块化架构

<!-- 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>
<!-- 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>
<!-- 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 只管布局和编排,模块专注业务逻辑
  • 独立维护:修改某个模块不影响其他模块
  • 可复用性:模块可以在其他页面复用
  • 可测试性:每个模块可以独立测试
  • 代码清晰:职责明确,代码结构清晰

反模式(不要这样做):

<!--  错误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 层,确认是否支持设计需求
  • 如果需要新增字段/表/枚举值,先询问用户确认
  • 列出所有数据变更,等待用户批准后再继续

示例确认流程:

## 数据表结构确认

根据设计图分析,需要以下数据支持:

### 现有实体:
✅ 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. 跳转目标

    • "查看全部"跳转到哪里?
    • "管理预算"跳转到哪个页面?
    • 通知按钮打开什么?

示例确认:

## 交互行为确认

### 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
  • 匹配卡片阴影、边框样式
  • 复现设计图的布局层级

对照检查:

// 设计图:
// - 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: 设计分析与数据确认

// 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: 交互逻辑确认

## 交互确认清单

根据设计图,以下交互需要确认:

### 1. [按钮/链接名称]
**位置:** [设计图位置描述]
**问题:** [需要确认的问题]
**选项:**
- A. [选项1]
- B. [选项2]
- C. [其他]

### 2. [数据计算逻辑]
**显示内容:** [设计图显示的数据]
**问题:** [计算逻辑或数据来源]
**选项:**
- A. [选项1]
- B. [选项2]

请逐一明确以上交互。

阶段 3: 后端开发

3.1 实体层变更(如需要)

// 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 扩展(如需要)

// 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

// 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 测试

// 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 客户端

// 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 组件实现

<!-- 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。
<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 运行测试

# 后端测试
dotnet test WebApi.Test/WebApi.Test.csproj

# 前端构建
cd Web
pnpm lint
pnpm build

5.2 视觉验证

// 使用 pencil 截图对比
pencil_get_screenshot(nodeId: "jF3SD") // 设计图截图

// 在浏览器中截图实际效果
// 对比以下方面:
// - 间距是否一致padding, gap
// - 字号是否匹配fontSize
// - 颜色是否准确fill, textColor
// - 圆角是否正确cornerRadius
// - 布局是否对齐layout, justifyContent, alignItems

5.3 交互验证

  • 周期切换按钮工作正常
  • 核心指标数据正确显示
  • 徽章百分比计算准确
  • 点击卡片跳转正确
  • 空状态显示合理
  • 加载状态流畅
  • 错误处理友好

避免的反模式

不要这样做:

1. 未经确认修改数据表

// ❌ 直接新增字段
public class TransactionCategory : BaseEntity
{
    public string IconName { get; set; } // ❌ 未经用户确认
}

2. 猜测交互逻辑

// ❌ 假设跳转目标
<van-icon @click="router.push('/notifications')" /> 
<!--  未确认通知按钮跳转到哪里 -->

3. 不按设计图样式

// 设计图: padding: 24px
.card {
  padding: 20px; // ❌ 不匹配设计
}

4. 跳过测试

# ❌ 不运行测试直接提交
git add .
git commit -m "完成实施"

5. 忽略暗色模式

.card {
  background: #FFFFFF; // ❌ 只实现亮色模式
}

应该这样做:

1. 数据变更先确认

## 数据表变更请求

需要在 TransactionCategory 新增以下字段:
- IconName (string): 存储图标名称
- 用途: 前端渲染 lucide 图标

是否批准?

2. 交互逻辑先确认

## 交互确认

通知按钮点击后:
- A. 跳转到 MessageView
- B. 弹出通知列表
- C. 其他

请明确选择。

3. 严格按照设计图

// 设计图: padding: 24px, gap: 16px, borderRadius: 12px
.card {
  padding: 24px;        // ✅ 匹配设计
  gap: 16px;            // ✅ 匹配设计
  border-radius: 12px;  // ✅ 匹配设计
}

4. 完整测试

# ✅ 运行所有测试
dotnet test
cd Web && pnpm lint && pnpm build

5. 支持暗色模式

.card {
  background: #FFFFFF;

  @media (prefers-color-scheme: dark) {
    background: #18181B; // ✅ 暗色适配
  }
}

委派与任务管理

使用此技能时:

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
)

快速参考

数据表确认模板

## 数据表结构确认

### 现有实体分析
✅ [实体名] - 支持 [功能]
❌ [实体名] - 缺少 [字段]

### 需要变更
1. **[表名] 新增字段**
   - 字段名: [字段名] ([类型])
   - 用途: [用途说明]
   - 原因: [为什么需要]

2. **[表名] 修改字段**
   - 字段名: [字段名]
   - 变更: [原类型] → [新类型]
   - 原因: [为什么修改]

### 是否批准?

交互确认模板

## 交互逻辑确认

### 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 开发团队