@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,281 @@
1
+ 'use client';
2
+
3
+ import { Plus } from 'lucide-react';
4
+
5
+ import { useEntityComboboxContext } from '@client/src/components/business-ui/entity-combobox/context';
6
+ import { SearchTrigger } from '@client/src/components/business-ui/entity-combobox/search-trigger';
7
+ import {
8
+ addButtonIconVariants,
9
+ addButtonVariants,
10
+ type ComboboxSize,
11
+ } from '@client/src/components/business-ui/entity-combobox/size-variants';
12
+ import type {
13
+ ClassNamesConfig,
14
+ ItemValue,
15
+ TriggerRenderProps,
16
+ TriggerType,
17
+ } from '@client/src/components/business-ui/entity-combobox/types';
18
+ import { cn } from '@/lib/utils';
19
+ import { Badge } from '@client/src/components/ui/badge';
20
+ import { Button } from '@client/src/components/ui/button';
21
+ import {
22
+ Tooltip,
23
+ TooltipContent,
24
+ TooltipTrigger,
25
+ } from '@client/src/components/ui/tooltip';
26
+
27
+ export type BaseComboboxTriggerProps = {
28
+ triggerType?: TriggerType;
29
+ size?: ComboboxSize;
30
+ placeholder?: string;
31
+ renderTrigger?: (props: TriggerRenderProps) => React.ReactNode;
32
+ maxTagCount?: number | 'responsive';
33
+ maxTagTextLength?: number;
34
+ tagClosable?: boolean;
35
+ classNames?: ClassNamesConfig;
36
+ onFocus?: (e: React.FocusEvent<HTMLElement>) => void;
37
+ onBlur?: (e: React.FocusEvent<HTMLElement>) => void;
38
+ renderTag?: (
39
+ value: ItemValue,
40
+ onClose: (value: ItemValue, e: React.MouseEvent) => void,
41
+ disabled?: boolean,
42
+ ) => React.ReactNode;
43
+ /**
44
+ * 判断选项是否禁用
45
+ */
46
+ getOptionDisabled?: (value: ItemValue) => boolean;
47
+ /**
48
+ * button 模式下,自定义渲染加号按钮的包裹器
49
+ * 用于将按钮包裹在 PopoverTrigger 中,使浮层跟随加号按钮而非整个容器
50
+ */
51
+ renderAddButton?: (button: React.ReactNode) => React.ReactNode;
52
+ ref?: React.Ref<HTMLButtonElement>;
53
+ } & Omit<React.ComponentPropsWithoutRef<'button'>, 'children'>;
54
+
55
+ export const BaseComboboxTrigger = ({
56
+ triggerType = 'button',
57
+ size = 'medium',
58
+ placeholder = '请选择',
59
+ renderTrigger,
60
+ maxTagCount,
61
+ maxTagTextLength,
62
+ tagClosable = true,
63
+ classNames,
64
+ onFocus,
65
+ onBlur,
66
+ renderTag,
67
+ getOptionDisabled,
68
+ renderAddButton,
69
+ ref,
70
+ ...props
71
+ }: BaseComboboxTriggerProps) => {
72
+ const {
73
+ selectedValue,
74
+ multiple,
75
+ open,
76
+ disabled,
77
+ handleClear,
78
+ handleDeselect,
79
+ } = useEntityComboboxContext();
80
+
81
+ // 自定义渲染 (custom 类型)
82
+ if (triggerType === 'custom' && renderTrigger) {
83
+ return <>{renderTrigger({ selectedValue, multiple, open, disabled })}</>;
84
+ }
85
+
86
+ // search 类型
87
+ if (triggerType === 'search') {
88
+ return (
89
+ <SearchTrigger
90
+ size={size}
91
+ placeholder={placeholder}
92
+ maxTagCount={maxTagCount}
93
+ maxTagTextLength={maxTagTextLength}
94
+ tagClosable={tagClosable}
95
+ classNames={classNames}
96
+ onFocus={onFocus as (e: React.FocusEvent<HTMLDivElement>) => void}
97
+ onBlur={onBlur as (e: React.FocusEvent<HTMLDivElement>) => void}
98
+ renderTag={renderTag}
99
+ getOptionDisabled={getOptionDisabled}
100
+ />
101
+ );
102
+ }
103
+
104
+ // button 类型(默认)
105
+ const hasValue = !multiple
106
+ ? !!selectedValue
107
+ : Array.isArray(selectedValue) && selectedValue.length > 0;
108
+
109
+ // 计算最大显示标签数
110
+ const maxDisplayCount =
111
+ typeof maxTagCount === 'number' ? maxTagCount : Infinity;
112
+
113
+ // 计算隐藏的标签
114
+ const hiddenCount =
115
+ multiple && Array.isArray(selectedValue) && typeof maxTagCount === 'number'
116
+ ? Math.max(0, selectedValue.length - maxTagCount)
117
+ : 0;
118
+
119
+ const hiddenTags =
120
+ multiple && Array.isArray(selectedValue) && typeof maxTagCount === 'number'
121
+ ? selectedValue.slice(maxTagCount)
122
+ : [];
123
+
124
+ // 标签关闭回调
125
+ const handleTagClose = (value: ItemValue, e: React.MouseEvent) => {
126
+ e.stopPropagation(); // 阻止触发按钮点击
127
+ // 禁用的选项不允许关闭
128
+ if (getOptionDisabled?.(value)) return;
129
+ if (!multiple) {
130
+ handleClear(); // 单选模式清空
131
+ } else {
132
+ handleDeselect(value); // 多选模式删除指定项
133
+ }
134
+ };
135
+
136
+ return (
137
+ <div
138
+ className={cn(
139
+ 'flex w-full flex-wrap items-center gap-2',
140
+ classNames?.trigger,
141
+ )}
142
+ >
143
+ {/* 已选择的标签显示区域 */}
144
+ {hasValue && (
145
+ <>
146
+ {!multiple && selectedValue ? (
147
+ renderTag ? (
148
+ renderTag(
149
+ selectedValue as ItemValue,
150
+ handleTagClose,
151
+ getOptionDisabled?.(selectedValue as ItemValue),
152
+ )
153
+ ) : (
154
+ <Badge
155
+ variant="secondary"
156
+ className={cn('h-7 text-xs', classNames?.tag)}
157
+ >
158
+ {(selectedValue as ItemValue).name}
159
+ </Badge>
160
+ )
161
+ ) : multiple &&
162
+ Array.isArray(selectedValue) &&
163
+ selectedValue.length > 0 ? (
164
+ <>
165
+ {renderTag ? (
166
+ <>
167
+ {selectedValue
168
+ .slice(0, maxDisplayCount)
169
+ .map((item) =>
170
+ renderTag(
171
+ item,
172
+ handleTagClose,
173
+ getOptionDisabled?.(item),
174
+ ),
175
+ )}
176
+ {hiddenCount > 0 && (
177
+ <Tooltip delayDuration={200}>
178
+ <TooltipTrigger asChild>
179
+ <Badge
180
+ variant="secondary"
181
+ className={cn(
182
+ 'h-7 cursor-pointer text-xs',
183
+ classNames?.tag,
184
+ )}
185
+ >
186
+ +{hiddenCount}
187
+ </Badge>
188
+ </TooltipTrigger>
189
+ <TooltipContent
190
+ className="w-auto max-w-80 p-2"
191
+ align="start"
192
+ >
193
+ <div className="flex flex-wrap gap-1">
194
+ {hiddenTags.map((item) =>
195
+ renderTag(
196
+ item,
197
+ handleTagClose,
198
+ getOptionDisabled?.(item),
199
+ ),
200
+ )}
201
+ </div>
202
+ </TooltipContent>
203
+ </Tooltip>
204
+ )}
205
+ </>
206
+ ) : (
207
+ <>
208
+ {selectedValue.slice(0, maxDisplayCount).map((item) => (
209
+ <Badge
210
+ key={item.id}
211
+ variant="secondary"
212
+ className={cn('h-7 text-xs', classNames?.tag)}
213
+ >
214
+ {item.name}
215
+ </Badge>
216
+ ))}
217
+ {hiddenCount > 0 && (
218
+ <Tooltip delayDuration={200}>
219
+ <TooltipTrigger asChild>
220
+ <Badge
221
+ variant="secondary"
222
+ className={cn(
223
+ 'h-7 cursor-pointer text-xs',
224
+ classNames?.tag,
225
+ )}
226
+ >
227
+ +{hiddenCount}
228
+ </Badge>
229
+ </TooltipTrigger>
230
+ <TooltipContent
231
+ className="w-auto max-w-80 p-2"
232
+ align="start"
233
+ >
234
+ <div className="flex flex-wrap gap-1">
235
+ {hiddenTags.map((item) => (
236
+ <Badge
237
+ key={item.id}
238
+ variant="secondary"
239
+ className={cn('h-7 text-xs', classNames?.tag)}
240
+ >
241
+ {item.name}
242
+ </Badge>
243
+ ))}
244
+ </div>
245
+ </TooltipContent>
246
+ </Tooltip>
247
+ )}
248
+ </>
249
+ )}
250
+ </>
251
+ ) : null}
252
+ </>
253
+ )}
254
+
255
+ {/* 加号按钮 - 紧跟标签 */}
256
+ {(() => {
257
+ const addButton = (
258
+ <Button
259
+ ref={ref}
260
+ variant="secondary"
261
+ size="icon"
262
+ role="combobox"
263
+ aria-expanded={open}
264
+ data-state={open ? 'open' : 'closed'}
265
+ aria-controls="combobox-content"
266
+ disabled={disabled}
267
+ className={addButtonVariants({ size })}
268
+ onFocus={
269
+ onFocus as (e: React.FocusEvent<HTMLButtonElement>) => void
270
+ }
271
+ onBlur={onBlur as (e: React.FocusEvent<HTMLButtonElement>) => void}
272
+ {...props}
273
+ >
274
+ <Plus className={addButtonIconVariants({ size })} />
275
+ </Button>
276
+ );
277
+ return renderAddButton ? renderAddButton(addButton) : addButton;
278
+ })()}
279
+ </div>
280
+ );
281
+ };
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+
3
+ import { BaseComboboxContent } from '@client/src/components/business-ui/entity-combobox/base-combobox-content';
4
+ import { BaseComboboxEmpty } from '@client/src/components/business-ui/entity-combobox/base-combobox-empty';
5
+ import { BaseComboboxItem } from '@client/src/components/business-ui/entity-combobox/base-combobox-item';
6
+ import { BaseComboboxList } from '@client/src/components/business-ui/entity-combobox/base-combobox-list';
7
+ import { BaseComboboxLoading } from '@client/src/components/business-ui/entity-combobox/base-combobox-loading';
8
+ import { BaseComboboxSearch } from '@client/src/components/business-ui/entity-combobox/base-combobox-search';
9
+ import { EntityCombobox } from '@client/src/components/business-ui/entity-combobox/entity-combobox';
10
+ import { PopoverWrapper } from '@client/src/components/business-ui/entity-combobox/popover-wrapper';
11
+ import type {
12
+ BaseComboboxProps,
13
+ ItemValue,
14
+ } from '@client/src/components/business-ui/entity-combobox/types';
15
+
16
+ export function BaseCombobox<
17
+ T = unknown,
18
+ TRaw = unknown,
19
+ TValue extends ItemValue<TRaw> = ItemValue<TRaw>,
20
+ >(props: BaseComboboxProps<T, TRaw, TValue>) {
21
+ const {
22
+ fetchFn,
23
+ size = 'medium',
24
+ triggerType = 'button',
25
+ renderTrigger,
26
+ multiple = false,
27
+ value,
28
+ defaultValue,
29
+ onChange,
30
+ open,
31
+ defaultOpen,
32
+ onOpenChange,
33
+ debounce = 300,
34
+ disabled = false,
35
+ showSearch = true,
36
+ renderItem,
37
+ renderTag,
38
+ placeholder = '请选择',
39
+ searchPlaceholder = '',
40
+ emptyText = '没有匹配结果,换个关键词试试吧',
41
+ loadingText = '加载中...',
42
+ className,
43
+ classNames = {},
44
+ tagClosable = true,
45
+ maxTagCount,
46
+ maxTagTextLength,
47
+ placement = 'bottom',
48
+ align = 'start',
49
+ sideOffset = 4,
50
+ onSelect,
51
+ onDeselect,
52
+ onSearch,
53
+ onClear,
54
+ onFocus,
55
+ onBlur,
56
+ getItemValue = (item: unknown) =>
57
+ ({
58
+ id:
59
+ (item as { id?: string; value?: string }).id ||
60
+ (item as { id?: string; value?: string }).value ||
61
+ '',
62
+ name:
63
+ (item as { name?: string; label?: string }).name ||
64
+ (item as { name?: string; label?: string }).label ||
65
+ '',
66
+ avatar: (item as { avatar?: string }).avatar,
67
+ raw: item,
68
+ }) as TValue,
69
+ getOptionDisabled,
70
+ slotProps,
71
+ children,
72
+ } = props;
73
+
74
+ // 浮层包装组件的通用 props
75
+ const popoverProps = {
76
+ size,
77
+ triggerType,
78
+ renderTrigger,
79
+ placeholder,
80
+ maxTagCount,
81
+ maxTagTextLength,
82
+ tagClosable,
83
+ className,
84
+ classNames,
85
+ placement,
86
+ align,
87
+ sideOffset,
88
+ showSearch,
89
+ searchPlaceholder,
90
+ renderItem: renderItem!,
91
+ renderTag,
92
+ getItemValue,
93
+ getOptionDisabled,
94
+ emptyText,
95
+ loadingText,
96
+ onFocus,
97
+ onBlur,
98
+ slotProps,
99
+ children,
100
+ };
101
+
102
+ return (
103
+ <EntityCombobox
104
+ fetchFn={fetchFn}
105
+ size={size}
106
+ multiple={multiple}
107
+ value={value}
108
+ defaultValue={defaultValue}
109
+ onChange={onChange}
110
+ open={open}
111
+ defaultOpen={defaultOpen}
112
+ onOpenChange={onOpenChange}
113
+ debounce={debounce}
114
+ disabled={disabled}
115
+ onSelect={onSelect}
116
+ onDeselect={onDeselect}
117
+ onSearch={onSearch}
118
+ onClear={onClear}
119
+ getItemValue={getItemValue}
120
+ >
121
+ <PopoverWrapper {...popoverProps} />
122
+ </EntityCombobox>
123
+ );
124
+ }
125
+
126
+ // 导出子组件以支持自定义使用
127
+ BaseCombobox.Content = BaseComboboxContent;
128
+ BaseCombobox.Search = BaseComboboxSearch;
129
+ BaseCombobox.List = BaseComboboxList;
130
+ BaseCombobox.Item = BaseComboboxItem;
131
+ BaseCombobox.Empty = BaseComboboxEmpty;
132
+ BaseCombobox.Loading = BaseComboboxLoading;
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+
5
+ import type { EntityComboboxContextValue } from '@client/src/components/business-ui/entity-combobox/types';
6
+
7
+ const EntityComboboxContext = createContext<any | undefined>(undefined);
8
+
9
+ export const EntityComboboxProvider = EntityComboboxContext.Provider;
10
+
11
+ export function useEntityComboboxContext(): EntityComboboxContextValue {
12
+ const context = useContext(EntityComboboxContext);
13
+ if (!context) {
14
+ throw new Error(
15
+ 'useEntityComboboxContext must be used within EntityCombobox',
16
+ );
17
+ }
18
+ return context;
19
+ }
@@ -0,0 +1,145 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useState } from 'react';
4
+ import { useControllableState } from 'radix-ui/internal';
5
+
6
+ import { EntityComboboxProvider } from '@client/src/components/business-ui/entity-combobox/context';
7
+ import { useDebounce } from '@client/src/components/business-ui/entity-combobox/hooks';
8
+ import type {
9
+ EntityComboboxProps,
10
+ ItemValue,
11
+ } from '@client/src/components/business-ui/entity-combobox/types';
12
+ import { useFetchData } from '@client/src/components/business-ui/entity-combobox/use-fetch-data';
13
+
14
+ export function EntityCombobox<
15
+ T = unknown,
16
+ TRaw = unknown,
17
+ TValue extends ItemValue<TRaw> = ItemValue<TRaw>,
18
+ >(props: EntityComboboxProps<T, TRaw, TValue>) {
19
+ const {
20
+ fetchFn,
21
+ size = 'medium',
22
+ multiple = false,
23
+ value: valueProp,
24
+ defaultValue,
25
+ onChange,
26
+ open: openProp,
27
+ defaultOpen = false,
28
+ onOpenChange,
29
+ debounce = 300,
30
+ disabled = false,
31
+ onSelect,
32
+ onDeselect,
33
+ onSearch,
34
+ onClear,
35
+ children,
36
+ getItemValue,
37
+ } = props;
38
+
39
+ // 受控状态
40
+ const [selectedValue, setSelectedValue] = useControllableState({
41
+ prop: valueProp,
42
+ defaultProp: defaultValue ?? (!multiple ? null : []),
43
+ onChange,
44
+ });
45
+
46
+ const [searchValue, setSearchValue] = useState('');
47
+ const debouncedSearch = useDebounce(searchValue, debounce);
48
+
49
+ // 代理 onOpenChange,关闭时清空搜索
50
+ const handleOpenChange = useCallback(
51
+ (newOpen: boolean) => {
52
+ if (!newOpen) {
53
+ setSearchValue('');
54
+ }
55
+ onOpenChange?.(newOpen);
56
+ },
57
+ [onOpenChange],
58
+ );
59
+
60
+ const [open, setOpen] = useControllableState({
61
+ prop: openProp,
62
+ defaultProp: defaultOpen,
63
+ onChange: handleOpenChange,
64
+ });
65
+
66
+ // 使用自定义 hook 处理数据请求
67
+ const { data, isFetching, isError, isSuccess, fetchStatus, refetch } =
68
+ useFetchData<T>({
69
+ fetchFn,
70
+ enabled: open,
71
+ search: debouncedSearch,
72
+ onSearch,
73
+ });
74
+
75
+ // 选择逻辑
76
+ const handleSelect = useCallback(
77
+ (itemValue: TValue) => {
78
+ if (!multiple) {
79
+ setSelectedValue(itemValue);
80
+ setOpen(false);
81
+ onSelect?.(itemValue);
82
+ } else {
83
+ const current = Array.isArray(selectedValue) ? selectedValue : [];
84
+ const isSelected = current.some((v) => v.id === itemValue.id);
85
+
86
+ if (isSelected) {
87
+ const newValue = current.filter((v) => v.id !== itemValue.id);
88
+ setSelectedValue(newValue);
89
+ onDeselect?.(itemValue);
90
+ } else {
91
+ const newValue = [...current, itemValue];
92
+ setSelectedValue(newValue);
93
+ onSelect?.(itemValue);
94
+ }
95
+ }
96
+ },
97
+ [multiple, selectedValue, setSelectedValue, setOpen, onSelect, onDeselect],
98
+ );
99
+
100
+ const handleDeselect = useCallback(
101
+ (itemValue: TValue) => {
102
+ if (multiple && Array.isArray(selectedValue)) {
103
+ const newValue = selectedValue.filter((v) => v.id !== itemValue.id);
104
+ setSelectedValue(newValue);
105
+ onDeselect?.(itemValue);
106
+ }
107
+ },
108
+ [multiple, selectedValue, setSelectedValue, onDeselect],
109
+ );
110
+
111
+ const handleClear = useCallback(() => {
112
+ setSelectedValue(!multiple ? null : []);
113
+ onClear?.();
114
+ }, [multiple, setSelectedValue, onClear]);
115
+
116
+ // Context 值
117
+ const contextValue = {
118
+ open,
119
+ setOpen,
120
+ searchValue,
121
+ setSearchValue,
122
+ selectedValue: selectedValue ?? (!multiple ? null : []),
123
+ debouncedSearch,
124
+ handleSelect,
125
+ handleDeselect,
126
+ handleClear,
127
+ data,
128
+ isFetching,
129
+ isError,
130
+ refetch,
131
+ isSuccess,
132
+ fetchStatus,
133
+ isPlaceholderData: false,
134
+ multiple,
135
+ disabled,
136
+ size,
137
+ getItemValue: getItemValue as (item: unknown) => ItemValue,
138
+ };
139
+
140
+ return (
141
+ <EntityComboboxProvider value={contextValue}>
142
+ {children}
143
+ </EntityComboboxProvider>
144
+ );
145
+ }
@@ -0,0 +1,52 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+
5
+ export type HighlightTextProps = {
6
+ text: string;
7
+ highlight?: string;
8
+ className?: string;
9
+ highlightClassName?: string;
10
+ };
11
+
12
+ /**
13
+ * 高亮文本组件
14
+ * 将文本中匹配的部分用高亮样式标记
15
+ */
16
+ export const HighlightText = ({
17
+ text,
18
+ highlight,
19
+ className,
20
+ highlightClassName = 'text-primary',
21
+ }: HighlightTextProps) => {
22
+ const parts = useMemo(() => {
23
+ if (!highlight?.trim()) {
24
+ return [{ text, isMatch: false }];
25
+ }
26
+
27
+ const escapedHighlight = highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
+ const regex = new RegExp(`(${escapedHighlight})`, 'gi');
29
+ const splitParts = text.split(regex);
30
+
31
+ return splitParts
32
+ .filter((part) => part !== '')
33
+ .map((part) => ({
34
+ text: part,
35
+ isMatch: part.toLowerCase() === highlight.toLowerCase(),
36
+ }));
37
+ }, [text, highlight]);
38
+
39
+ return (
40
+ <span className={className}>
41
+ {parts.map((part, index) =>
42
+ part.isMatch ? (
43
+ <span key={index} className={highlightClassName}>
44
+ {part.text}
45
+ </span>
46
+ ) : (
47
+ <span key={index}>{part.text}</span>
48
+ ),
49
+ )}
50
+ </span>
51
+ );
52
+ };
@@ -0,0 +1,21 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+
5
+ export { useEntityComboboxContext } from '@client/src/components/business-ui/entity-combobox/context';
6
+
7
+ export function useDebounce<T>(value: T, delay: number): T {
8
+ const [debouncedValue, setDebouncedValue] = useState(value);
9
+
10
+ useEffect(() => {
11
+ const handler = setTimeout(() => {
12
+ setDebouncedValue(value);
13
+ }, delay);
14
+
15
+ return () => {
16
+ clearTimeout(handler);
17
+ };
18
+ }, [value, delay]);
19
+
20
+ return debouncedValue;
21
+ }
@@ -0,0 +1,31 @@
1
+ // Entity Combobox
2
+ export { EntityCombobox } from '@client/src/components/business-ui/entity-combobox/entity-combobox';
3
+ export { useEntityComboboxContext } from '@client/src/components/business-ui/entity-combobox/context';
4
+ export type {
5
+ EntityComboboxProps,
6
+ EntityComboboxContextValue,
7
+ } from '@client/src/components/business-ui/entity-combobox/types';
8
+
9
+ // Base Combobox
10
+ export { BaseCombobox } from '@client/src/components/business-ui/entity-combobox/base-combobox';
11
+ export { BaseComboboxItem } from '@client/src/components/business-ui/entity-combobox/base-combobox-item';
12
+ export { BaseComboboxList } from '@client/src/components/business-ui/entity-combobox/base-combobox-list';
13
+ export { BaseComboboxSearch } from '@client/src/components/business-ui/entity-combobox/base-combobox-search';
14
+ export { BaseComboboxTrigger } from '@client/src/components/business-ui/entity-combobox/base-combobox-trigger';
15
+ export { BaseComboboxContent } from '@client/src/components/business-ui/entity-combobox/base-combobox-content';
16
+ export { BaseComboboxEmpty } from '@client/src/components/business-ui/entity-combobox/base-combobox-empty';
17
+ export { BaseComboboxLoading } from '@client/src/components/business-ui/entity-combobox/base-combobox-loading';
18
+ export { BaseComboboxError } from '@client/src/components/business-ui/entity-combobox/base-combobox-error';
19
+
20
+ // Components
21
+ export { ItemPill } from '@client/src/components/business-ui/entity-combobox/item-pill';
22
+
23
+ // Types
24
+ export type {
25
+ ItemValue,
26
+ TriggerType,
27
+ TriggerRenderProps,
28
+ BaseEntitySelectProps,
29
+ ClassNamesConfig,
30
+ PopoverSlotProps,
31
+ } from '@client/src/components/business-ui/entity-combobox/shared-types';