@deppon/deppon-skills 2.4.16 → 2.4.20
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/dist/deppon-npm-skills/SKILL.md +7 -5
- package/dist/deppon-prd-generator/SKILL.md +273 -0
- package/dist/deppon-prd-generator/examples/README.md +17 -0
- package/dist/deppon-prd-generator/examples/app-shell-navigation/layout-spec.md +54 -0
- package/dist/deppon-prd-generator/examples/app-shell-navigation/pages/demo-form.html +57 -0
- package/dist/deppon-prd-generator/examples/app-shell-navigation/pages/demo-home.html +92 -0
- package/dist/deppon-prd-generator/examples/app-shell-navigation/pages/demo-list.html +47 -0
- package/dist/deppon-prd-generator/examples/app-shell-navigation/prd.md +164 -0
- package/dist/deppon-prd-generator/examples/app-shell-navigation/prototype.html +794 -0
- package/dist/deppon-prd-generator/examples/backend-list/prd.md +91 -0
- package/dist/deppon-prd-generator/examples/backend-list/prototype.html +369 -0
- package/dist/deppon-prd-generator/examples/data-dashboard/prd.md +127 -0
- package/dist/deppon-prd-generator/examples/data-dashboard/prototype.html +281 -0
- package/dist/deppon-prd-generator/examples/form-edit/prd.md +108 -0
- package/dist/deppon-prd-generator/examples/form-edit/prototype.html +280 -0
- package/dist/deppon-prd-generator/examples/form-preview/prd.md +106 -0
- package/dist/deppon-prd-generator/examples/form-preview/prototype.html +240 -0
- package/dist/deppon-prd-generator/examples/list-crud/prd.md +151 -0
- package/dist/deppon-prd-generator/examples/list-crud/prototype.html +1339 -0
- package/dist/deppon-prd-generator/examples/user-frontend/prd.md +86 -0
- package/dist/deppon-prd-generator/examples/user-frontend/prototype.html +223 -0
- package/dist/deppon-prd-generator/quick-reference.md +170 -0
- package/dist/deppon-prd-generator/template/app-shell-navigation-prd-template.md +180 -0
- package/dist/deppon-prd-generator/template/backend-form-edit-prd-template.md +116 -0
- package/dist/deppon-prd-generator/template/backend-form-preview-prd-template.md +112 -0
- package/dist/deppon-prd-generator/template/backend-list-prd-template.md +110 -0
- package/dist/deppon-prd-generator/template/data-prd-template.md +194 -0
- package/dist/deppon-prd-generator/template/user-frontend-prd-template.md +59 -0
- package/dist/deppon-prd-generator/workflow.md +269 -0
- package/dist/deppon-pro-form/SKILL.md +1 -1
- package/dist/deppon-pro-page-insert-detail-workflow/SKILL.md +94 -0
- package/dist/{deppon-pro-page-crud → deppon-pro-page-query-table-form}/SKILL.md +20 -7
- package/dist/deppon-pro-table/SKILL.md +5 -0
- package/package.json +1 -1
- package/dist/deppon-npm-prd/SKILL.md +0 -70
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
用户管理高保真可交互原型(技能内置参考示例:examples/list-crud/)
|
|
3
|
+
对应文档:同目录 prd.md(§3 列表、§4 详情、§5 表单、§6 删除、§9 对照表)
|
|
4
|
+
技术:Tailwind Play CDN + marked(Markdown)+ 原生 JS;业务数据为 mock。
|
|
5
|
+
需求标注:运行时 fetch 同目录 prd.md,按章节切片渲染进标注弹框(与 PRD 联动);请用本地 HTTP 打开本目录,勿依赖 file://。
|
|
6
|
+
弹层内长表格/长文:纵向 + 横向可滚动(见样式 #annoDlgBody .prd-md)。
|
|
7
|
+
-->
|
|
8
|
+
<!DOCTYPE html>
|
|
9
|
+
<html lang="zh-CN">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8" />
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
13
|
+
<title>用户管理 — 原型</title>
|
|
14
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
15
|
+
<script>
|
|
16
|
+
tailwind.config = {
|
|
17
|
+
theme: {
|
|
18
|
+
extend: {
|
|
19
|
+
fontFamily: { sans: ['system-ui', 'Segoe UI', 'PingFang SC', 'sans-serif'] },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
</script>
|
|
24
|
+
<style>
|
|
25
|
+
.anno-trigger {
|
|
26
|
+
display: inline-flex;
|
|
27
|
+
align-items: center;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
border-radius: 0.375rem;
|
|
30
|
+
border: 1px solid rgba(217, 119, 6, 0.85);
|
|
31
|
+
background: rgba(254, 243, 199, 0.95);
|
|
32
|
+
padding: 0.15rem 0.5rem;
|
|
33
|
+
font-size: 0.625rem;
|
|
34
|
+
font-weight: 700;
|
|
35
|
+
letter-spacing: 0.04em;
|
|
36
|
+
color: rgb(69 26 3);
|
|
37
|
+
line-height: 1.2;
|
|
38
|
+
white-space: nowrap;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
}
|
|
41
|
+
.anno-trigger:hover {
|
|
42
|
+
background: rgb(253 230 138);
|
|
43
|
+
}
|
|
44
|
+
.anno-trigger:focus-visible {
|
|
45
|
+
outline: 2px solid rgb(217 119 6);
|
|
46
|
+
outline-offset: 2px;
|
|
47
|
+
}
|
|
48
|
+
/* 标注弹框内:Markdown 渲染区,表格过宽时可横向滚动 */
|
|
49
|
+
#annoDlgBody .prd-md {
|
|
50
|
+
max-width: none;
|
|
51
|
+
}
|
|
52
|
+
#annoDlgBody .prd-md table {
|
|
53
|
+
width: 100%;
|
|
54
|
+
border-collapse: collapse;
|
|
55
|
+
font-size: 0.75rem;
|
|
56
|
+
line-height: 1.35;
|
|
57
|
+
}
|
|
58
|
+
#annoDlgBody .prd-md th,
|
|
59
|
+
#annoDlgBody .prd-md td {
|
|
60
|
+
border: 1px solid rgb(226 232 240);
|
|
61
|
+
padding: 0.35rem 0.5rem;
|
|
62
|
+
vertical-align: top;
|
|
63
|
+
}
|
|
64
|
+
#annoDlgBody .prd-md th {
|
|
65
|
+
background: rgb(248 250 252);
|
|
66
|
+
font-weight: 600;
|
|
67
|
+
text-align: left;
|
|
68
|
+
}
|
|
69
|
+
#annoDlgBody .prd-md h2 {
|
|
70
|
+
font-size: 1rem;
|
|
71
|
+
font-weight: 700;
|
|
72
|
+
margin: 0.75rem 0 0.5rem;
|
|
73
|
+
color: rgb(15 23 42);
|
|
74
|
+
}
|
|
75
|
+
#annoDlgBody .prd-md h3 {
|
|
76
|
+
font-size: 0.875rem;
|
|
77
|
+
font-weight: 600;
|
|
78
|
+
margin: 0.65rem 0 0.35rem;
|
|
79
|
+
color: rgb(30 41 59);
|
|
80
|
+
}
|
|
81
|
+
#annoDlgBody .prd-md ul {
|
|
82
|
+
margin: 0.35rem 0 0.35rem 1.1rem;
|
|
83
|
+
list-style: disc;
|
|
84
|
+
}
|
|
85
|
+
#annoDlgBody .prd-md p {
|
|
86
|
+
margin: 0.35rem 0;
|
|
87
|
+
}
|
|
88
|
+
#annoDlgBody .prd-md code {
|
|
89
|
+
font-size: 0.7rem;
|
|
90
|
+
border-radius: 0.25rem;
|
|
91
|
+
background: rgb(241 245 249);
|
|
92
|
+
padding: 0.1rem 0.3rem;
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
95
|
+
</head>
|
|
96
|
+
<body class="min-h-screen bg-slate-100 text-slate-800 antialiased">
|
|
97
|
+
<div
|
|
98
|
+
id="prdLoadBanner"
|
|
99
|
+
class="hidden border-b border-amber-300 bg-amber-100 px-4 py-2 text-center text-xs text-amber-950"
|
|
100
|
+
role="status"
|
|
101
|
+
></div>
|
|
102
|
+
<div
|
|
103
|
+
id="toast"
|
|
104
|
+
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"
|
|
105
|
+
role="status"
|
|
106
|
+
></div>
|
|
107
|
+
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
id="btnAnnoLegend"
|
|
111
|
+
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 focus:outline-none focus:ring-2 focus:ring-amber-500 focus:ring-offset-2"
|
|
112
|
+
title="需求标注说明"
|
|
113
|
+
>
|
|
114
|
+
<span
|
|
115
|
+
class="flex h-6 w-6 items-center justify-center rounded-full bg-amber-500 text-xs font-bold text-white"
|
|
116
|
+
aria-hidden="true"
|
|
117
|
+
>?</span
|
|
118
|
+
>
|
|
119
|
+
标注说明
|
|
120
|
+
</button>
|
|
121
|
+
|
|
122
|
+
<header class="border-b border-slate-200 bg-white">
|
|
123
|
+
<div class="mx-auto flex max-w-6xl items-center px-4 py-4">
|
|
124
|
+
<div class="flex items-start gap-2">
|
|
125
|
+
<div>
|
|
126
|
+
<h1 class="text-lg font-semibold text-slate-900">用户管理</h1>
|
|
127
|
+
<p class="mt-0.5 text-xs text-slate-500">原型 · 与 prd.md 对齐</p>
|
|
128
|
+
</div>
|
|
129
|
+
<button
|
|
130
|
+
type="button"
|
|
131
|
+
class="anno-trigger mt-0.5 shrink-0"
|
|
132
|
+
data-anno-key="s1"
|
|
133
|
+
aria-label="查看 PRD 标注:引言与菜单"
|
|
134
|
+
>
|
|
135
|
+
PRD §1
|
|
136
|
+
</button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</header>
|
|
140
|
+
|
|
141
|
+
<main class="mx-auto max-w-6xl px-4 py-6">
|
|
142
|
+
<!-- §3.1 筛选 -->
|
|
143
|
+
<section
|
|
144
|
+
class="mb-6 rounded-xl border border-slate-200 bg-white p-4 shadow-sm"
|
|
145
|
+
aria-labelledby="filter-title"
|
|
146
|
+
>
|
|
147
|
+
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
|
|
148
|
+
<h2 id="filter-title" class="text-sm font-semibold text-slate-900">筛选条件</h2>
|
|
149
|
+
<button
|
|
150
|
+
type="button"
|
|
151
|
+
class="anno-trigger"
|
|
152
|
+
data-anno-key="s31"
|
|
153
|
+
aria-label="查看 PRD 标注:筛选查询区"
|
|
154
|
+
>
|
|
155
|
+
PRD §3.1
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
159
|
+
<label class="block text-xs font-medium text-slate-600"
|
|
160
|
+
>用户名
|
|
161
|
+
<input
|
|
162
|
+
type="text"
|
|
163
|
+
id="fUsername"
|
|
164
|
+
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"
|
|
165
|
+
placeholder="模糊匹配"
|
|
166
|
+
autocomplete="off"
|
|
167
|
+
/>
|
|
168
|
+
</label>
|
|
169
|
+
<label class="block text-xs font-medium text-slate-600"
|
|
170
|
+
>姓名
|
|
171
|
+
<input
|
|
172
|
+
type="text"
|
|
173
|
+
id="fName"
|
|
174
|
+
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"
|
|
175
|
+
placeholder="模糊匹配"
|
|
176
|
+
autocomplete="off"
|
|
177
|
+
/>
|
|
178
|
+
</label>
|
|
179
|
+
<label class="block text-xs font-medium text-slate-600"
|
|
180
|
+
>手机号
|
|
181
|
+
<input
|
|
182
|
+
type="text"
|
|
183
|
+
id="fPhone"
|
|
184
|
+
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"
|
|
185
|
+
placeholder="精准"
|
|
186
|
+
inputmode="numeric"
|
|
187
|
+
autocomplete="off"
|
|
188
|
+
/>
|
|
189
|
+
</label>
|
|
190
|
+
<label class="block text-xs font-medium text-slate-600"
|
|
191
|
+
>用户状态
|
|
192
|
+
<select
|
|
193
|
+
id="fStatus"
|
|
194
|
+
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"
|
|
195
|
+
>
|
|
196
|
+
<option value="">全部</option>
|
|
197
|
+
<option value="启用">启用</option>
|
|
198
|
+
<option value="禁用">禁用</option>
|
|
199
|
+
</select>
|
|
200
|
+
</label>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="mt-4 flex flex-wrap gap-2">
|
|
203
|
+
<button
|
|
204
|
+
type="button"
|
|
205
|
+
id="btnQuery"
|
|
206
|
+
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
207
|
+
>
|
|
208
|
+
查询
|
|
209
|
+
</button>
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
id="btnReset"
|
|
213
|
+
class="rounded-lg border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2"
|
|
214
|
+
>
|
|
215
|
+
重置
|
|
216
|
+
</button>
|
|
217
|
+
</div>
|
|
218
|
+
</section>
|
|
219
|
+
|
|
220
|
+
<!-- §3.2 列表 -->
|
|
221
|
+
<section
|
|
222
|
+
class="overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm"
|
|
223
|
+
aria-labelledby="table-title"
|
|
224
|
+
>
|
|
225
|
+
<div class="flex flex-wrap items-center justify-between gap-2 border-b border-slate-100 px-4 py-3">
|
|
226
|
+
<div class="flex items-center gap-2">
|
|
227
|
+
<h2 id="table-title" class="text-sm font-semibold text-slate-900">用户列表</h2>
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
class="anno-trigger"
|
|
231
|
+
data-anno-key="s32"
|
|
232
|
+
aria-label="查看 PRD 标注:数据列表"
|
|
233
|
+
>
|
|
234
|
+
PRD §3.2
|
|
235
|
+
</button>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
238
|
+
<button
|
|
239
|
+
type="button"
|
|
240
|
+
class="anno-trigger"
|
|
241
|
+
data-anno-key="s2_toolbar"
|
|
242
|
+
aria-label="查看 PRD 标注:工具栏与新增"
|
|
243
|
+
>
|
|
244
|
+
PRD §2·§3.1
|
|
245
|
+
</button>
|
|
246
|
+
<button
|
|
247
|
+
type="button"
|
|
248
|
+
id="btnAdd"
|
|
249
|
+
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
250
|
+
>
|
|
251
|
+
新增用户
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="overflow-x-auto">
|
|
256
|
+
<table class="min-w-full divide-y divide-slate-200 text-left text-sm">
|
|
257
|
+
<thead class="bg-slate-50 text-xs font-medium uppercase tracking-wide text-slate-600">
|
|
258
|
+
<tr>
|
|
259
|
+
<th class="px-4 py-3">用户名</th>
|
|
260
|
+
<th class="px-4 py-3">姓名</th>
|
|
261
|
+
<th class="px-4 py-3">手机号</th>
|
|
262
|
+
<th class="px-4 py-3">部门</th>
|
|
263
|
+
<th class="px-4 py-3">状态</th>
|
|
264
|
+
<th class="px-4 py-3">创建时间</th>
|
|
265
|
+
<th class="px-4 py-3 text-right">操作</th>
|
|
266
|
+
</tr>
|
|
267
|
+
</thead>
|
|
268
|
+
<tbody id="tbody" class="divide-y divide-slate-100 bg-white"></tbody>
|
|
269
|
+
</table>
|
|
270
|
+
</div>
|
|
271
|
+
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-slate-100 px-4 py-3">
|
|
272
|
+
<div class="flex flex-wrap items-center gap-4 text-xs text-slate-600">
|
|
273
|
+
<span id="totalHint" class="shrink-0 text-slate-500"></span>
|
|
274
|
+
<span>每页</span>
|
|
275
|
+
<select id="pageSize" class="rounded border border-slate-300 px-2 py-1 text-sm">
|
|
276
|
+
<option value="5">5</option>
|
|
277
|
+
<option value="10" selected>10</option>
|
|
278
|
+
<option value="20">20</option>
|
|
279
|
+
</select>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="flex items-center gap-2">
|
|
282
|
+
<button
|
|
283
|
+
type="button"
|
|
284
|
+
id="btnPrev"
|
|
285
|
+
class="rounded border border-slate-300 px-3 py-1.5 text-xs font-medium hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
286
|
+
>
|
|
287
|
+
上一页
|
|
288
|
+
</button>
|
|
289
|
+
<span id="pageInfo" class="text-xs text-slate-600"></span>
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
id="btnNext"
|
|
293
|
+
class="rounded border border-slate-300 px-3 py-1.5 text-xs font-medium hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
294
|
+
>
|
|
295
|
+
下一页
|
|
296
|
+
</button>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
</section>
|
|
300
|
+
</main>
|
|
301
|
+
|
|
302
|
+
<!-- §4 详情 -->
|
|
303
|
+
<div
|
|
304
|
+
id="modalDetail"
|
|
305
|
+
class="fixed inset-0 z-40 hidden items-center justify-center bg-black/40 p-4"
|
|
306
|
+
role="dialog"
|
|
307
|
+
aria-modal="true"
|
|
308
|
+
aria-labelledby="detailTitle"
|
|
309
|
+
>
|
|
310
|
+
<div
|
|
311
|
+
class="max-h-[90vh] w-full max-w-md overflow-y-auto rounded-xl bg-white p-6 shadow-xl"
|
|
312
|
+
onclick="event.stopPropagation()"
|
|
313
|
+
>
|
|
314
|
+
<div class="flex flex-wrap items-start justify-between gap-2">
|
|
315
|
+
<h3 id="detailTitle" class="text-base font-semibold text-slate-900">用户详情</h3>
|
|
316
|
+
<button
|
|
317
|
+
type="button"
|
|
318
|
+
class="anno-trigger shrink-0"
|
|
319
|
+
data-anno-key="s4"
|
|
320
|
+
aria-label="查看 PRD 标注:用户详情"
|
|
321
|
+
>
|
|
322
|
+
PRD §4
|
|
323
|
+
</button>
|
|
324
|
+
</div>
|
|
325
|
+
<dl id="detailBody" class="mt-4 space-y-3 text-sm"></dl>
|
|
326
|
+
<div class="mt-6 flex justify-end">
|
|
327
|
+
<button
|
|
328
|
+
type="button"
|
|
329
|
+
class="btn-close-detail rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400"
|
|
330
|
+
>
|
|
331
|
+
关闭
|
|
332
|
+
</button>
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
<!-- §5 新增/编辑 -->
|
|
338
|
+
<div
|
|
339
|
+
id="modalForm"
|
|
340
|
+
class="fixed inset-0 z-40 hidden items-center justify-center bg-black/40 p-4"
|
|
341
|
+
role="dialog"
|
|
342
|
+
aria-modal="true"
|
|
343
|
+
aria-labelledby="formTitle"
|
|
344
|
+
>
|
|
345
|
+
<div
|
|
346
|
+
class="max-h-[90vh] w-full max-w-lg overflow-y-auto rounded-xl bg-white p-6 shadow-xl"
|
|
347
|
+
onclick="event.stopPropagation()"
|
|
348
|
+
>
|
|
349
|
+
<div class="flex flex-wrap items-start justify-between gap-2">
|
|
350
|
+
<h3 id="formTitle" class="text-base font-semibold text-slate-900">新增用户</h3>
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
class="anno-trigger shrink-0"
|
|
354
|
+
data-anno-key="s5"
|
|
355
|
+
aria-label="查看 PRD 标注:新增编辑表单"
|
|
356
|
+
>
|
|
357
|
+
PRD §5
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
<form id="userForm" class="mt-4 space-y-4">
|
|
361
|
+
<input type="hidden" id="formId" />
|
|
362
|
+
<div>
|
|
363
|
+
<label class="text-xs font-medium text-slate-600"
|
|
364
|
+
>用户名 <span class="text-red-500">*</span></label
|
|
365
|
+
>
|
|
366
|
+
<input
|
|
367
|
+
name="username"
|
|
368
|
+
id="formUsername"
|
|
369
|
+
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"
|
|
370
|
+
autocomplete="off"
|
|
371
|
+
/>
|
|
372
|
+
<p id="errUsername" class="mt-1 hidden text-xs text-red-600"></p>
|
|
373
|
+
</div>
|
|
374
|
+
<div>
|
|
375
|
+
<label class="text-xs font-medium text-slate-600"
|
|
376
|
+
>姓名 <span class="text-red-500">*</span></label
|
|
377
|
+
>
|
|
378
|
+
<input
|
|
379
|
+
name="name"
|
|
380
|
+
id="formName"
|
|
381
|
+
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"
|
|
382
|
+
autocomplete="off"
|
|
383
|
+
/>
|
|
384
|
+
<p id="errName" class="mt-1 hidden text-xs text-red-600"></p>
|
|
385
|
+
</div>
|
|
386
|
+
<div>
|
|
387
|
+
<label class="text-xs font-medium text-slate-600"
|
|
388
|
+
>手机号 <span class="text-red-500">*</span></label
|
|
389
|
+
>
|
|
390
|
+
<input
|
|
391
|
+
name="phone"
|
|
392
|
+
id="formPhone"
|
|
393
|
+
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"
|
|
394
|
+
inputmode="numeric"
|
|
395
|
+
autocomplete="off"
|
|
396
|
+
/>
|
|
397
|
+
<p id="errPhone" class="mt-1 hidden text-xs text-red-600"></p>
|
|
398
|
+
</div>
|
|
399
|
+
<div>
|
|
400
|
+
<label class="text-xs font-medium text-slate-600">邮箱</label>
|
|
401
|
+
<input
|
|
402
|
+
name="email"
|
|
403
|
+
id="formEmail"
|
|
404
|
+
type="email"
|
|
405
|
+
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"
|
|
406
|
+
autocomplete="off"
|
|
407
|
+
/>
|
|
408
|
+
<p id="errEmail" class="mt-1 hidden text-xs text-red-600"></p>
|
|
409
|
+
</div>
|
|
410
|
+
<div>
|
|
411
|
+
<label class="text-xs font-medium text-slate-600"
|
|
412
|
+
>部门 <span class="text-red-500">*</span></label
|
|
413
|
+
>
|
|
414
|
+
<select
|
|
415
|
+
id="formDept"
|
|
416
|
+
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"
|
|
417
|
+
>
|
|
418
|
+
<option value="">请选择</option>
|
|
419
|
+
<option value="研发中心">研发中心</option>
|
|
420
|
+
<option value="运营中心">运营中心</option>
|
|
421
|
+
<option value="财务部">财务部</option>
|
|
422
|
+
</select>
|
|
423
|
+
<p id="errDept" class="mt-1 hidden text-xs text-red-600"></p>
|
|
424
|
+
</div>
|
|
425
|
+
<fieldset>
|
|
426
|
+
<legend class="text-xs font-medium text-slate-600">
|
|
427
|
+
角色 <span class="text-red-500">*</span>(至少选一项)
|
|
428
|
+
</legend>
|
|
429
|
+
<div class="mt-2 flex flex-wrap gap-3 text-sm">
|
|
430
|
+
<label class="inline-flex items-center gap-2"
|
|
431
|
+
><input
|
|
432
|
+
type="checkbox"
|
|
433
|
+
name="role"
|
|
434
|
+
value="管理员"
|
|
435
|
+
class="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
|
436
|
+
/>
|
|
437
|
+
管理员</label
|
|
438
|
+
>
|
|
439
|
+
<label class="inline-flex items-center gap-2"
|
|
440
|
+
><input
|
|
441
|
+
type="checkbox"
|
|
442
|
+
name="role"
|
|
443
|
+
value="开发"
|
|
444
|
+
class="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
|
445
|
+
/>
|
|
446
|
+
开发</label
|
|
447
|
+
>
|
|
448
|
+
<label class="inline-flex items-center gap-2"
|
|
449
|
+
><input
|
|
450
|
+
type="checkbox"
|
|
451
|
+
name="role"
|
|
452
|
+
value="运营"
|
|
453
|
+
class="rounded border-slate-300 text-blue-600 focus:ring-blue-500"
|
|
454
|
+
/>
|
|
455
|
+
运营</label
|
|
456
|
+
>
|
|
457
|
+
</div>
|
|
458
|
+
<p id="errRoles" class="mt-1 hidden text-xs text-red-600"></p>
|
|
459
|
+
</fieldset>
|
|
460
|
+
<div>
|
|
461
|
+
<label class="text-xs font-medium text-slate-600">用户状态</label>
|
|
462
|
+
<div class="mt-2 flex gap-4 text-sm">
|
|
463
|
+
<label class="inline-flex items-center gap-2"
|
|
464
|
+
><input
|
|
465
|
+
type="radio"
|
|
466
|
+
name="status"
|
|
467
|
+
value="启用"
|
|
468
|
+
checked
|
|
469
|
+
class="text-blue-600 focus:ring-blue-500"
|
|
470
|
+
/>
|
|
471
|
+
启用</label
|
|
472
|
+
>
|
|
473
|
+
<label class="inline-flex items-center gap-2"
|
|
474
|
+
><input
|
|
475
|
+
type="radio"
|
|
476
|
+
name="status"
|
|
477
|
+
value="禁用"
|
|
478
|
+
class="text-blue-600 focus:ring-blue-500"
|
|
479
|
+
/>
|
|
480
|
+
禁用</label
|
|
481
|
+
>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
<div class="flex justify-end gap-2 border-t border-slate-100 pt-4">
|
|
485
|
+
<button
|
|
486
|
+
type="button"
|
|
487
|
+
id="btnFormCancel"
|
|
488
|
+
class="rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400"
|
|
489
|
+
>
|
|
490
|
+
取消
|
|
491
|
+
</button>
|
|
492
|
+
<button
|
|
493
|
+
type="submit"
|
|
494
|
+
class="rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
|
495
|
+
>
|
|
496
|
+
保存
|
|
497
|
+
</button>
|
|
498
|
+
</div>
|
|
499
|
+
</form>
|
|
500
|
+
</div>
|
|
501
|
+
</div>
|
|
502
|
+
|
|
503
|
+
<!-- §6 删除确认 -->
|
|
504
|
+
<div
|
|
505
|
+
id="modalDelete"
|
|
506
|
+
class="fixed inset-0 z-50 hidden items-center justify-center bg-black/40 p-4"
|
|
507
|
+
role="dialog"
|
|
508
|
+
aria-modal="true"
|
|
509
|
+
aria-labelledby="delTitle"
|
|
510
|
+
>
|
|
511
|
+
<div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-xl" onclick="event.stopPropagation()">
|
|
512
|
+
<div class="flex flex-wrap items-start justify-between gap-2">
|
|
513
|
+
<h3 id="delTitle" class="text-base font-semibold text-slate-900">确认删除</h3>
|
|
514
|
+
<button
|
|
515
|
+
type="button"
|
|
516
|
+
class="anno-trigger shrink-0"
|
|
517
|
+
data-anno-key="s6"
|
|
518
|
+
aria-label="查看 PRD 标注:删除"
|
|
519
|
+
>
|
|
520
|
+
PRD §6
|
|
521
|
+
</button>
|
|
522
|
+
</div>
|
|
523
|
+
<p id="delMsg" class="mt-2 text-sm text-slate-600"></p>
|
|
524
|
+
<div class="mt-6 flex justify-end gap-2">
|
|
525
|
+
<button
|
|
526
|
+
type="button"
|
|
527
|
+
id="btnDelCancel"
|
|
528
|
+
class="rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400"
|
|
529
|
+
>
|
|
530
|
+
取消
|
|
531
|
+
</button>
|
|
532
|
+
<button
|
|
533
|
+
type="button"
|
|
534
|
+
id="btnDelOk"
|
|
535
|
+
class="rounded-lg bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
|
536
|
+
>
|
|
537
|
+
删除
|
|
538
|
+
</button>
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
<!-- 关闭表单二次确认(页面内弹层,非浏览器原生 confirm) -->
|
|
544
|
+
<div
|
|
545
|
+
id="modalFormLeave"
|
|
546
|
+
class="fixed inset-0 z-[55] hidden items-center justify-center bg-black/45 p-4"
|
|
547
|
+
role="dialog"
|
|
548
|
+
aria-modal="true"
|
|
549
|
+
aria-labelledby="formLeaveTitle"
|
|
550
|
+
>
|
|
551
|
+
<div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-xl" onclick="event.stopPropagation()">
|
|
552
|
+
<h3 id="formLeaveTitle" class="text-base font-semibold text-slate-900">关闭表单</h3>
|
|
553
|
+
<p class="mt-2 text-sm text-slate-600">确定关闭表单?未保存的修改将丢失。</p>
|
|
554
|
+
<div class="mt-6 flex justify-end gap-2">
|
|
555
|
+
<button
|
|
556
|
+
type="button"
|
|
557
|
+
id="btnFormLeaveCancel"
|
|
558
|
+
class="rounded-lg border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 focus:outline-none focus:ring-2 focus:ring-slate-400"
|
|
559
|
+
>
|
|
560
|
+
返回编辑
|
|
561
|
+
</button>
|
|
562
|
+
<button
|
|
563
|
+
type="button"
|
|
564
|
+
id="btnFormLeaveOk"
|
|
565
|
+
class="rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm font-medium text-red-700 hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2"
|
|
566
|
+
>
|
|
567
|
+
确定离开
|
|
568
|
+
</button>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
</div>
|
|
572
|
+
|
|
573
|
+
<!-- PRD 需求标注弹框(走查用,非业务功能;z-index 高于业务弹层) -->
|
|
574
|
+
<div
|
|
575
|
+
id="modalAnno"
|
|
576
|
+
class="fixed inset-0 z-[70] hidden items-center justify-center bg-slate-900/55 p-4 backdrop-blur-[1px]"
|
|
577
|
+
role="dialog"
|
|
578
|
+
aria-modal="true"
|
|
579
|
+
aria-labelledby="annoDlgTitle"
|
|
580
|
+
>
|
|
581
|
+
<div
|
|
582
|
+
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 ring-4 ring-amber-300/30"
|
|
583
|
+
role="document"
|
|
584
|
+
>
|
|
585
|
+
<div
|
|
586
|
+
class="flex shrink-0 items-center justify-between border-b border-amber-200 bg-amber-50 px-4 py-2"
|
|
587
|
+
>
|
|
588
|
+
<div class="min-w-0 pr-2">
|
|
589
|
+
<p class="text-[10px] font-bold uppercase tracking-wider text-amber-900">
|
|
590
|
+
需求标注 · 非业务弹窗
|
|
591
|
+
</p>
|
|
592
|
+
<h3 id="annoDlgTitle" class="mt-0.5 truncate text-sm font-semibold text-slate-900"></h3>
|
|
593
|
+
</div>
|
|
594
|
+
<button
|
|
595
|
+
type="button"
|
|
596
|
+
id="btnAnnoClose"
|
|
597
|
+
class="shrink-0 rounded-md p-1.5 text-slate-600 hover:bg-amber-200/60 hover:text-slate-900 focus:outline-none focus:ring-2 focus:ring-amber-600"
|
|
598
|
+
aria-label="关闭标注"
|
|
599
|
+
>
|
|
600
|
+
✕
|
|
601
|
+
</button>
|
|
602
|
+
</div>
|
|
603
|
+
<div
|
|
604
|
+
id="annoDlgBody"
|
|
605
|
+
class="min-h-0 flex-1 overflow-y-auto overflow-x-auto p-4 text-sm leading-relaxed text-slate-700"
|
|
606
|
+
tabindex="-1"
|
|
607
|
+
></div>
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
|
|
611
|
+
<script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script>
|
|
612
|
+
<script>
|
|
613
|
+
function runPrototype(prdMd, prdLoadError) {
|
|
614
|
+
const MOCK = [
|
|
615
|
+
{
|
|
616
|
+
id: '10001',
|
|
617
|
+
username: 'zhangsan',
|
|
618
|
+
name: '张三',
|
|
619
|
+
phone: '13812345678',
|
|
620
|
+
dept: '研发中心',
|
|
621
|
+
status: '启用',
|
|
622
|
+
email: 'zs@example.com',
|
|
623
|
+
roles: ['管理员', '开发'],
|
|
624
|
+
createdAt: '2025-01-10 09:00',
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
id: '10002',
|
|
628
|
+
username: 'lisi',
|
|
629
|
+
name: '李四',
|
|
630
|
+
phone: '13987654321',
|
|
631
|
+
dept: '运营中心',
|
|
632
|
+
status: '启用',
|
|
633
|
+
email: 'ls@example.com',
|
|
634
|
+
roles: ['运营'],
|
|
635
|
+
createdAt: '2025-01-12 14:20',
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
id: '10003',
|
|
639
|
+
username: 'wangwu',
|
|
640
|
+
name: '王五',
|
|
641
|
+
phone: '13600001111',
|
|
642
|
+
dept: '财务部',
|
|
643
|
+
status: '禁用',
|
|
644
|
+
email: '',
|
|
645
|
+
roles: ['管理员'],
|
|
646
|
+
createdAt: '2025-02-01 11:05',
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
id: '10004',
|
|
650
|
+
username: 'zhaoliu',
|
|
651
|
+
name: '赵六',
|
|
652
|
+
phone: '13522223333',
|
|
653
|
+
dept: '研发中心',
|
|
654
|
+
status: '启用',
|
|
655
|
+
email: 'zl@example.com',
|
|
656
|
+
roles: ['开发'],
|
|
657
|
+
createdAt: '2025-02-15 16:40',
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
id: '10005',
|
|
661
|
+
username: 'sunqi',
|
|
662
|
+
name: '孙七',
|
|
663
|
+
phone: '18800009999',
|
|
664
|
+
dept: '运营中心',
|
|
665
|
+
status: '启用',
|
|
666
|
+
email: 'sq@example.com',
|
|
667
|
+
roles: ['运营', '开发'],
|
|
668
|
+
createdAt: '2025-03-02 08:30',
|
|
669
|
+
},
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
let allUsers = MOCK.map(function (u) {
|
|
673
|
+
return Object.assign({}, u, { roles: u.roles.slice() });
|
|
674
|
+
});
|
|
675
|
+
let applied = { username: '', name: '', phone: '', status: '' };
|
|
676
|
+
let page = 1;
|
|
677
|
+
let pageSize = 10;
|
|
678
|
+
let deleteTarget = null;
|
|
679
|
+
let nextId = 10006;
|
|
680
|
+
/** 打开表单时的快照 JSON,用于判断是否有未保存修改 */
|
|
681
|
+
var formSnapshotJson = null;
|
|
682
|
+
|
|
683
|
+
const $ = function (id) {
|
|
684
|
+
return document.getElementById(id);
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
function escapeHtml(s) {
|
|
688
|
+
return String(s)
|
|
689
|
+
.replace(/&/g, '&')
|
|
690
|
+
.replace(/</g, '<')
|
|
691
|
+
.replace(/>/g, '>')
|
|
692
|
+
.replace(/"/g, '"');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (typeof marked !== 'undefined' && marked.use) {
|
|
696
|
+
marked.use({ gfm: true, breaks: true });
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
(function showPrdBanner() {
|
|
700
|
+
var el = $('prdLoadBanner');
|
|
701
|
+
if (!el) return;
|
|
702
|
+
if (prdLoadError || !prdMd) {
|
|
703
|
+
el.textContent =
|
|
704
|
+
'未加载到同目录 prd.md:需求标注仅显示兜底摘要。请在本文件所在目录启动本地静态服务后刷新页面(勿直接 file:// 双击打开)。';
|
|
705
|
+
el.classList.remove('hidden');
|
|
706
|
+
}
|
|
707
|
+
})();
|
|
708
|
+
|
|
709
|
+
function sliceBetween(md, startRe, endRe) {
|
|
710
|
+
if (!md) return '';
|
|
711
|
+
var m = md.match(startRe);
|
|
712
|
+
if (!m || m.index === undefined) return '';
|
|
713
|
+
var i0 = m.index;
|
|
714
|
+
if (!endRe) return md.slice(i0).trim();
|
|
715
|
+
var tail = md.slice(i0 + m[0].length);
|
|
716
|
+
var j = tail.search(endRe);
|
|
717
|
+
if (j === -1) return md.slice(i0).trim();
|
|
718
|
+
return md.slice(i0, i0 + m[0].length + j).trim();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function mdToHtml(src) {
|
|
722
|
+
if (!src)
|
|
723
|
+
return '<p class="text-slate-500 text-xs">(在 prd.md 中未匹配到对应章节,请检查标题层级是否与 ANNO_EXTRACT 正则一致。)</p>';
|
|
724
|
+
if (typeof marked !== 'undefined' && typeof marked.parse === 'function') {
|
|
725
|
+
return '<div class="prd-md">' + marked.parse(src) + '</div>';
|
|
726
|
+
}
|
|
727
|
+
return (
|
|
728
|
+
'<pre class="max-h-[60vh] overflow-auto whitespace-pre-wrap rounded border border-slate-200 bg-slate-50 p-3 font-mono text-[11px] leading-snug">' +
|
|
729
|
+
escapeHtml(src) +
|
|
730
|
+
'</pre>'
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
var LEGEND_INTRO =
|
|
735
|
+
'<ul class="list-disc space-y-2 pl-4 text-slate-700"><li>琥珀色顶栏弹框为 <strong>PRD 走查标注</strong>,正文由<strong>同目录 prd.md</strong>按章节切片后经 Markdown 渲染得到;修改 prd.md 后<strong>刷新页面</strong>即可同步。</li><li>表格或章节较长时,可在本弹框内<strong>上下左右滚动</strong>查看。</li><li>按 <kbd class="rounded border border-slate-300 bg-slate-50 px-1 text-xs">Esc</kbd>、点遮罩或 ✕ 关闭;本层 z-index 高于业务弹窗。</li></ul>';
|
|
736
|
+
|
|
737
|
+
var ANNO_EXTRACT = {
|
|
738
|
+
s1: { start: /^## 1\./m, end: /^## 2\./m, title: 'prd.md §1 引言(原文)' },
|
|
739
|
+
s2_toolbar: {
|
|
740
|
+
start: /^## 2\./m,
|
|
741
|
+
end: /^### 3\.2/m,
|
|
742
|
+
title: 'prd.md §2·§3.1 原文(菜单权限至 §3.2 之前)',
|
|
743
|
+
},
|
|
744
|
+
s31: { start: /^### 3\.1/m, end: /^### 3\.2/m, title: 'prd.md §3.1 筛选查询区(原文)' },
|
|
745
|
+
s32: { start: /^### 3\.2/m, end: /^## 4\./m, title: 'prd.md §3.2 数据列表(原文)' },
|
|
746
|
+
s4: { start: /^## 4\./m, end: /^## 5\./m, title: 'prd.md §4 用户详情(原文)' },
|
|
747
|
+
s5: { start: /^## 5\./m, end: /^## 6\./m, title: 'prd.md §5 新增/编辑表单(原文)' },
|
|
748
|
+
s6: { start: /^## 6\./m, end: /^## 7\./m, title: 'prd.md §6 删除与审计(原文)' },
|
|
749
|
+
};
|
|
750
|
+
|
|
751
|
+
var ANNO_FALLBACK = {
|
|
752
|
+
s1: {
|
|
753
|
+
title: 'prd.md §1 引言(兜底)',
|
|
754
|
+
html: '<ul class="list-disc space-y-2 pl-4"><li>定位:后台用户账号统一管理(查、增、改、删)。</li><li>功能清单见 PRD §1.2;本原型用 mock 数据演示主路径。</li><li>请用 HTTP 打开同目录以加载 prd.md 全文联动。</li></ul>',
|
|
755
|
+
},
|
|
756
|
+
s2_toolbar: {
|
|
757
|
+
title: 'prd.md §2·§3.1(兜底)',
|
|
758
|
+
html: '<ul class="list-disc space-y-2 pl-4"><li><strong>新增用户</strong>需「编辑」类权限(PRD §2.2)。</li><li>「新增用户」位于列表标题行右侧。</li></ul>',
|
|
759
|
+
},
|
|
760
|
+
s31: {
|
|
761
|
+
title: 'prd.md §3.1(兜底)',
|
|
762
|
+
html: '<ul class="list-disc space-y-2 pl-4"><li>变更条件后须点击<strong>查询</strong>才刷新列表。</li><li><strong>重置</strong>恢复默认。</li></ul>',
|
|
763
|
+
},
|
|
764
|
+
s32: {
|
|
765
|
+
title: 'prd.md §3.2(兜底)',
|
|
766
|
+
html: '<ul class="list-disc space-y-2 pl-4"><li>列字段、分页以 PRD 表格为准。</li><li>行操作:查看 / 编辑 / 删除。</li></ul>',
|
|
767
|
+
},
|
|
768
|
+
s4: {
|
|
769
|
+
title: 'prd.md §4(兜底)',
|
|
770
|
+
html: '<ul class="list-disc space-y-2 pl-4"><li>只读详情弹层;字段见 PRD §4。</li></ul>',
|
|
771
|
+
},
|
|
772
|
+
s5: {
|
|
773
|
+
title: 'prd.md §5(兜底)',
|
|
774
|
+
html: '<ul class="list-disc space-y-2 pl-4"><li>新增与编辑校验见 PRD §5 表格。</li></ul>',
|
|
775
|
+
},
|
|
776
|
+
s6: {
|
|
777
|
+
title: 'prd.md §6(兜底)',
|
|
778
|
+
html: '<ul class="list-disc space-y-2 pl-4"><li>删除须二次确认;建议审计日志。</li></ul>',
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
function openAnno(key) {
|
|
783
|
+
var warn =
|
|
784
|
+
!prdMd || prdLoadError
|
|
785
|
+
? '<div class="mb-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900"><strong>prd.md 未联动:</strong>请用本地静态服务打开与本文件同级目录(勿 file://),以下为兜底摘要。</div>'
|
|
786
|
+
: '';
|
|
787
|
+
|
|
788
|
+
if (key === 'legend') {
|
|
789
|
+
$('annoDlgTitle').textContent = '需求标注说明(附录 · prd.md §9)';
|
|
790
|
+
var s9 = prdMd ? sliceBetween(prdMd, /^## 9\./m, /^## 10\./m) : '';
|
|
791
|
+
$('annoDlgBody').innerHTML =
|
|
792
|
+
warn +
|
|
793
|
+
LEGEND_INTRO +
|
|
794
|
+
(s9
|
|
795
|
+
? '<div class="mt-4 border-t border-amber-100 pt-3"><p class="mb-2 text-xs font-semibold text-slate-700">prd.md · §9 原型与 PRD 对应关系(原文)</p>' +
|
|
796
|
+
mdToHtml(s9) +
|
|
797
|
+
'</div>'
|
|
798
|
+
: '');
|
|
799
|
+
$('modalAnno').classList.remove('hidden');
|
|
800
|
+
$('modalAnno').classList.add('flex');
|
|
801
|
+
$('annoDlgBody').scrollTop = 0;
|
|
802
|
+
$('annoDlgBody').focus({ preventScroll: true });
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
var spec = ANNO_EXTRACT[key];
|
|
807
|
+
if (!spec) return;
|
|
808
|
+
var slice = prdMd ? sliceBetween(prdMd, spec.start, spec.end) : '';
|
|
809
|
+
if (slice) {
|
|
810
|
+
$('annoDlgTitle').textContent = spec.title;
|
|
811
|
+
$('annoDlgBody').innerHTML = warn + mdToHtml(slice);
|
|
812
|
+
} else if (ANNO_FALLBACK[key]) {
|
|
813
|
+
$('annoDlgTitle').textContent = ANNO_FALLBACK[key].title;
|
|
814
|
+
$('annoDlgBody').innerHTML = warn + ANNO_FALLBACK[key].html;
|
|
815
|
+
} else {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
$('modalAnno').classList.remove('hidden');
|
|
819
|
+
$('modalAnno').classList.add('flex');
|
|
820
|
+
$('annoDlgBody').scrollTop = 0;
|
|
821
|
+
$('annoDlgBody').focus({ preventScroll: true });
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function closeAnno() {
|
|
825
|
+
$('modalAnno').classList.add('hidden');
|
|
826
|
+
$('modalAnno').classList.remove('flex');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function isAnnoOpen() {
|
|
830
|
+
return $('modalAnno') && !$('modalAnno').classList.contains('hidden');
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function getFormStateJson() {
|
|
834
|
+
var roles = [];
|
|
835
|
+
document.querySelectorAll('input[name="role"]:checked').forEach(function (c) {
|
|
836
|
+
roles.push(c.value);
|
|
837
|
+
});
|
|
838
|
+
roles.sort();
|
|
839
|
+
var statusEl = document.querySelector('input[name="status"]:checked');
|
|
840
|
+
return JSON.stringify({
|
|
841
|
+
username: $('formUsername').value.trim(),
|
|
842
|
+
name: $('formName').value.trim(),
|
|
843
|
+
phone: $('formPhone').value.trim(),
|
|
844
|
+
email: $('formEmail').value.trim(),
|
|
845
|
+
dept: $('formDept').value,
|
|
846
|
+
roles: roles,
|
|
847
|
+
status: statusEl ? statusEl.value : '启用',
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function captureFormSnapshot() {
|
|
852
|
+
formSnapshotJson = getFormStateJson();
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function isFormDirty() {
|
|
856
|
+
return formSnapshotJson !== null && getFormStateJson() !== formSnapshotJson;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function isFormModalOpen() {
|
|
860
|
+
return $('modalForm') && !$('modalForm').classList.contains('hidden');
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
function isFormLeaveModalOpen() {
|
|
864
|
+
return $('modalFormLeave') && !$('modalFormLeave').classList.contains('hidden');
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function openFormLeaveConfirm() {
|
|
868
|
+
$('modalFormLeave').classList.remove('hidden');
|
|
869
|
+
$('modalFormLeave').classList.add('flex');
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function closeFormLeaveConfirm() {
|
|
873
|
+
$('modalFormLeave').classList.add('hidden');
|
|
874
|
+
$('modalFormLeave').classList.remove('flex');
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
document.addEventListener(
|
|
878
|
+
'click',
|
|
879
|
+
function (e) {
|
|
880
|
+
var t = e.target.closest('[data-anno-key]');
|
|
881
|
+
if (!t) return;
|
|
882
|
+
var key = t.getAttribute('data-anno-key');
|
|
883
|
+
if (!key) return;
|
|
884
|
+
e.preventDefault();
|
|
885
|
+
e.stopPropagation();
|
|
886
|
+
openAnno(key);
|
|
887
|
+
},
|
|
888
|
+
true,
|
|
889
|
+
);
|
|
890
|
+
|
|
891
|
+
$('btnAnnoClose').addEventListener('click', closeAnno);
|
|
892
|
+
$('modalAnno').addEventListener('click', function (e) {
|
|
893
|
+
if (e.target === $('modalAnno')) closeAnno();
|
|
894
|
+
});
|
|
895
|
+
$('btnAnnoLegend').addEventListener('click', function () {
|
|
896
|
+
openAnno('legend');
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
function toast(msg) {
|
|
900
|
+
var el = $('toast');
|
|
901
|
+
el.textContent = msg;
|
|
902
|
+
el.classList.remove('hidden');
|
|
903
|
+
clearTimeout(el._t);
|
|
904
|
+
el._t = setTimeout(function () {
|
|
905
|
+
el.classList.add('hidden');
|
|
906
|
+
}, 2600);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function maskPhone(p) {
|
|
910
|
+
if (!p || p.length < 7) return p;
|
|
911
|
+
return p.slice(0, 3) + '****' + p.slice(-4);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function getFiltered() {
|
|
915
|
+
return allUsers.filter(function (u) {
|
|
916
|
+
if (applied.username && u.username.indexOf(applied.username.trim()) === -1) return false;
|
|
917
|
+
if (applied.name && u.name.indexOf(applied.name.trim()) === -1) return false;
|
|
918
|
+
if (applied.phone && u.phone !== applied.phone.trim()) return false;
|
|
919
|
+
if (applied.status && u.status !== applied.status) return false;
|
|
920
|
+
return true;
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function renderTable() {
|
|
925
|
+
var list = getFiltered();
|
|
926
|
+
var total = list.length;
|
|
927
|
+
var ps = parseInt($('pageSize').value, 10);
|
|
928
|
+
pageSize = ps;
|
|
929
|
+
var pages = Math.max(1, Math.ceil(total / pageSize));
|
|
930
|
+
if (page > pages) page = pages;
|
|
931
|
+
var start = (page - 1) * pageSize;
|
|
932
|
+
var slice = list.slice(start, start + pageSize);
|
|
933
|
+
|
|
934
|
+
$('totalHint').textContent = '共 ' + total + ' 条';
|
|
935
|
+
$('pageInfo').textContent = '第 ' + page + ' / ' + pages + ' 页';
|
|
936
|
+
|
|
937
|
+
var tb = $('tbody');
|
|
938
|
+
tb.innerHTML = '';
|
|
939
|
+
if (slice.length === 0) {
|
|
940
|
+
var tr = document.createElement('tr');
|
|
941
|
+
tr.innerHTML =
|
|
942
|
+
'<td colspan="7" class="px-4 py-12 text-center text-sm text-slate-500">暂无数据</td>';
|
|
943
|
+
tb.appendChild(tr);
|
|
944
|
+
return;
|
|
945
|
+
}
|
|
946
|
+
slice.forEach(function (u) {
|
|
947
|
+
var tr = document.createElement('tr');
|
|
948
|
+
tr.className = 'hover:bg-slate-50';
|
|
949
|
+
var stCls = u.status === '启用' ? 'text-emerald-600' : 'text-slate-400';
|
|
950
|
+
tr.innerHTML =
|
|
951
|
+
'<td class="whitespace-nowrap px-4 py-3 font-mono text-xs">' +
|
|
952
|
+
escapeHtml(u.username) +
|
|
953
|
+
'</td>' +
|
|
954
|
+
'<td class="whitespace-nowrap px-4 py-3">' +
|
|
955
|
+
escapeHtml(u.name) +
|
|
956
|
+
'</td>' +
|
|
957
|
+
'<td class="whitespace-nowrap px-4 py-3 font-mono text-xs">' +
|
|
958
|
+
escapeHtml(maskPhone(u.phone)) +
|
|
959
|
+
'</td>' +
|
|
960
|
+
'<td class="whitespace-nowrap px-4 py-3">' +
|
|
961
|
+
escapeHtml(u.dept) +
|
|
962
|
+
'</td>' +
|
|
963
|
+
'<td class="whitespace-nowrap px-4 py-3 ' +
|
|
964
|
+
stCls +
|
|
965
|
+
'">' +
|
|
966
|
+
escapeHtml(u.status) +
|
|
967
|
+
'</td>' +
|
|
968
|
+
'<td class="whitespace-nowrap px-4 py-3 text-xs text-slate-500">' +
|
|
969
|
+
escapeHtml(u.createdAt) +
|
|
970
|
+
'</td>' +
|
|
971
|
+
'<td class="whitespace-nowrap px-4 py-3 text-right text-xs">' +
|
|
972
|
+
'<button type="button" data-a="view" data-id="' +
|
|
973
|
+
u.id +
|
|
974
|
+
'" class="mr-2 text-blue-600 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 rounded px-1">查看</button>' +
|
|
975
|
+
'<button type="button" data-a="edit" data-id="' +
|
|
976
|
+
u.id +
|
|
977
|
+
'" class="mr-2 text-blue-600 hover:underline focus:outline-none focus:ring-2 focus:ring-blue-500 rounded px-1">编辑</button>' +
|
|
978
|
+
'<button type="button" data-a="del" data-id="' +
|
|
979
|
+
u.id +
|
|
980
|
+
'" class="text-red-600 hover:underline focus:outline-none focus:ring-2 focus:ring-red-500 rounded px-1">删除</button>' +
|
|
981
|
+
'</td>';
|
|
982
|
+
tb.appendChild(tr);
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function openDetail(u) {
|
|
987
|
+
var body = $('detailBody');
|
|
988
|
+
body.innerHTML =
|
|
989
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">用户 ID</dt><dd class="font-mono text-slate-900">' +
|
|
990
|
+
escapeHtml(u.id) +
|
|
991
|
+
'</dd></div>' +
|
|
992
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">用户名</dt><dd>' +
|
|
993
|
+
escapeHtml(u.username) +
|
|
994
|
+
'</dd></div>' +
|
|
995
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">姓名</dt><dd>' +
|
|
996
|
+
escapeHtml(u.name) +
|
|
997
|
+
'</dd></div>' +
|
|
998
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">手机号</dt><dd class="font-mono">' +
|
|
999
|
+
escapeHtml(u.phone) +
|
|
1000
|
+
'</dd></div>' +
|
|
1001
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">邮箱</dt><dd>' +
|
|
1002
|
+
escapeHtml(u.email || '—') +
|
|
1003
|
+
'</dd></div>' +
|
|
1004
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">部门</dt><dd>' +
|
|
1005
|
+
escapeHtml(u.dept) +
|
|
1006
|
+
'</dd></div>' +
|
|
1007
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">角色</dt><dd>' +
|
|
1008
|
+
escapeHtml(u.roles.join('、')) +
|
|
1009
|
+
'</dd></div>' +
|
|
1010
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">状态</dt><dd>' +
|
|
1011
|
+
escapeHtml(u.status) +
|
|
1012
|
+
'</dd></div>' +
|
|
1013
|
+
'<div class="flex justify-between gap-4"><dt class="text-slate-500">创建时间</dt><dd class="text-xs">' +
|
|
1014
|
+
escapeHtml(u.createdAt) +
|
|
1015
|
+
'</dd></div>';
|
|
1016
|
+
$('modalDetail').classList.remove('hidden');
|
|
1017
|
+
$('modalDetail').classList.add('flex');
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function closeDetail() {
|
|
1021
|
+
$('modalDetail').classList.add('hidden');
|
|
1022
|
+
$('modalDetail').classList.remove('flex');
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function clearFormErrors() {
|
|
1026
|
+
['errUsername', 'errName', 'errPhone', 'errEmail', 'errDept', 'errRoles'].forEach(function (id) {
|
|
1027
|
+
var el = $(id);
|
|
1028
|
+
el.classList.add('hidden');
|
|
1029
|
+
el.textContent = '';
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function openFormAdd() {
|
|
1034
|
+
clearFormErrors();
|
|
1035
|
+
$('formTitle').textContent = '新增用户';
|
|
1036
|
+
$('formId').value = '';
|
|
1037
|
+
$('formUsername').value = '';
|
|
1038
|
+
$('formUsername').disabled = false;
|
|
1039
|
+
$('formName').value = '';
|
|
1040
|
+
$('formPhone').value = '';
|
|
1041
|
+
$('formEmail').value = '';
|
|
1042
|
+
$('formDept').value = '';
|
|
1043
|
+
document.querySelectorAll('input[name="role"]').forEach(function (c) {
|
|
1044
|
+
c.checked = false;
|
|
1045
|
+
});
|
|
1046
|
+
document.querySelector('input[name="status"][value="启用"]').checked = true;
|
|
1047
|
+
$('modalForm').classList.remove('hidden');
|
|
1048
|
+
$('modalForm').classList.add('flex');
|
|
1049
|
+
$('formUsername').focus();
|
|
1050
|
+
captureFormSnapshot();
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function openFormEdit(u) {
|
|
1054
|
+
clearFormErrors();
|
|
1055
|
+
$('formTitle').textContent = '编辑用户';
|
|
1056
|
+
$('formId').value = u.id;
|
|
1057
|
+
$('formUsername').value = u.username;
|
|
1058
|
+
$('formUsername').disabled = true;
|
|
1059
|
+
$('formName').value = u.name;
|
|
1060
|
+
$('formPhone').value = u.phone;
|
|
1061
|
+
$('formEmail').value = u.email || '';
|
|
1062
|
+
$('formDept').value = u.dept;
|
|
1063
|
+
document.querySelectorAll('input[name="role"]').forEach(function (c) {
|
|
1064
|
+
c.checked = u.roles.indexOf(c.value) !== -1;
|
|
1065
|
+
});
|
|
1066
|
+
document.querySelectorAll('input[name="status"]').forEach(function (r) {
|
|
1067
|
+
r.checked = r.value === u.status;
|
|
1068
|
+
});
|
|
1069
|
+
$('modalForm').classList.remove('hidden');
|
|
1070
|
+
$('modalForm').classList.add('flex');
|
|
1071
|
+
$('formName').focus();
|
|
1072
|
+
captureFormSnapshot();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function closeForm() {
|
|
1076
|
+
closeFormLeaveConfirm();
|
|
1077
|
+
formSnapshotJson = null;
|
|
1078
|
+
$('modalForm').classList.add('hidden');
|
|
1079
|
+
$('modalForm').classList.remove('flex');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function validateForm() {
|
|
1083
|
+
clearFormErrors();
|
|
1084
|
+
var ok = true;
|
|
1085
|
+
var username = $('formUsername').value.trim();
|
|
1086
|
+
var isEdit = !!$('formId').value;
|
|
1087
|
+
if (!isEdit) {
|
|
1088
|
+
if (!username) {
|
|
1089
|
+
showErr('errUsername', '请输入用户名');
|
|
1090
|
+
ok = false;
|
|
1091
|
+
} else if (!/^[a-zA-Z0-9_]{4,64}$/.test(username)) {
|
|
1092
|
+
showErr('errUsername', '4~64 位字母数字下划线');
|
|
1093
|
+
ok = false;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
var name = $('formName').value.trim();
|
|
1097
|
+
if (!name) {
|
|
1098
|
+
showErr('errName', '请输入姓名');
|
|
1099
|
+
ok = false;
|
|
1100
|
+
} else if (name.length > 64) {
|
|
1101
|
+
showErr('errName', '姓名过长');
|
|
1102
|
+
ok = false;
|
|
1103
|
+
}
|
|
1104
|
+
var phone = $('formPhone').value.trim();
|
|
1105
|
+
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
|
1106
|
+
showErr('errPhone', '请输入合法手机号');
|
|
1107
|
+
ok = false;
|
|
1108
|
+
}
|
|
1109
|
+
var email = $('formEmail').value.trim();
|
|
1110
|
+
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
1111
|
+
showErr('errEmail', '邮箱格式不正确');
|
|
1112
|
+
ok = false;
|
|
1113
|
+
}
|
|
1114
|
+
if (!$('formDept').value) {
|
|
1115
|
+
showErr('errDept', '请选择部门');
|
|
1116
|
+
ok = false;
|
|
1117
|
+
}
|
|
1118
|
+
var roles = [];
|
|
1119
|
+
document.querySelectorAll('input[name="role"]:checked').forEach(function (c) {
|
|
1120
|
+
roles.push(c.value);
|
|
1121
|
+
});
|
|
1122
|
+
if (roles.length === 0) {
|
|
1123
|
+
showErr('errRoles', '至少选择一个角色');
|
|
1124
|
+
ok = false;
|
|
1125
|
+
}
|
|
1126
|
+
if (!isEdit) {
|
|
1127
|
+
var dup = allUsers.some(function (x) {
|
|
1128
|
+
return x.username === username;
|
|
1129
|
+
});
|
|
1130
|
+
if (dup) {
|
|
1131
|
+
showErr('errUsername', '用户名已存在');
|
|
1132
|
+
ok = false;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
var dupPhone = allUsers.some(function (x) {
|
|
1136
|
+
if (isEdit && x.id === $('formId').value) return false;
|
|
1137
|
+
return x.phone === phone;
|
|
1138
|
+
});
|
|
1139
|
+
if (dupPhone) {
|
|
1140
|
+
showErr('errPhone', '手机号已被占用');
|
|
1141
|
+
ok = false;
|
|
1142
|
+
}
|
|
1143
|
+
return ok;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function showErr(id, msg) {
|
|
1147
|
+
var el = $(id);
|
|
1148
|
+
el.textContent = msg;
|
|
1149
|
+
el.classList.remove('hidden');
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
$('btnQuery').addEventListener('click', function () {
|
|
1153
|
+
applied = {
|
|
1154
|
+
username: $('fUsername').value.trim(),
|
|
1155
|
+
name: $('fName').value.trim(),
|
|
1156
|
+
phone: $('fPhone').value.trim(),
|
|
1157
|
+
status: $('fStatus').value,
|
|
1158
|
+
};
|
|
1159
|
+
page = 1;
|
|
1160
|
+
renderTable();
|
|
1161
|
+
toast('已按条件查询(前端 mock 过滤)');
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
$('btnReset').addEventListener('click', function () {
|
|
1165
|
+
$('fUsername').value = '';
|
|
1166
|
+
$('fName').value = '';
|
|
1167
|
+
$('fPhone').value = '';
|
|
1168
|
+
$('fStatus').value = '';
|
|
1169
|
+
applied = { username: '', name: '', phone: '', status: '' };
|
|
1170
|
+
page = 1;
|
|
1171
|
+
renderTable();
|
|
1172
|
+
toast('已重置筛选');
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
$('pageSize').addEventListener('change', function () {
|
|
1176
|
+
page = 1;
|
|
1177
|
+
renderTable();
|
|
1178
|
+
});
|
|
1179
|
+
$('btnPrev').addEventListener('click', function () {
|
|
1180
|
+
if (page > 1) {
|
|
1181
|
+
page--;
|
|
1182
|
+
renderTable();
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
$('btnNext').addEventListener('click', function () {
|
|
1186
|
+
var list = getFiltered();
|
|
1187
|
+
var pages = Math.max(1, Math.ceil(list.length / pageSize));
|
|
1188
|
+
if (page < pages) {
|
|
1189
|
+
page++;
|
|
1190
|
+
renderTable();
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
$('tbody').addEventListener('click', function (e) {
|
|
1195
|
+
var btn = e.target.closest('button[data-a]');
|
|
1196
|
+
if (!btn) return;
|
|
1197
|
+
var id = btn.getAttribute('data-id');
|
|
1198
|
+
var u = allUsers.find(function (x) {
|
|
1199
|
+
return x.id === id;
|
|
1200
|
+
});
|
|
1201
|
+
if (!u) return;
|
|
1202
|
+
if (btn.getAttribute('data-a') === 'view') openDetail(u);
|
|
1203
|
+
if (btn.getAttribute('data-a') === 'edit') openFormEdit(u);
|
|
1204
|
+
if (btn.getAttribute('data-a') === 'del') {
|
|
1205
|
+
deleteTarget = u;
|
|
1206
|
+
$('delMsg').textContent =
|
|
1207
|
+
'确定删除用户「' + u.name + '」吗?删除后不可恢复。(原型:仅从列表移除)';
|
|
1208
|
+
$('modalDelete').classList.remove('hidden');
|
|
1209
|
+
$('modalDelete').classList.add('flex');
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
document.querySelectorAll('.btn-close-detail').forEach(function (b) {
|
|
1214
|
+
b.addEventListener('click', closeDetail);
|
|
1215
|
+
});
|
|
1216
|
+
$('modalDetail').addEventListener('click', function (e) {
|
|
1217
|
+
if (e.target === $('modalDetail')) closeDetail();
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
$('btnAdd').addEventListener('click', openFormAdd);
|
|
1221
|
+
$('btnFormCancel').addEventListener('click', function () {
|
|
1222
|
+
if (!isFormDirty()) {
|
|
1223
|
+
closeForm();
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
openFormLeaveConfirm();
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
$('btnFormLeaveCancel').addEventListener('click', closeFormLeaveConfirm);
|
|
1230
|
+
$('btnFormLeaveOk').addEventListener('click', function () {
|
|
1231
|
+
closeFormLeaveConfirm();
|
|
1232
|
+
closeForm();
|
|
1233
|
+
});
|
|
1234
|
+
$('modalFormLeave').addEventListener('click', function (e) {
|
|
1235
|
+
if (e.target === $('modalFormLeave')) closeFormLeaveConfirm();
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
$('userForm').addEventListener('submit', function (e) {
|
|
1239
|
+
e.preventDefault();
|
|
1240
|
+
if (!validateForm()) return;
|
|
1241
|
+
var id = $('formId').value;
|
|
1242
|
+
var status = document.querySelector('input[name="status"]:checked').value;
|
|
1243
|
+
var roles = [];
|
|
1244
|
+
document.querySelectorAll('input[name="role"]:checked').forEach(function (c) {
|
|
1245
|
+
roles.push(c.value);
|
|
1246
|
+
});
|
|
1247
|
+
if (id) {
|
|
1248
|
+
var u = allUsers.find(function (x) {
|
|
1249
|
+
return x.id === id;
|
|
1250
|
+
});
|
|
1251
|
+
if (u) {
|
|
1252
|
+
u.name = $('formName').value.trim();
|
|
1253
|
+
u.phone = $('formPhone').value.trim();
|
|
1254
|
+
u.email = $('formEmail').value.trim();
|
|
1255
|
+
u.dept = $('formDept').value;
|
|
1256
|
+
u.roles = roles;
|
|
1257
|
+
u.status = status;
|
|
1258
|
+
}
|
|
1259
|
+
toast('保存成功(模拟):已更新用户');
|
|
1260
|
+
} else {
|
|
1261
|
+
var nu = {
|
|
1262
|
+
id: String(nextId++),
|
|
1263
|
+
username: $('formUsername').value.trim(),
|
|
1264
|
+
name: $('formName').value.trim(),
|
|
1265
|
+
phone: $('formPhone').value.trim(),
|
|
1266
|
+
email: $('formEmail').value.trim(),
|
|
1267
|
+
dept: $('formDept').value,
|
|
1268
|
+
roles: roles,
|
|
1269
|
+
status: status,
|
|
1270
|
+
createdAt: new Date().toISOString().slice(0, 16).replace('T', ' '),
|
|
1271
|
+
};
|
|
1272
|
+
allUsers.push(nu);
|
|
1273
|
+
toast('创建成功(模拟):已加入列表');
|
|
1274
|
+
}
|
|
1275
|
+
closeForm();
|
|
1276
|
+
renderTable();
|
|
1277
|
+
});
|
|
1278
|
+
|
|
1279
|
+
$('btnDelCancel').addEventListener('click', function () {
|
|
1280
|
+
$('modalDelete').classList.add('hidden');
|
|
1281
|
+
$('modalDelete').classList.remove('flex');
|
|
1282
|
+
deleteTarget = null;
|
|
1283
|
+
});
|
|
1284
|
+
$('btnDelOk').addEventListener('click', function () {
|
|
1285
|
+
if (!deleteTarget) return;
|
|
1286
|
+
allUsers = allUsers.filter(function (x) {
|
|
1287
|
+
return x.id !== deleteTarget.id;
|
|
1288
|
+
});
|
|
1289
|
+
$('modalDelete').classList.add('hidden');
|
|
1290
|
+
$('modalDelete').classList.remove('flex');
|
|
1291
|
+
deleteTarget = null;
|
|
1292
|
+
renderTable();
|
|
1293
|
+
toast('已删除(模拟)');
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
document.addEventListener('keydown', function (e) {
|
|
1297
|
+
if (e.key !== 'Escape') return;
|
|
1298
|
+
if (isAnnoOpen()) {
|
|
1299
|
+
closeAnno();
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (isFormLeaveModalOpen()) {
|
|
1303
|
+
closeFormLeaveConfirm();
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
var delOpen = $('modalDelete') && !$('modalDelete').classList.contains('hidden');
|
|
1307
|
+
if (delOpen) {
|
|
1308
|
+
$('modalDelete').classList.add('hidden');
|
|
1309
|
+
$('modalDelete').classList.remove('flex');
|
|
1310
|
+
deleteTarget = null;
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (isFormModalOpen()) {
|
|
1314
|
+
if (isFormDirty()) openFormLeaveConfirm();
|
|
1315
|
+
else closeForm();
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if ($('modalDetail') && !$('modalDetail').classList.contains('hidden')) {
|
|
1319
|
+
closeDetail();
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
renderTable();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
fetch('prd.md', { cache: 'no-store' })
|
|
1327
|
+
.then(function (r) {
|
|
1328
|
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
1329
|
+
return r.text();
|
|
1330
|
+
})
|
|
1331
|
+
.then(function (text) {
|
|
1332
|
+
runPrototype(text, null);
|
|
1333
|
+
})
|
|
1334
|
+
.catch(function () {
|
|
1335
|
+
runPrototype('', new Error('fetch'));
|
|
1336
|
+
});
|
|
1337
|
+
</script>
|
|
1338
|
+
</body>
|
|
1339
|
+
</html>
|