node-resource-utils 0.0.0-stage → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -2
  3. package/lib/sync.js +121 -0
  4. package/package.json +14 -4
  5. package/resources/.cache/ref.b64 +1 -0
  6. package/resources/README.md +97 -0
  7. package/resources/_.env +6 -0
  8. package/resources/_.env.example +25 -0
  9. package/resources/_.gitignore +18 -0
  10. package/resources/_.lintstagedrc.json +9 -0
  11. package/resources/_.npmrc +5 -0
  12. package/resources/_.prettierrc +7 -0
  13. package/resources/commitlint.config.mjs +1 -0
  14. package/resources/content/docs/README.md +63 -0
  15. package/resources/content/docs/faq.md +107 -0
  16. package/resources/content/docs/features/_meta.json +5 -0
  17. package/resources/content/docs/features/ai.md +71 -0
  18. package/resources/content/docs/features/appearance.md +47 -0
  19. package/resources/content/docs/features/feedback.md +79 -0
  20. package/resources/content/docs/features/files.md +74 -0
  21. package/resources/content/docs/features/graph.md +66 -0
  22. package/resources/content/docs/features/inbox.md +61 -0
  23. package/resources/content/docs/guide/_meta.json +5 -0
  24. package/resources/content/docs/guide/account.md +76 -0
  25. package/resources/content/docs/guide/data.md +87 -0
  26. package/resources/content/docs/guide/navigation.md +81 -0
  27. package/resources/content/docs/guide/permissions.md +67 -0
  28. package/resources/content/docs/guide/system.md +92 -0
  29. package/resources/content/docs/quick-start.md +70 -0
  30. package/resources/content/docs/shortcuts.md +69 -0
  31. package/resources/eslint.config.mjs +17 -0
  32. package/resources/next-env.d.ts +6 -0
  33. package/resources/next.config.ts +13 -0
  34. package/resources/postcss.config.mjs +7 -0
  35. package/resources/prisma/org.config.ts +98 -0
  36. package/resources/prisma/schema.prisma +336 -0
  37. package/resources/prisma/seed.ts +721 -0
  38. package/resources/prisma/seedSafe.ts +347 -0
  39. package/resources/prisma.config.ts +14 -0
  40. package/resources/scripts/check-config.mjs +773 -0
  41. package/resources/scripts/lib/schema-graph.mjs +410 -0
  42. package/resources/scripts/patch-next-trace.mjs +86 -0
  43. package/resources/scripts/suggest-relations.mjs +105 -0
  44. package/resources/src/app/(admin)/ai/page.tsx +469 -0
  45. package/resources/src/app/(admin)/dashboard/page.tsx +260 -0
  46. package/resources/src/app/(admin)/demo/employees/page.tsx +508 -0
  47. package/resources/src/app/(admin)/demo/files/page.tsx +451 -0
  48. package/resources/src/app/(admin)/demo/loading/page.tsx +513 -0
  49. package/resources/src/app/(admin)/error.tsx +96 -0
  50. package/resources/src/app/(admin)/feedback/page.tsx +573 -0
  51. package/resources/src/app/(admin)/graph/page.tsx +1224 -0
  52. package/resources/src/app/(admin)/inbox/page.tsx +31 -0
  53. package/resources/src/app/(admin)/layout.tsx +58 -0
  54. package/resources/src/app/(admin)/loading.tsx +31 -0
  55. package/resources/src/app/(admin)/system/depts/page.tsx +415 -0
  56. package/resources/src/app/(admin)/system/logs/page.tsx +215 -0
  57. package/resources/src/app/(admin)/system/menus/page.tsx +403 -0
  58. package/resources/src/app/(admin)/system/roles/page.tsx +433 -0
  59. package/resources/src/app/(admin)/system/users/page.tsx +620 -0
  60. package/resources/src/app/api/account-recovery/route.ts +132 -0
  61. package/resources/src/app/api/ai/act/route.ts +70 -0
  62. package/resources/src/app/api/ai/ask/route.ts +82 -0
  63. package/resources/src/app/api/ai/chat/route.ts +404 -0
  64. package/resources/src/app/api/ai/report/route.ts +164 -0
  65. package/resources/src/app/api/auth/[...nextauth]/route.ts +3 -0
  66. package/resources/src/app/api/auth/change-password/route.ts +57 -0
  67. package/resources/src/app/api/auth/forgot/route.ts +98 -0
  68. package/resources/src/app/api/auth/register/route.ts +95 -0
  69. package/resources/src/app/api/auth/register-config/route.ts +51 -0
  70. package/resources/src/app/api/badges/route.ts +39 -0
  71. package/resources/src/app/api/depts/[id]/route.ts +145 -0
  72. package/resources/src/app/api/depts/route.ts +126 -0
  73. package/resources/src/app/api/docs/export/route.ts +87 -0
  74. package/resources/src/app/api/docs/parse/route.ts +57 -0
  75. package/resources/src/app/api/docs/search/route.ts +22 -0
  76. package/resources/src/app/api/employees/[id]/route.ts +147 -0
  77. package/resources/src/app/api/employees/export/route.ts +115 -0
  78. package/resources/src/app/api/employees/import/route.ts +205 -0
  79. package/resources/src/app/api/employees/route.ts +149 -0
  80. package/resources/src/app/api/export/route.ts +254 -0
  81. package/resources/src/app/api/favorites/route.ts +56 -0
  82. package/resources/src/app/api/feedback/ai/route.ts +31 -0
  83. package/resources/src/app/api/feedback/approve/route.ts +67 -0
  84. package/resources/src/app/api/feedback/report/route.ts +213 -0
  85. package/resources/src/app/api/feedback/route.ts +246 -0
  86. package/resources/src/app/api/files/[name]/route.ts +82 -0
  87. package/resources/src/app/api/graph/focus/route.ts +75 -0
  88. package/resources/src/app/api/graph/node/move/route.ts +48 -0
  89. package/resources/src/app/api/graph/node/route.ts +274 -0
  90. package/resources/src/app/api/graph/route.ts +272 -0
  91. package/resources/src/app/api/health/route.ts +11 -0
  92. package/resources/src/app/api/inbox/read/route.ts +36 -0
  93. package/resources/src/app/api/inbox/summary/route.ts +15 -0
  94. package/resources/src/app/api/logs/route.ts +82 -0
  95. package/resources/src/app/api/menus/[id]/route.ts +132 -0
  96. package/resources/src/app/api/menus/mine/route.ts +23 -0
  97. package/resources/src/app/api/menus/route.ts +89 -0
  98. package/resources/src/app/api/roles/[id]/menus/route.ts +73 -0
  99. package/resources/src/app/api/roles/[id]/route.ts +96 -0
  100. package/resources/src/app/api/roles/route.ts +114 -0
  101. package/resources/src/app/api/search/route.ts +22 -0
  102. package/resources/src/app/api/stats/route.ts +92 -0
  103. package/resources/src/app/api/upload/route.ts +38 -0
  104. package/resources/src/app/api/users/[id]/password/route.ts +55 -0
  105. package/resources/src/app/api/users/[id]/route.ts +145 -0
  106. package/resources/src/app/api/users/route.ts +153 -0
  107. package/resources/src/app/api/visits/route.ts +48 -0
  108. package/resources/src/app/change-password/ChangePasswordForm.tsx +174 -0
  109. package/resources/src/app/change-password/page.tsx +19 -0
  110. package/resources/src/app/docs/[[...slug]]/page.tsx +56 -0
  111. package/resources/src/app/docs/layout.tsx +34 -0
  112. package/resources/src/app/docs/loading.tsx +19 -0
  113. package/resources/src/app/docs/not-found.tsx +25 -0
  114. package/resources/src/app/forgot/page.tsx +392 -0
  115. package/resources/src/app/global-error.tsx +75 -0
  116. package/resources/src/app/globals.css +235 -0
  117. package/resources/src/app/layout.tsx +44 -0
  118. package/resources/src/app/login/page.tsx +267 -0
  119. package/resources/src/app/page.tsx +14 -0
  120. package/resources/src/app/register/page.tsx +237 -0
  121. package/resources/src/auth.ts +215 -0
  122. package/resources/src/components/AiPanel.tsx +970 -0
  123. package/resources/src/components/AuthContext.tsx +82 -0
  124. package/resources/src/components/DataTable.tsx +459 -0
  125. package/resources/src/components/EChart.tsx +140 -0
  126. package/resources/src/components/FeedbackAiModal.tsx +476 -0
  127. package/resources/src/components/FeedbackReportModal.tsx +479 -0
  128. package/resources/src/components/Markdown.tsx +532 -0
  129. package/resources/src/components/OntologyGraph.tsx +1196 -0
  130. package/resources/src/components/Providers.tsx +99 -0
  131. package/resources/src/components/VisitTracker.tsx +89 -0
  132. package/resources/src/components/admin/AdminShell.tsx +749 -0
  133. package/resources/src/components/admin/CountBadge.tsx +67 -0
  134. package/resources/src/components/admin/GlobalSearch.tsx +509 -0
  135. package/resources/src/components/admin/InboxBell.tsx +78 -0
  136. package/resources/src/components/admin/VisitStats.tsx +145 -0
  137. package/resources/src/components/admin/menu-icons.tsx +56 -0
  138. package/resources/src/components/ai/AiInputForm.tsx +351 -0
  139. package/resources/src/components/appearance/AppearanceDrawer.tsx +268 -0
  140. package/resources/src/components/appearance/AppearanceProvider.tsx +160 -0
  141. package/resources/src/components/docs/DocArticle.tsx +106 -0
  142. package/resources/src/components/docs/DocToc.tsx +100 -0
  143. package/resources/src/components/docs/DocsChrome.tsx +259 -0
  144. package/resources/src/components/docs/DocsHome.tsx +209 -0
  145. package/resources/src/components/docs/DocsSearch.tsx +365 -0
  146. package/resources/src/components/docs/docs.css +1458 -0
  147. package/resources/src/components/feedback/AsyncButton.tsx +108 -0
  148. package/resources/src/components/feedback/LoadingOverlay.tsx +125 -0
  149. package/resources/src/components/feedback/PressableCard.tsx +92 -0
  150. package/resources/src/components/feedback/README.md +97 -0
  151. package/resources/src/components/feedback/RouteProgress.tsx +108 -0
  152. package/resources/src/components/feedback/Skeleton.tsx +249 -0
  153. package/resources/src/components/feedback/StatCard.tsx +70 -0
  154. package/resources/src/components/feedback/TaskHint.tsx +153 -0
  155. package/resources/src/components/feedback/index.ts +93 -0
  156. package/resources/src/components/feedback/motion.css +204 -0
  157. package/resources/src/components/feedback/motion.ts +63 -0
  158. package/resources/src/components/feedback/useCountUp.ts +110 -0
  159. package/resources/src/components/feedback/usePending.ts +209 -0
  160. package/resources/src/components/feedback/useRowFlash.ts +40 -0
  161. package/resources/src/components/inbox/InboxView.tsx +409 -0
  162. package/resources/src/components/ui/button.tsx +35 -0
  163. package/resources/src/components/ui/card.tsx +37 -0
  164. package/resources/src/components/ui/input.tsx +19 -0
  165. package/resources/src/lib/ai.ts +310 -0
  166. package/resources/src/lib/aiContext.ts +357 -0
  167. package/resources/src/lib/aiForm.test.ts +191 -0
  168. package/resources/src/lib/aiForm.ts +532 -0
  169. package/resources/src/lib/aiFormTypes.ts +83 -0
  170. package/resources/src/lib/aiToken.ts +72 -0
  171. package/resources/src/lib/aiTools.ts +823 -0
  172. package/resources/src/lib/api.ts +160 -0
  173. package/resources/src/lib/appInfo.ts +128 -0
  174. package/resources/src/lib/appearance.ts +643 -0
  175. package/resources/src/lib/badge-count.ts +146 -0
  176. package/resources/src/lib/badges.test.ts +58 -0
  177. package/resources/src/lib/badges.ts +79 -0
  178. package/resources/src/lib/brand-mark.test.ts +87 -0
  179. package/resources/src/lib/brand-mark.ts +45 -0
  180. package/resources/src/lib/docs-shared.test.ts +148 -0
  181. package/resources/src/lib/docs-shared.ts +422 -0
  182. package/resources/src/lib/docs.ts +381 -0
  183. package/resources/src/lib/err-text.ts +21 -0
  184. package/resources/src/lib/excel.ts +152 -0
  185. package/resources/src/lib/favorite-path.ts +60 -0
  186. package/resources/src/lib/favorites.ts +59 -0
  187. package/resources/src/lib/features.ts +69 -0
  188. package/resources/src/lib/feedback-access.ts +17 -0
  189. package/resources/src/lib/feedbackAi.ts +1621 -0
  190. package/resources/src/lib/formRules.ts +76 -0
  191. package/resources/src/lib/format.ts +52 -0
  192. package/resources/src/lib/graph-cascade.ts +304 -0
  193. package/resources/src/lib/graph-focus.ts +301 -0
  194. package/resources/src/lib/graph-link.ts +17 -0
  195. package/resources/src/lib/graph-node-write.ts +150 -0
  196. package/resources/src/lib/graph-schema.ts +475 -0
  197. package/resources/src/lib/graph-scope.test.ts +46 -0
  198. package/resources/src/lib/graph-scope.ts +28 -0
  199. package/resources/src/lib/http.ts +157 -0
  200. package/resources/src/lib/inbox-types.ts +57 -0
  201. package/resources/src/lib/inbox.ts +55 -0
  202. package/resources/src/lib/menu-service.ts +204 -0
  203. package/resources/src/lib/notifications.ts +113 -0
  204. package/resources/src/lib/office.ts +531 -0
  205. package/resources/src/lib/ontology.ts +277 -0
  206. package/resources/src/lib/openaiCompat.ts +257 -0
  207. package/resources/src/lib/pending.ts +98 -0
  208. package/resources/src/lib/prisma.ts +30 -0
  209. package/resources/src/lib/rbac.test.ts +106 -0
  210. package/resources/src/lib/rbac.ts +185 -0
  211. package/resources/src/lib/search-data.ts +158 -0
  212. package/resources/src/lib/security-questions.ts +20 -0
  213. package/resources/src/lib/skin-theme.ts +155 -0
  214. package/resources/src/lib/system-config.ts +55 -0
  215. package/resources/src/lib/theme.ts +96 -0
  216. package/resources/src/lib/todos/feedback.ts +86 -0
  217. package/resources/src/lib/todos/index.ts +73 -0
  218. package/resources/src/lib/todos/types.ts +33 -0
  219. package/resources/src/lib/upload.ts +106 -0
  220. package/resources/src/lib/utils.test.ts +9 -0
  221. package/resources/src/lib/utils.ts +6 -0
  222. package/resources/src/lib/validation.test.ts +297 -0
  223. package/resources/src/lib/validation.ts +658 -0
  224. package/resources/src/lib/visit-stats.ts +109 -0
  225. package/resources/src/lib/visits.ts +111 -0
  226. package/resources/src/lib/zodField.ts +44 -0
  227. package/resources/tsconfig.json +41 -0
  228. package/resources/vitest.config.ts +16 -0
  229. package/resources/vitest.setup.ts +1 -0
