proto-gen

根据 PRD 终稿生成 B 端 HTML 高保真原型,参考项目现有前端组件风格,每个页面独立文件,支持单页微调。 当用户提到"生成原型"、"做原型"、"画页面"、"出原型"、"HTML 原型"、"页面原型"、 "把 PRD 变成页面"、"根据需求出个原型"时使用此 skill。 即使用户只是说"这个需求的页面大概长什么样",也可以用这个 skill 快速生成可浏览的原型。

Purpose

PRD 写好了,但开发和业务方对"页面长什么样"的理解往往不一致。传统做法是用 Figma/Axure 画原型,但对于 B 端后台系统,大量页面都是表格、表单、弹窗的组合——用 HTML 直接生成反而更快,而且可以直接在浏览器中交互。

这个 skill 读取 PRD 和项目前端代码,识别页面列表和交互流程,生成一套可浏览的 HTML 高保真原型。关键设计是:每个页面独立文件,改一个不影响其他;全局样式用 CSS 变量控制,微调风格只需改一个文件;组件样式参考现有前端代码,让原型看起来和正式系统一致。

输入

  • PRD 文件路径(Markdown 格式,通常是 prd-draft 或手写的 PRD 终稿)
  • 项目前端代码(用于提取组件样式,非必须)

工作流程

Step 1: 分析 PRD 和前端代码

  1. 读取 PRD:提取以下信息:

    • 功能清单(每个功能对应的页面)
    • 业务流程图(页面间的跳转关系)
    • 需求详情(每个页面的交互说明、表单字段、表格列)
    • 角色权限(不同角色看到的内容差异)
    • 数据模型(表单字段的类型、枚举值、约束)
  2. 读取前端代码(如果存在):

    • 查找项目中的前端代码目录(通常是 src/frontend/web/ 等)
    • 读取全局样式文件(如 variables.csstheme.lesstailwind.config.js)提取:
      • 主色调、辅助色
      • 字号体系
      • 间距体系
      • 圆角、阴影等视觉参数
    • 读取常用组件样式(表格、表单、弹窗、按钮、搜索栏、分页器)提取:
      • 表格的表头样式、行高、斑马纹
      • 表单的标签对齐方式、输入框样式
      • 弹窗的宽度、标题栏样式
      • 按钮的颜色、大小层级
    • 如果使用了 UI 框架(Ant Design、Element UI 等),记录框架名和版本,在生成样式时参考其默认风格
  3. 如果没有前端代码:使用中性的 B 端默认风格(类似 Ant Design 默认主题),不会影响原型的生成。

  4. 交互完整性检查:逐个扫描 PRD 需求详情中每个 F-XX 的交互说明,判断是否足够支撑原型生成。如果某个功能的交互描述缺失或过于模糊(如只有一句"支持批量导出"但没说交互形式),列出来让用户决定:

⚠️ 以下功能的交互描述不够详细,可能影响原型准确性:

| 功能 | 当前描述 | 缺少的信息 |
|------|----------|-----------|
| F-03 批量导出 | "支持批量导出" | 触发方式(按钮/菜单)、格式选择(弹窗/直接下载)、进度反馈 |
| F-05 审核 | "提交后进入审核流程" | 审核页面布局、通过/驳回的交互、驳回原因输入方式 |

可以选择:
1. 先补充这些交互细节,我再生成原型
2. 先按常见 B 端做法生成,后续再调整

用户选择补充时,等补充完再继续。用户选择按默认生成时,在对应页面中按常见 B 端交互模式处理,并在 Step 6 输出总结中标注哪些页面使用了默认交互。

  1. 规划页面列表:基于 PRD 功能清单,列出需要生成的页面:
📋 页面规划:

| 页面 | 类型 | 对应功能 | 文件名 |
|------|------|----------|--------|
| {列表页} | table | F-01 | list.html |
| {详情页} | detail | F-02 | detail.html |
| {新建/编辑} | form | F-03 | form.html |
| ... | ... | ... | ... |

共 X 个页面,确认后开始生成。

Step 2: 生成共享样式文件 (styles.css)

这是所有页面的风格基础。使用 CSS 变量控制全局参数,用户只需修改变量值就能调整整体风格。

