@lark-apaas/coding-template-nestjs-react-fullstack 0.1.0-alpha.1

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 (250) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +41 -0
  3. package/hooks/after-install.mjs +3 -0
  4. package/hooks/after-render.mjs +3 -0
  5. package/hooks/before-render.mjs +3 -0
  6. package/package.json +36 -0
  7. package/template/.codebase/pipelines/ci.yaml +34 -0
  8. package/template/.codebase/pipelines/coco-code-review.yaml +21 -0
  9. package/template/.prettierrc +8 -0
  10. package/template/.spark_project +16 -0
  11. package/template/.stylelintrc.js +12 -0
  12. package/template/_env.local +38 -0
  13. package/template/_gitignore +64 -0
  14. package/template/_npmrc +1 -0
  15. package/template/client/index.html +23 -0
  16. package/template/client/public/favicon.svg +40 -0
  17. package/template/client/src/api/index.ts +19 -0
  18. package/template/client/src/app.tsx +22 -0
  19. package/template/client/src/components/Layout.tsx +11 -0
  20. package/template/client/src/components/business-ui/README.md +519 -0
  21. package/template/client/src/components/business-ui/api/chats/queries.ts +39 -0
  22. package/template/client/src/components/business-ui/api/chats/service.ts +29 -0
  23. package/template/client/src/components/business-ui/api/departments/queries.ts +42 -0
  24. package/template/client/src/components/business-ui/api/departments/service.ts +18 -0
  25. package/template/client/src/components/business-ui/api/files/service.ts +28 -0
  26. package/template/client/src/components/business-ui/api/user-profiles/queries.ts +42 -0
  27. package/template/client/src/components/business-ui/api/user-profiles/service.ts +23 -0
  28. package/template/client/src/components/business-ui/api/users/queries.ts +34 -0
  29. package/template/client/src/components/business-ui/api/users/service.ts +40 -0
  30. package/template/client/src/components/business-ui/chat-select/README.md +128 -0
  31. package/template/client/src/components/business-ui/chat-select/chat-item.tsx +85 -0
  32. package/template/client/src/components/business-ui/chat-select/chat-select-tag.tsx +83 -0
  33. package/template/client/src/components/business-ui/chat-select/chat-select.tsx +197 -0
  34. package/template/client/src/components/business-ui/chat-select/index.tsx +12 -0
  35. package/template/client/src/components/business-ui/chat-select/types.ts +102 -0
  36. package/template/client/src/components/business-ui/chat-select/use-chat-value.ts +180 -0
  37. package/template/client/src/components/business-ui/chat-select/utils.ts +81 -0
  38. package/template/client/src/components/business-ui/department-select/department-item.tsx +55 -0
  39. package/template/client/src/components/business-ui/department-select/department-select-field.tsx +86 -0
  40. package/template/client/src/components/business-ui/department-select/department-select-tag.tsx +56 -0
  41. package/template/client/src/components/business-ui/department-select/department-select.tsx +168 -0
  42. package/template/client/src/components/business-ui/department-select/icon-department.tsx +22 -0
  43. package/template/client/src/components/business-ui/department-select/index.tsx +7 -0
  44. package/template/client/src/components/business-ui/department-select/types.ts +15 -0
  45. package/template/client/src/components/business-ui/department-select/utils.ts +17 -0
  46. package/template/client/src/components/business-ui/entity-combobox/base-combobox-content.tsx +35 -0
  47. package/template/client/src/components/business-ui/entity-combobox/base-combobox-empty.tsx +28 -0
  48. package/template/client/src/components/business-ui/entity-combobox/base-combobox-error.tsx +35 -0
  49. package/template/client/src/components/business-ui/entity-combobox/base-combobox-item.tsx +55 -0
  50. package/template/client/src/components/business-ui/entity-combobox/base-combobox-list.tsx +174 -0
  51. package/template/client/src/components/business-ui/entity-combobox/base-combobox-loading.tsx +19 -0
  52. package/template/client/src/components/business-ui/entity-combobox/base-combobox-search.tsx +31 -0
  53. package/template/client/src/components/business-ui/entity-combobox/base-combobox-trigger.tsx +281 -0
  54. package/template/client/src/components/business-ui/entity-combobox/base-combobox.tsx +132 -0
  55. package/template/client/src/components/business-ui/entity-combobox/context.tsx +19 -0
  56. package/template/client/src/components/business-ui/entity-combobox/entity-combobox.tsx +145 -0
  57. package/template/client/src/components/business-ui/entity-combobox/highlight-text.tsx +52 -0
  58. package/template/client/src/components/business-ui/entity-combobox/hooks.tsx +21 -0
  59. package/template/client/src/components/business-ui/entity-combobox/index.tsx +31 -0
  60. package/template/client/src/components/business-ui/entity-combobox/item-pill.tsx +179 -0
  61. package/template/client/src/components/business-ui/entity-combobox/popover-wrapper.tsx +149 -0
  62. package/template/client/src/components/business-ui/entity-combobox/search-trigger.tsx +262 -0
  63. package/template/client/src/components/business-ui/entity-combobox/shared-types.ts +296 -0
  64. package/template/client/src/components/business-ui/entity-combobox/size-variants.tsx +322 -0
  65. package/template/client/src/components/business-ui/entity-combobox/types.ts +246 -0
  66. package/template/client/src/components/business-ui/entity-combobox/use-fetch-data.tsx +81 -0
  67. package/template/client/src/components/business-ui/entity-combobox/use-infinite-scroll.tsx +99 -0
  68. package/template/client/src/components/business-ui/entity-combobox/use-popover-outside-click.tsx +32 -0
  69. package/template/client/src/components/business-ui/form/checkbox-field.tsx +83 -0
  70. package/template/client/src/components/business-ui/form/context.tsx +38 -0
  71. package/template/client/src/components/business-ui/form/field-layout.tsx +91 -0
  72. package/template/client/src/components/business-ui/form/form.tsx +54 -0
  73. package/template/client/src/components/business-ui/form/hooks/form-context.tsx +4 -0
  74. package/template/client/src/components/business-ui/form/hooks/form-utils.ts +23 -0
  75. package/template/client/src/components/business-ui/form/hooks/form.tsx +40 -0
  76. package/template/client/src/components/business-ui/form/index.tsx +4 -0
  77. package/template/client/src/components/business-ui/form/input-field.tsx +74 -0
  78. package/template/client/src/components/business-ui/form/radio-group-field.tsx +138 -0
  79. package/template/client/src/components/business-ui/form/select-field.tsx +155 -0
  80. package/template/client/src/components/business-ui/form/switch-field.tsx +66 -0
  81. package/template/client/src/components/business-ui/form/textarea-field.tsx +72 -0
  82. package/template/client/src/components/business-ui/form/types.ts +32 -0
  83. package/template/client/src/components/business-ui/tiptap-editor/README.md +324 -0
  84. package/template/client/src/components/business-ui/tiptap-editor/components/attachment-toolbar-button.tsx +74 -0
  85. package/template/client/src/components/business-ui/tiptap-editor/components/blockquote-toolbar-button.tsx +41 -0
  86. package/template/client/src/components/business-ui/tiptap-editor/components/code-block-toolbar-button.tsx +41 -0
  87. package/template/client/src/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button.tsx +141 -0
  88. package/template/client/src/components/business-ui/tiptap-editor/components/heading-toolbar-button.tsx +99 -0
  89. package/template/client/src/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button.tsx +39 -0
  90. package/template/client/src/components/business-ui/tiptap-editor/components/image-upload-toolbar-button.tsx +65 -0
  91. package/template/client/src/components/business-ui/tiptap-editor/components/link-edit-form.tsx +127 -0
  92. package/template/client/src/components/business-ui/tiptap-editor/components/link-hover-toolbar.tsx +380 -0
  93. package/template/client/src/components/business-ui/tiptap-editor/components/link-toolbar-button.tsx +96 -0
  94. package/template/client/src/components/business-ui/tiptap-editor/components/list-toolbar-button.tsx +75 -0
  95. package/template/client/src/components/business-ui/tiptap-editor/components/mark-toolbar-button.tsx +118 -0
  96. package/template/client/src/components/business-ui/tiptap-editor/components/text-align-toolbar-button.tsx +84 -0
  97. package/template/client/src/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button.tsx +54 -0
  98. package/template/client/src/components/business-ui/tiptap-editor/extensions/attachment.tsx +539 -0
  99. package/template/client/src/components/business-ui/tiptap-editor/extensions/code-block-shiki.tsx +236 -0
  100. package/template/client/src/components/business-ui/tiptap-editor/extensions/complete-kit.ts +260 -0
  101. package/template/client/src/components/business-ui/tiptap-editor/extensions/image.tsx +456 -0
  102. package/template/client/src/components/business-ui/tiptap-editor/hooks/use-tiptap-editor.ts +47 -0
  103. package/template/client/src/components/business-ui/tiptap-editor/index.ts +38 -0
  104. package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor-complete.tsx +112 -0
  105. package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor.tsx +169 -0
  106. package/template/client/src/components/business-ui/types/user.ts +74 -0
  107. package/template/client/src/components/business-ui/user-display/index.tsx +2 -0
  108. package/template/client/src/components/business-ui/user-display/overflow-tooltip-text.tsx +90 -0
  109. package/template/client/src/components/business-ui/user-display/type.ts +27 -0
  110. package/template/client/src/components/business-ui/user-display/user-display.tsx +99 -0
  111. package/template/client/src/components/business-ui/user-display/user-with-avatar.tsx +158 -0
  112. package/template/client/src/components/business-ui/user-display/utils.ts +37 -0
  113. package/template/client/src/components/business-ui/user-profile/error-image.tsx +302 -0
  114. package/template/client/src/components/business-ui/user-profile/user-external-script.ts +73 -0
  115. package/template/client/src/components/business-ui/user-profile/user-profile.tsx +370 -0
  116. package/template/client/src/components/business-ui/user-select/index.tsx +28 -0
  117. package/template/client/src/components/business-ui/user-select/types.ts +109 -0
  118. package/template/client/src/components/business-ui/user-select/use-user-value.ts +160 -0
  119. package/template/client/src/components/business-ui/user-select/user-item.tsx +98 -0
  120. package/template/client/src/components/business-ui/user-select/user-pill.tsx +140 -0
  121. package/template/client/src/components/business-ui/user-select/user-select-tag.tsx +82 -0
  122. package/template/client/src/components/business-ui/user-select/user-select.tsx +359 -0
  123. package/template/client/src/components/business-ui/user-select/utils.tsx +188 -0
  124. package/template/client/src/components/business-ui/utils/user.ts +72 -0
  125. package/template/client/src/components/ui/README.md +134 -0
  126. package/template/client/src/components/ui/accordion.tsx +66 -0
  127. package/template/client/src/components/ui/alert-dialog.tsx +157 -0
  128. package/template/client/src/components/ui/alert.tsx +71 -0
  129. package/template/client/src/components/ui/aspect-ratio.tsx +11 -0
  130. package/template/client/src/components/ui/avatar.tsx +53 -0
  131. package/template/client/src/components/ui/badge.tsx +42 -0
  132. package/template/client/src/components/ui/breadcrumb.tsx +109 -0
  133. package/template/client/src/components/ui/button-group.tsx +83 -0
  134. package/template/client/src/components/ui/button.tsx +69 -0
  135. package/template/client/src/components/ui/calendar.tsx +213 -0
  136. package/template/client/src/components/ui/card.tsx +82 -0
  137. package/template/client/src/components/ui/carousel.tsx +241 -0
  138. package/template/client/src/components/ui/chart.tsx +357 -0
  139. package/template/client/src/components/ui/checkbox.tsx +32 -0
  140. package/template/client/src/components/ui/collapsible.tsx +33 -0
  141. package/template/client/src/components/ui/command.tsx +208 -0
  142. package/template/client/src/components/ui/context-menu.tsx +324 -0
  143. package/template/client/src/components/ui/dialog.tsx +143 -0
  144. package/template/client/src/components/ui/drawer.tsx +135 -0
  145. package/template/client/src/components/ui/dropdown-menu.tsx +329 -0
  146. package/template/client/src/components/ui/empty.tsx +104 -0
  147. package/template/client/src/components/ui/field.tsx +248 -0
  148. package/template/client/src/components/ui/form.tsx +167 -0
  149. package/template/client/src/components/ui/hover-card.tsx +44 -0
  150. package/template/client/src/components/ui/icons/file-ae-colorful-icon.tsx +21 -0
  151. package/template/client/src/components/ui/icons/file-ai-colorful-icon.tsx +36 -0
  152. package/template/client/src/components/ui/icons/file-android-colorful-icon.tsx +33 -0
  153. package/template/client/src/components/ui/icons/file-audio-colorful-icon.tsx +21 -0
  154. package/template/client/src/components/ui/icons/file-code-colorful-icon.tsx +28 -0
  155. package/template/client/src/components/ui/icons/file-csv-colorful-icon.tsx +21 -0
  156. package/template/client/src/components/ui/icons/file-eml-colorful-icon.tsx +29 -0
  157. package/template/client/src/components/ui/icons/file-ios-colorful-icon.tsx +25 -0
  158. package/template/client/src/components/ui/icons/file-keynote-colorful-icon.tsx +29 -0
  159. package/template/client/src/components/ui/icons/file-pages-colorful-icon.tsx +29 -0
  160. package/template/client/src/components/ui/icons/file-ps-colorful-icon.tsx +21 -0
  161. package/template/client/src/components/ui/icons/file-sketch-colorful-icon.tsx +21 -0
  162. package/template/client/src/components/ui/icons/file-slide-colorful-icon.tsx +21 -0
  163. package/template/client/src/components/ui/icons/file-vcf-colorful-icon.tsx +29 -0
  164. package/template/client/src/components/ui/icons/file-wiki-excel-colorful-icon.tsx +23 -0
  165. package/template/client/src/components/ui/icons/file-wiki-image-colorful-icon.tsx +27 -0
  166. package/template/client/src/components/ui/icons/file-wiki-pdf-colorful-icon.tsx +20 -0
  167. package/template/client/src/components/ui/icons/file-wiki-ppt-colorful-icon.tsx +21 -0
  168. package/template/client/src/components/ui/icons/file-wiki-text-colorful-icon.tsx +12 -0
  169. package/template/client/src/components/ui/icons/file-wiki-unknown-colorful-icon.tsx +14 -0
  170. package/template/client/src/components/ui/icons/file-wiki-video-colorful-icon.tsx +23 -0
  171. package/template/client/src/components/ui/icons/file-wiki-word-colorful-icon.tsx +38 -0
  172. package/template/client/src/components/ui/icons/file-wiki-zip-colorful-icon.tsx +21 -0
  173. package/template/client/src/components/ui/image.tsx +183 -0
  174. package/template/client/src/components/ui/input-group.tsx +166 -0
  175. package/template/client/src/components/ui/input-otp.tsx +77 -0
  176. package/template/client/src/components/ui/input.tsx +21 -0
  177. package/template/client/src/components/ui/item.tsx +193 -0
  178. package/template/client/src/components/ui/kbd.tsx +28 -0
  179. package/template/client/src/components/ui/label.tsx +24 -0
  180. package/template/client/src/components/ui/menubar.tsx +348 -0
  181. package/template/client/src/components/ui/native-select.tsx +48 -0
  182. package/template/client/src/components/ui/navigation-menu.tsx +168 -0
  183. package/template/client/src/components/ui/pagination.tsx +127 -0
  184. package/template/client/src/components/ui/popover.tsx +48 -0
  185. package/template/client/src/components/ui/progress.tsx +31 -0
  186. package/template/client/src/components/ui/radio-group.tsx +45 -0
  187. package/template/client/src/components/ui/resizable.tsx +56 -0
  188. package/template/client/src/components/ui/scroll-area.tsx +58 -0
  189. package/template/client/src/components/ui/select.tsx +243 -0
  190. package/template/client/src/components/ui/separator.tsx +28 -0
  191. package/template/client/src/components/ui/sheet.tsx +139 -0
  192. package/template/client/src/components/ui/sidebar.tsx +727 -0
  193. package/template/client/src/components/ui/skeleton.tsx +13 -0
  194. package/template/client/src/components/ui/slider.tsx +87 -0
  195. package/template/client/src/components/ui/sonner.tsx +67 -0
  196. package/template/client/src/components/ui/spinner.tsx +16 -0
  197. package/template/client/src/components/ui/streamdown.tsx +186 -0
  198. package/template/client/src/components/ui/switch.tsx +31 -0
  199. package/template/client/src/components/ui/table.tsx +116 -0
  200. package/template/client/src/components/ui/tabs.tsx +66 -0
  201. package/template/client/src/components/ui/textarea.tsx +18 -0
  202. package/template/client/src/components/ui/toggle-group.tsx +83 -0
  203. package/template/client/src/components/ui/toggle.tsx +47 -0
  204. package/template/client/src/components/ui/tooltip.tsx +61 -0
  205. package/template/client/src/hooks/use-example.ts +52 -0
  206. package/template/client/src/hooks/use-mobile.ts +19 -0
  207. package/template/client/src/index.css +7 -0
  208. package/template/client/src/index.tsx +36 -0
  209. package/template/client/src/lib/shiki.ts +92 -0
  210. package/template/client/src/lib/utils.ts +6 -0
  211. package/template/client/src/pages/ExamplePage/ExamplePage.tsx +37 -0
  212. package/template/client/src/pages/NotFound/NotFound.tsx +11 -0
  213. package/template/client/src/tailwind-theme.css +257 -0
  214. package/template/client/src/types/common.ts +34 -0
  215. package/template/client/src/types/global.d.ts +53 -0
  216. package/template/client/src/types/index.ts +4 -0
  217. package/template/client/src/typography.css +41 -0
  218. package/template/client/src/utils/img-resources/avatar-placeholders.ts +12 -0
  219. package/template/client/src/utils/img-resources/banner-placeholders.ts +29 -0
  220. package/template/client/src/utils/img-resources/cover-placeholders.ts +47 -0
  221. package/template/components.json +21 -0
  222. package/template/eslint.config.js +50 -0
  223. package/template/nest-cli.json +25 -0
  224. package/template/package-lock.json +29195 -0
  225. package/template/package.json +202 -0
  226. package/template/postcss.config.js +10 -0
  227. package/template/scripts/build.sh +120 -0
  228. package/template/scripts/dev.js +295 -0
  229. package/template/scripts/dev.sh +2 -0
  230. package/template/scripts/lint.js +150 -0
  231. package/template/scripts/prune-smart.js +281 -0
  232. package/template/scripts/run.sh +5 -0
  233. package/template/server/app.module.ts +27 -0
  234. package/template/server/common/constants/api_response_code.ts +54 -0
  235. package/template/server/common/filters/exception.filter.ts +78 -0
  236. package/template/server/common/interfaces/api_response.interface.ts +21 -0
  237. package/template/server/common/interfaces/exception.interface.ts +19 -0
  238. package/template/server/database/schema.ts +90 -0
  239. package/template/server/main.ts +31 -0
  240. package/template/server/modules/hello/hello.controller.ts +16 -0
  241. package/template/server/modules/hello/hello.module.ts +10 -0
  242. package/template/server/modules/hello/hello.service.ts +28 -0
  243. package/template/server/modules/view/view.controller.ts +17 -0
  244. package/template/server/modules/view/view.module.ts +8 -0
  245. package/template/shared/api.interface.ts +1 -0
  246. package/template/tailwind.config.ts +9 -0
  247. package/template/tsconfig.app.json +23 -0
  248. package/template/tsconfig.json +8 -0
  249. package/template/tsconfig.node.json +16 -0
  250. package/template/vite.config.ts +10 -0
