@deppon/deppon-prd-mcp 0.1.0

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.
Files changed (48) hide show
  1. package/README.md +83 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +16 -0
  4. package/dist/config.d.ts +8 -0
  5. package/dist/config.js +30 -0
  6. package/dist/core/prd-generator.d.ts +4 -0
  7. package/dist/core/prd-generator.js +133 -0
  8. package/dist/core/project-store.d.ts +23 -0
  9. package/dist/core/project-store.js +77 -0
  10. package/dist/core/prototype-generator.d.ts +2 -0
  11. package/dist/core/prototype-generator.js +272 -0
  12. package/dist/core/types.d.ts +204 -0
  13. package/dist/core/types.js +160 -0
  14. package/dist/http/server.d.ts +2 -0
  15. package/dist/http/server.js +115 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +2 -0
  18. package/dist/mcp/server.d.ts +4 -0
  19. package/dist/mcp/server.js +194 -0
  20. package/package.json +55 -0
  21. package/templates/examples/README.md +17 -0
  22. package/templates/examples/app-shell-navigation/layout-spec.md +54 -0
  23. package/templates/examples/app-shell-navigation/pages/demo-form.html +57 -0
  24. package/templates/examples/app-shell-navigation/pages/demo-home.html +92 -0
  25. package/templates/examples/app-shell-navigation/pages/demo-list.html +47 -0
  26. package/templates/examples/app-shell-navigation/prd.md +164 -0
  27. package/templates/examples/app-shell-navigation/prototype.html +794 -0
  28. package/templates/examples/backend-list/prd.md +97 -0
  29. package/templates/examples/backend-list/prototype.html +378 -0
  30. package/templates/examples/data-dashboard/prd.md +127 -0
  31. package/templates/examples/data-dashboard/prototype.html +281 -0
  32. package/templates/examples/form-edit/prd.md +108 -0
  33. package/templates/examples/form-edit/prototype.html +280 -0
  34. package/templates/examples/form-preview/prd.md +106 -0
  35. package/templates/examples/form-preview/prototype.html +240 -0
  36. package/templates/examples/list-crud/prd.md +151 -0
  37. package/templates/examples/list-crud/prototype.html +1348 -0
  38. package/templates/examples/user-frontend/prd.md +86 -0
  39. package/templates/examples/user-frontend/prototype.html +223 -0
  40. package/templates/template/app-shell-navigation-prd-template.md +180 -0
  41. package/templates/template/backend-form-edit-prd-template.md +116 -0
  42. package/templates/template/backend-form-preview-prd-template.md +112 -0
  43. package/templates/template/backend-list-prd-template.md +120 -0
  44. package/templates/template/data-prd-template.md +194 -0
  45. package/templates/template/user-frontend-prd-template.md +59 -0
  46. package/web/app.js +342 -0
  47. package/web/index.html +106 -0
  48. package/web/styles.css +100 -0
