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,392 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { useRouter } from 'next/navigation';
6
+ import {
7
+ Alert,
8
+ App,
9
+ Button,
10
+ Card,
11
+ Form,
12
+ Input,
13
+ Segmented,
14
+ Space,
15
+ Typography,
16
+ Upload,
17
+ } from 'antd';
18
+ import type { UploadFile } from 'antd';
19
+ import {
20
+ ArrowLeftOutlined,
21
+ IdcardOutlined,
22
+ LockOutlined,
23
+ SafetyCertificateOutlined,
24
+ UploadOutlined,
25
+ UserOutlined,
26
+ } from '@ant-design/icons';
27
+ import { APP_NAME, SHOW_DEMO_ACCOUNT_HINT } from '@/lib/appInfo';
28
+ import { DEMO_SECURITY_ANSWER, DEMO_SECURITY_QUESTION } from '@/lib/security-questions';
29
+ import { fs, MILITARY } from '@/lib/theme';
30
+ import { formRules } from '@/lib/formRules';
31
+ import { validateField } from '@/lib/validation';
32
+
33
+ const { Title, Text, Paragraph } = Typography;
34
+
35
+ type Mode = 'question' | 'appeal';
36
+
37
+ type ApiResp<T> = { code: number; data?: T; message?: string };
38
+
39
+ /**
40
+ * 忘记密码(未登录可访问)。
41
+ *
42
+ * 两条路:
43
+ * 1. 密保问题:用户名 → 取问题 → 答案校验通过后由用户自己设新密码
44
+ * 2. 人工申诉:密保也忘了,就提交账号信息 + 证明材料,超管核实后在反馈页重置
45
+ *
46
+ * 页面刻意与登录页同一套军绿视觉,避免"跳出系统"的割裂感。
47
+ */
48
+ export default function ForgotPage() {
49
+ const router = useRouter();
50
+ const { message } = App.useApp();
51
+
52
+ const [mode, setMode] = useState<Mode>('question');
53
+
54
+ // ---- 密保问题找回 ----
55
+ const [username, setUsername] = useState('');
56
+ const [question, setQuestion] = useState<string | null>(null);
57
+ const [looking, setLooking] = useState(false);
58
+ const [resetting, setResetting] = useState(false);
59
+ const [resetForm] = Form.useForm<{ answer: string; newPassword: string; confirm: string }>();
60
+
61
+ const lookup = async () => {
62
+ const checked = validateField('username', username, { label: '用户名', required: true });
63
+ if (!checked.ok) {
64
+ message.warning(checked.message);
65
+ return;
66
+ }
67
+ const name = checked.value;
68
+ setLooking(true);
69
+ setQuestion(null);
70
+ try {
71
+ const r = await fetch(`/api/auth/forgot?username=${encodeURIComponent(name)}`);
72
+ const j = (await r.json()) as ApiResp<{ username: string; question: string | null }>;
73
+ if (j.code !== 0 || !j.data) {
74
+ message.error(j.message || '查询失败');
75
+ return;
76
+ }
77
+ setUsername(j.data.username);
78
+ setQuestion(j.data.question);
79
+ if (!j.data.question) {
80
+ message.warning('该账号未设置密保问题,请改用「人工申诉」');
81
+ }
82
+ } catch (e) {
83
+ message.error(`查询失败:${String(e)}`);
84
+ } finally {
85
+ setLooking(false);
86
+ }
87
+ };
88
+
89
+ const submitReset = async (values: { answer: string; newPassword: string; confirm: string }) => {
90
+ setResetting(true);
91
+ try {
92
+ const r = await fetch('/api/auth/forgot', {
93
+ method: 'POST',
94
+ headers: { 'Content-Type': 'application/json' },
95
+ body: JSON.stringify({
96
+ username,
97
+ answer: values.answer,
98
+ newPassword: values.newPassword,
99
+ }),
100
+ });
101
+ const j = (await r.json()) as ApiResp<null>;
102
+ if (j.code !== 0) {
103
+ message.error(j.message || '重置失败');
104
+ return;
105
+ }
106
+ message.success('密码已重置,请使用新密码登录');
107
+ router.push('/login');
108
+ } catch (e) {
109
+ message.error(`重置失败:${String(e)}`);
110
+ } finally {
111
+ setResetting(false);
112
+ }
113
+ };
114
+
115
+ // ---- 人工申诉 ----
116
+ const [appealForm] = Form.useForm<{
117
+ username: string;
118
+ realName: string;
119
+ contact: string;
120
+ detail: string;
121
+ }>();
122
+ const [fileList, setFileList] = useState<UploadFile[]>([]);
123
+ const [appealing, setAppealing] = useState(false);
124
+ const [appealDone, setAppealDone] = useState(false);
125
+
126
+ const submitAppeal = async (values: {
127
+ username: string;
128
+ realName: string;
129
+ contact: string;
130
+ detail: string;
131
+ }) => {
132
+ const fd = new FormData();
133
+ fd.append('username', values.username.trim());
134
+ fd.append('realName', values.realName.trim());
135
+ fd.append('contact', values.contact.trim());
136
+ fd.append('detail', values.detail.trim());
137
+ for (const f of fileList) {
138
+ if (f.originFileObj) fd.append('files', f.originFileObj);
139
+ }
140
+
141
+ setAppealing(true);
142
+ try {
143
+ const r = await fetch('/api/account-recovery', { method: 'POST', body: fd });
144
+ const j = (await r.json()) as ApiResp<{ id: string }>;
145
+ if (j.code !== 0) {
146
+ message.error(j.message || '提交失败');
147
+ return;
148
+ }
149
+ setAppealDone(true);
150
+ setFileList([]);
151
+ appealForm.resetFields();
152
+ } catch (e) {
153
+ message.error(`提交失败:${String(e)}`);
154
+ } finally {
155
+ setAppealing(false);
156
+ }
157
+ };
158
+
159
+ return (
160
+ <div
161
+ style={{
162
+ minHeight: '100vh',
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ justifyContent: 'center',
166
+ padding: 24,
167
+ backgroundImage:
168
+ 'var(--wb-brand-bg)',
169
+ }}
170
+ >
171
+ <Card
172
+ style={{
173
+ width: '100%',
174
+ maxWidth: 560,
175
+ borderRadius: 14,
176
+ boxShadow: 'var(--wb-brand-shadow)',
177
+ }}
178
+ >
179
+ <div style={{ textAlign: 'center', marginBottom: 20 }}>
180
+ <div
181
+ style={{
182
+ width: 44,
183
+ height: 44,
184
+ borderRadius: 12,
185
+ margin: '0 auto 12px',
186
+ display: 'grid',
187
+ placeItems: 'center',
188
+ background: MILITARY.soft,
189
+ border: `1px solid ${MILITARY.softStrong}`,
190
+ color: MILITARY.primary,
191
+ }}
192
+ >
193
+ <SafetyCertificateOutlined style={{ fontSize: fs(22) }} />
194
+ </div>
195
+ <Title level={3} style={{ marginBottom: 4 }}>
196
+ 找回密码
197
+ </Title>
198
+ <Text type="secondary">{APP_NAME} · 通过密保问题或人工申诉找回账号</Text>
199
+ </div>
200
+
201
+ <Segmented
202
+ block
203
+ value={mode}
204
+ onChange={(v) => setMode(v as Mode)}
205
+ options={[
206
+ { label: '密保问题找回', value: 'question' },
207
+ { label: '人工申诉', value: 'appeal' },
208
+ ]}
209
+ style={{ marginBottom: 20 }}
210
+ />
211
+
212
+ {mode === 'question' ? (
213
+ <>
214
+ <Space.Compact style={{ width: '100%', marginBottom: 16 }}>
215
+ <Input
216
+ size="large"
217
+ prefix={<UserOutlined />}
218
+ placeholder="请输入用户名"
219
+ value={username}
220
+ onChange={(e) => {
221
+ setUsername(e.target.value);
222
+ setQuestion(null);
223
+ }}
224
+ onPressEnter={lookup}
225
+ autoComplete="username"
226
+ />
227
+ <Button size="large" loading={looking} onClick={lookup}>
228
+ 查询密保
229
+ </Button>
230
+ </Space.Compact>
231
+
232
+ {question ? (
233
+ <Form form={resetForm} layout="vertical" onFinish={submitReset} requiredMark={false}>
234
+ <Alert
235
+ type="info"
236
+ showIcon
237
+ style={{ marginBottom: 16 }}
238
+ title={`密保问题:${question}`}
239
+ />
240
+ <Form.Item
241
+ name="answer"
242
+ label="密保答案"
243
+ rules={[{ required: true, message: '请输入密保答案' }]}
244
+ >
245
+ <Input prefix={<IdcardOutlined />} placeholder="请输入注册时设置的答案" />
246
+ </Form.Item>
247
+ <Form.Item
248
+ name="newPassword"
249
+ label="新密码"
250
+ rules={formRules('password', { label: '新密码', required: true })}
251
+ >
252
+ <Input.Password
253
+ prefix={<LockOutlined />}
254
+ maxLength={64}
255
+ placeholder="6-64 位,不能含空格"
256
+ autoComplete="new-password"
257
+ />
258
+ </Form.Item>
259
+ <Form.Item
260
+ name="confirm"
261
+ label="确认新密码"
262
+ dependencies={['newPassword']}
263
+ rules={[
264
+ { required: true, message: '请再次输入新密码' },
265
+ ({ getFieldValue }) => ({
266
+ validator: (_r, value) =>
267
+ !value || getFieldValue('newPassword') === value
268
+ ? Promise.resolve()
269
+ : Promise.reject(new Error('两次输入的密码不一致')),
270
+ }),
271
+ ]}
272
+ >
273
+ <Input.Password
274
+ prefix={<LockOutlined />}
275
+ placeholder="再次输入"
276
+ autoComplete="new-password"
277
+ />
278
+ </Form.Item>
279
+ <Button type="primary" htmlType="submit" block size="large" loading={resetting}>
280
+ 重置密码
281
+ </Button>
282
+ </Form>
283
+ ) : (
284
+ <Alert
285
+ type="warning"
286
+ showIcon
287
+ title="还不知道密保问题?"
288
+ description="先输入用户名并点击「查询密保」;如果该账号未设置密保或答案也忘了,请切换到「人工申诉」提交证明材料。"
289
+ style={{ marginBottom: 8 }}
290
+ />
291
+ )}
292
+ </>
293
+ ) : appealDone ? (
294
+ <Alert
295
+ type="success"
296
+ showIcon
297
+ title="申诉已提交"
298
+ description="超级管理员会核实你提交的证明材料,处理结果可在问题反馈中查看。通过后密码将重置为 123456,首次登录需要修改密码。"
299
+ />
300
+ ) : (
301
+ <>
302
+ <Alert
303
+ type="info"
304
+ showIcon
305
+ style={{ marginBottom: 16 }}
306
+ title="申诉通过后,密码将重置为 123456"
307
+ description="请如实填写账号信息,并尽量上传工牌、身份证、入职通知等能证明账号归属的材料。首次使用 123456 登录后,系统会要求你立即修改密码。"
308
+ />
309
+ <Form
310
+ form={appealForm}
311
+ layout="vertical"
312
+ onFinish={submitAppeal}
313
+ requiredMark={false}
314
+ >
315
+ <Form.Item
316
+ name="username"
317
+ label="要找回的用户名"
318
+ rules={formRules('username', { label: '用户名', required: true })}
319
+ >
320
+ <Input
321
+ prefix={<UserOutlined />}
322
+ maxLength={32}
323
+ placeholder="登录账号"
324
+ autoComplete="username"
325
+ />
326
+ </Form.Item>
327
+ <Form.Item
328
+ name="realName"
329
+ label="真实姓名"
330
+ rules={formRules('name', { label: '真实姓名', required: true })}
331
+ >
332
+ <Input maxLength={32} placeholder="用于管理员核对身份" />
333
+ </Form.Item>
334
+ <Form.Item
335
+ name="contact"
336
+ label="联系方式"
337
+ rules={formRules('contact', { label: '联系方式', required: true })}
338
+ >
339
+ <Input maxLength={254} placeholder="手机号或邮箱,方便管理员回访确认" />
340
+ </Form.Item>
341
+ <Form.Item
342
+ name="detail"
343
+ label="情况说明"
344
+ rules={[
345
+ { required: true, message: '请说明忘记密码的情况' },
346
+ { min: 10, message: '至少 10 个字' },
347
+ ]}
348
+ >
349
+ <Input.TextArea
350
+ rows={4}
351
+ maxLength={500}
352
+ showCount
353
+ placeholder="例如:我是技术部张三,手机号换了收不到验证码,密保问题也记不清了"
354
+ />
355
+ </Form.Item>
356
+ <Form.Item
357
+ label="证明材料"
358
+ extra="支持图片 / PDF / Word / TXT,最多 3 个,每个不超过 10MB"
359
+ >
360
+ <Upload
361
+ multiple
362
+ maxCount={3}
363
+ accept=".png,.jpg,.jpeg,.webp,.gif,.pdf,.doc,.docx,.txt"
364
+ beforeUpload={() => false}
365
+ fileList={fileList}
366
+ onChange={({ fileList: next }) => setFileList(next)}
367
+ >
368
+ <Button icon={<UploadOutlined />}>选择文件</Button>
369
+ </Upload>
370
+ </Form.Item>
371
+ <Button type="primary" htmlType="submit" block size="large" loading={appealing}>
372
+ 提交申诉
373
+ </Button>
374
+ </Form>
375
+ </>
376
+ )}
377
+
378
+ {SHOW_DEMO_ACCOUNT_HINT && mode === 'question' && (
379
+ <Paragraph type="secondary" style={{ fontSize: fs(12), marginTop: 14, marginBottom: 0 }}>
380
+ 演示账号密保:问题「{DEMO_SECURITY_QUESTION}」答案「{DEMO_SECURITY_ANSWER}」
381
+ </Paragraph>
382
+ )}
383
+
384
+ <div style={{ textAlign: 'center', marginTop: 18 }}>
385
+ <Link href="/login">
386
+ <ArrowLeftOutlined /> 返回登录
387
+ </Link>
388
+ </div>
389
+ </Card>
390
+ </div>
391
+ );
392
+ }
@@ -0,0 +1,75 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * 最外层错误兜底:连根布局(Providers / antd / 主题)都挂了的时候才会到这里。
5
+ *
6
+ * 所以这里刻意**不用 antd、不用任何项目组件、不依赖 globals.css**,
7
+ * 全部内联样式 —— 能保证在任何情况下都渲染得出来。
8
+ * 样式仍然沿用军绿,不至于突然变成一张默认白页。
9
+ */
10
+ export default function GlobalError({
11
+ error,
12
+ reset,
13
+ }: {
14
+ error: Error & { digest?: string };
15
+ reset: () => void;
16
+ }) {
17
+ return (
18
+ <html lang="zh-CN">
19
+ <body
20
+ style={{
21
+ margin: 0,
22
+ minHeight: '100vh',
23
+ display: 'flex',
24
+ alignItems: 'center',
25
+ justifyContent: 'center',
26
+ background: '#F2F3EA',
27
+ color: '#202619',
28
+ fontFamily:
29
+ "system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', sans-serif",
30
+ padding: 24,
31
+ }}
32
+ >
33
+ <div style={{ maxWidth: 460, textAlign: 'center' }}>
34
+ <div style={{ fontSize: 18, fontWeight: 600, marginBottom: 10 }}>
35
+ 系统没能启动这一屏
36
+ </div>
37
+ <div style={{ fontSize: 13.5, lineHeight: 1.75, color: '#5B6252', marginBottom: 16 }}>
38
+ 页面加载过程中断了。点下面的按钮重新加载;如果反复出现,
39
+ 请确认后端服务(端口 3000)是否还在运行。
40
+ </div>
41
+ <div
42
+ style={{
43
+ fontSize: 11.5,
44
+ color: '#8B9181',
45
+ fontFamily: 'ui-monospace, Consolas, monospace',
46
+ wordBreak: 'break-all',
47
+ background: '#F8F9F3',
48
+ border: '1px solid #EEF0E5',
49
+ borderRadius: 8,
50
+ padding: '6px 10px',
51
+ marginBottom: 18,
52
+ }}
53
+ >
54
+ {error.digest ? `错误编号 ${error.digest}` : error.message || '未知错误'}
55
+ </div>
56
+ <button
57
+ type="button"
58
+ onClick={reset}
59
+ style={{
60
+ padding: '8px 20px',
61
+ fontSize: 14,
62
+ color: '#fff',
63
+ background: '#4E5F3C',
64
+ border: 'none',
65
+ borderRadius: 8,
66
+ cursor: 'pointer',
67
+ }}
68
+ >
69
+ 重新加载
70
+ </button>
71
+ </div>
72
+ </body>
73
+ </html>
74
+ );
75
+ }
@@ -0,0 +1,235 @@
1
+ @import 'tailwindcss';
2
+ /* 操作反馈层(wb-pressable / wb-flash)的视觉定义,
3
+ 跟着 components/feedback 走,不在这里重复写。 */
4
+ @import '../components/feedback/motion.css';
5
+
6
+ :root {
7
+ /* 画布与文字跟着皮肤走:--wb-* 由 lib/appearance.ts 生成(见 app/layout.tsx) */
8
+ --background: var(--wb-bg);
9
+ --foreground: var(--wb-text);
10
+ color-scheme: light;
11
+ }
12
+
13
+ @theme inline {
14
+ --color-background: var(--background);
15
+ --color-foreground: var(--foreground);
16
+ }
17
+
18
+ body {
19
+ background: var(--wb-bg);
20
+ color: var(--wb-text);
21
+ /* 基准字号也要吃 --wb-fs:没显式写 font-size 的文字(正文、说明)才跟着放大 */
22
+ font-size: calc(14px * var(--wb-fs));
23
+ font-family: system-ui, -apple-system, 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', sans-serif;
24
+ -webkit-font-smoothing: antialiased;
25
+ text-rendering: optimizeLegibility;
26
+ }
27
+
28
+ /* 侧边菜单选中项:左侧一条定位条(纯 CSS,兼容性无风险)。
29
+ 深色侧栏用点缀色(卡其/荧光绿),浅色侧栏用主色 —— 否则浅底上白条看不见。 */
30
+ .ant-menu-dark .ant-menu-item-selected,
31
+ .ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title {
32
+ box-shadow: inset 3px 0 0 var(--wb-sider-mark);
33
+ }
34
+ /* 含选中项的分组(一级)标题:文字色必须单独拉回侧栏强色。
35
+ 根因:antd 深色 Menu 把「选中项文字色」和「分组标题文字色」绑在同一个 token 上
36
+ (node_modules/antd/es/menu/style/index.js:subMenuItemSelectedColor = darkItemSelectedColor),
37
+ 而分组标题并没有选中项那块高亮底 —— 炫酷皮肤的选中项是「亮绿底 + 近黑字」,
38
+ 标题沿用同一文字色就变成「近黑字压近黑舱壁」,点开二级菜单后一级标题像消失
39
+ 了一样(2026-10-23 实测)。选中项自己的文字色不动(亮底深字对比度足够),
40
+ 这里只修标题。用 .ant-menu-root 把特异性抬到 (0,4,0),压过 antd 运行时注入的
41
+ (0,3,0) 规则,不依赖两张样式表的先后顺序。 */
42
+ .ant-menu-dark.ant-menu-root .ant-menu-submenu-selected > .ant-menu-submenu-title {
43
+ color: var(--wb-sider-text-strong);
44
+ }
45
+ .ant-menu-light .ant-menu-item-selected {
46
+ box-shadow: inset 3px 0 0 var(--wb-primary);
47
+ }
48
+
49
+ /* ---------------------------------------------------------------
50
+ 侧栏品牌标题(系统名 / 副标题)
51
+ 222px 的侧栏里,扣掉左右各 16px 内边距、26px 标识块和 10px 间距,
52
+ 留给系统名的只有 **154px** —— 15px/字距 1.2 的中文一行只放得下约 9 个字。
53
+ 以前标题既不约束宽度也不限制行数、品牌行又是固定 height:56px,于是:
54
+ · 系统名一超过 9 个字就自己折行,12 个字就折成 2 行;
55
+ · 折行后整块内容 52px 塞进 56px 固定高,副标题被顶到标题脚下,
56
+ 12~18 个字时品牌区明显挤成一团(2026-10-23 实测)。
57
+ 现在:标题最多 2 行(第 3 行起省略号,完整名字由 title 属性兜),
58
+ 行高 1.35 给足呼吸;品牌行改成 min-height + 内边距,行数变多时**自己长高**。
59
+ text-wrap: balance 让 12 个字折成 6+6,而不是 9+3 留一个孤零零的「系统」。
60
+ --------------------------------------------------------------- */
61
+ .wb-brand-title {
62
+ font-weight: 600;
63
+ letter-spacing: 1.2px;
64
+ line-height: 1.35;
65
+ display: -webkit-box;
66
+ -webkit-box-orient: vertical;
67
+ -webkit-line-clamp: 2;
68
+ line-clamp: 2;
69
+ overflow: hidden;
70
+ overflow-wrap: anywhere;
71
+ text-wrap: balance;
72
+ }
73
+
74
+ /* 副标题永远单行:它是"一句话定位",多行会把品牌区撑得比正文还重 */
75
+ .wb-brand-sub {
76
+ margin-top: 2px;
77
+ line-height: 1.5;
78
+ white-space: nowrap;
79
+ overflow: hidden;
80
+ text-overflow: ellipsis;
81
+ }
82
+
83
+ /* AI 思考中的三点指示:纯 keyframes,不依赖任何新特性 */
84
+ @keyframes ai-dot-bounce {
85
+ 0%,
86
+ 80%,
87
+ 100% {
88
+ opacity: 0.25;
89
+ transform: translateY(0);
90
+ }
91
+ 40% {
92
+ opacity: 1;
93
+ transform: translateY(-2px);
94
+ }
95
+ }
96
+
97
+ .ai-typing-dot {
98
+ width: 5px;
99
+ height: 5px;
100
+ border-radius: 50%;
101
+ background: var(--wb-primary);
102
+ display: inline-block;
103
+ animation: ai-dot-bounce 1.2s infinite ease-in-out;
104
+ }
105
+ .ai-typing-dot:nth-child(2) {
106
+ animation-delay: 0.15s;
107
+ }
108
+ .ai-typing-dot:nth-child(3) {
109
+ animation-delay: 0.3s;
110
+ }
111
+
112
+ /* ---------------------------------------------------------------
113
+ 等待反馈体系(components/feedback)的动画底座。
114
+ 全部是 keyframes + 背景位移,没有任何 filter / backdrop-filter,
115
+ 老内核和离线浏览器都不会掉帧。
116
+ --------------------------------------------------------------- */
117
+
118
+ /* 骨架屏流光:用背景位移做,比 opacity 闪烁稳定(不会和 antd 的 loading 抢合成层) */
119
+ @keyframes wb-shimmer {
120
+ 0% {
121
+ background-position: 200% 0;
122
+ }
123
+ 100% {
124
+ background-position: -200% 0;
125
+ }
126
+ }
127
+
128
+ .wb-skeleton {
129
+ background-image: linear-gradient(
130
+ 90deg,
131
+ var(--wb-skeleton-a) 25%,
132
+ var(--wb-skeleton-b) 40%,
133
+ var(--wb-skeleton-a) 55%
134
+ );
135
+ background-size: 400% 100%;
136
+ animation: wb-shimmer 1.5s ease-in-out infinite;
137
+ border-radius: 6px;
138
+ }
139
+
140
+ /* 全局等待浮层:从右下淡入并轻轻上移,出现得"有礼貌",不打断视线 */
141
+ @keyframes wb-hint-in {
142
+ from {
143
+ opacity: 0;
144
+ transform: translateY(8px) scale(0.98);
145
+ }
146
+ to {
147
+ opacity: 1;
148
+ transform: translateY(0) scale(1);
149
+ }
150
+ }
151
+
152
+ .wb-hint-enter {
153
+ animation: wb-hint-in 0.22s ease-out both;
154
+ }
155
+
156
+ /* 不确定进度条:一段高光在轨道里循环滑过,表示"在动但不知道多久" */
157
+ @keyframes wb-indeterminate {
158
+ 0% {
159
+ transform: translateX(-100%);
160
+ }
161
+ 100% {
162
+ transform: translateX(260%);
163
+ }
164
+ }
165
+
166
+ .wb-indeterminate-bar {
167
+ animation: wb-indeterminate 1.4s ease-in-out infinite;
168
+ }
169
+
170
+ /* 顶部路由进度条:完成时向右淡出 */
171
+ @keyframes wb-route-done {
172
+ from {
173
+ opacity: 1;
174
+ }
175
+ to {
176
+ opacity: 0;
177
+ }
178
+ }
179
+
180
+ /* 通用旋转:全局浮层的小圈、以及任何需要"在转"的地方 */
181
+ @keyframes wb-spin {
182
+ to {
183
+ transform: rotate(360deg);
184
+ }
185
+ }
186
+
187
+ /* 选中文字用主色系,和当前皮肤呼应 */
188
+ ::selection {
189
+ background: var(--wb-selection);
190
+ color: var(--wb-selection-text);
191
+ }
192
+
193
+ /* 顶栏在窄屏下的收缩策略:优先保住业务内容(搜索、统计、头像),
194
+ 把「用户姓名 / 搜索框提示语 / 统计项文字」这些可悬停补全的部分收起来。
195
+ 否则新增的统计条会把头像挤出可视区。 */
196
+ @media (max-width: 1180px) {
197
+ .header-search-trigger {
198
+ min-width: 0 !important;
199
+ padding: 0 8px !important;
200
+ }
201
+ .header-search-label,
202
+ .header-search-kbd,
203
+ .header-user-name {
204
+ display: none;
205
+ }
206
+ }
207
+
208
+ @media (max-width: 900px) {
209
+ .visit-stats {
210
+ gap: 8px !important;
211
+ padding: 0 8px !important;
212
+ }
213
+ .visit-stat-label {
214
+ display: none;
215
+ }
216
+ }
217
+
218
+ /* 滚动条也收敛到当前色系,避免出现系统默认的亮蓝细条 */
219
+ ::-webkit-scrollbar {
220
+ width: 10px;
221
+ height: 10px;
222
+ }
223
+ ::-webkit-scrollbar-track {
224
+ background: transparent;
225
+ }
226
+ ::-webkit-scrollbar-thumb {
227
+ background: var(--wb-scrollbar);
228
+ border: 2px solid transparent;
229
+ border-radius: 999px;
230
+ background-clip: padding-box;
231
+ }
232
+ ::-webkit-scrollbar-thumb:hover {
233
+ background: var(--wb-scrollbar-hover);
234
+ background-clip: padding-box;
235
+ }