
Html Prototype Generator
- 8 installs
- Updated August 2, 2026
- anian0/pick-skills
html-prototype-generator is a Claude skill that generates standalone Ant Design-styled HTML prototype pages from an existing Vue page or a requirements document.
About
This skill produces standalone HTML prototype pages that mirror the look of a project's admin backend using Ant Design 4.x structure and a fixed color palette. It supports cloning an existing Vue page one-to-one, generating a new page from requirements, or revising an existing prototype, and outputs a single browser-openable HTML file with mock data. A developer uses it to quickly share a visual prototype of a page or feature without a build step.
- Generates single-file HTML prototype pages styled to match a kt-agent-framework admin backend (Ant Design 4.x)
- Three modes: clone an existing .vue page 1:1, generate from a requirements doc, or adjust an existing prototype
- Overlays an interactive requirement-guide layer that highlights and annotates each new/modified/removed element
Html Prototype Generator by the numbers
- 8 all-time installs (skills.sh)
- Ranked #1,481 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
html-prototype-generator capabilities & compatibility
free; runs locally, no API keys
- Capabilities
- ui prototyping · page cloning · mockup generation · web design
- Use cases
- ui design · web design · frontend
- Runs
- Runs locally
- Pricing
- Free
What html-prototype-generator says it does
生成与 kt-agent-framework 管理后台风格一致的独立HTML原型页面。产物为单个HTML文件,可直接在浏览器打开,方便分享。
**核心原则:先 1:1 完整还原,再叠加需求改动。** 绝不能跳过还原阶段直接按需求重写。
npx skills add https://github.com/anian0/pick-skills --skill html-prototype-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 8 |
|---|---|
| Last updated | August 2, 2026 |
| Repository | anian0/pick-skills ↗ |
What it does
Generate standalone, shareable HTML prototype pages that match an admin backend style from a Vue page or requirements doc.
Who is it for?
Quickly producing a shareable, no-build HTML prototype that matches an existing admin-UI style for design review.
Skip if: Producing production Vue/React components or connecting real APIs; output uses hardcoded mock data.
When should I use this skill?
The user asks to generate an HTML prototype, turn requirements into a prototype page, clone an existing page as a prototype, or types /htmlproto.
What you get
A single self-contained HTML file that visually matches the real admin UI and annotates each requirement change.
- Single-file HTML prototype page(s)
- An index.html directory page for multi-page output
By the numbers
- 3 working modes (clone, requirement, dialogue)
- 5-phase workflow
- 4 page types (list, form, detail, dashboard)
Files
HTML Prototype Generator
目标
生成与 kt-agent-framework 管理后台风格一致的独立HTML原型页面。产物为单个HTML文件,可直接在浏览器打开,方便分享。
三种工作模式:
- 复刻模式:读取已有 .vue 页面,1:1 还原后再叠加需求改动
- 需求模式:根据需求文档生成全新原型页面
- 对话模式:读取已有原型HTML,讨论并调整
---
设计核心
原型的核心价值是「与真实页面尽可能一致」。以下是必须遵守的设计规范:
样式原则
1. Ant Design 组件结构优先:组件 HTML 结构和类名必须遵循 Ant Design 4.x 规范(如 ant-btn、ant-table、ant-form 等),项目配色覆盖主色但不改变组件结构 2. 禁止使用 Emoji:侧边栏图标、按钮图标、状态标识等一律使用 CSS/SVG 实现或 Ant Design 图标类名(如 anticon anticon-home),绝不使用 emoji 字符(🏠🤖📋 等) 3. 状态标签:简单状态优先使用 ant-tag 系列,需要带圆点和背景色时才用自定义 .kt-status
配色(来自项目 const.less)
| 用途 | 色值 | 背景色 |
|---|---|---|
| Primary | #3e8dff | #d6e7ff |
| Success | #00e5c7 | #effffb |
| Danger | #ff8b78 | #fff5f3 |
| Warning | #ffb71d | #fff2d6 |
| Default | #bfc6d1 | #f5f6f9 |
| Ant Blue | #1890ff | - |
布局参数
- 侧边栏:#1e202a,220px宽
- 顶栏:#fff,56px高
- 内容区背景:#f0f2f5
- 卡片:白色,8px圆角,shadow-sm
- 表格:bordered + small 模式
详细组件规范见 docs/design-guide.md。
---
工作流程
Phase 1: 确定工作模式
| 用户意图 | 模式 | 第一步 |
|---|---|---|
| 提到已有页面路径、复制页面、在现有页面上加功能 | 复刻模式 | 读取 .vue 源文件 |
| 提到需求文档、新建页面 | 需求模式 | 读取需求文档 |
| 提到调整已有原型 | 对话模式 | 读取原型HTML |
---
Phase 2A: 复刻模式(⚠️ 必须严格遵守两阶段原则)
核心原则:先 1:1 完整还原,再叠加需求改动。 绝不能跳过还原阶段直接按需求重写。
阶段一:1:1 还原源页面(不做任何改动)
1. 读取 .vue 主文件及其关联的 data.ts、子组件(路径在 D:\workspace\Document\input\kt-agent-framework\admin_frontend\src\pages\) 2. 逐一还原以下所有元素(缺一不可):
- 侧边栏菜单项(完全按源页面菜单结构)
- 筛选区所有字段(来自
searchSchema,字段顺序、label、组件类型必须一致) - 操作按钮(来自
tl-filter-left区域,按钮文案、顺序一致) - 表格所有列(来自
columns,列顺序、title、宽度、对齐方式一致) - 操作列按钮(来自
action-group,按钮文案、顺序一致;移除hasButtonPerm判断直接展示) - 状态标签(来自
yc-status,还原为.kt-status+ 对应颜色) - 抽屉/表单(来自子组件 EditForm,还原所有表单字段、required 标记)
- Mock 数据(≥3行,字段值与业务语义一致)
3. 还原完成后,向用户展示分析结果表格:
✅ 已还原:[筛选字段 N 个] [表格列 N 个] [操作按钮 N 个] [表单字段 N 个]
📋 待叠加改动:[用户需求描述]等待用户确认还原是否准确,如有偏差先修正再进入阶段二。
阶段二:叠加需求改动
在已还原的 HTML 基础上,按需求增删改元素。改动必须通过需求引导层标注(见 Phase 3 引导层规范):
- 新增的元素:加
data-guide-new属性 - 修改的元素:加
data-guide-modified属性 - 删除的位置:插入
data-guide-removed占位注释
.vue → HTML 映射规则:
<tl>→ 整个页面容器<tl-main>→ 主内容区(flex: 1, overflow auto)<tl-filter>→ 筛选区卡片<tl-filter-left>→ 筛选区内容(flex wrap)<yc-form v-bind="searchVBind" />→ 根据 searchSchema 生成筛选字段<tl-table>→ 表格卡片容器(白色背景 + 圆角 + shadow)<a-table>→ Ant Design 表格(HTML版)<tl-drawer>→ 右侧抽屉<action-group>→ 操作按钮组(a-space + link buttons)<yc-status>→ 状态标签(简单状态用ant-tag,需要圆点背景色用.kt-status)useDrawer→ JS 控制抽屉开关(openDrawer/closeDrawer)useTable→ 静态表格数据 + 分页hasButtonPerm()→ 移除权限判断,直接展示按钮- API 调用 → 硬编码 Mock 数据
---
Phase 2B: 需求模式
1. 读取需求文档,提取功能列表 2. 输出页面清单表格,等待用户确认
| 序号 | 页面名称 | 页面类型 | 功能描述 | 优先级 |
|---|
3. 分轮次澄清需求(每轮最多3问) 4. 确认后生成HTML原型,需求描述通过引导层展示(见 Phase 3)
生成前检查现有页面:
- 扫描
.dev/prototype/目录查找现有原型 - 继承现有页面的配色和组件风格
---
Phase 2C: 对话模式
1. 读取已有原型HTML文件 2. 与用户讨论需要调整的部分 3. 修改对应的HTML并保存为新版本
---
Phase 3: 需求引导层(⚠️ 新功能,所有含需求改动的原型必须包含)
什么是需求引导层?
类似新手引导的交互效果:原型页面上叠加一个半透明蒙版,对每一处需求改动(新增/修改/删除)显示高亮框 + 气泡说明,让查看者直观理解"这里改了什么、为什么改"。
实现规则:
1. 默认显示引导层(首次打开即展示) 2. 右上角提供「关闭引导」/「开启引导」切换按钮 3. 引导项按序号排列,可点击序号跳转到对应元素 4. 高亮框自动定位到目标元素(使用 getBoundingClientRect()) 5. 气泡显示在元素旁边,避免遮挡关键内容
引导项数据结构(在 `<script>` 中定义):
const guideSteps = [
{
id: 1,
selector: '#btn-export', // 目标元素 CSS 选择器
type: 'new', // new | modified | removed
title: '新增:导出按钮',
desc: '支持将当前筛选结果导出为 Excel 文件,权限:运营角色及以上可见',
},
{
id: 2,
selector: 'th[data-col="status"]',
type: 'modified',
title: '修改:状态列',
desc: '原「启用/禁用」两态改为「启用/禁用/待审核」三态,新增黄色待审核样式',
},
{
id: 3,
selector: '.guide-removed-placeholder-1',
type: 'removed',
title: '移除:批量操作栏',
desc: '经产品确认,当前版本不需要批量操作,已移除,后续迭代再加',
},
];视觉规范:
| 改动类型 | 高亮色 | 气泡标签色 |
|---|---|---|
| new(新增) | #3e8dff(蓝色边框) | #3e8dff 背景,白字「新增」 |
| modified(修改) | #ffb71d(橙色边框) | #ffb71d 背景,白字「修改」 |
| removed(移除) | #ff8b78(红色边框) | #ff8b78 背景,白字「移除」 |
完整HTML实现示例见 docs/design-guide.md → 第8节「需求引导层」。
---
Phase 4: 生成原型
单页面生成:直接调用脚本或手写 HTML。
python scripts/generate_page.py --input '{
"page_name": "角色列表",
"page_type": "list",
"scenario": "clone",
"source_vue": "src/pages/role/index.vue",
"output_path": ".dev/prototype",
"clarification": {
"layout": {},
"data": {},
"interactions": {},
"modifications": "新增启用/禁用开关列"
}
}'多页面生成 — 使用 Subagent 隔离上下文:
当需要同时生成多个原型页面时,必须为每个页面启动独立的 Subagent,避免上下文污染。
# 每个页面一个 subagent,并行生成
# 使用 Agent 工具,subagent_type 为 "general-purpose"
# 每个 subagent 独立读取 SKILL.md 和 docs/
# 每个 subagent 独立完成一个页面的生成并写入文件---
Phase 5: 文件输出
输出目录: .dev/prototype/
命名规则:
- 新原型:
prototype-{YYYYMMDD}-{页面名称}.html - 改造原型:
prototype-{YYYYMMDD}-{页面名称}-v{N}.html
页面结构:
- 抽屉、弹窗等交互元素合并到主页面HTML中
- 通过 CSS
display:none/ JavaScript 控制显示隐藏 - 需求引导层默认展示,右上角可关闭
- 文档面板(交互说明)默认隐藏,点击「交互说明」按钮展开
- 保留旧文件(绝不覆盖)
目录页(index.html):
- 多页面生成完成后,必须在
.dev/prototype/下生成index.html目录页 - 详细规范见
docs/output-format.md
---
页面类型
| 类型 | 说明 | 典型组件 |
|---|---|---|
| list | 列表页 | 筛选区、表格、分页、操作按钮、抽屉 |
| form | 表单页 | 表单字段、提交/取消 |
| detail | 详情页 | 信息分组、描述列表 |
| dashboard | 仪表盘 | 统计卡片、图表、快捷入口 |
---
约束
- 必须使用 Ant Design 4.x CSS:
antd.min.cssCDN - 必须应用项目配色:#3e8dff 主色,非 Ant Design 默认 #1890ff
- 遵循 Ant Design 组件结构:即使配色被覆盖,HTML 结构和类名遵循 Ant Design 4.x 规范
- 禁止使用 Emoji:所有图标使用 Ant Design 图标类名或 CSS 实现
- Tailwind 仅用于布局工具类:flex、grid、间距,不用于组件样式
- 单文件输出:每个HTML包含完整原型,可直接打开
- Mock数据:硬编码,无API调用
- 不使用 @apply:CDN模式不生效
- 复刻模式两阶段:必须先1:1还原,再叠加改动
- 有需求改动必须有引导层:不能用文档面板替代
- 多页面用 Subagent:同时生成多个页面时,每个页面在独立 subagent 中生成
---
参考资源
- 设计规范:
docs/design-guide.md— 完整设计规范和组件HTML示例(第8节为引导层实现) - 输出格式:
docs/output-format.md— HTML输出格式详细规范 - 页面生成脚本:
scripts/generate_page.py— 单页面生成 - 批量生成脚本:
scripts/batch_generate.py— 多页面批量生成 - 复刻分析脚本:
scripts/clone_page.py— .vue 文件结构提取
项目前端源码参考: D:\workspace\Document\input\kt-agent-framework\admin_frontend\src\
---
自检清单
- [ ] HTML文件可在浏览器正常打开
- [ ] 配色使用项目色值(#3e8dff 主色等)
- [ ] 侧边栏 #1e202a,顶栏白色
- [ ] 使用 Ant Design 组件类名(ant-btn、ant-table 等)
- [ ] 无 emoji 字符,图标使用 anticon 类名或 CSS 实现
- [ ] 状态标签:简单用 ant-tag,需圆点用 .kt-status
- [ ] 抽屉/弹窗合并到主页面
- [ ] 文档面板可切换显示
- [ ] 文件保存到
.dev/prototype/ - [ ] 复刻模式:源页面所有字段/列/按钮已1:1还原(不遗漏)
- [ ] 有需求改动:已包含需求引导层,改动项已全部标注
- [ ] Mock数据合理(≥3行,语义正确)
- [ ] 多页面时已生成 index.html 目录页
- [ ] 多页面时使用了 subagent 隔离上下文
KT Admin 原型设计规范
本规范确保原型页面与 kt-agent-framework 管理后台风格一致。
0. 设计原则
1. Ant Design 优先:组件结构和交互模式必须遵循 Ant Design 4.x 规范,项目配色覆盖主色但不改变组件结构 2. 禁止使用 Emoji:侧边栏图标、按钮图标、状态标识等一律使用 CSS/SVG 实现或 Ant Design 图标类名(如 anticon anticon-home),绝不使用 emoji 字符(🏠🤖📋 等),emoji 在不同系统渲染不一致,显得不专业 3. 项目配色覆盖:主色使用 #3e8dff 替代 Ant Design 默认 #1890ff,但 Ant Design 组件类名和结构保持不变 4. 需求可见性:所有对源页面的改动(新增/修改/移除)必须通过引导层在原型上直接标注,不能仅在文档面板中描述
1. 技术栈
- Ant Design 4.x CSS:
https://cdnjs.cloudflare.com/ajax/libs/antd/4.24.15/antd.min.css - Tailwind CSS:
https://cdn.tailwindcss.com(仅用于布局工具类) - 纯 HTML/CSS/JavaScript,无框架依赖
2. 配色方案
主色与状态色(来自项目 const.less)
| 名称 | 色值 | 背景色 | 用途 |
|---|---|---|---|
| Primary | #3e8dff | #d6e7ff | 主按钮、链接、选中态、主标签 |
| Success | #00e5c7 | #effffb | 启用、在线、成功状态 |
| Danger | #ff8b78 | #fff5f3 | 删除、错误、禁用状态 |
| Warning | #ffb71d | #fff2d6 | 警告、待审核状态 |
| Default | #bfc6d1 | #f5f6f9 | 默认、未知状态 |
| Ant Blue | #1890ff | - | Ant Design 默认主色(仅用于组件内部hover等) |
中性色
| 名称 | 色值 | 用途 |
|---|---|---|
| 侧边栏背景 | #1e202a | 左侧深色导航 |
| 侧边栏选中 | #3e8dff | 菜单项选中态 |
| 内容区背景 | #f0f2f5 | 页面主背景 |
| 顶栏背景 | #fff | 顶部栏 |
| 文字主要 | rgba(0,0,0,0.85) | 标题、正文 |
| 文字次要 | rgba(0,0,0,0.65) | 描述、辅助文字 |
| 文字提示 | rgba(0,0,0,0.45) | placeholder、提示 |
| 边框 | #f0f0f0 | 分割线、卡片边框 |
| 分割线 | #e8e8e8 | 较明显的分割 |
3. 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="https://cdnjs.cloudflare.com/ajax/libs/antd/4.24.15/antd.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<!-- Ant Design 图标(SVG symbol 方式) -->
<script src="https://at.alicdn.com/t/c/font_3649831_vn4l2gpuij.js"></script>
<style>
/* CSS 变量 + 覆盖 + 组件样式(见第3节完整版) */
</style>
</head>完整样式块(每个HTML文件必须包含):
:root {
--kt-primary: #3e8dff;
--kt-primary-bg: #d6e7ff;
--kt-success: #00e5c7;
--kt-success-bg: #effffb;
--kt-danger: #ff8b78;
--kt-danger-bg: #fff5f3;
--kt-warning: #ffb71d;
--kt-warning-bg: #fff2d6;
--kt-default: #bfc6d1;
--kt-default-bg: #f5f6f9;
--kt-sidebar: #1e202a;
--kt-content-bg: #f0f2f5;
}
/* 覆盖 Ant Design 主色 */
.ant-btn-primary { background-color: #3e8dff; border-color: #3e8dff; }
.ant-btn-primary:hover { background-color: #5ea3ff; border-color: #5ea3ff; }
.ant-btn-primary:active { background-color: #2a75d4; border-color: #2a75d4; }
a, .ant-btn-link { color: #3e8dff; }
a:hover, .ant-btn-link:hover { color: #5ea3ff; }
/* yc-status 状态标签 */
.kt-status { display: inline-flex; align-items: center; gap: 6px; padding: 0 8px; height: 24px; border-radius: 4px; font-size: 12px; line-height: 24px; }
.kt-status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
.kt-status-primary { background: #d6e7ff; color: #3e8dff; }
.kt-status-primary .kt-status-dot { background: #3e8dff; }
.kt-status-success { background: #effffb; color: #00a89a; }
.kt-status-success .kt-status-dot { background: #00e5c7; }
.kt-status-danger { background: #fff5f3; color: #d4634f; }
.kt-status-danger .kt-status-dot { background: #ff8b78; }
.kt-status-warning { background: #fff2d6; color: #c99200; }
.kt-status-warning .kt-status-dot { background: #ffb71d; }
.kt-status-default { background: #f5f6f9; color: #8c95a6; }
.kt-status-default .kt-status-dot { background: #bfc6d1; }
/* TL 布局系统 */
.tl-main { flex: 1; overflow: auto; display: flex; flex-direction: column; gap: 0; }
.tl-filter { background: #fff; padding: 16px 24px; border-radius: 8px; margin-bottom: 16px; }
.tl-filter-left { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.tl-table { background: #fff; padding: 24px; padding-top: 16px; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }
/* 操作按钮组 */
.action-group { display: inline-flex; align-items: center; gap: 0; }
.action-group .ant-btn-link { padding: 0 4px; height: auto; }
.action-group .action-divider { width: 1px; height: 14px; background: #e8e8e8; margin: 0 4px; }
/* 文档面板 */
.doc-toggle { position: fixed; top: 50%; right: 0; transform: translateY(-50%); z-index: 1001; background: #3e8dff; color: #fff; border: none; padding: 12px 4px; border-radius: 4px 0 0 4px; cursor: pointer; writing-mode: vertical-rl; font-size: 13px; letter-spacing: 2px; transition: right 0.3s; }
.doc-panel { position: fixed; top: 0; right: -400px; width: 400px; height: 100vh; background: #fafafa; border-left: 1px solid #e8e8e8; padding: 24px; overflow-y: auto; z-index: 1000; transition: right 0.3s; }
.doc-panel.open { right: 0; }
.doc-panel .doc-section { background: #fff; border-radius: 8px; padding: 16px; margin-bottom: 12px; }
.doc-panel .doc-title { font-size: 15px; font-weight: 600; color: rgba(0,0,0,0.85); margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }4. 整体布局结构
<body>
<div class="ant-layout" style="min-height: 100vh;">
<!-- 侧边栏 -->
<aside style="background: #1e202a; width: 220px; flex-shrink: 0; position: fixed; left: 0; top: 0; bottom: 0; z-index: 100;">
<div style="height: 56px; display: flex; align-items: center; padding: 0 20px; border-bottom: 1px solid rgba(255,255,255,0.08);">
<span style="color: #fff; font-size: 16px; font-weight: 600;">KT Agent</span>
</div>
<ul style="list-style: none; padding: 8px 0; margin: 0;">
<li style="padding: 10px 20px; color: rgba(255,255,255,0.65); cursor: pointer; font-size: 14px; display: flex; align-items: center;">
<span class="anticon anticon-home" style="margin-right: 10px; font-size: 14px;"></span>首页
</li>
<li style="padding: 10px 16px; background: #3e8dff; color: #fff; cursor: pointer; font-size: 14px; border-radius: 4px; margin: 0 8px; display: flex; align-items: center;">
<span class="anticon anticon-file-text" style="margin-right: 10px; font-size: 14px;"></span>当前页面
</li>
</ul>
</aside>
<!-- 右侧 -->
<div style="margin-left: 220px; flex: 1; display: flex; flex-direction: column;">
<header style="height: 56px; background: #fff; padding: 0 24px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 1px 4px rgba(0,21,41,0.08); position: sticky; top: 0; z-index: 99;">
<span style="font-size: 16px; font-weight: 500; color: rgba(0,0,0,0.85);">页面标题</span>
<div style="display: flex; align-items: center; gap: 16px;">
<span style="color: rgba(0,0,0,0.65); font-size: 14px;">管理员</span>
</div>
</header>
<main style="flex: 1; padding: 16px; background: #f0f2f5; overflow: auto;">
<!-- 页面内容 -->
</main>
</div>
</div>
</body>5. 组件规范
图标使用规范(禁止 Emoji)
推荐方式:
<script src="https://at.alicdn.com/t/c/font_3649831_vn4l2gpuij.js"></script>
<svg class="anticon" style="width: 14px; height: 14px; margin-right: 10px;">
<use xlink:href="#anticon-home"></use>
</svg>Fallback(文字色块):
<span style="display: inline-flex; width: 18px; height: 18px; border-radius: 3px; background: #3e8dff; color: #fff; font-size: 11px; align-items: center; justify-content: center; margin-right: 10px;">首</span>yc-status 状态标签
优先方案 — ant-tag:
<span class="ant-tag ant-tag-success">启用</span>
<span class="ant-tag ant-tag-error">禁用</span>
<span class="ant-tag ant-tag-warning">待审核</span>增强方案 — .kt-status(带圆点背景色):
<span class="kt-status kt-status-success"><span class="kt-status-dot"></span>启用</span>
<span class="kt-status kt-status-danger"><span class="kt-status-dot"></span>禁用</span>
<span class="kt-status kt-status-warning"><span class="kt-status-dot"></span>待审核</span>
<span class="kt-status kt-status-primary"><span class="kt-status-dot"></span>运行中</span>
<span class="kt-status kt-status-default"><span class="kt-status-dot"></span>未知</span>表格(bordered + small)
<div class="tl-table kt-table">
<div class="ant-table-wrapper">
<div class="ant-table">
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto; width: 100%;">
<thead class="ant-table-thead">
<tr>
<th class="ant-table-cell" style="text-align: center; width: 60px;">序号</th>
<th class="ant-table-cell">名称</th>
<th class="ant-table-cell">状态</th>
<th class="ant-table-cell" style="text-align: center; width: 180px;">操作</th>
</tr>
</thead>
<tbody class="ant-table-tbody">
<tr class="ant-table-row">
<td class="ant-table-cell" style="text-align: center;">1</td>
<td class="ant-table-cell">示例数据</td>
<td class="ant-table-cell"><span class="kt-status kt-status-success"><span class="kt-status-dot"></span>启用</span></td>
<td class="ant-table-cell" style="text-align: center;">
<div class="action-group">
<a style="color: #3e8dff; cursor: pointer;">详情</a>
<span class="action-divider"></span>
<a style="color: #3e8dff; cursor: pointer;">编辑</a>
<span class="action-divider"></span>
<a style="color: #ff4d4f; cursor: pointer;">删除</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<div style="padding: 16px 0 0; display: flex; justify-content: flex-end;">
<ul class="ant-pagination">
<li class="ant-pagination-total-text">共 50 条</li>
<li class="ant-pagination-prev ant-pagination-disabled"><button class="ant-pagination-item-link" disabled><</button></li>
<li class="ant-pagination-item ant-pagination-item-active"><a>1</a></li>
<li class="ant-pagination-next"><button class="ant-pagination-item-link">></button></li>
</ul>
</div>
</div>抽屉
<div id="drawer" style="display: none;">
<div class="ant-drawer" style="position: fixed; top: 0; right: 0; bottom: 0; z-index: 1000;">
<div class="ant-drawer-mask" onclick="closeDrawer()" style="background: rgba(0,0,0,0.45); position: fixed; inset: 0;"></div>
<div class="ant-drawer-content-wrapper" style="width: 980px; position: fixed; top: 0; right: 0; bottom: 0;">
<div class="ant-drawer-content">
<div class="ant-drawer-wrapper-body" style="display: flex; flex-direction: column; height: 100%;">
<div class="ant-drawer-header" style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; justify-content: space-between;">
<div class="ant-drawer-title" id="drawerTitle" style="font-size: 16px; font-weight: 500;">标题</div>
<button onclick="closeDrawer()" style="border: none; background: none; font-size: 16px; cursor: pointer; color: rgba(0,0,0,0.45);">✕</button>
</div>
<div class="ant-drawer-body" style="flex: 1; overflow: auto; padding: 24px;">
<!-- 表单内容 -->
</div>
<div class="ant-drawer-footer" id="drawerFooter" style="border-top: 1px solid #f0f0f0; padding: 12px 24px; text-align: right;">
<button class="ant-btn" style="margin-right: 8px;" onclick="closeDrawer()">取消</button>
<button class="ant-btn ant-btn-primary">保存</button>
</div>
</div>
</div>
</div>
</div>
</div>确认弹窗
<div id="confirmModal" style="display: none;">
<div class="ant-modal-mask" style="background: rgba(0,0,0,0.45); position: fixed; inset: 0; z-index: 1001;"></div>
<div class="ant-modal-wrap" style="position: fixed; inset: 0; z-index: 1002; display: flex; align-items: center; justify-content: center;">
<div class="ant-modal" style="width: 420px;">
<div class="ant-modal-content">
<div class="ant-modal-header"><div class="ant-modal-title">确认删除</div></div>
<div class="ant-modal-body" style="padding: 24px;"><p>确定要删除此记录吗?此操作不可撤销。</p></div>
<div class="ant-modal-footer">
<button class="ant-btn" onclick="closeConfirmModal()">取消</button>
<button class="ant-btn ant-btn-primary" style="background: #ff4d4f; border-color: #ff4d4f;">确认删除</button>
</div>
</div>
</div>
</div>
</div>表单(抽屉内)
<form class="ant-form ant-form-vertical">
<div class="ant-form-item">
<div class="ant-form-item-label"><label><span style="color: #ff4d4f; margin-right: 4px;">*</span>名称</label></div>
<div class="ant-form-item-control"><div class="ant-form-item-control-input">
<input class="ant-input" placeholder="请输入名称" style="max-width: 400px;">
</div></div>
</div>
<div class="ant-form-item">
<div class="ant-form-item-label"><label>描述</label></div>
<div class="ant-form-item-control"><div class="ant-form-item-control-input">
<textarea class="ant-input" placeholder="请输入描述" rows="3" style="max-width: 400px; resize: vertical;"></textarea>
</div></div>
</div>
</form>---
6. 交互脚本模板
每个HTML文件底部必须包含:
<script>
function openDrawer(title, mode) {
document.getElementById('drawer').style.display = 'block';
document.getElementById('drawerTitle').textContent = title;
const footer = document.getElementById('drawerFooter');
if (mode === 'detail') {
footer.innerHTML = '<button class="ant-btn" onclick="closeDrawer()">返回</button>';
} else {
footer.innerHTML = '<button class="ant-btn" style="margin-right:8px" onclick="closeDrawer()">取消</button><button class="ant-btn ant-btn-primary">保存</button>';
}
}
function closeDrawer() { document.getElementById('drawer').style.display = 'none'; }
function openConfirmModal() { document.getElementById('confirmModal').style.display = 'block'; }
function closeConfirmModal() { document.getElementById('confirmModal').style.display = 'none'; }
function toggleDoc() { document.getElementById('docPanel').classList.toggle('open'); }
function mockAction(action, data) { console.log('[Mock]', action, data); }
</script>---
7. 文档面板(交互说明)
文档面板记录页面整体说明、字段含义等,默认隐藏,点击右侧「交互说明」按钮展开。
注意:文档面板 ≠ 需求说明。需求改动必须用引导层(第8节)标注,文档面板只记录页面通用说明。
<button class="doc-toggle" onclick="toggleDoc()">交互说明</button>
<div class="doc-panel" id="docPanel">
<div class="doc-section">
<div class="doc-title">页面说明</div>
<p style="color: rgba(0,0,0,0.65); font-size: 14px;">一句话描述页面用途。</p>
</div>
<div class="doc-section">
<div class="doc-title">交互逻辑</div>
<div style="font-size: 14px; color: rgba(0,0,0,0.65); line-height: 2;">
<div><b>查询:</b>输入筛选条件后点击查询按钮</div>
<div><b>新增:</b>点击新增按钮打开右侧抽屉</div>
<div><b>删除:</b>弹出确认弹窗,确认后删除</div>
</div>
</div>
</div>---
8. 需求引导层(⚠️ 有需求改动时必须包含)
什么是引导层
类似新手引导的视觉效果,在原型页面上叠加一个半透明蒙版,对每一处需求改动显示彩色高亮框 + 气泡说明卡片,让查看者直观理解"这里改了什么"。
效果示意:
┌────────────────────────────────────────┐
│ ┌──────────────────────────────────┐ │
│ │ 筛选区(已还原) │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │ ← 蒙版盖住页面
│ │ 表格(已还原) │ │
│ │ ┌──────┐│ │
│ │ [导出按钮] ←蓝色高亮框 │ 1 ││ │ ← 高亮框 + 序号徽标
│ │ └──────┘│ │
│ └──────────────────────────────────┘ │
│ │
│ ┌─────────────────┐ │
│ │ ① 新增:导出按钮 │ ← 气泡说明卡片 │
│ │ 支持将当前筛选 │ │
│ │ 结果导出 Excel │ │
│ └─────────────────┘ │
└────────────────────────────────────────┘颜色规范
| 改动类型 | 高亮框颜色 | 气泡标签 | 蒙版色 |
|---|---|---|---|
new(新增) | #3e8dff 蓝色,3px 实线 | 蓝底白字「新增」 | 无(高亮框足够) |
modified(修改) | #ffb71d 橙色,3px 实线 | 橙底白字「修改」 | 无 |
removed(移除) | #ff8b78 红色,3px 虚线 | 红底白字「移除」 | 无 |
高亮框使用 box-shadow: 0 0 0 3px <color> 实现,不影响布局。
完整实现代码
在 HTML 文件的 </body> 前插入以下代码块:
<!-- ==================== 需求引导层 ==================== -->
<!-- 引导开关按钮(固定在右上角) -->
<div id="guide-toggle-bar" style="position: fixed; top: 14px; right: 16px; z-index: 9999; display: flex; align-items: center; gap: 8px;">
<div style="background: rgba(0,0,0,0.6); color: #fff; font-size: 12px; padding: 4px 10px; border-radius: 12px; letter-spacing: 0.5px;">
需求改动预览
</div>
<button id="guide-toggle-btn" onclick="toggleGuide()" style="background: #3e8dff; color: #fff; border: none; padding: 5px 14px; border-radius: 4px; font-size: 13px; cursor: pointer; box-shadow: 0 2px 6px rgba(62,141,255,0.4);">
关闭引导
</button>
</div>
<!-- 引导说明卡片容器(绝对定位,由 JS 动态插入) -->
<div id="guide-cards" style="position: fixed; bottom: 24px; left: 240px; z-index: 9998; display: flex; flex-direction: column; gap: 8px; max-width: 360px;"></div>
<style>
/* 高亮框样式 */
.guide-highlight-new {
box-shadow: 0 0 0 3px #3e8dff !important;
border-radius: 4px;
position: relative;
z-index: 100;
}
.guide-highlight-modified {
box-shadow: 0 0 0 3px #ffb71d !important;
border-radius: 4px;
position: relative;
z-index: 100;
}
.guide-highlight-removed {
box-shadow: 0 0 0 3px #ff8b78 !important;
border-radius: 4px;
border: 2px dashed #ff8b78 !important;
position: relative;
z-index: 100;
}
/* 序号徽标 */
.guide-badge {
position: absolute;
top: -10px;
right: -10px;
width: 20px;
height: 20px;
border-radius: 50%;
color: #fff;
font-size: 11px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
z-index: 101;
pointer-events: none;
}
/* 说明卡片 */
.guide-card {
background: #fff;
border-radius: 8px;
padding: 10px 14px;
box-shadow: 0 4px 16px rgba(0,0,0,0.14);
display: flex;
align-items: flex-start;
gap: 10px;
font-size: 13px;
line-height: 1.6;
cursor: pointer;
transition: box-shadow 0.15s;
}
.guide-card:hover { box-shadow: 0 6px 20px rgba(0,0,0,0.18); }
.guide-card-label {
padding: 1px 7px;
border-radius: 3px;
color: #fff;
font-size: 11px;
font-weight: 600;
white-space: nowrap;
flex-shrink: 0;
margin-top: 2px;
}
.guide-card-content { flex: 1; }
.guide-card-title { font-weight: 600; color: rgba(0,0,0,0.85); }
.guide-card-desc { color: rgba(0,0,0,0.55); margin-top: 2px; }
</style>
<script>
// ===== 需求改动数据(按实际需求填写)=====
const guideSteps = [
{
id: 1,
selector: '#btn-export', // 目标元素的 CSS 选择器(建议加 id)
type: 'new', // new | modified | removed
title: '新增:导出按钮',
desc: '支持将当前筛选结果导出为 Excel,权限:运营及以上角色可见',
},
{
id: 2,
selector: '[data-col="status"]',
type: 'modified',
title: '修改:状态列',
desc: '原「启用/禁用」两态改为「启用/禁用/待审核」三态,新增黄色待审核样式',
},
{
id: 3,
selector: '.guide-removed-1', // 被移除位置用占位元素标记(见下方说明)
type: 'removed',
title: '移除:批量操作栏',
desc: '当前版本不需要批量操作,已移除;后续迭代版本再加',
},
];
// ===== 颜色映射 =====
const guideColors = {
new: { border: '#3e8dff', label: '#3e8dff', text: '新增' },
modified: { border: '#ffb71d', label: '#ffb71d', text: '修改' },
removed: { border: '#ff8b78', label: '#ff8b78', text: '移除' },
};
let guideActive = true;
// 记录已包裹的元素,用于还原
const highlightedEls = [];
function renderGuide() {
// 1. 清除旧状态
highlightedEls.forEach(({ el, wrapper }) => {
if (wrapper && wrapper.parentNode) {
wrapper.parentNode.insertBefore(el, wrapper); // 把原元素还原到 wrapper 位置
wrapper.remove();
}
el.classList.remove('guide-highlight-new', 'guide-highlight-modified', 'guide-highlight-removed');
});
highlightedEls.length = 0;
document.getElementById('guide-cards').innerHTML = '';
if (!guideActive) return;
guideSteps.forEach(step => {
const el = document.querySelector(step.selector);
if (!el) return;
const color = guideColors[step.type];
// 2. 高亮目标元素
el.classList.add(`guide-highlight-${step.type}`);
// 3. 用 wrapper 包裹,以便绝对定位序号徽标
const wrapper = document.createElement('span');
wrapper.style.cssText = 'position:relative; display:inline-block;';
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
const badge = document.createElement('span');
badge.style.cssText = `position:absolute;top:-10px;right:-10px;width:20px;height:20px;border-radius:50%;background:${color.border};color:#fff;font-size:11px;font-weight:700;display:flex;align-items:center;justify-content:center;z-index:101;pointer-events:none;`;
badge.textContent = step.id;
wrapper.appendChild(badge);
highlightedEls.push({ el, wrapper });
// 4. 生成说明卡片
const card = document.createElement('div');
card.className = 'guide-card';
card.innerHTML = `
<span class="guide-card-label" style="background:${color.label};">${color.text}</span>
<div class="guide-card-content">
<div class="guide-card-title">${step.id}. ${step.title}</div>
<div class="guide-card-desc">${step.desc}</div>
</div>
`;
card.onclick = () => el.scrollIntoView({ behavior: 'smooth', block: 'center' });
document.getElementById('guide-cards').appendChild(card);
});
}
function toggleGuide() {
guideActive = !guideActive;
const btn = document.getElementById('guide-toggle-btn');
btn.textContent = guideActive ? '关闭引导' : '开启引导';
btn.style.background = guideActive ? '#3e8dff' : '#8c8c8c';
renderGuide();
}
// 页面加载后自动渲染
window.addEventListener('DOMContentLoaded', renderGuide);
</script>
<!-- ==================== /需求引导层 ==================== -->如何标记被移除的元素
被移除的元素在页面上不再存在,需要插入一个占位符让引导层有东西可以高亮:
<!-- 原来批量操作栏的位置,已移除,保留占位供引导层高亮 -->
<div class="guide-removed-1" style="height: 0; overflow: hidden;" aria-hidden="true"></div>使用要点
1. selector 精确性:建议给需要标注的新增/修改元素加 id 属性,如 id="btn-export" 2. 卡片位置:默认显示在页面左下角,可根据页面内容调整 #guide-cards 的 bottom/left 值 3. 卡片数量:建议不超过 8 个,过多时可合并相关改动 4. desc 写法:一句话说清楚「改了什么 + 为什么改 / 权限 / 注意事项」
---
9. 重要规则汇总
1. 必须使用项目配色:#3e8dff 主色,非 Ant Design 默认 #1890ff 2. 禁止使用 Emoji:所有图标使用 Ant Design 图标类名(anticon anticon-xxx)或 CSS 实现 3. 复刻模式两阶段:先1:1还原所有元素,确认后再叠加改动 4. 需求改动必须用引导层:有任何对源页面的改动,必须通过引导层标注,不能只写在文档面板 5. 状态标签优先用 ant-tag:需要带圆点背景色时才用 .kt-status 6. TL 布局模式:.tl-filter + .tl-table + 抽屉 7. Tailwind 仅用于布局:flex、grid、间距 8. 不使用 @apply:CDN 模式不生效 9. 遵循 Ant Design 组件结构:即使配色被覆盖,组件 HTML 结构和类名必须遵循 Ant Design 4.x 规范
原型HTML输出格式规范
文件输出位置
默认输出目录: .dev/prototype/
文件命名
- 新原型:
prototype-{YYYYMMDD}-{页面名称}.html - 改造原型:
prototype-{YYYYMMDD}-{页面名称}-v{N}.html - 示例:
prototype-20260506-role-list.html、prototype-20260506-role-list-v2.html
HTML 文件结构
每个HTML文件必须包含完整原型,可直接在浏览器打开。
必需的 HEAD 部分
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>原型 - {页面名称}</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.24.15/antd.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<!-- 项目配色覆盖 + 自定义组件样式 -->
<style>/* 见 design-guide.md 中的完整样式 */</style>
</head>BODY 结构
<body>
<div class="ant-layout"> ← 整体布局
<aside> ← 深色侧边栏 (固定定位)
<div> ← 右侧区域
<header> ← 白色顶栏 (sticky)
<main> ← 内容区 (#f0f2f5 背景)
<div class="tl-filter"> ← 筛选区
<div class="tl-table"> ← 表格区
</main>
</div>
</div>
<div id="drawer"> ← 抽屉 (display:none)
<div id="confirmModal"> ← 确认弹窗 (display:none)
<button class="doc-toggle"> ← 文档面板切换按钮
<div class="doc-panel"> ← 文档面板 (默认隐藏)
<script> ← 交互脚本
</body>文档面板(可切换)
文档面板默认隐藏在右侧,点击固定在右侧的「交互说明」按钮展开。
实现方式
<!-- 切换按钮,固定在视口右侧中间 -->
<button class="doc-toggle" onclick="toggleDoc()">交互说明</button>
<!-- 文档面板,默认 right: -400px 隐藏 -->
<div class="doc-panel" id="docPanel">
<div class="doc-section">
<div class="doc-title">页面说明</div>
<!-- 页面功能描述 -->
</div>
<div class="doc-section">
<div class="doc-title">交互逻辑</div>
<!-- 各操作的行为说明 -->
</div>
<div class="doc-section">
<div class="doc-title">字段说明</div>
<!-- 表格列、表单字段说明 -->
</div>
</div>文档内容要求
| 板块 | 内容 |
|---|---|
| 页面说明 | 一句话概述页面用途 |
| 交互逻辑 | 每个按钮/操作的行为描述 |
| 字段说明 | 表格列含义、表单字段校验规则 |
| 状态说明 | 各状态值对应的颜色和含义 |
| 边界场景 | 空数据、加载中、错误等场景处理 |
抽屉/弹窗合并规则
所有抽屉和弹窗统一合并到主页面HTML中:
1. 抽屉:一个 <div id="drawer"> 容器,通过 JS 切换标题和内容
openDrawer(title, mode)- 打开抽屉,mode 可为 add/edit/detailcloseDrawer()- 关闭抽屉- detail 模式:只显示「返回」按钮,表单字段 disabled
- add/edit 模式:显示「取消」和「保存」按钮
2. 确认弹窗:一个 <div id="confirmModal"> 容器
openConfirmModal()- 打开确认弹窗closeConfirmModal()- 关闭确认弹窗
3. 多状态抽屉:如果页面需要多种抽屉内容(如新增和编辑表单不同),通过 JS 动态切换内部 HTML
Mock 交互脚本
每个HTML文件底部必须包含交互脚本:
<script>
// 抽屉控制
function openDrawer(title, mode) {
document.getElementById('drawer').style.display = 'block';
document.getElementById('drawerTitle').textContent = title;
const footer = document.getElementById('drawerFooter');
if (mode === 'detail') {
footer.innerHTML = '<button class="ant-btn" onclick="closeDrawer()">返回</button>';
} else {
footer.innerHTML = '<button class="ant-btn" style="margin-right:8px" onclick="closeDrawer()">取消</button><button class="ant-btn ant-btn-primary">保存</button>';
}
}
function closeDrawer() {
document.getElementById('drawer').style.display = 'none';
}
// 确认弹窗
function openConfirmModal() {
document.getElementById('confirmModal').style.display = 'block';
}
function closeConfirmModal() {
document.getElementById('confirmModal').style.display = 'none';
}
// 文档面板
function toggleDoc() {
document.getElementById('docPanel').classList.toggle('open');
}
// 通用操作
function mockAction(action, data) {
console.log('[Mock]', action, data);
}
</script>复刻模式输出
复刻模式生成的HTML应尽可能还原源 .vue 页面的完整结构:
1. 筛选区:根据 searchSchema 生成所有搜索字段 2. 操作按钮:保留原有按钮,移除权限判断(hasButtonPerm 直接为 true) 3. 表格列:根据 columns 配置生成所有列 4. 状态标签:使用 .kt-status 替代 <yc-status> 5. 抽屉:根据 EditForm 组件生成表单 6. 详情模式:详情展示替代为描述列表
自检规则
1. HTML 可直接在浏览器打开,无控制台报错 2. 配色使用项目色值(#3e8dff 主色) 3. 侧边栏 #1e202a 4. 抽屉/弹窗可正常打开关闭 5. 文档面板可切换显示 6. 文件保存到 .dev/prototype/ 7. 不覆盖已有文件 8. 多页面生成后存在 index.html 目录页
目录页(index.html)
当批量生成多个原型页面时,必须在 .dev/prototype/ 目录下生成一个 index.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="https://cdnjs.cloudflare.com/ajax/libs/antd/4.24.15/antd.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body { background: #f0f2f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
.index-card { background: #fff; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); padding: 20px 24px; margin-bottom: 16px; transition: box-shadow 0.2s; }
.index-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.index-card a { color: #3e8dff; text-decoration: none; font-size: 16px; font-weight: 500; }
.index-card a:hover { color: #5ea3ff; }
.page-type-tag { display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-left: 8px; }
.type-list { background: #e6f7ff; color: #1890ff; }
.type-form { background: #f6ffed; color: #52c41a; }
.type-detail { background: #fff7e6; color: #fa8c16; }
.type-dashboard { background: #f9f0ff; color: #722ed1; }
.meta { color: rgba(0,0,0,0.45); font-size: 13px; margin-top: 8px; }
</style>
</head>
<body>
<div style="max-width: 800px; margin: 0 auto; padding: 40px 24px;">
<h1 style="font-size: 24px; font-weight: 600; color: rgba(0,0,0,0.85); margin-bottom: 8px;">原型页面目录</h1>
<p style="color: rgba(0,0,0,0.45); font-size: 14px; margin-bottom: 32px;">生成日期:{YYYY-MM-DD} | 共 {N} 个页面</p>
<!-- 页面列表,每个原型一个卡片 -->
<div class="index-card">
<a href="prototype-20260512-role-list.html" target="_blank">
角色列表
<span class="page-type-tag type-list">list</span>
</a>
<div class="meta">角色管理列表页,支持角色的新增、编辑、删除和查看详情</div>
</div>
<div class="index-card">
<a href="prototype-20260512-user-detail.html" target="_blank">
用户详情
<span class="page-type-tag type-detail">detail</span>
</a>
<div class="meta">用户信息详情页,展示用户基本信息和关联记录</div>
</div>
</div>
</body>
</html>目录页规则
1. 生成时机:每次批量生成完成后自动生成,或手动生成单个页面后更新 2. 位置:.dev/prototype/index.html,与所有原型页面同目录 3. 内容:每个原型页面对应一个卡片,包含页面名称(可点击跳转)、页面类型标签、功能描述 4. 更新策略:新生成时覆盖旧的 index.html(目录页始终是最新快照) 5. 排序:按页面类型分组(list → form → detail → dashboard),同类型按名称排序
#!/usr/bin/env python3
"""
batch_generate.py - 批量生成HTML原型页面。
用法:
python batch_generate.py --config pages.json
pages.json 格式:
{
"output_path": ".dev/prototype",
"pages": [
{
"page_name": "角色列表",
"page_type": "list",
"scenario": "new",
"clarification": {...}
},
...
]
}
"""
import argparse
import json
import os
import sys
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime
from pathlib import Path
def generate_single_page(page_config, output_path, script_dir):
"""生成单个页面"""
page_config['output_path'] = output_path
input_json = json.dumps(page_config, ensure_ascii=False)
script_path = os.path.join(script_dir, 'generate_page.py')
try:
result = subprocess.run(
[sys.executable, script_path, '--input', input_json],
capture_output=True, text=True, encoding='utf-8'
)
if result.returncode == 0:
return {'page': page_config.get('page_name', ''), 'status': 'success', 'output': result.stdout}
else:
return {'page': page_config.get('page_name', ''), 'status': 'error', 'error': result.stderr}
except Exception as e:
return {'page': page_config.get('page_name', ''), 'status': 'error', 'error': str(e)}
def generate_index_html(output_path, pages_info):
"""生成 index.html 目录页"""
type_order = {'list': 0, 'form': 1, 'detail': 2, 'dashboard': 3}
type_labels = {'list': 'list', 'form': 'form', 'detail': 'detail', 'dashboard': 'dashboard'}
type_css = {'list': 'type-list', 'form': 'type-form', 'detail': 'type-detail', 'dashboard': 'type-dashboard'}
sorted_pages = sorted(pages_info, key=lambda p: (type_order.get(p.get('page_type', 'list'), 99), p.get('page_name', '')))
cards_html = ''
for page in sorted_pages:
file_name = page.get('file_name', '')
page_name = page.get('page_name', '')
page_type = page.get('page_type', 'list')
description = page.get('description', '')
type_css_class = type_css.get(page_type, 'type-list')
type_label = type_labels.get(page_type, page_type)
cards_html += f'''
<div class="index-card">
<a href="{file_name}" target="_blank">
{page_name}
<span class="page-type-tag {type_css_class}">{type_label}</span>
</a>
<div class="meta">{description}</div>
</div>'''
date_str = datetime.now().strftime('%Y-%m-%d')
total = len(sorted_pages)
html = f'''<!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="https://cdnjs.cloudflare.com/ajax/libs/antd/4.24.15/antd.min.css">
<script src="https://cdn.tailwindcss.com"></script>
<style>
body {{ background: #f0f2f5; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }}
.index-card {{ background: #fff; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); padding: 20px 24px; margin-bottom: 16px; transition: box-shadow 0.2s; }}
.index-card:hover {{ box-shadow: 0 4px 12px rgba(0,0,0,0.1); }}
.index-card a {{ color: #3e8dff; text-decoration: none; font-size: 16px; font-weight: 500; }}
.index-card a:hover {{ color: #5ea3ff; }}
.page-type-tag {{ display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 12px; margin-left: 8px; }}
.type-list {{ background: #e6f7ff; color: #1890ff; }}
.type-form {{ background: #f6ffed; color: #52c41a; }}
.type-detail {{ background: #fff7e6; color: #fa8c16; }}
.type-dashboard {{ background: #f9f0ff; color: #722ed1; }}
.meta {{ color: rgba(0,0,0,0.45); font-size: 13px; margin-top: 8px; }}
</style>
</head>
<body>
<div style="max-width: 800px; margin: 0 auto; padding: 40px 24px;">
<h1 style="font-size: 24px; font-weight: 600; color: rgba(0,0,0,0.85); margin-bottom: 8px;">原型页面目录</h1>
<p style="color: rgba(0,0,0,0.45); font-size: 14px; margin-bottom: 32px;">生成日期:{date_str} | 共 {total} 个页面</p>
{cards_html}
</div>
</body>
</html>'''
index_path = os.path.join(output_path, 'index.html')
with open(index_path, 'w', encoding='utf-8') as f:
f.write(html)
return index_path
def main():
parser = argparse.ArgumentParser(description='批量生成HTML原型页面')
parser.add_argument('--config', required=True, help='批量配置文件路径')
parser.add_argument('--workers', type=int, default=3, help='并行工作线程数(默认3)')
parser.add_argument('--skip-index', action='store_true', help='跳过生成 index.html 目录页')
args = parser.parse_args()
with open(args.config, 'r', encoding='utf-8') as f:
config = json.load(f)
output_path = config.get('output_path', '.dev/prototype')
pages = config.get('pages', [])
script_dir = os.path.dirname(os.path.abspath(__file__))
os.makedirs(output_path, exist_ok=True)
results = []
with ThreadPoolExecutor(max_workers=args.workers) as executor:
futures = {
executor.submit(generate_single_page, page, output_path, script_dir): page
for page in pages
}
for future in as_completed(futures):
results.append(future.result())
success_count = sum(1 for r in results if r['status'] == 'success')
error_count = sum(1 for r in results if r['status'] == 'error')
print(f'\n批量生成完成: {success_count} 成功, {error_count} 失败')
for r in results:
if r['status'] == 'success':
print(f" [OK] {r['page']}: {r['output'].strip()}")
else:
print(f" [FAIL] {r['page']}: {r['error']}")
# 输出结果汇总
summary = {
'total': len(pages),
'success': success_count,
'error': error_count,
'results': results
}
summary_path = os.path.join(output_path, 'batch_summary.json')
with open(summary_path, 'w', encoding='utf-8') as f:
json.dump(summary, f, ensure_ascii=False, indent=2)
print(f'\n汇总已保存到: {summary_path}')
# 生成 index.html 目录页
if not args.skip_index:
pages_info = []
for i, page_config in enumerate(pages):
page_info = {
'page_name': page_config.get('page_name', ''),
'page_type': page_config.get('page_type', 'list'),
'description': page_config.get('clarification', {}).get('description', '')
if isinstance(page_config.get('clarification'), dict)
else '',
}
# 从生成结果中提取文件名
if i < len(results) and results[i]['status'] == 'success':
try:
result_data = json.loads(results[i]['output'])
page_info['file_name'] = result_data.get('file_name', '')
except (json.JSONDecodeError, KeyError):
page_info['file_name'] = ''
pages_info.append(page_info)
index_path = generate_index_html(output_path, pages_info)
print(f'目录页已生成: {index_path}')
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
clone_page.py - 从 .vue 文件提取组件结构,输出 JSON 供原型生成使用。
用法:
python clone_page.py --vue-file "path/to/page.vue" [--data-file "path/to/data.ts"] [--output ".dev/prototype/analysis.json"]
功能:
1. 解析 .vue SFC 文件,提取 template、script、style 部分
2. 分析 template 中的组件结构(tl 布局、表格、筛选、抽屉等)
3. 解析 data.ts 中的 columns、searchSchema、状态枚举
4. 输出结构化 JSON
"""
import argparse
import json
import os
import re
import sys
def read_file(path):
"""读取文件内容"""
if not os.path.exists(path):
return None
with open(path, 'r', encoding='utf-8') as f:
return f.read()
def parse_vue_sfc(content):
"""解析 Vue SFC,提取 template、script、style 部分"""
result = {'template': '', 'script': '', 'styles': []}
# 提取 template
template_match = re.search(r'<template>(.*?)</template>', content, re.DOTALL)
if template_match:
result['template'] = template_match.group(1).strip()
# 提取 script
script_match = re.search(r'<script[^>]*>(.*?)</script>', content, re.DOTALL)
if script_match:
result['script'] = script_match.group(1).strip()
# 提取 style
style_matches = re.findall(r'<style[^>]*>(.*?)</style>', content, re.DOTALL)
result['styles'] = [s.strip() for s in style_matches]
return result
def extract_template_components(template):
"""从 template 中提取组件结构"""
components = {
'has_tl_layout': bool(re.search(r'<tl[>\s]', template)),
'has_tl_filter': bool(re.search(r'<tl-filter', template)),
'has_tl_table': bool(re.search(r'<tl-table', template)),
'has_tl_drawer': bool(re.search(r'<tl-drawer', template)),
'has_yc_form': bool(re.search(r'<yc-form', template)),
'has_yc_status': bool(re.search(r'<yc-status', template)),
'has_action_group': bool(re.search(r'<action-group', template)),
'has_a_table': bool(re.search(r'<a-table', template)),
'has_modal': bool(re.search(r'<a-modal|<tl-modal', template)),
}
# 提取 drawer 属性
drawer_attrs = {}
drawer_match = re.search(r'<tl-drawer([^>]*)>', template)
if drawer_match:
attrs_str = drawer_match.group(1)
width_match = re.search(r'width="([^"]*)"', attrs_str)
if width_match:
drawer_attrs['width'] = width_match.group(1)
# 提取 action-group 中的按钮
action_buttons = []
ag_matches = re.findall(r'<a-button([^>]*)>(.*?)</a-button>', template, re.DOTALL)
for attrs, btn_text in ag_matches:
clean_text = re.sub(r'<[^>]+>', '', btn_text).strip()
if clean_text:
btn_type = 'danger' if 'danger' in attrs else 'link'
action_buttons.append({
'text': clean_text,
'type': btn_type
})
# 提取搜索按钮区域
search_buttons = []
filter_section = re.search(r'<tl-filter-left>(.*?)</tl-filter-left>', template, re.DOTALL)
if filter_section:
btn_matches = re.findall(r'<a-button([^>]*)>(.*?)</a-button>', filter_section.group(1), re.DOTALL)
for attrs, text in btn_matches:
clean_text = re.sub(r'<[^>]+>', '', text).strip()
btn_type = 'primary' if 'type="primary"' in attrs else 'default'
click_handler = re.search(r'@click="([^"]*)"', attrs)
search_buttons.append({
'text': clean_text,
'type': btn_type,
'handler': click_handler.group(1) if click_handler else None
})
# 提取 bodyCell 模板中的列渲染逻辑
body_cell_blocks = re.findall(r'<template\s+#bodyCell[^>]*>(.*?)</template>', template, re.DOTALL)
return {
**components,
'drawer_attrs': drawer_attrs,
'action_buttons': action_buttons,
'search_buttons': search_buttons,
'body_cell_blocks': body_cell_blocks,
}
def extract_script_data(script):
"""从 script 中提取数据结构信息"""
result = {
'imports': [],
'page_name': None,
'in_params': {},
'api_methods': [],
'drawer_modes': [],
}
# 提取 import
import_matches = re.findall(r"import\s+\{([^}]+)\}\s+from\s+'([^']+)'", script)
for items, source in import_matches:
for item in items.split(','):
result['imports'].append({'name': item.strip(), 'from': source})
# 提取 PAGE_NAME
page_name_match = re.search(r"PAGE_NAME\s*=\s*'([^']+)'", script)
if page_name_match:
result['page_name'] = page_name_match.group(1)
# 提取 inParams 结构
in_params_match = re.search(r'(?:const|let)\s+\w+\s*=\s*reactive(?:<[^>]+>)?\(\{([^}]+)\}', script)
if in_params_match:
params_str = in_params_match.group(1)
for line in params_str.split(','):
line = line.strip()
if ':' in line:
key, value = line.split(':', 1)
result['in_params'][key.strip()] = value.strip()
# 提取 drawer 模式
drawer_open_matches = re.findall(r"drawer\.open\('(\w+)'", script)
result['drawer_modes'] = list(set(drawer_open_matches))
# 提取 API 方法
api_matches = re.findall(r"await\s+(\w+)\(", script)
result['api_methods'] = list(set(api_matches))
return result
def extract_data_ts(content):
"""解析 data.ts 文件,提取 columns、searchSchema、枚举"""
result = {
'columns': [],
'search_schema': [],
'enums': {},
}
if not content:
return result
# 提取 columns 数组
columns_match = re.search(r'(?:export\s+const\s+columns\s*[:=]\s*)(?:\w+\s*)?(?:\[|=\s*\[)(.*?)\]', content, re.DOTALL)
if columns_match:
cols_str = columns_match.group(1)
# 提取每个 column 对象
col_objects = re.findall(r'\{([^}]+)\}', cols_str)
for col_str in col_objects:
col = {}
title_match = re.search(r'title:\s*["\']([^"\']+)["\']', col_str)
data_index_match = re.search(r'dataIndex:\s*["\']([^"\']+)["\']', col_str)
width_match = re.search(r'width:\s*["\']?(\d+px?)["\']?', col_str)
align_match = re.search(r'align:\s*["\']([^"\']+)["\']', col_str)
fixed_match = re.search(r'fixed:\s*["\']([^"\']+)["\']', col_str)
if title_match:
col['title'] = title_match.group(1)
if data_index_match:
col['dataIndex'] = data_index_match.group(1)
if width_match:
col['width'] = width_match.group(1)
if align_match:
col['align'] = align_match.group(1)
if fixed_match:
col['fixed'] = fixed_match.group(1)
if col:
result['columns'].append(col)
# 提取 searchSchema
schema_match = re.search(r'(?:export\s+const\s+searchSchema\s*[:=]\s*)(?:\w+\s*)?\[([\s\S]*?)\]', content)
if schema_match:
schema_str = schema_match.group(1)
field_objects = re.findall(r'\{([^}]+)\}', schema_str)
for field_str in field_objects:
field = {}
field_match = re.search(r'field:\s*["\']([^"\']+)["\']', field_str)
label_match = re.search(r'label:\s*["\']([^"\']+)["\']', field_str)
component_match = re.search(r'component:\s*["\']([^"\']+)["\']', field_str)
placeholder_match = re.search(r'placeholder:\s*["\']([^"\']+)["\']', field_str)
if field_match:
field['field'] = field_match.group(1)
if label_match:
field['label'] = label_match.group(1)
if component_match:
field['component'] = component_match.group(1)
if placeholder_match:
field['placeholder'] = placeholder_match.group(1)
if field:
result['search_schema'].append(field)
# 提取枚举定义
enum_matches = re.findall(r'(?:export\s+)?(?:const|enum)\s+(\w+)\s*[=:]\s*\{([^}]+)\}', content)
for name, values_str in enum_matches:
enum_values = {}
for item in values_str.split(','):
item = item.strip()
if '=' in item:
key, value = item.split('=', 1)
enum_values[key.strip()] = value.strip().strip("'\"")
if enum_values:
result['enums'][name] = enum_values
return result
def find_associated_files(vue_path):
"""查找与 .vue 文件关联的 data.ts 和子组件"""
directory = os.path.dirname(vue_path)
associated = {'data_ts': None, 'components': []}
# 查找 data.ts
data_file = os.path.join(directory, 'data.ts')
if os.path.exists(data_file):
associated['data_ts'] = data_file
# 查找 components/ 目录下的子组件
components_dir = os.path.join(directory, 'components')
if os.path.isdir(components_dir):
for f in os.listdir(components_dir):
if f.endswith('.vue'):
associated['components'].append(os.path.join(components_dir, f))
return associated
def analyze_vue(vue_path, data_file=None):
"""分析 .vue 文件,输出结构化 JSON"""
content = read_file(vue_path)
if not content:
return {'error': f'文件不存在: {vue_path}'}
# 解析 SFC
sfc = parse_vue_sfc(content)
# 提取组件结构
template_info = extract_template_components(sfc['template'])
# 提取脚本数据
script_info = extract_script_data(sfc['script'])
# 查找关联文件
associated = find_associated_files(vue_path)
# 解析 data.ts
data_ts_content = None
data_file_path = data_file or associated['data_ts']
if data_file_path:
data_ts_content = read_file(data_file_path)
data_info = extract_data_ts(data_ts_content)
# 解析子组件(EditForm 等)
sub_components = {}
for comp_path in associated['components']:
comp_content = read_file(comp_path)
if comp_content:
comp_sfc = parse_vue_sfc(comp_content)
comp_name = os.path.splitext(os.path.basename(comp_path))[0]
sub_components[comp_name] = {
'template': comp_sfc['template'][:2000], # 限制长度
'has_form': bool(re.search(r'<a-form', comp_sfc['template'])),
'form_fields': extract_form_fields(comp_sfc['template']),
}
return {
'source_file': vue_path,
'page_type': 'list' if template_info['has_a_table'] else 'form' if template_info.get('has_form') else 'detail',
'template_components': template_info,
'script_data': script_info,
'data_config': data_info,
'sub_components': sub_components,
'associated_files': {
'data_ts': data_file_path,
'components': associated['components'],
}
}
def extract_form_fields(template):
"""从表单模板中提取字段"""
fields = []
# 匹配 a-form-item
form_items = re.findall(r'<a-form-item[^>]*>(.*?)</a-form-item>', template, re.DOTALL)
for item in form_items:
field = {}
label_match = re.search(r'label="([^"]*)"', item)
name_match = re.search(r'name="([^"]*)"', item)
required = 'required' in item or '*必填' in item
if label_match:
field['label'] = label_match.group(1)
if name_match:
field['name'] = name_match.group(1)
field['required'] = required
# 识别输入类型
if '<a-input' in item:
field['type'] = 'input'
elif '<a-textarea' in item:
field['type'] = 'textarea'
elif '<a-select' in item:
field['type'] = 'select'
elif '<a-tree-select' in item:
field['type'] = 'tree-select'
elif '<a-input-number' in item:
field['type'] = 'number'
elif '<a-switch' in item:
field['type'] = 'switch'
elif '<a-radio' in item:
field['type'] = 'radio'
elif '<a-date-picker' in item:
field['type'] = 'date'
else:
field['type'] = 'input'
# 提取 placeholder
placeholder_match = re.search(r'placeholder="([^"]*)"', item)
if placeholder_match:
field['placeholder'] = placeholder_match.group(1)
if field:
fields.append(field)
return fields
def main():
parser = argparse.ArgumentParser(description='分析 .vue 文件结构')
parser.add_argument('--vue-file', required=True, help='.vue 文件路径')
parser.add_argument('--data-file', help='data.ts 文件路径(可选,自动查找)')
parser.add_argument('--output', help='输出 JSON 文件路径(可选,默认输出到控制台)')
args = parser.parse_args()
result = analyze_vue(args.vue_file, args.data_file)
output_json = json.dumps(result, ensure_ascii=False, indent=2)
if args.output:
os.makedirs(os.path.dirname(args.output) if os.path.dirname(args.output) else '.', exist_ok=True)
with open(args.output, 'w', encoding='utf-8') as f:
f.write(output_json)
print(f"分析结果已保存到: {args.output}")
else:
print(output_json)
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
generate_page.py - 根据配置生成HTML原型页面。
用法:
python generate_page.py --input '{
"page_name": "角色列表",
"page_type": "list",
"scenario": "new",
"output_path": ".dev/prototype",
"clarification": {...}
}'
或指定配置文件:
python generate_page.py --config page_config.json
"""
import argparse
import json
import os
import re
import sys
from datetime import datetime
# 项目配色常量
COLORS = {
'primary': '#3e8dff',
'primary_bg': '#d6e7ff',
'primary_hover': '#5ea3ff',
'success': '#00e5c7',
'success_bg': '#effffb',
'danger': '#ff8b78',
'danger_bg': '#fff5f3',
'warning': '#ffb71d',
'warning_bg': '#fff2d6',
'default': '#bfc6d1',
'default_bg': '#f5f6f9',
'sidebar': '#1e202a',
'content_bg': '#f0f2f5',
}
ANTD_CSS_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/antd/4.24.15/antd.min.css'
TAILWIND_CDN = 'https://cdn.tailwindcss.com'
def generate_css_variables():
"""生成 CSS 变量和覆盖样式"""
return f"""
:root {{
--kt-primary: {COLORS['primary']};
--kt-primary-bg: {COLORS['primary_bg']};
--kt-success: {COLORS['success']};
--kt-success-bg: {COLORS['success_bg']};
--kt-danger: {COLORS['danger']};
--kt-danger-bg: {COLORS['danger_bg']};
--kt-warning: {COLORS['warning']};
--kt-warning-bg: {COLORS['warning_bg']};
--kt-default: {COLORS['default']};
--kt-default-bg: {COLORS['default_bg']};
--kt-sidebar: {COLORS['sidebar']};
--kt-content-bg: {COLORS['content_bg']};
}}
.ant-btn-primary {{ background-color: {COLORS['primary']}; border-color: {COLORS['primary']}; }}
.ant-btn-primary:hover {{ background-color: {COLORS['primary_hover']}; border-color: {COLORS['primary_hover']}; }}
.ant-btn-primary:active {{ background-color: #2a75d4; border-color: #2a75d4; }}
a, .ant-btn-link {{ color: {COLORS['primary']}; }}
a:hover, .ant-btn-link:hover {{ color: {COLORS['primary_hover']}; }}
.kt-status {{ display: inline-flex; align-items: center; gap: 6px; padding: 0 8px; height: 24px; border-radius: 4px; font-size: 12px; line-height: 24px; }}
.kt-status-dot {{ width: 6px; height: 6px; border-radius: 50%; display: inline-block; }}
.kt-status-primary {{ background: {COLORS['primary_bg']}; color: {COLORS['primary']}; }}
.kt-status-primary .kt-status-dot {{ background: {COLORS['primary']}; }}
.kt-status-success {{ background: {COLORS['success_bg']}; color: #00a89a; }}
.kt-status-success .kt-status-dot {{ background: {COLORS['success']}; }}
.kt-status-danger {{ background: {COLORS['danger_bg']}; color: #d4634f; }}
.kt-status-danger .kt-status-dot {{ background: {COLORS['danger']}; }}
.kt-status-warning {{ background: {COLORS['warning_bg']}; color: #c99200; }}
.kt-status-warning .kt-status-dot {{ background: {COLORS['warning']}; }}
.kt-status-default {{ background: {COLORS['default_bg']}; color: #8c95a6; }}
.kt-status-default .kt-status-dot {{ background: {COLORS['default']}; }}
.tl-main {{ flex: 1; overflow: auto; display: flex; flex-direction: column; gap: 0; }}
.tl-filter {{ background: #fff; padding: 16px 24px; border-radius: 8px; margin-bottom: 16px; }}
.tl-filter-left {{ display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }}
.tl-table {{ background: #fff; padding: 24px; padding-top: 16px; border-radius: 8px; box-shadow: 0 1px 2px rgba(0,0,0,0.04); }}
.action-group {{ display: inline-flex; align-items: center; gap: 0; }}
.action-group .ant-btn-link {{ padding: 0 4px; height: auto; }}
.action-group .action-divider {{ width: 1px; height: 14px; background: #e8e8e8; margin: 0 4px; }}
.doc-toggle {{ position: fixed; top: 50%; right: 0; transform: translateY(-50%); z-index: 1001; background: {COLORS['primary']}; color: #fff; border: none; padding: 12px 4px; border-radius: 4px 0 0 4px; cursor: pointer; writing-mode: vertical-rl; font-size: 13px; letter-spacing: 2px; transition: right 0.3s; }}
.doc-panel {{ position: fixed; top: 0; right: -400px; width: 400px; height: 100vh; background: #fafafa; border-left: 1px solid #e8e8e8; padding: 24px; overflow-y: auto; z-index: 1000; transition: right 0.3s; }}
.doc-panel.open {{ right: 0; }}
.doc-panel .doc-section {{ background: #fff; border-radius: 8px; padding: 16px; margin-bottom: 12px; }}
.doc-panel .doc-title {{ font-size: 15px; font-weight: 600; color: rgba(0,0,0,0.85); margin-bottom: 12px; padding-bottom: 8px; border-bottom: 1px solid #f0f0f0; }}
"""
def generate_sidebar(menu_items, active_item):
"""生成侧边栏 HTML"""
# 图标映射:菜单名 -> anticon 类名
ICON_MAP = {
'首页': 'anticon-home',
'Agent 管理': 'anticon-robot',
'角色管理': 'anticon-team',
'用户管理': 'anticon-user',
'系统设置': 'anticon-setting',
'日志': 'anticon-file-text',
'监控': 'anticon-dashboard',
'任务': 'anticon-thunderbolt',
}
menu_html = ''
for item in menu_items:
is_active = item.get('name') == active_item
# 优先使用配置中的 icon,否则从映射表取,最后用色块首字母
icon_class = item.get('icon', '')
if icon_class and not icon_class.startswith('anticon'):
# 如果传入的不是 anticon 类名,忽略(禁止 emoji)
icon_class = ''
if not icon_class:
icon_class = ICON_MAP.get(item['name'], '')
if icon_class:
icon_html = f'<span class="anticon {icon_class}" style="margin-right: 10px; font-size: 14px;"></span>'
else:
# fallback: 首字母色块
first_char = item['name'][0] if item['name'] else ''
icon_html = f'<span style="display: inline-flex; width: 18px; height: 18px; border-radius: 3px; background: {"#3e8dff" if is_active else "rgba(255,255,255,0.25)"}; color: #fff; font-size: 11px; align-items: center; justify-content: center; margin-right: 10px;">{first_char}</span>'
style = f'padding: 10px 16px; background: {COLORS["primary"]}; color: #fff; font-size: 14px; cursor: pointer; border-radius: 4px; margin: 0 8px; display: flex; align-items: center;' if is_active else f'padding: 10px 20px; color: rgba(255,255,255,0.65); font-size: 14px; cursor: pointer; display: flex; align-items: center;'
menu_html += f' <li style="{style}">{icon_html}{item["name"]}</li>\n'
return f""" <aside style="background: {COLORS['sidebar']}; width: 220px; flex-shrink: 0; position: fixed; left: 0; top: 0; bottom: 0; z-index: 100;">
<div style="height: 56px; display: flex; align-items: center; padding: 0 20px; border-bottom: 1px solid rgba(255,255,255,0.08);">
<span style="color: #fff; font-size: 16px; font-weight: 600;">KT Agent</span>
</div>
<ul style="list-style: none; padding: 8px 0; margin: 0;">
{menu_html} </ul>
</aside>"""
def generate_header(title):
"""生成顶栏 HTML"""
return f""" <header style="height: 56px; background: #fff; padding: 0 24px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 1px 4px rgba(0,21,41,0.08); position: sticky; top: 0; z-index: 99;">
<span style="font-size: 16px; font-weight: 500; color: rgba(0,0,0,0.85);">{title}</span>
<div style="display: flex; align-items: center; gap: 16px;">
<span style="color: rgba(0,0,0,0.65); font-size: 14px;">管理员</span>
</div>
</header>"""
def generate_filter_fields(fields):
"""生成筛选字段 HTML"""
html = ''
for field in fields:
ftype = field.get('type', 'input')
label = field.get('label', '')
placeholder = field.get('placeholder', f'请输入{label}')
width = field.get('width', '200px')
if ftype == 'input':
html += f' <input class="ant-input" placeholder="{placeholder}" style="width: {width};">\n'
elif ftype == 'select':
options = field.get('options', [])
default = field.get('default', f'全部{label}')
html += f' <div class="ant-select ant-select-single" style="width: {width};">\n'
html += f' <div class="ant-select-selector"><span class="ant-select-selection-item">{default}</span></div>\n'
html += f' </div>\n'
elif ftype == 'date':
html += f' <span class="ant-picker" style="width: 220px;"><span class="ant-picker-input"><input readonly placeholder="{placeholder}" style="border: none; outline: none; width: 100%;"></span></span>\n'
return html
def generate_table_columns(columns):
"""生成表格列 HTML"""
thead = ''
for col in columns:
title = col.get('title', '')
width = col.get('width', '')
align = col.get('align', 'left')
width_style = f'width: {width};' if width else ''
align_style = f'text-align: {align};' if align != 'left' else ''
style = f'{width_style} {align_style}'.strip()
style_attr = f' style="{style}"' if style else ''
thead += f' <th class="ant-table-cell"{style_attr}>{title}</th>\n'
return thead
def generate_table_rows(columns, rows):
"""生成表格行 HTML"""
html = ''
for i, row in enumerate(rows, 1):
html += ' <tr class="ant-table-row">\n'
for col in columns:
data_index = col.get('dataIndex', '')
col_type = col.get('type', 'text')
value = row.get(data_index, '')
if col_type == 'status':
status_type = row.get(f'{data_index}_type', 'default')
html += f' <td class="ant-table-cell"><span class="kt-status kt-status-{status_type}"><span class="kt-status-dot"></span>{value}</span></td>\n'
elif col_type == 'action':
actions = col.get('actions', ['详情', '编辑', '删除'])
action_html = ''
for j, action in enumerate(actions):
if j > 0:
action_html += '<span class="action-divider"></span>'
is_delete = '删除' in action
color = '#ff4d4f' if is_delete else COLORS['primary']
click = f'openConfirmModal()' if is_delete else f'openDrawer(\'{action}\', \'edit\')'
action_html += f'<a style="color: {color}; cursor: pointer;" onclick="{click}">{action}</a>'
html += f' <td class="ant-table-cell" style="text-align: center;"><div class="action-group">{action_html}</div></td>\n'
elif col_type == 'index':
html += f' <td class="ant-table-cell" style="text-align: center;">{i}</td>\n'
else:
html += f' <td class="ant-table-cell">{value}</td>\n'
html += ' </tr>\n'
return html
def generate_form_fields(fields):
"""生成表单字段 HTML"""
html = ''
for field in fields:
label = field.get('label', '')
ftype = field.get('type', 'input')
required = field.get('required', False)
placeholder = field.get('placeholder', f'请输入{label}')
disabled = field.get('disabled', False)
required_html = '<span style="color: #ff4d4f; margin-right: 4px;">*</span>' if required else ''
disabled_attr = 'disabled' if disabled else ''
html += f' <div class="ant-form-item">\n'
html += f' <div class="ant-form-item-label"><label>{required_html}{label}</label></div>\n'
html += f' <div class="ant-form-item-control"><div class="ant-form-item-control-input">\n'
if ftype in ('input', 'number'):
html += f' <input class="ant-input" placeholder="{placeholder}" style="max-width: 400px;" {disabled_attr}>\n'
elif ftype == 'textarea':
html += f' <textarea class="ant-input" placeholder="{placeholder}" rows="3" style="max-width: 400px; resize: vertical;" {disabled_attr}></textarea>\n'
elif ftype == 'select':
html += f' <div class="ant-select ant-select-single" style="width: 200px;"><div class="ant-select-selector"><span class="ant-select-selection-item">{placeholder}</span></div></div>\n'
elif ftype == 'switch':
html += f' <button class="ant-switch" type="button"></button>\n'
html += f' </div></div>\n'
html += f' </div>\n'
return html
def generate_list_page(config):
"""生成列表页 HTML"""
page_name = config.get('page_name', '页面')
clarification = config.get('clarification', {})
layout = clarification.get('layout', {})
data = clarification.get('data', {})
interactions = clarification.get('interactions', {})
# 筛选字段
filter_fields = layout.get('filter_fields', [])
filter_buttons = layout.get('filter_buttons', ['查询', '重置'])
action_buttons = layout.get('action_buttons', [])
# 表格
columns = data.get('columns', [{'title': '名称', 'dataIndex': 'name', 'type': 'text'}])
rows = data.get('rows', [{'name': '示例数据'}])
total = data.get('total', len(rows))
# 抽屉
drawer_width = layout.get('drawer_width', '980px')
form_fields = data.get('form_fields', [])
# 菜单
menu_items = layout.get('menu_items', [
{'name': '首页', 'icon': 'anticon-home'},
{'name': page_name, 'icon': 'anticon-file-text'},
])
# 生成筛选区
filter_html = generate_filter_fields(filter_fields)
filter_btn_html = ''
for btn in filter_buttons:
if btn == '查询':
filter_btn_html += f' <button class="ant-btn ant-btn-primary" onclick="mockAction(\'查询\')">{btn}</button>\n'
else:
filter_btn_html += f' <button class="ant-btn">{btn}</button>\n'
for btn in action_buttons:
filter_btn_html += f' <button class="ant-btn ant-btn-primary" onclick="openDrawer(\'{btn}\', \'add\')">{btn}</button>\n'
# 生成表格
thead_html = generate_table_columns(columns)
tbody_html = generate_table_rows(columns, rows)
# 生成表单
form_html = generate_form_fields(form_fields)
# 生成文档内容
doc_content = generate_doc_content(page_name, clarification)
# 组装完整 HTML
html = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>原型 - {page_name}</title>
<link rel="stylesheet" href="{ANTD_CSS_CDN}">
<script src="{TAILWIND_CDN}"></script>
<script src="https://at.alicdn.com/t/c/font_3649831_vn4l2gpuij.js"></script>
<style>{generate_css_variables()}
</style>
</head>
<body>
<div class="ant-layout" style="min-height: 100vh;">
{generate_sidebar(menu_items, page_name)}
<div style="margin-left: 220px; flex: 1; display: flex; flex-direction: column;">
{generate_header(page_name)}
<main style="flex: 1; padding: 16px; background: {COLORS['content_bg']}; overflow: auto;">
<div class="tl-filter">
<div class="tl-filter-left">
{filter_html}{filter_btn_html} </div>
</div>
<div class="tl-table">
<div class="ant-table-wrapper">
<div class="ant-table">
<div class="ant-table-container">
<div class="ant-table-content">
<table style="table-layout: auto; width: 100%;">
<thead class="ant-table-thead">
<tr>
{thead_html} </tr>
</thead>
<tbody class="ant-table-tbody">
{tbody_html} </tbody>
</table>
</div>
</div>
</div>
</div>
<div style="padding: 16px 0 0; display: flex; justify-content: flex-end;">
<ul class="ant-pagination">
<li class="ant-pagination-total-text">共 {total} 条</li>
<li class="ant-pagination-prev ant-pagination-disabled"><button class="ant-pagination-item-link" disabled><</button></li>
<li class="ant-pagination-item ant-pagination-item-active"><a>1</a></li>
<li class="ant-pagination-next"><button class="ant-pagination-item-link">></button></li>
</ul>
</div>
</div>
</main>
</div>
</div>
<!-- 抽屉 -->
<div id="drawer" style="display: none;">
<div class="ant-drawer" style="position: fixed; top: 0; right: 0; bottom: 0; z-index: 1000;">
<div class="ant-drawer-mask" onclick="closeDrawer()" style="background: rgba(0,0,0,0.45); position: fixed; inset: 0;"></div>
<div class="ant-drawer-content-wrapper" style="width: {drawer_width}; position: fixed; top: 0; right: 0; bottom: 0;">
<div class="ant-drawer-content">
<div class="ant-drawer-wrapper-body" style="display: flex; flex-direction: column; height: 100%;">
<div class="ant-drawer-header" style="padding: 16px 24px; border-bottom: 1px solid #f0f0f0; display: flex; align-items: center; justify-content: space-between;">
<div class="ant-drawer-title" id="drawerTitle" style="font-size: 16px; font-weight: 500;">新增</div>
<button onclick="closeDrawer()" style="border: none; background: none; font-size: 16px; cursor: pointer; color: rgba(0,0,0,0.45);">✕</button>
</div>
<div class="ant-drawer-body" style="flex: 1; overflow: auto; padding: 24px;">
<form class="ant-form ant-form-vertical">
{form_html} </form>
</div>
<div class="ant-drawer-footer" id="drawerFooter" style="border-top: 1px solid #f0f0f0; padding: 12px 24px; text-align: right;">
<button class="ant-btn" style="margin-right: 8px;" onclick="closeDrawer()">取消</button>
<button class="ant-btn ant-btn-primary">保存</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 确认弹窗 -->
<div id="confirmModal" style="display: none;">
<div class="ant-modal-mask" style="background: rgba(0,0,0,0.45); position: fixed; inset: 0; z-index: 1001;"></div>
<div class="ant-modal-wrap" style="position: fixed; inset: 0; z-index: 1002; display: flex; align-items: center; justify-content: center;">
<div class="ant-modal" style="width: 420px;">
<div class="ant-modal-content">
<div class="ant-modal-header"><div class="ant-modal-title">确认删除</div></div>
<div class="ant-modal-body" style="padding: 24px;"><p>确定要删除此记录吗?此操作不可撤销。</p></div>
<div class="ant-modal-footer">
<button class="ant-btn" onclick="closeConfirmModal()">取消</button>
<button class="ant-btn ant-btn-primary" style="background: #ff4d4f; border-color: #ff4d4f;">确认删除</button>
</div>
</div>
</div>
</div>
</div>
<!-- 文档面板 -->
<button class="doc-toggle" onclick="toggleDoc()">交互说明</button>
<div class="doc-panel" id="docPanel">
{doc_content}
</div>
<script>
function openDrawer(title, mode) {{
document.getElementById('drawer').style.display = 'block';
document.getElementById('drawerTitle').textContent = title;
const footer = document.getElementById('drawerFooter');
if (mode === 'detail') {{
footer.innerHTML = '<button class="ant-btn" onclick="closeDrawer()">返回</button>';
}} else {{
footer.innerHTML = '<button class="ant-btn" style="margin-right:8px" onclick="closeDrawer()">取消</button><button class="ant-btn ant-btn-primary">保存</button>';
}}
}}
function closeDrawer() {{ document.getElementById('drawer').style.display = 'none'; }}
function openConfirmModal() {{ document.getElementById('confirmModal').style.display = 'block'; }}
function closeConfirmModal() {{ document.getElementById('confirmModal').style.display = 'none'; }}
function toggleDoc() {{ document.getElementById('docPanel').classList.toggle('open'); }}
function mockAction(action, data) {{ console.log('[Mock]', action, data); }}
</script>
</body>
</html>"""
return html
def generate_doc_content(page_name, clarification):
"""生成文档面板内容"""
interactions = clarification.get('interactions', {})
data = clarification.get('data', {})
doc_html = f""" <div class="doc-section">
<div class="doc-title">页面说明</div>
<p style="color: rgba(0,0,0,0.65); font-size: 14px;">{page_name},支持数据的查询、新增、编辑和删除操作。</p>
</div>
<div class="doc-section">
<div class="doc-title">交互逻辑</div>
<div style="font-size: 14px; color: rgba(0,0,0,0.65); line-height: 2;">
<div><b>查询:</b>输入筛选条件后点击查询按钮筛选列表</div>
<div><b>重置:</b>清空筛选条件恢复默认列表</div>
<div><b>新增:</b>点击新增按钮打开右侧抽屉填写表单</div>
<div><b>编辑:</b>点击编辑打开抽屉并回填数据</div>
<div><b>删除:</b>弹出确认弹窗,确认后删除</div>
<div><b>详情:</b>点击详情打开只读抽屉</div>
</div>
</div>"""
# 字段说明
columns = data.get('columns', [])
if columns:
doc_html += '\n <div class="doc-section">\n <div class="doc-title">字段说明</div>\n'
doc_html += ' <table style="width: 100%; font-size: 13px; border-collapse: collapse;">\n'
doc_html += ' <tr style="border-bottom: 1px solid #f0f0f0;"><th style="text-align: left; padding: 6px 0; color: rgba(0,0,0,0.85);">列名</th><th style="text-align: left; padding: 6px 0; color: rgba(0,0,0,0.85);">字段</th></tr>\n'
for col in columns:
title = col.get('title', '')
data_index = col.get('dataIndex', '')
if title and data_index:
doc_html += f' <tr style="border-bottom: 1px solid #f0f0f0;"><td style="padding: 6px 0; color: rgba(0,0,0,0.65);">{title}</td><td style="padding: 6px 0; color: rgba(0,0,0,0.45);">{data_index}</td></tr>\n'
doc_html += ' </table>\n </div>'
return doc_html
def generate_output_filename(page_name, scenario, output_dir, version=None):
"""生成输出文件名"""
date_str = datetime.now().strftime('%Y%m%d')
safe_name = re.sub(r'[^\w一-鿿-]', '-', page_name).strip('-')
if scenario == 'refactor' and version:
filename = f'prototype-{date_str}-{safe_name}-v{version}.html'
else:
filename = f'prototype-{date_str}-{safe_name}.html'
# 检查文件是否已存在,如果存在则递增版本号
full_path = os.path.join(output_dir, filename)
if os.path.exists(full_path) and scenario != 'refactor':
v = 2
while os.path.exists(os.path.join(output_dir, f'prototype-{date_str}-{safe_name}-v{v}.html')):
v += 1
filename = f'prototype-{date_str}-{safe_name}-v{v}.html'
return filename
def main():
parser = argparse.ArgumentParser(description='生成HTML原型页面')
parser.add_argument('--input', help='JSON 配置字符串')
parser.add_argument('--config', help='JSON 配置文件路径')
args = parser.parse_args()
if args.config:
with open(args.config, 'r', encoding='utf-8') as f:
config = json.load(f)
elif args.input:
config = json.loads(args.input)
else:
print('错误: 请提供 --input 或 --config 参数', file=sys.stderr)
sys.exit(1)
page_type = config.get('page_type', 'list')
output_dir = config.get('output_path', '.dev/prototype')
os.makedirs(output_dir, exist_ok=True)
if page_type == 'list':
html = generate_list_page(config)
else:
# 其他类型暂使用列表页模板
html = generate_list_page(config)
filename = generate_output_filename(
config.get('page_name', 'page'),
config.get('scenario', 'new'),
output_dir,
config.get('version')
)
filepath = os.path.join(output_dir, filename)
with open(filepath, 'w', encoding='utf-8') as f:
f.write(html)
result = {
'file_path': filepath,
'file_name': filename,
'status': 'success',
'message': '生成成功',
'page_info': {
'name': config.get('page_name', ''),
'type': page_type,
}
}
print(json.dumps(result, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()
Related skills
FAQ
What does the output look like?
A single standalone HTML file styled with Ant Design 4.x CSS and the project palette, openable directly in a browser with hardcoded mock data.
Can it copy an existing page?
Yes. In clone mode it restores a source .vue page 1:1 (filters, table columns, buttons, forms) before layering on requested changes.