node-resource-utils 0.0.0-stage → 1.1.7

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,44 @@
1
+ import type { Metadata } from 'next';
2
+ import { APP_NAME, APP_DESC, APP_TITLE_SUFFIX } from '@/lib/appInfo';
3
+ import { AntdRegistry } from '@ant-design/nextjs-registry';
4
+ import Providers from '@/components/Providers';
5
+ import { APPEARANCE_BOOT, buildAppearanceCss } from '@/lib/appearance';
6
+ import './globals.css';
7
+
8
+ export const metadata: Metadata = {
9
+ // 后缀同样是可配置项:AI 在 appInfo 里填了「· 车辆派遣」之类,标签页就要跟着变
10
+ title: APP_TITLE_SUFFIX ? `${APP_NAME}${APP_TITLE_SUFFIX}` : APP_NAME,
11
+ description: APP_DESC,
12
+ };
13
+
14
+ /** 外观变量表:三套皮肤的 `--wb-*` 全在首屏 HTML 里,切换只改 <html> 上的一个属性 */
15
+ const APPEARANCE_CSS = buildAppearanceCss();
16
+
17
+ /**
18
+ * 根布局。
19
+ *
20
+ * AntdRegistry 必须包在最外层:antd 用运行时 CSS-in-JS,
21
+ * 没有它首屏样式要靠 JS 注入,会出现「先裸奔一下再变好看」的闪烁(FOUC)。
22
+ *
23
+ * ★ 外观底座在这里落地(顺序很重要):
24
+ * 1. `<style>` 先定义全部皮肤的 CSS 变量 —— 任何一帧都能解析 var();
25
+ * 2. 紧跟一行启动脚本,把本机保存的皮肤/字号写到 <html data-wb-skin/data-wb-font>。
26
+ * 它必须**同步**执行(不能放 useEffect):否则会先按原版画一帧,
27
+ * 再"啪"地跳成深色,那一帧白闪比什么体验问题都刺眼。
28
+ * React 只负责后续的切换与 antd 主题,SSR 阶段完全不依赖浏览器存储。
29
+ */
30
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
31
+ return (
32
+ <html lang="zh-CN" suppressHydrationWarning>
33
+ <head>
34
+ <style id="wb-appearance" dangerouslySetInnerHTML={{ __html: APPEARANCE_CSS }} />
35
+ <script dangerouslySetInnerHTML={{ __html: APPEARANCE_BOOT }} />
36
+ </head>
37
+ <body>
38
+ <AntdRegistry>
39
+ <Providers>{children}</Providers>
40
+ </AntdRegistry>
41
+ </body>
42
+ </html>
43
+ );
44
+ }
@@ -0,0 +1,267 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { APP_NAME, APP_SUBTITLE, APP_FEATURES, APP_DESC, SHOW_DEMO_ACCOUNT_HINT, DEMO_ACCOUNT_TEXT } from '@/lib/appInfo';
5
+
6
+ import Link from 'next/link';
7
+ import { useRouter } from 'next/navigation';
8
+ import { signIn } from 'next-auth/react';
9
+ import { Alert, App, Button, Card, Checkbox, Form, Input, Typography } from 'antd';
10
+ import { LockOutlined, SafetyCertificateOutlined, UserOutlined } from '@ant-design/icons';
11
+ import { fs, MILITARY } from '@/lib/theme';
12
+
13
+ /**
14
+ * 登录页。
15
+ *
16
+ * 用 signIn(..., { redirect: false }) 而不是默认跳转:
17
+ * 默认行为失败时会把用户甩到 NextAuth 自己的错误页,体验很割裂。
18
+ * 手动接管后,失败就在当前页弹一句「用户名或密码错误」,页面不跳。
19
+ */
20
+
21
+ const { Title, Text } = Typography;
22
+
23
+ export default function LoginPage() {
24
+ const router = useRouter();
25
+ const { message } = App.useApp();
26
+ const [loading, setLoading] = useState(false);
27
+ /** 注册开关(超管在用户管理页控制);关着时登录页不出现注册入口 */
28
+ const [registerEnabled, setRegisterEnabled] = useState(false);
29
+ /** 从 /register 跳回来时显示的「注册成功」提示 */
30
+ const [justRegistered, setJustRegistered] = useState(false);
31
+
32
+ useEffect(() => {
33
+ let alive = true;
34
+ fetch('/api/auth/register-config')
35
+ .then((r) => r.json())
36
+ .then((j: { code: number; data?: { enabled: boolean } }) => {
37
+ if (alive && j.code === 0) setRegisterEnabled(Boolean(j.data?.enabled));
38
+ })
39
+ .catch(() => {});
40
+ return () => {
41
+ alive = false;
42
+ };
43
+ }, []);
44
+
45
+ useEffect(() => {
46
+ // 读 query 不用 useSearchParams:那是静态渲染才需要的 Suspense 边界,
47
+ // 这里登录页每次都是动态渲染,window.location 读一次就够
48
+ if (typeof window !== 'undefined' && window.location.search.includes('registered=1')) {
49
+ setJustRegistered(true);
50
+ }
51
+ }, []);
52
+
53
+ const onFinish = async (values: { username: string; password: string }) => {
54
+ setLoading(true);
55
+ try {
56
+ const res = await signIn('credentials', {
57
+ username: values.username.trim(),
58
+ password: values.password,
59
+ redirect: false,
60
+ });
61
+
62
+ if (!res || res.error) {
63
+ message.error('用户名或密码错误');
64
+ return;
65
+ }
66
+
67
+ message.success('登录成功');
68
+ router.push('/dashboard');
69
+ router.refresh(); // 让服务端 layout 重新取一次会话,否则侧边栏还是旧状态
70
+ } catch {
71
+ message.error('登录失败,请稍后重试');
72
+ } finally {
73
+ setLoading(false);
74
+ }
75
+ };
76
+
77
+ return (
78
+ <div
79
+ className="wb-brand"
80
+ style={{
81
+ minHeight: '100vh',
82
+ display: 'flex',
83
+ backgroundImage:
84
+ 'var(--wb-brand-bg)',
85
+ }}
86
+ >
87
+ {/* 左侧品牌区:宽屏才显示,小屏直接让登录卡片占满,避免挤压 */}
88
+ <div
89
+ className="hidden lg:flex wb-brand-copy"
90
+ style={{
91
+ flex: 1,
92
+ flexDirection: 'column',
93
+ justifyContent: 'center',
94
+ padding: '0 8%',
95
+ color: 'var(--wb-brand-text)',
96
+ }}
97
+ >
98
+ <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 20 }}>
99
+ <span
100
+ style={{
101
+ width: 46,
102
+ height: 46,
103
+ borderRadius: 12,
104
+ display: 'grid',
105
+ placeItems: 'center',
106
+ background: 'var(--wb-brand-glass)',
107
+ border: '1px solid var(--wb-brand-line)',
108
+ color: 'var(--wb-brand-kicker)',
109
+ }}
110
+ >
111
+ <SafetyCertificateOutlined style={{ fontSize: fs(24) }} />
112
+ </span>
113
+ <div>
114
+ <div style={{ fontSize: fs(10.5), letterSpacing: 3, color: 'var(--wb-brand-kicker)', fontWeight: 600 }}>
115
+ INTERNAL SYSTEM
116
+ </div>
117
+ <Title level={2} style={{ color: 'var(--wb-brand-text)', margin: '2px 0 0', letterSpacing: 0.5 }}>
118
+ {APP_NAME}
119
+ </Title>
120
+ {/* 副标题同样是可配置项,不渲染就等于白填 */}
121
+ {APP_SUBTITLE ? (
122
+ <div style={{ color: 'var(--wb-brand-text-muted)', fontSize: fs(12.5), marginTop: 4 }}>
123
+ {APP_SUBTITLE}
124
+ </div>
125
+ ) : null}
126
+ </div>
127
+ </div>
128
+ <Text style={{ color: 'var(--wb-brand-text-soft)', fontSize: fs(15), lineHeight: 2 }}>
129
+ {APP_DESC}
130
+ <br />
131
+ <span style={{ color: 'var(--wb-brand-text-muted)', fontSize: fs(13) }}>
132
+ {APP_FEATURES.slice(0, 3).join(' · ')}
133
+ </span>
134
+ </Text>
135
+ <div
136
+ style={{
137
+ marginTop: 36,
138
+ padding: '16px 20px',
139
+ borderRadius: 10,
140
+ background: 'var(--wb-brand-glass)',
141
+ border: '1px solid var(--wb-brand-line)',
142
+ color: 'var(--wb-brand-text-soft)',
143
+ fontSize: fs(14),
144
+ lineHeight: 2,
145
+ }}
146
+ >
147
+ {SHOW_DEMO_ACCOUNT_HINT ? (
148
+ <>
149
+ <div>测试账号</div>
150
+ <div>
151
+ 管理员:<b>admin</b> / 123456
152
+ </div>
153
+ <div>
154
+ 普通用户:<b>zhangsan</b> / 123456
155
+ </div>
156
+ </>
157
+ ) : (
158
+ <div>{APP_DESC}</div>
159
+ )}
160
+ </div>
161
+ </div>
162
+
163
+ {/* 右侧登录卡片 */}
164
+ <div
165
+ style={{
166
+ width: 'min(100%, 480px)',
167
+ display: 'flex',
168
+ alignItems: 'center',
169
+ justifyContent: 'center',
170
+ padding: 24,
171
+ }}
172
+ >
173
+ <Card
174
+ className="wb-brand-card"
175
+ style={{ width: '100%', maxWidth: 400, boxShadow: 'var(--wb-brand-shadow)', borderRadius: 12 }}
176
+ >
177
+ <div style={{ textAlign: 'center', marginBottom: 24 }}>
178
+ <div
179
+ style={{
180
+ width: 44,
181
+ height: 44,
182
+ borderRadius: 12,
183
+ margin: '0 auto 12px',
184
+ display: 'grid',
185
+ placeItems: 'center',
186
+ background: MILITARY.soft,
187
+ border: `1px solid ${MILITARY.softStrong}`,
188
+ color: MILITARY.primary,
189
+ }}
190
+ >
191
+ <SafetyCertificateOutlined style={{ fontSize: fs(22) }} />
192
+ </div>
193
+ <Title level={3} style={{ marginBottom: 4 }}>
194
+ 欢迎回来
195
+ </Title>
196
+ <Text type="secondary">请使用系统账号登录</Text>
197
+ </div>
198
+
199
+ <Form
200
+ name="login"
201
+ size="large"
202
+ initialValues={{ username: 'admin', password: '123456', remember: true }}
203
+ onFinish={onFinish}
204
+ >
205
+ <Form.Item name="username" rules={[{ required: true, message: '请输入用户名' }]}>
206
+ <Input prefix={<UserOutlined />} placeholder="用户名" autoComplete="username" />
207
+ </Form.Item>
208
+
209
+ <Form.Item name="password" rules={[{ required: true, message: '请输入密码' }]}>
210
+ <Input.Password
211
+ prefix={<LockOutlined />}
212
+ placeholder="密码"
213
+ autoComplete="current-password"
214
+ />
215
+ </Form.Item>
216
+
217
+ <div
218
+ style={{
219
+ display: 'flex',
220
+ alignItems: 'center',
221
+ justifyContent: 'space-between',
222
+ marginBottom: 12,
223
+ }}
224
+ >
225
+ <Form.Item name="remember" valuePropName="checked" noStyle>
226
+ <Checkbox>记住我</Checkbox>
227
+ </Form.Item>
228
+ <Link href="/forgot">忘记密码?</Link>
229
+ </div>
230
+
231
+ <Form.Item style={{ marginBottom: 8 }}>
232
+ <Button type="primary" htmlType="submit" block loading={loading}>
233
+ 登录
234
+ </Button>
235
+ </Form.Item>
236
+ </Form>
237
+
238
+ {justRegistered ? (
239
+ <Alert
240
+ type="success"
241
+ showIcon
242
+ closable
243
+ onClose={() => setJustRegistered(false)}
244
+ message="注册成功"
245
+ description="请使用新账号登录;账号权限需管理员分配后生效。"
246
+ style={{ marginBottom: 12 }}
247
+ />
248
+ ) : null}
249
+
250
+ <Text type="secondary" style={{ fontSize: fs(12) }}>
251
+ {SHOW_DEMO_ACCOUNT_HINT && (
252
+ <Text type="secondary" style={{ fontSize: fs(12) }}>
253
+ {DEMO_ACCOUNT_TEXT}
254
+ </Text>
255
+ )}
256
+ </Text>
257
+
258
+ {registerEnabled ? (
259
+ <div style={{ textAlign: 'center', marginTop: 14, fontSize: fs(13) }}>
260
+ 还没有账号?<Link href="/register">注册账号</Link>
261
+ </div>
262
+ ) : null}
263
+ </Card>
264
+ </div>
265
+ </div>
266
+ );
267
+ }
@@ -0,0 +1,14 @@
1
+ import { redirect } from 'next/navigation';
2
+ import { currentUser } from '@/auth';
3
+
4
+ export const dynamic = 'force-dynamic';
5
+
6
+ /**
7
+ * 根路径只做分流:登录了就进工作台,没登录就去登录页。
8
+ * 不做「首页」是因为业务系统的首页永远是工作台,多一层欢迎页纯属浪费点击。
9
+ */
10
+ export default async function Home() {
11
+ const user = await currentUser();
12
+ if (!user) redirect('/login');
13
+ redirect(user.mustChangePassword ? '/change-password' : '/dashboard');
14
+ }
@@ -0,0 +1,237 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { useRouter } from 'next/navigation';
6
+ import { Alert, App, Button, Card, Form, Input, Spin, Typography } from 'antd';
7
+ import {
8
+ ArrowLeftOutlined,
9
+ LockOutlined,
10
+ MailOutlined,
11
+ PhoneOutlined,
12
+ SafetyCertificateOutlined,
13
+ UserOutlined,
14
+ } from '@ant-design/icons';
15
+ import { APP_NAME } from '@/lib/appInfo';
16
+ import { fs, MILITARY } from '@/lib/theme';
17
+ import { formRules } from '@/lib/formRules';
18
+
19
+ const { Title, Text } = Typography;
20
+
21
+ type ApiResp<T> = { code: number; data?: T; message?: string };
22
+
23
+ type FormValues = {
24
+ username: string;
25
+ name: string;
26
+ password: string;
27
+ confirm: string;
28
+ phone?: string;
29
+ email?: string;
30
+ };
31
+
32
+ /**
33
+ * 自助注册(未登录可访问)。
34
+ *
35
+ * 入口由超级管理员控制:登录页只在开关打开时才显示「注册账号」链接,
36
+ * 本页也会在加载时读一次开关 —— 关着时直接显示「未开放」,不留表单。
37
+ * (接口侧同样会校验开关,前端隐藏只是体验,不是边界。)
38
+ *
39
+ * 注册成功不自动登录,跳回登录页并提示:新账号默认没有角色,
40
+ * 登进去也是空菜单,由管理员分配角色后才能使用功能。
41
+ */
42
+ export default function RegisterPage() {
43
+ const router = useRouter();
44
+ const { message } = App.useApp();
45
+
46
+ /** null = 还在读开关;true/false = 开关状态 */
47
+ const [enabled, setEnabled] = useState<boolean | null>(null);
48
+ const [submitting, setSubmitting] = useState(false);
49
+ const [form] = Form.useForm<FormValues>();
50
+
51
+ useEffect(() => {
52
+ let alive = true;
53
+ fetch('/api/auth/register-config')
54
+ .then((r) => r.json())
55
+ .then((j: ApiResp<{ enabled: boolean }>) => {
56
+ if (alive) setEnabled(j.code === 0 ? Boolean(j.data?.enabled) : false);
57
+ })
58
+ .catch(() => {
59
+ if (alive) setEnabled(false); // 读不到开关时按「未开放」处理,宁可少一个入口也不少一道门
60
+ });
61
+ return () => {
62
+ alive = false;
63
+ };
64
+ }, []);
65
+
66
+ const submit = async (values: FormValues) => {
67
+ setSubmitting(true);
68
+ try {
69
+ const r = await fetch('/api/auth/register', {
70
+ method: 'POST',
71
+ headers: { 'Content-Type': 'application/json' },
72
+ body: JSON.stringify({
73
+ username: values.username,
74
+ name: values.name,
75
+ password: values.password,
76
+ phone: values.phone || '',
77
+ email: values.email || '',
78
+ }),
79
+ });
80
+ const j = (await r.json()) as ApiResp<{ id: string; username: string }>;
81
+ if (j.code !== 0) {
82
+ message.error(j.message || '注册失败');
83
+ return;
84
+ }
85
+ message.success('注册成功,请使用新账号登录');
86
+ router.push('/login?registered=1');
87
+ } catch (e) {
88
+ message.error(`注册失败:${String(e)}`);
89
+ } finally {
90
+ setSubmitting(false);
91
+ }
92
+ };
93
+
94
+ return (
95
+ <div
96
+ style={{
97
+ minHeight: '100vh',
98
+ display: 'flex',
99
+ alignItems: 'center',
100
+ justifyContent: 'center',
101
+ padding: 24,
102
+ backgroundImage: 'var(--wb-brand-bg)',
103
+ }}
104
+ >
105
+ <Card
106
+ className="wb-brand-card"
107
+ style={{
108
+ width: '100%',
109
+ maxWidth: 480,
110
+ borderRadius: 14,
111
+ boxShadow: 'var(--wb-brand-shadow)',
112
+ }}
113
+ >
114
+ <div style={{ textAlign: 'center', marginBottom: 20 }}>
115
+ <div
116
+ style={{
117
+ width: 44,
118
+ height: 44,
119
+ borderRadius: 12,
120
+ margin: '0 auto 12px',
121
+ display: 'grid',
122
+ placeItems: 'center',
123
+ background: MILITARY.soft,
124
+ border: `1px solid ${MILITARY.softStrong}`,
125
+ color: MILITARY.primary,
126
+ }}
127
+ >
128
+ <SafetyCertificateOutlined style={{ fontSize: fs(22) }} />
129
+ </div>
130
+ <Title level={3} style={{ marginBottom: 4 }}>
131
+ 注册账号
132
+ </Title>
133
+ <Text type="secondary">{APP_NAME} · 自助开通账号</Text>
134
+ </div>
135
+
136
+ {enabled === null ? (
137
+ <div style={{ display: 'grid', placeItems: 'center', padding: '36px 0' }}>
138
+ <Spin tip="正在确认注册状态…" />
139
+ </div>
140
+ ) : enabled ? (
141
+ <>
142
+ <Alert
143
+ type="info"
144
+ showIcon
145
+ style={{ marginBottom: 16 }}
146
+ title="注册成功后账号默认没有角色"
147
+ description="需管理员在「用户管理」中分配角色与部门后,才能使用系统功能。"
148
+ />
149
+ <Form form={form} layout="vertical" onFinish={submit} requiredMark={false}>
150
+ <Form.Item
151
+ name="username"
152
+ label="用户名"
153
+ rules={formRules('username', { label: '用户名', required: true })}
154
+ >
155
+ <Input
156
+ prefix={<UserOutlined />}
157
+ maxLength={32}
158
+ placeholder="字母开头,可含数字、下划线、短横线"
159
+ autoComplete="username"
160
+ />
161
+ </Form.Item>
162
+
163
+ <Form.Item
164
+ name="name"
165
+ label="姓名"
166
+ rules={formRules('name', { label: '姓名', required: true })}
167
+ >
168
+ <Input maxLength={32} placeholder="真实姓名" autoComplete="name" />
169
+ </Form.Item>
170
+
171
+ <Form.Item
172
+ name="password"
173
+ label="密码"
174
+ rules={formRules('password', { label: '密码', required: true })}
175
+ >
176
+ <Input.Password
177
+ prefix={<LockOutlined />}
178
+ maxLength={64}
179
+ placeholder="6-64 位,不能含空格"
180
+ autoComplete="new-password"
181
+ />
182
+ </Form.Item>
183
+
184
+ <Form.Item
185
+ name="confirm"
186
+ label="确认密码"
187
+ dependencies={['password']}
188
+ rules={[
189
+ { required: true, message: '请再次输入密码' },
190
+ ({ getFieldValue }) => ({
191
+ validator: (_r, value) =>
192
+ !value || getFieldValue('password') === value
193
+ ? Promise.resolve()
194
+ : Promise.reject(new Error('两次输入的密码不一致')),
195
+ }),
196
+ ]}
197
+ >
198
+ <Input.Password
199
+ prefix={<LockOutlined />}
200
+ maxLength={64}
201
+ placeholder="再次输入密码"
202
+ autoComplete="new-password"
203
+ />
204
+ </Form.Item>
205
+
206
+ <Form.Item name="phone" label="手机号" rules={formRules('phone', { label: '手机号' })}>
207
+ <Input prefix={<PhoneOutlined />} maxLength={20} placeholder="选填,11 位手机号" />
208
+ </Form.Item>
209
+
210
+ <Form.Item name="email" label="邮箱" rules={formRules('email', { label: '邮箱' })}>
211
+ <Input prefix={<MailOutlined />} maxLength={254} placeholder="选填,如 name@example.com" />
212
+ </Form.Item>
213
+
214
+ <Button type="primary" htmlType="submit" block size="large" loading={submitting}>
215
+ 注册
216
+ </Button>
217
+ </Form>
218
+ </>
219
+ ) : (
220
+ <Alert
221
+ type="warning"
222
+ showIcon
223
+ title="注册功能未开放"
224
+ description="请联系超级管理员在「系统管理 → 用户管理」中开启注册开关。已有账号请直接登录。"
225
+ style={{ marginBottom: 16 }}
226
+ />
227
+ )}
228
+
229
+ <div style={{ textAlign: 'center', marginTop: 18 }}>
230
+ <Link href="/login">
231
+ <ArrowLeftOutlined /> 返回登录
232
+ </Link>
233
+ </div>
234
+ </Card>
235
+ </div>
236
+ );
237
+ }