prd-workflow-cli 1.3.0 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -179,54 +179,51 @@ description: A2UI 可视化指南 - 架构图与界面原型
179
179
  📁 位置:02_迭代记录/第01轮迭代/C1_UI原型/"
180
180
  ```
181
181
 
182
- ### HTML 预览文件要求
182
+ ### 🚀 生成独立预览文件 (HTML)
183
183
 
184
- 1. 包含完整的 CSS 样式
185
- 2. 包含完整的渲染引擎 JS 代码
186
- 3. 内嵌 JSON 数据(不依赖外部文件)
187
- 4. 顶部显示需求编号、界面名称、确认时间
188
- 5. 可以脱离服务器,双击直接打开
184
+ PM 确认原型后,AI 必须生成一个**独立 HTML 文件**,该文件可脱离环境直接打开,方便分享给相关方。
189
185
 
190
- ---
186
+ **生成步骤**:
187
+ 1. 读取下方的 **[HTML 独立文件模板]** 代码
188
+ 2. 替换以下占位符:
189
+ - `{{TITLE}}` -> 需求编号+名称(如 "#REQ-001 用户登录")
190
+ - `{{REQ_ID}}` -> 需求编号
191
+ - `{{NAME}}` -> 界面名称
192
+ - `{{DATE}}` -> 当前日期 (YYYY-MM-DD)
193
+ - `{{JSON_DATA}}` -> 完整的 A2UI JSON 数据(注意:不要加引号,直接作为 JS 对象插入)
194
+ 3. **关键:必须保存两份文件**:
195
+ - 📄 **原始数据**:保存为 `.json` (如 `REQ-001-登录页.json`) -> **用于后续溯源和修改**
196
+ - 👁️ **预览页面**:保存为 `.html` (如 `REQ-001-登录页.html`) -> **用于交付和查看**
197
+ - 路径统一为:`02_迭代记录/第XX轮迭代/C1_UI原型/`
191
198
 
192
- ## 📝 第三部分:通用规范
199
+ **[HTML 独立文件模板]**:
193
200
 
194
- ### AI 触发流程
201
+ 请读取文件:`prd-cli/templates/a2ui-standalone.html` 获取完整模板代码(包含 HTML/CSS/React/AntD 逻辑)。
195
202
 
196
- ```
197
- 1. PM 描述结构/界面
198
-
199
- 2. AI 识别关键词("系统模块"、"页面"、"表单"等)
200
-
201
- 3. AI 主动提议:"让我生成一个可视化的图..."
202
-
203
- 4. AI 生成 JSON 并写入 `.a2ui/current.json`
204
-
205
- 5. AI 提示:👉 请刷新浏览器 (http://localhost:3333) 查看
206
-
207
- 6. PM 反馈 → AI 迭代修改 → PM 确认 → AI 正式保存
208
- ```
209
-
210
- ### 在 Markdown 中嵌入预览
203
+ ⚠️ **注意**:AI 在生成文件时,必须先读取上述模板文件的内容,然后执行替换操作。不要自己编造 HTML 结构。
211
204
 
212
- ```markdown
213
- ## 系统架构图
205
+ ### 👀 历史溯源与多文件预览
214
206
 
215
- > 查看 [系统架构图](./B1_架构图/架构图-系统架构.html)
207
+ **方法 A:命令行指定文件**
208
+ ```bash
209
+ prd ui ./path/to/specific.json
210
+ ```
216
211
 
217
- <!-- 或使用 iframe(部分编辑器支持)-->
218
- <iframe src="./B1_架构图/架构图-系统架构.html" width="100%" height="400"></iframe>
212
+ **方法 B:浏览器 URL 参数(推荐)**
213
+ 启动服务后,直接在浏览器地址栏添加 `?file=` 参数:
214
+ ```
215
+ http://localhost:3333/?file=02_迭代记录/第01轮迭代/C1_UI原型/REQ-001.json
219
216
  ```
220
217
 
221
- ### index.md 索引文件格式
218
+ > 💡 **提示**:你可以在 Markdown 文档中直接复制文件的相对路径,粘贴到 `file=` 后面。
222
219
 
223
- ```markdown
224
- # UI 原型索引
220
+ ### 📂 多原型文件管理指南
225
221
 
226
- | 编号 | 名称 | 👁️ 预览 | 📄 数据 | 确认时间 |
227
- |------|------|--------|--------|---------|
228
- | REQ-001 | 登录页 | [.html](./REQ-001-登录页.html) | [.json](./REQ-001-登录页.json) | 2025-12-28 |
229
- ```
222
+ 一个项目中通常会有多个需求点,建议按以下方式管理:
223
+
224
+ 1. **命名规范**:`REQ-{编号}-{名称}.html` (如 `REQ-003-用户反馈.html`)
225
+ 2. **索引文件**:务必更新 `index.md`,提供所有原型的入口列表
226
+ 3. **版本控制**:如果需求变更,直接覆盖旧文件,或另存为 `_v2.html`
230
227
 
231
228
  ---
232
229
 
@@ -399,7 +399,8 @@
399
399
 
400
400
  async function checkUpdate() {
401
401
  try {
402
- const res = await fetch('/ui.json');
402
+ // 获取数据
403
+ const res = await fetch('/ui.json' + window.location.search);
403
404
  if (res.ok) {
404
405
  const data = await res.json();
405
406
  const dataStr = JSON.stringify(data);
@@ -412,6 +413,21 @@
412
413
 
413
414
  document.getElementById('status-text').textContent = '已连接';
414
415
  document.querySelector('.status-dot').style.background = '#52c41a';
416
+
417
+ // 如果有 file 参数,显示当前文件
418
+ const params = new URLSearchParams(window.location.search);
419
+ if (params.get('file')) {
420
+ document.title = `预览: ${params.get('file')} - PRD A2UI`;
421
+ // 可以在页面上也显示一下
422
+ const statusBar = document.querySelector('.status-bar');
423
+ if (!document.getElementById('status-file-info')) {
424
+ const fileInfo = document.createElement('span');
425
+ fileInfo.id = 'status-file-info';
426
+ fileInfo.style.marginLeft = '10px';
427
+ fileInfo.textContent = `| 文件: ${params.get('file')}`;
428
+ statusBar.appendChild(fileInfo);
429
+ }
430
+ }
415
431
  }
416
432
  } catch (e) {
417
433
  document.getElementById('status-text').textContent = '连接失败';
package/bin/prd-cli.js CHANGED
@@ -102,12 +102,12 @@ program
102
102
 
103
103
  // A2UI 预览服务
104
104
  program
105
- .command('ui')
106
- .description('启动 A2UI 界面预览服务')
105
+ .command('ui [file]')
106
+ .description('启动 A2UI 界面预览服务 [可指定 .json 文件]')
107
107
  .option('-p, --port <number>', '指定端口号', '3333')
108
- .action((options) => {
108
+ .action((file, options) => {
109
109
  const A2UIServer = require('../commands/a2ui-server');
110
- const server = new A2UIServer(options.port);
110
+ const server = new A2UIServer(options.port, file);
111
111
  server.start();
112
112
  });
113
113
 
@@ -5,8 +5,9 @@ const chalk = require('chalk');
5
5
  const { exec } = require('child_process');
6
6
 
7
7
  class A2UIServer {
8
- constructor(port = 3333) {
8
+ constructor(port = 3333, targetFile = null) {
9
9
  this.port = port;
10
+ this.targetFile = targetFile;
10
11
  this.viewerPath = path.join(__dirname, '../a2ui-viewer');
11
12
  this.projectPath = process.cwd();
12
13
  }
@@ -18,11 +19,32 @@ class A2UIServer {
18
19
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
19
20
 
20
21
  // 路由处理
21
- if (req.url === '/') {
22
+ const parsedUrl = new URL(req.url, `http://localhost:${this.port}`);
23
+ const pathname = parsedUrl.pathname;
24
+
25
+ if (pathname === '/') {
22
26
  this.serveFile(res, path.join(this.viewerPath, 'index.html'), 'text/html');
23
- } else if (req.url === '/ui.json') {
24
- // 读取项目根目录下的 a2ui-data.json
25
- this.serveFile(res, path.join(this.projectPath, '.a2ui/current.json'), 'application/json');
27
+ } else if (pathname === '/ui.json') {
28
+ // Priority: 1. URL Query Param (?file=...) 2. CLI Argument 3. Default
29
+ const queryFile = parsedUrl.searchParams.get('file');
30
+ let jsonPath = path.join(this.projectPath, '.a2ui/current.json');
31
+
32
+ if (queryFile) {
33
+ // Prevent directory traversal attacks (basic check)
34
+ // Allow relative paths from project root
35
+ jsonPath = path.resolve(this.projectPath, queryFile);
36
+ if (!jsonPath.startsWith(this.projectPath)) {
37
+ res.writeHead(403);
38
+ res.end('Access denied: File outside project directory');
39
+ return;
40
+ }
41
+ } else if (this.targetFile) {
42
+ jsonPath = path.isAbsolute(this.targetFile)
43
+ ? this.targetFile
44
+ : path.join(this.projectPath, this.targetFile);
45
+ }
46
+
47
+ this.serveFile(res, jsonPath, 'application/json');
26
48
  } else {
27
49
  res.writeHead(404);
28
50
  res.end('Not found');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prd-workflow-cli",
3
- "version": "1.3.0",
3
+ "version": "1.3.4",
4
4
  "description": "产品需求管理规范 CLI 工具 - 基于 A→R→B→C 流程,集成 PM 确认机制和对话归档的需求管理命令行工具",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,127 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>{{TITLE}} - PRD UI 原型</title>
8
+ <!-- React -->
9
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js" crossorigin></script>
10
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
11
+ <!-- Ant Design -->
12
+ <link rel="stylesheet" href="https://unpkg.com/antd@5/dist/reset.css">
13
+ <script src="https://unpkg.com/dayjs@1/dayjs.min.js"></script>
14
+ <script src="https://unpkg.com/antd@5/dist/antd.min.js"></script>
15
+ <!-- Icons -->
16
+ <script src="https://unpkg.com/@ant-design/icons@5/dist/index.umd.min.js"></script>
17
+ <style>
18
+ body {
19
+ margin: 0;
20
+ padding: 24px;
21
+ background: #f5f5f5;
22
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
23
+ }
24
+
25
+ #root {
26
+ max-width: 1400px;
27
+ margin: 0 auto;
28
+ }
29
+
30
+ .page-header {
31
+ margin-bottom: 16px;
32
+ }
33
+
34
+ .page-title {
35
+ font-size: 20px;
36
+ font-weight: 600;
37
+ color: rgba(0, 0, 0, 0.88);
38
+ margin: 0;
39
+ }
40
+
41
+ .meta-info {
42
+ background: #fff;
43
+ border: 1px solid #d9d9d9;
44
+ border-radius: 6px;
45
+ padding: 12px 16px;
46
+ margin-bottom: 16px;
47
+ font-size: 13px;
48
+ color: rgba(0, 0, 0, 0.65);
49
+ }
50
+
51
+ .meta-info span {
52
+ margin-right: 24px;
53
+ }
54
+
55
+ .meta-info strong {
56
+ color: rgba(0, 0, 0, 0.88);
57
+ }
58
+ </style>
59
+ </head>
60
+
61
+ <body>
62
+ <div class="meta-info">
63
+ <span><strong>需求编号:</strong>{{REQ_ID}}</span>
64
+ <span><strong>界面名称:</strong>{{NAME}}</span>
65
+ <span><strong>确认时间:</strong>{{DATE}}</span>
66
+ </div>
67
+ <div id="root"></div>
68
+
69
+ <script>
70
+ // UI 数据(内嵌)
71
+ const UI_DATA = {{JSON_DATA}};
72
+
73
+ // A2UI 渲染器
74
+ const { ConfigProvider, Card, Button, Input, Select, Table, Tabs, Tag, Badge, Space, Row, Col, Typography, Divider, Alert, Upload, Form } = antd;
75
+ const { Title, Text } = Typography;
76
+ const { TextArea } = Input;
77
+ const { PlusOutlined, InboxOutlined } = icons;
78
+ const { Dragger } = Upload;
79
+
80
+ const A2UIRenderer = ({ data }) => {
81
+ const renderNode = (node) => {
82
+ if (!node) return null;
83
+ const { type, children, ...props } = node;
84
+
85
+ switch (type) {
86
+ case 'Page': return React.createElement('div', { key: props.id }, props.title && React.createElement('div', { className: 'page-header' }, React.createElement('h1', { className: 'page-title' }, props.title)), children && children.map((child, i) => renderNode({ ...child, key: i })));
87
+ case 'Panel': return React.createElement(Card, { key: props.key, title: props.title, extra: props.extra && React.createElement(Space, null, props.extra.map((btn, i) => React.createElement(Button, { key: i, type: btn.variant === 'primary' ? 'primary' : 'default' }, btn.text || btn))), style: { marginBottom: 16 } }, children && children.map((child, i) => renderNode({ ...child, key: i })));
88
+ case 'Row': return React.createElement(Row, { key: props.key, gutter: 16 }, children && children.map((child, i) => renderNode({ ...child, key: i })));
89
+ case 'Col': return React.createElement(Col, { key: props.key, flex: 1 }, children && children.map((child, i) => renderNode({ ...child, key: i })));
90
+ case 'Input': return React.createElement(Form.Item, { key: props.key, label: props.label, required: props.required, style: { marginBottom: 16 } }, React.createElement(Input, { placeholder: props.placeholder }));
91
+ case 'Textarea': return React.createElement(Form.Item, { key: props.key, label: props.label, style: { marginBottom: 16 } }, React.createElement(TextArea, { placeholder: props.placeholder, rows: props.rows || 4 }));
92
+ case 'Select': return React.createElement(Form.Item, { key: props.key, label: props.label, style: { marginBottom: 16 } }, React.createElement(Select, { placeholder: '请选择', options: (props.options || []).map(opt => ({ value: typeof opt === 'string' ? opt : opt.value, label: typeof opt === 'string' ? opt : opt.label })), style: { width: '100%' } }));
93
+ case 'Button': return React.createElement(Button, { key: props.key, type: props.variant === 'secondary' ? 'default' : props.variant === 'danger' ? 'primary' : 'primary', danger: props.variant === 'danger', style: { marginRight: 8 } }, props.text);
94
+ case 'Text': return React.createElement(Text, { key: props.key, style: { display: 'block', marginBottom: 8 } }, props.content);
95
+ case 'Tabs': return React.createElement(Tabs, { key: props.key, items: (props.items || []).map((item, i) => ({ key: String(i), label: item })), style: { marginBottom: 16 } });
96
+ case 'Table':
97
+ const columns = (props.columns || []).map(col => {
98
+ const column = { key: col.key || col, dataIndex: col.key || col, title: col.title || col };
99
+ if (col.type === 'link') column.render = (text) => React.createElement('a', null, text);
100
+ else if (col.type === 'badge') column.render = (text) => React.createElement(Tag, { color: col.variantMap?.[text] === 'success' ? 'green' : col.variantMap?.[text] === 'warning' ? 'orange' : col.variantMap?.[text] === 'danger' ? 'red' : 'blue' }, text);
101
+ else if (col.type === 'status') column.render = (text) => React.createElement(Badge, { status: text === '已发布' ? 'success' : 'default', text });
102
+ else if (col.type === 'actions') column.render = () => React.createElement(Space, null, React.createElement('a', null, '编辑'), React.createElement('a', null, '复制'), React.createElement('a', { style: { color: '#ff4d4f' } }, '删除'));
103
+ return column;
104
+ });
105
+ return React.createElement(Table, { key: props.key, columns, dataSource: (props.data || []).map((row, i) => ({ ...row, key: i })), pagination: false, size: 'middle' });
106
+ case 'Badge': return React.createElement(Tag, { key: props.key, color: props.variant === 'success' ? 'green' : props.variant === 'warning' ? 'orange' : props.variant === 'danger' ? 'red' : 'blue' }, props.text);
107
+ case 'Card': return React.createElement(Card, { key: props.key, size: 'small', style: { marginBottom: 12 } }, React.createElement(Row, { justify: 'space-between', align: 'middle' }, React.createElement(Col, null, React.createElement(Space, { direction: 'vertical', size: 0 }, React.createElement(Text, { strong: true }, props.title), props.status && React.createElement(Badge, { status: props.status === '已发布' ? 'success' : 'default', text: props.status }))), props.actions && React.createElement(Col, null, React.createElement(Space, null, props.actions.map((action, i) => React.createElement(Button, { key: i, size: 'small' }, action.text || action))))));
108
+ case 'Upload': return React.createElement(Dragger, { key: props.key }, React.createElement('p', { className: 'ant-upload-drag-icon' }, React.createElement(InboxOutlined)), React.createElement('p', { className: 'ant-upload-text' }, props.text || '点击或拖拽文件上传'));
109
+ case 'Divider': return React.createElement(Divider, { key: props.key });
110
+ case 'Alert': return React.createElement(Alert, { key: props.key, type: props.variant === 'danger' ? 'error' : props.variant || 'info', message: props.content || props.text, showIcon: true, style: { marginBottom: 16 } });
111
+ case 'Diagram': return React.createElement('div', { key: props.key, style: { background: 'linear-gradient(135deg, #1677ff 0%, #722ed1 100%)', borderRadius: 8, padding: 32, minHeight: 300 } }, props.title && React.createElement('div', { style: { color: 'white', fontSize: 18, fontWeight: 600, textAlign: 'center', marginBottom: 24 } }, props.title), React.createElement('div', { style: { display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 16 } }, children && children.map((child, i) => renderNode({ ...child, key: i }))));
112
+ case 'Box': return React.createElement(Card, { key: props.key, size: 'small', style: { minWidth: 120, textAlign: 'center', borderLeft: props.color ? `3px solid ${props.color}` : undefined } }, React.createElement(Text, { strong: true }, props.title), props.desc && React.createElement('div', null, React.createElement(Text, { type: 'secondary', style: { fontSize: 12 } }, props.desc)));
113
+ case 'Arrow': return React.createElement('div', { key: props.key, style: { color: 'white', fontSize: 24, textAlign: 'center' } }, (props.direction === 'up' ? '↑' : props.direction === 'left' ? '←' : props.direction === 'right' ? '→' : '↓'), props.label && React.createElement('span', { style: { fontSize: 12, marginLeft: 8 } }, props.label));
114
+ case 'Layer': return React.createElement('div', { key: props.key, style: { display: 'flex', flexWrap: 'wrap', gap: 12, justifyContent: 'center', width: '100%' } }, props.title && React.createElement('div', { style: { width: '100%', textAlign: 'center', color: 'rgba(255,255,255,0.8)', fontSize: 12, marginBottom: 8 } }, props.title), children && children.map((child, i) => renderNode({ ...child, key: i })));
115
+ case 'DiagramGroup': return React.createElement('div', { key: props.key, style: { background: 'rgba(255,255,255,0.1)', border: '1px dashed rgba(255,255,255,0.3)', borderRadius: 8, padding: 16, width: '100%' } }, props.title && React.createElement('div', { style: { color: 'rgba(255,255,255,0.9)', fontSize: 14, marginBottom: 12 } }, props.title), React.createElement('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 12, justifyContent: 'center' } }, children && children.map((child, i) => renderNode({ ...child, key: i })));
116
+ default: return React.createElement(Alert, { key: props.key, type: 'warning', message: `未知组件: ${type}` });
117
+ }
118
+ };
119
+ return React.createElement(ConfigProvider, { theme: { token: { colorPrimary: '#1677ff', borderRadius: 6 } } }, renderNode(data));
120
+ };
121
+
122
+ const root = ReactDOM.createRoot(document.getElementById('root'));
123
+ root.render(React.createElement(A2UIRenderer, { data: UI_DATA }));
124
+ </script>
125
+ </body>
126
+
127
+ </html>