@jx3box/jx3box-editor 2.2.47 → 3.0.0

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 (175) hide show
  1. package/config/global.js +79 -0
  2. package/config/global.less +16 -0
  3. package/index.js +21 -8
  4. package/package.json +64 -63
  5. package/readme.md +25 -99
  6. package/src/Article.vue +96 -57
  7. package/src/ArticleMarkdown.vue +54 -47
  8. package/src/BoxResource.vue +67 -42
  9. package/src/Buff.vue +18 -7
  10. package/src/GameText.vue +32 -45
  11. package/src/Item.vue +143 -235
  12. package/src/ItemSimple.vue +27 -37
  13. package/src/Markdown.vue +362 -210
  14. package/src/Npc.vue +51 -30
  15. package/src/Resource.vue +296 -252
  16. package/src/Skill.vue +36 -26
  17. package/src/Tinymce.vue +162 -126
  18. package/src/Upload.vue +238 -155
  19. package/src/UploadAlbum.vue +189 -118
  20. package/{assets → src/assets}/css/article.less +1 -0
  21. package/src/assets/css/markdown.less +4 -0
  22. package/{assets → src/assets}/css/module/author.less +4 -3
  23. package/{assets → src/assets}/css/module/directory.less +23 -32
  24. package/{assets → src/assets}/css/module/talent.less +2 -2
  25. package/{assets → src/assets}/css/resource.less +56 -22
  26. package/src/assets/css/tinymce/_.less +28 -0
  27. package/src/assets/css/tinymce/a.less +21 -0
  28. package/{assets → src/assets}/css/tinymce/code.less +1 -1
  29. package/{assets/css → src/assets/css/tinymce}/combo.less +123 -18
  30. package/{assets → src/assets}/css/tinymce/fold.less +3 -6
  31. package/src/assets/css/tinymce/h.less +90 -0
  32. package/{assets → src/assets}/css/tinymce/latex.less +14 -14
  33. package/{assets → src/assets}/css/tinymce/macro.less +3 -3
  34. package/{assets → src/assets}/css/tinymce/pz.less +2 -2
  35. package/{assets → src/assets}/css/tinymce/table.less +5 -10
  36. package/{assets → src/assets}/css/tinymce.less +8 -4
  37. package/src/assets/css/upload.less +195 -0
  38. package/src/assets/css/upload_album.less +164 -0
  39. package/src/assets/css/var.less +2 -0
  40. package/src/assets/img/other/qr-code.svg +1 -0
  41. package/{assets → src/assets}/js/audio.js +2 -2
  42. package/{assets → src/assets}/js/directory.js +51 -23
  43. package/src/assets/js/hljs_languages.js +177 -0
  44. package/src/assets/js/katex.js +211 -0
  45. package/src/assets/js/renderImgPreview.js +49 -0
  46. package/{assets → src/assets}/js/xss.js +48 -5
  47. package/src/components/Author.vue +32 -13
  48. package/src/components/Avatar.vue +22 -3
  49. package/src/components/Combo.vue +118 -72
  50. package/src/components/PostAuthor.vue +13 -11
  51. package/src/components/QRcode.vue +136 -0
  52. package/src/components/SkillMartial.vue +13 -12
  53. package/src/pages/article.js +14 -0
  54. package/src/pages/index.js +5 -0
  55. package/src/pages/markdown.js +14 -0
  56. package/src/pages/tinymce.js +49 -0
  57. package/src/pages/upload.js +14 -0
  58. package/{service → src/service}/author.js +2 -2
  59. package/{service → src/service}/cms.js +5 -3
  60. package/{service → src/service}/database.js +4 -2
  61. package/{service → src/service}/item.js +1 -1
  62. package/{service → src/service}/node.js +1 -1
  63. package/{service → src/service}/resource.js +1 -1
  64. package/src/views/article.vue +72 -0
  65. package/src/views/index.vue +11 -0
  66. package/src/views/markdown.vue +58 -0
  67. package/src/views/tinymce.vue +58 -0
  68. package/src/views/upload.vue +111 -0
  69. package/.env +0 -2
  70. package/.vscode/settings.json +0 -5
  71. package/assets/css/katex-fix.css +0 -20
  72. package/assets/css/tinymce/_.less +0 -30
  73. package/assets/css/tinymce/a.less +0 -30
  74. package/assets/css/tinymce/combo.less +0 -111
  75. package/assets/css/tinymce/h.less +0 -58
  76. package/assets/css/upload.less +0 -105
  77. package/assets/js/katex.js +0 -191
  78. package/assets/js/renderImgPreview.js +0 -25
  79. package/babel.config.js +0 -3
  80. package/docs/markdown.md +0 -16
  81. package/jsconfig.json +0 -9
  82. package/public/article.html +0 -15
  83. package/public/css/article.css +0 -2481
  84. package/public/css/article.less +0 -3
  85. package/public/favicon.ico +0 -0
  86. package/public/tinymce.html +0 -33
  87. package/src/Equip.vue +0 -301
  88. package/src/components/LetterDemo.vue +0 -93
  89. package/src/components/medal.vue +0 -43
  90. package/test-audio.html +0 -121
  91. package/vue.config.js +0 -147
  92. /package/{assets → src/assets}/css/markdown/_.less +0 -0
  93. /package/{assets → src/assets}/css/markdown/macro.less +0 -0
  94. /package/{assets/css/article_markdown.less → src/assets/css/markdown/markdown-article.less} +0 -0
  95. /package/{assets/css/markdown.less → src/assets/css/markdown/markdown-editor.less} +0 -0
  96. /package/{assets → src/assets}/css/markdown/talent.less +0 -0
  97. /package/{assets → src/assets}/css/markdown/video.less +0 -0
  98. /package/{assets → src/assets}/css/module/buff.less +0 -0
  99. /package/{assets → src/assets}/css/module/icon.less +0 -0
  100. /package/{assets → src/assets}/css/module/item.less +0 -0
  101. /package/{assets → src/assets}/css/module/item_simple.less +0 -0
  102. /package/{assets → src/assets}/css/module/jx3_element.less +0 -0
  103. /package/{assets → src/assets}/css/module/macro.less +0 -0
  104. /package/{assets → src/assets}/css/module/npc.less +0 -0
  105. /package/{assets → src/assets}/css/module/resource.less +0 -0
  106. /package/{assets → src/assets}/css/module/skill.less +0 -0
  107. /package/{assets → src/assets}/css/tinymce/hr.less +0 -0
  108. /package/{assets → src/assets}/css/tinymce/img.less +0 -0
  109. /package/{assets → src/assets}/css/tinymce/imgpreview.less +0 -0
  110. /package/{assets → src/assets}/css/tinymce/list.less +0 -0
  111. /package/{assets → src/assets}/css/tinymce/nextpage.less +0 -0
  112. /package/{assets → src/assets}/css/tinymce/p.less +0 -0
  113. /package/{assets → src/assets}/css/tinymce/plugin.less +0 -0
  114. /package/{assets → src/assets}/css/tinymce/qixue.less +0 -0
  115. /package/{assets → src/assets}/css/tinymce/quote.less +0 -0
  116. /package/{assets → src/assets}/css/tinymce/video.less +0 -0
  117. /package/{assets → src/assets}/css/tinymce/voice.less +0 -0
  118. /package/{assets → src/assets}/data/detach_type.json +0 -0
  119. /package/{assets → src/assets}/data/game_font.json +0 -0
  120. /package/{assets → src/assets}/data/markdown_whitelist.json +0 -0
  121. /package/{assets → src/assets}/data/weapon_type.json +0 -0
  122. /package/{assets → src/assets}/img/buff.svg +0 -0
  123. /package/{assets → src/assets}/img/equip_bg.png +0 -0
  124. /package/{assets → src/assets}/img/file.svg +0 -0
  125. /package/{assets → src/assets}/img/icons.svg +0 -0
  126. /package/{assets → src/assets}/img/item/pve.png +0 -0
  127. /package/{assets → src/assets}/img/item/pvp.png +0 -0
  128. /package/{assets → src/assets}/img/item/pvx.png +0 -0
  129. /package/{assets → src/assets}/img/item/std.png +0 -0
  130. /package/{assets → src/assets}/img/item/wujie.png +0 -0
  131. /package/{assets → src/assets}/img/item.svg +0 -0
  132. /package/{assets → src/assets}/img/jx3.svg +0 -0
  133. /package/{assets → src/assets}/img/jx3box.svg +0 -0
  134. /package/{assets → src/assets}/img/npc/attack.svg +0 -0
  135. /package/{assets → src/assets}/img/npc/buff.svg +0 -0
  136. /package/{assets → src/assets}/img/npc/energy.svg +0 -0
  137. /package/{assets → src/assets}/img/npc/miss.svg +0 -0
  138. /package/{assets → src/assets}/img/npc/npc.svg +0 -0
  139. /package/{assets → src/assets}/img/npc/radar.svg +0 -0
  140. /package/{assets → src/assets}/img/npc/shield.svg +0 -0
  141. /package/{assets → src/assets}/img/npc/sight.svg +0 -0
  142. /package/{assets → src/assets}/img/npc/skull.svg +0 -0
  143. /package/{assets → src/assets}/img/npc/target.svg +0 -0
  144. /package/{assets → src/assets}/img/skill.svg +0 -0
  145. /package/{assets → src/assets}/img/skillset.png +0 -0
  146. /package/{assets → src/assets}/js/a.js +0 -0
  147. /package/{assets → src/assets}/js/code.js +0 -0
  148. /package/{assets → src/assets}/js/combo.js +0 -0
  149. /package/{assets → src/assets}/js/drag.js +0 -0
  150. /package/{assets → src/assets}/js/filter2.js +0 -0
  151. /package/{assets → src/assets}/js/fold.js +0 -0
  152. /package/{assets → src/assets}/js/gallery.js +0 -0
  153. /package/{assets → src/assets}/js/iframe.js +0 -0
  154. /package/{assets → src/assets}/js/img.js +0 -0
  155. /package/{assets → src/assets}/js/item/attribute_percent.js +0 -0
  156. /package/{assets → src/assets}/js/item/bind.js +0 -0
  157. /package/{assets → src/assets}/js/item/border.js +0 -0
  158. /package/{assets → src/assets}/js/item/border_quest.js +0 -0
  159. /package/{assets → src/assets}/js/item/color.js +0 -0
  160. /package/{assets → src/assets}/js/item/hljs_languages.js +0 -0
  161. /package/{assets → src/assets}/js/item/icon_url.js +0 -0
  162. /package/{assets → src/assets}/js/item/second_format.js +0 -0
  163. /package/{assets → src/assets}/js/jx3_element.js +0 -0
  164. /package/{assets → src/assets}/js/macro.js +0 -0
  165. /package/{assets → src/assets}/js/nextpage.js +0 -0
  166. /package/{assets → src/assets}/js/pswp.js +0 -0
  167. /package/{assets → src/assets}/js/pswp_template.js +0 -0
  168. /package/{assets → src/assets}/js/pz_iframe.js +0 -0
  169. /package/{assets → src/assets}/js/qixue.js +0 -0
  170. /package/{assets → src/assets}/js/script.js +0 -0
  171. /package/{assets → src/assets}/js/talent2.js +0 -0
  172. /package/{assets → src/assets}/js/tex-mml-chtml.js +0 -0
  173. /package/{service → src/service}/enum/CollectionPublic.js +0 -0
  174. /package/{service → src/service}/enum/EquipPosition.js +0 -0
  175. /package/{service → src/service}/enum/EquipType.js +0 -0
