@jx3box/jx3box-editor 3.0.1 → 3.0.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jx3box/jx3box-editor",
3
- "version": "3.0.1",
3
+ "version": "3.0.3",
4
4
  "description": "JX3BOX Article & Editor",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -32,6 +32,7 @@
32
32
  "js-base64": "^3.6.1",
33
33
  "katex": "^0.16.4",
34
34
  "lodash": "^4.17.21",
35
+ "lucide": "^0.577.0",
35
36
  "photoswipe": "^4.1.2",
36
37
  "sanitize-html": "^2.17.0",
37
38
  "sortablejs": "^1.15.0",
package/src/Article.vue CHANGED
@@ -8,12 +8,19 @@
8
8
  v-for="(text, i) in data"
9
9
  :key="i"
10
10
  v-html="text"
11
- :class="{ on: i == page - 1 || all == true }"
11
+ :class="{ on: i == page - 1 || all == true, 'markdown-body': isMarkdownMode }"
12
12
  :id="'c-article-part' + ~~(i + 1)"
13
13
  ></div>
14
14
  </div>
15
15
 
16
- <div id="c-article" class="c-article" ref="article" v-else-if="data && data.length" v-html="data[0]"></div>
16
+ <div
17
+ id="c-article"
18
+ class="c-article"
19
+ ref="article"
20
+ v-else-if="data && data.length"
21
+ v-html="data[0]"
22
+ :class="{ 'markdown-body': isMarkdownMode }"
23
+ ></div>
17
24
 
18
25
  <el-button class="c-article-all" type="primary" v-if="!all && hasPages" @click="showAll">加载全部</el-button>
19
26
 
@@ -48,6 +55,8 @@
48
55
 
49
56
  <script>
50
57
  import $ from "jquery";
58
+ import Vditor from "vditor";
59
+ import "github-markdown-css/github-markdown-light.css";
51
60
 
52
61
  // XSS
53
62
  import execFilterXSS from "./assets/js/xss";
