@deppon/deppon-skills 2.4.15 → 2.4.18

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