@@ -0,0 +1,188 @@
1
+ import type { UserInfo, SearchAvatar } from '@lark-apaas/client-toolkit/tools/services';
2
+
3
+ import type { AccountType } from '@client/src/components/business-ui/api/users/service';
4
+ import type {
5
+ UserSelectItemValue,
6
+ UserSelectValue,
7
+ } from '@client/src/components/business-ui/user-select/types';
8
+ import { getI18nText } from '@client/src/components/business-ui/utils/user';
9
+ import type { User } from '@client/src/components/business-ui/types/user';
10
+
11
+ export function getUserDisplayName(name: UserInfo['name']): string {
12
+ return name.zh_cn || name.en_us || '';
13
+ }
14
+
15
+ export function getUserId(
16
+ user: Pick<UserInfo, 'userID' | 'larkUserID'>,
17
+ accountType: AccountType,
18
+ ): string {
19
+ if (accountType === 'lark') {
20
+ return user.larkUserID || `_unknown_${Math.random().toString(36).slice(2)}`;
21
+ }
22
+ // 未注册外部联系人没有 userID,暂用 larkUserID 作为临时标识
23
+ return user.userID || user.larkUserID || `_unknown_${Math.random().toString(36).slice(2)}`;
24
+ }
25
+
26
+ /**
27
+ * 判断用户是否为未注册的外部联系人
28
+ */
29
+ export function isUnregisteredExternalContact(user: UserSelectItemValue): boolean {
30
+ const raw = user.raw;
31
+ return !!raw && !raw.user_id && !!raw.larkUserId;
32
+ }
33
+
34
+ export function createUnknownUser(id: string): UserSelectItemValue {
35
+ return {
36
+ id,
37
+ name: '未知用户',
38
+ avatar: undefined,
39
+ raw: undefined,
40
+ };
41
+ }
42
+
43
+ export function userInfoToUser(
44
+ userInfo: UserInfo & SearchAvatar,
45
+ accountType: AccountType,
46
+ ): UserSelectItemValue {
47
+ // 构建统一的 User 类型作为 raw 数据
48
+ const rawUser: User = {
49
+ user_id: userInfo.userID || undefined,
50
+ larkUserId: userInfo.larkUserID || undefined,
51
+ name: userInfo.name,
52
+ avatar: userInfo.avatar?.image?.large,
53
+ user_type: userInfo.userType,
54
+ department: userInfo.department as any,
55
+ };
56
+
57
+ return {
58
+ id: getUserId(userInfo, accountType),
59
+ name: getI18nText(userInfo.name),
60
+ avatar: userInfo.avatar?.image?.large,
61
+ raw: rawUser,
62
+ };
63
+ }
64
+
65
+ export function searchUserInfoToUser(
66
+ userInfo: UserInfo & { avatar?: string; tenantName?: string },
67
+ accountType: AccountType,
68
+ ): UserSelectItemValue {
69
+ // 构建统一的 User 类型作为 raw 数据
70
+ const rawUser: User = {
71
+ user_id: userInfo.userID || undefined,
72
+ larkUserId: userInfo.larkUserID || undefined,
73
+ name: userInfo.name,
74
+ avatar: userInfo.avatar,
75
+ user_type: userInfo.userType,
76
+ department: userInfo.department as any,
77
+ tenantName: userInfo.tenantName,
78
+ };
79
+
80
+ return {
81
+ id: getUserId(userInfo, accountType),
82
+ name: getI18nText(userInfo.name),
83
+ avatar: userInfo.avatar,
84
+ raw: rawUser,
85
+ };
86
+ }
87
+
88
+ declare global {
89
+ interface Window {
90
+ MIAODA_APP_ID?: string;
91
+ __platform__?: {
92
+ appId?: string;
93
+ };
94
+ appId?: string;
95
+ }
96
+ }
97
+
98
+ export function getAppId(path: string): string | null | undefined {
99
+ if (window?.__platform__?.appId) {
100
+ return window.__platform__.appId;
101
+ }
102
+
103
+ if (window.MIAODA_APP_ID) {
104
+ return window.MIAODA_APP_ID;
105
+ }
106
+
107
+ let prefix: string;
108
+
109
+ /**
110
+ * 从路径中提取 appId
111
+ * @example "/app/my-app/settings" → "my-app"
112
+ */
113
+ const appMatch = path.match(/\/app\/([^/]+)/);
114
+ if (appMatch) {
115
+ return appMatch[1];
116
+ }
117
+
118
+ // 检查路径是否以固定前缀开头
119
+ if (path.includes('/ai/feida/runtime/')) {
120
+ prefix = '/ai/feida/runtime/';
121
+ } else if (path.includes('/spark/r/')) {
122
+ prefix = '/spark/r/';
123
+ } else {
124
+ prefix = '/ai/miaoda/';
125
+ }
126
+
127
+ // 兼容全栈appId挂载在window上,作为兜底
128
+ const windowAppId = window.appId || null;
129
+
130
+ if (!path.startsWith(prefix)) {
131
+ return windowAppId;
132
+ }
133
+
134
+ // 截取前缀后的部分
135
+ const remainder = path.substring(prefix.length);
136
+
137
+ // 找到下一个斜杠的位置
138
+ const nextSlashIndex = remainder.indexOf('/');
139
+
140
+ // 如果没有斜杠,返回整个remainder;否则返回斜杠前的部分
141
+ if (nextSlashIndex === -1) {
142
+ return remainder || windowAppId;
143
+ }
144
+ return remainder.substring(0, nextSlashIndex) || windowAppId;
145
+ }
146
+
147
+ /**
148
+ * 从 value 中提取 ID 列表
149
+ * 支持 User 类型(user_id)和 UserSelectItemValue 类型(id)
150
+ */
151
+ export function extractIdsFromValue(value: UserSelectValue): string[] {
152
+ if (value === null || value === undefined) {
153
+ return [];
154
+ }
155
+ if (typeof value === 'string') {
156
+ return [value];
157
+ }
158
+ if (Array.isArray(value)) {
159
+ if (value.length === 0) {
160
+ return [];
161
+ }
162
+ // 检查第一个元素判断类型
163
+ if (typeof value[0] === 'string') {
164
+ return value as string[];
165
+ }
166
+ // 对象数组:可能是 User[] 或 UserSelectItemValue[]
167
+ return value.map((item: any) => {
168
+ // UserSelectItemValue 有 id 字段(内部类型)
169
+ if ('id' in item && item.id) {
170
+ return item.id;
171
+ }
172
+ // User 有 user_id 字段(外部类型)
173
+ if ('user_id' in item && item.user_id) {
174
+ return item.user_id;
175
+ }
176
+ return '';
177
+ }).filter(Boolean);
178
+ }
179
+ // 单个对象:可能是 User 或 UserSelectItemValue
180
+ const item = value as any;
181
+ if ('id' in item && item.id) {
182
+ return [item.id];
183
+ }
184
+ if ('user_id' in item && item.user_id) {
185
+ return [item.user_id];
186
+ }
187
+ return [];
188
+ }
@@ -0,0 +1,72 @@
1
+ // client/src/utils/user.ts
2
+
3
+ import type { I18nText, User, UserInput } from '../types/user';
4
+
5
+ /**
6
+ * 获取国际化文本的显示值
7
+ * @param text 国际化文本或普通字符串
8
+ * @param locale 语言,默认 zh_cn
9
+ */
10
+ export function getI18nText(
11
+ text: I18nText | string | undefined,
12
+ locale: 'zh_cn' | 'en_us' | 'ja_jp' = 'zh_cn',
13
+ ): string {
14
+ if (!text) return '';
15
+ if (typeof text === 'string') return text;
16
+ // 添加更完整的 fallback 逻辑
17
+ return text[locale] || text.zh_cn || text.en_us || text.ja_jp || '';
18
+ }
19
+
20
+ /**
21
+ * 将任意格式的用户数据转换为标准 User 类型(下划线命名)
22
+ */
23
+ export function normalizeUser(input: UserInput): User {
24
+ const data = input as any;
25
+
26
+ // 提取 user_id(兼容驼峰和下划线)
27
+ const user_id = data.user_id || data.userId || '';
28
+
29
+ // 提取其他字段
30
+ const larkUserId = data.larkUserId; // larkUserId 保持驼峰
31
+ const name = data.name;
32
+ const avatar = data.avatar;
33
+ const email = data.email;
34
+
35
+ // 提取 user_type(兼容驼峰和下划线)
36
+ const user_type = data.user_type || data.userType;
37
+
38
+ const department = data.department;
39
+
40
+ return {
41
+ user_id,
42
+ larkUserId,
43
+ name,
44
+ avatar,
45
+ email,
46
+ user_type,
47
+ department,
48
+ };
49
+ }
50
+
51
+ /**
52
+ * 验证 user_id 是否有效
53
+ */
54
+ export function isValidUserId(user_id: string | undefined): boolean {
55
+ if (!user_id) return false;
56
+ const trimmed = user_id.trim();
57
+ if (!trimmed) return false;
58
+ if (trimmed === '""') return false; // 过滤 AI 可能生成的 ""
59
+ return true;
60
+ }
61
+
62
+ /**
63
+ * 获取用户显示名称
64
+ * 自动处理 I18nText 类型
65
+ */
66
+ export function getUserDisplayName(
67
+ user: User | undefined,
68
+ locale: 'zh_cn' | 'en_us' | 'ja_jp' = 'zh_cn',
69
+ ): string {
70
+ if (!user) return '';
71
+ return getI18nText(user.name, locale) || '无效人员';
72
+ }
@@ -0,0 +1,134 @@
1
+ # shadcn/ui 组件开发指南
2
+
3
+ ## 核心原则
4
+
5
+ - 组件位置:`/client/src/components/ui/`
6
+ - 图标库:必须使用 `lucide-react`,禁用 Emoji
7
+ - 查阅源码实现:直接读取组件源码了解最新组件实现
8
+ - 交互系统:Button、Badge 使用 elevate 遮罩系统处理 hover/active 状态(通过 `::after` 伪元素叠加 `--elevate-1` / `--elevate-2`)
9
+
10
+ ## 关键组件规范
11
+
12
+ #### 1. Button 组件
13
+
14
+ **导入路径**:`import { Button } from '@/components/ui/button'`
15
+
16
+ **Props**:
17
+ - `variant`: `"link" | "default" | "destructive" | "outline" | "secondary" | "ghost"` - 按钮样式变体
18
+
19
+ ```tsx
20
+ // ✅ 使用变体
21
+ <Button variant="secondary">标准按钮</Button>
22
+
23
+ // ✅ 自定义颜色时必须配对前景色
24
+ <Button className="bg-primary text-primary-foreground">自定义按钮</Button>
25
+ ```
26
+
27
+ #### 2. Badge 组件
28
+
29
+ **导入路径**:`import { Badge } from '@/components/ui/badge'`
30
+
31
+ **Props**:
32
+ - `variant`: `"default" | "destructive" | "outline" | "secondary"` - 样式变体
33
+
34
+ ```tsx
35
+ // ✅ 使用变体
36
+ <Badge>默认</Badge>
37
+ <Badge variant="secondary">次要</Badge>
38
+ <Badge variant="outline">边框</Badge>
39
+ <Badge variant="destructive">危险</Badge>
40
+ ```
41
+
42
+ #### 3. Alert 组件
43
+
44
+ **导入路径**:`import { Alert, AlertTitle, AlertDescription, AlertAction } from '@/components/ui/alert'`
45
+
46
+ **Props**:
47
+ - `variant`: `"default" | "destructive" | "success" | "warning"` - 样式变体
48
+
49
+ ```tsx
50
+ // ✅ default / destructive / success / warning 四种变体
51
+ <Alert variant="success">
52
+ <CheckCircle className="size-4" />
53
+ <AlertTitle>操作成功</AlertTitle>
54
+ <AlertDescription>您的更改已保存</AlertDescription>
55
+ </Alert>
56
+
57
+ <Alert variant="warning">
58
+ <AlertTriangle className="size-4" />
59
+ <AlertTitle>警告</AlertTitle>
60
+ <AlertDescription>请注意检查</AlertDescription>
61
+ </Alert>
62
+
63
+ ```
64
+
65
+ #### 4. Empty 组件
66
+
67
+ **导入路径**:`import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent } from '@/components/ui/empty'`
68
+
69
+ **子组件 Props**:
70
+ - `EmptyMedia` 组件的 `variant`: `"default" | "icon"` - 媒体展示方式
71
+
72
+ ```tsx
73
+ // 标准结构:EmptyHeader 包含 EmptyMedia + EmptyTitle + EmptyDescription
74
+ <Empty>
75
+ <EmptyHeader>
76
+ <EmptyMedia variant="icon">
77
+ <SearchIcon className="size-6" />
78
+ </EmptyMedia>
79
+ <EmptyTitle>暂无数据</EmptyTitle>
80
+ <EmptyDescription>当前没有找到相关内容</EmptyDescription>
81
+ </EmptyHeader>
82
+ <EmptyContent>
83
+ <Button>添加数据</Button>
84
+ </EmptyContent>
85
+ </Empty>
86
+ ```
87
+
88
+ #### 5. Card Padding 系统
89
+
90
+ **导入路径**:`import { CardHeader, CardContent, CardFooter } from '@/components/ui/card'`
91
+
92
+ - `CardHeader`: `p-6` (24px 全方向)
93
+ - `CardContent`: `p-6 pt-0` (与 header 无缝衔接)
94
+ - `CardFooter`: `p-6 pt-0` (与 content 无缝衔接)
95
+
96
+ #### 6. Dialog 组件
97
+
98
+ **导入路径**:`import { Dialog, DialogContent } from '@/components/ui/dialog'`
99
+
100
+ Dialog 默认提供了右上角的close能力,同时也提供了自定义关闭按钮的能力,即通过设置showCloseButton为false来关闭默认的关闭按钮。所以当默认存在close时,禁止在内容区域提供自定义的关闭按钮。
101
+
102
+ #### 7. Image 组件
103
+
104
+ **导入路径**:`import { Image } from '@/components/ui/image'`
105
+
106
+ **Props**:支持原生 `<img>` 标签所有属性。
107
+
108
+ ```typescript
109
+ interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement>
110
+ ```
111
+
112
+ **使用规范**:
113
+
114
+ 1. **响应式图片场景**:当图片尺寸需要根据视口宽度变化时,必须设置 `sizes` 属性
115
+ 2. **固定尺寸图片场景**:当图片有固定尺寸时,必须设置 `width` 属性(number 类型)
116
+ 3. **必须提供有意义的 `alt` 属性**
117
+
118
+ ```tsx
119
+ // ✅ 响应式图片
120
+ <Image
121
+ src="/path/to/image.jpg"
122
+ alt="描述文字"
123
+ sizes="(max-width: 768px) 100vw, 50vw"
124
+ />
125
+
126
+ // ✅ 固定尺寸图片(width/height 使用 number)
127
+ <Image
128
+ src="/path/to/image.jpg"
129
+ alt="描述文字"
130
+ width={300}
131
+ height={200}
132
+ />
133
+
134
+ ```
@@ -0,0 +1,66 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion"
5
+ import { ChevronDownIcon } from "lucide-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("border-b last:border-b-0", className)}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "focus-visible:border-ring focus-visible:ring-ring/20 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none enabled:hover:underline focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
39
+ className
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ )
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ )
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
@@ -0,0 +1,157 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5
+
6
+ import { cn } from "@/lib/utils"
7
+ import { buttonVariants } from "@/components/ui/button"
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18
+ return (
19
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ )
21
+ }
22
+
23
+ function AlertDialogPortal({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26
+ return (
27
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28
+ )
29
+ }
30
+
31
+ function AlertDialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35
+ return (
36
+ <AlertDialogPrimitive.Overlay
37
+ data-slot="alert-dialog-overlay"
38
+ className={cn(
39
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+ className
41
+ )}
42
+ {...props}
43
+ />
44
+ )
45
+ }
46
+
47
+ function AlertDialogContent({
48
+ className,
49
+ ...props
50
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
51
+ return (
52
+ <AlertDialogPortal>
53
+ <AlertDialogOverlay />
54
+ <AlertDialogPrimitive.Content
55
+ data-slot="alert-dialog-content"
56
+ className={cn(
57
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-sm:max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 max-w-lg",
58
+ className
59
+ )}
60
+ {...props}
61
+ />
62
+ </AlertDialogPortal>
63
+ )
64
+ }
65
+
66
+ function AlertDialogHeader({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div
72
+ data-slot="alert-dialog-header"
73
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
74
+ {...props}
75
+ />
76
+ )
77
+ }
78
+
79
+ function AlertDialogFooter({
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="alert-dialog-footer"
86
+ className={cn(
87
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
88
+ className
89
+ )}
90
+ {...props}
91
+ />
92
+ )
93
+ }
94
+
95
+ function AlertDialogTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
99
+ return (
100
+ <AlertDialogPrimitive.Title
101
+ data-slot="alert-dialog-title"
102
+ className={cn("text-lg font-semibold", className)}
103
+ {...props}
104
+ />
105
+ )
106
+ }
107
+
108
+ function AlertDialogDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
112
+ return (
113
+ <AlertDialogPrimitive.Description
114
+ data-slot="alert-dialog-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props}
117
+ />
118
+ )
119
+ }
120
+
121
+ function AlertDialogAction({
122
+ className,
123
+ ...props
124
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
125
+ return (
126
+ <AlertDialogPrimitive.Action
127
+ className={cn(buttonVariants(), className)}
128
+ {...props}
129
+ />
130
+ )
131
+ }
132
+
133
+ function AlertDialogCancel({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
137
+ return (
138
+ <AlertDialogPrimitive.Cancel
139
+ className={cn(buttonVariants({ variant: "outline" }), className)}
140
+ {...props}
141
+ />
142
+ )
143
+ }
144
+
145
+ export {
146
+ AlertDialog,
147
+ AlertDialogPortal,
148
+ AlertDialogOverlay,
149
+ AlertDialogTrigger,
150
+ AlertDialogContent,
151
+ AlertDialogHeader,
152
+ AlertDialogFooter,
153
+ AlertDialogTitle,
154
+ AlertDialogDescription,
155
+ AlertDialogAction,
156
+ AlertDialogCancel,
157
+ }
@@ -0,0 +1,71 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ // 新增语义化 success/warning variant
15
+ success:
16
+ "bg-success text-success-foreground border-success/50 [&>svg]:text-success-foreground *:data-[slot=alert-description]:text-success-foreground/90",
17
+ warning:
18
+ "bg-warning text-warning-foreground border-warning/50 [&>svg]:text-warning-foreground *:data-[slot=alert-description]:text-warning-foreground/90",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "default",
23
+ },
24
+ }
25
+ )
26
+
27
+ function Alert({
28
+ className,
29
+ variant,
30
+ ...props
31
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
32
+ return (
33
+ <div
34
+ data-slot="alert"
35
+ role="alert"
36
+ className={cn(alertVariants({ variant }), className)}
37
+ {...props}
38
+ />
39
+ )
40
+ }
41
+
42
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
43
+ return (
44
+ <div
45
+ data-slot="alert-title"
46
+ className={cn(
47
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
48
+ className
49
+ )}
50
+ {...props}
51
+ />
52
+ )
53
+ }
54
+
55
+ function AlertDescription({
56
+ className,
57
+ ...props
58
+ }: React.ComponentProps<"div">) {
59
+ return (
60
+ <div
61
+ data-slot="alert-description"
62
+ className={cn(
63
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
64
+ className
65
+ )}
66
+ {...props}
67
+ />
68
+ )
69
+ }
70
+
71
+ export { Alert, AlertTitle, AlertDescription }
@@ -0,0 +1,11 @@
1
+ "use client"
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
4
+
5
+ function AspectRatio({
6
+ ...props
7
+ }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
8
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />
9
+ }
10
+
11
+ export { AspectRatio }