package/src/Markdown.vue CHANGED
@@ -1,58 +1,51 @@
1
1
  <template>
2
2
  <div class="c-editor-markdown">
3
-
4
3
  <slot name="prepend"></slot>
5
4
 
6
- <div class="c-editor-header">
7
- <Upload v-if="attachmentEnable" @insert="insertAttachments" :enable="true" />
8
- <Resource style="margin-right: 5px;" v-if="resourceEnable" @insert="insertResource" :enable="true" />
9
- <BoxResource v-if="resourceEnable" @insert="insertResource" :enable="true" />
5
+ <div class="c-editor-markdown__toolbar">
6
+ <div class="c-editor-markdown__toolbar-left">
7
+ <span class="c-editor-markdown__toolbar-label">编辑模式</span>
8
+ <el-radio-group v-model="editorMode" size="small">
9
+ <el-radio-button v-for="item in modeOptions" :key="item.value" :value="item.value">
10
+ {{ item.label }}
11
+ </el-radio-button>
12
+ </el-radio-group>
13
+ </div>
14
+ <div class="c-editor-markdown__toolbar-tip">GitHub 样式预览,支持 Ctrl+V 粘贴图片上传</div>
10
15
  </div>
11
16
 
12
17
  <slot></slot>
13
18
 
14
- <markdown-editor class="c-markdown c-article" ref="md" v-model="data" @change="updateData" :subfield="false" :xssOptions="xssOptions">
15
- <template slot="left-toolbar-after">
16
- <markdown-katex @insert="insertKatex" />
17
- <span class="c-markdown-toolbar-image c-markdown-toolbar-item" title="上传图片" @click="selectImages"><i class="el-icon-picture-outline-round"></i></span>
18
- <span class="c-markdown-toolbar-file c-markdown-toolbar-item" title="上传附件" @click="selectFiles"><i class="el-icon-paperclip"></i></span>
19
- <markdown-video @insert="insertVideo" />
20
- <markdown-macro @insert="insertMacro" />
21
- <markdown-pz @insert="insertPz" />
22
- <markdown-talent @insert="insertTalent" />
23
- <markdown-talent2 @insert="insertTalent2" />
24
- </template>
25
- </markdown-editor>
26
- <input class="c-markdown-store-item" id="c-markdown-store-images" type="file" @change="uploadImages" ref="markdownImages" multiple :accept="allow_image_types" />
27
- <input class="c-markdown-store-item" id="c-markdown-store-files" type="file" @change="uploadFiles" ref="markdownFiles" multiple />
19
+ <div ref="editorHost" class="c-editor-markdown__host"></div>
28
20
 