@@ -85,8 +94,7 @@ import renderJx3Element from "./assets/js/jx3_element";
85
94
  export default {
86
95
  name: "Article",
87
96
  props: {
88
-
89
- post_mode : {
97
+ post_mode: {
90
98
  type: String,
91
99
  default: "tinymce",
92
100
  },
@@ -134,6 +142,7 @@ export default {
134
142
  page: 1,
135
143
  data: [],
136
144
  mode: "",
145
+ renderVersion: 0,
137
146
 
138
147
  // 画廊
139
148
  gallery_index: null,
@@ -180,14 +189,17 @@ export default {
180
189
  },
181
190
  computed: {
182
191
  total: function () {
183
- return this.chunks.length;
192
+ return this.data.length;
184
193
  },
185
194
  hasPages: function () {
186
- return this.chunks.length > 1;
195
+ return this.data.length > 1;
187
196
  },
188
197
  origin: function () {
189
198
  return this.content;
190
199
  },
200
+ isMarkdownMode: function () {
201
+ return ["markdown", "md", "vditor"].includes(String(this.post_mode || "").toLowerCase());
202
+ },
191
203
  chunks: function () {
192
204
  return this.pageable ? execSplitPages(this.origin) : [this.origin];
193
205
  },
@@ -195,9 +207,7 @@ export default {
195
207
  methods: {
196
208
  getHeaderHeight: function () {
197
209
  // 页面上可能没有这些元素,取存在的第一个:.c-header 优先,其次 .c-breadcrumb
198
- const el =
199
- document.querySelector(".c-header") ||
200
- document.querySelector(".c-breadcrumb");
210
+ const el = document.querySelector(".c-header") || document.querySelector(".c-breadcrumb");
201
211
  if (!el) return 0;
202
212
  const rect = el.getBoundingClientRect && el.getBoundingClientRect();
203
213
  const h = (rect && rect.height) || el.offsetHeight || 0;
@@ -232,6 +242,31 @@ export default {
232
242
  return "";
233
243
  }
234
244
  },
245
+ renderMarkdownChunk: async function (chunk) {
246
+ const temp = document.createElement("div");
247
+
248
+ await Vditor.preview(temp, chunk || "", {
249
+ mode: "light",
250
+ lang: "zh_CN",
251
+ hljs: {
252
+ enable: true,
253
+ lineNumber: false,
254
+ style: "github",
255
+ },
256
+ markdown: {
257
+ sanitize: true,
258
+ },
259
+ icon: "ant",
260
+ });
261
+
262
+ return temp.innerHTML;
263
+ },
264
+ renderMarkdown: async function () {
265
+ const html = await this.renderMarkdownChunk(this.origin);
266
+ const chunks = this.pageable ? execSplitPages(html) : [html];
267
+
268
+ return chunks.map((chunk) => this.doReg(chunk));
269
+ },
235
270
  doDOM: function ($root) {
236
271
  // 折叠块
237
272
  renderFoldBlock($root);
@@ -305,16 +340,27 @@ export default {
305
340
  this.doDir();
306
341
  });
307
342
  },
308
- render: function () {
343
+ render: async function () {
344
+ const version = ++this.renderVersion;
309
345
  let result = [];
310
- for (let chunk of this.chunks) {
311
- let _chunk = this.doReg(chunk);
312
- result.push(_chunk);
346
+
347
+ if (this.isMarkdownMode) {
348
+ result = await this.renderMarkdown();
349
+ } else {
350
+ for (let chunk of this.chunks) {
351
+ let _chunk = this.doReg(chunk);
352
+ result.push(_chunk);
353
+ }
313
354
  }
355
+
356
+ if (version !== this.renderVersion) return;
314
357
  this.data = result;
315
358
  },
316
- run: function () {
317
- this.render();
359
+ run: async function () {
360
+ this.page = 1;
361
+ this.all = false;
362
+ await this.render();
363
+ if (!this.data.length) return;
318
364
 
319
365
  // 等待html加载完毕后
320
366
  this.$nextTick(() => {
@@ -334,6 +380,9 @@ export default {
334
380
  content: function () {
335
381
  this.run();
336
382
  },
383
+ post_mode: function () {
384
+ this.run();
385
+ },
337
386
  },
338
387
  mounted: function () {
339
388
  const params = new URLSearchParams(location.search);
package/src/Markdown.vue CHANGED
@@ -1,19 +1,7 @@
1
1
  <template>
2
- <div class="c-editor-markdown">
2
+ <div class="c-editor-markdown" :class="{ 'c-editor-markdown--preview': previewVisible }">
3
3
  <slot name="prepend"></slot>
4
4
 
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>
15
- </div>
16
-
17
5
  <slot></slot>
18
6
 
19
7
  <div ref="editorHost" class="c-editor-markdown__host"></div>
@@ -25,17 +13,81 @@
25
13
  <script>
26
14
  import axios from "axios";
27
15
  import JX3BOX from "@jx3box/jx3box-common/data/jx3box.json";
16
+ import {
17
+ Bold,
18
+ Code2,
19
+ Expand,
20
+ Eye,
21
+ EyeOff,
22
+ Heading,
23
+ Italic,
24
+ Link2,
25
+ List,
26
+ ListChecks,
27
+ ListOrdered,
28
+ Quote,
29
+ Redo2,
30
+ SquareCode,
31
+ Strikethrough,
32
+ TableProperties,
33
+ Undo2,
34
+ } from "lucide";
28
35
  import Vditor from "vditor";
29
36
  import "vditor/dist/index.css";
30
- import "github-markdown-css/github-markdown.css";
37
+ import "github-markdown-css/github-markdown-light.css";
31
38
 
32
39
  const { __cms } = JX3BOX;
33
40
  const UPLOAD_API = `${__cms}api/cms/upload`;
34
- const MODE_OPTIONS = [
35
- { label: "所见即所得", value: "wysiwyg" },
36
- { label: "即时渲染", value: "ir" },
37
- { label: "分屏预览", value: "sv" },
38
- ];
41
+ const TOOLBAR_ICON_SIZE = 16;
42
+ const STATIC_TOOLBAR_ICONS = {
43
+ headings: Heading,
44
+ bold: Bold,
45
+ italic: Italic,
46
+ strike: Strikethrough,
47
+ link: Link2,
48
+ list: List,
49
+ "ordered-list": ListOrdered,
50
+ check: ListChecks,
51
+ quote: Quote,
52
+ code: Code2,
53
+ "inline-code": SquareCode,
54
+ table: TableProperties,
55
+ undo: Undo2,
56
+ redo: Redo2,
57
+ fullscreen: Expand,
58
+ };
59
+
60
+ function renderLucideIcon(iconNode) {
61
+ if (!iconNode) return "";
62
+
63
+ const attrs = {
64
+ xmlns: "http://www.w3.org/2000/svg",
65
+ width: TOOLBAR_ICON_SIZE,
66
+ height: TOOLBAR_ICON_SIZE,
67
+ viewBox: "0 0 24 24",
68
+ fill: "none",
69
+ stroke: "currentColor",
70
+ "stroke-width": "2",
71
+ "stroke-linecap": "round",
72
+ "stroke-linejoin": "round",
73
+ class: "c-editor-markdown__icon",
74
+ "aria-hidden": "true",
75
+ };
76
+ const svgAttrs = Object.entries(attrs)
77
+ .map(([key, value]) => `${key}="${value}"`)
78
+ .join(" ");
79
+ const content = iconNode
80
+ .map(([tag, tagAttrs]) => {
81
+ const elementAttrs = Object.entries(tagAttrs)
82
+ .map(([key, value]) => `${key}="${value}"`)
83
+ .join(" ");
84
+
85
+ return `<${tag} ${elementAttrs}></${tag}>`;
86
+ })
87
+ .join("");
88
+
89
+ return `<svg ${svgAttrs}>${content}</svg>`;
90
+ }
39
91
 
40
92
  export default {
41
93
  name: "Markdown",
@@ -74,10 +126,14 @@ export default {
74
126
  data: this.modelValue ?? this.content ?? "",
75
127
  editor: null,
76
128
  editorReady: false,
77
- editorMode: "ir",
78
- modeOptions: MODE_OPTIONS,
129
+ editorMode: "sv",
130
+ previewVisible: false,
79
131
  isRebuildingEditor: false,
80
132
  isUploadingImage: false,
133
+ counterElement: null,
134
+ counterClickListener: null,
135
+ counterClickTimestamps: [],
136
+ pendingTip: "",
81
137
  };
82
138
  },
83
139
  watch: {
@@ -105,13 +161,6 @@ export default {
105
161
  this.applyEditableState();
106
162
  },
107
163
  },
108
- editorMode(nextMode, prevMode) {
109
- if (!this.editorReady || this.isRebuildingEditor || nextMode === prevMode) {
110
- return;
111
- }
112
-
113
- this.rebuildEditor(nextMode);
114
- },
115
164
  },
116
165
  mounted() {
117
166
  this.initEditor();
@@ -120,11 +169,28 @@ export default {
120
169
  this.destroyEditor();
121
170
  },
122
171
  methods: {
123
- getPreviewMode(mode) {
124
- return mode === "sv" ? "both" : "editor";
172
+ getPreviewMode() {
173
+ return this.previewVisible ? "both" : "editor";
174
+ },
175
+ getPreviewTip() {
176
+ return this.previewVisible ? "返回编辑" : "查看预览";
177
+ },
178
+ getToolbarIconMap() {
179
+ return {
180
+ ...STATIC_TOOLBAR_ICONS,
181
+ "toggle-preview": this.previewVisible ? EyeOff : Eye,
182
+ };
125
183
  },
126
184
  getToolbar() {
127
185
  return [
186
+ {
187
+ name: "toggle-preview",
188
+ icon: renderLucideIcon(Eye),
189
+ tip: this.getPreviewTip(),
190
+ tipPosition: "ne",
191
+ click: () => this.togglePreview(),
192
+ },
193
+ "|",
128
194
  "headings",
129
195
  "bold",
130
196
  "italic",
@@ -171,7 +237,7 @@ export default {
171
237
  markdown: {
172
238
  sanitize: true,
173
239
  },
174
- mode: this.getPreviewMode(this.editorMode),
240
+ mode: this.getPreviewMode(),
175
241
  },
176
242
  customWysiwygToolbar() {},
177
243
  toolbar: this.getToolbar(),
@@ -190,6 +256,10 @@ export default {
190
256
  this.editorReady = true;
191
257
  this.syncEditorValue(this.data);
192
258
  this.applyEditableState();
259
+ this.applyPreviewState();
260
+ this.syncToolbarState();
261
+ this.bindCounterShortcut();
262
+ this.showPendingTip();
193
263
  },
194
264
  input: (value) => {
195
265
  this.handleEditorInput(value);
@@ -204,6 +274,7 @@ export default {
204
274
  this.editor = new Vditor(host, this.buildEditorOptions(initialValue));
205
275
  },
206
276
  destroyEditor() {
277
+ this.unbindCounterShortcut();
207
278
  if (!this.editor) return;
208
279
 
209
280
  this.editor.destroy();
@@ -212,9 +283,11 @@ export default {
212
283
  },
213
284
  rebuildEditor() {
214
285
  const currentValue = this.editor?.getValue?.() ?? this.data ?? "";
286
+
215
287
  this.data = currentValue;
216
288
  this.isRebuildingEditor = true;
217
289
  this.destroyEditor();
290
+
218
291
  this.$nextTick(() => {
219
292
  this.initEditor(currentValue);
220
293
  this.isRebuildingEditor = false;
@@ -226,6 +299,112 @@ export default {
226
299
  const nextValue = value ?? "";
227
300
  if (this.editor.getValue() === nextValue) return;
228
301
  this.editor.setValue(nextValue);
302
+ if (this.previewVisible) {
303
+ this.editor.renderPreview();
304
+ }
305
+ },
306
+ getToolbarButton(name) {
307
+ return this.$refs.editorHost?.querySelector?.(`.vditor-toolbar button[data-type="${name}"]`) || null;
308
+ },
309
+ syncToolbarState() {
310
+ const iconMap = this.getToolbarIconMap();
311
+
312
+ Object.entries(iconMap).forEach(([name, iconNode]) => {
313
+ const button = this.getToolbarButton(name);
314
+ if (!button) return;
315
+
316
+ button.innerHTML = renderLucideIcon(iconNode);
317
+ });
318
+
319
+ const previewButton = this.getToolbarButton("toggle-preview");
320
+ if (previewButton) {
321
+ previewButton.setAttribute("aria-label", this.getPreviewTip());
322
+ previewButton.classList.toggle("vditor-menu--current", this.previewVisible);
323
+ }
324
+
325
+ this.$refs.editorHost?.querySelectorAll?.(".vditor-toolbar button[data-type]").forEach((button) => {
326
+ const isVisibleInPreview = ["toggle-preview", "fullscreen"].includes(button.dataset.type || "");
327
+
328
+ button.parentElement.style.display = this.previewVisible && !isVisibleInPreview ? "none" : "";
329
+ });
330
+ this.$refs.editorHost?.querySelectorAll?.(".vditor-toolbar .vditor-toolbar__divider").forEach((divider) => {
331
+ divider.style.display = this.previewVisible ? "none" : "";
332
+ });
333
+ },
334
+ syncEditorPanels() {
335
+ const host = this.$refs.editorHost;
336
+ const preview = host?.querySelector?.(".vditor-preview");
337
+ const sv = host?.querySelector?.(".vditor-sv");
338
+ const irWrapper = host?.querySelector?.(".vditor-ir");
339
+ const wysiwygWrapper = host?.querySelector?.(".vditor-wysiwyg");
340
+
341
+ if (preview) {
342
+ preview.style.display = this.previewVisible ? "block" : "none";
343
+ }
344
+ if (sv) {
345
+ sv.style.display = this.editorMode === "sv" && !this.previewVisible ? "block" : "none";
346
+ }
347
+ if (irWrapper) {
348
+ irWrapper.style.display = "none";
349
+ }
350
+ if (wysiwygWrapper) {
351
+ wysiwygWrapper.style.display = this.editorMode === "wysiwyg" && !this.previewVisible ? "block" : "none";
352
+ }
353
+ },
354
+ applyPreviewState() {
355
+ if (!this.editorReady || !this.editor) return;
356
+
357
+ this.syncEditorPanels();
358
+ if (this.previewVisible) {
359
+ this.editor.renderPreview();
360
+ }
361
+ },
362
+ togglePreview() {
363
+ this.previewVisible = !this.previewVisible;
364
+
365
+ if (!this.editorReady || !this.editor) return;
366
+
367
+ this.applyPreviewState();
368
+ this.syncToolbarState();
369
+ },
370
+ toggleSecretEditorMode() {
371
+ if (this.isRebuildingEditor) return;
372
+
373
+ this.editorMode = this.editorMode === "wysiwyg" ? "sv" : "wysiwyg";
374
+ this.pendingTip = this.editorMode === "wysiwyg" ? "已切换到所见即所得模式" : "已切换到 Markdown 源码模式";
375
+ this.rebuildEditor();
376
+ },
377
+ bindCounterShortcut() {
378
+ const counter = this.$refs.editorHost?.querySelector?.(".vditor-counter");
379
+ if (!counter || counter === this.counterElement) return;
380
+
381
+ this.unbindCounterShortcut();
382
+ this.counterClickListener = this.counterClickListener || (() => this.handleCounterClick());
383
+ this.counterElement = counter;
384
+ this.counterElement.addEventListener("click", this.counterClickListener);
385
+ },
386
+ unbindCounterShortcut() {
387
+ if (!this.counterElement) return;
388
+
389
+ this.counterElement.removeEventListener("click", this.counterClickListener);
390
+ this.counterElement = null;
391
+ },
392
+ handleCounterClick() {
393
+ const now = Date.now();
394
+
395
+ this.counterClickTimestamps = this.counterClickTimestamps.filter((time) => now - time <= 2000);
396
+ this.counterClickTimestamps.push(now);
397
+
398
+ if (this.counterClickTimestamps.length < 6) return;
399
+
400
+ this.counterClickTimestamps = [];
401
+ this.toggleSecretEditorMode();
402
+ },
403
+ showPendingTip() {
404
+ if (!this.pendingTip || !this.editor?.tip) return;
405
+
406
+ this.editor.tip(this.pendingTip, 1500);
407
+ this.pendingTip = "";
229
408
  },
230
409
  applyEditableState() {
231
410
  if (!this.editorReady || !this.editor) return;
@@ -329,28 +508,6 @@ export default {
329
508
  flex-direction: column;
330
509
  gap: 12px;
331
510
 
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
511
  &__host {
355
512
  overflow: visible;
356
513
  border: 1px solid #dcdfe6;
@@ -358,8 +515,10 @@ export default {
358
515
  background-color: #ffffff;
359
516
  }
360
517
 
361
- .el-radio-group {
362
- box-shadow: none;
518
+ &__icon {
519
+ display: block;
520
+ width: 16px;
521
+ height: 16px;
363
522
  }
364
523
 
365
524
  .vditor {
@@ -369,10 +528,74 @@ export default {
369
528
  }
370
529
 
371
530
  .vditor-toolbar {
531
+ display: flex;
532
+ align-items: center;
372
533
  padding: 10px 12px;
373
534
  background: #f6f8fa;
374
535
  border-bottom: 1px solid #d8dee4;
375
536
  overflow: visible;
537
+
538
+ .vditor-toolbar__item {
539
+ margin-right: 2px;
540
+ }
541
+
542
+ .vditor-toolbar__divider {
543
+ margin: 0 4px;
544
+ }
545
+
546
+ .vditor-toolbar__item > .vditor-tooltipped {
547
+ display: inline-flex;
548
+ align-items: center;
549
+ justify-content: center;
550
+ width: 30px;
551
+ height: 30px;
552
+ padding: 0;
553
+ color: #4b5563;
554
+ }
555
+
556
+ .vditor-toolbar__item > .vditor-tooltipped svg {
557
+ fill: none !important;
558
+ stroke: currentColor !important;
559
+ stroke-width: 2 !important;
560
+ stroke-linecap: round;
561
+ stroke-linejoin: round;
562
+ width: 16px;
563
+ height: 16px;
564
+ overflow: visible;
565
+ }
566
+
567
+ .vditor-toolbar__item > .vditor-tooltipped svg * {
568
+ fill: none !important;
569
+ stroke: currentColor !important;
570
+ stroke-width: 2 !important;
571
+ stroke-linecap: round;
572
+ stroke-linejoin: round;
573
+ }
574
+
575
+ .vditor-panel button {
576
+ width: auto;
577
+ height: auto;
578
+ padding: 4px 8px;
579
+ color: inherit;
580
+ display: block;
581
+ line-height: 1.5;
582
+ text-align: left;
583
+ white-space: nowrap;
584
+ }
585
+ }
586
+
587
+ &--preview {
588
+ .vditor-sv,
589
+ .vditor-ir,
590
+ .vditor-wysiwyg,
591
+ .vditor-counter {
592
+ display: none !important;
593
+ }
594
+
595
+ .vditor-preview {
596
+ display: block !important;
597
+ width: 100%;
598
+ }
376
599
  }
377
600
 
378
601
  .vditor-reset {
@@ -400,9 +623,20 @@ export default {
400
623
  }
401
624
 
402
625
  .vditor-counter {
403
- padding: 8px 12px;
404
- border-top: 1px solid #d8dee4;
405
- background: #f6f8fa;
626
+ padding: 3px 8px;
627
+ border-top: 0;
628
+ background: rgba(27, 31, 35, 0.05);
629
+ border-radius: 3px;
630
+ line-height: 1.4;
631
+ margin: 0;
632
+ position: absolute;
633
+ right: 12px;
634
+
635
+ &::before,
636
+ &::after {
637
+ display: none !important;
638
+ content: none !important;
639
+ }
406
640
  }
407
641
 
408
642
  .vditor-preview__action {
@@ -414,11 +648,12 @@ export default {
414
648
  z-index: 30;
415
649
  }
416
650
 
417
- @media (max-width: 768px) {
418
- &__toolbar {
419
- align-items: flex-start;
420
- }
651
+ .vditor-menu--current {
652
+ color: #0969da;
653
+ background: #eaf2ff;
654
+ }
421
655
 
656
+ @media (max-width: 768px) {
422
657
  .vditor-reset.markdown-body,
423
658
  .vditor-sv__editor,
424
659
  .vditor-ir__marker,
package/src/Upload.vue CHANGED
@@ -90,6 +90,7 @@ import { showImgPreview } from "./assets/js/renderImgPreview";
90
90
  const { __cms } = JX3BOX;
91
91
  const API = __cms + "api/cms/upload";
92
92
  const imgtypes = ["jpg", "png", "gif", "bmp", "webp", "jpeg", "svg"];
93
+ const DEFAULT_ACCEPT = `${allow_types.accept},image/svg+xml,.svg`;
93
94
 
94
95
  export default {
95
96
  name: "Upload",
@@ -111,7 +112,7 @@ export default {
111
112
  },
112
113
  accept: {
113
114
  type: String,
114
- default: allow_types.accept,
115
+ default: DEFAULT_ACCEPT,
115
116
  },
116
117
  enable: {
117
118
  type: Boolean,
@@ -133,13 +133,14 @@
133
133
  .y(-3px);
134
134
  }
135
135
  }
136
- .u-medals {
136
+ .m-medals {
137
137
  padding: 0 2px;
138
138
  line-height: unset;
139
139
 
140
140
  .m-medal {
141
141
  line-height: normal;
142
142
  flex-wrap: wrap;
143
+ .flex;
143
144
  }
144
145
  }
145
146
 
@@ -69,12 +69,12 @@
69
69
  display:flex;
70
70
  justify-content: space-between;
71
71
  .c-article-directory-title-label {
72
- font-weight: 300;
73
- font-size: 18px;
72
+ font-weight: 500;
73
+ font-size: 15px;
74
74
  line-height: 18px;
75
75
  display: inline-flex;
76
76
  align-items: center;
77
- gap:3px;
77
+ gap:5px;
78
78
  .u-icon{
79
79
  font-size: 18px;
80
80
  }
@@ -86,6 +86,8 @@
86
86
  line-height: 18px;
87
87
  color: darken(#dcdfe6, 5%);
88
88
  cursor: pointer;
89
+ .flex;
90
+ align-items: center;
89
91
  &:hover {
90
92
  color: darken(#dcdfe6, 20%);
91
93
  }
@@ -19,7 +19,7 @@
19
19
  transition: border-color 0.2s ease, background-color 0.2s ease;
20
20
 
21
21
  &.is-dragover {
22
- border-color: #409eff;
22
+ border-color: var(--el-color-primary);
23
23
  background: #f0f9ff;
24
24
  }
25
25
 
@@ -120,6 +120,7 @@
120
120
  gap: 6px;
121
121
  padding: 0 16px;
122
122
  background: #fafafa;
123
+ font-size: 14px;
123
124
 
124
125
  &.is-empty {
125
126
  width: 100%;
@@ -133,7 +134,8 @@
133
134
  }
134
135
 
135
136
  .u-drop-link {
136
- color: #409eff;
137
+ color: var(--el-color-primary);
138
+ font-weight: 500;
137
139
  }
138
140
  }
139
141
 
@@ -157,8 +159,8 @@
157
159
  }
158
160
 
159
161
  &:hover {
160
- border-color: #409eff;
161
- color: #409eff;
162
+ border-color: var(--el-color-primary);
163
+ color: var(--el-color-primary);
162
164
  }
163
165
  }
164
166
  }
@@ -7,12 +7,28 @@ const FORBID = new Set(["script", "object", "embed", "applet", "base", "meta", "
7
7
 
8
8
  const EXTRA_TAGS = [
9
9
  "img",
10
- "h1", "h2", "h3", "h4", "h5", "h6",
11
- "table", "thead", "tbody", "tr", "th", "td",
12
- "blockquote", "pre", "code", "hr",
13
- "video", "source",
14
- "iframe", "style",
15
- "colgroup", "col",
10
+ "h1",
11
+ "h2",
12
+ "h3",
13
+ "h4",
14
+ "h5",
15
+ "h6",
16
+ "table",
17
+ "thead",
18
+ "tbody",
19
+ "tr",
20
+ "th",
21
+ "td",
22
+ "blockquote",
23
+ "pre",
24
+ "code",
25
+ "hr",
26
+ "video",
27
+ "source",
28
+ "iframe",
29
+ "style",
30
+ "colgroup",
31
+ "col",
16
32
  ];
17
33
 
18
34
  // 必须顶层的 at-rule(你说不需要动画,但 keyframes 也可能被编辑器/作者写进来,留着更稳)
@@ -116,6 +132,7 @@ export default function sanitizeRichText(html) {
116
132
  .filter((t) => !FORBID.has(t));
117
133
 
118
134
  return sanitizeHtml(html, {
135
+ allowVulnerableTags: true,
119
136
  disallowedTagsMode: "discard",
120
137
  allowedTags,
121
138
 
@@ -125,7 +142,18 @@ export default function sanitizeRichText(html) {
125
142
  img: ["src", "alt", "title", "width", "height", "class", "style", "loading", "decoding"],
126
143
  video: ["controls", "width", "height", "class", "style"],
127
144
  source: ["src", "type"],
128
- iframe: ["src", "width", "height", "frameborder", "scrolling", "allowfullscreen", "sandbox", "referrerpolicy", "class", "style"],
145
+ iframe: [
146
+ "src",
147
+ "width",
148
+ "height",
149
+ "frameborder",
150
+ "scrolling",
151
+ "allowfullscreen",
152
+ "sandbox",
153
+ "referrerpolicy",
154
+ "class",
155
+ "style",
156
+ ],
129
157
  td: ["colspan", "rowspan", "align", "valign", "class", "style"],
130
158
  th: ["colspan", "rowspan", "align", "valign", "class", "style"],
131
159
  col: ["span", "width", "class", "style"],
@@ -43,7 +43,7 @@
43
43
  </div>
44
44
  <!-- <div class="u-honor" :style="honorStyle" v-if="honor">{{ honor }}</div> -->
45
45
  <div class="u-trophy" v-if="hasTrophy">
46
- <div class="u-medals" v-if="medals && medals.length">
46
+ <div class="m-medals" v-if="medals && medals.length">
47
47
  <div class="m-medal">
48
48
  <a
49
49
  v-for="item in medals"
@@ -4,7 +4,7 @@
4
4
  <el-segmented v-model="group" :options="groupOptions" />
5
5
  </div>
6
6
  <div class="container">
7
- <Article class="article" :content="content"></Article>
7
+ <Article class="article" :content="content" :post_mode="postMode"></Article>
8
8
  <div id="directory"></div>
9
9
  </div>
10
10
  </template>
@@ -29,6 +29,11 @@ export default {
29
29
  ],
30
30
  };
31
31
  },
32
+ computed: {
33
+ postMode() {
34
+ return this.group === "markdown" ? "markdown" : "tinymce";
35
+ },
36
+ },
32
37
  watch: {
33
38
  group: {
34
39
  immediate: true,
@@ -53,20 +58,19 @@ export default {
53
58
  .flex(x);
54
59
  margin-bottom: 1rem;
55
60
  }
56
- .container{
57
- #directory{
61
+ .container {
62
+ #directory {
58
63
  .w(200px);
59
64
  position: fixed;
60
- right:20px;
61
- top:20px;
65
+ right: 20px;
66
+ top: 20px;
62
67
  }
63
- .article{
68
+ .article {
64
69
  margin-right: 320px;
65
70
  }
66
-
67
71
  }
68
- body{
69
- min-height:2000px;
70
- padding:0 20px;
72
+ body {
73
+ min-height: 2000px;
74
+ padding: 0 20px;
71
75
  }
72
76
  </style>