@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,539 @@
1
+ import * as React from 'react';
2
+ import { mergeAttributes, Node, type CommandProps } from '@tiptap/core';
3
+ import { type Node as ProseMirrorNode } from '@tiptap/pm/model';
4
+ import {
5
+ NodeViewWrapper,
6
+ ReactNodeViewRenderer,
7
+ type NodeViewProps,
8
+ } from '@tiptap/react';
9
+ import { CircleAlert, DownloadIcon, EyeIcon, Trash2Icon } from 'lucide-react';
10
+ import { nanoid } from 'nanoid';
11
+ import { toast } from 'sonner';
12
+
13
+ import { uploadFile } from '@/components/business-ui/api/files/service';
14
+ import { cn } from '@/lib/utils';
15
+ import { Button } from '@/components/ui/button';
16
+ import { FileAeColorfulIcon } from '@/components/ui/icons/file-ae-colorful-icon';
17
+ import { FileAiColorfulIcon } from '@/components/ui/icons/file-ai-colorful-icon';
18
+ import { FileAudioColorfulIcon } from '@/components/ui/icons/file-audio-colorful-icon';
19
+ import { FileCodeColorfulIcon } from '@/components/ui/icons/file-code-colorful-icon';
20
+ import { FileCsvColorfulIcon } from '@/components/ui/icons/file-csv-colorful-icon';
21
+ import { FileKeynoteColorfulIcon } from '@/components/ui/icons/file-keynote-colorful-icon';
22
+ import { FilePagesColorfulIcon } from '@/components/ui/icons/file-pages-colorful-icon';
23
+ import { FilePsColorfulIcon } from '@/components/ui/icons/file-ps-colorful-icon';
24
+ import { FileSketchColorfulIcon } from '@/components/ui/icons/file-sketch-colorful-icon';
25
+ import { FileWikiExcelColorfulIcon } from '@/components/ui/icons/file-wiki-excel-colorful-icon';
26
+ import { FileWikiImageColorfulIcon } from '@/components/ui/icons/file-wiki-image-colorful-icon';
27
+ import { FileWikiPdfColorfulIcon } from '@/components/ui/icons/file-wiki-pdf-colorful-icon';
28
+ import { FileWikiPptColorfulIcon } from '@/components/ui/icons/file-wiki-ppt-colorful-icon';
29
+ import { FileWikiTextColorfulIcon } from '@/components/ui/icons/file-wiki-text-colorful-icon';
30
+ import { FileWikiUnknownColorfulIcon } from '@/components/ui/icons/file-wiki-unknown-colorful-icon';
31
+ import { FileWikiVideoColorfulIcon } from '@/components/ui/icons/file-wiki-video-colorful-icon';
32
+ import { FileWikiWordColorfulIcon } from '@/components/ui/icons/file-wiki-word-colorful-icon';
33
+ import { FileWikiZipColorfulIcon } from '@/components/ui/icons/file-wiki-zip-colorful-icon';
34
+ import { Spinner } from '@/components/ui/spinner';
35
+
36
+ export interface AttachmentAttributes {
37
+ url: string;
38
+ fileName?: string;
39
+ fileSize?: string;
40
+ fileType?: string;
41
+ fileExt?: string;
42
+ uploadId?: string | null;
43
+ }
44
+
45
+ export type AttachmentUploadFn = (file: File) => Promise<string>;
46
+
47
+ export interface AttachmentExtensionOptions {
48
+ upload?: AttachmentUploadFn;
49
+ }
50
+
51
+ declare module '@tiptap/core' {
52
+ interface Commands<ReturnType> {
53
+ attachment: {
54
+ setAttachment: (attributes: AttachmentAttributes) => ReturnType;
55
+ insertAttachments: (files: File[]) => ReturnType;
56
+ };
57
+ }
58
+ }
59
+
60
+ function findAttachmentByUploadId(doc: ProseMirrorNode, uploadId: string) {
61
+ let foundPos: number | null = null;
62
+ doc.descendants((node: ProseMirrorNode, pos: number) => {
63
+ if (foundPos != null) return false;
64
+ if (node.type?.name === 'attachment' && node.attrs?.uploadId === uploadId) {
65
+ foundPos = pos;
66
+ return false;
67
+ }
68
+ return true;
69
+ });
70
+ return foundPos;
71
+ }
72
+
73
+ function fileToAttachmentAttributes(
74
+ file: File,
75
+ uploadId: string,
76
+ ): AttachmentAttributes {
77
+ const fileSize = (file.size / 1024).toFixed(2) + ' KB';
78
+ const fileExt = file.name.split('.').pop()?.toLowerCase() || '';
79
+ const fileName = fileExt
80
+ ? file.name.slice(0, -(fileExt.length + 1))
81
+ : file.name;
82
+ const fileType = file.type || fileExt || '';
83
+
84
+ return {
85
+ url: '',
86
+ fileName,
87
+ fileExt,
88
+ fileSize,
89
+ fileType,
90
+ uploadId,
91
+ };
92
+ }
93
+
94
+ function withDownloadParam(url: string) {
95
+ // blob URL 追加 search 会导致地址失效
96
+ if (url.startsWith('blob:')) return url;
97
+
98
+ try {
99
+ const u = new URL(url, window.location.href);
100
+ u.searchParams.set('download', 'true');
101
+ return u.toString();
102
+ } catch {
103
+ return url;
104
+ }
105
+ }
106
+
107
+ function getFileIcon(fileType?: string) {
108
+ if (!fileType) return FileWikiUnknownColorfulIcon;
109
+
110
+ const type = fileType.toLowerCase();
111
+
112
+ switch (type) {
113
+ case 'jpg':
114
+ case 'jpeg':
115
+ case 'png':
116
+ case 'gif':
117
+ case 'svg':
118
+ case 'webp':
119
+ return FileWikiImageColorfulIcon;
120
+ case 'mp4':
121
+ case 'webm':
122
+ case 'mov':
123
+ return FileWikiVideoColorfulIcon;
124
+ case 'mp3':
125
+ case 'wav':
126
+ case 'ogg':
127
+ return FileAudioColorfulIcon;
128
+ case 'pdf':
129
+ return FileWikiPdfColorfulIcon;
130
+ case 'doc':
131
+ case 'docx':
132
+ case 'rtf':
133
+ return FileWikiWordColorfulIcon;
134
+ case 'xls':
135
+ case 'xlsx':
136
+ return FileWikiExcelColorfulIcon;
137
+ case 'csv':
138
+ return FileCsvColorfulIcon;
139
+ case 'ppt':
140
+ case 'pptx':
141
+ return FileWikiPptColorfulIcon;
142
+ case 'txt':
143
+ case 'md':
144
+ return FileWikiTextColorfulIcon;
145
+ case 'zip':
146
+ case 'rar':
147
+ case '7z':
148
+ case 'tar':
149
+ case 'gz':
150
+ return FileWikiZipColorfulIcon;
151
+ case 'js':
152
+ case 'ts':
153
+ case 'jsx':
154
+ case 'tsx':
155
+ case 'html':
156
+ case 'css':
157
+ case 'json':
158
+ case 'py':
159
+ case 'java':
160
+ case 'c':
161
+ case 'cpp':
162
+ case 'go':
163
+ case 'rs':
164
+ case 'php':
165
+ return FileCodeColorfulIcon;
166
+ case 'ai':
167
+ return FileAiColorfulIcon;
168
+ case 'psd':
169
+ case 'ps':
170
+ return FilePsColorfulIcon;
171
+ case 'ae':
172
+ return FileAeColorfulIcon;
173
+ case 'sketch':
174
+ return FileSketchColorfulIcon;
175
+ case 'key':
176
+ case 'keynote':
177
+ return FileKeynoteColorfulIcon;
178
+ case 'pages':
179
+ return FilePagesColorfulIcon;
180
+ default:
181
+ return FileWikiUnknownColorfulIcon;
182
+ }
183
+ }
184
+
185
+ function AttachmentNodeView(props: NodeViewProps) {
186
+ const { url, fileName, fileExt, fileSize, fileType, uploadId } = props.node
187
+ .attrs as AttachmentAttributes;
188
+
189
+ const uploading = Boolean(uploadId);
190
+
191
+ const effectiveUrl = url || '';
192
+ const displayName =
193
+ fileName ||
194
+ (effectiveUrl ? effectiveUrl.split('/').pop() : '') ||
195
+ 'Untitled';
196
+ const displayExt = fileExt || '';
197
+
198
+ const iconComponent = getFileIcon(displayExt || fileType);
199
+
200
+ const deleteNode = () => {
201
+ props.deleteNode();
202
+ };
203
+
204
+ const handleDownload = (e: React.MouseEvent) => {
205
+ e.preventDefault();
206
+ if (!effectiveUrl || uploading) return;
207
+
208
+ const downloadUrl = withDownloadParam(effectiveUrl);
209
+
210
+ const filename =
211
+ displayName + (displayExt ? `.${displayExt}` : '') || '附件';
212
+
213
+ try {
214
+ const a = document.createElement('a');
215
+ a.href = downloadUrl;
216
+ a.download = filename;
217
+ a.rel = 'noopener';
218
+ document.body.appendChild(a);
219
+ a.click();
220
+ a.remove();
221
+ } catch {
222
+ window.open(downloadUrl, '_blank');
223
+ }
224
+ };
225
+
226
+ const handlePreview = (e: React.MouseEvent) => {
227
+ e.preventDefault();
228
+ if (!effectiveUrl || uploading) return;
229
+ window.open(effectiveUrl, '_blank');
230
+ };
231
+
232
+ const highlighted = props.selected;
233
+ const error = !uploading && !effectiveUrl;
234
+
235
+ return (
236
+ <NodeViewWrapper className="my-3 select-none">
237
+ <div
238
+ aria-invalid={error || undefined}
239
+ className={cn(
240
+ 'relative flex w-full items-center gap-2 rounded-md border bg-card px-3 py-2 text-card-foreground transition-colors',
241
+ uploading && 'opacity-70',
242
+ 'hover:border-primary aria-invalid:border-destructive aria-invalid:hover:border-destructive',
243
+ highlighted && 'border-primary',
244
+ )}
245
+ >
246
+ <div className="flex shrink-0 items-center justify-center text-muted-foreground">
247
+ {React.createElement(iconComponent, {
248
+ className: 'size-6',
249
+ strokeWidth: 1.5,
250
+ })}
251
+ </div>
252
+
253
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5">
254
+ <div className="truncate text-sm/[22px] font-normal text-foreground">
255
+ {displayName}
256
+ {displayExt ? `.${displayExt}` : ''}
257
+ </div>
258
+ <div className="truncate text-xs text-muted-foreground">
259
+ {fileSize || 'Unknown size'}
260
+ </div>
261
+ </div>
262
+
263
+ <div className="flex items-center gap-2">
264
+ {uploading ? (
265
+ <div className="flex items-center">
266
+ <Spinner className="text-muted-foreground" />
267
+ </div>
268
+ ) : (
269
+ <div className="flex items-center gap-2">
270
+ {error ? (
271
+ <Button
272
+ type="button"
273
+ variant="ghost"
274
+ size="icon"
275
+ aria-label="附件不可用"
276
+ title="附件不可用"
277
+ onClick={() => {
278
+ toast.error('该附件链接缺失,请重新上传');
279
+ }}
280
+ >
281
+ <CircleAlert className="size-4 text-destructive" />
282
+ </Button>
283
+ ) : (
284
+ <>
285
+ <Button
286
+ type="button"
287
+ variant="ghost"
288
+ size="icon"
289
+ onClick={handlePreview}
290
+ disabled={!effectiveUrl}
291
+ aria-label="预览"
292
+ title="预览"
293
+ >
294
+ <EyeIcon className="size-4" />
295
+ </Button>
296
+
297
+ <Button
298
+ type="button"
299
+ variant="ghost"
300
+ size="icon"
301
+ onClick={handleDownload}
302
+ disabled={!effectiveUrl}
303
+ aria-label="下载"
304
+ title="下载"
305
+ >
306
+ <DownloadIcon className="size-4" />
307
+ </Button>
308
+ </>
309
+ )}
310
+
311
+ {props.editor.isEditable && (
312
+ <Button
313
+ type="button"
314
+ variant="ghost"
315
+ size="icon"
316
+ onClick={deleteNode}
317
+ aria-label="删除"
318
+ title="删除"
319
+ >
320
+ <Trash2Icon className="size-4" />
321
+ </Button>
322
+ )}
323
+ </div>
324
+ )}
325
+ </div>
326
+ </div>
327
+ </NodeViewWrapper>
328
+ );
329
+ }
330
+
331
+ export const Attachment = Node.create<AttachmentExtensionOptions>({
332
+ name: 'attachment',
333
+ group: 'block',
334
+ atom: true,
335
+
336
+ addOptions() {
337
+ return {
338
+ upload: async (file: File) => {
339
+ const data = await uploadFile(file);
340
+ return data.url;
341
+ },
342
+ };
343
+ },
344
+
345
+ addAttributes() {
346
+ return {
347
+ url: {
348
+ default: null,
349
+ parseHTML: (element) => {
350
+ return element.getAttribute('data-url') || null;
351
+ },
352
+ renderHTML: (attributes) => {
353
+ return {
354
+ 'data-url': attributes.url || null,
355
+ };
356
+ },
357
+ },
358
+ fileName: {
359
+ default: null,
360
+ parseHTML: (element) => {
361
+ return element.getAttribute('data-file-name') || null;
362
+ },
363
+ renderHTML: (attributes) => {
364
+ return {
365
+ 'data-file-name': attributes.fileName || null,
366
+ };
367
+ },
368
+ },
369
+ fileSize: {
370
+ default: null,
371
+ parseHTML: (element) => {
372
+ return element.getAttribute('data-file-size') || null;
373
+ },
374
+ renderHTML: (attributes) => {
375
+ return {
376
+ 'data-file-size': attributes.fileSize || null,
377
+ };
378
+ },
379
+ },
380
+ fileType: {
381
+ default: null,
382
+ parseHTML: (element) => {
383
+ return element.getAttribute('data-file-type') || null;
384
+ },
385
+ renderHTML: (attributes) => {
386
+ return {
387
+ 'data-file-type': attributes.fileType || null,
388
+ };
389
+ },
390
+ },
391
+ fileExt: {
392
+ default: null,
393
+ parseHTML: (element) => {
394
+ return element.getAttribute('data-file-ext') || null;
395
+ },
396
+ renderHTML: (attributes) => {
397
+ return {
398
+ 'data-file-ext': attributes.fileExt || null,
399
+ };
400
+ },
401
+ },
402
+ uploadId: {
403
+ default: null,
404
+ rendered: false,
405
+ },
406
+ };
407
+ },
408
+
409
+ parseHTML() {
410
+ return [{ tag: 'div[data-type="attachment"]' }];
411
+ },
412
+
413
+ renderHTML({ node, HTMLAttributes }) {
414
+ const attrs = node.attrs as AttachmentAttributes;
415
+ const url = attrs.url || '';
416
+ const fileName = attrs.fileName || '';
417
+ const fileExt = attrs.fileExt || '';
418
+ const fileSize = attrs.fileSize || '';
419
+
420
+ const displayTitle =
421
+ fileName + (fileExt ? `.${fileExt}` : '') || 'Attachment';
422
+
423
+ const meta = fileSize;
424
+
425
+ const merged = mergeAttributes(HTMLAttributes, {
426
+ 'data-type': 'attachment',
427
+ title: displayTitle,
428
+ });
429
+
430
+ if (!url) {
431
+ return ['div', merged];
432
+ }
433
+
434
+ return [
435
+ 'div',
436
+ merged,
437
+ [
438
+ 'a',
439
+ {
440
+ href: url,
441
+ target: '_blank',
442
+ rel: 'noopener noreferrer',
443
+ },
444
+ ['span', {}, displayTitle],
445
+ meta ? ['span', {}, ` (${meta})`] : '',
446
+ ],
447
+ ];
448
+ },
449
+
450
+ addNodeView() {
451
+ return ReactNodeViewRenderer(AttachmentNodeView);
452
+ },
453
+
454
+ addCommands() {
455
+ return {
456
+ setAttachment:
457
+ (attributes: AttachmentAttributes) =>
458
+ ({ commands }: CommandProps) => {
459
+ return commands.insertContent({
460
+ type: this.name,
461
+ attrs: attributes,
462
+ });
463
+ },
464
+
465
+ insertAttachments:
466
+ (files: File[]) =>
467
+ ({ editor, commands }: CommandProps) => {
468
+ if (!files || files.length === 0) return false;
469
+
470
+ const upload = this.options.upload;
471
+ if (!upload) return false;
472
+
473
+ const uploads = files.map((file) => {
474
+ const uploadId = nanoid();
475
+ return {
476
+ file,
477
+ uploadId,
478
+ attrs: fileToAttachmentAttributes(file, uploadId),
479
+ };
480
+ });
481
+
482
+ // 一次性插入多个节点,避免 NodeSelection 导致后续插入替换掉前一个节点
483
+ const inserted = commands.insertContent([
484
+ ...uploads.map(({ attrs }) => ({
485
+ type: this.name,
486
+ attrs,
487
+ })),
488
+ { type: 'paragraph' },
489
+ ]);
490
+
491
+ // 并发上传:不阻塞命令返回
492
+ void Promise.all(
493
+ uploads.map(async ({ file, uploadId }) => {
494
+ try {
495
+ const url = await upload(file);
496
+
497
+ // Ensure state is settled.
498
+ await new Promise((resolve) => requestAnimationFrame(resolve));
499
+
500
+ const pos = findAttachmentByUploadId(
501
+ editor.state.doc,
502
+ uploadId,
503
+ );
504
+ if (pos == null) return;
505
+
506
+ const node = editor.state.doc.nodeAt(pos);
507
+ if (!node || node.type.name !== this.name) return;
508
+
509
+ editor
510
+ .chain()
511
+ .command(({ tr }) => {
512
+ tr.setNodeMarkup(pos, undefined, {
513
+ ...node.attrs,
514
+ url,
515
+ uploadId: null,
516
+ });
517
+ return true;
518
+ })
519
+ .run();
520
+ } catch {
521
+ const pos = findAttachmentByUploadId(
522
+ editor.state.doc,
523
+ uploadId,
524
+ );
525
+ if (pos != null) {
526
+ editor
527
+ .chain()
528
+ .deleteRange({ from: pos, to: pos + 1 })
529
+ .run();
530
+ }
531
+ }
532
+ }),
533
+ );
534
+
535
+ return inserted;
536
+ },
537
+ };
538
+ },
539
+ });