@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,236 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { CodeBlock } from '@tiptap/extension-code-block';
5
+ import { Plugin, PluginKey } from '@tiptap/pm/state';
6
+ import { Decoration, DecorationSet } from '@tiptap/pm/view';
7
+ import {
8
+ NodeViewContent,
9
+ NodeViewWrapper,
10
+ ReactNodeViewRenderer,
11
+ type NodeViewProps,
12
+ } from '@tiptap/react';
13
+ import { debounce } from 'es-toolkit';
14
+
15
+ import {
16
+ bundledLanguages,
17
+ codeToTokensWithThemes,
18
+ type BundledLanguage,
19
+ } from '@/lib/shiki';
20
+ import { cn } from '@/lib/utils';
21
+
22
+ const codeBlockShikiPluginKey = new PluginKey<DecorationSet>(
23
+ 'codeBlockShikiDecorations',
24
+ );
25
+
26
+ const CodeNodeViewContent = NodeViewContent<'code'>;
27
+
28
+ type Language = BundledLanguage | 'text';
29
+
30
+ const themes = {
31
+ light: 'github-light',
32
+ dark: 'github-dark',
33
+ } as const;
34
+
35
+ const fallbackCodeLanguage = 'text';
36
+
37
+ function resolveLanguage(language: unknown) {
38
+ if (typeof language !== 'string' || !language.trim()) {
39
+ return fallbackCodeLanguage;
40
+ }
41
+
42
+ const normalized = language.trim().toLowerCase();
43
+ if (Object.keys(bundledLanguages).includes(normalized)) {
44
+ return normalized as BundledLanguage;
45
+ }
46
+
47
+ return fallbackCodeLanguage;
48
+ }
49
+
50
+ function getTokenInlineStyle(token: {
51
+ variants?: Record<string, { color?: string }>;
52
+ }) {
53
+ const lightColor = token.variants?.light?.color;
54
+ const darkColor = token.variants?.dark?.color;
55
+
56
+ if (!lightColor && !darkColor) {
57
+ return undefined;
58
+ }
59
+
60
+ const styles: string[] = [];
61
+ if (lightColor) styles.push(`--shiki-light: ${lightColor};`);
62
+ if (darkColor) styles.push(`--shiki-dark: ${darkColor}`);
63
+
64
+ return styles.join();
65
+ }
66
+
67
+ /**
68
+ * 使用 Shiki 为单个代码块生成 ProseMirror 装饰器
69
+ *
70
+ * @param code - 代码块的文本内容
71
+ * @param lang - 代码语言
72
+ * @param pos - 代码块在文档中的绝对起始位置
73
+ * @returns 解析生成的高亮装饰器数组
74
+ */
75
+ async function getDecorationsForCodeBlock(
76
+ code: string,
77
+ lang: Language,
78
+ pos: number,
79
+ ): Promise<Decoration[]> {
80
+ const decorations: Decoration[] = [];
81
+ if (lang === 'text') {
82
+ return decorations;
83
+ }
84
+
85
+ try {
86
+ const tokenizedLines = await codeToTokensWithThemes(code, { lang, themes });
87
+
88
+ for (const line of tokenizedLines) {
89
+ for (const token of line) {
90
+ const content = token.content ?? '';
91
+ if (!content) continue;
92
+
93
+ const style = getTokenInlineStyle(token);
94
+ if (!style) continue;
95
+
96
+ // 计算 token 在 ProseMirror 中的绝对位置
97
+ const from = pos + 1 + token.offset;
98
+ const to = from + content.length;
99
+ decorations.push(Decoration.inline(from, to, { style }));
100
+ }
101
+ }
102
+ } catch {
103
+ // 如果 Shiki 无法进行 tokenize(未知语言等),则回退到纯文本
104
+ }
105
+
106
+ return decorations;
107
+ }
108
+
109
+ async function buildDecorationsForDoc(doc: import('@tiptap/pm/model').Node) {
110
+ // 遍历整个 doc,为所有 codeBlock 生成高亮 Decorations。
111
+ // 注意:tokenize 是重操作,所以这里采用异步 + 合并 Promise 的方式。
112
+ const tasks: Array<Promise<Decoration[]>> = [];
113
+
114
+ doc.descendants((node, pos) => {
115
+ if (node.type.name !== 'codeBlock') return;
116
+
117
+ const code = node.textContent;
118
+ const lang = resolveLanguage(node.attrs.language);
119
+
120
+ tasks.push(getDecorationsForCodeBlock(code, lang, pos));
121
+ });
122
+
123
+ const results = await Promise.all(tasks);
124
+ return DecorationSet.create(doc, results.flat());
125
+ }
126
+
127
+ function CodeBlockShikiNodeView(props: NodeViewProps) {
128
+ const code = props.node.textContent;
129
+ const lines = React.useMemo(() => {
130
+ const split = code.split('\n');
131
+ return split.length ? split : [''];
132
+ }, [code]);
133
+
134
+ return (
135
+ <NodeViewWrapper className={cn('not-prose my-3')}>
136
+ <div
137
+ className={cn(
138
+ 'flex flex-col gap-2 rounded-md border border-border bg-muted/30 p-3',
139
+ )}
140
+ >
141
+ <div className="text-xs text-muted-foreground select-none">代码块</div>
142
+
143
+ <div className="flex items-start gap-2 overflow-x-auto">
144
+ <div className="shrink-0 select-none">
145
+ {lines.map((_, index) => (
146
+ <div
147
+ key={index}
148
+ className={cn(
149
+ 'w-4 text-center font-mono text-xs/5 text-muted-foreground select-none',
150
+ )}
151
+ >
152
+ {index + 1}
153
+ </div>
154
+ ))}
155
+ </div>
156
+
157
+ <pre
158
+ className={cn(
159
+ 'shiki m-0 flex-1 bg-transparent p-0 font-mono text-xs/5 text-foreground',
160
+ '[&_span]:text-(--shiki-light) dark:[&_span]:text-(--shiki-dark)',
161
+ )}
162
+ >
163
+ <CodeNodeViewContent
164
+ as="code"
165
+ className="block min-w-max whitespace-pre"
166
+ style={{ whiteSpace: 'pre' }}
167
+ />
168
+ </pre>
169
+ </div>
170
+ </div>
171
+ </NodeViewWrapper>
172
+ );
173
+ }
174
+
175
+ export const CodeBlockShiki = CodeBlock.extend({
176
+ addNodeView() {
177
+ return ReactNodeViewRenderer(CodeBlockShikiNodeView);
178
+ },
179
+
180
+ addProseMirrorPlugins() {
181
+ const basePlugins = this.parent?.() ?? [];
182
+
183
+ const highlightPlugin = new Plugin<DecorationSet>({
184
+ key: codeBlockShikiPluginKey,
185
+ state: {
186
+ init: () => DecorationSet.empty,
187
+ apply: (tr, previous) => {
188
+ const meta = tr.getMeta(codeBlockShikiPluginKey) as
189
+ | { decorations?: DecorationSet }
190
+ | undefined;
191
+
192
+ if (meta?.decorations) return meta.decorations;
193
+ if (tr.docChanged) return previous.map(tr.mapping, tr.doc);
194
+ return previous;
195
+ },
196
+ },
197
+ props: {
198
+ decorations(state) {
199
+ return codeBlockShikiPluginKey.getState(state) ?? undefined;
200
+ },
201
+ },
202
+ view: (view) => {
203
+ const schedule = debounce(async () => {
204
+ const snapshotDoc = view.state.doc;
205
+ try {
206
+ const decorations = await buildDecorationsForDoc(snapshotDoc);
207
+ if (!view.state.doc.eq(snapshotDoc)) return;
208
+ const tr = view.state.tr.setMeta(codeBlockShikiPluginKey, {
209
+ decorations,
210
+ });
211
+ view.dispatch(tr);
212
+ } catch {
213
+ // ignore
214
+ }
215
+ }, 150);
216
+
217
+ schedule();
218
+
219
+ return {
220
+ update: (_view, prevState) => {
221
+ const docChanged = !prevState.doc.eq(view.state.doc);
222
+
223
+ if (docChanged) {
224
+ schedule();
225
+ }
226
+ },
227
+ destroy: () => {
228
+ schedule.cancel();
229
+ },
230
+ };
231
+ },
232
+ });
233
+
234
+ return [...basePlugins, highlightPlugin];
235
+ },
236
+ });
@@ -0,0 +1,260 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import Color, { type ColorOptions } from '@tiptap/extension-color';
3
+ import Highlight, { type HighlightOptions } from '@tiptap/extension-highlight';
4
+ import type { LinkOptions } from '@tiptap/extension-link';
5
+ import Placeholder, {
6
+ type PlaceholderOptions,
7
+ } from '@tiptap/extension-placeholder';
8
+ import TaskItem, { type TaskItemOptions } from '@tiptap/extension-task-item';
9
+ import TaskList, { type TaskListOptions } from '@tiptap/extension-task-list';
10
+ import TextAlign, { type TextAlignOptions } from '@tiptap/extension-text-align';
11
+ import { TextStyle, type TextStyleOptions } from '@tiptap/extension-text-style';
12
+ import StarterKit, { type StarterKitOptions } from '@tiptap/starter-kit';
13
+
14
+ import {
15
+ Attachment,
16
+ type AttachmentExtensionOptions,
17
+ } from '@/components/business-ui/tiptap-editor/extensions/attachment';
18
+ import { CodeBlockShiki } from '@/components/business-ui/tiptap-editor/extensions/code-block-shiki';
19
+ import {
20
+ Image,
21
+ type ImageOptions,
22
+ } from '@/components/business-ui/tiptap-editor/extensions/image';
23
+
24
+ /**
25
+ * 从 StarterKitOptions 中排除不需要单独配置的选项
26
+ */
27
+ type StarterKitBaseOptions = Omit<
28
+ StarterKitOptions,
29
+ 'heading' | 'link' | 'codeBlock'
30
+ >;
31
+
32
+ export interface CompleteKitOptions extends StarterKitBaseOptions {
33
+ // #region 块级扩展
34
+ /**
35
+ * Heading 配置。设置为 false 禁用。
36
+ * @default { levels: [1, 2, 3, 4, 5, 6] }
37
+ */
38
+ heading: StarterKitOptions['heading'];
39
+
40
+ /**
41
+ * CodeBlock (Shiki 语法高亮) 配置。设置为 false 禁用。
42
+ * 注意:此扩展使用 CodeBlockShiki 替代官方 CodeBlock。
43
+ */
44
+ codeBlock: false;
45
+ // #endregion
46
+
47
+ // #region 行内标记扩展
48
+ /**
49
+ * Link 配置。设置为 false 禁用。
50
+ * @default { autolink: true, openOnClick: true }
51
+ */
52
+ link: Partial<LinkOptions> | false;
53
+ // #endregion
54
+
55
+ // #region 列表扩展
56
+ /**
57
+ * TaskList 配置。设置为 false 禁用。
58
+ */
59
+ taskList: Partial<TaskListOptions> | false;
60
+
61
+ /**
62
+ * TaskItem 配置。设置为 false 禁用。
63
+ * @default { nested: true }
64
+ */
65
+ taskItem: Partial<TaskItemOptions> | false;
66
+ // #endregion
67
+
68
+ // #region 样式扩展
69
+ /**
70
+ * TextStyle 配置。设置为 false 禁用。
71
+ */
72
+ textStyle: Partial<TextStyleOptions> | false;
73
+
74
+ /**
75
+ * Color 配置。设置为 false 禁用。
76
+ */
77
+ color: Partial<ColorOptions> | false;
78
+
79
+ /**
80
+ * Highlight 配置。设置为 false 禁用。
81
+ * @default { multicolor: true }
82
+ */
83
+ highlight: Partial<HighlightOptions> | false;
84
+
85
+ /**
86
+ * TextAlign 配置。设置为 false 禁用。
87
+ * @default { types: ["heading", "paragraph"] }
88
+ */
89
+ textAlign: Partial<TextAlignOptions> | false;
90
+ // #endregion
91
+
92
+ // #region 媒体扩展
93
+ /**
94
+ * Image 配置。设置为 false 禁用。
95
+ */
96
+ image: Partial<ImageOptions> | false;
97
+
98
+ /**
99
+ * Attachment 配置。设置为 false 禁用。
100
+ */
101
+ attachment: Partial<AttachmentExtensionOptions> | false;
102
+ // #endregion
103
+
104
+ // #region 其他扩展
105
+ /**
106
+ * Placeholder 配置。设置为 false 禁用。
107
+ * @default { placeholder: "请输入..." }
108
+ */
109
+ placeholder: Partial<PlaceholderOptions> | false;
110
+ // #endregion
111
+ }
112
+
113
+ /**
114
+ * CompleteKit 是一个完整的编辑器扩展集合。
115
+ *
116
+ * 它是构建富文本编辑器的良好起点,包含了所有常用的编辑功能。
117
+ *
118
+ * @example
119
+ * ```ts
120
+ * // 使用默认配置
121
+ * const editor = useEditor({
122
+ * extensions: [CompleteKit],
123
+ * })
124
+ *
125
+ * // 自定义配置
126
+ * const editor = useEditor({
127
+ * extensions: [
128
+ * CompleteKit.configure({
129
+ * // 禁用 image 扩展
130
+ * image: false,
131
+ * // 自定义 placeholder
132
+ * placeholder: { placeholder: "开始输入..." },
133
+ * // 自定义 heading
134
+ * heading: { levels: [1, 2, 3] },
135
+ * // 禁用 link
136
+ * link: false,
137
+ * }),
138
+ * ],
139
+ * })
140
+ * ```
141
+ */
142
+ export const CompleteKit = Extension.create<CompleteKitOptions>({
143
+ name: 'completeKit',
144
+
145
+ addExtensions() {
146
+ const extensions = [];
147
+
148
+ // StarterKit 基础扩展(排除 heading、link、codeBlock)
149
+ const starterKitOptions: Partial<StarterKitOptions> = {
150
+ // 将顶级选项映射到 StarterKit
151
+ blockquote: this.options.blockquote,
152
+ bold: this.options.bold,
153
+ bulletList: this.options.bulletList,
154
+ code: this.options.code,
155
+ document: this.options.document,
156
+ dropcursor: this.options.dropcursor,
157
+ gapcursor: this.options.gapcursor,
158
+ hardBreak: this.options.hardBreak,
159
+ horizontalRule: this.options.horizontalRule,
160
+ italic: this.options.italic,
161
+ listItem: this.options.listItem,
162
+ listKeymap: this.options.listKeymap,
163
+ orderedList: this.options.orderedList,
164
+ paragraph: this.options.paragraph,
165
+ strike: this.options.strike,
166
+ text: this.options.text,
167
+ trailingNode: this.options.trailingNode,
168
+ underline: this.options.underline,
169
+ undoRedo: this.options.undoRedo,
170
+ // 默认禁用 StarterKit 的 codeBlock,使用 CodeBlockShiki 替代
171
+ codeBlock: false,
172
+ // 配置 heading
173
+ heading:
174
+ this.options.heading !== false
175
+ ? {
176
+ levels: [1, 2, 3, 4, 5, 6],
177
+ ...this.options.heading,
178
+ }
179
+ : false,
180
+ // 配置 link
181
+ link:
182
+ this.options.link !== false
183
+ ? {
184
+ autolink: true,
185
+ openOnClick: true,
186
+ ...this.options.link,
187
+ }
188
+ : false,
189
+ };
190
+
191
+ extensions.push(StarterKit.configure(starterKitOptions));
192
+
193
+ // CodeBlock (Shiki)
194
+ if (this.options.codeBlock !== false) {
195
+ extensions.push(CodeBlockShiki);
196
+ }
197
+
198
+ // TaskList & TaskItem
199
+ if (this.options.taskList !== false) {
200
+ extensions.push(TaskList.configure(this.options.taskList));
201
+ }
202
+
203
+ if (this.options.taskItem !== false) {
204
+ extensions.push(
205
+ TaskItem.configure({
206
+ nested: true,
207
+ ...this.options.taskItem,
208
+ }),
209
+ );
210
+ }
211
+
212
+ // 样式扩展
213
+ if (this.options.textStyle !== false) {
214
+ extensions.push(TextStyle.configure(this.options.textStyle));
215
+ }
216
+
217
+ if (this.options.color !== false) {
218
+ extensions.push(Color.configure(this.options.color));
219
+ }
220
+
221
+ if (this.options.highlight !== false) {
222
+ extensions.push(
223
+ Highlight.configure({
224
+ multicolor: true,
225
+ ...this.options.highlight,
226
+ }),
227
+ );
228
+ }
229
+
230
+ if (this.options.textAlign !== false) {
231
+ extensions.push(
232
+ TextAlign.configure({
233
+ types: ['heading', 'paragraph'],
234
+ ...this.options.textAlign,
235
+ }),
236
+ );
237
+ }
238
+
239
+ // 媒体扩展
240
+ if (this.options.image !== false) {
241
+ extensions.push(Image.configure(this.options.image));
242
+ }
243
+
244
+ if (this.options.attachment !== false) {
245
+ extensions.push(Attachment.configure(this.options.attachment));
246
+ }
247
+
248
+ // 其他扩展
249
+ if (this.options.placeholder !== false) {
250
+ extensions.push(
251
+ Placeholder.configure({
252
+ placeholder: '请输入...',
253
+ ...this.options.placeholder,
254
+ }),
255
+ );
256
+ }
257
+
258
+ return extensions;
259
+ },
260
+ });