@@ -0,0 +1,272 @@
1
+ import { defaultMockRows } from './types.js';
2
+ function escapeHtml(text) {
3
+ return text
4
+ .replace(/&/g, '&')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;');
8
+ }
9
+ function filterInputHtml(filter, index) {
10
+ const id = `f${index}`;
11
+ if (filter.type.includes('下拉') || filter.type.includes('select')) {
12
+ const opts = (filter.limit.match(/[^/、,,\s]+/g) || ['全部', '启用', '停用'])
13
+ .map(v => v.trim())
14
+ .filter(Boolean);
15
+ const options = opts
16
+ .map((o, i) => `<option value="${i === 0 ? '' : escapeHtml(o)}">${escapeHtml(o)}</option>`)
17
+ .join('');
18
+ return `<label class="block text-xs font-medium text-slate-600">${escapeHtml(filter.name)}
19
+ <select id="${id}" data-filter="${escapeHtml(filter.name)}" class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500">${options}</select>
20
+ </label>`;
21
+ }
22
+ return `<label class="block text-xs font-medium text-slate-600">${escapeHtml(filter.name)}
23
+ <input type="text" id="${id}" data-filter="${escapeHtml(filter.name)}" class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" placeholder="${escapeHtml(filter.matchRule)}" autocomplete="off" />
24
+ </label>`;
25
+ }
26
+ export function generatePrototypeHtml(project) {
27
+ const listTitle = `${project.pageName.replace(/列表|管理/g, '').trim() || project.pageName}列表`;
28
+ const mockRows = project.mockRows.length ? project.mockRows : defaultMockRows(project.columns);
29
+ const configJson = JSON.stringify({ ...project, mockRows }).replace(/</g, '\\u003c');
30
+ const filterHtml = project.filters.map((f, i) => filterInputHtml(f, i)).join('\n ');
31
+ const columnHeaders = project.columns.map(c => `<th class="px-4 py-3">${escapeHtml(c.name)}</th>`).join('\n ');
32
+ const toolbarHtml = project.toolbarButtons
33
+ .map((btn, i) => {
34
+ const primary = i === 0 ? 'bg-blue-600 text-white shadow hover:bg-blue-700' : 'border border-slate-300 bg-white text-slate-700 hover:bg-slate-50';
35
+ const id = i === 0 && project.enableCrud ? ' id="btnAdd"' : '';
36
+ return `<button type="button"${id} class="rounded-lg ${primary} px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2" data-toolbar="${escapeHtml(btn)}">${escapeHtml(btn)}</button>`;
37
+ })
38
+ .join('\n ');
39
+ const rowActionsHtml = project.rowActions
40
+ .map(a => `<button type="button" class="row-action text-blue-600 hover:underline" data-action="${escapeHtml(a)}">${escapeHtml(a)}</button>`)
41
+ .join('\n ');
42
+ return `<!DOCTYPE html>
43
+ <html lang="zh-CN">
44
+ <head>
45
+ <meta charset="UTF-8" />
46
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
47
+ <title>${escapeHtml(project.pageName)} — 原型</title>
48
+ <script src="https://cdn.tailwindcss.com"><\/script>
49
+ <script>tailwind.config={theme:{extend:{fontFamily:{sans:['system-ui','Segoe UI','PingFang SC','sans-serif']}}}};<\/script>
50
+ <style>
51
+ .anno-trigger{display:inline-flex;align-items:center;justify-content:center;border-radius:.375rem;border:1px solid rgba(217,119,6,.85);background:rgba(254,243,199,.95);padding:.15rem .5rem;font-size:.625rem;font-weight:700;letter-spacing:.04em;color:rgb(69 26 3);cursor:pointer;white-space:nowrap}
52
+ .anno-trigger:hover{background:rgb(253 230 138)}
53
+ #editToolbar{position:fixed;top:0;left:0;right:0;z-index:80;display:none;align-items:center;gap:.75rem;border-bottom:1px solid rgb(251 191 36);background:rgb(255 251 235);padding:.5rem 1rem;font-size:.75rem;color:rgb(69 26 3)}
54
+ body.edit-mode #editToolbar{display:flex}
55
+ body.edit-mode [data-editable]{outline:1px dashed rgba(59,130,246,.45);outline-offset:2px;cursor:text;border-radius:.25rem}
56
+ body.edit-mode [data-editable]:focus{outline:2px solid rgb(59 130 246);background:rgba(239,246,255,.6)}
57
+ #annoDlgBody .prd-md table{width:100%;border-collapse:collapse;font-size:.75rem}
58
+ #annoDlgBody .prd-md th,#annoDlgBody .prd-md td{border:1px solid rgb(226 232 240);padding:.35rem .5rem}
59
+ #annoDlgBody .prd-md th{background:rgb(248 250 252);font-weight:600;text-align:left}
60
+ </style>
61
+ </head>
62
+ <body class="min-h-screen bg-slate-100 text-slate-800 antialiased">
63
+ <div id="editToolbar" role="toolbar" aria-label="在线编辑">
64
+ <strong>在线编辑模式</strong>
65
+ <span>点击页面中带虚线框的文字可直接修改;改完后点「保存到项目」</span>
66
+ <button type="button" id="btnSaveEdit" class="rounded bg-blue-600 px-3 py-1.5 text-white hover:bg-blue-700">保存到项目</button>
67
+ <button type="button" id="btnExitEdit" class="rounded border border-slate-300 bg-white px-3 py-1.5 hover:bg-slate-50">退出编辑</button>
68
+ </div>
69
+ <div id="toast" class="fixed top-4 right-4 z-[60] hidden max-w-sm rounded-lg bg-slate-900 px-4 py-3 text-sm text-white shadow-lg"></div>
70
+ <button type="button" id="btnAnnoLegend" class="fixed bottom-5 left-5 z-[65] flex items-center gap-2 rounded-full border border-amber-400 bg-white px-3 py-2 text-xs font-medium text-amber-950 shadow-lg hover:bg-amber-50">? 标注说明</button>
71
+
72
+ <header class="border-b border-slate-200 bg-white">
73
+ <div class="mx-auto flex max-w-6xl items-center px-4 py-4">
74
+ <div class="flex items-start gap-2">
75
+ <div>
76
+ <h1 class="text-lg font-semibold text-slate-900" data-editable="pageName">${escapeHtml(project.pageName)}</h1>
77
+ <p class="mt-0.5 text-xs text-slate-500">原型 · 与 prd.md 对齐 · deppon-prd-mcp</p>
78
+ </div>
79
+ <button type="button" class="anno-trigger mt-0.5" data-anno-key="s1">PRD §1</button>
80
+ </div>
81
+ </div>
82
+ </header>
83
+
84
+ <main class="mx-auto max-w-6xl px-4 py-6">
85
+ <section class="mb-6 rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
86
+ <div class="mb-3 flex flex-wrap items-center gap-2">
87
+ <h2 class="text-sm font-semibold text-slate-900" data-editable="filterTitle">筛选条件</h2>
88
+ <button type="button" class="anno-trigger" data-anno-key="s31">PRD §3.1</button>
89
+ </div>
90
+ <div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4" id="filterGrid">${filterHtml}</div>
91
+ <div class="mt-4 flex flex-wrap gap-2">
92
+ <button type="button" id="btnQuery" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow hover:bg-blue-700">查询</button>
93
+ <button type="button" id="btnReset" class="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">重置</button>
94
+ </div>
95
+ </section>
96
+
97
+ <section class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
98
+ <div class="flex flex-wrap items-center justify-between gap-2 border-b border-slate-100 px-4 py-3">
99
+ <div class="flex items-center gap-2">
100
+ <h2 class="text-sm font-semibold text-slate-900" data-editable="listTitle">${escapeHtml(listTitle)}</h2>
101
+ <button type="button" class="anno-trigger" data-anno-key="s32">PRD §3.2</button>
102
+ </div>
103
+ <div class="flex flex-wrap items-center gap-2" id="toolbar">${toolbarHtml}</div>
104
+ </div>
105
+ <div class="overflow-x-auto">
106
+ <table class="min-w-full divide-y divide-slate-200 text-left text-sm">
107
+ <thead class="bg-slate-50 text-xs font-medium uppercase tracking-wide text-slate-600"><tr>${columnHeaders}<th class="px-4 py-3 text-right">操作</th></tr></thead>
108
+ <tbody id="tbody" class="divide-y divide-slate-100 bg-white"></tbody>
109
+ </table>
110
+ </div>
111
+ <div class="flex flex-wrap items-center justify-between gap-3 border-t border-slate-100 px-4 py-3 text-xs text-slate-600">
112
+ <span id="totalHint"></span>
113
+ <div class="flex items-center gap-2">
114
+ <button type="button" id="btnPrev" class="rounded border border-slate-300 px-3 py-1.5 text-xs font-medium hover:bg-slate-50">上一页</button>
115
+ <span id="pageInfo"></span>
116
+ <button type="button" id="btnNext" class="rounded border border-slate-300 px-3 py-1.5 text-xs font-medium hover:bg-slate-50">下一页</button>
117
+ </div>
118
+ </div>
119
+ </section>
120
+ </main>
121
+
122
+ <div id="modalDetail" class="fixed inset-0 z-40 hidden items-center justify-center bg-black/40 p-4" role="dialog" aria-modal="true">
123
+ <div class="max-h-[90vh] w-full max-w-md overflow-y-auto rounded-xl bg-white p-6 shadow-xl" onclick="event.stopPropagation()">
124
+ <div class="flex items-start justify-between gap-2">
125
+ <h3 class="text-base font-semibold text-slate-900">详情</h3>
126
+ <button type="button" class="anno-trigger" data-anno-key="s4">PRD §4</button>
127
+ </div>
128
+ <dl id="detailBody" class="mt-4 space-y-3 text-sm"></dl>
129
+ <div class="mt-6 flex justify-end"><button type="button" id="btnCloseDetail" class="rounded-lg border border-slate-300 px-4 py-2 text-sm">关闭</button></div>
130
+ </div>
131
+ </div>
132
+
133
+ <div id="modalForm" class="fixed inset-0 z-40 hidden items-center justify-center bg-black/40 p-4" role="dialog" aria-modal="true">
134
+ <div class="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-xl bg-white p-6 shadow-xl" onclick="event.stopPropagation()">
135
+ <div class="flex items-start justify-between gap-2">
136
+ <h3 id="formTitle" class="text-base font-semibold text-slate-900">${escapeHtml(project.createButtonLabel || '新建')}</h3>
137
+ <button type="button" class="anno-trigger" data-anno-key="s5">PRD §5</button>
138
+ </div>
139
+ <form id="entityForm" class="mt-4 space-y-4"></form>
140
+ <div class="mt-6 flex justify-end gap-2">
141
+ <button type="button" id="btnCancelForm" class="rounded-lg border border-slate-300 px-4 py-2 text-sm">取消</button>
142
+ <button type="submit" form="entityForm" class="rounded-lg bg-blue-600 px-4 py-2 text-sm text-white">保存</button>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <div id="modalDelete" class="fixed inset-0 z-40 hidden items-center justify-center bg-black/40 p-4" role="dialog" aria-modal="true">
148
+ <div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-xl" onclick="event.stopPropagation()">
149
+ <div class="flex items-start justify-between gap-2">
150
+ <h3 class="text-base font-semibold text-slate-900">确认删除</h3>
151
+ <button type="button" class="anno-trigger" data-anno-key="s6">PRD §6</button>
152
+ </div>
153
+ <p class="mt-3 text-sm text-slate-600">确定删除该条记录?此操作为原型 mock。</p>
154
+ <div class="mt-6 flex justify-end gap-2">
155
+ <button type="button" id="btnCancelDelete" class="rounded-lg border border-slate-300 px-4 py-2 text-sm">取消</button>
156
+ <button type="button" id="btnConfirmDelete" class="rounded-lg bg-red-600 px-4 py-2 text-sm text-white">删除</button>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <div id="modalAnno" class="fixed inset-0 z-50 hidden items-center justify-center bg-black/50 p-4" role="dialog" aria-modal="true">
162
+ <div class="flex max-h-[85vh] w-full max-w-2xl flex-col rounded-xl bg-white shadow-2xl">
163
+ <div class="flex items-center justify-between border-b border-slate-100 px-5 py-3">
164
+ <h3 id="annoTitle" class="text-sm font-semibold text-slate-900">PRD 标注</h3>
165
+ <button type="button" id="btnCloseAnno" class="rounded p-1 text-slate-500 hover:bg-slate-100">✕</button>
166
+ </div>
167
+ <div id="annoDlgBody" class="overflow-auto px-5 py-4 text-sm text-slate-700"><div class="prd-md">加载中…</div></div>
168
+ </div>
169
+ </div>
170
+
171
+ <script id="deppon-config" type="application/json">${configJson}</script>
172
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"><\/script>
173
+ <script>
174
+ (function(){
175
+ const CFG = JSON.parse(document.getElementById('deppon-config').textContent);
176
+ let rows = CFG.mockRows.slice();
177
+ let page = 1;
178
+ const pageSize = 10;
179
+ let deleteIndex = -1;
180
+ const $ = (s, r=document) => r.querySelector(s);
181
+ const $$ = (s, r=document) => [...r.querySelectorAll(s)];
182
+
183
+ function toast(msg){ const t=$('#toast'); t.textContent=msg; t.classList.remove('hidden'); setTimeout(()=>t.classList.add('hidden'),2400); }
184
+ function showModal(el){ el.classList.remove('hidden'); el.classList.add('flex'); }
185
+ function hideModal(el){ el.classList.add('hidden'); el.classList.remove('flex'); }
186
+
187
+ function renderTable(){
188
+ const start=(page-1)*pageSize;
189
+ const slice=rows.slice(start,start+pageSize);
190
+ const cols=CFG.columns.map(c=>c.name);
191
+ $('#tbody').innerHTML = slice.map((row,i)=> {
192
+ const abs=start+i;
193
+ const cells=cols.map(c=>'<td class="px-4 py-3">'+ (row[c]??'—') +'</td>').join('');
194
+ const actions=${JSON.stringify(project.rowActions)}.map(a=>'<button type="button" class="row-action ml-2 text-blue-600 hover:underline" data-action="'+a+'" data-index="'+abs+'">'+a+'</button>').join('');
195
+ return '<tr class="hover:bg-slate-50">'+cells+'<td class="px-4 py-3 text-right whitespace-nowrap">'+actions+'</td></tr>';
196
+ }).join('');
197
+ $('#totalHint').textContent='共 '+rows.length+' 条';
198
+ const pages=Math.max(1,Math.ceil(rows.length/pageSize));
199
+ $('#pageInfo').textContent=page+' / '+pages;
200
+ $('#btnPrev').disabled=page<=1;
201
+ $('#btnNext').disabled=page>=pages;
202
+ }
203
+
204
+ function buildForm(row){
205
+ const fields=CFG.columns.slice(0,4);
206
+ $('#entityForm').innerHTML = fields.map(f=>'<div><label class="text-xs font-medium text-slate-600">'+f.name+'<input name="'+f.name+'" class="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" value="'+(row&&row[f.name]?String(row[f.name]).replace(/"/g,'&quot;'):'')+'" /></label></div>').join('');
207
+ }
208
+
209
+ $('#btnQuery').onclick=()=>{ page=1; toast('已按筛选条件查询(mock)'); renderTable(); };
210
+ $('#btnReset').onclick=()=>{ $$('#filterGrid input,#filterGrid select').forEach(el=>{ if(el.tagName==='SELECT') el.selectedIndex=0; else el.value=''; }); page=1; toast('已重置'); renderTable(); };
211
+ $('#btnPrev').onclick=()=>{ if(page>1){ page--; renderTable(); } };
212
+ $('#btnNext').onclick=()=>{ const pages=Math.max(1,Math.ceil(rows.length/pageSize)); if(page<pages){ page++; renderTable(); } };
213
+
214
+ const btnAdd=$('#btnAdd');
215
+ if(btnAdd){ btnAdd.onclick=()=>{ $('#formTitle').textContent=CFG.createButtonLabel||'新建'; buildForm(null); showModal($('#modalForm')); }; }
216
+
217
+ $('#btnCancelForm').onclick=()=>hideModal($('#modalForm'));
218
+ $('#entityForm').onsubmit=e=>{ e.preventDefault(); hideModal($('#modalForm')); toast('保存成功(mock)'); };
219
+ $('#btnCloseDetail').onclick=()=>hideModal($('#modalDetail'));
220
+ $('#btnCancelDelete').onclick=()=>hideModal($('#modalDelete'));
221
+ $('#btnConfirmDelete').onclick=()=>{ if(deleteIndex>=0){ rows.splice(deleteIndex,1); deleteIndex=-1; renderTable(); toast('已删除(mock)'); } hideModal($('#modalDelete')); };
222
+
223
+ $('#tbody').onclick=e=>{
224
+ const btn=e.target.closest('.row-action'); if(!btn) return;
225
+ const idx=Number(btn.dataset.index); const action=btn.dataset.action; const row=rows[idx];
226
+ if(action==='查看'){ $('#detailBody').innerHTML=CFG.columns.map(c=>'<div class="flex justify-between gap-4 border-b border-slate-100 pb-2"><dt class="text-slate-500">'+c.name+'</dt><dd class="font-medium text-slate-900">'+(row[c]??'—')+'</dd></div>').join(''); showModal($('#modalDetail')); }
227
+ else if(action==='编辑'){ $('#formTitle').textContent='编辑'; buildForm(row); showModal($('#modalForm')); }
228
+ else if(action==='删除'){ deleteIndex=idx; showModal($('#modalDelete')); }
229
+ else toast(action+'(mock)');
230
+ };
231
+
232
+ $$('[data-toolbar]').forEach(btn=>{ if(btn.id==='btnAdd') return; btn.onclick=()=>toast(btn.dataset.toolbar+'(mock)'); });
233
+
234
+ const ANNO_KEYS={ s1:'## 1.', s31:'### 3.1', s32:'### 3.2', s4:'## 4.', s5:'## 5.', s6:'## 6.' };
235
+ let prdCache='';
236
+ async function loadPrd(){ try{ const r=await fetch('./prd.md'); prdCache=await r.text(); }catch{ prdCache=''; } }
237
+ function slicePrd(key){ if(!prdCache) return '<p>无法加载 prd.md,请用 HTTP 打开。</p>'; const marker=ANNO_KEYS[key]; if(!marker) return '<p>无对应章节</p>'; const i=prdCache.indexOf(marker); if(i<0) return '<p>未找到 '+marker+'</p>'; const rest=prdCache.slice(i); const next=rest.search(/\\n## /); const chunk=next>0?rest.slice(0,next):rest; return marked.parse(chunk); }
238
+ $$('.anno-trigger').forEach(btn=>{ btn.onclick=async()=>{ if(!prdCache) await loadPrd(); $('#annoTitle').textContent='PRD 标注 · '+btn.textContent.trim(); $('.prd-md',$('#annoDlgBody')).innerHTML=slicePrd(btn.dataset.annoKey); showModal($('#modalAnno')); }; });
239
+ $('#btnCloseAnno').onclick=()=>hideModal($('#modalAnno'));
240
+ $('#btnAnnoLegend').onclick=async()=>{ if(!prdCache) await loadPrd(); $('#annoTitle').textContent='需求标注说明'; $('.prd-md',$('#annoDlgBody')).innerHTML='<p>点击页面上的 <strong>PRD §x</strong> 按钮可查看对应 prd.md 章节。改 PRD 后刷新页面即可联动。</p>'; showModal($('#modalAnno')); };
241
+ [$('#modalDetail'),$('#modalForm'),$('#modalDelete'),$('#modalAnno')].forEach(m=>{ m.onclick=e=>{ if(e.target===m) hideModal(m); }; });
242
+
243
+ const params=new URLSearchParams(location.search);
244
+ const editMode=params.get('edit')==='1';
245
+ if(editMode){
246
+ document.body.classList.add('edit-mode');
247
+ $$('[data-editable]').forEach(el=>{ el.contentEditable='true'; el.spellcheck=false; });
248
+ $('#btnSaveEdit').onclick=()=>{
249
+ const payload={ slug:CFG.slug, overrides:{
250
+ pageName: $('[data-editable=pageName]').textContent.trim(),
251
+ listTitle: $('[data-editable=listTitle]').textContent.trim(),
252
+ filterTitle: $('[data-editable=filterTitle]').textContent.trim(),
253
+ }};
254
+ window.parent.postMessage({ type:'deppon-prd-save-overrides', payload }, '*');
255
+ toast('已发送保存请求');
256
+ };
257
+ $('#btnExitEdit').onclick=()=>{ location.search=''; };
258
+ }
259
+ window.addEventListener('message',e=>{
260
+ if(e.data&&e.data.type==='deppon-prd-apply-config'){
261
+ Object.assign(CFG,e.data.config||{});
262
+ if(e.data.config&&e.data.config.pageName) $('[data-editable=pageName]').textContent=e.data.config.pageName;
263
+ if(e.data.config&&e.data.config.mockRows) { rows=e.data.config.mockRows.slice(); renderTable(); }
264
+ }
265
+ });
266
+
267
+ renderTable();
268
+ })();
269
+ <\/script>
270
+ </body>
271
+ </html>`;
272
+ }
@@ -0,0 +1,204 @@
1
+ import { z } from 'zod';
2
+ export declare const PageTypeSchema: z.ZodEnum<["list-crud", "backend-list", "form-edit", "form-preview", "data-dashboard", "user-frontend"]>;
3
+ export type PageType = z.infer<typeof PageTypeSchema>;
4
+ export declare const FilterFieldSchema: z.ZodObject<{
5
+ name: z.ZodString;
6
+ type: z.ZodDefault<z.ZodString>;
7
+ limit: z.ZodDefault<z.ZodString>;
8
+ matchRule: z.ZodDefault<z.ZodString>;
9
+ }, "strip", z.ZodTypeAny, {
10
+ type: string;
11
+ name: string;
12
+ limit: string;
13
+ matchRule: string;
14
+ }, {
15
+ name: string;
16
+ type?: string | undefined;
17
+ limit?: string | undefined;
18
+ matchRule?: string | undefined;
19
+ }>;
20
+ export declare const ColumnFieldSchema: z.ZodObject<{
21
+ name: z.ZodString;
22
+ description: z.ZodDefault<z.ZodString>;
23
+ sortable: z.ZodDefault<z.ZodBoolean>;
24
+ }, "strip", z.ZodTypeAny, {
25
+ name: string;
26
+ description: string;
27
+ sortable: boolean;
28
+ }, {
29
+ name: string;
30
+ description?: string | undefined;
31
+ sortable?: boolean | undefined;
32
+ }>;
33
+ export declare const FeatureRowSchema: z.ZodObject<{
34
+ module: z.ZodString;
35
+ feature: z.ZodString;
36
+ description: z.ZodDefault<z.ZodString>;
37
+ }, "strip", z.ZodTypeAny, {
38
+ description: string;
39
+ module: string;
40
+ feature: string;
41
+ }, {
42
+ module: string;
43
+ feature: string;
44
+ description?: string | undefined;
45
+ }>;
46
+ export declare const PermissionRowSchema: z.ZodObject<{
47
+ point: z.ZodString;
48
+ scope: z.ZodDefault<z.ZodString>;
49
+ dataScope: z.ZodDefault<z.ZodString>;
50
+ }, "strip", z.ZodTypeAny, {
51
+ point: string;
52
+ scope: string;
53
+ dataScope: string;
54
+ }, {
55
+ point: string;
56
+ scope?: string | undefined;
57
+ dataScope?: string | undefined;
58
+ }>;
59
+ export declare const PrdProjectSchema: z.ZodObject<{
60
+ slug: z.ZodString;
61
+ pageName: z.ZodString;
62
+ pageType: z.ZodDefault<z.ZodEnum<["list-crud", "backend-list", "form-edit", "form-preview", "data-dashboard", "user-frontend"]>>;
63
+ batchDate: z.ZodOptional<z.ZodString>;
64
+ useDpItTitle: z.ZodDefault<z.ZodBoolean>;
65
+ background: z.ZodString;
66
+ features: z.ZodDefault<z.ZodArray<z.ZodObject<{
67
+ module: z.ZodString;
68
+ feature: z.ZodString;
69
+ description: z.ZodDefault<z.ZodString>;
70
+ }, "strip", z.ZodTypeAny, {
71
+ description: string;
72
+ module: string;
73
+ feature: string;
74
+ }, {
75
+ module: string;
76
+ feature: string;
77
+ description?: string | undefined;
78
+ }>, "many">>;
79
+ menuPath: z.ZodDefault<z.ZodString>;
80
+ permissions: z.ZodDefault<z.ZodArray<z.ZodObject<{
81
+ point: z.ZodString;
82
+ scope: z.ZodDefault<z.ZodString>;
83
+ dataScope: z.ZodDefault<z.ZodString>;
84
+ }, "strip", z.ZodTypeAny, {
85
+ point: string;
86
+ scope: string;
87
+ dataScope: string;
88
+ }, {
89
+ point: string;
90
+ scope?: string | undefined;
91
+ dataScope?: string | undefined;
92
+ }>, "many">>;
93
+ filters: z.ZodDefault<z.ZodArray<z.ZodObject<{
94
+ name: z.ZodString;
95
+ type: z.ZodDefault<z.ZodString>;
96
+ limit: z.ZodDefault<z.ZodString>;
97
+ matchRule: z.ZodDefault<z.ZodString>;
98
+ }, "strip", z.ZodTypeAny, {
99
+ type: string;
100
+ name: string;
101
+ limit: string;
102
+ matchRule: string;
103
+ }, {
104
+ name: string;
105
+ type?: string | undefined;
106
+ limit?: string | undefined;
107
+ matchRule?: string | undefined;
108
+ }>, "many">>;
109
+ columns: z.ZodDefault<z.ZodArray<z.ZodObject<{
110
+ name: z.ZodString;
111
+ description: z.ZodDefault<z.ZodString>;
112
+ sortable: z.ZodDefault<z.ZodBoolean>;
113
+ }, "strip", z.ZodTypeAny, {
114
+ name: string;
115
+ description: string;
116
+ sortable: boolean;
117
+ }, {
118
+ name: string;
119
+ description?: string | undefined;
120
+ sortable?: boolean | undefined;
121
+ }>, "many">>;
122
+ toolbarButtons: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
123
+ rowActions: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
124
+ createButtonLabel: z.ZodOptional<z.ZodString>;
125
+ enableCrud: z.ZodDefault<z.ZodBoolean>;
126
+ mockRows: z.ZodDefault<z.ZodArray<z.ZodRecord<z.ZodString, z.ZodString>, "many">>;
127
+ }, "strip", z.ZodTypeAny, {
128
+ slug: string;
129
+ pageName: string;
130
+ pageType: "list-crud" | "backend-list" | "form-edit" | "form-preview" | "data-dashboard" | "user-frontend";
131
+ useDpItTitle: boolean;
132
+ background: string;
133
+ features: {
134
+ description: string;
135
+ module: string;
136
+ feature: string;
137
+ }[];
138
+ menuPath: string;
139
+ permissions: {
140
+ point: string;
141
+ scope: string;
142
+ dataScope: string;
143
+ }[];
144
+ filters: {
145
+ type: string;
146
+ name: string;
147
+ limit: string;
148
+ matchRule: string;
149
+ }[];
150
+ columns: {
151
+ name: string;
152
+ description: string;
153
+ sortable: boolean;
154
+ }[];
155
+ toolbarButtons: string[];
156
+ rowActions: string[];
157
+ enableCrud: boolean;
158
+ mockRows: Record<string, string>[];
159
+ batchDate?: string | undefined;
160
+ createButtonLabel?: string | undefined;
161
+ }, {
162
+ slug: string;
163
+ pageName: string;
164
+ background: string;
165
+ pageType?: "list-crud" | "backend-list" | "form-edit" | "form-preview" | "data-dashboard" | "user-frontend" | undefined;
166
+ batchDate?: string | undefined;
167
+ useDpItTitle?: boolean | undefined;
168
+ features?: {
169
+ module: string;
170
+ feature: string;
171
+ description?: string | undefined;
172
+ }[] | undefined;
173
+ menuPath?: string | undefined;
174
+ permissions?: {
175
+ point: string;
176
+ scope?: string | undefined;
177
+ dataScope?: string | undefined;
178
+ }[] | undefined;
179
+ filters?: {
180
+ name: string;
181
+ type?: string | undefined;
182
+ limit?: string | undefined;
183
+ matchRule?: string | undefined;
184
+ }[] | undefined;
185
+ columns?: {
186
+ name: string;
187
+ description?: string | undefined;
188
+ sortable?: boolean | undefined;
189
+ }[] | undefined;
190
+ toolbarButtons?: string[] | undefined;
191
+ rowActions?: string[] | undefined;
192
+ createButtonLabel?: string | undefined;
193
+ enableCrud?: boolean | undefined;
194
+ mockRows?: Record<string, string>[] | undefined;
195
+ }>;
196
+ export type PrdProject = z.infer<typeof PrdProjectSchema>;
197
+ export declare const PAGE_TYPE_LABELS: Record<PageType, string>;
198
+ export declare const TEMPLATE_MAP: Record<PageType, string>;
199
+ export declare const EXAMPLE_MAP: Record<PageType, string>;
200
+ export declare function slugify(text: string): string;
201
+ export declare function formatBatchDate(date?: Date): string;
202
+ export declare function buildDpItTitle(project: PrdProject): string;
203
+ export declare function defaultMockRows(columns: PrdProject['columns']): Array<Record<string, string>>;
204
+ export declare function createDefaultProject(partial: Partial<PrdProject> & Pick<PrdProject, 'pageName'>): PrdProject;
@@ -0,0 +1,160 @@
1
+ import { z } from 'zod';
2
+ export const PageTypeSchema = z.enum([
3
+ 'list-crud',
4
+ 'backend-list',
5
+ 'form-edit',
6
+ 'form-preview',
7
+ 'data-dashboard',
8
+ 'user-frontend',
9
+ ]);
10
+ export const FilterFieldSchema = z.object({
11
+ name: z.string().min(1),
12
+ type: z.string().default('文本'),
13
+ limit: z.string().default(''),
14
+ matchRule: z.string().default('模糊'),
15
+ });
16
+ export const ColumnFieldSchema = z.object({
17
+ name: z.string().min(1),
18
+ description: z.string().default(''),
19
+ sortable: z.boolean().default(false),
20
+ });
21
+ export const FeatureRowSchema = z.object({
22
+ module: z.string().min(1),
23
+ feature: z.string().min(1),
24
+ description: z.string().default(''),
25
+ });
26
+ export const PermissionRowSchema = z.object({
27
+ point: z.string().min(1),
28
+ scope: z.string().default(''),
29
+ dataScope: z.string().default('全量'),
30
+ });
31
+ export const PrdProjectSchema = z.object({
32
+ slug: z.string().min(1),
33
+ pageName: z.string().min(1),
34
+ pageType: PageTypeSchema.default('list-crud'),
35
+ batchDate: z.string().regex(/^\d{8}$/).optional(),
36
+ useDpItTitle: z.boolean().default(true),
37
+ background: z.string().min(1),
38
+ features: z.array(FeatureRowSchema).default([]),
39
+ menuPath: z.string().default(''),
40
+ permissions: z.array(PermissionRowSchema).default([]),
41
+ filters: z.array(FilterFieldSchema).default([]),
42
+ columns: z.array(ColumnFieldSchema).default([]),
43
+ toolbarButtons: z.array(z.string()).default(['新建', '导出 CSV']),
44
+ rowActions: z.array(z.string()).default(['查看', '编辑', '删除']),
45
+ createButtonLabel: z.string().optional(),
46
+ enableCrud: z.boolean().default(true),
47
+ mockRows: z
48
+ .array(z.record(z.string()))
49
+ .default([]),
50
+ });
51
+ export const PAGE_TYPE_LABELS = {
52
+ 'list-crud': '后台列表 + CRUD',
53
+ 'backend-list': '后台纯列表',
54
+ 'form-edit': '后台表单编辑',
55
+ 'form-preview': '后台表单预览',
56
+ 'data-dashboard': '数据看板',
57
+ 'user-frontend': '用户前台',
58
+ };
59
+ export const TEMPLATE_MAP = {
60
+ 'list-crud': 'backend-list-prd-template.md',
61
+ 'backend-list': 'backend-list-prd-template.md',
62
+ 'form-edit': 'backend-form-edit-prd-template.md',
63
+ 'form-preview': 'backend-form-preview-prd-template.md',
64
+ 'data-dashboard': 'data-prd-template.md',
65
+ 'user-frontend': 'user-frontend-prd-template.md',
66
+ };
67
+ export const EXAMPLE_MAP = {
68
+ 'list-crud': 'list-crud',
69
+ 'backend-list': 'backend-list',
70
+ 'form-edit': 'form-edit',
71
+ 'form-preview': 'form-preview',
72
+ 'data-dashboard': 'data-dashboard',
73
+ 'user-frontend': 'user-frontend',
74
+ };
75
+ export function slugify(text) {
76
+ const trimmed = text.trim();
77
+ const ascii = trimmed
78
+ .toLowerCase()
79
+ .replace(/[\s_]+/g, '-')
80
+ .replace(/[^a-z0-9-]/g, '')
81
+ .replace(/-+/g, '-')
82
+ .replace(/^-|-$/g, '');
83
+ if (ascii.length >= 2)
84
+ return ascii;
85
+ return `page-${formatBatchDate()}-${Date.now().toString(36).slice(-6)}`;
86
+ }
87
+ export function formatBatchDate(date = new Date()) {
88
+ const y = date.getFullYear();
89
+ const m = String(date.getMonth() + 1).padStart(2, '0');
90
+ const d = String(date.getDate()).padStart(2, '0');
91
+ return `${y}${m}${d}`;
92
+ }
93
+ export function buildDpItTitle(project) {
94
+ const date = project.batchDate || formatBatchDate();
95
+ return `DP-IT-${date}-${project.pageName}-用户需求说明书(PRD)`;
96
+ }
97
+ export function defaultMockRows(columns) {
98
+ const names = columns.map(c => c.name);
99
+ if (names.length === 0) {
100
+ return [
101
+ { 名称: '示例 A', 状态: '启用', 更新时间: '2026-05-01 10:00' },
102
+ { 名称: '示例 B', 状态: '停用', 更新时间: '2026-05-02 11:30' },
103
+ ];
104
+ }
105
+ return [
106
+ Object.fromEntries(names.map((n, i) => [n, i === 0 ? '示例 A' : n.includes('状态') ? '启用' : `值-${i + 1}`])),
107
+ Object.fromEntries(names.map((n, i) => [n, i === 0 ? '示例 B' : n.includes('状态') ? '停用' : `值-${i + 2}`])),
108
+ Object.fromEntries(names.map((n, i) => [n, i === 0 ? '示例 C' : n.includes('状态') ? '启用' : `值-${i + 3}`])),
109
+ ];
110
+ }
111
+ export function createDefaultProject(partial) {
112
+ const pageName = partial.pageName;
113
+ const slug = partial.slug || slugify(pageName);
114
+ const pageType = partial.pageType || 'list-crud';
115
+ const moduleName = pageName.replace(/列表|管理/g, '').trim() || pageName;
116
+ return PrdProjectSchema.parse({
117
+ slug,
118
+ pageName,
119
+ pageType,
120
+ batchDate: partial.batchDate || formatBatchDate(),
121
+ useDpItTitle: partial.useDpItTitle ?? true,
122
+ background: partial.background || `为业务人员提供统一的${pageName}能力:支持筛选、分页浏览与常规维护操作。`,
123
+ features: partial.features?.length
124
+ ? partial.features
125
+ : [
126
+ { module: pageName, feature: '条件查询', description: '多条件筛选;查询 / 重置' },
127
+ { module: pageName, feature: '列表展示', description: '分页表格展示' },
128
+ { module: pageName, feature: '查看详情', description: '行内查看只读弹层' },
129
+ { module: pageName, feature: '新建', description: '表头新建打开表单弹层' },
130
+ { module: pageName, feature: '编辑', description: '行内编辑回填表单' },
131
+ { module: pageName, feature: '删除', description: '单行删除二次确认' },
132
+ ],
133
+ menuPath: partial.menuPath || `业务管理 / ${pageName}`,
134
+ permissions: partial.permissions?.length
135
+ ? partial.permissions
136
+ : [
137
+ { point: '查看', scope: '筛选、分页、详情', dataScope: '全量' },
138
+ { point: '编辑', scope: '新建、编辑、删除', dataScope: '全量' },
139
+ { point: '导出', scope: '导出 CSV', dataScope: '全量' },
140
+ ],
141
+ filters: partial.filters?.length
142
+ ? partial.filters
143
+ : [
144
+ { name: `${moduleName}名称`, type: '文本', limit: '1 ~ 64,trim', matchRule: '模糊' },
145
+ { name: '状态', type: '下拉', limit: '全部 / 启用 / 停用', matchRule: '精准' },
146
+ ],
147
+ columns: partial.columns?.length
148
+ ? partial.columns
149
+ : [
150
+ { name: `${moduleName}名称`, description: '业务名称', sortable: false },
151
+ { name: '状态', description: '启用 / 停用', sortable: true },
152
+ { name: '更新时间', description: 'YYYY-MM-DD HH:mm', sortable: true },
153
+ ],
154
+ toolbarButtons: partial.toolbarButtons || ['新建', '导出 CSV'],
155
+ rowActions: partial.rowActions || ['查看', '编辑', '删除'],
156
+ createButtonLabel: partial.createButtonLabel || `新建${moduleName}`,
157
+ enableCrud: partial.enableCrud ?? pageType === 'list-crud',
158
+ mockRows: partial.mockRows,
159
+ });
160
+ }
@@ -0,0 +1,2 @@
1
+ export declare function startHttpServer(): Promise<void>;
2
+ export declare function startWebOnly(): Promise<void>;