|
@@ -1,5 +1,5 @@
|
|
|
<template>
|
|
|
- <div class="rich-wrapper">
|
|
|
+ <div ref="richArea" class="rich-wrapper">
|
|
|
<Editor
|
|
|
v-bind="$attrs"
|
|
|
:id="id"
|
|
@@ -12,13 +12,18 @@
|
|
|
@onBlur="handleRichTextBlur"
|
|
|
/>
|
|
|
<div v-show="isShow" :style="contentmenu" class="contentmenu">
|
|
|
- <SvgIcon icon-class="slice" size="16" @click="setFill" />
|
|
|
- <span class="button" @click="setFill">设为填空</span>
|
|
|
- <span class="line"></span>
|
|
|
- <SvgIcon icon-class="close-circle" size="16" @click="deleteFill" />
|
|
|
- <span class="button" @click="deleteFill">删除填空</span>
|
|
|
+ <div v-if="isViewNote" @click="openExplanatoryNoteDialog">
|
|
|
+ <SvgIcon icon-class="mark" size="14" />
|
|
|
+ <span class="button"> 编辑注释</span>
|
|
|
+ </div>
|
|
|
+ <div v-else>
|
|
|
+ <SvgIcon icon-class="slice" size="16" @click="setFill" />
|
|
|
+ <span class="button" @click="setFill">设为填空</span>
|
|
|
+ <span class="line"></span>
|
|
|
+ <SvgIcon icon-class="close-circle" size="16" @click="deleteFill" />
|
|
|
+ <span class="button" @click="deleteFill">删除填空</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
-
|
|
|
<MathDialog :visible.sync="isViewMathDialog" @confirm="mathConfirm" />
|
|
|
</div>
|
|
|
</template>
|
|
@@ -112,6 +117,10 @@ export default {
|
|
|
type: String,
|
|
|
default: '',
|
|
|
},
|
|
|
+ isViewNote: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
@@ -151,12 +160,13 @@ export default {
|
|
|
statusbar: false, // 状态栏
|
|
|
setup: (editor) => {
|
|
|
let isRendered = false; // 标记是否已渲染
|
|
|
+ let that = this;
|
|
|
editor.on('init', () => {
|
|
|
editor.getBody().style.fontSize = `${this.font_size}pt`; // 设置默认字体大小
|
|
|
editor.getBody().style.fontFamily = 'Arial'; // 设置默认字体
|
|
|
});
|
|
|
|
|
|
- editor.on('click', () => {
|
|
|
+ editor.on('click', (e) => {
|
|
|
if (editor?.queryCommandState('ToggleToolbarDrawer')) {
|
|
|
editor.execCommand('ToggleToolbarDrawer');
|
|
|
}
|
|
@@ -164,6 +174,12 @@ export default {
|
|
|
isRendered = true;
|
|
|
window.MathJax.typesetPromise([editor.getBody()]);
|
|
|
}
|
|
|
+ if (e.target.classList.contains('rich-fill')) {
|
|
|
+ const noteId = e.target.getAttribute('data-annotation-id');
|
|
|
+ that.$emit('selectNote', noteId);
|
|
|
+ } else {
|
|
|
+ that.$emit('selectNote', '');
|
|
|
+ }
|
|
|
});
|
|
|
|
|
|
// 添加 MathJax 按钮
|
|
@@ -177,6 +193,35 @@ export default {
|
|
|
|
|
|
// 内容变化时重新渲染公式
|
|
|
editor.on('change', () => this.renderMath());
|
|
|
+
|
|
|
+ editor.on('KeyDown', function (e) {
|
|
|
+ // 检测删除或退格键
|
|
|
+ if (e.keyCode === 8 || e.keyCode === 46) {
|
|
|
+ // 延迟执行以确保删除已完成
|
|
|
+ setTimeout(function () {
|
|
|
+ that.cleanupRemovedAnnotations(editor);
|
|
|
+ }, 500);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 也可以监听剪切操作
|
|
|
+ editor.on('Cut', function () {
|
|
|
+ setTimeout(function () {
|
|
|
+ that.cleanupRemovedAnnotations(editor);
|
|
|
+ }, 500);
|
|
|
+ });
|
|
|
+ // editor.on('NodeChange', function (e) {
|
|
|
+ // if (
|
|
|
+ // e.element &&
|
|
|
+ // e.element.tagName === 'SPAN' &&
|
|
|
+ // e.element.hasAttribute('data-annotation-id') &&
|
|
|
+ // (!e.element.textContent || /^\s*$/.test(e.element.textContent))
|
|
|
+ // ) {
|
|
|
+ // const annotationId = e.element.getAttribute('data-annotation-id');
|
|
|
+ // e.element.parentNode.removeChild(e.element);
|
|
|
+ // that.$emit('selectContentSetMemo', null, annotationId);
|
|
|
+ // }
|
|
|
+ // });
|
|
|
},
|
|
|
font_formats:
|
|
|
'楷体=楷体,微软雅黑;' +
|
|
@@ -195,7 +240,7 @@ export default {
|
|
|
images_upload_handler: this.imagesUploadHandler,
|
|
|
file_picker_types: 'media', // 文件上传类型
|
|
|
file_picker_callback: this.filePickerCallback,
|
|
|
- init_instance_callback: this.isFill ? this.initInstanceCallback : '',
|
|
|
+ init_instance_callback: this.isFill || this.isViewNote ? this.initInstanceCallback : '',
|
|
|
paste_enable_default_filters: false, // 禁用默认的粘贴过滤器
|
|
|
// 粘贴预处理
|
|
|
paste_preprocess(plugin, args) {
|
|
@@ -210,12 +255,25 @@ export default {
|
|
|
},
|
|
|
};
|
|
|
},
|
|
|
+ watch: {
|
|
|
+ isViewNote: {
|
|
|
+ handler(newVal, oldVal) {
|
|
|
+ if (newVal) {
|
|
|
+ let editor = tinymce.get(this.id);
|
|
|
+ if (editor) {
|
|
|
+ let start = editor.selection.getStart();
|
|
|
+ this.$emit('selectNote', start.getAttribute('data-annotation-id'));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
created() {
|
|
|
if (this.pageFrom !== 'audit') {
|
|
|
window.addEventListener('click', this.hideToolbarDrawer);
|
|
|
}
|
|
|
|
|
|
- if (this.isFill) {
|
|
|
+ if (this.isFill || this.isViewNote) {
|
|
|
window.addEventListener('click', this.hideContentmenu);
|
|
|
}
|
|
|
this.setBackgroundColor();
|
|
@@ -225,7 +283,7 @@ export default {
|
|
|
window.removeEventListener('click', this.hideToolbarDrawer);
|
|
|
}
|
|
|
|
|
|
- if (this.isFill) {
|
|
|
+ if (this.isFill || this.isViewNote) {
|
|
|
window.removeEventListener('click', this.hideContentmenu);
|
|
|
}
|
|
|
},
|
|
@@ -549,9 +607,10 @@ export default {
|
|
|
},
|
|
|
showContentmenu({ pixelsFromLeft, pixelsFromTop }) {
|
|
|
this.isShow = true;
|
|
|
+ // console.log(pixelsFromLeft, pixelsFromTop);
|
|
|
this.contentmenu = {
|
|
|
- left: `${pixelsFromLeft + 14}px`,
|
|
|
- top: `${pixelsFromTop + 22}px`,
|
|
|
+ left: `${this.isViewNote ? pixelsFromLeft : pixelsFromLeft + 14}px`,
|
|
|
+ top: `${this.isViewNote ? pixelsFromTop + 62 : pixelsFromTop + 22}px`,
|
|
|
};
|
|
|
},
|
|
|
|
|
@@ -587,6 +646,90 @@ export default {
|
|
|
this.mathEleIsInit = true;
|
|
|
}
|
|
|
},
|
|
|
+
|
|
|
+ // 获取高亮 span 标签
|
|
|
+ getLightSpanString(noteId, str) {
|
|
|
+ return `<span data-annotation-id="${noteId}" class="rich-fill" style="background-color:#FACB2F;cursor: pointer;">${str}</span>`;
|
|
|
+ },
|
|
|
+ // 选中文本打开弹窗
|
|
|
+ openExplanatoryNoteDialog() {
|
|
|
+ let editor = tinymce.get(this.id);
|
|
|
+ let start = editor.selection.getStart();
|
|
|
+ this.$emit('view-explanatory-note', { visible: true, noteId: start.getAttribute('data-annotation-id') });
|
|
|
+ this.hideContentmenu();
|
|
|
+ },
|
|
|
+ // 设置高亮背景,并保留备注
|
|
|
+ setExplanatoryNote(richData) {
|
|
|
+ let noteId = '';
|
|
|
+ let editor = tinymce.get(this.id);
|
|
|
+ let start = editor.selection.getStart();
|
|
|
+ let content = editor.selection.getContent();
|
|
|
+ if (isNodeType(start, 'span')) {
|
|
|
+ noteId = start.getAttribute('data-annotation-id');
|
|
|
+ } else {
|
|
|
+ noteId = `${this.id}_${Math.floor(Math.random() * 1000000)
|
|
|
+ .toString()
|
|
|
+ .padStart(10, '0')}`;
|
|
|
+ let str = this.replaceSpanString(content);
|
|
|
+ editor.selection.setContent(this.getLightSpanString(noteId, str));
|
|
|
+ }
|
|
|
+ let selectText = content.replace(/<[^>]+>/g, '');
|
|
|
+ let note = { id: noteId, note: richData.note, selectText };
|
|
|
+ this.$emit('selectContentSetMemo', note);
|
|
|
+ },
|
|
|
+ // 取消注释
|
|
|
+ cancelExplanatoryNote() {
|
|
|
+ let editor = tinymce.get(this.id);
|
|
|
+ let start = editor.selection.getStart();
|
|
|
+ if (isNodeType(start, 'span')) {
|
|
|
+ let textContent = start.textContent;
|
|
|
+ let content = editor.selection.getContent();
|
|
|
+ let str = textContent.split(content);
|
|
|
+ start.remove();
|
|
|
+ editor.selection.setContent(str.join(content));
|
|
|
+ this.$emit('selectContentSetMemo', null, start.getAttribute('data-annotation-id'));
|
|
|
+ } else {
|
|
|
+ this.collapse();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 删除,监听处理备注
|
|
|
+ cleanupRemovedAnnotations(editor) {
|
|
|
+ if (!this.isViewNote) return; // 只有富文本才处理
|
|
|
+ const body = editor.getBody();
|
|
|
+ const annotations = body.querySelectorAll('span[data-annotation-id]');
|
|
|
+
|
|
|
+ this.handleEmptySpan(editor);
|
|
|
+
|
|
|
+ // 存储所有现有的注释ID
|
|
|
+ const existingIds = new Set();
|
|
|
+ annotations.forEach((span) => {
|
|
|
+ existingIds.add(span.getAttribute('data-annotation-id'));
|
|
|
+ });
|
|
|
+
|
|
|
+ // 与你存储的注释数据对比,清理不存在的
|
|
|
+ this.$emit('compareAnnotationAndSave', existingIds);
|
|
|
+ },
|
|
|
+ // 删除span里面的文字之后,会出现空 span 标签残留,需处理掉
|
|
|
+ handleEmptySpan(editor) {
|
|
|
+ let that = this;
|
|
|
+ const selection = editor.selection;
|
|
|
+ const selectedNode = selection.getNode();
|
|
|
+ // 如果选中的是注释span内的内容
|
|
|
+ if (selectedNode.nodeType === 1 && selectedNode.hasAttribute('data-annotation-id')) {
|
|
|
+ const span = selectedNode;
|
|
|
+ // 检查删除后是否为空
|
|
|
+ if (!span.textContent || /^\s*$/.test(span.textContent)) {
|
|
|
+ // 保存注释ID
|
|
|
+ const annotationId = span.getAttribute('data-annotation-id');
|
|
|
+
|
|
|
+ // 用其父节点替换span
|
|
|
+ span.parentNode.replaceChild(document.createTextNode(''), span);
|
|
|
+
|
|
|
+ // 从存储中移除注释
|
|
|
+ that.$emit('selectContentSetMemo', null, annotationId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
},
|
|
|
};
|
|
|
</script>
|