29
21
  <slot name="append"></slot>
30
22
  </div>
31
23
  </template>
32
24
 
33
25
  <script>
34
- import markdownEditor from '@jx3box/markdown/src/editor.vue'
35
-
36
- import {xssOptions} from '../assets/data/markdown_whitelist.json'
37
- import { uploadFile } from "../service/cms";
38
-
39
- import Upload from "./Upload";
40
- import Resource from "./Resource";
41
- import BoxResource from "./BoxResource";
26
+ import axios from "axios";
27
+ import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
28
+ import Vditor from "vditor";
29
+ import "vditor/dist/index.css";
30
+ import "github-markdown-css/github-markdown.css";
42
31
 
43
- // jx3
44
- import markdownMacro from './components/markdown/macro.vue'
45
- import markdownPz from './components/markdown/pz.vue'
46
- import markdownKatex from './components/markdown/katex.vue'
47
- import markdownTalent from './components/markdown/talent.vue'
48
- import markdownTalent2 from './components/markdown/talent2.vue'
49
- import markdownVideo from './components/markdown/video.vue'
32
+ const { __cms } = JX3BOX;
33
+ const UPLOAD_API = `${__cms}api/cms/upload`;
34
+ const MODE_OPTIONS = [
35
+ { label: "所见即所得", value: "wysiwyg" },
36
+ { label: "即时渲染", value: "ir" },
37
+ { label: "分屏预览", value: "sv" },
38
+ ];
50
39
 
