@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,169 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import {
5
+ EditorContent,
6
+ EditorContext,
7
+ useCurrentEditor,
8
+ useEditor,
9
+ type EditorContentProps,
10
+ type Extensions,
11
+ } from '@tiptap/react';
12
+ import { Slot } from 'radix-ui';
13
+
14
+ import { LinkHoverToolbar } from '@/components/business-ui/tiptap-editor/components/link-hover-toolbar';
15
+ import { CompleteKit } from '@/components/business-ui/tiptap-editor/extensions/complete-kit';
16
+ import { cn } from '@/lib/utils';
17
+ import { Separator } from '@/components/ui/separator';
18
+
19
+ export interface TiptapEditorProps extends React.ComponentProps<'div'> {
20
+ /** 受控 value (HTML)。 */
21
+ value?: string;
22
+ /** 默认值 (HTML)。 */
23
+ defaultValue?: string;
24
+ /** 当编辑器更新时,触发并传递最新的 HTML。 */
25
+ onValueChange?: (value: string) => void;
26
+ children?: React.ReactNode;
27
+ className?: string;
28
+ /**
29
+ * 自定义 Tiptap 扩展。
30
+ * @default [CompleteKit]
31
+ */
32
+ extensions?: Extensions;
33
+ /** 是否为只读模式。 */
34
+ readOnly?: boolean;
35
+ asChild?: boolean;
36
+ }
37
+
38
+ const defaultExtensions = [CompleteKit];
39
+
40
+ export function TiptapEditor({
41
+ value,
42
+ defaultValue,
43
+ onValueChange,
44
+ children,
45
+ className,
46
+ extensions = defaultExtensions,
47
+ readOnly = false,
48
+ asChild = false,
49
+ autoFocus = false,
50
+ 'aria-disabled': ariaDisabled,
51
+ ...props
52
+ }: TiptapEditorProps) {
53
+ const Comp = asChild ? Slot.Root : 'div';
54
+
55
+ const initialContent = value !== undefined ? value : defaultValue;
56
+ const editable = !readOnly && !ariaDisabled;
57
+
58
+ const editor = useEditor({
59
+ autofocus: autoFocus,
60
+ editable,
61
+ editorProps: {
62
+ attributes: {
63
+ class: cn('prose max-w-none flex-1 px-3 py-2 dark:prose-invert'),
64
+ },
65
+ },
66
+ immediatelyRender: false,
67
+ content: initialContent,
68
+ onUpdate: ({ editor }) => {
69
+ onValueChange?.(editor.getHTML());
70
+ },
71
+ extensions,
72
+ });
73
+
74
+ const valueRef = React.useRef(value);
75
+ React.useEffect(() => {
76
+ if (!editor || value === undefined) return;
77
+ valueRef.current = value;
78
+
79
+ // Tiptap 内部会使用 flushSync,因此我们将内容更新延迟到微任务队列
80
+ queueMicrotask(() => {
81
+ const current = editor.getHTML();
82
+ if (valueRef.current !== current) {
83
+ editor.commands.setContent(value);
84
+ }
85
+ });
86
+ }, [value, editor]);
87
+
88
+ React.useEffect(() => {
89
+ if (!editor) return;
90
+ editor.setEditable(editable);
91
+ }, [editor, editable]);
92
+
93
+ const contextValue = React.useMemo(() => ({ editor }), [editor]);
94
+
95
+ return (
96
+ <EditorContext value={contextValue}>
97
+ <Comp
98
+ aria-disabled={ariaDisabled}
99
+ className={cn(
100
+ "relative flex w-full flex-col rounded-md border ring-offset-background focus-within:border-ring focus-within:ring-[3px] focus-within:ring-ring/20 focus-within:outline-hidden hover:border-ring aria-disabled:cursor-not-allowed aria-disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 **:data-[type='taskList']:list-none **:data-[type='taskList']:p-0 [&_[data-type='taskList']>li]:flex [&_[data-type='taskList']>li]:items-start [&_[data-type='taskList']>li]:gap-2 [&_[data-type='taskList']>li>div]:min-w-0 [&_[data-type='taskList']>li>div]:flex-1 [&_[data-type='taskList']>li>div>p]:m-0 [&_[data-type='taskList']>li>label]:mt-0.5 [&_[data-type='taskList']>li>label]:shrink-0 **:[a]:cursor-pointer",
101
+ className,
102
+ )}
103
+ data-slot="tiptap-editor"
104
+ {...props}
105
+ >
106
+ {children}
107
+ <LinkHoverToolbar />
108
+ </Comp>
109
+ </EditorContext>
110
+ );
111
+ }
112
+
113
+ export function TiptapEditorContent({
114
+ className,
115
+ ...props
116
+ }: Omit<EditorContentProps, 'editor'>) {
117
+ const { editor } = useCurrentEditor();
118
+
119
+ return (
120
+ <EditorContent
121
+ editor={editor}
122
+ className={cn(
123
+ 'flex flex-1 flex-col overflow-y-auto outline-hidden',
124
+ className,
125
+ )}
126
+ data-slot="tiptap-editor-content"
127
+ {...props}
128
+ />
129
+ );
130
+ }
131
+
132
+ export function TiptapEditorToolbar({
133
+ className,
134
+ children,
135
+ asChild,
136
+ ...props
137
+ }: React.ComponentProps<'div'> & { asChild?: boolean }) {
138
+ const Comp = asChild ? Slot.Root : 'div';
139
+
140
+ return (
141
+ <Comp
142
+ className={cn(
143
+ 'flex flex-wrap items-center gap-x-1 gap-y-2 p-1.5',
144
+ className,
145
+ )}
146
+ data-slot="tiptap-editor-toolbar"
147
+ {...props}
148
+ >
149
+ {children}
150
+ </Comp>
151
+ );
152
+ }
153
+
154
+ export function TiptapEditorToolbarSeparator({
155
+ className,
156
+ ...props
157
+ }: React.ComponentProps<typeof Separator>) {
158
+ return (
159
+ <Separator
160
+ orientation="vertical"
161
+ className={cn(
162
+ 'mx-0.5 h-4 w-px bg-border data-[orientation=vertical]:h-4',
163
+ className,
164
+ )}
165
+ data-slot="tiptap-editor-toolbar-separator"
166
+ {...props}
167
+ />
168
+ );
169
+ }
@@ -0,0 +1,74 @@
1
+ // client/src/types/user.ts
2
+
3
+ import type { I18nText } from '@/components/business-ui/entity-combobox/types';
4
+ import type { Department } from '@/components/business-ui/department-select/types';
5
+
6
+ /**
7
+ * 重新导出相关类型,方便统一导入
8
+ */
9
+ export type { I18nText, Department };
10
+
11
+ /**
12
+ * 用户类型枚举
13
+ */
14
+ export type UserType = '_employee' | '_externalUser' | '_anonymousUser';
15
+
16
+ /**
17
+ * 统一的标准用户类型(单一事实来源)
18
+ * 使用下划线命名(与后端统一),larkUserId 除外
19
+ */
20
+ export interface User {
21
+ /** 用户 ID(未注册外部联系人可能没有) */
22
+ user_id?: string;
23
+
24
+ /** 飞书用户 ID(保持驼峰) */
25
+ larkUserId?: string;
26
+
27
+ /**
28
+ * 用户名称
29
+ * 支持简单字符串或国际化文本
30
+ */
31
+ name?: I18nText | string;
32
+
33
+ /** 用户头像 URL */
34
+ avatar?: string;
35
+
36
+ /** 用户邮箱 */
37
+ email?: string;
38
+
39
+ /**
40
+ * 用户类型
41
+ * - '_employee': 内部员工
42
+ * - '_externalUser': 外部用户
43
+ * - '_anonymousUser': 匿名用户
44
+ */
45
+ user_type?: UserType;
46
+
47
+ /** 部门信息(引用 DepartmentSelect 的类型) */
48
+ department?: Department;
49
+
50
+ /** 租户名称(外部用户显示) */
51
+ tenantName?: string;
52
+
53
+ /** 用户状态 */
54
+ status?: number;
55
+ }
56
+
57
+ /**
58
+ * 用户数据输入类型(用于组件 Props)
59
+ * 兼容驼峰和下划线命名,内部自动转换为标准 User 类型
60
+ */
61
+ export type UserInput =
62
+ | User
63
+ | {
64
+ user_id?: string;
65
+ userId?: string; // 兼容驼峰
66
+ larkUserId?: string;
67
+ name?: I18nText | string;
68
+ avatar?: string;
69
+ email?: string;
70
+ user_type?: UserType;
71
+ userType?: UserType; // 兼容驼峰
72
+ department?: Department | any;
73
+ status?: number;
74
+ };
@@ -0,0 +1,2 @@
1
+ export { UserDisplay } from '@client/src/components/business-ui/user-display/user-display';
2
+ export type { UserWithAvatarProps } from '@client/src/components/business-ui/user-display/type';
@@ -0,0 +1,90 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipProvider,
8
+ TooltipTrigger,
9
+ } from '@client/src/components/ui/tooltip';
10
+
11
+ export interface OverflowTooltipTextProps extends React.HTMLAttributes<HTMLSpanElement> {
12
+ text: string;
13
+ }
14
+
15
+ /**
16
+ * Displays truncated inline text and shows a tooltip with the full content when overflowed.
17
+ */
18
+ export function OverflowTooltipText({
19
+ text,
20
+ className,
21
+ ...props
22
+ }: OverflowTooltipTextProps) {
23
+ const textRef = React.useRef<HTMLSpanElement>(null);
24
+ const [isOverflowing, setIsOverflowing] = React.useState(false);
25
+
26
+ const checkOverflow = React.useCallback(() => {
27
+ const el = textRef.current;
28
+ if (!el) {
29
+ return;
30
+ }
31
+
32
+ setIsOverflowing(el.scrollWidth > el.clientWidth + 1);
33
+ }, []);
34
+
35
+ React.useLayoutEffect(() => {
36
+ checkOverflow();
37
+ }, [checkOverflow]);
38
+
39
+ React.useEffect(() => {
40
+ const el = textRef.current;
41
+ if (!el) {
42
+ return;
43
+ }
44
+ const observer = new ResizeObserver(() => {
45
+ checkOverflow();
46
+ });
47
+ observer.observe(el);
48
+ return () => {
49
+ observer.disconnect();
50
+ };
51
+ }, [checkOverflow]);
52
+
53
+ React.useEffect(() => {
54
+ const handleResize = () => {
55
+ checkOverflow();
56
+ };
57
+ window.addEventListener('resize', handleResize);
58
+ return () => {
59
+ window.removeEventListener('resize', handleResize);
60
+ };
61
+ }, [checkOverflow]);
62
+
63
+ const textNode = (
64
+ <span
65
+ ref={textRef}
66
+ className={cn('inline-block truncate', className)}
67
+ {...props}
68
+ >
69
+ {text}
70
+ </span>
71
+ );
72
+
73
+ if (!isOverflowing) {
74
+ return textNode;
75
+ }
76
+
77
+ return (
78
+ <TooltipProvider>
79
+ <Tooltip delayDuration={80}>
80
+ <TooltipTrigger asChild>{textNode}</TooltipTrigger>
81
+ <TooltipContent
82
+ sideOffset={4}
83
+ className="bg-[rgb(31,35,41)] text-white ring-0"
84
+ >
85
+ {text}
86
+ </TooltipContent>
87
+ </Tooltip>
88
+ </TooltipProvider>
89
+ );
90
+ }
@@ -0,0 +1,27 @@
1
+ import type { AccountType } from '@lark-apaas/client-toolkit/tools/services';
2
+ import type { UserInput } from '@client/src/components/business-ui/types/user';
3
+
4
+ export interface UserWithAvatarProps {
5
+ /**
6
+ * 用户数据,可以只传 userId,组件会自动查询 name 和 avatar
7
+ */
8
+ data: UserInput;
9
+ /**
10
+ * @default medium
11
+ */
12
+ size?: 'small' | 'medium' | 'large';
13
+ /**
14
+ * @default tag
15
+ */
16
+ mode?: 'tag' | 'plain';
17
+ className?: string;
18
+ /**
19
+ * @default true
20
+ */
21
+ showLabel?: boolean;
22
+ /**
23
+ * 账户类型,用于查询用户信息
24
+ * @default 'apaas'
25
+ */
26
+ accountType?: AccountType;
27
+ }
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { normalizeUser, isValidUserId } from '@client/src/components/business-ui/utils/user';
6
+ import type { UserInput } from '@client/src/components/business-ui/types/user';
7
+ import { UserWithAvatar } from '@client/src/components/business-ui/user-display/user-with-avatar';
8
+ import { UserProfile } from '@client/src/components/business-ui/user-profile/user-profile';
9
+ import { cn } from '@/lib/utils';
10
+ import {
11
+ Popover,
12
+ PopoverContent,
13
+ PopoverTrigger,
14
+ } from '@client/src/components/ui/popover';
15
+
16
+ export interface IUserDisplayProps {
17
+ /**
18
+ * 用户列表
19
+ */
20
+ value?: string | string[] | UserInput | UserInput[];
21
+ size?: 'small' | 'medium' | 'large';
22
+ className?: string;
23
+ style?: React.CSSProperties;
24
+ showLabel?: boolean;
25
+ showUserProfile?: boolean;
26
+ userId?: string;
27
+ user_id?: string;
28
+ }
29
+
30
+ export const UserDisplay: React.FC<IUserDisplayProps> = ({
31
+ userId,
32
+ user_id,
33
+ value,
34
+ size,
35
+ className,
36
+ style,
37
+ showLabel = true,
38
+ }) => {
39
+ // 统一规范化为 User[],兼容所有格式
40
+ const normalizedUsers = React.useMemo(() => {
41
+ let list: UserInput[];
42
+
43
+ if (value) {
44
+ // 处理 value 的不同类型
45
+ if (Array.isArray(value)) {
46
+ // string[] 或 UserInput[]
47
+ list = value.map((item: string | UserInput) =>
48
+ typeof item === 'string' ? { user_id: item } : item,
49
+ );
50
+ } else {
51
+ // string 或 UserInput
52
+ list = [typeof value === 'string' ? { user_id: value } : value];
53
+ }
54
+ } else if (userId) {
55
+ // userId 快捷方式:转换为对象
56
+ list = [{ user_id: userId }];
57
+ } else if (user_id) {
58
+ // user_id 快捷方式:转换为对象
59
+ list = [{ user_id }];
60
+ } else {
61
+ return [];
62
+ }
63
+
64
+ // 转换为标准 User 类型并过滤无效 ID
65
+ return list
66
+ .map(normalizeUser)
67
+ .filter((user) => isValidUserId(user.user_id));
68
+ }, [value, userId, user_id]);
69
+
70
+ if (!normalizedUsers.length) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <div className={cn('flex flex-wrap gap-1', className)} style={style}>
76
+ {normalizedUsers.map((user) => (
77
+ <Popover key={user.user_id}>
78
+ <PopoverTrigger asChild>
79
+ <div>
80
+ <UserWithAvatar
81
+ data={user}
82
+ size={size}
83
+ showLabel={showLabel}
84
+ className="cursor-pointer hover:bg-[rgba(31,35,41,0.15)] active:bg-[rgba(31,35,41,0.2)]"
85
+ />
86
+ </div>
87
+ </PopoverTrigger>
88
+ <PopoverContent
89
+ align="start"
90
+ sideOffset={8}
91
+ className="w-[320px] border-0 border-border/50 bg-card p-0 shadow-[0px_8px_24px_8px_rgba(31,35,41,0.04),0px_6px_12px_rgba(31,35,41,0.04),0px_4px_8px_-8px_rgba(31,35,41,0.06)]"
92
+ >
93
+ <UserProfile value={user} />
94
+ </PopoverContent>
95
+ </Popover>
96
+ ))}
97
+ </div>
98
+ );
99
+ };
@@ -0,0 +1,158 @@
1
+ 'use client';
2
+ import React from 'react';
3
+
4
+ import { useMemo } from 'react';
5
+
6
+ import { OverflowTooltipText } from '@client/src/components/business-ui/user-display/overflow-tooltip-text';
7
+ import { type UserWithAvatarProps } from '@client/src/components/business-ui/user-display/type';
8
+ import {
9
+ userInfoToUser,
10
+ createUnknownUser,
11
+ } from '@client/src/components/business-ui/user-display/utils';
12
+ import { normalizeUser, getI18nText } from '@client/src/components/business-ui/utils/user';
13
+ import type { User } from '@client/src/components/business-ui/types/user';
14
+ import { useUsersByIds } from '@client/src/components/business-ui/api/users/queries';
15
+ import { cn } from '@/lib/utils';
16
+ import {
17
+ Avatar,
18
+ AvatarFallback,
19
+ AvatarImage,
20
+ } from '@client/src/components/ui/avatar';
21
+ import { cva } from 'class-variance-authority';
22
+
23
+ const userWithAvatarVariants = cva(
24
+ 'flex min-w-0 items-center gap-1 rounded-full',
25
+ {
26
+ variants: {
27
+ size: {
28
+ small: 'max-w-37',
29
+ medium: 'max-w-43',
30
+ large: 'max-w-49',
31
+ },
32
+ showLabel: {
33
+ true: '',
34
+ false: '',
35
+ },
36
+ mode: {
37
+ tag: 'bg-muted',
38
+ plain: '',
39
+ },
40
+ },
41
+ compoundVariants: [
42
+ { size: 'small', showLabel: true, class: 'py-0.5 pr-1.5 pl-0.5' },
43
+ { size: 'medium', showLabel: true, class: 'py-0.5 pr-2 pl-0.5' },
44
+ { size: 'large', showLabel: true, class: 'py-1 pr-2.5 pl-1' },
45
+ { size: 'small', showLabel: false, class: '' },
46
+ { size: 'medium', showLabel: false, class: '' },
47
+ { size: 'large', showLabel: false, class: '' },
48
+ ],
49
+ defaultVariants: {
50
+ size: 'medium',
51
+ mode: 'tag',
52
+ showLabel: true,
53
+ },
54
+ },
55
+ );
56
+
57
+ const avatarVariants = cva('', {
58
+ variants: {
59
+ size: {
60
+ small: 'h-4 w-4',
61
+ medium: 'h-5 w-5',
62
+ large: 'h-6 w-6',
63
+ },
64
+ },
65
+ defaultVariants: {
66
+ size: 'medium',
67
+ },
68
+ });
69
+
70
+ const avatarFallbackVariants = cva('leading-none text-foreground', {
71
+ variants: {
72
+ size: {
73
+ small: 'text-[10px]',
74
+ medium: 'text-[12px]',
75
+ large: 'text-[14px]',
76
+ },
77
+ },
78
+ defaultVariants: {
79
+ size: 'medium',
80
+ },
81
+ });
82
+
83
+ const textVariants = cva('text-card-foreground', {
84
+ variants: {
85
+ size: {
86
+ small: 'text-[12px] leading-4',
87
+ medium: 'text-[14px] leading-5',
88
+ large: 'text-[16px] leading-6',
89
+ },
90
+ },
91
+ defaultVariants: {
92
+ size: 'medium',
93
+ },
94
+ });
95
+
96
+ export function UserWithAvatar({
97
+ data,
98
+ size = 'medium',
99
+ mode = 'tag',
100
+ className,
101
+ showLabel = true,
102
+ accountType = 'apaas',
103
+ }: UserWithAvatarProps) {
104
+ // 统一转换为标准 User 类型
105
+ const user: User = normalizeUser(data);
106
+ const { user_id, avatar: propsAvatar, name: propsName } = user;
107
+
108
+ // 判断是否需要从 API 获取用户信息
109
+ const needsFetch = !propsName && !propsAvatar && !!user_id;
110
+
111
+ // 使用 react-query hook 获取用户信息
112
+ const idsToFetch = useMemo(
113
+ () => (needsFetch ? [user_id] : []),
114
+ [needsFetch, user_id],
115
+ );
116
+ const { data: response, isLoading } = useUsersByIds(idsToFetch, accountType);
117
+
118
+ // 从原始 API 响应中提取并转换用户信息
119
+ const fetchedUser = useMemo(() => {
120
+ if (!needsFetch) return null;
121
+ const userInfoMap = response?.data?.userInfoMap || {};
122
+ const userInfo = userInfoMap[user_id];
123
+ return userInfo
124
+ ? userInfoToUser(userInfo, accountType)
125
+ : createUnknownUser(user_id);
126
+ }, [response, needsFetch, user_id, accountType]);
127
+
128
+ const avatar = propsAvatar || fetchedUser?.avatar;
129
+ const name = propsName || fetchedUser?.name;
130
+
131
+ // 直接使用已合并的 name,通过 getI18nText 处理国际化
132
+ const displayName = isLoading ? '' : (getI18nText(name) || '无效人员');
133
+ const formatSize = ['small', 'medium', 'large'].includes(size)
134
+ ? size
135
+ : 'medium';
136
+
137
+ return (
138
+ <div
139
+ className={cn(
140
+ userWithAvatarVariants({ size: formatSize, showLabel, mode }),
141
+ className,
142
+ )}
143
+ >
144
+ <Avatar className={cn(avatarVariants({ size: formatSize }))}>
145
+ <AvatarImage className="m-0" src={avatar} alt={displayName} />
146
+ <AvatarFallback
147
+ className={cn(avatarFallbackVariants({ size: formatSize }))}
148
+ />
149
+ </Avatar>
150
+ {showLabel && (
151
+ <OverflowTooltipText
152
+ text={displayName}
153
+ className={cn(textVariants({ size: formatSize }))}
154
+ />
155
+ )}
156
+ </div>
157
+ );
158
+ }
@@ -0,0 +1,37 @@
1
+ import { normalizeUser, isValidUserId } from '@client/src/components/business-ui/utils/user';
2
+ import type { User } from '@client/src/components/business-ui/types/user';
3
+ import type { UserInfo } from '@lark-apaas/client-toolkit/tools/services';
4
+ import type { AccountType } from '@client/src/components/business-ui/api/users/service';
5
+
6
+ // 直接导出统一的函数
7
+ export { normalizeUser, isValidUserId };
8
+
9
+ // user-display 专用的转换函数
10
+ export function userInfoToUser(
11
+ userInfo: (UserInfo & { avatar?: any }) | any,
12
+ accountType: AccountType,
13
+ ): User {
14
+ let avatarUrl: string | undefined;
15
+
16
+ if (typeof userInfo.avatar === 'string') {
17
+ avatarUrl = userInfo.avatar;
18
+ } else if (userInfo.avatar?.image?.large) {
19
+ avatarUrl = userInfo.avatar.image.large;
20
+ }
21
+
22
+ return {
23
+ user_id: accountType === 'lark' ? userInfo.larkUserID : userInfo.userID,
24
+ larkUserId: userInfo.larkUserID,
25
+ name: userInfo.name,
26
+ avatar: avatarUrl,
27
+ user_type: userInfo.userType,
28
+ department: userInfo.department as any,
29
+ };
30
+ }
31
+
32
+ export function createUnknownUser(user_id: string): User {
33
+ return {
34
+ user_id,
35
+ name: '未知用户',
36
+ };
37
+ }