@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,42 @@
1
+ 'use client';
2
+
3
+ import { useQuery, queryOptions, type QueryClient } from '@tanstack/react-query';
4
+
5
+ import { fetchUserProfile, type AccountType } from './service';
6
+
7
+ export const userProfileQueries = {
8
+ all: () => ['userProfiles'] as const,
9
+ byId: (userId: string, accountType: AccountType = 'apaas') =>
10
+ queryOptions({
11
+ queryKey: [...userProfileQueries.all(), 'byId', accountType, userId],
12
+ queryFn: ({ signal }) => fetchUserProfile(userId, accountType, signal),
13
+ staleTime: 1 * 60 * 1000,
14
+ gcTime: 5 * 60 * 1000,
15
+ enabled: !!userId,
16
+ }),
17
+ };
18
+
19
+ export type UseUserProfileOptions = {
20
+ userId: string;
21
+ accountType?: AccountType;
22
+ enabled?: boolean;
23
+ };
24
+
25
+ /**
26
+ * 获取用户 Profile 卡片数据的 hook
27
+ */
28
+ export function useUserProfile(options: UseUserProfileOptions) {
29
+ const { userId, accountType = 'apaas', enabled = true } = options;
30
+
31
+ return useQuery({
32
+ ...userProfileQueries.byId(userId, accountType),
33
+ enabled: enabled && !!userId,
34
+ });
35
+ }
36
+
37
+ /**
38
+ * 清除用户 Profile 缓存
39
+ */
40
+ export function clearUserProfileCache(queryClient: QueryClient): void {
41
+ queryClient.invalidateQueries({ queryKey: userProfileQueries.all() });
42
+ }
@@ -0,0 +1,23 @@
1
+ import {
2
+ UserProfileService,
3
+ getAssetsUrl,
4
+ type AccountType,
5
+ type UserProfileData,
6
+ } from '@lark-apaas/client-toolkit/tools/services';
7
+
8
+ const userProfileService = new UserProfileService();
9
+
10
+ /**
11
+ * 获取用户 Profile 卡片数据
12
+ */
13
+ export async function fetchUserProfile(
14
+ userId: string,
15
+ accountType: AccountType = 'apaas',
16
+ signal?: AbortSignal,
17
+ ): Promise<UserProfileData> {
18
+ return userProfileService.getUserProfile(userId, accountType, signal);
19
+ }
20
+
21
+ // Re-export for convenience
22
+ export { getAssetsUrl };
23
+ export type { AccountType, UserProfileData };
@@ -0,0 +1,34 @@
1
+ 'use client';
2
+
3
+ import { useQuery, queryOptions, type QueryClient } from '@tanstack/react-query';
4
+
5
+ import { listUsersByIds, type AccountType } from './service';
6
+
7
+ export const userQueries = {
8
+ all: () => ['users'] as const,
9
+ byIds: (userIds: string[], accountType: AccountType = 'apaas') =>
10
+ queryOptions({
11
+ queryKey: [...userQueries.all(), 'byIds', accountType, userIds.join(',')],
12
+ queryFn: () => listUsersByIds(userIds),
13
+ staleTime: 1 * 60 * 1000,
14
+ gcTime: 5 * 60 * 1000,
15
+ enabled: userIds.length > 0,
16
+ }),
17
+ };
18
+
19
+ /**
20
+ * 批量获取用户信息的 hook
21
+ */
22
+ export function useUsersByIds(
23
+ userIds: string[],
24
+ accountType: AccountType = 'apaas',
25
+ ) {
26
+ return useQuery(userQueries.byIds(userIds, accountType));
27
+ }
28
+
29
+ /**
30
+ * 清除用户缓存
31
+ */
32
+ export function clearUserCache(queryClient: QueryClient): void {
33
+ queryClient.invalidateQueries({ queryKey: userQueries.all() });
34
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ UserService,
3
+ type AccountType,
4
+ type SearchUsersParams,
5
+ type SearchUsersResponse,
6
+ type BatchGetUsersResponse,
7
+ type ConvertExternalContactResponse,
8
+ } from '@lark-apaas/client-toolkit/tools/services';
9
+
10
+ const userService = new UserService();
11
+
12
+ /**
13
+ * 搜索用户
14
+ */
15
+ export async function searchUsers(
16
+ params: SearchUsersParams,
17
+ ): Promise<SearchUsersResponse> {
18
+ return userService.searchUsers({ ...params, searchExternalContact: true });
19
+ }
20
+
21
+ /**
22
+ * 批量根据用户 ID 查询用户信息
23
+ */
24
+ export async function listUsersByIds(
25
+ userIds: string[],
26
+ ): Promise<BatchGetUsersResponse> {
27
+ const numericIds = userIds.filter((id) => !isNaN(Number(id)));
28
+ return userService.listUsersByIds(numericIds);
29
+ }
30
+
31
+ /**
32
+ * 调用开户接口,将外部联系人的 larkUserID 转换为已注册用户
33
+ */
34
+ export async function convertExternalContact(
35
+ larkUserID: string,
36
+ ): Promise<ConvertExternalContactResponse> {
37
+ return userService.convertExternalContact(larkUserID);
38
+ }
39
+
40
+ export type { AccountType, SearchUsersParams, SearchUsersResponse, BatchGetUsersResponse, ConvertExternalContactResponse };
@@ -0,0 +1,128 @@
1
+ # ChatSelect 群组选择组件
2
+
3
+ **组件路径**:`@/components/business-ui/chat-select`
4
+
5
+ ---
6
+
7
+ ## 群组类型
8
+
9
+ ```typescript
10
+ /** 群组数据结构 */
11
+ interface Chat {
12
+ /** 唯一标识:对应 chatID */
13
+ id: string;
14
+ /** 群组名称:内部优先 zh_cn,其次 en_us */
15
+ name: string;
16
+ /** 头像:URL 或 16 进制 RGB 颜色 */
17
+ avatar: string;
18
+ }
19
+ ```
20
+
21
+ ---
22
+
23
+ ## 功能概述
24
+
25
+ - 群组字段选择组件,用于表单中录入"群组"字段
26
+ - 通过触发器打开弹层,弹层内置搜索功能(始终可用)
27
+ - **仅支持 ID 模式**:`value/onChange` 只使用群组 `chatID`(string),该 ID 为飞书群聊 ID。
28
+ - 组件在 UI 上可展示群组信息(头像/名称),但对外值仅返回/接收 `chatID`
29
+ - 选择后自动回显已选群组信息:群头像 + 群名称(多选以 tag 形式展示)
30
+ - 组件自带群组检索能力,无需额外传搜索接口/数据源
31
+
32
+ ## 类型定义
33
+
34
+ ```typescript
35
+ /** 触发器类型 */
36
+ type TriggerType = 'button' | 'search' | 'custom';
37
+
38
+ /** 单选模式 Props */
39
+ interface ChatSelectSingleProps {
40
+ /** 当前选中的群组 ID,空值为 null */
41
+ value: string | null;
42
+ /** 选中变化回调 */
43
+ onChange: (value: string | null) => void;
44
+ /** 是否多选,单选时为 false 或不传 */
45
+ multiple?: false;
46
+ /** 触发器类型,仅影响样式,不影响搜索功能 */
47
+ triggerType?: TriggerType;
48
+ /** 是否禁用 */
49
+ disabled?: boolean;
50
+ /** 占位文本 */
51
+ placeholder?: string;
52
+ }
53
+
54
+ /** 多选模式 Props */
55
+ interface ChatSelectMultipleProps {
56
+ /** 当前选中的群组 ID 列表,空值为 [] */
57
+ value: string[];
58
+ /** 选中变化回调 */
59
+ onChange: (value: string[]) => void;
60
+ /** 是否多选,多选时为 true */
61
+ multiple: true;
62
+ /** 触发器类型 */
63
+ triggerType?: TriggerType;
64
+ /** 是否禁用 */
65
+ disabled?: boolean;
66
+ /** 占位文本 */
67
+ placeholder?: string;
68
+ }
69
+
70
+ type ChatSelectProps = ChatSelectSingleProps | ChatSelectMultipleProps;
71
+ ```
72
+
73
+ ## Props 说明
74
+
75
+ | 属性 | 类型 | 默认值 | 说明 |
76
+ |------|------|--------|------|
77
+ | `value` | `string \| null` / `string[]` | - | **必填**。单选传 `string \| null`(群组 ID),多选传 `string[]` |
78
+ | `onChange` | `function` | - | **必填**。值变化回调,返回值形态与 `value` 匹配 |
79
+ | `multiple` | `boolean` | `false` | 是否多选 |
80
+ | `triggerType` | `'button' \| 'search' \| 'custom'` | `'button'` | 触发器类型,仅影响样式 |
81
+ | `disabled` | `boolean` | `false` | 是否禁用 |
82
+ | `placeholder` | `string` | `'请选择'` | 占位文本 |
83
+ | `getOptionDisabled` | `(chat) => boolean` | - | 判断选项是否禁用 |
84
+
85
+ ## 受控规范
86
+
87
+ > ⚠️ **强约束**
88
+
89
+ 1. **必须受控**:始终传 `value` + `onChange`
90
+ 2. **禁止传**:`undefined` / 群组对象 / 其他类型
91
+ 3. **空值规则**:
92
+ - 单选清空:`null`
93
+ - 多选清空:`[]`
94
+
95
+ ## 使用示例
96
+
97
+ ```tsx
98
+ import { useState } from 'react';
99
+ import { ChatSelect } from '@/components/business-ui/chat-select';
100
+
101
+ // 单选
102
+ function SingleSelectExample() {
103
+ const [chatId, setChatId] = useState<string | null>(null);
104
+
105
+ return (
106
+ <ChatSelect
107
+ value={chatId}
108
+ onChange={setChatId}
109
+ placeholder="请选择群组"
110
+ />
111
+ );
112
+ }
113
+
114
+ // 多选
115
+ function MultipleSelectExample() {
116
+ const [chatIds, setChatIds] = useState<string[]>([]);
117
+
118
+ return (
119
+ <ChatSelect
120
+ multiple
121
+ value={chatIds}
122
+ onChange={setChatIds}
123
+ placeholder="请选择群组"
124
+ getOptionDisabled={(chat) => chat.id === 'disabled-id'}
125
+ />
126
+ );
127
+ }
128
+ ```
@@ -0,0 +1,85 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import type { Chat } from '@client/src/components/business-ui/chat-select/types';
6
+ import { BaseComboboxItem } from '@client/src/components/business-ui/entity-combobox/base-combobox-item';
7
+ import { HighlightText } from '@client/src/components/business-ui/entity-combobox/highlight-text';
8
+ import { isHexColor } from '@client/src/components/business-ui/entity-combobox/item-pill';
9
+ import {
10
+ listItemAvatarVariants,
11
+ listItemNameVariants,
12
+ listItemSubTextVariants,
13
+ listItemVariants,
14
+ type ComboboxSize,
15
+ } from '@client/src/components/business-ui/entity-combobox/size-variants';
16
+ import { cn } from '@/lib/utils';
17
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
18
+
19
+ const ExternalChatTag = () => (
20
+ <span className="inline-flex shrink-0 items-center rounded-sm bg-blue-500/20 px-1.5 py-0.5 text-xs leading-4 text-blue-900 dark:text-blue-200">
21
+ 外部
22
+ </span>
23
+ );
24
+
25
+ export const ChatItem = ({
26
+ chatValue,
27
+ isSelected,
28
+ className,
29
+ size = 'medium',
30
+ searchKeyword,
31
+ disabled,
32
+ }: {
33
+ chatValue: Chat;
34
+ isSelected: boolean;
35
+ className?: string;
36
+ size?: ComboboxSize;
37
+ searchKeyword?: string;
38
+ disabled?: boolean;
39
+ }) => {
40
+ const chat = chatValue.raw;
41
+ const displayName = chat?.name?.zh_cn || chat?.name?.en_us || chatValue.name;
42
+ const userCount = chat?.userCount;
43
+ const isExternal = chat?.isExternal;
44
+
45
+ const isHex = isHexColor(chat?.avatar);
46
+
47
+ const avatarImage = !isHex ? (
48
+ <AvatarImage src={chat?.avatar} alt={displayName} loading="eager" />
49
+ ) : (
50
+ <AvatarFallback style={{ backgroundColor: chat?.avatar }}>
51
+ {displayName?.charAt?.(0).toUpperCase()}
52
+ </AvatarFallback>
53
+ );
54
+
55
+ return (
56
+ <BaseComboboxItem
57
+ item={chatValue}
58
+ getItemValue={(item) => item}
59
+ isSelected={isSelected}
60
+ className={cn(listItemVariants({ size }), className)}
61
+ disabled={disabled}
62
+ >
63
+ <Avatar className={listItemAvatarVariants({ size })}>
64
+ {chat?.avatar ? (
65
+ avatarImage
66
+ ) : (
67
+ <AvatarFallback>{displayName?.charAt?.(0)}</AvatarFallback>
68
+ )}
69
+ </Avatar>
70
+ <div className="flex min-w-0 flex-1 items-center gap-1">
71
+ <HighlightText
72
+ text={chatValue.name}
73
+ highlight={searchKeyword}
74
+ className={cn(listItemNameVariants({ size }), 'truncate')}
75
+ />
76
+ {userCount != null && (
77
+ <span className={cn(listItemSubTextVariants({ size }), 'shrink-0')}>
78
+ ({userCount})
79
+ </span>
80
+ )}
81
+ {isExternal && <ExternalChatTag />}
82
+ </div>
83
+ </BaseComboboxItem>
84
+ );
85
+ };
@@ -0,0 +1,83 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { X } from 'lucide-react';
6
+
7
+ import type { Chat } from '@client/src/components/business-ui/chat-select/types';
8
+ import {
9
+ ItemPill,
10
+ renderPillAvatar,
11
+ } from '@client/src/components/business-ui/entity-combobox/item-pill';
12
+ import {
13
+ tagCloseIconVariants,
14
+ type ComboboxSize,
15
+ } from '@client/src/components/business-ui/entity-combobox/size-variants';
16
+ import { Skeleton } from '@/components/ui/skeleton';
17
+
18
+ export const ChatSelectTag = ({
19
+ chatValue,
20
+ onClose,
21
+ maxTextLength,
22
+ className,
23
+ size = 'medium',
24
+ disabled,
25
+ isLoading,
26
+ }: {
27
+ chatValue: Chat;
28
+ onClose: (value: Chat, e: React.MouseEvent) => void;
29
+ maxTextLength?: number;
30
+ className?: string;
31
+ size?: ComboboxSize;
32
+ disabled?: boolean;
33
+ /**
34
+ * 是否正在加载群组信息(ID 模式下查询中)
35
+ */
36
+ isLoading?: boolean;
37
+ }) => {
38
+ const chat = chatValue.raw;
39
+
40
+ const handleClose = (e: React.MouseEvent) => {
41
+ e.stopPropagation();
42
+ onClose(chatValue, e);
43
+ };
44
+
45
+ const closeButton = !disabled ? (
46
+ <button
47
+ type="button"
48
+ className="rounded-sm text-foreground opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none"
49
+ onClick={handleClose}
50
+ >
51
+ <X className={tagCloseIconVariants({ size })} />
52
+ <span className="sr-only">移除 {chatValue.name}</span>
53
+ </button>
54
+ ) : null;
55
+
56
+ // 加载中状态显示 Skeleton
57
+ if (isLoading && !chat) {
58
+ return (
59
+ <div className="inline-flex items-center gap-1 rounded-md bg-secondary px-1.5 py-0.5">
60
+ <Skeleton className="h-4 w-4 rounded-full" />
61
+ <Skeleton className="h-3 w-12" />
62
+ </div>
63
+ );
64
+ }
65
+
66
+ const avatarElement = renderPillAvatar({
67
+ avatarUrl: chatValue.avatar || chat?.avatar,
68
+ label: chatValue.name,
69
+ size,
70
+ });
71
+
72
+ return (
73
+ <ItemPill
74
+ label={chatValue.name}
75
+ avatar={avatarElement}
76
+ className={className}
77
+ maxTextLength={maxTextLength}
78
+ suffix={closeButton}
79
+ size={size}
80
+ disabled={disabled}
81
+ />
82
+ );
83
+ };
@@ -0,0 +1,197 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useMemo } from 'react';
4
+
5
+ import { searchChats } from '@client/src/components/business-ui/api/chats/service';
6
+ import { ChatItem } from '@client/src/components/business-ui/chat-select/chat-item';
7
+ import { ChatSelectTag } from '@client/src/components/business-ui/chat-select/chat-select-tag';
8
+ import type {
9
+ Chat,
10
+ ChatSelectProps,
11
+ } from '@client/src/components/business-ui/chat-select/types';
12
+ import { useChatValue } from '@client/src/components/business-ui/chat-select/use-chat-value';
13
+ import { chatInfoToChat } from '@client/src/components/business-ui/chat-select/utils';
14
+ import { BaseCombobox } from '@client/src/components/business-ui/entity-combobox/base-combobox';
15
+ import { useEntityComboboxContext } from '@client/src/components/business-ui/entity-combobox/context';
16
+
17
+ function createChatsFetcher(pageSize: number = 100) {
18
+ return async (search: string) => {
19
+ const response = await searchChats({ query: search, pageSize });
20
+ const chatList = response?.data?.result?.chatResult?.items || [];
21
+ return { items: chatList.map((chat) => chatInfoToChat(chat)) };
22
+ };
23
+ }
24
+
25
+ // 内部包装组件用于获取 context 中的 size
26
+ const ChatItemWrapper = ({
27
+ chatValue,
28
+ isSelected,
29
+ className,
30
+ disabled,
31
+ }: {
32
+ chatValue: Chat;
33
+ isSelected: boolean;
34
+ className?: string;
35
+ disabled?: boolean;
36
+ }) => {
37
+ const { size, searchValue } = useEntityComboboxContext();
38
+ return (
39
+ <ChatItem
40
+ chatValue={chatValue}
41
+ isSelected={isSelected}
42
+ className={className}
43
+ size={size}
44
+ searchKeyword={searchValue}
45
+ disabled={disabled}
46
+ />
47
+ );
48
+ };
49
+
50
+ const ChatSelectTagWrapper = ({
51
+ chatValue,
52
+ onClose,
53
+ className,
54
+ disabled,
55
+ isLoading,
56
+ }: {
57
+ chatValue: Chat;
58
+ onClose: (value: Chat, e: React.MouseEvent) => void;
59
+ className?: string;
60
+ disabled?: boolean;
61
+ isLoading?: boolean;
62
+ }) => {
63
+ const { size } = useEntityComboboxContext();
64
+ return (
65
+ <ChatSelectTag
66
+ chatValue={chatValue}
67
+ onClose={onClose}
68
+ className={className}
69
+ size={size}
70
+ disabled={disabled}
71
+ isLoading={isLoading}
72
+ />
73
+ );
74
+ };
75
+
76
+ export const ChatSelect: React.FC<ChatSelectProps> = (props) => {
77
+ const {
78
+ size = 'medium',
79
+ triggerType = 'button',
80
+ renderTrigger,
81
+ multiple = false,
82
+ value,
83
+ valueType = 'string',
84
+ defaultValue,
85
+ onChange,
86
+ defaultOpen,
87
+ disabled,
88
+ autoFocus,
89
+ required,
90
+ name,
91
+ className,
92
+ classNames,
93
+ placeholder = '请选择',
94
+ emptyText = '没有匹配结果,换个关键词试试吧',
95
+ tagClosable,
96
+ maxTagCount,
97
+ onSelect,
98
+ onDeselect,
99
+ onClear,
100
+ onFocus,
101
+ onBlur,
102
+ slotProps,
103
+ getOptionDisabled,
104
+ } = props;
105
+
106
+ const isControlled = value !== undefined;
107
+
108
+ // 使用 useChatValue 处理不同类型的 value(包括 defaultValue)
109
+ const { internalValue, isLoading, toExternalValue } = useChatValue(
110
+ value ?? null,
111
+ multiple,
112
+ valueType,
113
+ );
114
+
115
+ // 同样处理 defaultValue,确保 ID 字符串类型的 defaultValue 也能正确解析
116
+ const { internalValue: internalDefaultValue } = useChatValue(
117
+ defaultValue ?? null,
118
+ multiple,
119
+ valueType,
120
+ );
121
+
122
+ const fetchFn = useMemo(() => createChatsFetcher(), []);
123
+
124
+ // 包装 onChange,根据输入模式转换输出格式
125
+ const handleChange = useCallback(
126
+ (newValue: Chat | Chat[] | null) => {
127
+ if (!onChange) return;
128
+
129
+ const externalValue = toExternalValue(newValue, multiple);
130
+ (onChange as (value: unknown) => void)(externalValue);
131
+ },
132
+ [onChange, toExternalValue, multiple],
133
+ );
134
+
135
+ const renderTagWithLoading = useCallback(
136
+ (
137
+ chatValue: Chat,
138
+ onClose: (value: Chat, e: React.MouseEvent) => void,
139
+ tagDisabled?: boolean,
140
+ ) => (
141
+ <ChatSelectTagWrapper
142
+ key={chatValue.id}
143
+ chatValue={chatValue}
144
+ onClose={onClose}
145
+ disabled={tagDisabled}
146
+ isLoading={isLoading}
147
+ />
148
+ ),
149
+ [isLoading],
150
+ );
151
+
152
+ return (
153
+ <BaseCombobox
154
+ autoFocus={autoFocus}
155
+ className={className}
156
+ classNames={classNames}
157
+ debounce={300}
158
+ defaultOpen={defaultOpen}
159
+ defaultValue={internalDefaultValue}
160
+ disabled={disabled}
161
+ emptyText={emptyText}
162
+ fetchFn={fetchFn}
163
+ getItemLabel={(chatValue: Chat) => chatValue.name}
164
+ getItemValue={(chatValue: Chat) => chatValue}
165
+ getOptionDisabled={getOptionDisabled}
166
+ maxTagCount={maxTagCount}
167
+ multiple={multiple}
168
+ name={name}
169
+ onBlur={onBlur}
170
+ onChange={handleChange}
171
+ onClear={onClear}
172
+ onDeselect={onDeselect}
173
+ onFocus={onFocus}
174
+ onSelect={onSelect}
175
+ placeholder={placeholder}
176
+ renderItem={(chatValue, isSelected, itemClassName, itemDisabled) => (
177
+ <ChatItemWrapper
178
+ key={chatValue.id}
179
+ chatValue={chatValue}
180
+ isSelected={isSelected}
181
+ className={itemClassName}
182
+ disabled={itemDisabled}
183
+ />
184
+ )}
185
+ renderTag={renderTagWithLoading}
186
+ renderTrigger={renderTrigger}
187
+ required={required}
188
+ searchPlaceholder=""
189
+ showSearch
190
+ size={size}
191
+ slotProps={slotProps}
192
+ tagClosable={tagClosable}
193
+ triggerType={triggerType}
194
+ value={isControlled ? internalValue : undefined}
195
+ />
196
+ );
197
+ };
@@ -0,0 +1,12 @@
1
+ export { ChatSelect } from '@client/src/components/business-ui/chat-select/chat-select';
2
+ export type {
3
+ ChatInfo,
4
+ Chat,
5
+ ChatSelectProps,
6
+ ChatSelectValue,
7
+ ValueType as ChatSelectValueType,
8
+ } from '@client/src/components/business-ui/chat-select/types';
9
+ export {
10
+ extractIdsFromValue,
11
+ isIdModeValue,
12
+ } from '@client/src/components/business-ui/chat-select/utils';