@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.
- package/README.md +83 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +16 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +30 -0
- package/dist/core/prd-generator.d.ts +4 -0
- package/dist/core/prd-generator.js +133 -0
- package/dist/core/project-store.d.ts +23 -0
- package/dist/core/project-store.js +77 -0
- package/dist/core/prototype-generator.d.ts +2 -0
- package/dist/core/prototype-generator.js +272 -0
- package/dist/core/types.d.ts +204 -0
- package/dist/core/types.js +160 -0
- package/dist/http/server.d.ts +2 -0
- package/dist/http/server.js +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.js +194 -0
- package/package.json +55 -0
- package/templates/examples/README.md +17 -0
- package/templates/examples/app-shell-navigation/layout-spec.md +54 -0
- package/templates/examples/app-shell-navigation/pages/demo-form.html +57 -0
- package/templates/examples/app-shell-navigation/pages/demo-home.html +92 -0
- package/templates/examples/app-shell-navigation/pages/demo-list.html +47 -0
- package/templates/examples/app-shell-navigation/prd.md +164 -0
- package/templates/examples/app-shell-navigation/prototype.html +794 -0
- package/templates/examples/backend-list/prd.md +97 -0
- package/templates/examples/backend-list/prototype.html +378 -0
- package/templates/examples/data-dashboard/prd.md +127 -0
- package/templates/examples/data-dashboard/prototype.html +281 -0
- package/templates/examples/form-edit/prd.md +108 -0
- package/templates/examples/form-edit/prototype.html +280 -0
- package/templates/examples/form-preview/prd.md +106 -0
- package/templates/examples/form-preview/prototype.html +240 -0
- package/templates/examples/list-crud/prd.md +151 -0
- package/templates/examples/list-crud/prototype.html +1348 -0
- package/templates/examples/user-frontend/prd.md +86 -0
- package/templates/examples/user-frontend/prototype.html +223 -0
- package/templates/template/app-shell-navigation-prd-template.md +180 -0
- package/templates/template/backend-form-edit-prd-template.md +116 -0
- package/templates/template/backend-form-preview-prd-template.md +112 -0
- package/templates/template/backend-list-prd-template.md +120 -0
- package/templates/template/data-prd-template.md +194 -0
- package/templates/template/user-frontend-prd-template.md +59 -0
- package/web/app.js +342 -0
- package/web/index.html +106 -0
- package/web/styles.css +100 -0
package/web/app.js
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
const $ = (s, r = document) => r.querySelector(s);
|
|
2
|
+
const $$ = (s, r = document) => [...r.querySelectorAll(s)];
|
|
3
|
+
|
|
4
|
+
const state = {
|
|
5
|
+
pageTypes: {},
|
|
6
|
+
project: null,
|
|
7
|
+
editMode: false,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
function toast(msg) {
|
|
11
|
+
const el = $('#toast');
|
|
12
|
+
el.textContent = msg;
|
|
13
|
+
el.classList.remove('hidden');
|
|
14
|
+
setTimeout(() => el.classList.add('hidden'), 2600);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function api(path, options = {}) {
|
|
18
|
+
const res = await fetch(path, {
|
|
19
|
+
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
|
20
|
+
...options,
|
|
21
|
+
});
|
|
22
|
+
const data = await res.json().catch(() => ({}));
|
|
23
|
+
if (!res.ok) throw new Error(data.error || res.statusText);
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function todayBatch() {
|
|
28
|
+
const d = new Date();
|
|
29
|
+
return `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
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()}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function renderFilterRow(item = { name: '', type: '文本', limit: '', matchRule: '模糊' }, index) {
|
|
43
|
+
const div = document.createElement('div');
|
|
44
|
+
div.className = 'row-card';
|
|
45
|
+
div.dataset.index = String(index);
|
|
46
|
+
div.innerHTML = `
|
|
47
|
+
<input data-k="name" placeholder="字段名" value="${esc(item.name)}" />
|
|
48
|
+
<input data-k="type" placeholder="类型" value="${esc(item.type)}" />
|
|
49
|
+
<input data-k="limit" placeholder="输入限制" value="${esc(item.limit)}" />
|
|
50
|
+
<input data-k="matchRule" placeholder="匹配规则" value="${esc(item.matchRule)}" />
|
|
51
|
+
<button type="button" data-remove>删除</button>`;
|
|
52
|
+
div.querySelector('[data-remove]').onclick = () => div.remove();
|
|
53
|
+
return div;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function renderColumnRow(item = { name: '', description: '', sortable: false }, index) {
|
|
57
|
+
const div = document.createElement('div');
|
|
58
|
+
div.className = 'row-card';
|
|
59
|
+
div.dataset.index = String(index);
|
|
60
|
+
div.innerHTML = `
|
|
61
|
+
<input data-k="name" placeholder="列名" value="${esc(item.name)}" />
|
|
62
|
+
<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>
|
|
64
|
+
<span></span>
|
|
65
|
+
<button type="button" data-remove>删除</button>`;
|
|
66
|
+
div.querySelector('[data-remove]').onclick = () => div.remove();
|
|
67
|
+
return div;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function esc(s) {
|
|
71
|
+
return String(s || '')
|
|
72
|
+
.replace(/&/g, '&')
|
|
73
|
+
.replace(/"/g, '"')
|
|
74
|
+
.replace(/</g, '<');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function readList(container, kind) {
|
|
78
|
+
return $$(`.row-card`, container).map(row => {
|
|
79
|
+
if (kind === 'filters') {
|
|
80
|
+
return {
|
|
81
|
+
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() || '模糊',
|
|
85
|
+
};
|
|
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);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function collectForm() {
|
|
96
|
+
const pageName = $('#pageName').value.trim();
|
|
97
|
+
if (!pageName) throw new Error('请填写页面名称');
|
|
98
|
+
const slug = ($('#slug').value.trim() || slugify(pageName));
|
|
99
|
+
return {
|
|
100
|
+
slug,
|
|
101
|
+
pageName,
|
|
102
|
+
pageType: $('#pageType').value,
|
|
103
|
+
batchDate: $('#batchDate').value.trim() || todayBatch(),
|
|
104
|
+
useDpItTitle: $('#useDpItTitle').checked,
|
|
105
|
+
enableCrud: $('#enableCrud').checked,
|
|
106
|
+
background: $('#background').value.trim(),
|
|
107
|
+
menuPath: $('#menuPath').value.trim(),
|
|
108
|
+
filters: readList($('#filtersList'), 'filters'),
|
|
109
|
+
columns: readList($('#columnsList'), 'columns'),
|
|
110
|
+
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),
|
|
113
|
+
features: [],
|
|
114
|
+
permissions: [],
|
|
115
|
+
mockRows: state.project?.mockRows || [],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function fillForm(project) {
|
|
120
|
+
$('#pageName').value = project.pageName || '';
|
|
121
|
+
$('#slug').value = project.slug || '';
|
|
122
|
+
$('#pageType').value = project.pageType || 'list-crud';
|
|
123
|
+
$('#batchDate').value = project.batchDate || todayBatch();
|
|
124
|
+
$('#useDpItTitle').checked = project.useDpItTitle !== false;
|
|
125
|
+
$('#enableCrud').checked = project.enableCrud !== false;
|
|
126
|
+
$('#background').value = project.background || '';
|
|
127
|
+
$('#menuPath').value = project.menuPath || '';
|
|
128
|
+
$('#createButtonLabel').value = project.createButtonLabel || '';
|
|
129
|
+
$('#toolbarButtons').value = (project.toolbarButtons || []).join(', ');
|
|
130
|
+
$('#rowActions').value = (project.rowActions || []).join(', ');
|
|
131
|
+
|
|
132
|
+
$('#filtersList').innerHTML = '';
|
|
133
|
+
(project.filters || []).forEach((f, i) => $('#filtersList').appendChild(renderFilterRow(f, i)));
|
|
134
|
+
if (!(project.filters || []).length) $('#filtersList').appendChild(renderFilterRow({}, 0));
|
|
135
|
+
|
|
136
|
+
$('#columnsList').innerHTML = '';
|
|
137
|
+
(project.columns || []).forEach((c, i) => $('#columnsList').appendChild(renderColumnRow(c, i)));
|
|
138
|
+
if (!(project.columns || []).length) $('#columnsList').appendChild(renderColumnRow({}, 0));
|
|
139
|
+
|
|
140
|
+
updateLinks(project.slug);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function updateLinks(slug) {
|
|
144
|
+
if (!slug) return;
|
|
145
|
+
$('#linkPrdFile').href = `/projects/${slug}/prd.md`;
|
|
146
|
+
$('#linkProtoFile').href = `/projects/${slug}/prototype.html`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function switchTab(name) {
|
|
150
|
+
$$('.tab').forEach(t => t.classList.toggle('active', t.dataset.tab === name));
|
|
151
|
+
$$('.panel').forEach(p => p.classList.toggle('hidden', p.id !== `panel-${name}`));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function loadProtoFrame(slug, edit = false) {
|
|
155
|
+
if (!slug) return;
|
|
156
|
+
const q = edit ? '?edit=1&' : '?';
|
|
157
|
+
$('#protoFrame').src = `/projects/${slug}/prototype.html${q}t=${Date.now()}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function loadProjects() {
|
|
161
|
+
const { projects } = await api('/api/projects');
|
|
162
|
+
const sel = $('#projectSelect');
|
|
163
|
+
sel.innerHTML = '<option value="">— 选择项目 —</option>';
|
|
164
|
+
projects.forEach(p => {
|
|
165
|
+
const opt = document.createElement('option');
|
|
166
|
+
opt.value = p.slug;
|
|
167
|
+
opt.textContent = `${p.pageName} (${p.slug})`;
|
|
168
|
+
sel.appendChild(opt);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function loadProject(slug) {
|
|
173
|
+
if (!slug) return;
|
|
174
|
+
const data = await api(`/api/projects/${slug}`);
|
|
175
|
+
state.project = data.config;
|
|
176
|
+
fillForm(data.config);
|
|
177
|
+
$('#prdEditor').value = data.prd;
|
|
178
|
+
loadProtoFrame(slug, state.editMode);
|
|
179
|
+
const url = new URL(location.href);
|
|
180
|
+
url.searchParams.set('slug', slug);
|
|
181
|
+
history.replaceState({}, '', url);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function saveProject(body) {
|
|
185
|
+
const payload = body || collectForm();
|
|
186
|
+
const method = state.project?.slug === payload.slug && payload.slug ? 'PUT' : 'POST';
|
|
187
|
+
const url = method === 'PUT' ? `/api/projects/${payload.slug}` : '/api/projects';
|
|
188
|
+
const res = await api(url, { method, body: JSON.stringify(payload) });
|
|
189
|
+
state.project = payload;
|
|
190
|
+
await loadProjects();
|
|
191
|
+
$('#projectSelect').value = res.slug;
|
|
192
|
+
updateLinks(res.slug);
|
|
193
|
+
return res;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function init() {
|
|
197
|
+
const meta = await api('/api/meta');
|
|
198
|
+
state.pageTypes = meta.pageTypes;
|
|
199
|
+
$('#pageType').innerHTML = Object.entries(meta.pageTypes)
|
|
200
|
+
.map(([k, v]) => `<option value="${k}">${v}</option>`)
|
|
201
|
+
.join('');
|
|
202
|
+
|
|
203
|
+
await loadProjects();
|
|
204
|
+
|
|
205
|
+
const params = new URLSearchParams(location.search);
|
|
206
|
+
const slug = params.get('slug');
|
|
207
|
+
if (slug) {
|
|
208
|
+
$('#projectSelect').value = slug;
|
|
209
|
+
await loadProject(slug);
|
|
210
|
+
} else {
|
|
211
|
+
fillForm({
|
|
212
|
+
pageName: '',
|
|
213
|
+
pageType: 'list-crud',
|
|
214
|
+
batchDate: todayBatch(),
|
|
215
|
+
filters: [{ name: '名称', type: '文本', limit: '1~64', matchRule: '模糊' }],
|
|
216
|
+
columns: [
|
|
217
|
+
{ name: '名称', description: '业务名称', sortable: false },
|
|
218
|
+
{ name: '状态', description: '启用/停用', sortable: true },
|
|
219
|
+
],
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
$$('.tab').forEach(btn => btn.onclick = () => switchTab(btn.dataset.tab));
|
|
224
|
+
|
|
225
|
+
$('#pageName').addEventListener('blur', () => {
|
|
226
|
+
if (!$('#slug').value.trim() && $('#pageName').value.trim()) {
|
|
227
|
+
$('#slug').value = slugify($('#pageName').value);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
$$('[data-add]').forEach(btn => {
|
|
232
|
+
btn.onclick = () => {
|
|
233
|
+
const list = btn.dataset.add === 'filters' ? $('#filtersList') : $('#columnsList');
|
|
234
|
+
const row = btn.dataset.add === 'filters' ? renderFilterRow({}, list.children.length) : renderColumnRow({}, list.children.length);
|
|
235
|
+
list.appendChild(row);
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
$('#projectSelect').onchange = () => {
|
|
240
|
+
const v = $('#projectSelect').value;
|
|
241
|
+
if (v) loadProject(v);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
$('#btnNew').onclick = () => {
|
|
245
|
+
state.project = null;
|
|
246
|
+
fillForm({ pageName: '', pageType: 'list-crud', batchDate: todayBatch(), filters: [{}], columns: [{}] });
|
|
247
|
+
$('#prdEditor').value = '';
|
|
248
|
+
switchTab('form');
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
$('#btnPreviewPrd').onclick = async () => {
|
|
252
|
+
try {
|
|
253
|
+
const body = collectForm();
|
|
254
|
+
const { markdown } = await api('/api/projects/preview-prd', { method: 'POST', body: JSON.stringify(body) });
|
|
255
|
+
$('#prdEditor').value = markdown;
|
|
256
|
+
switchTab('prd');
|
|
257
|
+
} catch (e) {
|
|
258
|
+
toast(e.message);
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
$('#btnGenerate').onclick = async () => {
|
|
263
|
+
try {
|
|
264
|
+
const res = await saveProject();
|
|
265
|
+
const files = await api(`/api/projects/${res.slug}`);
|
|
266
|
+
$('#prdEditor').value = files.prd;
|
|
267
|
+
loadProtoFrame(res.slug, false);
|
|
268
|
+
toast(`已生成:${res.slug}`);
|
|
269
|
+
switchTab('proto');
|
|
270
|
+
} catch (e) {
|
|
271
|
+
toast(e.message);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
$('#btnRefreshPrd').onclick = $('#btnPreviewPrd').onclick;
|
|
276
|
+
|
|
277
|
+
$('#btnSavePrd').onclick = async () => {
|
|
278
|
+
try {
|
|
279
|
+
if (!state.project?.slug) throw new Error('请先生成项目');
|
|
280
|
+
const slug = state.project.slug;
|
|
281
|
+
await fetch(`/api/projects/${slug}`, {
|
|
282
|
+
method: 'PUT',
|
|
283
|
+
headers: { 'Content-Type': 'application/json' },
|
|
284
|
+
body: JSON.stringify({ ...collectForm(), prdOverride: $('#prdEditor').value }),
|
|
285
|
+
});
|
|
286
|
+
toast('配置已保存(prd 文本请在磁盘上直接编辑 prototype 联动需重新生成)');
|
|
287
|
+
} catch (e) {
|
|
288
|
+
toast(e.message);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
$('#btnRefreshProto').onclick = async () => {
|
|
293
|
+
try {
|
|
294
|
+
const res = await saveProject();
|
|
295
|
+
loadProtoFrame(res.slug, state.editMode);
|
|
296
|
+
toast('原型已重新生成');
|
|
297
|
+
} catch (e) {
|
|
298
|
+
toast(e.message);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
$('#btnApplyPreview').onclick = async () => {
|
|
303
|
+
try {
|
|
304
|
+
const body = collectForm();
|
|
305
|
+
const { html } = await api('/api/projects/preview-prototype', { method: 'POST', body: JSON.stringify(body) });
|
|
306
|
+
const blob = new Blob([html], { type: 'text/html' });
|
|
307
|
+
$('#protoFrame').src = URL.createObjectURL(blob);
|
|
308
|
+
toast('已应用预览(未保存到磁盘)');
|
|
309
|
+
} catch (e) {
|
|
310
|
+
toast(e.message);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
$('#btnSaveProject').onclick = async () => {
|
|
315
|
+
try {
|
|
316
|
+
const res = await saveProject();
|
|
317
|
+
toast(`项目已保存:${res.slug}`);
|
|
318
|
+
} catch (e) {
|
|
319
|
+
toast(e.message);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
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);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
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);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
init().catch(err => toast(err.message));
|
package/web/index.html
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Deppon PRD Studio</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<link rel="stylesheet" href="./styles.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body class="min-h-screen bg-slate-50 text-slate-800">
|
|
11
|
+
<header class="border-b border-slate-200 bg-white shadow-sm">
|
|
12
|
+
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-3">
|
|
13
|
+
<div>
|
|
14
|
+
<h1 class="text-lg font-bold text-slate-900">Deppon PRD Studio</h1>
|
|
15
|
+
<p class="text-xs text-slate-500">基于 deppon-prd-generator · 私有化 MCP</p>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="flex items-center gap-2">
|
|
18
|
+
<select id="projectSelect" class="rounded border border-slate-300 px-2 py-1.5 text-sm"></select>
|
|
19
|
+
<button id="btnNew" type="button" class="rounded border border-slate-300 px-3 py-1.5 text-sm hover:bg-slate-50">新建</button>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<nav class="mx-auto flex max-w-7xl gap-1 px-4 pb-2">
|
|
23
|
+
<button type="button" class="tab active" data-tab="form">① PRD 要素</button>
|
|
24
|
+
<button type="button" class="tab" data-tab="prd">② PRD 文档</button>
|
|
25
|
+
<button type="button" class="tab" data-tab="proto">③ 原型编辑</button>
|
|
26
|
+
</nav>
|
|
27
|
+
</header>
|
|
28
|
+
|
|
29
|
+
<main class="mx-auto max-w-7xl px-4 py-4">
|
|
30
|
+
<!-- Step 1: Form -->
|
|
31
|
+
<section id="panel-form" class="panel">
|
|
32
|
+
<div class="grid gap-4 lg:grid-cols-2">
|
|
33
|
+
<div class="space-y-4 rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
|
34
|
+
<h2 class="text-sm font-semibold">基础信息</h2>
|
|
35
|
+
<label class="field"><span>页面名称 *</span><input id="pageName" type="text" placeholder="产品折扣列表" /></label>
|
|
36
|
+
<label class="field"><span>Slug(目录名,建议英文 kebab-case)</span><input id="slug" type="text" placeholder="visitor-appointment-list" /></label>
|
|
37
|
+
<label class="field"><span>页面类型</span>
|
|
38
|
+
<select id="pageType"></select>
|
|
39
|
+
</label>
|
|
40
|
+
<label class="field"><span>批次日 YYYYMMDD</span><input id="batchDate" type="text" maxlength="8" placeholder="20260601" /></label>
|
|
41
|
+
<label class="field"><span>菜单路径</span><input id="menuPath" type="text" placeholder="产品管理 / 折扣与价格 / 产品折扣列表" /></label>
|
|
42
|
+
<label class="field"><span>背景描述 §1.1</span><textarea id="background" rows="3"></textarea></label>
|
|
43
|
+
<label class="inline-flex items-center gap-2 text-sm"><input id="useDpItTitle" type="checkbox" checked /> 使用 DP-IT 标准标题</label>
|
|
44
|
+
<label class="inline-flex items-center gap-2 text-sm"><input id="enableCrud" type="checkbox" checked /> 含 CRUD(查看/新建/编辑/删除)</label>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="space-y-4">
|
|
47
|
+
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
|
48
|
+
<div class="mb-2 flex items-center justify-between">
|
|
49
|
+
<h2 class="text-sm font-semibold">筛选字段 §3.1</h2>
|
|
50
|
+
<button type="button" class="btn-sm" data-add="filters">+ 添加</button>
|
|
51
|
+
</div>
|
|
52
|
+
<div id="filtersList" class="space-y-2"></div>
|
|
53
|
+
</div>
|
|
54
|
+
<div class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
|
55
|
+
<div class="mb-2 flex items-center justify-between">
|
|
56
|
+
<h2 class="text-sm font-semibold">列表字段 §3.2</h2>
|
|
57
|
+
<button type="button" class="btn-sm" data-add="columns">+ 添加</button>
|
|
58
|
+
</div>
|
|
59
|
+
<div id="columnsList" class="space-y-2"></div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="mt-4 flex flex-wrap gap-2">
|
|
64
|
+
<button id="btnGenerate" type="button" class="btn-primary">生成 PRD + 原型</button>
|
|
65
|
+
<button id="btnPreviewPrd" type="button" class="btn-secondary">仅预览 PRD</button>
|
|
66
|
+
</div>
|
|
67
|
+
</section>
|
|
68
|
+
|
|
69
|
+
<!-- Step 2: PRD preview -->
|
|
70
|
+
<section id="panel-prd" class="panel hidden">
|
|
71
|
+
<div class="mb-3 flex flex-wrap gap-2">
|
|
72
|
+
<button id="btnRefreshPrd" type="button" class="btn-secondary">从表单刷新</button>
|
|
73
|
+
<button id="btnSavePrd" type="button" class="btn-primary">保存 PRD 到项目</button>
|
|
74
|
+
<a id="linkPrdFile" href="#" target="_blank" class="btn-secondary">打开 prd.md</a>
|
|
75
|
+
</div>
|
|
76
|
+
<textarea id="prdEditor" class="min-h-[70vh] w-full rounded-xl border border-slate-200 p-4 font-mono text-sm shadow-sm" spellcheck="false"></textarea>
|
|
77
|
+
</section>
|
|
78
|
+
|
|
79
|
+
<!-- Step 3: Prototype -->
|
|
80
|
+
<section id="panel-proto" class="panel hidden">
|
|
81
|
+
<div class="mb-3 flex flex-wrap items-center gap-2">
|
|
82
|
+
<button id="btnRefreshProto" type="button" class="btn-secondary">从表单重新生成原型</button>
|
|
83
|
+
<button id="btnToggleEdit" type="button" class="btn-primary">开启 iframe 在线编辑</button>
|
|
84
|
+
<button id="btnSaveProject" type="button" class="btn-primary">保存整个项目</button>
|
|
85
|
+
<a id="linkProtoFile" href="#" target="_blank" class="btn-secondary">新窗口打开原型</a>
|
|
86
|
+
</div>
|
|
87
|
+
<div class="grid gap-4 lg:grid-cols-5">
|
|
88
|
+
<aside class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm lg:col-span-2">
|
|
89
|
+
<h2 class="mb-3 text-sm font-semibold">动态调整</h2>
|
|
90
|
+
<p class="mb-3 text-xs text-slate-500">修改后点「应用预览」实时刷新右侧原型;保存后写入磁盘。</p>
|
|
91
|
+
<label class="field"><span>新建按钮文案</span><input id="createButtonLabel" type="text" /></label>
|
|
92
|
+
<label class="field"><span>工具栏按钮(逗号分隔)</span><input id="toolbarButtons" type="text" placeholder="新建, 导出 CSV" /></label>
|
|
93
|
+
<label class="field"><span>行操作(逗号分隔)</span><input id="rowActions" type="text" placeholder="查看, 编辑, 删除" /></label>
|
|
94
|
+
<button id="btnApplyPreview" type="button" class="btn-primary mt-2 w-full">应用预览</button>
|
|
95
|
+
</aside>
|
|
96
|
+
<div class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm lg:col-span-3">
|
|
97
|
+
<iframe id="protoFrame" title="原型预览" class="h-[75vh] w-full border-0 bg-slate-100"></iframe>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
</main>
|
|
102
|
+
|
|
103
|
+
<div id="toast" class="toast hidden"></div>
|
|
104
|
+
<script src="./app.js" type="module"></script>
|
|
105
|
+
</body>
|
|
106
|
+
</html>
|
package/web/styles.css
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
.tab {
|
|
2
|
+
border-radius: 0.5rem;
|
|
3
|
+
padding: 0.4rem 0.85rem;
|
|
4
|
+
font-size: 0.8125rem;
|
|
5
|
+
font-weight: 500;
|
|
6
|
+
color: rgb(71 85 105);
|
|
7
|
+
}
|
|
8
|
+
.tab:hover {
|
|
9
|
+
background: rgb(241 245 249);
|
|
10
|
+
}
|
|
11
|
+
.tab.active {
|
|
12
|
+
background: rgb(37 99 235);
|
|
13
|
+
color: white;
|
|
14
|
+
}
|
|
15
|
+
.panel.hidden {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
.field {
|
|
19
|
+
display: block;
|
|
20
|
+
font-size: 0.75rem;
|
|
21
|
+
}
|
|
22
|
+
.field > span {
|
|
23
|
+
display: block;
|
|
24
|
+
margin-bottom: 0.25rem;
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
color: rgb(71 85 105);
|
|
27
|
+
}
|
|
28
|
+
.field input,
|
|
29
|
+
.field select,
|
|
30
|
+
.field textarea {
|
|
31
|
+
width: 100%;
|
|
32
|
+
border-radius: 0.375rem;
|
|
33
|
+
border: 1px solid rgb(203 213 225);
|
|
34
|
+
padding: 0.45rem 0.65rem;
|
|
35
|
+
font-size: 0.8125rem;
|
|
36
|
+
}
|
|
37
|
+
.field textarea {
|
|
38
|
+
resize: vertical;
|
|
39
|
+
}
|
|
40
|
+
.btn-primary {
|
|
41
|
+
border-radius: 0.5rem;
|
|
42
|
+
background: rgb(37 99 235);
|
|
43
|
+
padding: 0.45rem 0.9rem;
|
|
44
|
+
font-size: 0.8125rem;
|
|
45
|
+
font-weight: 600;
|
|
46
|
+
color: white;
|
|
47
|
+
}
|
|
48
|
+
.btn-primary:hover {
|
|
49
|
+
background: rgb(29 78 216);
|
|
50
|
+
}
|
|
51
|
+
.btn-secondary {
|
|
52
|
+
display: inline-flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
border-radius: 0.5rem;
|
|
55
|
+
border: 1px solid rgb(203 213 225);
|
|
56
|
+
background: white;
|
|
57
|
+
padding: 0.45rem 0.9rem;
|
|
58
|
+
font-size: 0.8125rem;
|
|
59
|
+
color: rgb(51 65 85);
|
|
60
|
+
}
|
|
61
|
+
.btn-secondary:hover {
|
|
62
|
+
background: rgb(248 250 252);
|
|
63
|
+
}
|
|
64
|
+
.btn-sm {
|
|
65
|
+
border-radius: 0.375rem;
|
|
66
|
+
border: 1px dashed rgb(148 163 184);
|
|
67
|
+
padding: 0.15rem 0.5rem;
|
|
68
|
+
font-size: 0.6875rem;
|
|
69
|
+
color: rgb(71 85 105);
|
|
70
|
+
}
|
|
71
|
+
.row-card {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: 1fr 1fr;
|
|
74
|
+
gap: 0.35rem;
|
|
75
|
+
border-radius: 0.5rem;
|
|
76
|
+
border: 1px solid rgb(226 232 240);
|
|
77
|
+
padding: 0.5rem;
|
|
78
|
+
font-size: 0.75rem;
|
|
79
|
+
}
|
|
80
|
+
.row-card button {
|
|
81
|
+
grid-column: 1 / -1;
|
|
82
|
+
justify-self: end;
|
|
83
|
+
color: rgb(220 38 38);
|
|
84
|
+
font-size: 0.6875rem;
|
|
85
|
+
}
|
|
86
|
+
.toast {
|
|
87
|
+
position: fixed;
|
|
88
|
+
top: 1rem;
|
|
89
|
+
right: 1rem;
|
|
90
|
+
z-index: 100;
|
|
91
|
+
border-radius: 0.5rem;
|
|
92
|
+
background: rgb(15 23 42);
|
|
93
|
+
padding: 0.65rem 1rem;
|
|
94
|
+
font-size: 0.8125rem;
|
|
95
|
+
color: white;
|
|
96
|
+
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
|
97
|
+
}
|
|
98
|
+
.toast.hidden {
|
|
99
|
+
display: none;
|
|
100
|
+
}
|