|
|
@@ -14,7 +14,7 @@
|
|
|
<div v-show="isShow" :style="contentmenu" class="contentmenu">
|
|
|
<div v-if="isViewNote" @click="openExplanatoryNoteDialog">
|
|
|
<SvgIcon icon-class="mark" size="14" />
|
|
|
- <span class="button"> 编辑注释</span>
|
|
|
+ <span class="button"> 气泡</span>
|
|
|
</div>
|
|
|
<div v-else>
|
|
|
<SvgIcon icon-class="slice" size="16" @click="setFill" />
|
|
|
@@ -65,6 +65,7 @@ export default {
|
|
|
Editor,
|
|
|
MathDialog,
|
|
|
},
|
|
|
+ inject: ['processHtmlString'],
|
|
|
inheritAttrs: false,
|
|
|
props: {
|
|
|
inline: {
|
|
|
@@ -91,7 +92,7 @@ export default {
|
|
|
type: [String, Boolean],
|
|
|
/* eslint-disable max-len */
|
|
|
default:
|
|
|
- 'fontselect fontsizeselect forecolor backcolor | lineheight underline | bold italic strikethrough alignleft aligncenter alignright | bullist numlist | image media | link blockquote hr mathjax',
|
|
|
+ 'fontselect fontsizeselect forecolor backcolor lineheight paragraphSpacing letterSpacing indent outdent customUnderline bold italic strikethrough alignleft aligncenter alignright bullist numlist dotEmphasis image media link blockquote hr mathjax',
|
|
|
},
|
|
|
wordlimitNum: {
|
|
|
type: [Number, Boolean],
|
|
|
@@ -109,6 +110,10 @@ export default {
|
|
|
type: String,
|
|
|
default: 'Arial',
|
|
|
},
|
|
|
+ fontColor: {
|
|
|
+ type: String,
|
|
|
+ default: '#000000',
|
|
|
+ },
|
|
|
isHasSpace: {
|
|
|
type: Boolean,
|
|
|
default: false,
|
|
|
@@ -129,6 +134,10 @@ export default {
|
|
|
type: Number,
|
|
|
default: null,
|
|
|
},
|
|
|
+ isTitle: {
|
|
|
+ type: Boolean,
|
|
|
+ default: false,
|
|
|
+ },
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
@@ -141,12 +150,29 @@ export default {
|
|
|
left: 0,
|
|
|
},
|
|
|
id: getRandomNumber(),
|
|
|
+ editorIsInited: false,
|
|
|
+ editorBeforeInitConfig: {},
|
|
|
init: {
|
|
|
content_style: `
|
|
|
mjx-container, mjx-container * {
|
|
|
font-size: 16px !important; /* 强制固定字体 */
|
|
|
line-height: 1.2 !important; /* 避免行高影响 */
|
|
|
- }`, // 解决公式每点击一次字体就变大
|
|
|
+ }
|
|
|
+ .rich-text-emphasis-dot {
|
|
|
+ --letter-spacing-value: attr(data-letter-spacing number, 0);
|
|
|
+ border-bottom: none;
|
|
|
+ background-image: radial-gradient(
|
|
|
+ circle at center,
|
|
|
+ currentColor 0.15em, /* 圆点大小相对于字体 */
|
|
|
+ transparent 0.16em
|
|
|
+ );
|
|
|
+ background-size: calc((1 + var(--letter-spacing-value)) * 1em) 0.3em;
|
|
|
+ background-repeat: repeat-x;
|
|
|
+ background-position: calc(var(--letter-spacing-value) * -0.5em) 100%;
|
|
|
+ padding-bottom: 0.3em; /* 间距也相对于字体 */
|
|
|
+ display: inline;
|
|
|
+ }
|
|
|
+ `, // 解决公式每点击一次字体就变大
|
|
|
valid_elements: '*[*]', // 允许所有标签和属性
|
|
|
valid_children: '+body[style]', // 允许 MathJax 的样式
|
|
|
extended_valid_elements: 'span[*],mjx-container[*],svg[*],path[*]', // 明确允许 MathJax 标签
|
|
|
@@ -164,17 +190,78 @@ export default {
|
|
|
plugins: 'link lists image hr media autoresize ax_wordlimit paste', // 移除 lineheight
|
|
|
toolbar: this.toolbar, // 工具栏
|
|
|
lineheight_formats: '0.5 1.0 1.2 1.5 2.0 2.5 3.0', // 行高选项(倍数)
|
|
|
+ paragraphheight_formats: [1.0, 1.2, 1.5, 2.0, 2.5, 3.0], // 段落间距
|
|
|
+ letterSpacing_formats: [1.0, 1.2, 1.5, 2.0, 2.5, 3.0], // 字间距选项
|
|
|
contextmenu: false, // 右键菜单
|
|
|
menubar: false, // 菜单栏
|
|
|
branding: false, // 品牌
|
|
|
statusbar: false, // 状态栏
|
|
|
entity_encoding: 'raw', // raw不编码任何字符;named: 使用命名实体(如 );numeric: 使用数字实体(如  )
|
|
|
+ target_list: false,
|
|
|
+ default_link_target: '_blank', // 或 '_self'
|
|
|
setup: (editor) => {
|
|
|
+ editor.on('GetContent', (e) => {
|
|
|
+ if (e.format === 'html') {
|
|
|
+ e.content = this.smartPreserveLineBreaks(editor, e.content);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
let isRendered = false; // 标记是否已渲染
|
|
|
- let that = this;
|
|
|
editor.on('init', () => {
|
|
|
editor.getBody().style.fontSize = this.init.font_size; // 设置默认字体大小
|
|
|
editor.getBody().style.fontFamily = this.init.font_family; // 设置默认字体
|
|
|
+ editor.getBody().style.color = this.fontColor; // 设置默认字体颜色
|
|
|
+
|
|
|
+ this.init.paragraphheight_formats.forEach((config) => {
|
|
|
+ const formatName = `paragraphSpacing${config}_em`;
|
|
|
+ editor.formatter.register(formatName, {
|
|
|
+ selector: 'p',
|
|
|
+ styles: { 'margin-bottom': `${config}em` },
|
|
|
+ });
|
|
|
+ });
|
|
|
+ this.init.letterSpacing_formats.forEach((config) => {
|
|
|
+ const formatName = `letterSpacing${config}_em`;
|
|
|
+ editor.formatter.register(formatName, {
|
|
|
+ inline: 'span',
|
|
|
+ styles: { 'letter-spacing': `${config}em` },
|
|
|
+ attributes: { 'data-letter-spacing': `${config}` },
|
|
|
+ wrapper: true,
|
|
|
+ remove_similar: true,
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!editor.formatter.has('emphasisDot')) {
|
|
|
+ editor.formatter.register('emphasisDot', {
|
|
|
+ inline: 'span',
|
|
|
+ classes: 'rich-text-emphasis-dot',
|
|
|
+ // styles: {
|
|
|
+ // 'border-bottom': 'none',
|
|
|
+ // 'background-image':
|
|
|
+ // 'radial-gradient(circle at center, currentColor 0.15em, transparent 0.16em)' /* 圆点大小相对于字体 */,
|
|
|
+ // 'background-size': '1em 0.3em' /* 间距相对于字体大小,高度相对字体 */,
|
|
|
+ // 'background-repeat': 'repeat-x',
|
|
|
+ // 'background-position': '0 100%',
|
|
|
+ // 'padding-bottom': '0.3em' /* 间距也相对于字体 */,
|
|
|
+ // display: 'inline',
|
|
|
+ // },
|
|
|
+ wrapper: true,
|
|
|
+ remove_similar: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ if (!editor.formatter.has('coloredunderline')) {
|
|
|
+ editor.formatter.register('coloredunderline', {
|
|
|
+ inline: 'span',
|
|
|
+ styles: {
|
|
|
+ 'border-bottom': `0.1em solid #000000`, // 要固定线粗细的话,就使用2px
|
|
|
+ 'padding-bottom': '1px',
|
|
|
+ },
|
|
|
+ merge_siblings: false,
|
|
|
+ exact: true, // 添加 exact 属性
|
|
|
+ wrapper: true,
|
|
|
+ remove_similar: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ this.editorIsInited = true;
|
|
|
});
|
|
|
|
|
|
// 自定义行高下拉(因为没有内置 lineheight 插件)
|
|
|
@@ -206,14 +293,190 @@ export default {
|
|
|
},
|
|
|
});
|
|
|
|
|
|
- editor.on('click', () => {
|
|
|
- if (editor?.queryCommandState('ToggleToolbarDrawer')) {
|
|
|
- editor.execCommand('ToggleToolbarDrawer');
|
|
|
- }
|
|
|
- if (!isRendered && window.MathJax) {
|
|
|
- isRendered = true;
|
|
|
- window.MathJax.typesetPromise([editor.getBody()]);
|
|
|
- }
|
|
|
+ // 重写下划线
|
|
|
+ editor.ui.registry.addButton('customUnderline', {
|
|
|
+ icon: 'underline',
|
|
|
+ tooltip: '下划线',
|
|
|
+ onAction: () => {
|
|
|
+ editor.windowManager.open({
|
|
|
+ title: '选择下划线颜色',
|
|
|
+ body: {
|
|
|
+ type: 'panel',
|
|
|
+ items: [
|
|
|
+ {
|
|
|
+ type: 'colorpicker',
|
|
|
+ name: 'color',
|
|
|
+ label: '颜色',
|
|
|
+ value: '#000000',
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ },
|
|
|
+ buttons: [
|
|
|
+ {
|
|
|
+ type: 'custom',
|
|
|
+ name: 'cancelUndeline',
|
|
|
+ text: '取消下划线',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'cancel',
|
|
|
+ text: '取消',
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'submit',
|
|
|
+ text: '确定',
|
|
|
+ buttonType: 'primary',
|
|
|
+ enabled: true,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ initialData: {
|
|
|
+ color: '#000000',
|
|
|
+ },
|
|
|
+ onSubmit: (api) => {
|
|
|
+ const color = api.getData().color;
|
|
|
+ // 加下划线
|
|
|
+ // editor.execCommand('Underline');
|
|
|
+ // 注册自定义格式
|
|
|
+ editor.formatter.register('coloredunderline', {
|
|
|
+ inline: 'span',
|
|
|
+ styles: {
|
|
|
+ 'border-bottom': `0.1em solid ${color}`, // 要固定线粗细的话,就使用2px
|
|
|
+ 'padding-bottom': '1px',
|
|
|
+ },
|
|
|
+ merge_siblings: false,
|
|
|
+ exact: true, // 添加 exact 属性
|
|
|
+ wrapper: true,
|
|
|
+ remove_similar: true,
|
|
|
+ });
|
|
|
+
|
|
|
+ // 应用格式
|
|
|
+ editor.formatter.apply('coloredunderline');
|
|
|
+
|
|
|
+ api.close();
|
|
|
+ },
|
|
|
+ onAction: (api, details) => {
|
|
|
+ if (details.name === 'cancelUndeline') {
|
|
|
+ editor.formatter.remove('coloredunderline');
|
|
|
+ api.close();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加段落间距下拉菜单
|
|
|
+ editor.ui.registry.addMenuButton('paragraphSpacing', {
|
|
|
+ icon: 'paragraph',
|
|
|
+ // text: '段落间距',
|
|
|
+ tooltip: '段落间距',
|
|
|
+ fetch: (callback) => {
|
|
|
+ const items = [];
|
|
|
+
|
|
|
+ // 动态生成菜单项
|
|
|
+ this.init.paragraphheight_formats.forEach((config) => {
|
|
|
+ const formatName = `paragraphSpacing${config}_em`;
|
|
|
+ items.push({
|
|
|
+ type: 'menuitem',
|
|
|
+ text: `${config}`,
|
|
|
+ onAction: () => {
|
|
|
+ // 先清除其他间距格式
|
|
|
+ this.init.paragraphheight_formats.forEach((cfg) => {
|
|
|
+ const fmtName = `paragraphSpacing${cfg}_em`;
|
|
|
+ editor.formatter.remove(fmtName);
|
|
|
+ });
|
|
|
+ // 应用当前选择的间距
|
|
|
+ editor.formatter.apply(formatName);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加清除间距选项
|
|
|
+ items.push({
|
|
|
+ type: 'separator', // 分隔线
|
|
|
+ });
|
|
|
+ items.push({
|
|
|
+ type: 'menuitem',
|
|
|
+ text: '清除间距',
|
|
|
+ onAction: () => {
|
|
|
+ // 清除所有间距格式
|
|
|
+ this.init.paragraphheight_formats.forEach((config) => {
|
|
|
+ const formatName = `paragraphSpacing${config}_em`;
|
|
|
+ editor.formatter.remove(formatName);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ callback(items);
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // 注册自定义字间距图标
|
|
|
+ editor.ui.registry.addIcon(
|
|
|
+ 'letter-spacing-icon',
|
|
|
+ `<svg viewBox="4 4 18 18" width="44" height="24">
|
|
|
+ <text x="17" y="18" text-anchor="middle" font-size="14" font-weight="bold" fill="#000">A</text>
|
|
|
+ <path d="M12 10 L6 10 M6 10 L7.5 8.5 M6 10 L7.5 11.5" stroke="#1E90FF" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ <path d="M22 10 L28 10 M28 10 L26.5 8.5 M28 10 L26.5 11.5" stroke="#1E90FF" stroke-width="1.2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
|
|
+ </svg>`,
|
|
|
+ );
|
|
|
+
|
|
|
+ // 添加字间距下拉菜单
|
|
|
+ editor.ui.registry.addMenuButton('letterSpacing', {
|
|
|
+ icon: 'letter-spacing-icon',
|
|
|
+ // text: '字间距',
|
|
|
+ tooltip: '字间距',
|
|
|
+ fetch: (callback) => {
|
|
|
+ const items = [];
|
|
|
+
|
|
|
+ // 动态生成菜单项
|
|
|
+ this.init.letterSpacing_formats.forEach((config) => {
|
|
|
+ const formatName = `letterSpacing${config}_em`;
|
|
|
+ items.push({
|
|
|
+ type: 'menuitem',
|
|
|
+ text: `${config}`,
|
|
|
+ onAction: () => {
|
|
|
+ // 先清除其他字间距格式
|
|
|
+ this.init.letterSpacing_formats.forEach((cfg) => {
|
|
|
+ const fmtName = `letterSpacing${cfg}_em`;
|
|
|
+ editor.formatter.remove(fmtName);
|
|
|
+ });
|
|
|
+ // 应用当前选择的字间距
|
|
|
+ editor.formatter.apply(formatName);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加清除字间距选项
|
|
|
+ items.push({
|
|
|
+ type: 'separator', // 分隔线
|
|
|
+ });
|
|
|
+ items.push({
|
|
|
+ type: 'menuitem',
|
|
|
+ text: '清除字间距',
|
|
|
+ onAction: () => {
|
|
|
+ // 清除所有字间距格式
|
|
|
+ this.init.letterSpacing_formats.forEach((config) => {
|
|
|
+ const formatName = `letterSpacing${config}_em`;
|
|
|
+ editor.formatter.remove(formatName);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ callback(items);
|
|
|
+ },
|
|
|
+ });
|
|
|
+ // 添加 添加着重点 按钮
|
|
|
+ editor.ui.registry.addButton('dotEmphasis', {
|
|
|
+ text: '●',
|
|
|
+ tooltip: '着重点',
|
|
|
+ onAction: () => {
|
|
|
+ const editor = tinymce.activeEditor;
|
|
|
+
|
|
|
+ if (editor.formatter.match('emphasisDot')) {
|
|
|
+ editor.formatter.remove('emphasisDot');
|
|
|
+ } else {
|
|
|
+ editor.formatter.apply('emphasisDot');
|
|
|
+ }
|
|
|
+ },
|
|
|
});
|
|
|
|
|
|
// 添加 MathJax 按钮
|
|
|
@@ -225,6 +488,16 @@ export default {
|
|
|
},
|
|
|
});
|
|
|
|
|
|
+ editor.on('click', () => {
|
|
|
+ if (editor?.queryCommandState('ToggleToolbarDrawer')) {
|
|
|
+ editor.execCommand('ToggleToolbarDrawer');
|
|
|
+ }
|
|
|
+ if (!isRendered && window.MathJax) {
|
|
|
+ isRendered = true;
|
|
|
+ window.MathJax.typesetPromise([editor.getBody()]);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
// 内容变化时重新渲染公式
|
|
|
editor.on('change', () => this.renderMath());
|
|
|
|
|
|
@@ -233,7 +506,7 @@ export default {
|
|
|
if (e.keyCode === 8 || e.keyCode === 46) {
|
|
|
// 延迟执行以确保删除已完成
|
|
|
setTimeout(() => {
|
|
|
- that.cleanupRemovedAnnotations(editor);
|
|
|
+ this.cleanupRemovedAnnotations(editor);
|
|
|
}, 500);
|
|
|
}
|
|
|
});
|
|
|
@@ -241,7 +514,7 @@ export default {
|
|
|
// 也可以监听剪切操作
|
|
|
editor.on('Cut', () => {
|
|
|
setTimeout(() => {
|
|
|
- that.cleanupRemovedAnnotations(editor);
|
|
|
+ this.cleanupRemovedAnnotations(editor);
|
|
|
}, 500);
|
|
|
});
|
|
|
// editor.on('NodeChange', function (e) {
|
|
|
@@ -253,7 +526,7 @@ export default {
|
|
|
// ) {
|
|
|
// const annotationId = e.element.getAttribute('data-annotation-id');
|
|
|
// e.element.parentNode.removeChild(e.element);
|
|
|
- // that.$emit('selectContentSetMemo', null, annotationId);
|
|
|
+ // this.$emit('selectContentSetMemo', null, annotationId);
|
|
|
// }
|
|
|
// });
|
|
|
},
|
|
|
@@ -264,7 +537,7 @@ export default {
|
|
|
'Arial=arial,helvetica,sans-serif;' +
|
|
|
'Times New Roman=times new roman,times,serif;' +
|
|
|
'拼音=League;',
|
|
|
- fontsize_formats: '8pt 10pt 11pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 30pt 32pt 34pt 36pt',
|
|
|
+ fontsize_formats: '8pt 10pt 12pt 14pt 16pt 18pt 20pt 22pt 24pt 26pt 28pt 30pt 32pt 34pt 36pt',
|
|
|
// 字数限制
|
|
|
ax_wordlimit_num: this.wordlimitNum,
|
|
|
ax_wordlimit_callback(editor) {
|
|
|
@@ -276,11 +549,13 @@ export default {
|
|
|
file_picker_callback: this.filePickerCallback,
|
|
|
init_instance_callback: this.isFill || this.isViewNote ? this.initInstanceCallback : '',
|
|
|
paste_enable_default_filters: false, // 禁用默认的粘贴过滤器
|
|
|
+ paste_as_text: true, // 默认作为纯文本粘贴
|
|
|
// 粘贴预处理
|
|
|
paste_preprocess(plugin, args) {
|
|
|
let content = args.content;
|
|
|
// 使用正则表达式去掉 style 中的 background 属性
|
|
|
content = content.replace(/background(-color)?:[^;]+;/g, '');
|
|
|
+ content = content.replace(/\t/g, ' '); // 将制表符替换为4个空格
|
|
|
args.content = content;
|
|
|
},
|
|
|
// 指定在 WebKit 中粘贴时要保留的样式
|
|
|
@@ -289,6 +564,7 @@ export default {
|
|
|
},
|
|
|
};
|
|
|
},
|
|
|
+ computed: {},
|
|
|
watch: {
|
|
|
isViewNote: {
|
|
|
handler(newVal) {
|
|
|
@@ -346,6 +622,29 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
},
|
|
|
+ isTitle: {
|
|
|
+ handler(newVal) {
|
|
|
+ this.displayToolbar(newVal);
|
|
|
+ },
|
|
|
+ },
|
|
|
+ // 未初始化完成或者数据未加载完成的时候,执行会有问题
|
|
|
+ editorIsInited: {
|
|
|
+ handler(newVal) {
|
|
|
+ if (newVal) {
|
|
|
+ let isTitle = this.editorBeforeInitConfig['isTitle'];
|
|
|
+ if (isTitle === true || isTitle === false) {
|
|
|
+ this.displayToolbar(isTitle);
|
|
|
+ this.editorBeforeInitConfig['isTitle'] = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ let style = this.editorBeforeInitConfig['style'];
|
|
|
+ if (style) {
|
|
|
+ this.setRichTitleFormat(style);
|
|
|
+ this.editorBeforeInitConfig['style'] = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ },
|
|
|
},
|
|
|
created() {
|
|
|
if (this.pageFrom !== 'audit') {
|
|
|
@@ -367,6 +666,89 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
methods: {
|
|
|
+ getRichContent() {
|
|
|
+ return tinymce.get(this.id).getContent();
|
|
|
+ },
|
|
|
+ getRichSelectionContent() {
|
|
|
+ return tinymce.get(this.id).selection.getContent();
|
|
|
+ },
|
|
|
+ displayToolbar(isTitle, isInit) {
|
|
|
+ if (!this.editorIsInited) {
|
|
|
+ this.editorBeforeInitConfig['isTitle'] = isTitle;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let editor = tinymce.get(this.id);
|
|
|
+ if (!editor) return;
|
|
|
+ const header = editor.editorContainer?.querySelector('.tox-editor-header');
|
|
|
+ if (header) {
|
|
|
+ header.style.display = isTitle ? 'none' : '';
|
|
|
+ if (!isInit) {
|
|
|
+ const body = editor.getBody();
|
|
|
+ if (!body) return;
|
|
|
+
|
|
|
+ const pElements = body.querySelectorAll('p');
|
|
|
+ if (this.processHtmlString && typeof this.processHtmlString === 'function') {
|
|
|
+ this.processHtmlString(pElements, {}, true);
|
|
|
+ }
|
|
|
+ editor.fire('change');
|
|
|
+ editor.nodeChanged();
|
|
|
+ this.setPinYinStyleForTitle();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ smartPreserveLineBreaks(editor, content) {
|
|
|
+ let body = editor.getBody();
|
|
|
+ let originalParagraphs = Array.from(body.getElementsByTagName('p'));
|
|
|
+
|
|
|
+ let tempDiv = document.createElement('div');
|
|
|
+ tempDiv.innerHTML = content;
|
|
|
+ let outputParagraphs = Array.from(tempDiv.getElementsByTagName('p'));
|
|
|
+
|
|
|
+ outputParagraphs.forEach((outputP, index) => {
|
|
|
+ let originalP = originalParagraphs[index];
|
|
|
+
|
|
|
+ if (originalP && outputP.innerHTML === '') {
|
|
|
+ // 判断这个空段落是否应该包含 <br>
|
|
|
+ let shouldHaveBr = this.shouldPreserveLineBreak(originalP, index, originalParagraphs);
|
|
|
+ if (shouldHaveBr) {
|
|
|
+ outputP.innerHTML = '<br>';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ return tempDiv.innerHTML;
|
|
|
+ },
|
|
|
+
|
|
|
+ shouldPreserveLineBreak(paragraph, index, allParagraphs) {
|
|
|
+ // 规则1:如果段落原本包含 <br>
|
|
|
+ if (paragraph.innerHTML.includes('<br>')) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 规则2:如果段落位于内容中间(不是第一个或最后一个)
|
|
|
+ if (index > 0 && index < allParagraphs.length - 1) {
|
|
|
+ let prevHasContent = allParagraphs[index - 1].textContent.trim() !== '';
|
|
|
+ let nextHasContent = allParagraphs[index + 1].textContent.trim() !== '';
|
|
|
+
|
|
|
+ if (prevHasContent && nextHasContent) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 规则3:如果段落是通过回车创建的(前后有内容)
|
|
|
+ let isBetweenContent = false;
|
|
|
+ if (index > 0 && allParagraphs[index - 1].textContent.trim() !== '') {
|
|
|
+ isBetweenContent = true;
|
|
|
+ }
|
|
|
+ if (index < allParagraphs.length - 1 && allParagraphs[index + 1].textContent.trim() !== '') {
|
|
|
+ isBetweenContent = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return isBetweenContent;
|
|
|
+ },
|
|
|
+
|
|
|
// 设置背景色
|
|
|
setBackgroundColor() {
|
|
|
let iframes = document.getElementsByTagName('iframe');
|
|
|
@@ -462,7 +844,45 @@ export default {
|
|
|
}
|
|
|
}
|
|
|
editor.selection.collapse(false);
|
|
|
+ editor.fire('change'); // 触发内容变化事件,确保内容更新
|
|
|
+
|
|
|
+ if (this.isViewPinyin) {
|
|
|
+ this.$nextTick(() => {
|
|
|
+ let styles = this.getFirstCharStyles();
|
|
|
+ this.$emit('createParsedTextInfoPinyin', null, styles);
|
|
|
+ });
|
|
|
+ }
|
|
|
},
|
|
|
+ setRichTitleFormat(config) {
|
|
|
+ if (!this.editorIsInited) {
|
|
|
+ this.editorBeforeInitConfig['style'] = config;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ let editor = tinymce.get(this.id);
|
|
|
+ if (!editor) return;
|
|
|
+
|
|
|
+ // 获取编辑器内容区域
|
|
|
+ const body = editor.getBody();
|
|
|
+ if (!body) return;
|
|
|
+
|
|
|
+ const pElements = body.querySelectorAll('p');
|
|
|
+ if (typeof this.processHtmlString === 'function') {
|
|
|
+ this.processHtmlString(pElements, config);
|
|
|
+ }
|
|
|
+ editor.fire('change');
|
|
|
+ editor.nodeChanged();
|
|
|
+ this.setPinYinStyleForTitle();
|
|
|
+ },
|
|
|
+ // 标题类型的富文本,如果开启了拼音,需要同步拼音样式
|
|
|
+ setPinYinStyleForTitle() {
|
|
|
+ if (this.isViewPinyin) {
|
|
|
+ let styles = this.getFirstCharStyles();
|
|
|
+ this.$emit('createParsedTextStyleForTitle', styles);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
/**
|
|
|
* 图片上传自定义逻辑函数
|
|
|
* @param {object} blobInfo 文件数据
|
|
|
@@ -603,7 +1023,7 @@ export default {
|
|
|
let start = editor.selection.getStart();
|
|
|
if (isNodeType(start, 'span')) {
|
|
|
let textContent = start.textContent;
|
|
|
- let content = editor.selection.getContent();
|
|
|
+ let content = this.getRichSelectionContent();
|
|
|
let str = textContent.split(content);
|
|
|
start.remove();
|
|
|
editor.selection.setContent(str.join(content));
|
|
|
@@ -615,7 +1035,7 @@ export default {
|
|
|
setContent() {
|
|
|
let editor = tinymce.get(this.id);
|
|
|
let start = editor.selection.getStart();
|
|
|
- let content = editor.selection.getContent();
|
|
|
+ let content = this.getRichSelectionContent();
|
|
|
if (isNodeType(start, 'span')) {
|
|
|
let textContent = start.textContent;
|
|
|
let str = textContent.split(content);
|
|
|
@@ -643,15 +1063,16 @@ export default {
|
|
|
replaceSpanString(str) {
|
|
|
return str.replace(/<span\b[^>]*>(.*?)<\/span>/gi, '$1');
|
|
|
},
|
|
|
- createParsedTextInfoPinyin(content) {
|
|
|
- let text = content.replace(/<[^>]+>/g, '');
|
|
|
- this.$emit('createParsedTextInfoPinyin', text);
|
|
|
- },
|
|
|
+
|
|
|
handleRichTextBlur() {
|
|
|
this.$emit('handleRichTextBlur', this.itemIndex);
|
|
|
- let content = tinymce.get(this.id).getContent();
|
|
|
+ let content = this.getRichContent();
|
|
|
+
|
|
|
if (this.isViewPinyin) {
|
|
|
- this.createParsedTextInfoPinyin(content);
|
|
|
+ // updated by 2026-3-24 失去焦点时,不再生成拼音,改成手动点击生成拼音
|
|
|
+ // let styles = this.getFirstCharStyles();
|
|
|
+ // let text = content.replace(/<[^>]+>/g, '');
|
|
|
+ // this.$emit('createParsedTextInfoPinyin', text, styles);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
@@ -659,7 +1080,7 @@ export default {
|
|
|
let isHasPinyin = content
|
|
|
.split(/<[^>]+>/g)
|
|
|
.filter((item) => item)
|
|
|
- .some((item) => item.match(/[a-zA-Z]+\d(\s| )*/));
|
|
|
+ .some((item) => item.match(/[a-zA-Z]+\d+(\s| )+/));
|
|
|
|
|
|
if (!isHasPinyin) {
|
|
|
return;
|
|
|
@@ -756,55 +1177,116 @@ export default {
|
|
|
}
|
|
|
if (eleMathArs.length === 0) return;
|
|
|
await this.$nextTick();
|
|
|
- window.MathJax.typesetPromise(eleMathArs).catch((err) => console.error('MathJax error:', err));
|
|
|
+ window.MathJax.typesetPromise(eleMathArs).catch((err) => console.error(err));
|
|
|
this.mathEleIsInit = true;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 获取高亮 span 标签
|
|
|
getLightSpanString(noteId, str) {
|
|
|
- return `<span data-annotation-id="${noteId}" class="rich-fill" style="background-color:#fff3b7;cursor: pointer;">${str}</span>`;
|
|
|
+ return `<span data-annotation-id="${noteId}" class="rich-fill" style="color:#8206BF;cursor: pointer;">${str}</span>`;
|
|
|
},
|
|
|
+
|
|
|
+ generateAnnotationId() {
|
|
|
+ return `${this.id}_${Math.floor(Math.random() * 1000000)
|
|
|
+ .toString()
|
|
|
+ .padStart(10, '0')}`;
|
|
|
+ },
|
|
|
+
|
|
|
// 选中文本打开弹窗
|
|
|
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') });
|
|
|
+ const editor = tinymce.get(this.id);
|
|
|
+ if (!editor) {
|
|
|
+ console.error('编辑器未初始化');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 获取当前选区的起始元素
|
|
|
+ const start = editor.selection.getStart();
|
|
|
+ let noteId = start.getAttribute('data-annotation-id');
|
|
|
+
|
|
|
+ // 如果当前选区没有注释ID,说明是新建注释,需要先创建高亮 span
|
|
|
+ if (!noteId) {
|
|
|
+ const content = this.getRichSelectionContent();
|
|
|
+ if (!content || !content.trim()) {
|
|
|
+ this.$message.warning('请先选择要添加注释的文本');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ noteId = this.generateAnnotationId();
|
|
|
+ // 清理已有的 span 标签
|
|
|
+ const cleanContent = this.replaceSpanString(content);
|
|
|
+ // 创建高亮 span
|
|
|
+ editor.selection.setContent(this.getLightSpanString(noteId, cleanContent));
|
|
|
+ // 获取纯文本
|
|
|
+ const selectText = content.replace(/<[^>]+>/g, '');
|
|
|
+ // 使用 Range 精确计算索引,解决重复文本定位错误问题
|
|
|
+ const indices = this.getSelectionIndices(editor);
|
|
|
+ const note = {
|
|
|
+ annota_id: noteId,
|
|
|
+ text: selectText,
|
|
|
+ begin_index: indices.end - selectText.length + 1,
|
|
|
+ end_index: indices.end,
|
|
|
+ note: '',
|
|
|
+ };
|
|
|
+ // console.log('初始化注释数据:', note);
|
|
|
+ this.$emit('selectContentSetMemo', note, noteId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 打开弹窗
|
|
|
+ this.$emit('view-explanatory-note', {
|
|
|
+ visible: true,
|
|
|
+ annota_id: noteId,
|
|
|
+ });
|
|
|
+
|
|
|
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');
|
|
|
+ // 获取当前选区在全文中的精确索引(开始和结束位置)
|
|
|
+ getSelectionIndices(editor) {
|
|
|
+ const rng = editor.selection.getRng();
|
|
|
+ if (!rng) return { start: 0, end: 0 };
|
|
|
+
|
|
|
+ // 创建一个临时的 Range 用于计算偏移量
|
|
|
+ const preRng = editor.dom.createRng();
|
|
|
+ preRng.selectNodeContents(editor.getBody());
|
|
|
+ preRng.setEnd(rng.startContainer, rng.startOffset);
|
|
|
+ // 开始索引 = 选区起点前的所有文本长度
|
|
|
+ const endIndex = preRng.toString().length;
|
|
|
+ return {
|
|
|
+ end: endIndex,
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 设置高亮文本样式,并保存注释
|
|
|
+ setExplanatoryNote(richData, richTextID) {
|
|
|
+ const editor = tinymce.get(richTextID);
|
|
|
+ if (!editor) {
|
|
|
+ console.error('编辑器未初始化');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const noteId = richData?.id || richData?.annota_id;
|
|
|
+ const noteContent = richData?.note || richData?.dataStr || '';
|
|
|
+ // 查找是否已存在该注释的 span(由 openExplanatoryNoteDialog 创建)
|
|
|
+ const existingSpan = editor.dom.select(`span[data-annotation-id="${noteId}"]`)[0];
|
|
|
+ if (existingSpan) {
|
|
|
+ // 从 span 中获取选中文本
|
|
|
+ const selectText = existingSpan.textContent || '';
|
|
|
+ // 发送更新后的数据到父组件
|
|
|
+ const note = {
|
|
|
+ id: noteId, // 确保这里有 id
|
|
|
+ annota_id: noteId, // 兼容字段
|
|
|
+ note: noteContent,
|
|
|
+ dataStr: noteContent, // 兼容字段
|
|
|
+ selectText,
|
|
|
+ };
|
|
|
+ this.$emit('selectContentSetMemo', note, noteId);
|
|
|
} 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));
|
|
|
+ console.error('未找到对应的注释');
|
|
|
}
|
|
|
- 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();
|
|
|
- }
|
|
|
+ cancelExplanatoryNote(noteId, richTextID) {
|
|
|
+ let editor = tinymce.get(richTextID);
|
|
|
+ const node = editor.dom.select(`span[data-annotation-id="${noteId}"]`)[0];
|
|
|
+ editor.dom.remove(node, true);
|
|
|
},
|
|
|
// 删除,监听处理备注
|
|
|
cleanupRemovedAnnotations(editor) {
|
|
|
@@ -825,7 +1307,6 @@ export default {
|
|
|
},
|
|
|
// 删除span里面的文字之后,会出现空 span 标签残留,需处理掉
|
|
|
handleEmptySpan(editor) {
|
|
|
- let that = this;
|
|
|
const selection = editor.selection;
|
|
|
const selectedNode = selection.getNode();
|
|
|
// 如果选中的是注释span内的内容
|
|
|
@@ -840,10 +1321,80 @@ export default {
|
|
|
span.parentNode.replaceChild(document.createTextNode(''), span);
|
|
|
|
|
|
// 从存储中移除注释
|
|
|
- that.$emit('selectContentSetMemo', null, annotationId);
|
|
|
+ this.$emit('selectContentSetMemo', null, annotationId);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
+
|
|
|
+ getFirstCharStyles() {
|
|
|
+ const editor = tinymce.activeEditor;
|
|
|
+ if (!editor) return {};
|
|
|
+
|
|
|
+ const firstTextNode = this.findFirstTextNode(editor.getBody());
|
|
|
+ if (!firstTextNode) return {};
|
|
|
+
|
|
|
+ const styles = {};
|
|
|
+ let element = firstTextNode.parentElement;
|
|
|
+
|
|
|
+ while (element && element !== editor.getBody().parentElement) {
|
|
|
+ const computed = window.getComputedStyle(element);
|
|
|
+
|
|
|
+ if (!styles.fontFamily && computed.fontFamily && computed.fontFamily !== 'inherit') {
|
|
|
+ styles.fontFamily = computed.fontFamily;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!styles.fontSize && computed.fontSize && computed.fontSize !== 'inherit') {
|
|
|
+ const fontSize = computed.fontSize;
|
|
|
+ const pxValue = parseFloat(fontSize);
|
|
|
+ if (isNaN(pxValue)) {
|
|
|
+ styles.fontSize = fontSize;
|
|
|
+ } else {
|
|
|
+ // px转pt公式:pt = px * 3/4
|
|
|
+ const ptValue = Math.round(pxValue * 0.75 * 10) / 10;
|
|
|
+ styles.fontSize = `${ptValue}pt`;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!styles.color && computed.color && computed.color !== 'inherit') {
|
|
|
+ styles.color = computed.color;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!styles.bold && (computed.fontWeight === 'bold' || computed.fontWeight >= '700')) {
|
|
|
+ styles.bold = true;
|
|
|
+ styles.fontWeight = 'bold';
|
|
|
+ } else {
|
|
|
+ styles.bold = false;
|
|
|
+ styles.fontWeight = '';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!styles.underline && computed.textDecoration.includes('underline')) {
|
|
|
+ styles.underline = true;
|
|
|
+ styles.textDecoration = 'underline';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!styles.strikethrough && computed.textDecoration.includes('line-through')) {
|
|
|
+ styles.strikethrough = true;
|
|
|
+ styles.textDecoration = 'line-through';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Object.keys(styles).length >= 6) break;
|
|
|
+
|
|
|
+ element = element.parentElement;
|
|
|
+ }
|
|
|
+ return styles;
|
|
|
+ },
|
|
|
+
|
|
|
+ findFirstTextNode(element) {
|
|
|
+ const walker = document.createTreeWalker(
|
|
|
+ element,
|
|
|
+ NodeFilter.SHOW_TEXT,
|
|
|
+ {
|
|
|
+ acceptNode: (node) => (node.textContent.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT),
|
|
|
+ },
|
|
|
+ false,
|
|
|
+ );
|
|
|
+ return walker.nextNode();
|
|
|
+ },
|
|
|
},
|
|
|
};
|
|
|
</script>
|