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,96 @@
1
+ /**
2
+ * 全站视觉语言的出口 —— 颜色、字号、图表色板都从这里取,页面不许写死色值。
3
+ *
4
+ * 三件事:
5
+ * 1. `MILITARY`:语义色的**变量名**(不是字面色值)。
6
+ * 为什么:外观设置里可以换皮肤(原版 / 简洁 / 炫酷),
7
+ * 页面如果写死 #4E5F3C,换皮肤就换不动了。
8
+ * `var(--wb-primary)` 会跟着 <html data-wb-skin> 自动变,
9
+ * 而 `lib/appearance.ts` 是这些变量的唯一来源。
10
+ * 2. `fs(px)`:字号工具。用户可调字号,页面写 `fs(13)` 就会跟着缩放;
11
+ * 直接写 `fontSize: 13` 会被固定住,在新皮肤/大字号下显得忽大忽小。
12
+ * 3. `CHART_COLORS` / `SEMANTIC`:图表用**字面值**(canvas 不认 CSS 变量)。
13
+ * 图表要跟随皮肤时用 `useAppearance().palette.chart`,不要用这里的常量。
14
+ *
15
+ * 语义色板(等级 / 状态这类有明确含义的分类)走 `SEMANTIC`,
16
+ * 别拿 CHART_COLORS 凑:通用色板首尾颜色太近,"优秀"和"不及格"会糊成一片。
17
+ */
18
+
19
+ import { SKINS } from './appearance';
20
+
21
+ /**
22
+ * 语义色 —— 值全是 CSS 变量,随皮肤切换。
23
+ *
24
+ * 用法:
25
+ * style={{ color: MILITARY.primary }}
26
+ * `1px solid ${MILITARY.border}`
27
+ * ⚠️ 不要拿去喂 ECharts / canvas / 需要参与计算的字符串拼接,
28
+ * 那些地方要真实色值(见 `chartPalette()` / `useAppearance().palette`)。
29
+ */
30
+ export const MILITARY = {
31
+ /** 主色 */
32
+ primary: 'var(--wb-primary)',
33
+ primaryHover: 'var(--wb-primary-hover)',
34
+ primaryActive: 'var(--wb-primary-active)',
35
+ /** 主色上的文字色(深色皮肤的主色偏亮,这里会变成近黑) */
36
+ onPrimary: 'var(--wb-on-primary)',
37
+ /** 深主色:侧边栏、品牌区底色 */
38
+ primaryDark: 'var(--wb-primary-dark)',
39
+ primaryDeep: 'var(--wb-primary-deep)',
40
+ /** 浅底:图标底、标签底、表头 */
41
+ soft: 'var(--wb-soft)',
42
+ softStrong: 'var(--wb-soft-strong)',
43
+ /** 点缀色(徽标、强调数字、定位条) */
44
+ accent: 'var(--wb-accent)',
45
+ accentSoft: 'var(--wb-accent-soft)',
46
+ /** 画布与卡片 */
47
+ bg: 'var(--wb-bg)',
48
+ card: 'var(--wb-card)',
49
+ paper: 'var(--wb-paper)',
50
+ border: 'var(--wb-border)',
51
+ borderSoft: 'var(--wb-border-soft)',
52
+ /** 文字 */
53
+ text: 'var(--wb-text)',
54
+ textSecondary: 'var(--wb-text-2)',
55
+ textMuted: 'var(--wb-text-3)',
56
+ /** 语义色 */
57
+ success: 'var(--wb-success)',
58
+ warning: 'var(--wb-warning)',
59
+ error: 'var(--wb-error)',
60
+ successBg: 'var(--wb-success-bg)',
61
+ warningBg: 'var(--wb-warning-bg)',
62
+ errorBg: 'var(--wb-error-bg)',
63
+ /** 阴影与圆角(皮肤决定"硬边"还是"厚面板") */
64
+ shadow: 'var(--wb-shadow)',
65
+ shadowSm: 'var(--wb-shadow-sm)',
66
+ radius: 'var(--wb-radius)',
67
+ radiusSm: 'var(--wb-radius-sm)',
68
+ } as const;
69
+
70
+ /** 语义状态:优秀 / 待处理 / 异常 / 中性。饼图、进度条、状态标签按语义取色 */
71
+ export const SEMANTIC = {
72
+ good: MILITARY.success,
73
+ warn: MILITARY.warning,
74
+ bad: MILITARY.error,
75
+ neutral: MILITARY.textMuted,
76
+ } as const;
77
+
78
+ /**
79
+ * 字号工具:把设计稿里的 13px 换成"跟着用户字号设置走"的正文字号。
80
+ *
81
+ * <span style={{ fontSize: fs(13) }}>
82
+ *
83
+ * 实现是 `13px * var(--wb-fs)`(0.9 / 1 / 1.1 / 1.2 四档)。
84
+ * 不要写成模板字符串拼 var(),读起来太吵;也不要忘了它只用于字号。
85
+ */
86
+ export const fs = (px: number): string => `calc(${px}px * var(--wb-fs))`;
87
+
88
+ /** 当前皮肤的字面色板(图表 / canvas 用)。默认返回原版,UI 里请用 useAppearance() */
89
+ export const chartPalette = (): string[] => SKINS.classic.palette.chart;
90
+
91
+ /**
92
+ * 图表默认色板(字面值)。
93
+ * ⚠️ 它不会跟着皮肤变 —— 需要跟随的图表请用 `useChartPalette()`(components/appearance)。
94
+ * 保留这个常量是为了兼容不关心皮肤的静态图表/图例。
95
+ */
96
+ export const CHART_COLORS: string[] = SKINS.classic.palette.chart;
@@ -0,0 +1,86 @@
1
+ import type { Prisma } from '@prisma/client';
2
+ import type { SessionUser } from '@/auth';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { isFeedbackAdmin } from '@/lib/feedback-access';
5
+ import { isEnabled } from '@/lib/features';
6
+ import type { TodoSource } from './types';
7
+
8
+ /**
9
+ * 问题反馈的待办来源 —— 消息中心自带的示例接入。
10
+ *
11
+ * 与 `badge-count.ts` 共用同一套角色视角,避免出现
12
+ * 「侧边栏红点 3 条、待办中心 0 条」这种对不上账的情况:
13
+ * - 管理员:全库 pending 反馈(他要干的活)
14
+ * - 普通用户:自己提交的、管理员已回复、还没点开看的反馈
15
+ */
16
+
17
+ const TYPE_LABEL: Record<string, string> = {
18
+ bug: '功能异常',
19
+ suggest: '体验建议',
20
+ data: '数据问题',
21
+ other: '其他',
22
+ account: '账号申诉',
23
+ };
24
+
25
+ const STATUS_LABEL: Record<string, string> = {
26
+ pending: '待处理',
27
+ doing: '处理中',
28
+ resolved: '已解决',
29
+ closed: '已关闭',
30
+ };
31
+
32
+ function whereOf(user: SessionUser): Prisma.FeedbackWhereInput {
33
+ if (isFeedbackAdmin(user)) {
34
+ return { deletedAt: null, status: 'pending' };
35
+ }
36
+ return {
37
+ deletedAt: null,
38
+ userId: user.id,
39
+ readAt: null,
40
+ reply: { not: null },
41
+ status: { not: 'closed' },
42
+ };
43
+ }
44
+
45
+ export const feedbackTodoSource: TodoSource = {
46
+ key: 'feedback',
47
+ name: '问题反馈',
48
+ description: '等待处理的反馈与未读回复',
49
+ icon: 'MessageOutlined',
50
+ order: 10,
51
+
52
+ visible: () => isEnabled('feedback'),
53
+
54
+ count: async (user) => prisma.feedback.count({ where: whereOf(user) }),
55
+
56
+ list: async (user, limit) => {
57
+ const admin = isFeedbackAdmin(user);
58
+ const rows = await prisma.feedback.findMany({
59
+ where: whereOf(user),
60
+ orderBy: { createdAt: 'desc' },
61
+ take: Math.min(50, Math.max(1, limit)),
62
+ select: {
63
+ id: true,
64
+ title: true,
65
+ type: true,
66
+ status: true,
67
+ username: true,
68
+ createdAt: true,
69
+ },
70
+ });
71
+
72
+ return rows.map((row) => ({
73
+ id: row.id,
74
+ title: row.title,
75
+ subtitle: admin
76
+ ? `${TYPE_LABEL[row.type] ?? '反馈'} · ${row.username}`
77
+ : `管理员已回复 · ${STATUS_LABEL[row.status] ?? row.status}`,
78
+ // 管理员带 status 参数,进去就是待处理列表;普通人直接定位到这条反馈
79
+ link: admin
80
+ ? `/feedback?id=${row.id}&status=pending`
81
+ : `/feedback?id=${row.id}`,
82
+ level: admin ? ('warning' as const) : ('info' as const),
83
+ createdAt: row.createdAt.toISOString(),
84
+ }));
85
+ },
86
+ };
@@ -0,0 +1,73 @@
1
+ import type { SessionUser } from '@/auth';
2
+ import type { InboxTodoGroup } from '@/lib/inbox-types';
3
+ import { feedbackTodoSource } from './feedback';
4
+ import type { TodoSource } from './types';
5
+
6
+ /**
7
+ * 待办来源注册表。
8
+ *
9
+ * 新模块只需在这里追加一行。消息中心会自动:
10
+ * - 按 order 排序分组
11
+ * - 只展示 visible(user) 为 true 的来源
12
+ * - 聚合计数给顶栏铃铛
13
+ *
14
+ * 所有来源都失败时也只是空列表,不会让 /inbox 报错。
15
+ */
16
+ export const TODO_SOURCES: TodoSource[] = [feedbackTodoSource];
17
+
18
+ function visibleSources(user: SessionUser): TodoSource[] {
19
+ return TODO_SOURCES.filter((source) => {
20
+ try {
21
+ return source.visible(user);
22
+ } catch (e) {
23
+ console.error(`[inbox] 待办来源 ${source.key} 的 visible() 失败(已跳过)`, e);
24
+ return false;
25
+ }
26
+ }).sort((a, b) => a.order - b.order);
27
+ }
28
+
29
+ /** 聚合待办分组;单个来源失败只跳过它自己 */
30
+ export async function listTodoGroups(
31
+ user: SessionUser,
32
+ limit = 20
33
+ ): Promise<InboxTodoGroup[]> {
34
+ const groups: (InboxTodoGroup | null)[] = await Promise.all(
35
+ visibleSources(user).map(async (source): Promise<InboxTodoGroup | null> => {
36
+ try {
37
+ const [count, items] = await Promise.all([
38
+ source.count(user),
39
+ source.list(user, limit),
40
+ ]);
41
+ if (count <= 0) return null;
42
+ return {
43
+ key: source.key,
44
+ name: source.name,
45
+ description: source.description,
46
+ icon: source.icon,
47
+ count,
48
+ items,
49
+ };
50
+ } catch (e) {
51
+ console.error(`[inbox] 待办来源 ${source.key} 查询失败(已跳过)`, e);
52
+ return null;
53
+ }
54
+ })
55
+ );
56
+
57
+ return groups.filter((group): group is InboxTodoGroup => group !== null);
58
+ }
59
+
60
+ /** 只要数字时走这里,避免把列表和明细也拉出来 */
61
+ export async function countTodos(user: SessionUser): Promise<number> {
62
+ const counts = await Promise.all(
63
+ visibleSources(user).map(async (source) => {
64
+ try {
65
+ return await source.count(user);
66
+ } catch (e) {
67
+ console.error(`[inbox] 待办来源 ${source.key} 计数失败(已按 0 处理)`, e);
68
+ return 0;
69
+ }
70
+ })
71
+ );
72
+ return counts.reduce((sum, value) => sum + (Number.isFinite(value) ? value : 0), 0);
73
+ }
@@ -0,0 +1,33 @@
1
+ import type { SessionUser } from '@/auth';
2
+ import type { InboxTodoItem } from '@/lib/inbox-types';
3
+
4
+ /**
5
+ * 待办来源接口。
6
+ *
7
+ * ★ 新增业务模块时,如果它有「待我审批 / 待处理 / 未读」语义:
8
+ * 1. 在本目录新增一个 `<模块>.ts`,实现下面四个字段;
9
+ * 2. 到 `index.ts` 的 `TODO_SOURCES` 数组里注册一行。
10
+ *
11
+ * 统一返回「业务无关」的 InboxTodoItem,消息中心只负责聚合与展示,
12
+ * 不关心待办来自审批、反馈还是工单。某来源查库失败会被跳过,
13
+ * 不会拖垮整个消息中心。
14
+ */
15
+
16
+ export type TodoSource = {
17
+ /** 全局唯一,用于 React key 与排障日志 */
18
+ key: string;
19
+ /** 来源名称,如「问题反馈」 */
20
+ name: string;
21
+ /** 一句话说明,展示在分组标题下 */
22
+ description?: string;
23
+ /** 菜单图标名(与 menu-icons 的映射表一致) */
24
+ icon?: string;
25
+ /** 越小越靠前 */
26
+ order: number;
27
+ /** 当前用户是否能看到该来源(权限、功能开关都在这里判断) */
28
+ visible: (user: SessionUser) => boolean;
29
+ /** 待办总数(可以大于 list 返回的条数) */
30
+ count: (user: SessionUser) => Promise<number>;
31
+ /** 最近待办;limit 由消息中心传入 */
32
+ list: (user: SessionUser, limit: number) => Promise<InboxTodoItem[]>;
33
+ };
@@ -0,0 +1,106 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * 本地文件上传。
6
+ *
7
+ * 演示系统用「项目根目录 uploads/」而不是对象存储:
8
+ * 内网单机、无云账号,落盘最省事;也不会因为网络问题让演示翻车。
9
+ * 真上生产换成 OSS 时,只需要替换本文件的两个函数实现。
10
+ */
11
+ export const UPLOAD_ROOT = path.join(process.cwd(), 'uploads');
12
+
13
+ /** 处理单个路径片段:去非法字符、拍平 `..`、去掉开头的点 */
14
+ function sanitizeSegment(name: string): string {
15
+ return name
16
+ .replace(/[\\/:*?"<>|]/g, '_') // 文件系统非法字符
17
+ .replace(/\u0000/g, '')
18
+ .replace(/\.{2,}/g, '_') // 连续的 .. 一律拍扁
19
+ .replace(/^[.\s]+/, '') // 开头的 . / 空格(避免生成隐藏文件)
20
+ .trim();
21
+ }
22
+
23
+ /** 文件名兜底:空名字也要能存下去 */
24
+ function safeBaseName(name: string): string {
25
+ return sanitizeSegment(name) || 'file';
26
+ }
27
+
28
+ /** 校验并拼出安全的绝对路径;越界直接抛错 */
29
+ function resolveInside(root: string, ...parts: string[]): string {
30
+ const target = path.join(root, ...parts);
31
+ const rel = path.relative(root, target);
32
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
33
+ throw new Error('非法的文件路径');
34
+ }
35
+ return target;
36
+ }
37
+
38
+ /**
39
+ * 保存上传文件,返回可访问信息。
40
+ * 文件名前缀时间戳 + 随机串:避免同名覆盖,也让目录里的自然排序约等于上传顺序。
41
+ *
42
+ * ★ ownerId 传了就在旁边写一个同名 `.meta.json` 记录上传人,
43
+ * 下载时校验 —— 否则任何登录用户拿到文件名就能读别人上传的 Excel
44
+ * (员工导入表、报表附件全在里面)。没传(老调用方)则不记录,下载放行。
45
+ */
46
+ export async function saveUpload(
47
+ file: File,
48
+ dir = '',
49
+ ownerId?: string
50
+ ): Promise<{ name: string; size: number; path: string; url: string }> {
51
+ const original = file.name || 'file';
52
+ const ext = path.extname(original).toLowerCase();
53
+ const base = safeBaseName(path.basename(original, ext));
54
+ const name = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}-${base}${ext}`;
55
+
56
+ // 注意:这里用 sanitizeSegment 而不是 safeBaseName。
57
+ // safeBaseName 对空串会兜底成 'file',而 dir 为空是「直接放 uploads 根目录」,
58
+ // 用错了会把文件全塞进 uploads/file/ 这个凭空出现的子目录里。
59
+ const safeDir = dir
60
+ .split(/[\\/]+/)
61
+ .map((s) => sanitizeSegment(s))
62
+ .filter(Boolean)
63
+ .join(path.sep);
64
+
65
+ const target = resolveInside(UPLOAD_ROOT, safeDir, name);
66
+ await fs.mkdir(path.dirname(target), { recursive: true });
67
+
68
+ const buffer = Buffer.from(await file.arrayBuffer());
69
+ await fs.writeFile(target, buffer);
70
+
71
+ if (ownerId) {
72
+ await fs.writeFile(`${target}.meta.json`, JSON.stringify({ ownerId }), 'utf8');
73
+ }
74
+
75
+ return { name, size: buffer.length, path: target, url: `/api/files/${encodeURIComponent(name)}` };
76
+ }
77
+
78
+ /** 读上传文件的归属人;没有元数据(历史文件)返回 null,调用方按放行处理 */
79
+ export async function uploadOwnerOf(name: string): Promise<string | null> {
80
+ try {
81
+ const raw = await fs.readFile(`${resolveUploadFile(name)}.meta.json`, 'utf8');
82
+ const parsed = JSON.parse(raw) as { ownerId?: string };
83
+ return parsed.ownerId ?? null;
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /** 只读取内容,不落盘 —— 文档解析、Excel 导入这类「一次性处理」用这个 */
90
+ export async function readUpload(
91
+ file: File
92
+ ): Promise<{ filename: string; buffer: Buffer; ext: string }> {
93
+ const filename = file.name || 'file';
94
+ const ext = path.extname(filename).toLowerCase();
95
+ const buffer = Buffer.from(await file.arrayBuffer());
96
+ return { filename, buffer, ext };
97
+ }
98
+
99
+ /** 供下载路由使用:把 URL 里的名字解析成 uploads 下的绝对路径(带越界校验) */
100
+ export function resolveUploadFile(name: string): string {
101
+ const decoded = decodeURIComponent(name);
102
+ if (decoded.includes('/') || decoded.includes('\\') || decoded.includes('..')) {
103
+ throw new Error('非法的文件名');
104
+ }
105
+ return resolveInside(UPLOAD_ROOT, decoded);
106
+ }
@@ -0,0 +1,9 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { cn } from './utils';
3
+
4
+ describe('cn', () => {
5
+ it('合并类名并去重冲突', () => {
6
+ expect(cn('px-2', 'px-4')).toBe('px-4');
7
+ expect(cn('text-sm', false && 'hidden', 'font-bold')).toBe('text-sm font-bold');
8
+ });
9
+ });
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from 'clsx';
2
+ import { twMerge } from 'tailwind-merge';
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }