@deppon/deppon-skills 2.4.18 → 2.4.21

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 (30) hide show
  1. package/dist/deppon-npm-skills/SKILL.md +3 -3
  2. package/dist/deppon-prd-generator/SKILL.md +81 -18
  3. package/dist/deppon-prd-generator/examples/README.md +17 -0
  4. package/dist/deppon-prd-generator/examples/app-shell-navigation/layout-spec.md +54 -0
  5. package/dist/deppon-prd-generator/examples/app-shell-navigation/pages/demo-form.html +57 -0
  6. package/dist/deppon-prd-generator/examples/app-shell-navigation/pages/demo-home.html +92 -0
  7. package/dist/deppon-prd-generator/examples/app-shell-navigation/pages/demo-list.html +47 -0
  8. package/dist/deppon-prd-generator/examples/app-shell-navigation/prd.md +164 -0
  9. package/dist/deppon-prd-generator/examples/app-shell-navigation/prototype.html +794 -0
  10. package/dist/deppon-prd-generator/examples/backend-list/prd.md +91 -0
  11. package/dist/deppon-prd-generator/examples/backend-list/prototype.html +369 -0
  12. package/dist/deppon-prd-generator/examples/data-dashboard/prd.md +127 -0
  13. package/dist/deppon-prd-generator/examples/data-dashboard/prototype.html +281 -0
  14. package/dist/deppon-prd-generator/examples/form-edit/prd.md +108 -0
  15. package/dist/deppon-prd-generator/examples/form-edit/prototype.html +280 -0
  16. package/dist/deppon-prd-generator/examples/form-preview/prd.md +106 -0
  17. package/dist/deppon-prd-generator/examples/form-preview/prototype.html +240 -0
  18. package/dist/deppon-prd-generator/{user-management → examples/list-crud}/prd.md +9 -4
  19. package/dist/deppon-prd-generator/{user-management → examples/list-crud}/prototype.html +246 -94
  20. package/dist/deppon-prd-generator/examples/user-frontend/prd.md +86 -0
  21. package/dist/deppon-prd-generator/examples/user-frontend/prototype.html +223 -0
  22. package/dist/deppon-prd-generator/quick-reference.md +23 -6
  23. package/dist/deppon-prd-generator/template/app-shell-navigation-prd-template.md +180 -0
  24. package/dist/deppon-prd-generator/template/backend-form-edit-prd-template.md +4 -0
  25. package/dist/deppon-prd-generator/template/backend-form-preview-prd-template.md +4 -0
  26. package/dist/deppon-prd-generator/template/backend-list-prd-template.md +4 -0
  27. package/dist/deppon-prd-generator/template/data-prd-template.md +4 -0
  28. package/dist/deppon-prd-generator/template/user-frontend-prd-template.md +4 -0
  29. package/dist/deppon-prd-generator/workflow.md +34 -16
  30. package/package.json +1 -1