/* ===== 全局 CSS 变量 ===== */
:root {
  /* 颜色体系 */
  --color-primary: #1890ff;        /* 主色 */
  --color-primary-hover: #40a9ff;  /* 主色悬停 */
  --color-success: #52c41a;
  --color-warning: #faad14;
  --color-danger: #ff4d4f;
  --color-text: #333;              /* 正文色 */
  --color-text-secondary: #999;    /* 辅助文字 */
  --color-border: #d9d9d9;
  --color-bg: #f5f5f5;             /* 页面背景 */
  --color-bg-white: #fff;          /* 卡片背景 */

  /* 字号体系 */
  --font-size-sm: 12px;
  --font-size-base: 14px;
  --font-size-lg: 16px;
  --font-size-title: 20px;

  /* 间距体系 */
  --spacing-xs: 4px;
  --spacing-sm: 8px;
  --spacing-md: 16px;
  --spacing-lg: 24px;
  --spacing-xl: 32px;

  /* 圆角 */
  --border-radius: 4px;

  /* 阴影 */
  --shadow-card: 0 2px 8px rgba(0,0,0,0.08);

  /* 布局尺寸 */
  --sidebar-width: 200px;
  --header-height: 48px;
}

/* ===== 基础重置 ===== */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-size: var(--font-size-base); color: var(--color-text); }

/* ===== 核心布局(必须严格遵守,不得覆盖) ===== */
.layout { display: flex; height: 100vh; width: 100%; overflow: hidden; }
.sidebar { width: var(--sidebar-width); min-width: var(--sidebar-width); height: 100vh; overflow-y: auto; background: #001529; color: #fff; }
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; height: 100vh; overflow: hidden; }
.header { height: var(--header-height); min-height: var(--header-height); display: flex; align-items: center; padding: 0 var(--spacing-lg); background: var(--color-bg-white); border-bottom: 1px solid var(--color-border); }
.content { flex: 1; overflow-y: auto; padding: var(--spacing-lg); background: var(--color-bg); }

布局规则(生成页面时必须遵守)

  • 每个页面的 HTML 结构必须是 .layout > .sidebar + .main > .header + .content
  • .mainflex: 1; min-width: 0 撑满侧边栏右侧的全部剩余空间,禁止给 .main 或 .content 设置 max-width 或固定宽度
  • 内容区 .content 内部可以放卡片容器,但卡片也应 width: 100%,不要用 max-width 限制

如果从前端代码中提取到了实际的样式参数,用提取到的值替换上述默认值。

样式文件还应包含以下常用组件的样式定义(以下为关键组件的参考实现,生成时以此为基础扩展):