@@ -0,0 +1,513 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Alert, App, Button, Card, Space, Tag, Typography } from 'antd';
5
+ import {
6
+ ClockCircleOutlined,
7
+ CloseCircleOutlined,
8
+ LoadingOutlined,
9
+ ThunderboltOutlined,
10
+ } from '@ant-design/icons';
11
+ import AsyncButton from '@/components/feedback/AsyncButton';
12
+ import LoadingOverlay from '@/components/feedback/LoadingOverlay';
13
+ import {
14
+ SkeletonCards,
15
+ SkeletonDetail,
16
+ SkeletonTable,
17
+ SkeletonText,
18
+ } from '@/components/feedback/Skeleton';
19
+ import { formatElapsed, useAsyncTask } from '@/components/feedback/usePending';
20
+ import { beginTask, endTask } from '@/lib/pending';
21
+ import { fs, MILITARY } from '@/lib/theme';
22
+
23
+ const { Paragraph, Text, Title } = Typography;
24
+
25
+ /**
26
+ * 「等待与反馈」样板间。
27
+ *
28
+ * 这个页面不解决业务问题,它解决的是**比赛现场的一个固定问题**:
29
+ * 出的题不确定,但"点了一下要等几秒"这件事一定会出现。
30
+ * 所以把各种等待形态集中在这里,新题来了照抄对应的一块就行。
31
+ *
32
+ * 五类形态,覆盖从 0.2 秒到 1 分钟:
33
+ * 1. 按钮级 —— 提交、保存、导出:换成一个 AsyncButton 即可
34
+ * 2. 区域级 —— 一块卡片整体在等:套一层 LoadingOverlay
35
+ * 3. 骨架屏 —— 首次加载还不知道等多久:用 Skeleton* 占住版式
36
+ * 4. 长任务 —— 超过 8 秒的导入/生成:给秒数 + 给取消
37
+ * 5. 全局兜底 —— 页面忘了写 loading 也有右下角提示(Providers 已装好)
38
+ */
39
+
40
+ /** 可被 AbortSignal 打断的 sleep —— 演示"取消"用 */
41
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
42
+ return new Promise((resolve, reject) => {
43
+ const timer = setTimeout(resolve, ms);
44
+ signal?.addEventListener(
45
+ 'abort',
46
+ () => {
47
+ clearTimeout(timer);
48
+ reject(new DOMException('aborted', 'AbortError'));
49
+ },
50
+ { once: true }
51
+ );
52
+ });
53
+ }
54
+
55
+ function Section({
56
+ index,
57
+ title,
58
+ desc,
59
+ children,
60
+ }: {
61
+ index: string;
62
+ title: string;
63
+ desc: string;
64
+ children: React.ReactNode;
65
+ }) {
66
+ return (
67
+ <Card
68
+ variant="borderless"
69
+ style={{ boxShadow: MILITARY.shadowSm, border: `1px solid ${MILITARY.borderSoft}` }}
70
+ title={
71
+ <Space size={10}>
72
+ <span
73
+ style={{
74
+ width: 22,
75
+ height: 22,
76
+ borderRadius: 6,
77
+ background: MILITARY.soft,
78
+ border: `1px solid ${MILITARY.softStrong}`,
79
+ color: MILITARY.primary,
80
+ fontSize: fs(12),
81
+ display: 'inline-grid',
82
+ placeItems: 'center',
83
+ }}
84
+ >
85
+ {index}
86
+ </span>
87
+ <span>{title}</span>
88
+ </Space>
89
+ }
90
+ >
91
+ <Paragraph style={{ fontSize: fs(12.5), color: MILITARY.textMuted, marginBottom: 14 }}>
92
+ {desc}
93
+ </Paragraph>
94
+ {children}
95
+ </Card>
96
+ );
97
+ }
98
+
99
+ export default function LoadingDemoPage() {
100
+ const { message } = App.useApp();
101
+
102
+ // 区域遮罩演示
103
+ const [regionLoading, setRegionLoading] = useState(false);
104
+ const [regionVersion, setRegionVersion] = useState(1);
105
+
106
+ // 骨架屏演示:切换"骨架 / 真实内容"
107
+ const [showSkeleton, setShowSkeleton] = useState(true);
108
+
109
+ /**
110
+ * 长任务演示:useAsyncTask 提供 pending / elapsed / stageText / cancel。
111
+ * 自己画等待 UI 时就靠这四个值,不用再手写 setInterval。
112
+ */
113
+ const longJob = useAsyncTask(
114
+ (signal) => sleep(20000, signal),
115
+ {
116
+ label: '正在导入 1 万行数据…',
117
+ /**
118
+ * silent: true —— 页面里已经有阶段文案 + 已耗时 + 取消按钮,
119
+ * 再让右下角浮出一张同样内容的卡片就是噪音了。
120
+ * 兜底层的定位是「没写提示的页面不至于没反应」,不是给写好的页面加压。
121
+ * 第 5 节会单独演示浮层长什么样。
122
+ */
123
+ silent: true,
124
+ stages: [
125
+ { after: 0, text: '正在校验文件格式…' },
126
+ { after: 2500, text: '正在写入数据库(分批 200 行)…' },
127
+ { after: 8000, text: '数据量较大,仍在写入,可以继续等待或取消…' },
128
+ ],
129
+ onSuccess: () => message.success('导入完成'),
130
+ onCancel: () => message.info('已取消导入'),
131
+ }
132
+ );
133
+
134
+ const reloadRegion = () => {
135
+ setRegionLoading(true);
136
+ setTimeout(() => {
137
+ setRegionLoading(false);
138
+ setRegionVersion((v) => v + 1);
139
+ }, 4000);
140
+ };
141
+
142
+ /**
143
+ * 全局浮层演示:手工登记一条任务,3 秒后右下角自己浮出来。
144
+ * 真实场景里这一步由 axios 拦截器/AsyncButton 自动完成,
145
+ * 这里手写是为了让你看到"浮层长什么样、什么时候出现"。
146
+ */
147
+ const simulateGlobal = () => {
148
+ const id = beginTask('正在生成月度统计报表(8 张表汇总)…', 'task');
149
+ setTimeout(() => {
150
+ endTask(id);
151
+ message.success('报表已生成');
152
+ }, 6000);
153
+ message.info('已发起,3 秒后右下角会出现提示');
154
+ };
155
+
156
+ return (
157
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
158
+ {/* 页头:和全站其他页面同一套节奏(kicker + 标题 + 一行说明) */}
159
+ <div>
160
+ <div
161
+ style={{
162
+ fontSize: fs(10.5),
163
+ letterSpacing: 3,
164
+ color: MILITARY.textMuted,
165
+ textTransform: 'uppercase',
166
+ marginBottom: 6,
167
+ }}
168
+ >
169
+ Loading & Feedback
170
+ </div>
171
+ <Title level={4} style={{ margin: 0, color: MILITARY.text }}>
172
+ 等待与反馈样板间
173
+ </Title>
174
+ <Paragraph style={{ margin: '4px 0 0', fontSize: fs(12.5), color: MILITARY.textSecondary }}>
175
+ 新题里任何&ldquo;点一下要等几秒&rdquo;的地方,从下面五种形态里挑一种套上去即可。
176
+ 全部不引新依赖、只走军绿主题,复制走就能用。
177
+ </Paragraph>
178
+ </div>
179
+
180
+ <Alert
181
+ type="info"
182
+ showIcon
183
+ title="三个阈值决定手感"
184
+ description={
185
+ <span>
186
+ <b>200ms</b> 以内完成的操作不出现 spinner(避免闪一下);
187
+ <b> 3 秒</b>后右下角浮出全局提示;
188
+ <b> 8 秒</b>后文案升级为「仍在继续,请勿重复提交」。
189
+ 改 <Text code>lib/pending.ts</Text> 的 <Text code>WAIT</Text> 即可全站生效。
190
+ </span>
191
+ }
192
+ />
193
+
194
+ {/* ---------- 1. 按钮级 ---------- */}
195
+ <Section
196
+ index="1"
197
+ title="按钮级:提交、保存、导出"
198
+ desc="把 <Button onClick={async …}> 换成 <AsyncButton task={…}>,自动得到:防重复点击、延迟转圈、失败弹提示、超过 3 秒在按钮上显示秒数。"
199
+ >
200
+ <Space wrap size={10}>
201
+ <AsyncButton
202
+ type="primary"
203
+ task={() => sleep(600)}
204
+ successText="已完成(0.6 秒)"
205
+ >
206
+ 短任务 0.6s
207
+ </AsyncButton>
208
+
209
+ <AsyncButton task={() => sleep(2500)} successText="已完成(2.5 秒)">
210
+ 标准任务 2.5s
211
+ </AsyncButton>
212
+
213
+ <AsyncButton
214
+ type="primary"
215
+ task={(signal) => sleep(9000, signal)}
216
+ label="正在生成季度报表…"
217
+ successText="报表已生成"
218
+ >
219
+ 长任务 9s(看右下角)
220
+ </AsyncButton>
221
+
222
+ <AsyncButton
223
+ danger
224
+ task={async () => {
225
+ await sleep(1200);
226
+ throw new Error('服务端返回:当前数据已被他人修改,请刷新后重试');
227
+ }}
228
+ >
229
+ 失败会怎样
230
+ </AsyncButton>
231
+ </Space>
232
+ </Section>
233
+
234
+ {/* ---------- 2. 区域遮罩 ---------- */}
235
+ <Section
236
+ index="2"
237
+ title="区域级:一块卡片整体在等"
238
+ desc="保留旧内容 + 半透明蒙层,用户知道「还在原地」;200ms 后才出现,快请求不会闪。超过 3 秒开始显示已等待秒数。"
239
+ >
240
+ <Space style={{ marginBottom: 12 }}>
241
+ <Button type="primary" onClick={reloadRegion} loading={regionLoading}>
242
+ 重新加载这块区域(4 秒)
243
+ </Button>
244
+ <Tag icon={<ClockCircleOutlined />} color="default">
245
+ 第 {regionVersion} 次加载
246
+ </Tag>
247
+ </Space>
248
+
249
+ <LoadingOverlay
250
+ loading={regionLoading}
251
+ text="正在重新统计这批数据…"
252
+ minHeight={180}
253
+ tone="card"
254
+ >
255
+ <div
256
+ style={{
257
+ border: `1px solid ${MILITARY.borderSoft}`,
258
+ borderRadius: 10,
259
+ overflow: 'hidden',
260
+ }}
261
+ >
262
+ {[
263
+ ['设备编号', '所属单位', '状态', '更新时间'],
264
+ ].map((row) => (
265
+ <div
266
+ key={row[0]}
267
+ style={{
268
+ display: 'grid',
269
+ gridTemplateColumns: '1.2fr 1.6fr .8fr 1fr',
270
+ gap: 8,
271
+ padding: '10px 14px',
272
+ background: MILITARY.soft,
273
+ fontSize: fs(12.5),
274
+ color: MILITARY.textSecondary,
275
+ fontWeight: 600,
276
+ }}
277
+ >
278
+ {row.map((c) => (
279
+ <span key={c}>{c}</span>
280
+ ))}
281
+ </div>
282
+ ))}
283
+ {Array.from({ length: 5 }).map((_, i) => (
284
+ <div
285
+ key={i}
286
+ style={{
287
+ display: 'grid',
288
+ gridTemplateColumns: '1.2fr 1.6fr .8fr 1fr',
289
+ gap: 8,
290
+ padding: '10px 14px',
291
+ fontSize: fs(12.5),
292
+ color: MILITARY.text,
293
+ borderTop: `1px solid ${MILITARY.borderSoft}`,
294
+ }}
295
+ >
296
+ <span>EQ-2026-{String(i + 1).padStart(3, '0')}</span>
297
+ <span>保障分队 · 第 {i + 1} 组</span>
298
+ <span style={{ color: i % 2 ? MILITARY.warning : MILITARY.success }}>
299
+ {i % 2 ? '检修中' : '正常'}
300
+ </span>
301
+ <span style={{ color: MILITARY.textMuted }}>
302
+ 10-{20 + i} {9 + i}:0{i}
303
+ </span>
304
+ </div>
305
+ ))}
306
+ </div>
307
+ </LoadingOverlay>
308
+ </Section>
309
+
310
+ {/* ---------- 3. 骨架屏 ---------- */}
311
+ <Section
312
+ index="3"
313
+ title="骨架屏:首次加载还不知道等多久"
314
+ desc="比转圈多给了一层信息:等来的是表格还是表单、大概几行。内容是空的但版式先立住,数据填进来时不会整页跳一下。"
315
+ >
316
+ <Space style={{ marginBottom: 14 }}>
317
+ <Button size="small" onClick={() => setShowSkeleton((v) => !v)}>
318
+ {showSkeleton ? '换成真实内容' : '换回骨架'}
319
+ </Button>
320
+ <span style={{ fontSize: fs(12), color: MILITARY.textMuted }}>
321
+ 三种骨架对应三种版式:列表 / 统计卡 / 表单详情
322
+ </span>
323
+ </Space>
324
+
325
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
326
+ <div>
327
+ <div style={{ fontSize: fs(12), color: MILITARY.textMuted, marginBottom: 6 }}>
328
+ 列表(SkeletonTable)
329
+ </div>
330
+ {showSkeleton ? (
331
+ <SkeletonTable rows={4} cols={4} />
332
+ ) : (
333
+ <div
334
+ style={{
335
+ border: `1px solid ${MILITARY.borderSoft}`,
336
+ borderRadius: 8,
337
+ padding: 14,
338
+ fontSize: fs(12.5),
339
+ color: MILITARY.textSecondary,
340
+ }}
341
+ >
342
+ 真实表格到了 —— 高度和上面的骨架基本一致,所以没有跳动。
343
+ </div>
344
+ )}
345
+ </div>
346
+
347
+ <div>
348
+ <div style={{ fontSize: fs(12), color: MILITARY.textMuted, marginBottom: 6 }}>
349
+ 统计卡(SkeletonCards)
350
+ </div>
351
+ {showSkeleton ? (
352
+ <SkeletonCards count={4} height={76} />
353
+ ) : (
354
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16 }}>
355
+ {[
356
+ ['在编人数', '128'],
357
+ ['本月新增', '12'],
358
+ ['待处理', '7'],
359
+ ['完成率', '86%'],
360
+ ].map(([k, v]) => (
361
+ <div
362
+ key={k}
363
+ style={{
364
+ border: `1px solid ${MILITARY.borderSoft}`,
365
+ borderRadius: 10,
366
+ padding: 14,
367
+ }}
368
+ >
369
+ <div style={{ fontSize: fs(11.5), color: MILITARY.textMuted }}>{k}</div>
370
+ <div style={{ fontSize: fs(20), fontWeight: 600, color: MILITARY.primary }}>{v}</div>
371
+ </div>
372
+ ))}
373
+ </div>
374
+ )}
375
+ </div>
376
+
377
+ <div>
378
+ <div style={{ fontSize: fs(12), color: MILITARY.textMuted, marginBottom: 6 }}>
379
+ 表单详情(SkeletonDetail / SkeletonText)
380
+ </div>
381
+ {showSkeleton ? (
382
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
383
+ <SkeletonDetail rows={3} />
384
+ <SkeletonText lines={5} />
385
+ </div>
386
+ ) : (
387
+ <div style={{ fontSize: fs(12.5), color: MILITARY.textSecondary }}>
388
+ 真实字段渲染出来了,位置与上面的占位一一对应。
389
+ </div>
390
+ )}
391
+ </div>
392
+ </div>
393
+ </Section>
394
+
395
+ {/* ---------- 4. 长任务 ---------- */}
396
+ <Section
397
+ index="4"
398
+ title="长任务:超过 8 秒要有秒数,也要能取消"
399
+ desc="导入、批量生成、大模型长回答都属于这一类。关键不是「转得快」,而是「我知道它进展到哪一步、我可以停」。"
400
+ >
401
+ <Space wrap style={{ marginBottom: 12 }}>
402
+ <Button
403
+ type="primary"
404
+ icon={<ThunderboltOutlined />}
405
+ onClick={() => void longJob.run()}
406
+ disabled={longJob.pending}
407
+ >
408
+ 开始导入(20 秒)
409
+ </Button>
410
+ <Button danger disabled={!longJob.pending} onClick={longJob.cancel}>
411
+ 取消
412
+ </Button>
413
+ {longJob.pending ? (
414
+ <Tag icon={<LoadingOutlined spin />} color="processing">
415
+ 已等待 {formatElapsed(longJob.elapsed)}
416
+ </Tag>
417
+ ) : (
418
+ <Tag color="default">空闲</Tag>
419
+ )}
420
+ </Space>
421
+
422
+ {longJob.pending ? (
423
+ <div
424
+ style={{
425
+ border: `1px solid ${MILITARY.borderSoft}`,
426
+ borderRadius: 10,
427
+ padding: '12px 14px',
428
+ background: MILITARY.paper,
429
+ }}
430
+ >
431
+ <div style={{ fontSize: fs(13), color: MILITARY.text, marginBottom: 8 }}>
432
+ {longJob.stageText}
433
+ </div>
434
+ <div
435
+ style={{
436
+ height: 4,
437
+ borderRadius: 999,
438
+ background: MILITARY.soft,
439
+ overflow: 'hidden',
440
+ }}
441
+ >
442
+ <div
443
+ className="wb-indeterminate-bar"
444
+ style={{
445
+ width: '38%',
446
+ height: '100%',
447
+ background: `linear-gradient(90deg, ${MILITARY.primaryHover}, ${MILITARY.accent})`,
448
+ }}
449
+ />
450
+ </div>
451
+ </div>
452
+ ) : (
453
+ <div style={{ fontSize: fs(12.5), color: MILITARY.textMuted }}>
454
+ 点&ldquo;开始导入&rdquo;看阶段文案怎么随耗时升级(0s → 2.5s → 8s)。
455
+ </div>
456
+ )}
457
+ </Section>
458
+
459
+ {/* ---------- 5. 全局兜底 ---------- */}
460
+ <Section
461
+ index="5"
462
+ title="全局兜底:页面忘了写 loading 也不会没反应"
463
+ desc="Providers 里已经装好 axios 拦截器 + 右下角浮层。任何请求超过 3 秒,右下角自动浮出「正在处理…已等待 N 秒」,与新题里写没写 loading 无关。"
464
+ >
465
+ <Space wrap style={{ marginBottom: 12 }}>
466
+ <Button onClick={simulateGlobal} icon={<ClockCircleOutlined />}>
467
+ 模拟一个 6 秒的耗时任务
468
+ </Button>
469
+ <Button
470
+ onClick={() => {
471
+ // 真接口演示:/api/stats 很快,但链路和专业版一致
472
+ void fetch('/api/health');
473
+ message.info('这条不会触发浮层:接口太快,3 秒内就结束了(这正是想要的效果)');
474
+ }}
475
+ >
476
+ 快请求不该打扰
477
+ </Button>
478
+ </Space>
479
+
480
+ <div
481
+ style={{
482
+ border: `1px dashed ${MILITARY.border}`,
483
+ borderRadius: 10,
484
+ padding: 12,
485
+ fontSize: fs(12.5),
486
+ color: MILITARY.textSecondary,
487
+ background: MILITARY.paper,
488
+ lineHeight: 1.9,
489
+ }}
490
+ >
491
+ <div>
492
+ <CloseCircleOutlined style={{ color: MILITARY.success, marginRight: 6 }} />
493
+ 3 秒内结束 → 什么都不显示(不打扰)
494
+ </div>
495
+ <div>
496
+ <ClockCircleOutlined style={{ color: MILITARY.primary, marginRight: 6 }} />
497
+ 超过 3 秒 → 右下角淡入,带已等待秒数和不确定进度条
498
+ </div>
499
+ <div>
500
+ <LoadingOutlined style={{ color: MILITARY.warning, marginRight: 6 }} />
501
+ 超过 8 秒 → 文案变黄并提示「请勿重复提交」,同时顶部路由条也换色
502
+ </div>
503
+ </div>
504
+ </Section>
505
+
506
+ <div style={{ fontSize: fs(11.5), color: MILITARY.textMuted, padding: '0 2px 8px' }}>
507
+ 代码位置:<Text code>src/components/feedback/</Text> | 阈值:
508
+ <Text code>src/lib/pending.ts</Text> | 请求文案映射:
509
+ <Text code>src/lib/http.ts</Text>
510
+ </div>
511
+ </div>
512
+ );
513
+ }
@@ -0,0 +1,96 @@
1
+ 'use client';
2
+
3
+ import { useEffect } from 'react';
4
+ import { Button } from 'antd';
5
+ import { useRouter } from 'next/navigation';
6
+ import { fs, MILITARY } from '@/lib/theme';
7
+
8
+ /**
9
+ * 路由级错误兜底。
10
+ *
11
+ * 「耗时的另一面是会不会失败」。只有 loading 没有 error,等于告诉用户
12
+ * "等下去总会有结果"——真挂了的时候反而更慌。所以这一层必须补:
13
+ * 明确说清"没打开"、给出可能的两个原因、并且**保留重试按钮**。
14
+ *
15
+ * 注意:错误信息做脱敏 —— 生产环境只给摘要(digest),
16
+ * 开发环境才显示原始 message,避免把堆栈/路径泄漏给评委看到。
17
+ */
18
+ export default function AdminError({
19
+ error,
20
+ reset,
21
+ }: {
22
+ error: Error & { digest?: string };
23
+ reset: () => void;
24
+ }) {
25
+ const router = useRouter();
26
+ const dev = process.env.NODE_ENV !== 'production';
27
+
28
+ useEffect(() => {
29
+ console.error('[页面异常]', error);
30
+ }, [error]);
31
+
32
+ return (
33
+ <div
34
+ style={{
35
+ minHeight: 420,
36
+ display: 'flex',
37
+ flexDirection: 'column',
38
+ alignItems: 'center',
39
+ justifyContent: 'center',
40
+ gap: 14,
41
+ padding: 40,
42
+ textAlign: 'center',
43
+ }}
44
+ >
45
+ <div
46
+ aria-hidden
47
+ style={{
48
+ width: 46,
49
+ height: 46,
50
+ borderRadius: 12,
51
+ background: MILITARY.accentSoft,
52
+ border: `1px solid ${MILITARY.border}`,
53
+ display: 'flex',
54
+ alignItems: 'center',
55
+ justifyContent: 'center',
56
+ fontSize: fs(22),
57
+ color: MILITARY.warning,
58
+ }}
59
+ >
60
+ !
61
+ </div>
62
+
63
+ <div style={{ fontSize: fs(17), fontWeight: 600, color: MILITARY.text }}>
64
+ 这个页面没能打开
65
+ </div>
66
+
67
+ <div style={{ fontSize: fs(13), color: MILITARY.textSecondary, lineHeight: 1.7, maxWidth: 460 }}>
68
+ 可能是后端服务正在重启,或者这一步处理超时了。可以先重试一次;
69
+ 如果一直这样,回到工作台确认其他页面是否正常。
70
+ </div>
71
+
72
+ <div
73
+ style={{
74
+ fontSize: fs(11.5),
75
+ color: MILITARY.textMuted,
76
+ fontFamily: 'ui-monospace, Consolas, monospace',
77
+ maxWidth: 520,
78
+ wordBreak: 'break-all',
79
+ background: MILITARY.paper,
80
+ border: `1px solid ${MILITARY.borderSoft}`,
81
+ borderRadius: 8,
82
+ padding: '6px 10px',
83
+ }}
84
+ >
85
+ {dev ? error.message || '未知错误' : `错误编号 ${error.digest ?? 'unknown'}`}
86
+ </div>
87
+
88
+ <div style={{ display: 'flex', gap: 10, marginTop: 4 }}>
89
+ <Button type="primary" onClick={reset}>
90
+ 重试
91
+ </Button>
92
+ <Button onClick={() => router.push('/dashboard')}>回到工作台</Button>
93
+ </div>
94
+ </div>
95
+ );
96
+ }