@@ -0,0 +1,280 @@
1
+ <!--
2
+ 技能示例 examples/form-edit/ — 对齐 backend-form-edit-prd-template
3
+ fetch 同目录 prd.md;须 HTTP 打开。
4
+ -->
5
+ <!DOCTYPE html>
6
+ <html lang="zh-CN">
7
+ <head>
8
+ <meta charset="UTF-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
+ <title>通知模板配置 — 原型</title>
11
+ <script src="https://cdn.tailwindcss.com"></script>
12
+ <style>
13
+ .anno-trigger {
14
+ display: inline-flex;
15
+ align-items: center;
16
+ border-radius: 0.375rem;
17
+ border: 1px solid rgba(217, 119, 6, 0.85);
18
+ background: rgba(254, 243, 199, 0.95);
19
+ padding: 0.15rem 0.5rem;
20
+ font-size: 0.625rem;
21
+ font-weight: 700;
22
+ color: rgb(69 26 3);
23
+ cursor: pointer;
24
+ }
25
+ #annoDlgBody .prd-md table {
26
+ width: 100%;
27
+ border-collapse: collapse;
28
+ font-size: 0.75rem;
29
+ }
30
+ #annoDlgBody .prd-md th,
31
+ #annoDlgBody .prd-md td {
32
+ border: 1px solid rgb(226 232 240);
33
+ padding: 0.35rem 0.45rem;
34
+ }
35
+ </style>
36
+ </head>
37
+ <body class="min-h-screen bg-slate-100">
38
+ <div id="prdLoadBanner" class="hidden border-b border-amber-300 bg-amber-100 px-4 py-2 text-center text-xs"></div>
39
+ <div id="toast" class="fixed top-4 right-4 z-[60] hidden rounded-lg bg-slate-900 px-4 py-2 text-sm text-white"></div>
40
+ <button
41
+ type="button"
42
+ id="btnAnnoLegend"
43
+ class="fixed bottom-5 left-5 z-[65] rounded-full border border-amber-400 bg-white px-3 py-2 text-xs shadow"
44
+ >
45
+ 标注说明
46
+ </button>
47
+
48
+ <header class="border-b bg-white px-4 py-4">
49
+ <div class="mx-auto flex max-w-2xl items-start gap-2">
50
+ <h1 class="text-lg font-semibold text-slate-900">通知模板配置</h1>
51
+ <button type="button" class="anno-trigger mt-0.5" data-anno-key="s1">PRD §1</button>
52
+ <button type="button" class="anno-trigger mt-0.5" data-anno-key="s2">PRD §2</button>
53
+ </div>
54
+ </header>
55
+
56
+ <main class="mx-auto max-w-2xl px-4 py-6">
57
+ <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
58
+ <div class="mb-4 flex flex-wrap items-center justify-between gap-2">
59
+ <h2 class="text-sm font-semibold text-slate-900">表单</h2>
60
+ <div class="flex flex-wrap gap-1">
61
+ <button type="button" class="anno-trigger" data-anno-key="s31">§3.1</button>
62
+ <button type="button" class="anno-trigger" data-anno-key="s32">§3.2</button>
63
+ <button type="button" class="anno-trigger" data-anno-key="s33">§3.3</button>
64
+ <button type="button" class="anno-trigger" data-anno-key="s34">§3.4</button>
65
+ <button type="button" class="anno-trigger" data-anno-key="s4">§4</button>
66
+ </div>
67
+ </div>
68
+ <div class="space-y-4">
69
+ <label class="block text-xs font-medium text-slate-600"
70
+ >模板名称 <span class="text-red-500">*</span>
71
+ <input id="fName" type="text" class="mt-1 w-full rounded-md border px-3 py-2 text-sm" placeholder="2~64 字"
72
+ /></label>
73
+ <fieldset>
74
+ <legend class="text-xs font-medium text-slate-600">推送渠道 <span class="text-red-500">*</span></legend>
75
+ <label class="mr-4 text-sm"><input type="checkbox" name="ch" value="站内信" checked /> 站内信</label>
76
+ <label class="mr-4 text-sm"><input type="checkbox" name="ch" value="邮件" /> 邮件</label>
77
+ <label class="text-sm"><input type="checkbox" name="ch" value="短信" /> 短信</label>
78
+ </fieldset>
79
+ <label class="block text-xs font-medium text-slate-600"
80
+ >标题前缀
81
+ <input id="fPrefix" type="text" class="mt-1 w-full rounded-md border px-3 py-2 text-sm"
82
+ /></label>
83
+ <label class="block text-xs font-medium text-slate-600"
84
+ >接收范围 <span class="text-red-500">*</span>
85
+ <select id="fScope" class="mt-1 w-full rounded-md border px-3 py-2 text-sm">
86
+ <option value="全部用户">全部用户</option>
87
+ <option value="按部门">按部门</option>
88
+ </select>
89
+ </label>
90
+ <div id="deptWrap" class="hidden">
91
+ <label class="block text-xs font-medium text-slate-600"
92
+ >部门 <span class="text-red-500">*</span>
93
+ <select id="fDept" class="mt-1 w-full rounded-md border px-3 py-2 text-sm">
94
+ <option value="">请选择</option>
95
+ <option>研发中心</option>
96
+ <option>运营中心</option>
97
+ </select>
98
+ </label>
99
+ </div>
100
+ </div>
101
+ <div class="mt-6 flex justify-end gap-2 border-t border-slate-100 pt-4">
102
+ <button type="button" id="btnCancel" class="rounded-lg border border-slate-300 px-4 py-2 text-sm">取消</button>
103
+ <button type="button" id="btnSave" class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow">
104
+ 保存
105
+ </button>
106
+ </div>
107
+ </div>
108
+ </main>
109
+
110
+ <div id="modalAnno" class="fixed inset-0 z-[70] hidden items-center justify-center bg-slate-900/55 p-4" role="dialog">
111
+ <div class="flex max-h-[88vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl border-2 border-amber-400 bg-white shadow-2xl">
112
+ <div class="flex shrink-0 justify-between border-b border-amber-200 bg-amber-50 px-4 py-2">
113
+ <h3 id="annoDlgTitle" class="truncate pr-2 text-sm font-semibold"></h3>
114
+ <button type="button" id="btnAnnoClose" class="text-slate-600">✕</button>
115
+ </div>
116
+ <div id="annoDlgBody" class="min-h-0 flex-1 overflow-auto p-4 text-sm" tabindex="-1"></div>
117
+ </div>
118
+ </div>
119
+
120
+ <script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
121
+ <script>
122
+ function runPrototype(prdMd, prdLoadError) {
123
+ function $(id) {
124
+ return document.getElementById(id);
125
+ }
126
+ function escapeHtml(s) {
127
+ return String(s)
128
+ .replace(/&/g, '&amp;')
129
+ .replace(/</g, '&lt;')
130
+ .replace(/>/g, '&gt;')
131
+ .replace(/"/g, '&quot;');
132
+ }
133
+ if (typeof marked !== 'undefined' && marked.use) marked.use({ gfm: true, breaks: true });
134
+ var ban = $('prdLoadBanner');
135
+ if (ban && (!prdMd || prdLoadError)) {
136
+ ban.textContent = '未加载 prd.md:请用 HTTP 打开本目录。';
137
+ ban.classList.remove('hidden');
138
+ }
139
+
140
+ function sliceBetween(md, startRe, endRe) {
141
+ if (!md) return '';
142
+ var m = md.match(startRe);
143
+ if (!m || m.index === undefined) return '';
144
+ var i0 = m.index;
145
+ if (!endRe) return md.slice(i0).trim();
146
+ var tail = md.slice(i0 + m[0].length);
147
+ var j = tail.search(endRe);
148
+ if (j === -1) return md.slice(i0).trim();
149
+ return md.slice(i0, i0 + m[0].length + j).trim();
150
+ }
151
+ function mdToHtml(src) {
152
+ if (!src) return '<p class="text-xs text-slate-500">(无匹配)</p>';
153
+ if (typeof marked !== 'undefined' && marked.parse)
154
+ return '<div class="prd-md">' + marked.parse(src) + '</div>';
155
+ return '<pre class="text-xs">' + escapeHtml(src) + '</pre>';
156
+ }
157
+
158
+ var LEGEND =
159
+ '<ul class="list-disc pl-4 text-sm"><li>标注正文来自 <strong>prd.md</strong> 切片。</li></ul>';
160
+ var EX = {
161
+ s1: { start: /^## 1\./m, end: /^## 2\./m, title: 'prd.md §1' },
162
+ s2: { start: /^## 2\./m, end: /^## 3\./m, title: 'prd.md §2' },
163
+ s31: { start: /^### 3\.1/m, end: /^### 3\.2/m, title: 'prd.md §3.1' },
164
+ s32: { start: /^### 3\.2/m, end: /^### 3\.3/m, title: 'prd.md §3.2' },
165
+ s33: { start: /^### 3\.3/m, end: /^### 3\.4/m, title: 'prd.md §3.3' },
166
+ s34: { start: /^### 3\.4/m, end: /^## 4\./m, title: 'prd.md §3.4' },
167
+ s4: { start: /^## 4\./m, end: /^## 9\./m, title: 'prd.md §4' },
168
+ };
169
+ var FB = {
170
+ s1: { title: '§1', html: '<p class="text-xs">兜底</p>' },
171
+ s2: { title: '§2', html: '<p class="text-xs">兜底</p>' },
172
+ s31: { title: '§3.1', html: '<p class="text-xs">兜底</p>' },
173
+ s32: { title: '§3.2', html: '<p class="text-xs">兜底</p>' },
174
+ s33: { title: '§3.3', html: '<p class="text-xs">兜底</p>' },
175
+ s34: { title: '§3.4', html: '<p class="text-xs">兜底</p>' },
176
+ s4: { title: '§4', html: '<p class="text-xs">兜底</p>' },
177
+ };
178
+
179
+ function openAnno(key) {
180
+ var w =
181
+ !prdMd || prdLoadError
182
+ ? '<div class="mb-2 rounded bg-amber-50 p-2 text-xs">prd 未联动</div>'
183
+ : '';
184
+ if (key === 'legend') {
185
+ $('annoDlgTitle').textContent = '说明 · §9';
186
+ var s9 = prdMd ? sliceBetween(prdMd, /^## 9\./m, /^## 10\./m) : '';
187
+ $('annoDlgBody').innerHTML = w + LEGEND + (s9 ? '<div class="mt-2">' + mdToHtml(s9) + '</div>' : '');
188
+ $('modalAnno').classList.remove('hidden');
189
+ $('modalAnno').classList.add('flex');
190
+ $('annoDlgBody').scrollTop = 0;
191
+ return;
192
+ }
193
+ var sp = EX[key];
194
+ if (!sp) return;
195
+ var sl = prdMd ? sliceBetween(prdMd, sp.start, sp.end) : '';
196
+ $('annoDlgTitle').textContent = sp.title;
197
+ $('annoDlgBody').innerHTML = w + (sl ? mdToHtml(sl) : FB[key].html);
198
+ $('modalAnno').classList.remove('hidden');
199
+ $('modalAnno').classList.add('flex');
200
+ $('annoDlgBody').scrollTop = 0;
201
+ }
202
+ function closeAnno() {
203
+ $('modalAnno').classList.add('hidden');
204
+ $('modalAnno').classList.remove('flex');
205
+ }
206
+
207
+ document.addEventListener(
208
+ 'click',
209
+ function (e) {
210
+ var t = e.target.closest('[data-anno-key]');
211
+ if (!t) return;
212
+ e.preventDefault();
213
+ e.stopPropagation();
214
+ openAnno(t.getAttribute('data-anno-key'));
215
+ },
216
+ true,
217
+ );
218
+ $('btnAnnoClose').addEventListener('click', closeAnno);
219
+ $('modalAnno').addEventListener('click', function (e) {
220
+ if (e.target === $('modalAnno')) closeAnno();
221
+ });
222
+ $('btnAnnoLegend').addEventListener('click', function () {
223
+ openAnno('legend');
224
+ });
225
+ document.addEventListener('keydown', function (e) {
226
+ if (e.key === 'Escape' && !$('modalAnno').classList.contains('hidden')) closeAnno();
227
+ });
228
+
229
+ function syncDept() {
230
+ var on = $('fScope').value === '按部门';
231
+ $('deptWrap').classList.toggle('hidden', !on);
232
+ }
233
+ $('fScope').addEventListener('change', syncDept);
234
+ syncDept();
235
+
236
+ $('btnSave').addEventListener('click', function () {
237
+ var n = $('fName').value.trim();
238
+ if (n.length < 2) {
239
+ toast('模板名称至少 2 字');
240
+ return;
241
+ }
242
+ var ch = document.querySelectorAll('input[name="ch"]:checked').length;
243
+ if (!ch) {
244
+ toast('请至少选一个渠道');
245
+ return;
246
+ }
247
+ if ($('fScope').value === '按部门' && !$('fDept').value) {
248
+ toast('请选择部门');
249
+ return;
250
+ }
251
+ toast('保存成功(模拟)');
252
+ });
253
+ $('btnCancel').addEventListener('click', function () {
254
+ toast('取消(模拟):可接二次确认');
255
+ });
256
+
257
+ function toast(m) {
258
+ var el = $('toast');
259
+ el.textContent = m;
260
+ el.classList.remove('hidden');
261
+ clearTimeout(el._t);
262
+ el._t = setTimeout(function () {
263
+ el.classList.add('hidden');
264
+ }, 2200);
265
+ }
266
+ }
267
+ fetch('prd.md', { cache: 'no-store' })
268
+ .then(function (r) {
269
+ if (!r.ok) throw new Error();
270
+ return r.text();
271
+ })
272
+ .then(function (t) {
273
+ runPrototype(t, null);
274
+ })
275
+ .catch(function () {
276
+ runPrototype('', new Error());
277
+ });
278
+ </script>
279
+ </body>
280
+ </html>
@@ -0,0 +1,106 @@
1
+ # 工单详情(只读)PRD(产品需求文档)
2
+
3
+ > **文档说明**:本文件位于技能目录 `examples/form-preview/`,对应 **`backend-form-preview-prd-template.md`**(只读详情 + 操作按钮区)。实际产出写入 `src/prototypes/<page-name>/prd.md`。
4
+
5
+ > **交互原型**:[打开 prototype.html](./prototype.html);标注 **`fetch` 同目录 `prd.md`**;须 **HTTP** 打开。
6
+
7
+ ---
8
+
9
+ ## 1. 引言
10
+
11
+ ### 1.1 背景
12
+
13
+ 供客服与处理人查看工单快照:状态、优先级、描述与处理记录只读展示;可跳转编辑或返回列表(权限控制)。
14
+
15
+ ### 1.2 功能清单
16
+
17
+ | 模块 | 功能点 | 功能描述 |
18
+ | -------- | -------- | ------------------ |
19
+ | 工单详情 | 信息展示 | 字段只读展示 |
20
+ | 工单详情 | 编辑跳转 | 有编辑权限时展示 |
21
+ | 工单详情 | 返回 | 返回上一页或列表 |
22
+
23
+ ---
24
+
25
+ ## 2. 页面菜单与权限规划
26
+
27
+ ### 2.1 页面菜单
28
+
29
+ **菜单路径**:服务台 / 工单管理 / 工单详情
30
+
31
+ ### 2.2 权限规划
32
+
33
+ | 权限点 | 操作范围 | 数据范围 |
34
+ | ------ | -------------- | -------- |
35
+ | 查看 | 详情只读、返回 | 按单号 |
36
+ | 编辑 | 跳转编辑页 | 按单号 |
37
+
38
+ ---
39
+
40
+ ## 3. 工单详情需求说明
41
+
42
+ ### 3.1 信息展示区域
43
+
44
+ | 字段名称 | 展示格式 | 说明 |
45
+ | -------- | ---------- | ---------- |
46
+ | 工单号 | 等宽文本 | 主键展示 |
47
+ | 状态 | 状态标签色 | 待处理/处理中/已关闭 |
48
+ | 优先级 | 文本 | P1~P3 |
49
+ | 创建时间 | YYYY-MM-DD HH:mm | — |
50
+ | 问题描述 | 多行文本 | 保留换行 |
51
+
52
+ ### 3.2 字段详细说明
53
+
54
+ #### 3.2.1 状态
55
+
56
+ - **字段说明**:驱动 SLA 展示与按钮显隐。
57
+ - **展示格式**:彩色标签。
58
+ - **空值展示**:—
59
+ - **特殊规则**:已关闭隐藏「编辑」。
60
+
61
+ ### 3.3 操作按钮
62
+
63
+ | 按钮名称 | 按钮位置 | 点击逻辑 | 权限要求 |
64
+ | -------- | -------- | ------------ | -------- |
65
+ | 编辑 | 右上 | 跳转编辑路由 | 编辑 |
66
+ | 返回 | 右上 | history.back | 无 |
67
+
68
+ #### 3.3.1 编辑按钮
69
+
70
+ 主按钮样式(主题蓝);无权限时隐藏或禁用。
71
+
72
+ #### 3.3.3 返回按钮
73
+
74
+ 次要样式(白底描边)。
75
+
76
+ ---
77
+
78
+ ## 4. 交互设计
79
+
80
+ ### 4.1 全局交互
81
+
82
+ 进入页 Loading;无数据 Empty;响应式布局。
83
+
84
+ ### 4.2 页面交互
85
+
86
+ 按 `id` 拉取详情;权限控制按钮;可选复制工单号。
87
+
88
+ ---
89
+
90
+ ## 9. 原型与 PRD 对应关系
91
+
92
+ | PRD 章节 | prototype.html 演示点 |
93
+ | -------- | ---------------------- |
94
+ | §1 | 页头「PRD §1」 |
95
+ | §3.1 | 详情区「PRD §3.1」 |
96
+ | §3.3 | 操作栏「PRD §3.3」 |
97
+ | §4 | 按钮点击 Toast 模拟 |
98
+ | 走查说明 | 左下角「标注说明」 |
99
+
100
+ ---
101
+
102
+ ## 10. 修订记录
103
+
104
+ | 版本 | 日期 | 说明 |
105
+ | ---- | ---------- | ---- |
106
+ | v0.1 | 2026-05-14 | 初版:对齐 backend-form-preview 模板示例 |
@@ -0,0 +1,240 @@
1
+ <!--
2
+ 技能示例 examples/form-preview/ — 对齐 backend-form-preview-prd-template
3
+ fetch prd.md;须 HTTP。
4
+ -->
5
+ <!DOCTYPE html>
6
+ <html lang="zh-CN">
7
+ <head>
8
+ <meta charset="UTF-8" />
9
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
+ <title>工单详情 — 原型</title>
11
+ <script src="https://cdn.tailwindcss.com"></script>
12
+ <style>
13
+ .anno-trigger {
14
+ display: inline-flex;
15
+ border-radius: 0.375rem;
16
+ border: 1px solid rgba(217, 119, 6, 0.85);
17
+ background: rgba(254, 243, 199, 0.95);
18
+ padding: 0.15rem 0.5rem;
19
+ font-size: 0.625rem;
20
+ font-weight: 700;
21
+ color: rgb(69 26 3);
22
+ cursor: pointer;
23
+ }
24
+ #annoDlgBody .prd-md table {
25
+ width: 100%;
26
+ border-collapse: collapse;
27
+ font-size: 0.75rem;
28
+ }
29
+ #annoDlgBody .prd-md th,
30
+ #annoDlgBody .prd-md td {
31
+ border: 1px solid rgb(226 232 240);
32
+ padding: 0.35rem;
33
+ }
34
+ </style>
35
+ </head>
36
+ <body class="min-h-screen bg-slate-100">
37
+ <div id="prdLoadBanner" class="hidden border-b border-amber-300 bg-amber-100 px-4 py-2 text-center text-xs"></div>
38
+ <div id="toast" class="fixed top-4 right-4 z-[60] hidden rounded-lg bg-slate-900 px-4 py-2 text-sm text-white"></div>
39
+ <button type="button" id="btnAnnoLegend" class="fixed bottom-5 left-5 z-[65] rounded-full border border-amber-400 bg-white px-3 py-2 text-xs shadow">
40
+ 标注说明
41
+ </button>
42
+
43
+ <header class="border-b bg-white">
44
+ <div class="mx-auto flex max-w-3xl flex-wrap items-center justify-between gap-2 px-4 py-3">
45
+ <div class="flex items-center gap-2">
46
+ <h1 class="text-lg font-semibold">工单详情</h1>
47
+ <button type="button" class="anno-trigger" data-anno-key="s1">PRD §1</button>
48
+ </div>
49
+ <div class="flex flex-wrap items-center gap-2">
50
+ <button type="button" class="anno-trigger" data-anno-key="s33">PRD §3.3</button>
51
+ <button type="button" id="btnBack" class="rounded-lg border border-slate-300 bg-white px-3 py-1.5 text-sm">
52
+ 返回
53
+ </button>
54
+ <button type="button" id="btnEdit" class="rounded-lg bg-blue-600 px-3 py-1.5 text-sm font-medium text-white">
55
+ 编辑
56
+ </button>
57
+ </div>
58
+ </div>
59
+ </header>
60
+
61
+ <main class="mx-auto max-w-3xl px-4 py-6">
62
+ <div class="rounded-xl border border-slate-200 bg-white p-6 shadow-sm">
63
+ <div class="mb-4 flex items-center justify-between">
64
+ <h2 class="text-sm font-semibold text-slate-900">只读信息</h2>
65
+ <div class="flex gap-1">
66
+ <button type="button" class="anno-trigger" data-anno-key="s31">§3.1</button>
67
+ <button type="button" class="anno-trigger" data-anno-key="s32">§3.2</button>
68
+ <button type="button" class="anno-trigger" data-anno-key="s4">§4</button>
69
+ </div>
70
+ </div>
71
+ <dl class="space-y-3 text-sm">
72
+ <div class="flex justify-between gap-4 border-b border-slate-50 py-2">
73
+ <dt class="text-slate-500">工单号</dt>
74
+ <dd class="font-mono text-slate-900">WO-2026-0001</dd>
75
+ </div>
76
+ <div class="flex justify-between gap-4 border-b border-slate-50 py-2">
77
+ <dt class="text-slate-500">状态</dt>
78
+ <dd><span class="rounded bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-900">处理中</span></dd>
79
+ </div>
80
+ <div class="flex justify-between gap-4 border-b border-slate-50 py-2">
81
+ <dt class="text-slate-500">优先级</dt>
82
+ <dd>P2</dd>
83
+ </div>
84
+ <div class="flex justify-between gap-4 border-b border-slate-50 py-2">
85
+ <dt class="text-slate-500">创建时间</dt>
86
+ <dd class="font-mono text-xs">2026-05-10 11:20</dd>
87
+ </div>
88
+ <div class="py-2">
89
+ <dt class="text-slate-500">问题描述</dt>
90
+ <dd class="mt-1 whitespace-pre-wrap text-slate-800">登录页偶发 502,复现步骤见附件日志。</dd>
91
+ </div>
92
+ </dl>
93
+ </div>
94
+ </main>
95
+
96
+ <div id="modalAnno" class="fixed inset-0 z-[70] hidden items-center justify-center bg-slate-900/55 p-4" role="dialog">
97
+ <div class="flex max-h-[88vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl border-2 border-amber-400 bg-white shadow-2xl">
98
+ <div class="flex shrink-0 justify-between border-b border-amber-200 bg-amber-50 px-4 py-2">
99
+ <h3 id="annoDlgTitle" class="truncate text-sm font-semibold"></h3>
100
+ <button type="button" id="btnAnnoClose">✕</button>
101
+ </div>
102
+ <div id="annoDlgBody" class="min-h-0 flex-1 overflow-auto p-4 text-sm" tabindex="-1"></div>
103
+ </div>
104
+ </div>
105
+
106
+ <script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
107
+ <script>
108
+ function runPrototype(prdMd, prdLoadError) {
109
+ function $(id) {
110
+ return document.getElementById(id);
111
+ }
112
+ function escapeHtml(s) {
113
+ return String(s)
114
+ .replace(/&/g, '&amp;')
115
+ .replace(/</g, '&lt;')
116
+ .replace(/>/g, '&gt;')
117
+ .replace(/"/g, '&quot;');
118
+ }
119
+ if (typeof marked !== 'undefined' && marked.use) marked.use({ gfm: true, breaks: true });
120
+ var ban = $('prdLoadBanner');
121
+ if (ban && (!prdMd || prdLoadError)) {
122
+ ban.textContent = '未加载 prd.md:请用 HTTP 打开本目录。';
123
+ ban.classList.remove('hidden');
124
+ }
125
+
126
+ function sliceBetween(md, startRe, endRe) {
127
+ if (!md) return '';
128
+ var m = md.match(startRe);
129
+ if (!m || m.index === undefined) return '';
130
+ var i0 = m.index;
131
+ if (!endRe) return md.slice(i0).trim();
132
+ var tail = md.slice(i0 + m[0].length);
133
+ var j = tail.search(endRe);
134
+ if (j === -1) return md.slice(i0).trim();
135
+ return md.slice(i0, i0 + m[0].length + j).trim();
136
+ }
137
+ function mdToHtml(src) {
138
+ if (!src) return '<p class="text-xs text-slate-500">(无)</p>';
139
+ if (typeof marked !== 'undefined' && marked.parse)
140
+ return '<div class="prd-md">' + marked.parse(src) + '</div>';
141
+ return '<pre class="text-xs">' + escapeHtml(src) + '</pre>';
142
+ }
143
+
144
+ var LEG = '<p class="text-sm">标注联动 <strong>prd.md</strong>。</p>';
145
+ var EX = {
146
+ s1: { start: /^## 1\./m, end: /^## 2\./m, title: 'prd.md §1' },
147
+ s31: { start: /^### 3\.1/m, end: /^### 3\.2/m, title: 'prd.md §3.1' },
148
+ s32: { start: /^### 3\.2/m, end: /^### 3\.3/m, title: 'prd.md §3.2' },
149
+ s33: { start: /^### 3\.3/m, end: /^## 4\./m, title: 'prd.md §3.3' },
150
+ s4: { start: /^## 4\./m, end: /^## 9\./m, title: 'prd.md §4' },
151
+ };
152
+ var FB = {
153
+ s1: { html: '<p class="text-xs">兜底</p>' },
154
+ s31: { html: '<p class="text-xs">兜底</p>' },
155
+ s32: { html: '<p class="text-xs">兜底</p>' },
156
+ s33: { html: '<p class="text-xs">兜底</p>' },
157
+ s4: { html: '<p class="text-xs">兜底</p>' },
158
+ };
159
+
160
+ function openAnno(key) {
161
+ var w =
162
+ !prdMd || prdLoadError
163
+ ? '<div class="mb-2 rounded bg-amber-50 p-2 text-xs">prd 未联动</div>'
164
+ : '';
165
+ if (key === 'legend') {
166
+ $('annoDlgTitle').textContent = '说明 · §9';
167
+ var s9 = prdMd ? sliceBetween(prdMd, /^## 9\./m, /^## 10\./m) : '';
168
+ $('annoDlgBody').innerHTML = w + LEG + (s9 ? '<div class="mt-2">' + mdToHtml(s9) + '</div>' : '');
169
+ show();
170
+ return;
171
+ }
172
+ var sp = EX[key];
173
+ if (!sp) return;
174
+ var sl = prdMd ? sliceBetween(prdMd, sp.start, sp.end) : '';
175
+ $('annoDlgTitle').textContent = sp.title;
176
+ $('annoDlgBody').innerHTML = w + (sl ? mdToHtml(sl) : FB[key].html);
177
+ show();
178
+ }
179
+ function show() {
180
+ $('modalAnno').classList.remove('hidden');
181
+ $('modalAnno').classList.add('flex');
182
+ $('annoDlgBody').scrollTop = 0;
183
+ }
184
+ function closeAnno() {
185
+ $('modalAnno').classList.add('hidden');
186
+ $('modalAnno').classList.remove('flex');
187
+ }
188
+
189
+ document.addEventListener(
190
+ 'click',
191
+ function (e) {
192
+ var t = e.target.closest('[data-anno-key]');
193
+ if (!t) return;
194
+ e.preventDefault();
195
+ e.stopPropagation();
196
+ openAnno(t.getAttribute('data-anno-key'));
197
+ },
198
+ true,
199
+ );
200
+ $('btnAnnoClose').addEventListener('click', closeAnno);
201
+ $('modalAnno').addEventListener('click', function (e) {
202
+ if (e.target === $('modalAnno')) closeAnno();
203
+ });
204
+ $('btnAnnoLegend').addEventListener('click', function () {
205
+ openAnno('legend');
206
+ });
207
+ document.addEventListener('keydown', function (e) {
208
+ if (e.key === 'Escape' && !$('modalAnno').classList.contains('hidden')) closeAnno();
209
+ });
210
+
211
+ function toast(m) {
212
+ var el = $('toast');
213
+ el.textContent = m;
214
+ el.classList.remove('hidden');
215
+ clearTimeout(el._t);
216
+ el._t = setTimeout(function () {
217
+ el.classList.add('hidden');
218
+ }, 2000);
219
+ }
220
+ $('btnBack').addEventListener('click', function () {
221
+ toast('返回(模拟)');
222
+ });
223
+ $('btnEdit').addEventListener('click', function () {
224
+ toast('跳转编辑页(模拟)');
225
+ });
226
+ }
227
+ fetch('prd.md', { cache: 'no-store' })
228
+ .then(function (r) {
229
+ if (!r.ok) throw new Error();
230
+ return r.text();
231
+ })
232
+ .then(function (t) {
233
+ runPrototype(t, null);
234
+ })
235
+ .catch(function () {
236
+ runPrototype('', new Error());
237
+ });
238
+ </script>
239
+ </body>
240
+ </html>
@@ -1,8 +1,8 @@
1
1
  # 用户管理 PRD(产品需求文档)
2
2
 
3
- > **文档说明**:通用后台**用户管理**(列表 + 详情 + 新增/编辑 + 删除)标准需求;与同级 `prototype.html` 对齐走查。落地以实际接口与权限码为准。
3
+ > **文档说明**:本文件位于技能目录 `examples/list-crud/`,是 **后台列表 + 详情 + 新增/编辑 + 删除** 的**参考示例**(业务场景:用户管理),供生成同类 PRD 时对照章节结构、表格字段与「原型与 PRD 对应关系」写法。**不是**业务项目的默认交付路径;实际产出仍写入 `src/prototypes/<page-name>/prd.md`。落地以实际接口与权限码为准。
4
4
 
5
- > **高保真原型**:同目录 `prototype.html`(Tailwind Play CDN + 原生 JS),可演示 §3 筛选与列表、§4 详情弹层、§5 新增/编辑弹层、删除确认与 Toast 模拟;数据为静态 mock。
5
+ > **交互原型**:[打开 prototype.html](./prototype.html)(Tailwind Play CDN + `marked` + 原生 JS)。**需求标注**:运行时 `fetch` 同目录本 `prd.md`,按章节切片 Markdown 渲染进标注弹框(改 PRD 后刷新即可联动);须用**本地 HTTP**打开目录(`file://` 无法加载 prd.md 时仅显示兜底摘要)。可演示 §3~§6 主路径与 Toast;业务数据为静态 mock。
6
6
 
7
7
  ---
8
8
 
@@ -120,6 +120,8 @@
120
120
 
121
121
  加载态、空态、防重复提交、列表与弹层焦点与 `aria-modal` 与组件库规范对齐。
122
122
 
123
+ - **主色按钮**:查询、新增用户、保存等主操作使用**主题蓝**实心按钮;重置、取消、关闭等为次要(白底描边);删除、确定离开等为危险(红色系)。高保真原型与 `deppon-prd-generator` 技能「技术约定」第 8 条一致。
124
+
123
125
  ---
124
126
 
125
127
  ## 9. 原型与 PRD 对应关系
@@ -133,8 +135,8 @@
133
135
  | §4 | 详情弹层内「PRD §4」+ 只读字段 |
134
136
  | §5 | 表单弹层内「PRD §5」+ 新增/编辑与校验 |
135
137
  | §6 | 删除弹层内「PRD §6」+ 二次确认 |
136
- | §8 | Toast 模拟成功/失败(无真实接口) |
137
- | 走查说明 | 左下角「标注说明」:琥珀色弹框为需求标注(非业务功能),见 deppon-prd-generator 技能约定 |
138
+ | §8 | 主色按钮主题蓝与次要/危险区分;Toast 模拟成功/失败(无真实接口) |
139
+ | 走查说明 | 左下角「标注说明」:操作说明 + **prd.md §9 原文**;各「PRD §」从 **prd.md 对应章节切片**渲染;弹内长表格/长文可纵向与横向滚动;未加载 prd.md 时见页面顶栏黄条与弹内兜底 |
138
140
 
139
141
  ---
140
142
 
@@ -144,3 +146,6 @@
144
146
  | ---- | ---------- | ---------------------------- |
145
147
  | v0.1 | 2026-05-13 | 用户列表 CRUD 初稿(他目录) |
146
148
  | v1.2 | 2026-05-13 | 原型:新增用户移至列表标题行;总条数移至分页栏「每页」旁 |
149
+ | v1.3 | 2026-05-14 | 迁入 `examples/list-crud/`,明确为技能内参考示例 |
150
+ | v1.4 | 2026-05-14 | 交互:主操作按钮统一主题蓝;与技能技术约定第 8 条对齐 |
151
+ | v1.5 | 2026-05-14 | 原型:标注正文 `fetch` 联动 prd.md 切片 + Markdown 渲染;弹层内可滚动;file:// 兜底说明 |