51
40
  export default {
52
41
  name: "Markdown",
53
42
  props: {
43
+ modelValue: {
44
+ type: String,
45
+ },
54
46
  content: {
55
47
  type: String,
48
+ default: "",
56
49
  },
57
50
  editable: {
58
51
  type: Boolean,
@@ -66,213 +59,372 @@ export default {
66
59
  type: Boolean,
67
60
  default: true,
68
61
  },
62
+ placeholder: {
63
+ type: String,
64
+ default: "支持 Markdown,支持 Ctrl+V 粘贴图片上传",
65
+ },
66
+ height: {
67
+ type: [String, Number],
68
+ default: 720,
69
+ },
69
70
  },
70
- components: {
71
- markdownEditor,
72
- Upload,
73
- Resource,
74
- BoxResource,
75
-
76
- markdownMacro,
77
- markdownPz,
78
- markdownKatex,
79
- markdownTalent,
80
- markdownTalent2,
81
- markdownVideo,
82
- },
83
- data: function() {
71
+ emits: ["update:modelValue", "update:content", "update", "updateData"],
72
+ data() {
84
73
  return {
85
- data: this.content,
86
- allow_image_types: ["image/png", "image/jpeg", "image/gif", "image/bmp", "image/webp"],
87
- allow_file_types: [],
88
- image_ext: ["png", "jpg", "gif", "bmp", "webp"],
89
- files: [],
90
- resolved_files: [],
91
-
92
- xssOptions
74
+ data: this.modelValue ?? this.content ?? "",
75
+ editor: null,
76
+ editorReady: false,
77
+ editorMode: "ir",
78
+ modeOptions: MODE_OPTIONS,
79
+ isRebuildingEditor: false,
80
+ isUploadingImage: false,
93
81
  };
94
82
  },
95
- model: {
96
- prop: "content",
97
- event: "update",
98
- },
99
83
  watch: {
100
- data: function(newval) {
101
- this.$emit("update", newval);
102
- },
103
- content: function(newval) {
104
- this.data = newval;
84
+ modelValue: {
85
+ immediate: true,
86
+ handler(value) {
87
+ if (value !== undefined && value !== this.data) {
88
+ this.data = value ?? "";
89
+ this.syncEditorValue(this.data);
90
+ }
91
+ },
105
92
  },
106
- // 监听过滤后的文件列表
107
- files_list: function(list) {
108
- this.bulkUpload(list);
93
+ content: {
94
+ immediate: true,
95
+ handler(value) {
96
+ if (this.modelValue === undefined && value !== this.data) {
97
+ this.data = value ?? "";
98
+ this.syncEditorValue(this.data);
99
+ }
100
+ },
109
101
  },
110
- },
111
- computed: {
112
- files_list: function() {
113
- let files = Array.from(this.files);
114
- return files;
102
+ editable: {
103
+ immediate: true,
104
+ handler() {
105
+ this.applyEditableState();
106
+ },
115
107
  },
116
- $md: function() {
117
- return this.$refs.md;
108
+ editorMode(nextMode, prevMode) {
109
+ if (!this.editorReady || this.isRebuildingEditor || nextMode === prevMode) {
110
+ return;
111
+ }
112
+
113
+ this.rebuildEditor(nextMode);
118
114
  },
119
115
  },
116
+ mounted() {
117
+ this.initEditor();
118
+ },
119
+ beforeUnmount() {
120
+ this.destroyEditor();
121
+ },
120
122
  methods: {
121
- // 点击上传按钮
122
- selectImages: function() {
123
- document.getElementById("c-markdown-store-images").dispatchEvent(new MouseEvent("click"));
124
- },
125
- selectFiles: function() {
126
- document.getElementById("c-markdown-store-files").dispatchEvent(new MouseEvent("click"));
123
+ getPreviewMode(mode) {
124
+ return mode === "sv" ? "both" : "editor";
127
125
  },
128
- // 监听选择结果变化
129
- uploadImages: function(e) {
130
- this.files = this.$refs.markdownImages.files;
126
+ getToolbar() {
127
+ return [
128
+ "headings",
129
+ "bold",
130
+ "italic",
131
+ "strike",
132
+ "link",
133
+ "|",
134
+ "list",
135
+ "ordered-list",
136
+ "check",
137
+ "quote",
138
+ "code",
139
+ "inline-code",
140
+ "table",
141
+ "|",
142
+ "undo",
143
+ "redo",
144
+ "fullscreen",
145
+ ];
131
146
  },
132
- uploadFiles: function(e) {
133
- this.files = this.$refs.markdownFiles.files;
147
+ buildEditorOptions(initialValue) {
148
+ return {
149
+ cache: {
150
+ enable: false,
151
+ },
152
+ classes: {
153
+ preview: "markdown-body",
154
+ },
155
+ counter: {
156
+ enable: true,
157
+ type: "markdown",
158
+ },
159
+ height: this.height,
160
+ icon: "ant",
161
+ lang: "zh_CN",
162
+ mode: this.editorMode,
163
+ placeholder: this.placeholder,
164
+ preview: {
165
+ delay: 0,
166
+ hljs: {
167
+ enable: true,
168
+ lineNumber: false,
169
+ style: "github",
170
+ },
171
+ markdown: {
172
+ sanitize: true,
173
+ },
174
+ mode: this.getPreviewMode(this.editorMode),
175
+ },
176
+ customWysiwygToolbar() {},
177
+ toolbar: this.getToolbar(),
178
+ toolbarConfig: {
179
+ pin: true,
180
+ },
181
+ upload: {
182
+ accept: "image/*",
183
+ max: 30 * 1024 * 1024,
184
+ multiple: false,
185
+ withCredentials: true,
186
+ handler: async (files) => this.handleImageUpload(files),
187
+ },
188
+ value: initialValue,
189
+ after: () => {
190
+ this.editorReady = true;
191
+ this.syncEditorValue(this.data);
192
+ this.applyEditableState();
193
+ },
194
+ input: (value) => {
195
+ this.handleEditorInput(value);
196
+ },
197
+ };
134
198
  },
135
- // 批量上传
136
- bulkUpload: function(list) {
137
- // 存在有效数据队列时
138
- if (!list || !list.length) return;
139
-
140
- // 上传队列
141
- let queue = [];
142
- for (let item of list) {
143
- let formdata = new FormData();
144
- formdata.append("file", item);
145
- queue.push(uploadFile(formdata));
146
- }
199
+ initEditor(initialValue = this.data) {
200
+ const host = this.$refs.editorHost;
201
+ if (!host) return;
147
202
 
148
- // 回调处理
149
- Promise.allSettled(queue)
150
- .then((results) => {
151
- results.forEach((result, i) => {
152
- if (result.status == "fulfilled") {
153
- let url = result.value.data?.data?.[0];
154
- this.resolved_files.push({
155
- url: url,
156
- filename: list[i]["name"],
157
- type: list[i]["type"],
158
- ext: list[i]["name"].split(".").pop(),
159
- });
160
- }
161
- });
162
- this.insertFiles();
163
- })
164
- .finally(() => {
165
- // 上传完成后清空input
166
- this.images = [];
167
- this.files = [];
168
- this.resolved_files = [];
169
- });
203
+ this.editorReady = false;
204
+ this.editor = new Vditor(host, this.buildEditorOptions(initialValue));
170
205
  },
171
- // 插入正文
172
- insertFiles: function() {
173
- for (let item of this.resolved_files) {
174
- // 插入图片
175
- if (this.image_ext.includes(item.ext)) {
176
- this.$md.insertText(this.$md.getTextareaDom(), {
177
- prefix: `![${item.filename}](${item.url})`,
178
- subfix: "",
179
- str: "",
180
- });
181
- // 插入文字链接
182
- } else {
183
- this.$md.insertText(this.$md.getTextareaDom(), {
184
- prefix: `[${item.filename}](${item.url})`,
185
- subfix: "",
186
- str: "",
187
- });
188
- }
189
- }
206
+ destroyEditor() {
207
+ if (!this.editor) return;
208
+
209
+ this.editor.destroy();
210
+ this.editor = null;
211
+ this.editorReady = false;
190
212
  },
191
- // 更新触发
192
- updateData: function(data, render) {
193
- this.$emit("updateData", {
194
- data,
195
- render,
213
+ rebuildEditor() {
214
+ const currentValue = this.editor?.getValue?.() ?? this.data ?? "";
215
+ this.data = currentValue;
216
+ this.isRebuildingEditor = true;
217
+ this.destroyEditor();
218
+ this.$nextTick(() => {
219
+ this.initEditor(currentValue);
220
+ this.isRebuildingEditor = false;
196
221
  });
197
- this.data = data
198
222
  },
199
- // 插入附件
200
- insertAttachments: function(data) {
201
- let list = data?.list || []
202
- for(let item of list){
203
- // 插入图片
204
- if (item.is_img) {
205
- this.$md.insertText(this.$md.getTextareaDom(), {
206
- prefix: `![${item.name}](${item.url})`,
207
- subfix: "",
208
- str: "",
209
- });
210
- // 插入文字链接
211
- } else {
212
- this.$md.insertText(this.$md.getTextareaDom(), {
213
- prefix: `[${item.name}](${item.url})`,
214
- subfix: "",
215
- str: "",
216
- });
217
- }
223
+ syncEditorValue(value) {
224
+ if (!this.editorReady || !this.editor) return;
225
+
226
+ const nextValue = value ?? "";
227
+ if (this.editor.getValue() === nextValue) return;
228
+ this.editor.setValue(nextValue);
229
+ },
230
+ applyEditableState() {
231
+ if (!this.editorReady || !this.editor) return;
232
+
233
+ if (this.editable) {
234
+ this.editor.enable();
235
+ } else {
236
+ this.editor.disabled();
218
237
  }
219
238
  },
220
- insertResource: function(data) {
221
- this.$md.insertText(this.$md.getTextareaDom(), {
222
- prefix: data,
223
- subfix: "",
224
- str: "",
225
- });
239
+ handleEditorInput(value) {
240
+ this.data = value;
241
+ this.emitEditorPayload(value);
226
242
  },
227
- insertMacro(data) {
228
- this.$md.insertText(this.$md.getTextareaDom(), {
229
- prefix: data,
230
- subfix: "",
231
- str: "",
232
- });
233
- },
234
- insertPz(data) {
235
- this.$md.insertText(this.$md.getTextareaDom(), {
236
- prefix: data,
237
- subfix: "",
238
- str: "",
243
+ emitEditorPayload(value) {
244
+ this.$emit("update:modelValue", value);
245
+ this.$emit("update:content", value);
246
+ this.$emit("update", value);
247
+ this.$emit("updateData", {
248
+ data: value,
249
+ render: this.editor?.getHTML?.() ?? "",
239
250
  });
240
251
  },
241
- insertKatex(data) {
242
- this.$md.insertText(this.$md.getTextareaDom(), {
243
- prefix: data,
244
- subfix: "",
245
- str: "",
246
- });
252
+ validateImageFile(file) {
253
+ if (!file) return "未检测到可上传的文件";
254
+ if (!file.type?.startsWith("image/")) return "当前仅支持粘贴图片上传";
255
+ if (file.size > 30 * 1024 * 1024) return "图片大小不能超过 30MB";
256
+ return "";
247
257
  },
248
- insertTalent(data) {
249
- this.$md.insertText(this.$md.getTextareaDom(), {
250
- prefix: data,
251
- subfix: "",
252
- str: "",
258
+ async uploadImage(file) {
259
+ const formData = new FormData();
260
+ formData.append("file", file);
261
+
262
+ const response = await axios.post(UPLOAD_API, formData, {
263
+ headers: {
264
+ "Content-Type": "multipart/form-data",
265
+ },
266
+ withCredentials: true,
267
+ auth: {
268
+ username: (localStorage && localStorage.getItem("token")) || "",
269
+ password: "cms common request",
270
+ },
253
271
  });
272
+
273
+ const payload = response.data || {};
274
+ if (payload.code) {
275
+ throw new Error(payload.msg || payload.message || "上传失败");
276
+ }
277
+
278
+ const url =
279
+ payload.location ||
280
+ payload.url ||
281
+ (payload.data &&
282
+ (Array.isArray(payload.data)
283
+ ? payload.data[0]
284
+ : payload.data.url || payload.data.location || payload.data));
285
+
286
+ if (!url) {
287
+ throw new Error("上传成功但未返回图片地址");
288
+ }
289
+
290
+ return url;
254
291
  },
255
- insertTalent2(data) {
256
- this.$md.insertText(this.$md.getTextareaDom(), {
257
- prefix: data,
258
- subfix: "",
259
- str: "",
260
- });
292
+ async handleImageUpload(files) {
293
+ if (this.isUploadingImage) return "图片上传中,请稍候";
294
+
295
+ const imageFiles = Array.from(files || []).filter(Boolean);
296
+ if (!imageFiles.length) return "未检测到可上传的图片";
297
+
298
+ for (const file of imageFiles) {
299
+ const message = this.validateImageFile(file);
300
+ if (message) return message;
301
+ }
302
+
303
+ this.isUploadingImage = true;
304
+
305
+ try {
306
+ const markdownList = [];
307
+ for (const file of imageFiles) {
308
+ const url = await this.uploadImage(file);
309
+ const alt = file.name || "image";
310
+ markdownList.push(`![${alt}](${url})`);
311
+ }
312
+
313
+ if (markdownList.length) {
314
+ this.editor?.insertMD(markdownList.join("\n"));
315
+ }
316
+ } catch (error) {
317
+ return error?.message || "图片上传失败";
318
+ } finally {
319
+ this.isUploadingImage = false;
320
+ }
261
321
  },
262
- insertVideo(data) {
263
- this.$md.insertText(this.$md.getTextareaDom(), {
264
- prefix: data,
265
- subfix: "",
266
- str: "",
267
- });
268
- }
269
322
  },
270
- filters: {},
271
- created: function() {},
272
- mounted: function() {},
273
323
  };
274
324
  </script>
275
325
 
276
326
  <style lang="less">
277
- @import "../assets/css/markdown.less";
327
+ .c-editor-markdown {
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: 12px;
331
+
332
+ &__toolbar {
333
+ display: flex;
334
+ align-items: center;
335
+ justify-content: space-between;
336
+ gap: 12px;
337
+ flex-wrap: wrap;
338
+ }
339
+
340
+ &__toolbar-left {
341
+ display: flex;
342
+ align-items: center;
343
+ gap: 12px;
344
+ flex-wrap: wrap;
345
+ }
346
+
347
+ &__toolbar-label,
348
+ &__toolbar-tip {
349
+ font-size: 13px;
350
+ color: #57606a;
351
+ line-height: 1.4;
352
+ }
353
+
354
+ &__host {
355
+ overflow: visible;
356
+ border: 1px solid #dcdfe6;
357
+ border-radius: 4px;
358
+ background-color: #ffffff;
359
+ }
360
+
361
+ .el-radio-group {
362
+ box-shadow: none;
363
+ }
364
+
365
+ .vditor {
366
+ border: 0;
367
+ background: #ffffff;
368
+ overflow: visible;
369
+ }
370
+
371
+ .vditor-toolbar {
372
+ padding: 10px 12px;
373
+ background: #f6f8fa;
374
+ border-bottom: 1px solid #d8dee4;
375
+ overflow: visible;
376
+ }
377
+
378
+ .vditor-reset {
379
+ min-height: 640px;
380
+ }
381
+
382
+ .vditor-reset.markdown-body {
383
+ box-sizing: border-box;
384
+ width: 100%;
385
+ max-width: none;
386
+ min-height: 100%;
387
+ padding: 24px 32px;
388
+ background: #ffffff;
389
+ }
390
+
391
+ .vditor-sv__editor,
392
+ .vditor-ir__marker,
393
+ .vditor-wysiwyg {
394
+ padding: 24px 32px;
395
+ }
396
+
397
+ .vditor-wysiwyg > .vditor-reset > :first-child,
398
+ .vditor-ir > .vditor-reset > :first-child {
399
+ margin-top: 0;
400
+ }
401
+
402
+ .vditor-counter {
403
+ padding: 8px 12px;
404
+ border-top: 1px solid #d8dee4;
405
+ background: #f6f8fa;
406
+ }
407
+
408
+ .vditor-preview__action {
409
+ display: none;
410
+ }
411
+
412
+ .vditor-tooltipped::before,
413
+ .vditor-tooltipped::after {
414
+ z-index: 30;
415
+ }
416
+
417
+ @media (max-width: 768px) {
418
+ &__toolbar {
419
+ align-items: flex-start;
420
+ }
421
+
422
+ .vditor-reset.markdown-body,
423
+ .vditor-sv__editor,
424
+ .vditor-ir__marker,
425
+ .vditor-wysiwyg {
426
+ padding: 16px;
427
+ }
428
+ }
429
+ }
278
430
  </style>