/* ===== 表格 ===== */
.table-container { width: 100%; overflow-x: auto; }
table { width: 100%; border-collapse: collapse; background: var(--color-bg-white); }
th { background: #fafafa; font-weight: 500; text-align: left; padding: 12px 16px; border-bottom: 1px solid var(--color-border); }
td { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; }
tr:hover td { background: #fafafa; }

/* ===== 表单 ===== */
.form-group { display: flex; align-items: flex-start; margin-bottom: var(--spacing-lg); }
.form-label { width: 120px; min-width: 120px; text-align: right; padding-right: 12px; line-height: 32px; color: var(--color-text); }
.form-label .required { color: var(--color-danger); margin-right: 4px; }
.form-control { flex: 1; min-width: 0; }
input[type="text"], input[type="number"], input[type="date"], select, textarea {
  width: 100%; height: 32px; padding: 4px 11px; border: 1px solid var(--color-border);
  border-radius: var(--border-radius); font-size: var(--font-size-base); outline: none;
}
input:focus, select:focus, textarea:focus { border-color: var(--color-primary); box-shadow: 0 0 0 2px rgba(24,144,255,0.2); }
textarea { height: auto; min-height: 64px; }

/* ===== 按钮 ===== */
.btn { display: inline-flex; align-items: center; height: 32px; padding: 0 15px; border: 1px solid var(--color-border); border-radius: var(--border-radius); cursor: pointer; font-size: var(--font-size-base); background: var(--color-bg-white); }
.btn-primary { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
.btn-primary:hover { background: var(--color-primary-hover); border-color: var(--color-primary-hover); }
.btn-danger { color: var(--color-danger); border-color: var(--color-danger); }

/* ===== 弹窗 ===== */
.modal-mask { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 1000; }
.modal-mask.active { display: flex; align-items: center; justify-content: center; }
.modal { background: var(--color-bg-white); border-radius: var(--border-radius); width: 520px; max-height: 80vh; overflow-y: auto; }
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 16px 24px; border-bottom: 1px solid var(--color-border); }
.modal-body { padding: 24px; }
.modal-footer { display: flex; justify-content: flex-end; gap: 8px; padding: 10px 24px; border-top: 1px solid var(--color-border); }

/* ===== 搜索栏 ===== */
.search-bar { display: flex; flex-wrap: wrap; gap: var(--spacing-md); padding: var(--spacing-md); background: var(--color-bg-white); border-radius: var(--border-radius); margin-bottom: var(--spacing-md); }
.search-bar .search-item { display: flex; align-items: center; gap: 8px; }
.search-bar .search-item label { white-space: nowrap; color: var(--color-text-secondary); }
.search-actions { display: flex; gap: 8px; margin-left: auto; }

/* ===== 分页器 ===== */
.pagination { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding: 16px 0; }
.pagination .page-info { color: var(--color-text-secondary); font-size: var(--font-size-sm); }

/* ===== 状态标签 ===== */
.tag { display: inline-block; padding: 0 8px; font-size: var(--font-size-sm); line-height: 22px; border-radius: 2px; }
.tag-success { color: var(--color-success); background: #f6ffed; border: 1px solid #b7eb8f; }
.tag-warning { color: var(--color-warning); background: #fffbe6; border: 1px solid #ffe58f; }
.tag-danger { color: var(--color-danger); background: #fff2f0; border: 1px solid #ffccc7; }
.tag-default { color: var(--color-text-secondary); background: #fafafa; border: 1px solid var(--color-border); }
.tag-processing { color: var(--color-primary); background: #e6f7ff; border: 1px solid #91d5ff; }

/* ===== 面包屑 ===== */
.breadcrumb { font-size: var(--font-size-sm); color: var(--color-text-secondary); }
.breadcrumb a { color: var(--color-text-secondary); text-decoration: none; }
.breadcrumb a:hover { color: var(--color-primary); }
.breadcrumb .separator { margin: 0 8px; }

/* ===== 卡片 ===== */
.card { background: var(--color-bg-white); border-radius: var(--border-radius); box-shadow: var(--shadow-card); padding: var(--spacing-lg); margin-bottom: var(--spacing-md); width: 100%; }
.card-title { font-size: var(--font-size-lg); font-weight: 500; margin-bottom: var(--spacing-md); }

/* ===== 操作栏 ===== */
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-md); }

生成 styles.css 时以上述为基础,根据实际页面需求扩展(如 Tab 切换、描述列表等)。如果从前端代码中提取到了实际样式参数,替换对应的值。

Step 3: 逐页生成 HTML

每个页面一个独立的 HTML 文件,必须使用以下骨架结构:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>{页面标题} - {模块名}</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <div class="layout">
    <div class="sidebar">
      <!-- 侧边栏导航,当前页高亮,链接到其他页面 -->
    </div>
    <div class="main">
      <div class="header">
        <!-- 面包屑 / 页面标题 -->
      </div>
      <div class="content">
        <!-- 页面主体内容 -->
      </div>
    </div>
  </div>
</body>
</html>

严格要求

  • HTML 结构必须是 .layout > .sidebar + .main > .header + .content,不得嵌套额外的容器层
  • 不得在 <style> 标签中覆盖 .layout.sidebar.main.header.content 的布局属性
  • 侧边栏导航必须包含所有页面的 <a> 链接,当前页用 .active 类标记

每个文件:

  • 引用共享的 styles.css
  • 侧边栏的导航菜单高亮当前页面,其他页面可点击跳转
  • 使用真实的示例数据(不是 lorem ipsum),基于 PRD 中的数据模型生成
  • 表单字段的类型和约束与 PRD 数据模型一致
  • 包含基本的交互效果(用纯 JS 实现,不依赖外部库):
    • 弹窗的打开/关闭
    • 表格行的选中
    • 表单的展开/收起
    • Tab 切换
    • 搜索栏的展开/收起

页面类型模板

列表页 (table)

  • 页面标题 + 面包屑
  • 搜索栏(基于 PRD 中的筛选条件)
  • 操作按钮区(新建、批量操作等)
  • 数据表格(列基于 PRD 数据模型,填充 5-8 行示例数据)
  • 分页器
  • 操作列(查看、编辑、删除等,基于 PRD 权限矩阵)

详情页 (detail)

  • 面包屑导航
  • 基础信息卡片
  • 关联信息 Tab
  • 操作按钮(编辑、删除、状态变更等)

表单页 (form)

  • 面包屑导航
  • 表单字段(类型、必填、校验提示与 PRD 一致)
  • 底部按钮(提交、取消)
  • 如果字段较多,分组展示

弹窗 (modal)

  • 不单独生成文件,嵌入在触发它的页面中
  • 点击按钮弹出,点击关闭/遮罩收起

Step 4: 生成导航入口页 (index.html)

index.html 使用同样的 .layout 布局骨架,内容区展示:

  • 项目名称和模块名称(作为页面标题)
  • 页面列表卡片:每个页面一张卡片,包含页面名称、类型标签、一句话描述,点击跳转
  • 业务流程概览:用文字或简单箭头描述页面间的流转关系(如"列表页 → 点击查看 → 详情页 → 点击编辑 → 表单页")
  • 角色说明(如 PRD 中有):不同角色的权限差异摘要

Step 5: 输出结构

requirements/{模块名}/prototype/
├── index.html          ← 导航入口
├── styles.css          ← 共享样式(CSS 变量在这里改)
├── list.html           ← 列表页
├── detail.html         ← 详情页
├── form.html           ← 表单页
└── ...                 ← 其他页面

Step 6: 输出总结

生成完成后告知用户:

  1. 原型文件位置
  2. 共生成了多少个页面
  3. 如何查看:open requirements/{模块名}/prototype/index.html
  4. 如何微调:
    • 调整全局风格:修改 styles.css 中的 CSS 变量
    • 调整单个页面:直接编辑对应的 HTML 文件,不影响其他页面
  5. 样式参考来源(从前端代码提取 / 使用默认风格)

原型修改模式

当用户要求修改已有原型页面(而非首次生成)时,进入修改模式。

触发条件

  • 用户要求修改已有原型的某个页面(如"把列表页加一个时间筛选"、"详情页多一个审核记录 Tab")
  • requirements/{模块名}/prototype/ 目录下已有 HTML 文件

执行修改

直接修改对应的 HTML 文件。修改范围仅限用户要求的页面,不影响其他页面文件。

输出交互变更摘要

修改完成后,对比本次变更与 PRD 中对应功能的描述,输出交互变更摘要:

📋 交互变更摘要(相对 PRD 的差异)

| 页面 | 变更内容 | 对应功能 | PRD 影响 |
|------|----------|----------|----------|
| list.html | 搜索栏新增"时间范围"筛选 | F-01 | 需求详情需补充筛选条件 |
| detail.html | 新增"审核记录" Tab | F-02 | 需求详情需补充 Tab 描述 |

💡 建议将以上变更同步到 PRD:告诉我"更新 PRD"即可进入 prd-draft 修改模式

摘要规则

  • 只列出与 PRD 有差异的变更。如果修改只是调整样式或修复布局问题,不影响 PRD 内容的,不需要列入摘要
  • "PRD 影响"列要具体到 PRD 的哪个章节需要更新(如"需求详情 F-01 需补充筛选条件"、"数据模型需新增字段")
  • 用户选择"更新 PRD"时,这份摘要直接作为 prd-draft 修改模式的输入

生成原则

保真度优先于花哨:B 端原型的价值在于让业务方和开发看到"页面大概长这样",而不是做得多好看。优先保证:字段完整、布局合理、交互逻辑正确。

真实数据优于占位符:表格和表单中使用基于 PRD 数据模型的真实示例数据(如"张三"、"2024-03-15"、"审核通过"),而不是"测试数据1"、"xxx"这类占位文字。让原型看起来像一个正在使用的系统。

独立性优于 DRY:每个 HTML 文件是完全独立的(除了引用 styles.css),即使这意味着侧边栏等公共部分在每个文件中重复。这样修改一个页面时不需要担心影响其他页面,也方便单独发给业务方确认某个页面。

纯 HTML/CSS/JS:不依赖任何外部框架或 CDN。原型文件可以直接用浏览器打开,不需要任何构建工具或网络连接。这保证了原型的便携性——可以打包发邮件、放到共享文件夹。

组件一致性:同一套原型中的表格、表单、按钮等组件保持视觉一致。不要一个页面的按钮是圆角的,另一个页面的是直角的。统一在 styles.css 中定义。

与其他 skill 的关系

coding-knowledge-init → prd-draft → prd-review → proto-gen
                            ↑                         |
                            └── 原型修改后同步 PRD ←──┘
  • 前置prd-draftprd-review 产出的 PRD 终稿
  • 反向:原型修改模式输出的交互变更摘要,可作为 prd-draft 修改模式的输入
  • 辅助coding-knowledge/business/prd-reference/design-patterns.md 提供现有交互模式参考

Common Pitfalls

布局不撑满全屏:最常见的问题。原因通常是:给 .main.content 设了 max-width、用了 margin: 0 auto 居中、或在 .layout 外面多套了一层容器。必须严格使用 styles.css 中定义的核心布局类,不要在页面内用 <style> 覆盖布局属性。

一个文件塞所有页面:多页面合并到一个 HTML 文件会导致难以维护和单独微调。每页一个文件是刻意的设计选择。

依赖外部资源:不要引用 CDN 上的字体、图标库或 CSS 框架。原型要能在离线环境下打开。如果需要图标,用 Unicode 字符或 SVG 内联。

过度交互:原型不需要实现完整的前端逻辑(如真正的表单校验、数据提交、API 调用)。只需要最基本的交互效果(弹窗开关、Tab 切换等)让业务方能理解操作流程即可。

忽略 PRD 中的数据模型:表格的列名、表单的字段名应该与 PRD 数据模型严格一致,而不是自己编一套。这是原型和 PRD 之间的桥梁。