@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.
@@ -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
- - 步骤1:浏览首屏权益。
45
- - 步骤2:点击「立即参与」。
44
+ - 步骤 1:浏览首屏权益。
45
+ - 步骤 2:点击「立即参与」。
46
46
  3. **操作反馈**:
47
- - 成功:已登录跳转任务页;未登录跳转注册(原型 Toast 模拟)。
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
- editMode: false,
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 text
34
- .trim()
35
- .toLowerCase()
36
- .replace(/[\s_]+/g, '-')
37
- .replace(/[^a-z0-9\u4e00-\u9fff-]/g, '')
38
- .replace(/-+/g, '-')
39
- .replace(/^-|-$/g, '') || `page-${Date.now()}`;
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" ${item.sortable ? 'checked' : ''} /> 可排序</label>
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).map(row => {
79
- if (kind === 'filters') {
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
- type: row.querySelector('[data-k=type]').value.trim() || '文本',
83
- limit: row.querySelector('[data-k=limit]').value.trim(),
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
- return {
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 = ($('#slug').value.trim() || slugify(pageName));
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').split(/[,,]/).map(s => s.trim()).filter(Boolean),
112
- rowActions: ($('#rowActions').value || '查看, 编辑, 删除').split(/[,,]/).map(s => s.trim()).filter(Boolean),
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, edit = false) {
166
+ function loadProtoFrame(slug, design = false) {
155
167
  if (!slug) return;
156
- const q = edit ? '?edit=1&' : '?';
157
- $('#protoFrame').src = `/projects/${slug}/prototype.html${q}t=${Date.now()}`;
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.editMode);
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 = btn.dataset.add === 'filters' ? renderFilterRow({}, list.children.length) : renderColumnRow({}, list.children.length);
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, false);
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.editMode);
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', { method: 'POST', body: JSON.stringify(body) });
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
- $('#btnToggleEdit').onclick = () => {
324
- state.editMode = !state.editMode;
325
- $('#btnToggleEdit').textContent = state.editMode ? '关闭 iframe 在线编辑' : '开启 iframe 在线编辑';
326
- if (state.project?.slug) loadProtoFrame(state.project.slug, state.editMode);
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 || e.data.type !== 'deppon-prd-save-overrides') return;
331
- try {
332
- const { slug, overrides } = e.data.payload;
333
- await api(`/api/projects/${slug}/overrides`, { method: 'POST', body: JSON.stringify(overrides) });
334
- toast('iframe 编辑已保存');
335
- await loadProject(slug);
336
- } catch (err) {
337
- toast(err.message);
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="btnToggleEdit" type="button" class="btn-primary">开启 iframe 在线编辑</button>
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">修改后点「应用预览」实时刷新右侧原型;保存后写入磁盘。</p>
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>