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,249 @@
1
+ 'use client';
2
+
3
+ import type { CSSProperties } from 'react';
4
+ import { MILITARY } from '@/lib/theme';
5
+
6
+ /**
7
+ * 骨架屏。
8
+ *
9
+ * 为什么不全用 Spin 转圈:
10
+ * - 转圈只说明"在等",骨架屏还告诉用户"等来的是什么东西、大概多大"
11
+ * - 页面高度不会先塌再撑,内容出来时不会跳一下
12
+ * 列表首屏、详情面板、统计卡这三处收益最明显,所以都给了现成的组合件。
13
+ *
14
+ * 流光动画走 globals.css 的 .wb-skeleton(背景位移,不用 filter,老内核也稳)。
15
+ */
16
+
17
+ type Size = number | string;
18
+
19
+ export function SkeletonBlock({
20
+ width = '100%',
21
+ height = 12,
22
+ radius = 6,
23
+ style,
24
+ }: {
25
+ width?: Size;
26
+ height?: Size;
27
+ radius?: number;
28
+ style?: CSSProperties;
29
+ }) {
30
+ return (
31
+ <div
32
+ className="wb-skeleton"
33
+ style={{ width, height, borderRadius: radius, ...style }}
34
+ aria-hidden
35
+ />
36
+ );
37
+ }
38
+
39
+ /** 多行文字占位:最后一行短一截,看起来像真的一段话 */
40
+ export function SkeletonText({
41
+ lines = 3,
42
+ lineHeight = 12,
43
+ gap = 8,
44
+ lastWidth = '62%',
45
+ }: {
46
+ lines?: number;
47
+ lineHeight?: number;
48
+ gap?: number;
49
+ lastWidth?: Size;
50
+ }) {
51
+ return (
52
+ <div style={{ display: 'flex', flexDirection: 'column', gap }}>
53
+ {Array.from({ length: lines }).map((_, i) => (
54
+ <SkeletonBlock
55
+ key={i}
56
+ height={lineHeight}
57
+ width={i === lines - 1 ? lastWidth : '100%'}
58
+ />
59
+ ))}
60
+ </div>
61
+ );
62
+ }
63
+
64
+ /** 统计卡占位(工作台的四个数字卡) */
65
+ export function SkeletonCards({
66
+ count = 4,
67
+ height = 88,
68
+ minWidth = 150,
69
+ gap = 16,
70
+ }: {
71
+ count?: number;
72
+ height?: number;
73
+ minWidth?: number;
74
+ gap?: number;
75
+ }) {
76
+ return (
77
+ <div
78
+ style={{
79
+ display: 'grid',
80
+ gridTemplateColumns: `repeat(auto-fit, minmax(${minWidth}px, 1fr))`,
81
+ gap,
82
+ }}
83
+ >
84
+ {Array.from({ length: count }).map((_, i) => (
85
+ <div
86
+ key={i}
87
+ style={{
88
+ height,
89
+ borderRadius: 10,
90
+ border: `1px solid ${MILITARY.borderSoft}`,
91
+ background: MILITARY.card,
92
+ padding: 14,
93
+ display: 'flex',
94
+ flexDirection: 'column',
95
+ gap: 10,
96
+ justifyContent: 'center',
97
+ }}
98
+ >
99
+ <SkeletonBlock width={64} height={10} />
100
+ <SkeletonBlock width={96} height={20} />
101
+ </div>
102
+ ))}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ /**
108
+ * 表格占位。
109
+ *
110
+ * 列宽用同一套比例(不外传真实列定义),这样加载完切到真表格不会明显跳变;
111
+ * 行高对齐 antd Table 的 middle 尺寸(约 47px),高度也基本对得上。
112
+ */
113
+ export function SkeletonTable({
114
+ rows = 6,
115
+ cols = 4,
116
+ showHeader = true,
117
+ pagination = false,
118
+ }: {
119
+ rows?: number;
120
+ cols?: number;
121
+ showHeader?: boolean;
122
+ /** 是否在底部留出分页条的位置(列表页用 true,避免加载完底部跳一下) */
123
+ pagination?: boolean;
124
+ }) {
125
+ return (
126
+ <div aria-hidden>
127
+ <div
128
+ style={{
129
+ border: `1px solid ${MILITARY.border}`,
130
+ borderRadius: 8,
131
+ overflow: 'hidden',
132
+ background: MILITARY.card,
133
+ }}
134
+ >
135
+ {showHeader ? (
136
+ <div
137
+ style={{
138
+ display: 'flex',
139
+ gap: 16,
140
+ padding: '12px 16px',
141
+ background: MILITARY.soft,
142
+ borderBottom: `1px solid ${MILITARY.border}`,
143
+ }}
144
+ >
145
+ {Array.from({ length: cols }).map((_, i) => (
146
+ <SkeletonBlock key={i} height={10} width={i === 0 ? 72 : `${100 / cols}%`} />
147
+ ))}
148
+ </div>
149
+ ) : null}
150
+
151
+ {Array.from({ length: rows }).map((_, r) => (
152
+ <div
153
+ key={r}
154
+ style={{
155
+ display: 'flex',
156
+ gap: 16,
157
+ padding: '14px 16px',
158
+ borderBottom: r === rows - 1 ? 'none' : `1px solid ${MILITARY.borderSoft}`,
159
+ alignItems: 'center',
160
+ }}
161
+ >
162
+ {Array.from({ length: cols }).map((_, c) => (
163
+ <SkeletonBlock
164
+ key={c}
165
+ height={11}
166
+ width={c === 0 ? 88 : `${100 / cols}%`}
167
+ // 中间列偶尔短一点,整体更像真实数据
168
+ style={c > 0 && (c + r) % 3 === 0 ? { maxWidth: '55%' } : undefined}
169
+ />
170
+ ))}
171
+ </div>
172
+ ))}
173
+ </div>
174
+
175
+ {pagination ? (
176
+ <div
177
+ style={{
178
+ display: 'flex',
179
+ justifyContent: 'flex-end',
180
+ alignItems: 'center',
181
+ gap: 8,
182
+ padding: '16px 4px 0',
183
+ }}
184
+ >
185
+ <SkeletonBlock width={64} height={10} />
186
+ {[26, 26, 26, 26].map((w, i) => (
187
+ <SkeletonBlock key={i} width={w} height={26} radius={6} />
188
+ ))}
189
+ <SkeletonBlock width={52} height={26} radius={6} />
190
+ </div>
191
+ ) : null}
192
+ </div>
193
+ );
194
+ }
195
+
196
+ /** 表单/详情面板占位:label + 输入框,成对出现 */
197
+ export function SkeletonDetail({
198
+ rows = 5,
199
+ labelWidth = 72,
200
+ }: {
201
+ rows?: number;
202
+ labelWidth?: number;
203
+ }) {
204
+ return (
205
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }} aria-hidden>
206
+ {Array.from({ length: rows }).map((_, i) => (
207
+ <div key={i} style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
208
+ <SkeletonBlock width={labelWidth} height={10} />
209
+ <SkeletonBlock height={30} radius={8} />
210
+ </div>
211
+ ))}
212
+ </div>
213
+ );
214
+ }
215
+
216
+ /**
217
+ * 整页骨架:页头(kicker + 标题 + 说明)+ 统计卡 + 表格。
218
+ * 给 (admin)/loading.tsx 用,路由切换时先看到版式而不是空白。
219
+ */
220
+ export function SkeletonPage({
221
+ cards = 4,
222
+ rows = 6,
223
+ cols = 5,
224
+ }: {
225
+ cards?: number;
226
+ rows?: number;
227
+ cols?: number;
228
+ }) {
229
+ return (
230
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }} aria-hidden>
231
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
232
+ <SkeletonBlock width={52} height={9} />
233
+ <SkeletonBlock width={210} height={20} />
234
+ <SkeletonBlock width="48%" height={10} />
235
+ </div>
236
+ {cards > 0 ? <SkeletonCards count={cards} /> : null}
237
+ <div
238
+ style={{
239
+ background: MILITARY.card,
240
+ border: `1px solid ${MILITARY.borderSoft}`,
241
+ borderRadius: 12,
242
+ padding: 16,
243
+ }}
244
+ >
245
+ <SkeletonTable rows={rows} cols={cols} />
246
+ </div>
247
+ </div>
248
+ );
249
+ }
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import type { ReactNode } from 'react';
4
+ import { Statistic } from 'antd';
5
+ import PressableCard from './PressableCard';
6
+ import { useCountUp } from './useCountUp';
7
+
8
+ /**
9
+ * 统计卡 —— 看板 / 首页 / 任何"一个数字 + 一句说明"的地方都用它。
10
+ *
11
+ * 它把三件容易漏、漏了就显得业余的事绑在了一起:
12
+ * 1. 数字滚动(useCountUp)—— 用户能分清"刚加载完"和"本来就是 5"
13
+ * 2. 可点手感(PressableCard)—— 能下钻的卡要让人看出来能点
14
+ * 3. 加载态(loading)—— 骨架屏期间不滚动,避免先滚一遍 0
15
+ *
16
+ * 用法:
17
+ * <StatCard title="用户总数" value={stats?.userCount} icon={<TeamOutlined />}
18
+ * color={CHART_COLORS[0]} href="/system/users" loading={isLoading} />
19
+ *
20
+ * 需要 Statistic 的完整能力(精度、前后缀以外的定制)时,
21
+ * 直接用 <PressableCard> + <Statistic> 手工组合,别往这里堆 props。
22
+ */
23
+
24
+ export type StatCardProps = {
25
+ title: ReactNode;
26
+ /** 目标值;undefined / null 期间显示 0,加载完自动滚到真实值 */
27
+ value: number | null | undefined;
28
+ icon?: ReactNode;
29
+ /** 数字与图标颜色,一般取 CHART_COLORS[n] 或 MILITARY.primary */
30
+ color?: string;
31
+ /** 给了就整卡可点并跳转;不给就是纯展示(不会有手感,避免骗人) */
32
+ href?: string;
33
+ onPress?: () => void;
34
+ /** 数据还在加载:此时显示 antd 骨架,且不启动计数动画 */
35
+ loading?: boolean;
36
+ /** 小数位,默认 0 */
37
+ precision?: number;
38
+ suffix?: ReactNode;
39
+ /** 计数时长,默认 320ms;超过 400 会显得在表演 */
40
+ duration?: number;
41
+ };
42
+
43
+ export default function StatCard({
44
+ title,
45
+ value,
46
+ icon,
47
+ color,
48
+ href,
49
+ onPress,
50
+ loading,
51
+ precision,
52
+ suffix,
53
+ duration,
54
+ }: StatCardProps) {
55
+ const shown = useCountUp(value, { enabled: !loading, duration });
56
+
57
+ return (
58
+ <PressableCard href={href} onPress={onPress}>
59
+ <Statistic
60
+ title={title}
61
+ value={shown}
62
+ loading={loading}
63
+ precision={precision}
64
+ suffix={suffix}
65
+ prefix={icon ? <span style={{ color, marginRight: 8 }}>{icon}</span> : undefined}
66
+ styles={{ content: color ? { color, fontWeight: 600 } : { fontWeight: 600 } }}
67
+ />
68
+ </PressableCard>
69
+ );
70
+ }
@@ -0,0 +1,153 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState, useSyncExternalStore } from 'react';
4
+ import { fs, MILITARY } from '@/lib/theme';
5
+ import {
6
+ endTask,
7
+ getTasksServerSnapshot,
8
+ getTasksSnapshot,
9
+ subscribeTasks,
10
+ WAIT,
11
+ } from '@/lib/pending';
12
+ import { formatElapsed } from './usePending';
13
+
14
+ /**
15
+ * 全局等待浮层(兜底的那一层)。
16
+ *
17
+ * 它不认识任何业务:只订阅全局任务表,发现有任务已经超过 3 秒还没结束,
18
+ * 就在右下角浮出一张很轻的提示卡。所以:
19
+ * - 新写的页面忘了加 loading → 有它兜着
20
+ * - 导出、导入、AI 报告、批量删除 → 同一个样子,不用各写一遍
21
+ * - 3 秒以内完成的请求 → 它根本不出现,不打扰
22
+ *
23
+ * 刻意不做成遮罩、也不做全屏 loading:
24
+ * 右下角一小块、`pointer-events: none`,用户还能继续看页面、翻别的内容。
25
+ */
26
+
27
+ /** 兜底清理:任务超过这个时间还没注销,认为是页面被关掉/跳走了,自动撤掉 */
28
+ const STALE_MS = 5 * 60 * 1000;
29
+
30
+ export default function TaskHint() {
31
+ const tasks = useSyncExternalStore(
32
+ subscribeTasks,
33
+ getTasksSnapshot,
34
+ getTasksServerSnapshot
35
+ );
36
+ const [now, setNow] = useState(0);
37
+
38
+ useEffect(() => {
39
+ if (!tasks.length) {
40
+ setNow(0);
41
+ return;
42
+ }
43
+ setNow(Date.now());
44
+ const timer = setInterval(() => {
45
+ const t = Date.now();
46
+ setNow(t);
47
+ // 顺手清掉"僵尸任务",避免浮层永远挂在屏幕上
48
+ for (const task of getTasksSnapshot()) {
49
+ if (t - task.startedAt > STALE_MS) endTask(task.id);
50
+ }
51
+ }, 500);
52
+ return () => clearInterval(timer);
53
+ }, [tasks.length]);
54
+
55
+ if (!tasks.length) return null;
56
+
57
+ // 取最早开始的那条:它才是用户真正在等的
58
+ const head = tasks.reduce((a, b) => (a.startedAt <= b.startedAt ? a : b));
59
+ const elapsed = Math.max(0, now - head.startedAt);
60
+ if (elapsed < WAIT.GLOBAL_APPEAR) return null;
61
+
62
+ const slow = elapsed >= WAIT.SLOW;
63
+ const extra = tasks.length - 1;
64
+
65
+ return (
66
+ <div
67
+ className="wb-hint-enter"
68
+ role="status"
69
+ aria-live="polite"
70
+ style={{
71
+ position: 'fixed',
72
+ right: 20,
73
+ bottom: 20,
74
+ zIndex: 1100,
75
+ minWidth: 236,
76
+ maxWidth: 320,
77
+ padding: '11px 14px 10px',
78
+ borderRadius: 12,
79
+ background: MILITARY.card,
80
+ border: `1px solid ${MILITARY.border}`,
81
+ borderLeft: `3px solid ${slow ? MILITARY.warning : MILITARY.primary}`,
82
+ boxShadow: MILITARY.shadow,
83
+ // 不挡任何点击:它只是"告知",不是"拦截"
84
+ pointerEvents: 'none',
85
+ }}
86
+ >
87
+ <div style={{ display: 'flex', alignItems: 'center', gap: 9 }}>
88
+ <span
89
+ aria-hidden
90
+ style={{
91
+ width: 13,
92
+ height: 13,
93
+ flex: '0 0 auto',
94
+ borderRadius: '50%',
95
+ border: `2px solid ${MILITARY.softStrong}`,
96
+ borderTopColor: slow ? MILITARY.warning : MILITARY.primary,
97
+ animation: 'wb-spin 0.8s linear infinite',
98
+ }}
99
+ />
100
+ <span
101
+ style={{
102
+ fontSize: fs(13),
103
+ color: MILITARY.text,
104
+ overflow: 'hidden',
105
+ textOverflow: 'ellipsis',
106
+ whiteSpace: 'nowrap',
107
+ }}
108
+ >
109
+ {head.label}
110
+ </span>
111
+ </div>
112
+
113
+ <div
114
+ style={{
115
+ marginTop: 5,
116
+ marginLeft: 22,
117
+ fontSize: fs(11.5),
118
+ color: MILITARY.textMuted,
119
+ display: 'flex',
120
+ gap: 6,
121
+ flexWrap: 'wrap',
122
+ }}
123
+ >
124
+ <span>已等待 {formatElapsed(elapsed)}</span>
125
+ {extra > 0 ? <span>· 另有 {extra} 项</span> : null}
126
+ {slow ? <span style={{ color: MILITARY.warning }}>· 仍在继续,请勿重复提交</span> : null}
127
+ </div>
128
+
129
+ {/* 不确定进度条:说明"它在动",但不敢假装知道还剩多久 */}
130
+ <div
131
+ style={{
132
+ marginTop: 9,
133
+ height: 3,
134
+ borderRadius: 999,
135
+ background: MILITARY.soft,
136
+ overflow: 'hidden',
137
+ }}
138
+ >
139
+ <div
140
+ className="wb-indeterminate-bar"
141
+ style={{
142
+ width: '38%',
143
+ height: '100%',
144
+ borderRadius: 999,
145
+ background: slow
146
+ ? `linear-gradient(90deg, ${MILITARY.accent}, ${MILITARY.warning})`
147
+ : `linear-gradient(90deg, ${MILITARY.primaryHover}, ${MILITARY.accent})`,
148
+ }}
149
+ />
150
+ </div>
151
+ </div>
152
+ );
153
+ }
@@ -0,0 +1,93 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * 反馈组件族 —— 统一出口。
5
+ *
6
+ * ★ 这个目录管两件事,别混着用:
7
+ *
8
+ * 【等待反馈】系统还在忙吗?—— 用户愿意等,只要知道在等
9
+ * AsyncButton 一个按钮点了之后要等 —— 最常用,直接替换 <Button>
10
+ * LoadingOverlay 一块区域整体在等 —— 卡片/面板/详情
11
+ * Skeleton* 首次加载、还不知道等多久 —— 列表、详情、整页
12
+ * TaskHint 全局兜底(>3s 自动出现) —— 已在 Providers 挂好,页面不用管
13
+ * RouteProgress 跳页面时的顶部进度条 —— 入口处调 startRouteProgress()
14
+ * useAsyncTask 自己画等待态时用它拿状态 —— 需要自定义 UI 时
15
+ *
16
+ * 【操作反馈】我这一下,系统收到了吗?—— 用户不该怀疑自己点没点上
17
+ * PressableCard 可点击卡片(自动判断可点性 + 手感 + 右上角 ›)
18
+ * StatCard 统计卡 = 数字滚动 + 可点手感 + 加载态,看板首选
19
+ * useRowFlash 列表页"刚才改的是这一行"的状态容器,配合 DataTable
20
+ * flashElement 让任意元素闪一下(非表格场景)
21
+ * useCountUp 单独用计数动画时(必须放在独立子组件里)
22
+ *
23
+ * —— 新题接入(三步,都只要一行)——
24
+ *
25
+ * 1. 看板 / 首页的指标卡
26
+ * <StatCard title="订单总数" value={stats?.orderCount} icon={<ShoppingOutlined />}
27
+ * color={CHART_COLORS[0]} href="/orders" loading={isLoading} />
28
+ *
29
+ * 2. 列表页保存 / 删除 / 切换状态后定位到那一行
30
+ * const { highlightRow, flashRow } = useRowFlash();
31
+ * // 成功后:flashRow(row.id)
32
+ * <DataTable highlightRow={highlightRow} … />
33
+ *
34
+ * 3. 自定义卡片(不走 antd Card 的场景)
35
+ * <div className="wb-pressable" onClick={…}>…</div>
36
+ * —— 只有真能点才加这个类,加了不能点比不加更像 bug。
37
+ *
38
+ * —— 调力度只改 motion.css 的变量 ——
39
+ * --wb-dur 0.18s(悬停)/ --wb-dur-fast 0.08s(按下)/ --wb-flash-dur 0.5s(结果落地)
40
+ * / --wb-lift -1px(抬升)。超过 0.24s 就开始像"系统在表演",不要加。
41
+ *
42
+ * —— 三条禁令 ——
43
+ * 1. 不给静态内容加手感(点了没反应会让用户以为坏了)
44
+ * 2. 不引 framer-motion / react-spring 之类的动画库(脚手架红线:不引新依赖)
45
+ * 3. 所有动效必须在 prefers-reduced-motion 下有静默版本(motion.css 已统一处理,
46
+ * 自己写的新动画要补上)
47
+ *
48
+ * 样式:motion.css 由 app/globals.css @import,页面里不用再引。
49
+ */
50
+
51
+ export { default as AsyncButton } from './AsyncButton';
52
+ export type { AsyncButtonProps } from './AsyncButton';
53
+
54
+ export { default as LoadingOverlay } from './LoadingOverlay';
55
+ export type { LoadingOverlayProps } from './LoadingOverlay';
56
+
57
+ export { default as TaskHint } from './TaskHint';
58
+
59
+ export { default as RouteProgress, startRouteProgress } from './RouteProgress';
60
+
61
+ export {
62
+ SkeletonBlock,
63
+ SkeletonText,
64
+ SkeletonCards,
65
+ SkeletonTable,
66
+ SkeletonDetail,
67
+ SkeletonPage,
68
+ } from './Skeleton';
69
+
70
+ export {
71
+ useAsyncTask,
72
+ useDelayedLoading,
73
+ useElapsed,
74
+ stageAt,
75
+ formatElapsed,
76
+ DEFAULT_STAGES,
77
+ } from './usePending';
78
+ export type { TaskStage, AsyncTask, UseAsyncTaskOptions } from './usePending';
79
+
80
+ // —— 操作反馈 ——
81
+ export { default as PressableCard } from './PressableCard';
82
+ export type { PressableCardProps } from './PressableCard';
83
+
84
+ export { default as StatCard } from './StatCard';
85
+ export type { StatCardProps } from './StatCard';
86
+
87
+ export { useCountUp } from './useCountUp';
88
+ export type { CountUpOptions } from './useCountUp';
89
+
90
+ export { useRowFlash } from './useRowFlash';
91
+ export type { RowFlashTarget, UseRowFlashResult } from './useRowFlash';
92
+
93
+ export { flashElement, flashTableRow, FLASH_DURATION_MS } from './motion';