@deppon/deppon-prd-mcp 0.1.1 → 2.5.10
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.
- package/README.md +53 -26
- package/dist/core/canvas-handoff.d.ts +72 -0
- package/dist/core/canvas-handoff.js +238 -0
- package/dist/core/prd-generator.js +3 -9
- package/dist/core/project-store.d.ts +12 -0
- package/dist/core/project-store.js +17 -0
- package/dist/core/prototype-generator.d.ts +1 -1
- package/dist/core/prototype-generator.js +93 -89
- package/dist/core/prototype-layout.d.ts +13 -0
- package/dist/core/prototype-layout.js +219 -0
- package/dist/core/tldraw-wireframe.d.ts +11 -0
- package/dist/core/tldraw-wireframe.js +205 -0
- package/dist/core/types.d.ts +52 -0
- package/dist/core/types.js +13 -4
- package/dist/http/server.js +12 -1
- package/dist/mcp/server.js +59 -2
- package/package.json +6 -3
- package/templates/examples/README.md +8 -8
- package/templates/examples/app-shell-navigation/layout-spec.md +21 -21
- package/templates/examples/app-shell-navigation/prd.md +56 -56
- package/templates/examples/backend-list/prd.md +32 -32
- package/templates/examples/data-dashboard/prd.md +46 -46
- package/templates/examples/form-edit/prd.md +33 -33
- package/templates/examples/form-preview/prd.md +21 -21
- package/templates/examples/user-frontend/prd.md +19 -19
- package/web/app.js +80 -45
- package/web/index.html +2 -2
|
@@ -14,18 +14,18 @@ C 端活动页:向新用户展示权益与参与规则,引导完成注册/
|
|
|
14
14
|
|
|
15
15
|
### 1.2 用户角色
|
|
16
16
|
|
|
17
|
-
| 用户角色 | 描述 | 使用场景
|
|
18
|
-
| -------- | ---------- |
|
|
19
|
-
| 游客 | 未登录访客 | 浏览活动规则
|
|
17
|
+
| 用户角色 | 描述 | 使用场景 |
|
|
18
|
+
| -------- | ---------- | -------------- |
|
|
19
|
+
| 游客 | 未登录访客 | 浏览活动规则 |
|
|
20
20
|
| 新用户 | 首次注册 | 领券、参与活动 |
|
|
21
21
|
|
|
22
22
|
### 1.3 功能清单
|
|
23
23
|
|
|
24
|
-
| 模块 | 功能点 | 功能描述
|
|
25
|
-
| ------ | -------- |
|
|
24
|
+
| 模块 | 功能点 | 功能描述 |
|
|
25
|
+
| ------ | -------- | -------------------- |
|
|
26
26
|
| 活动页 | 首屏展示 | 主标题、权益、主 CTA |
|
|
27
|
-
| 活动页 | 规则说明 | 折叠/锚点查看细则
|
|
28
|
-
| 活动页 | 立即参与 | 跳转注册或唤起登录
|
|
27
|
+
| 活动页 | 规则说明 | 折叠/锚点查看细则 |
|
|
28
|
+
| 活动页 | 立即参与 | 跳转注册或唤起登录 |
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -41,11 +41,11 @@ C 端活动页:向新用户展示权益与参与规则,引导完成注册/
|
|
|
41
41
|
|
|
42
42
|
1. **触发条件**:用户进入 H5/PC 落地页。
|
|
43
43
|
2. **操作步骤**:
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
- 步骤 1:浏览首屏权益。
|
|
45
|
+
- 步骤 2:点击「立即参与」。
|
|
46
46
|
3. **操作反馈**:
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
- 成功:已登录跳转任务页;未登录跳转注册(原型 Toast 模拟)。
|
|
48
|
+
- 失败:网络错误时轻提示。
|
|
49
49
|
|
|
50
50
|
### 2.2 规则与 FAQ
|
|
51
51
|
|
|
@@ -70,17 +70,17 @@ C 端活动页:向新用户展示权益与参与规则,引导完成注册/
|
|
|
70
70
|
## 9. 原型与 PRD 对应关系
|
|
71
71
|
|
|
72
72
|
| PRD 章节 | prototype.html 演示点 |
|
|
73
|
-
| -------- |
|
|
74
|
-
| §1 | 页内「PRD §1」
|
|
75
|
-
| §2.1 | 首屏区「PRD §2.1」
|
|
76
|
-
| §2.2 | 规则折叠「PRD §2.2」
|
|
77
|
-
| §3 | 全局反馈说明入口
|
|
78
|
-
| 走查说明 | 左下角「标注说明」
|
|
73
|
+
| -------- | --------------------- |
|
|
74
|
+
| §1 | 页内「PRD §1」 |
|
|
75
|
+
| §2.1 | 首屏区「PRD §2.1」 |
|
|
76
|
+
| §2.2 | 规则折叠「PRD §2.2」 |
|
|
77
|
+
| §3 | 全局反馈说明入口 |
|
|
78
|
+
| 走查说明 | 左下角「标注说明」 |
|
|
79
79
|
|
|
80
80
|
---
|
|
81
81
|
|
|
82
82
|
## 10. 修订记录
|
|
83
83
|
|
|
84
|
-
| 版本 | 日期 | 说明
|
|
85
|
-
| ---- | ---------- |
|
|
84
|
+
| 版本 | 日期 | 说明 |
|
|
85
|
+
| ---- | ---------- | --------------------------------- |
|
|
86
86
|
| v0.1 | 2026-05-14 | 初版:对齐 user-frontend 模板示例 |
|
package/web/app.js
CHANGED
|
@@ -4,7 +4,7 @@ const $$ = (s, r = document) => [...r.querySelectorAll(s)];
|
|
|
4
4
|
const state = {
|
|
5
5
|
pageTypes: {},
|
|
6
6
|
project: null,
|
|
7
|
-
|
|
7
|
+
designMode: false,
|
|
8
8
|
};
|
|
9
9
|
|
|
10
10
|
function toast(msg) {
|
|
@@ -30,13 +30,15 @@ function todayBatch() {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
function slugify(text) {
|
|
33
|
-
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
return (
|
|
34
|
+
text
|
|
35
|
+
.trim()
|
|
36
|
+
.toLowerCase()
|
|
37
|
+
.replace(/[\s_]+/g, '-')
|
|
38
|
+
.replace(/[^a-z0-9\u4e00-\u9fff-]/g, '')
|
|
39
|
+
.replace(/-+/g, '-')
|
|
40
|
+
.replace(/^-|-$/g, '') || `page-${Date.now()}`
|
|
41
|
+
);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
function renderFilterRow(item = { name: '', type: '文本', limit: '', matchRule: '模糊' }, index) {
|
|
@@ -60,7 +62,9 @@ function renderColumnRow(item = { name: '', description: '', sortable: false },
|
|
|
60
62
|
div.innerHTML = `
|
|
61
63
|
<input data-k="name" placeholder="列名" value="${esc(item.name)}" />
|
|
62
64
|
<input data-k="description" placeholder="说明" value="${esc(item.description)}" />
|
|
63
|
-
<label class="inline-flex items-center gap-1"><input data-k="sortable" type="checkbox" ${
|
|
65
|
+
<label class="inline-flex items-center gap-1"><input data-k="sortable" type="checkbox" ${
|
|
66
|
+
item.sortable ? 'checked' : ''
|
|
67
|
+
} /> 可排序</label>
|
|
64
68
|
<span></span>
|
|
65
69
|
<button type="button" data-remove>删除</button>`;
|
|
66
70
|
div.querySelector('[data-remove]').onclick = () => div.remove();
|
|
@@ -75,27 +79,29 @@ function esc(s) {
|
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
function readList(container, kind) {
|
|
78
|
-
return $$(`.row-card`, container)
|
|
79
|
-
|
|
82
|
+
return $$(`.row-card`, container)
|
|
83
|
+
.map(row => {
|
|
84
|
+
if (kind === 'filters') {
|
|
85
|
+
return {
|
|
86
|
+
name: row.querySelector('[data-k=name]').value.trim(),
|
|
87
|
+
type: row.querySelector('[data-k=type]').value.trim() || '文本',
|
|
88
|
+
limit: row.querySelector('[data-k=limit]').value.trim(),
|
|
89
|
+
matchRule: row.querySelector('[data-k=matchRule]').value.trim() || '模糊',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
80
92
|
return {
|
|
81
93
|
name: row.querySelector('[data-k=name]').value.trim(),
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
matchRule: row.querySelector('[data-k=matchRule]').value.trim() || '模糊',
|
|
94
|
+
description: row.querySelector('[data-k=description]').value.trim(),
|
|
95
|
+
sortable: row.querySelector('[data-k=sortable]').checked,
|
|
85
96
|
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
name: row.querySelector('[data-k=name]').value.trim(),
|
|
89
|
-
description: row.querySelector('[data-k=description]').value.trim(),
|
|
90
|
-
sortable: row.querySelector('[data-k=sortable]').checked,
|
|
91
|
-
};
|
|
92
|
-
}).filter(x => x.name);
|
|
97
|
+
})
|
|
98
|
+
.filter(x => x.name);
|
|
93
99
|
}
|
|
94
100
|
|
|
95
101
|
function collectForm() {
|
|
96
102
|
const pageName = $('#pageName').value.trim();
|
|
97
103
|
if (!pageName) throw new Error('请填写页面名称');
|
|
98
|
-
const slug =
|
|
104
|
+
const slug = $('#slug').value.trim() || slugify(pageName);
|
|
99
105
|
return {
|
|
100
106
|
slug,
|
|
101
107
|
pageName,
|
|
@@ -108,8 +114,14 @@ function collectForm() {
|
|
|
108
114
|
filters: readList($('#filtersList'), 'filters'),
|
|
109
115
|
columns: readList($('#columnsList'), 'columns'),
|
|
110
116
|
createButtonLabel: $('#createButtonLabel').value.trim() || undefined,
|
|
111
|
-
toolbarButtons: ($('#toolbarButtons').value || '新建, 导出 CSV')
|
|
112
|
-
|
|
117
|
+
toolbarButtons: ($('#toolbarButtons').value || '新建, 导出 CSV')
|
|
118
|
+
.split(/[,,]/)
|
|
119
|
+
.map(s => s.trim())
|
|
120
|
+
.filter(Boolean),
|
|
121
|
+
rowActions: ($('#rowActions').value || '查看, 编辑, 删除')
|
|
122
|
+
.split(/[,,]/)
|
|
123
|
+
.map(s => s.trim())
|
|
124
|
+
.filter(Boolean),
|
|
113
125
|
features: [],
|
|
114
126
|
permissions: [],
|
|
115
127
|
mockRows: state.project?.mockRows || [],
|
|
@@ -151,10 +163,10 @@ function switchTab(name) {
|
|
|
151
163
|
$$('.panel').forEach(p => p.classList.toggle('hidden', p.id !== `panel-${name}`));
|
|
152
164
|
}
|
|
153
165
|
|
|
154
|
-
function loadProtoFrame(slug,
|
|
166
|
+
function loadProtoFrame(slug, design = false) {
|
|
155
167
|
if (!slug) return;
|
|
156
|
-
const q =
|
|
157
|
-
$('#protoFrame').src = `/projects/${slug}/prototype.html${q}
|
|
168
|
+
const q = design ? `?design=1&t=${Date.now()}` : `?t=${Date.now()}`;
|
|
169
|
+
$('#protoFrame').src = `/projects/${slug}/prototype.html${q}`;
|
|
158
170
|
}
|
|
159
171
|
|
|
160
172
|
async function loadProjects() {
|
|
@@ -175,7 +187,7 @@ async function loadProject(slug) {
|
|
|
175
187
|
state.project = data.config;
|
|
176
188
|
fillForm(data.config);
|
|
177
189
|
$('#prdEditor').value = data.prd;
|
|
178
|
-
loadProtoFrame(slug, state.
|
|
190
|
+
loadProtoFrame(slug, state.designMode);
|
|
179
191
|
const url = new URL(location.href);
|
|
180
192
|
url.searchParams.set('slug', slug);
|
|
181
193
|
history.replaceState({}, '', url);
|
|
@@ -220,7 +232,7 @@ async function init() {
|
|
|
220
232
|
});
|
|
221
233
|
}
|
|
222
234
|
|
|
223
|
-
$$('.tab').forEach(btn => btn.onclick = () => switchTab(btn.dataset.tab));
|
|
235
|
+
$$('.tab').forEach(btn => (btn.onclick = () => switchTab(btn.dataset.tab)));
|
|
224
236
|
|
|
225
237
|
$('#pageName').addEventListener('blur', () => {
|
|
226
238
|
if (!$('#slug').value.trim() && $('#pageName').value.trim()) {
|
|
@@ -231,7 +243,10 @@ async function init() {
|
|
|
231
243
|
$$('[data-add]').forEach(btn => {
|
|
232
244
|
btn.onclick = () => {
|
|
233
245
|
const list = btn.dataset.add === 'filters' ? $('#filtersList') : $('#columnsList');
|
|
234
|
-
const row =
|
|
246
|
+
const row =
|
|
247
|
+
btn.dataset.add === 'filters'
|
|
248
|
+
? renderFilterRow({}, list.children.length)
|
|
249
|
+
: renderColumnRow({}, list.children.length);
|
|
235
250
|
list.appendChild(row);
|
|
236
251
|
};
|
|
237
252
|
});
|
|
@@ -264,7 +279,7 @@ async function init() {
|
|
|
264
279
|
const res = await saveProject();
|
|
265
280
|
const files = await api(`/api/projects/${res.slug}`);
|
|
266
281
|
$('#prdEditor').value = files.prd;
|
|
267
|
-
loadProtoFrame(res.slug,
|
|
282
|
+
loadProtoFrame(res.slug, state.designMode);
|
|
268
283
|
toast(`已生成:${res.slug}`);
|
|
269
284
|
switchTab('proto');
|
|
270
285
|
} catch (e) {
|
|
@@ -292,7 +307,7 @@ async function init() {
|
|
|
292
307
|
$('#btnRefreshProto').onclick = async () => {
|
|
293
308
|
try {
|
|
294
309
|
const res = await saveProject();
|
|
295
|
-
loadProtoFrame(res.slug, state.
|
|
310
|
+
loadProtoFrame(res.slug, state.designMode);
|
|
296
311
|
toast('原型已重新生成');
|
|
297
312
|
} catch (e) {
|
|
298
313
|
toast(e.message);
|
|
@@ -302,7 +317,10 @@ async function init() {
|
|
|
302
317
|
$('#btnApplyPreview').onclick = async () => {
|
|
303
318
|
try {
|
|
304
319
|
const body = collectForm();
|
|
305
|
-
const { html } = await api('/api/projects/preview-prototype', {
|
|
320
|
+
const { html } = await api('/api/projects/preview-prototype', {
|
|
321
|
+
method: 'POST',
|
|
322
|
+
body: JSON.stringify(body),
|
|
323
|
+
});
|
|
306
324
|
const blob = new Blob([html], { type: 'text/html' });
|
|
307
325
|
$('#protoFrame').src = URL.createObjectURL(blob);
|
|
308
326
|
toast('已应用预览(未保存到磁盘)');
|
|
@@ -320,21 +338,38 @@ async function init() {
|
|
|
320
338
|
}
|
|
321
339
|
};
|
|
322
340
|
|
|
323
|
-
$('#
|
|
324
|
-
state.
|
|
325
|
-
$('#
|
|
326
|
-
if (state.project?.slug) loadProtoFrame(state.project.slug, state.
|
|
341
|
+
$('#btnDesignMode').onclick = () => {
|
|
342
|
+
state.designMode = !state.designMode;
|
|
343
|
+
$('#btnDesignMode').textContent = state.designMode ? '退出设计模式' : '🎨 设计模式(拖拽)';
|
|
344
|
+
if (state.project?.slug) loadProtoFrame(state.project.slug, state.designMode);
|
|
327
345
|
};
|
|
328
346
|
|
|
329
347
|
window.addEventListener('message', async e => {
|
|
330
|
-
if (!e.data
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
348
|
+
if (!e.data) return;
|
|
349
|
+
if (e.data.type === 'deppon-prd-save-design') {
|
|
350
|
+
try {
|
|
351
|
+
const payload = e.data.payload;
|
|
352
|
+
await api(`/api/projects/${payload.slug}/design`, {
|
|
353
|
+
method: 'POST',
|
|
354
|
+
body: JSON.stringify(payload),
|
|
355
|
+
});
|
|
356
|
+
toast('布局已保存,PRD 已同步');
|
|
357
|
+
await loadProject(payload.slug);
|
|
358
|
+
if (state.designMode) loadProtoFrame(payload.slug, true);
|
|
359
|
+
} catch (err) {
|
|
360
|
+
toast(err.message);
|
|
361
|
+
}
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (e.data.type === 'deppon-prd-save-overrides') {
|
|
365
|
+
try {
|
|
366
|
+
const { slug, overrides } = e.data.payload;
|
|
367
|
+
await api(`/api/projects/${slug}/overrides`, { method: 'POST', body: JSON.stringify(overrides) });
|
|
368
|
+
toast('已保存');
|
|
369
|
+
await loadProject(slug);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
toast(err.message);
|
|
372
|
+
}
|
|
338
373
|
}
|
|
339
374
|
});
|
|
340
375
|
}
|
package/web/index.html
CHANGED
|
@@ -80,14 +80,14 @@
|
|
|
80
80
|
<section id="panel-proto" class="panel hidden">
|
|
81
81
|
<div class="mb-3 flex flex-wrap items-center gap-2">
|
|
82
82
|
<button id="btnRefreshProto" type="button" class="btn-secondary">从表单重新生成原型</button>
|
|
83
|
-
<button id="
|
|
83
|
+
<button id="btnDesignMode" type="button" class="btn-primary">🎨 设计模式(拖拽)</button>
|
|
84
84
|
<button id="btnSaveProject" type="button" class="btn-primary">保存整个项目</button>
|
|
85
85
|
<a id="linkProtoFile" href="#" target="_blank" class="btn-secondary">新窗口打开原型</a>
|
|
86
86
|
</div>
|
|
87
87
|
<div class="grid gap-4 lg:grid-cols-5">
|
|
88
88
|
<aside class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm lg:col-span-2">
|
|
89
89
|
<h2 class="mb-3 text-sm font-semibold">动态调整</h2>
|
|
90
|
-
<p class="mb-3 text-xs text-slate-500"
|
|
90
|
+
<p class="mb-3 text-xs text-slate-500">「设计模式」可拖拽区块、筛选项、列表列、工具栏按钮;保存后写入 project.config.json 并同步 PRD。</p>
|
|
91
91
|
<label class="field"><span>新建按钮文案</span><input id="createButtonLabel" type="text" /></label>
|
|
92
92
|
<label class="field"><span>工具栏按钮(逗号分隔)</span><input id="toolbarButtons" type="text" placeholder="新建, 导出 CSV" /></label>
|
|
93
93
|
<label class="field"><span>行操作(逗号分隔)</span><input id="rowActions" type="text" placeholder="查看, 编辑, 删除" /></label>
|