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
31 KiB
31 KiB
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 |
|
pancli-implement - 设计图实施技能
根据 pancli 设计图(.pen 文件)进行前后端完整实施,确保高度还原设计稿,包括数据表确认、API 开发、前端组件实现和交互验证。
何时使用此技能
总是使用此技能当:
- 需要根据 .pen 设计文件实施前后端功能
- 将设计图转换为可运行的代码
- 需要确认数据表结构是否支持设计需求
- 实施过程中需要询问用户确认交互细节
- 高度还原设计稿的视觉效果和交互体验
触发条件:
- 用户提到 "实施设计图"、"根据设计实现"、"按照 pancli"
- 任务涉及从 .pen 文件到代码的转换
- 需要同时开发前端和后端
核心实施原则
0. 模块化架构:复杂页面拆分
重要: 对于复杂页面(如日历、统计概览、仪表板),必须采用模块化架构:
目录结构规范:
views/
calendarV2/ # 页面目录
Index.vue # 主页面(编排器)
modules/ # 模块目录
Calendar.vue # 日历模块
Stats.vue # 统计模块
TransactionList.vue # 交易列表模块
模块化原则:
-
高内聚模块
- 每个模块是独立的功能单元
- 模块内部管理自己的数据和状态
- 模块自己调用 API 获取数据
- 模块自己处理内部事件
-
Index.vue 作为编排器
- 只负责布局和事件编排
- 不调用 API,不管理业务数据
- 接收用户交互(如日期选择)
- 通过 props 传递必要参数给模块
- 通过事件监听模块的输出
- 协调模块间的通信
-
模块间通信
- 父 → 子:通过 props 传递(只传必要参数,如
selectedDate) - 子 → 父:通过 emit 发送事件(如
@transaction-click) - 兄弟模块:通过父组件中转事件
- 父 → 子:通过 props 传递(只传必要参数,如
-
不使用 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. 交互细节,逐一确认
设计图无法表达所有交互逻辑,必须向用户确认:
需要确认的交互:
-
按钮点击行为
- 跳转到哪个页面?
- 弹出什么对话框?
- 触发什么 API?
-
周期切换逻辑(如周/月/年)
- 周:本周一到周日?还是最近7天?
- 月:自然月?还是最近30天?
- 年:当年1-12月?还是最近12个月?
-
数据计算逻辑
- 环比百分比如何计算?
- 趋势图数据范围?
- 空状态显示什么?
-
跳转目标
- "查看全部"跳转到哪里?
- "管理预算"跳转到哪个页面?
- 通知按钮打开什么?
示例确认:
## 交互行为确认
### 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 开发团队