@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,370 @@
1
+ 'use client';
2
+
3
+ import { logger } from '@lark-apaas/client-toolkit/logger';
4
+
5
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import SHA1 from 'crypto-js/sha1';
7
+
8
+ import {
9
+ fetchUserProfile,
10
+ getAssetsUrl,
11
+ } from '@client/src/components/business-ui/api/user-profiles/service';
12
+ import { ErrorImage } from '@client/src/components/business-ui/user-profile/error-image';
13
+ import { getEnv } from '@lark-apaas/client-toolkit/utils/getEnv';
14
+ import { useExternalScript } from '@client/src/components/business-ui/user-profile/user-external-script';
15
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
16
+ import { Badge } from '@/components/ui/badge';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Card, CardContent } from '@/components/ui/card';
19
+ import { Spinner } from '@/components/ui/spinner';
20
+ import { UserInput } from '@client/src/components/business-ui/types/user';
21
+
22
+ type WebComponentAPI = {
23
+ config(params: {
24
+ openId: string;
25
+ signature: string;
26
+ appId: string;
27
+ timestamp: string;
28
+ nonceStr: string;
29
+ url: string;
30
+ jsApiList: string[];
31
+ locale?: string[];
32
+ env?: string;
33
+ }): Promise<void>;
34
+ render(
35
+ name: string,
36
+ props: Record<string, unknown>,
37
+ container: HTMLElement | null,
38
+ ): Promise<unknown>;
39
+ onAuthError(cb: (error: Error) => void): void;
40
+ onError(cb: (error: Error) => void): void;
41
+ };
42
+
43
+ declare global {
44
+ // 运行环境标识(可根据需要扩展实际取值范围)
45
+ var ENVIRONMENT: 'staging' | 'local' | 'production' | string;
46
+ // 飞书 H5 webComponent SDK
47
+ var webComponent: WebComponentAPI;
48
+ }
49
+
50
+ interface UserProfileProps {
51
+ readonly value?: string | UserInput;
52
+ readonly userId?: string | undefined;
53
+ readonly user_id?: string | undefined;
54
+ readonly accountType?: 'apaas' | 'lark' | undefined;
55
+ }
56
+
57
+ function generateRandomString(length: number): string {
58
+ const characters =
59
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
60
+ let result = '';
61
+
62
+ for (let i = 0; i < length; i += 1) {
63
+ const randomIndex = Math.floor(Math.random() * characters.length);
64
+ result += characters.charAt(randomIndex);
65
+ }
66
+
67
+ return result;
68
+ }
69
+
70
+ type BaseUserProfileProps = BaseSimpleProfileProps | BaseOfficialProfileProps;
71
+
72
+ const ACCOUNT_STATUS = {
73
+ UNSPECIFIED: 0,
74
+ Inactive: 1,
75
+ Active: 2,
76
+ Disabled: 3,
77
+ Terminated: 4,
78
+ NotJoined: 5,
79
+ Resigned: 6,
80
+ } as const;
81
+ type AccountStatus = (typeof ACCOUNT_STATUS)[keyof typeof ACCOUNT_STATUS];
82
+
83
+ const AccountStatusMap: Record<AccountStatus, string> = {
84
+ [ACCOUNT_STATUS.UNSPECIFIED]: '',
85
+ [ACCOUNT_STATUS.Active]: '已启用',
86
+ [ACCOUNT_STATUS.Inactive]: '未激活',
87
+ [ACCOUNT_STATUS.Disabled]: '已停用',
88
+ [ACCOUNT_STATUS.Terminated]: '已停用',
89
+ [ACCOUNT_STATUS.NotJoined]: '未激活',
90
+ [ACCOUNT_STATUS.Resigned]: '已停用',
91
+ };
92
+
93
+ interface BaseSimpleProfileProps {
94
+ readonly useLarkCard: false;
95
+ readonly userProfileInfo: {
96
+ readonly name?: string;
97
+ readonly avatar?: string;
98
+ readonly email?: string;
99
+ //
100
+ readonly userStatus: AccountStatus;
101
+ readonly userType: '_employee' | '_externalUser';
102
+ };
103
+ }
104
+
105
+ interface BaseOfficialProfileProps {
106
+ readonly useLarkCard: true;
107
+ readonly larkCardParam: {
108
+ readonly needRedirect?: boolean;
109
+ readonly redirectURL?: string;
110
+ readonly larkAppID: string;
111
+ readonly jsAPITicket: string;
112
+ readonly larkOpenID: string;
113
+ readonly targetLarkOpenID: string;
114
+ };
115
+ }
116
+
117
+ /**
118
+ * 通用响应结构
119
+ * 用于标准化 API 响应格式
120
+ */
121
+ export interface WithCommonResponse<T> {
122
+ data: T;
123
+ }
124
+
125
+ async function renderLarkProfile({
126
+ larkAppID,
127
+ jsAPITicket,
128
+ larkOpenID,
129
+ cardRef,
130
+ targetLarkOpenID,
131
+ }: BaseOfficialProfileProps['larkCardParam'] & {
132
+ cardRef: React.RefObject<HTMLDivElement | null>;
133
+ }) {
134
+ const timestamp = Date.now().toString();
135
+ const nonceStr = generateRandomString(10);
136
+ const url = globalThis.location.href.split('#')[0] ?? '';
137
+ const message = `jsapi_ticket=${jsAPITicket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`;
138
+ const signature = SHA1(message).toString();
139
+
140
+ await globalThis.webComponent.config({
141
+ openId: larkOpenID,
142
+ signature,
143
+ appId: larkAppID,
144
+ timestamp,
145
+ nonceStr,
146
+ url,
147
+ jsApiList: ['user_profile'],
148
+ locale: ['zh_cn'],
149
+ ...(getEnv() === 'BOE'
150
+ ? { env: 'feishu_boe' }
151
+ : {}),
152
+ });
153
+
154
+ const myComponent = await globalThis.webComponent.render(
155
+ 'UserProfile',
156
+ {
157
+ openId: targetLarkOpenID,
158
+ },
159
+ cardRef.current,
160
+ );
161
+
162
+ return myComponent;
163
+ }
164
+
165
+ function UserProfile(props: UserProfileProps) {
166
+ const { value, userId: originUserId, user_id, accountType = 'apaas' } = props;
167
+
168
+ const userId =
169
+ (typeof value === 'string'
170
+ ? value
171
+ : (value as any)?.user_id || (value as any)?.userId) ||
172
+ user_id ||
173
+ originUserId;
174
+
175
+ const cardRef = useRef<HTMLDivElement>(null);
176
+ const [loading, setLoading] = useState<boolean>(true);
177
+ const redirectURLRef = useRef<string>('');
178
+ const [response, setResponse] = useState<BaseUserProfileProps>(
179
+ {} as BaseUserProfileProps,
180
+ );
181
+ const [error, setError] = useState<boolean>(false);
182
+
183
+ const onAuthError = useCallback(() => {
184
+ globalThis.webComponent.onAuthError(function (error: Error) {
185
+ const errorMessage = JSON.parse(error.message);
186
+ // 存在token过期情况,但后端无法识别,需要前端这边主动识别error,并进行鉴权重定向
187
+ if (
188
+ errorMessage?.msg?.code === 20442 &&
189
+ errorMessage?.msg?.msg === 'jsapi-ticket not exist'
190
+ ) {
191
+ globalThis.location.replace(redirectURLRef.current);
192
+ }
193
+ });
194
+
195
+ globalThis.webComponent.onError(function (error: Error) {
196
+ logger.info('webComponent onError', error);
197
+ });
198
+ }, []);
199
+
200
+ // 按需注入飞书 H5 JS SDK,仅在组件使用时加载
201
+ const larkSdkURl =
202
+ 'https://lf3-cdn-tos.bytegoofy.com/obj/goofy/locl/lark/external_js_sdk/h5-js-sdk-1.2.21.js';
203
+ const scriptOptions = useMemo(
204
+ () => ({ onloadCallback: onAuthError }),
205
+ [onAuthError],
206
+ );
207
+ const scriptStatus = useExternalScript(larkSdkURl, scriptOptions);
208
+
209
+ const fetchData = useCallback(async () => {
210
+ if (!userId) return;
211
+
212
+ const controller = new AbortController();
213
+
214
+ setLoading(true);
215
+ setError(false);
216
+
217
+ try {
218
+ const data = await fetchUserProfile(userId, accountType, controller.signal);
219
+ setResponse(data as BaseUserProfileProps);
220
+
221
+ if (data.useLarkCard && !data.larkCardParam.needRedirect) {
222
+ redirectURLRef.current = data.larkCardParam.redirectURL ?? '';
223
+ await renderLarkProfile({
224
+ ...data.larkCardParam,
225
+ cardRef,
226
+ });
227
+ setLoading(false);
228
+ } else {
229
+ setLoading(false);
230
+ }
231
+ } catch (error: unknown) {
232
+ if (error instanceof Error && error.name === 'AbortError') {
233
+ // 直接退出,不处理 AbortError
234
+ } else {
235
+ // 最终的UI反馈
236
+ setError(true);
237
+ setLoading(false);
238
+ }
239
+ }
240
+ }, [userId, accountType]);
241
+
242
+ useEffect(() => {
243
+ if (scriptStatus !== 'ready') return;
244
+
245
+ void fetchData();
246
+ }, [fetchData, scriptStatus]);
247
+
248
+ if (error) {
249
+ return (
250
+ <Card
251
+ ref={cardRef}
252
+ className="flex min-h-124 w-80 flex-col items-center justify-center gap-4 border-0 p-0"
253
+ >
254
+ <ErrorImage />
255
+ {/* <Button variant="outline" className="border-red-500 text-black hover:bg-red-50" onClick={fetchData}>
256
+ 加载失败 请重试
257
+ </Button> */}
258
+ <div>
259
+ <span className="text-sm">加载失败 请</span>
260
+ <Button
261
+ size="sm"
262
+ variant="ghost"
263
+ className="pr-1 pl-1 text-primary! not-disabled:hover:bg-primary/10 not-disabled:hover:text-primary/90 focus-visible:bg-primary/20"
264
+ onClick={fetchData}
265
+ >
266
+ 重试
267
+ </Button>
268
+ </div>
269
+ </Card>
270
+ );
271
+ }
272
+ return (
273
+ <Card
274
+ ref={cardRef}
275
+ className="flex h-130 w-80 flex-col items-center justify-center gap-6 border-0 p-0"
276
+ >
277
+ {loading ? <LoadingComponent /> : <BaseUserProfile {...response} />}
278
+ </Card>
279
+ );
280
+ }
281
+
282
+ function LoadingComponent() {
283
+ return <Spinner className="size-8 animate-spin text-primary" />;
284
+ }
285
+
286
+ function BaseUserProfile(props: BaseUserProfileProps) {
287
+ const { useLarkCard } = props;
288
+ if (!useLarkCard) {
289
+ const { userProfileInfo } = props as BaseSimpleProfileProps;
290
+
291
+ return <SimpleUserProfile userProfileInfo={userProfileInfo} />;
292
+ }
293
+
294
+ const { larkCardParam } = props;
295
+ const { needRedirect, redirectURL } = larkCardParam;
296
+ if (needRedirect) {
297
+ if (!redirectURL) {
298
+ return null;
299
+ }
300
+
301
+ globalThis.location.replace(redirectURL);
302
+ return null;
303
+ }
304
+ }
305
+
306
+ function SimpleUserProfile(props: {
307
+ readonly userProfileInfo: BaseSimpleProfileProps['userProfileInfo'];
308
+ }) {
309
+ const { userProfileInfo } = props;
310
+
311
+ return (
312
+ <Card className="flex w-80 flex-col gap-0 overflow-hidden border-0 p-0">
313
+ <div className="relative h-34 w-full">
314
+ <img
315
+ src={getAssetsUrl(
316
+ '/obj/eden-cn/lm-zhhwh/ljhwZthlaukjlkulzlp/ui/bg.png',
317
+ )}
318
+ alt="cover"
319
+ className="absolute inset-0 h-full w-full object-cover"
320
+ loading="lazy"
321
+ width={320}
322
+ height={112}
323
+ />
324
+ </div>
325
+
326
+ <CardContent className="h-96 w-80 pt-0 pr-4 pb-4 pl-4">
327
+ <div className="relative">
328
+ <Avatar className="absolute left-0 size-22.5 -translate-y-1/2 border-2 border-background">
329
+ <AvatarImage
330
+ src={userProfileInfo.avatar}
331
+ alt={userProfileInfo.name}
332
+ />
333
+ <AvatarFallback>{userProfileInfo.name?.at(0)}</AvatarFallback>
334
+ </Avatar>
335
+
336
+ <div className="pt-13.5">
337
+ <div className="flex items-center gap-2 pt-0.5 pb-1.5 leading-tight font-semibold">
338
+ <span className="text-xl">{userProfileInfo.name}</span>
339
+ {userProfileInfo.userType === '_externalUser' && (
340
+ <Badge className="rounded-sm border-0 bg-blue-500/20 pt-px pr-1.5 pb-px pl-1 text-sm text-blue-900">
341
+ 外部
342
+ </Badge>
343
+ )}
344
+ {AccountStatusMap[userProfileInfo.userStatus] !== '' && (
345
+ <Badge className="rounded-sm border-0 bg-foreground/10 pt-px pr-1.5 pb-px pl-1 text-sm text-foreground">
346
+ {AccountStatusMap[userProfileInfo.userStatus]}
347
+ </Badge>
348
+ )}
349
+ </div>
350
+ {userProfileInfo.email && (
351
+ <div className="mt-6 grid grid-cols-[84px_1fr] gap-y-3 text-sm">
352
+ <div className="text-muted-foreground">邮箱</div>
353
+ <div>
354
+ <a
355
+ className="break-all text-primary underline-offset-4 hover:underline"
356
+ href={`mailto:${userProfileInfo.email ?? ''}`}
357
+ >
358
+ {userProfileInfo.email}
359
+ </a>
360
+ </div>
361
+ </div>
362
+ )}
363
+ </div>
364
+ </div>
365
+ </CardContent>
366
+ </Card>
367
+ );
368
+ }
369
+
370
+ export { UserProfile };
@@ -0,0 +1,28 @@
1
+ export { UserSelect } from '@client/src/components/business-ui/user-select/user-select';
2
+ export type {
3
+ User,
4
+ UserSelectProps,
5
+ UserSelectValue,
6
+ UserSelectItemValue as UserValue,
7
+ ValueType,
8
+ } from '@client/src/components/business-ui/user-select/types';
9
+ export {
10
+ extractIdsFromValue,
11
+ isUnregisteredExternalContact,
12
+ } from '@client/src/components/business-ui/user-select/utils';
13
+ export {
14
+ searchUsers,
15
+ convertExternalContact,
16
+ type AccountType,
17
+ type SearchUsersParams,
18
+ } from '@client/src/components/business-ui/api/users/service';
19
+ export {
20
+ useUsersByIds,
21
+ clearUserCache,
22
+ userQueries,
23
+ } from '@client/src/components/business-ui/api/users/queries';
24
+ export { ItemPill } from '@client/src/components/business-ui/entity-combobox/item-pill';
25
+ export {
26
+ UserPill,
27
+ type UserPillProps,
28
+ } from '@client/src/components/business-ui/user-select/user-pill';
@@ -0,0 +1,109 @@
1
+ import type {
2
+ BaseEntitySelectProps,
3
+ ItemValue,
4
+ } from '@client/src/components/business-ui/entity-combobox/shared-types';
5
+ import type { User, I18nText, UserType, Department } from '@client/src/components/business-ui/types/user';
6
+
7
+ // 统一从 @lark-apaas/client-toolkit 导入 AccountType
8
+ export type { AccountType } from '@lark-apaas/client-toolkit/tools/services';
9
+ import type { AccountType } from '@lark-apaas/client-toolkit/tools/services';
10
+
11
+ // 重新导出统一的类型
12
+ export type { User, I18nText, UserType, Department };
13
+
14
+ /**
15
+ * 值类型
16
+ * - 'string': value 为 string 或 string[](与 multiple 联动)
17
+ * - 'object': value 为 User 或 User[](与 multiple 联动)
18
+ * @default 'string'
19
+ */
20
+ export type ValueType = 'string' | 'object';
21
+
22
+ export type SearchAvatar = {
23
+ avatar: {
24
+ image: {
25
+ large: string;
26
+ };
27
+ };
28
+ };
29
+
30
+ /**
31
+ * UserSelect 的值类型(包装 User)
32
+ */
33
+ export type UserSelectItemValue = ItemValue<User>;
34
+
35
+ /**
36
+ * 根据 valueType 和 multiple 推断 value 类型
37
+ */
38
+ export type InferValue<
39
+ V extends ValueType,
40
+ M extends boolean,
41
+ > = V extends 'string'
42
+ ? M extends true
43
+ ? string[]
44
+ : string | null
45
+ : M extends true
46
+ ? User[] // ✅ 直接返回标准 User 类型
47
+ : User | null; // ✅ 直接返回标准 User 类型
48
+
49
+ type UserSelectBaseProps<
50
+ V extends ValueType = 'string',
51
+ M extends boolean = false,
52
+ > = Omit<
53
+ BaseEntitySelectProps<UserSelectItemValue>,
54
+ 'value' | 'defaultValue' | 'onChange'
55
+ > & {
56
+ /**
57
+ * 值类型
58
+ * - 'string': value 为 string 或 string[](与 multiple 联动)
59
+ * - 'object': value 为 User 或 User[](与 multiple 联动)
60
+ * @default 'string'
61
+ */
62
+ valueType?: V;
63
+ /**
64
+ * 是否多选
65
+ * @default false
66
+ */
67
+ multiple?: M;
68
+ /**
69
+ * 账户类型,决定 value 中 id 字段的来源
70
+ * @default 'apaas'
71
+ */
72
+ accountType?: AccountType;
73
+ /**
74
+ * 当前值
75
+ */
76
+ value?: InferValue<V, M>;
77
+ /**
78
+ * 默认值
79
+ */
80
+ defaultValue?: InferValue<V, M>;
81
+ /**
82
+ * 值变化回调
83
+ */
84
+ onChange?: (value: InferValue<V, M>) => void;
85
+ };
86
+
87
+ /**
88
+ * UserSelect 组件 Props
89
+ *
90
+ * 类型组合:
91
+ * | valueType | multiple | value 类型 |
92
+ * |-----------|----------|------------|
93
+ * | 'string' | false | string \| null |
94
+ * | 'string' | true | string[] |
95
+ * | 'object' | false | User \| null |
96
+ * | 'object' | true | User[] |
97
+ */
98
+ export type UserSelectProps =
99
+ | UserSelectBaseProps<'string', false>
100
+ | UserSelectBaseProps<'string', true>
101
+ | UserSelectBaseProps<'object', false>
102
+ | UserSelectBaseProps<'object', true>;
103
+
104
+ export type UserSelectValue =
105
+ | User
106
+ | User[]
107
+ | string
108
+ | string[]
109
+ | null;
@@ -0,0 +1,160 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useMemo } from 'react';
4
+
5
+ import { useUsersByIds } from '@client/src/components/business-ui/api/users/queries';
6
+ import type {
7
+ AccountType,
8
+ UserSelectItemValue,
9
+ UserSelectValue,
10
+ ValueType,
11
+ } from '@client/src/components/business-ui/user-select/types';
12
+ import {
13
+ extractIdsFromValue,
14
+ userInfoToUser,
15
+ createUnknownUser,
16
+ } from '@client/src/components/business-ui/user-select/utils';
17
+ import { getI18nText } from '@client/src/components/business-ui/utils/user';
18
+ import type { User } from '@client/src/components/business-ui/types/user';
19
+
20
+ /**
21
+ * 将 User 转换为 UserSelectItemValue
22
+ */
23
+ function userToItemValue(user: User, accountType: AccountType): UserSelectItemValue {
24
+ return {
25
+ id: accountType === 'lark' ? (user.larkUserId || user.user_id || '') : (user.user_id || user.larkUserId || ''),
26
+ name: getI18nText(user.name) || '未知用户',
27
+ avatar: user.avatar,
28
+ raw: user,
29
+ };
30
+ }
31
+
32
+ export type UseUserValueResult = {
33
+ /**
34
+ * 标准化后的内部 value(UserSelectItemValue 对象格式)
35
+ * 用于传递给 BaseCombobox
36
+ */
37
+ internalValue: UserSelectItemValue | UserSelectItemValue[] | null;
38
+
39
+ /**
40
+ * 是否正在加载用户信息
41
+ */
42
+ isLoading: boolean;
43
+
44
+ /**
45
+ * 将内部 UserSelectItemValue 对象转换回外部格式
46
+ * ID 模式时返回字符串,对象模式时返回 UserSelectItemValue
47
+ */
48
+ toExternalValue: (
49
+ internalVal: UserSelectItemValue | UserSelectItemValue[] | null,
50
+ multiple: boolean,
51
+ ) => UserSelectValue;
52
+ };
53
+
54
+ /**
55
+ * 处理 UserSelect 的 value 类型转换
56
+ *
57
+ * - 根据 valueType 决定 value 是 ID 字符串还是 UserSelectItemValue 对象
58
+ * - ID 模式下调用 API 获取完整用户信息(使用 react-query 缓存)
59
+ * - 无效 ID 生成 "未知用户" 占位
60
+ *
61
+ * @param value - 外部传入的 value
62
+ * @param multiple - 是否多选模式
63
+ * @param accountType - 账户类型
64
+ * @param valueType - 值类型,'string' 表示 ID 模式,'object' 表示 UserSelectItemValue 对象模式
65
+ */
66
+ export function useUserValue(
67
+ value: UserSelectValue,
68
+ multiple: boolean = false,
69
+ accountType: AccountType = 'apaas',
70
+ valueType: ValueType = 'string',
71
+ ): UseUserValueResult {
72
+ const isIdMode = valueType === 'string';
73
+
74
+ // 提取当前 value 中的所有 ID
75
+ const currentIds = useMemo(() => extractIdsFromValue(value), [value]);
76
+
77
+ // ID 模式下需要获取的 ID 列表(去重和排序,确保相同的 ID 集合有相同的 queryKey)
78
+ const idsToFetch = useMemo(() => {
79
+ if (!isIdMode) return [];
80
+ return [...new Set(currentIds.map(String))].filter(Boolean).sort();
81
+ }, [isIdMode, currentIds]);
82
+
83
+ // 使用 react-query hook 获取用户信息
84
+ const { data: response, isLoading } = useUsersByIds(idsToFetch, accountType);
85
+
86
+ // 构建 ID -> UserSelectItemValue 的映射
87
+ const usersMap = useMemo(() => {
88
+ const map = new Map<string, UserSelectItemValue>();
89
+ const userInfoMap = response?.data?.userInfoMap || {};
90
+
91
+ for (const id of idsToFetch) {
92
+ const userInfo = userInfoMap[id];
93
+ const user = userInfo
94
+ ? userInfoToUser(userInfo, accountType)
95
+ : createUnknownUser(id);
96
+ map.set(String(id), user);
97
+ }
98
+ return map;
99
+ }, [response, idsToFetch, accountType]);
100
+
101
+ const internalValue = useMemo((): UserSelectItemValue | UserSelectItemValue[] | null => {
102
+ if (value === null || value === undefined) {
103
+ return multiple ? [] : null;
104
+ }
105
+
106
+ if (Array.isArray(value) && value.length === 0) {
107
+ return [];
108
+ }
109
+
110
+ if (!isIdMode) {
111
+ // Object 模式:将 User 转换为 UserSelectItemValue
112
+ if (Array.isArray(value)) {
113
+ return (value as User[]).map((user) => userToItemValue(user, accountType));
114
+ }
115
+ return userToItemValue(value as User, accountType);
116
+ }
117
+
118
+ const users = currentIds
119
+ .map((id) => usersMap.get(String(id)))
120
+ .filter((u): u is UserSelectItemValue => u !== undefined);
121
+
122
+ if (multiple) {
123
+ return users;
124
+ }
125
+
126
+ return users[0] ?? null;
127
+ }, [value, isIdMode, currentIds, usersMap, multiple, accountType]);
128
+
129
+ const toExternalValue = useCallback(
130
+ (
131
+ internalVal: UserSelectItemValue | UserSelectItemValue[] | null,
132
+ isMultiple: boolean,
133
+ ): UserSelectValue => {
134
+ if (internalVal === null) {
135
+ return null;
136
+ }
137
+
138
+ if (isIdMode) {
139
+ // ID 模式:返回 string 或 string[]
140
+ if (isMultiple) {
141
+ return (internalVal as UserSelectItemValue[]).map((u) => String(u.id));
142
+ }
143
+ return String((internalVal as UserSelectItemValue).id);
144
+ }
145
+
146
+ // Object 模式:提取 raw 数据,返回标准 User 类型
147
+ if (isMultiple) {
148
+ return (internalVal as UserSelectItemValue[]).map((u) => u.raw!);
149
+ }
150
+ return (internalVal as UserSelectItemValue).raw!;
151
+ },
152
+ [isIdMode],
153
+ );
154
+
155
+ return {
156
+ internalValue,
157
+ isLoading,
158
+ toExternalValue,
159
+ };
160
+ }