From ed498040ba211f6da694d3f3fe790b0fa519be4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A7=8B=E4=BA=8E=E5=88=9D=E8=A7=81?= <752204717@qq.com> Date: Thu, 28 May 2026 15:04:54 +0800 Subject: [PATCH] tijiao --- src/api/index.js | 4 +- src/components/common/langs/en.js | 3 + src/components/common/langs/zh.js | 3 + .../page/components/Tinymce/index.vue | 257 +++++++++++------- .../page/components/email/CkeditorMail.vue | 2 +- .../email/MailTemplatePreviewDialog.vue | 91 +++++++ .../page/components/email/TmrEmailEditor.vue | 36 +-- src/components/page/editPublicRefRdit.vue | 2 +- src/components/page/mailboxMould.vue | 62 +++-- src/components/page/mailboxMouldDetail.vue | 182 ++++++++++++- src/utils/mailTemplatePreview.js | 76 ++++++ src/utils/mailTemplateVariables.js | 64 +++++ 12 files changed, 606 insertions(+), 176 deletions(-) create mode 100644 src/components/page/components/email/MailTemplatePreviewDialog.vue create mode 100644 src/utils/mailTemplatePreview.js create mode 100644 src/utils/mailTemplateVariables.js diff --git a/src/api/index.js b/src/api/index.js index 525753d..487fb66 100644 --- a/src/api/index.js +++ b/src/api/index.js @@ -19,8 +19,8 @@ const service = axios.create({ // baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换 // baseURL: 'http://www.tougao.com/', //测试本地 记得切换 // baseURL: 'http://192.168.110.110/tougao/public/index.php/', - // baseURL: '/api', //本地 - baseURL: '/', //正式 + baseURL: '/api', //本地 + // baseURL: '/', //正式 }); diff --git a/src/components/common/langs/en.js b/src/components/common/langs/en.js index 17df994..858ee9a 100644 --- a/src/components/common/langs/en.js +++ b/src/components/common/langs/en.js @@ -625,6 +625,9 @@ const en = { versionPlaceholder: '1.0.0', bodyEdit: 'Body editor', activeStatus: 'Active', + insertVariable: 'Copy variable', + copyVariableSuccess: 'Copied {token}', + copyVariableFail: 'Copy failed, please copy manually', variablesJson: 'Variables (JSON)', variablesPlaceholder: '{"name": "string"}', previewTab: 'Preview', diff --git a/src/components/common/langs/zh.js b/src/components/common/langs/zh.js index b5c1363..e3c854c 100644 --- a/src/components/common/langs/zh.js +++ b/src/components/common/langs/zh.js @@ -614,6 +614,9 @@ const zh = { versionPlaceholder: '1.0.0', bodyEdit: '正文编辑', activeStatus: '激活状态', + insertVariable: '复制变量', + copyVariableSuccess: '已复制 {token}', + copyVariableFail: '复制失败,请手动复制', variablesJson: '动态变量 (JSON)', variablesPlaceholder: '{"name": "string"}', previewTab: '预览窗口', diff --git a/src/components/page/components/Tinymce/index.vue b/src/components/page/components/Tinymce/index.vue index 6490f58..fbd9c68 100644 --- a/src/components/page/components/Tinymce/index.vue +++ b/src/components/page/components/Tinymce/index.vue @@ -129,17 +129,14 @@ export default { }, watch: { value: { - handler(val) { - - - if (!this.hasChange && this.hasInit) { - this.$nextTick(() => { - window.tinymce.get(this.tinymceId).setContent(val); - }); - } - }, - immediate: true + if (!this.hasChange && this.hasInit) { + this.$nextTick(() => { + window.tinymce.get(this.tinymceId).setContent(val); + }); + } + }, + immediate: true } }, mounted() { @@ -487,7 +484,10 @@ export default { if (!this.isPlainLatexText(normalized)) return ''; - const lines = normalized.split(/\n/).map((l) => l.trim()).filter(Boolean); + const lines = normalized + .split(/\n/) + .map((l) => l.trim()) + .filter(Boolean); if (lines.length > 1 && lines.every((l) => this.isPlainLatexText(l))) { return lines.map((l) => this.createWmathHtml(l, this.resolveWmathWrapMode(l))).join(''); } @@ -531,68 +531,86 @@ export default { return new Blob([u8arr], { type: mime }); }, formatHtml(val) { - const rawValue = val || ''; // 处理 null - const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g; - const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g; - const removeBr = //gi; // 移除所有 br 标签 + const rawValue = val || ''; // 处理 null + const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g; + const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g; + const removeBr = //gi; // 移除所有 br 标签 - if (rawValue.includes('wordTableHtml')) { - const parser = new DOMParser(); - const doc = parser.parseFromString(rawValue, 'text/html'); - const cells = doc.querySelectorAll('td, th'); - - cells.forEach((cell) => { - cell.innerHTML = cell.innerHTML - .replace(cleanEmptyTags, '') - .replace(removeBr, '') // 针对你“不想要br”的需求 - .replace(replaceSpaces, ' '); - }); - return doc.body.innerHTML; - } else { - return rawValue - .replace(cleanEmptyTags, '') - .replace(removeBr, '') - .replace(replaceSpaces, ' '); - } - }, - getSafeContent(val) { - const rawValue = val || ''; - const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g; - const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g; + if (rawValue.includes('wordTableHtml')) { + const parser = new DOMParser(); + const doc = parser.parseFromString(rawValue, 'text/html'); + const cells = doc.querySelectorAll('td, th'); - - const escapeIllegalLT = (str) => { - return str.replace(/<(?!(\/?(p|div|span|table|tr|td|th|b|i|strong|em|ul|ol|li|br|img|myh3|myfigure|mytable|wmath)))/gi, '<'); - }; + cells.forEach((cell) => { + cell.innerHTML = cell.innerHTML + .replace(cleanEmptyTags, '') + .replace(removeBr, '') // 针对你“不想要br”的需求 + .replace(replaceSpaces, ' '); + }); + return doc.body.innerHTML; + } else { + return rawValue.replace(cleanEmptyTags, '').replace(removeBr, '').replace(replaceSpaces, ' '); + } + }, + getSafeContent(val) { + const rawValue = val || ''; + const cleanEmptyTags = /<([a-zA-Z1-6]+)\b[^>]*><\/\1>/g; + const replaceSpaces = /\s+(?=<)|(?<=>)\s+/g; - let processedHtml = ''; + const escapeIllegalLT = (str) => { + return str.replace( + /<(?!(\/?(p|div|span|table|tr|td|th|b|i|strong|em|ul|ol|li|br|img|myh3|myfigure|mytable|wmath)))/gi, + '<' + ); + }; - if (rawValue.includes('wordTableHtml')) { - const parser = new DOMParser(); - const doc = parser.parseFromString(rawValue, 'text/html'); - const cells = doc.querySelectorAll('td, th'); + let processedHtml = ''; - cells.forEach((cell) => { - - let cellText = cell.innerHTML; - - cell.innerHTML = cellText - .replace(cleanEmptyTags, '') - .replace(replaceSpaces, ' '); - }); - processedHtml = doc.body.innerHTML; - } else { - - processedHtml = rawValue - .replace(cleanEmptyTags, '') - .replace(replaceSpaces, ' '); - } + if (rawValue.includes('wordTableHtml')) { + const parser = new DOMParser(); + const doc = parser.parseFromString(rawValue, 'text/html'); + const cells = doc.querySelectorAll('td, th'); - return processedHtml; - }, + cells.forEach((cell) => { + let cellText = cell.innerHTML; + cell.innerHTML = cellText.replace(cleanEmptyTags, '').replace(replaceSpaces, ' '); + }); + processedHtml = doc.body.innerHTML; + } else { + processedHtml = rawValue.replace(cleanEmptyTags, '').replace(replaceSpaces, ' '); + } + + return processedHtml; + }, + parseWordLinearTextToStandardLatex(text) { + let result = text; + + // 🌟 动态特征一:修复分式与包裹大括号(如把 13(...) 转换为 \frac{1}{3}\left( ... \right)) + // 通过捕捉连续的两位数字和紧跟的括号,不管它是 12 还是 13 还是 25,动态拆分为分子分母 + result = result.replace(/^(\d)(\d)\s*\(([\s\S]+)\)$/g, '\\frac{$1}{$2}\\left( $3 \\right)'); + + // 🌟 动态特征二:学术公式多变量复合下标高精度自动恢复(如 MIi 变成 \widehat{\text{MI}}_i) + // 抽象规律:大写字母组成的复合单词(代表一个统计变量),后面紧跟着一个用于做索引循环的小写字母(如 i, j, k, n) + // 我们用正则边界 \b 精准识别这种大小写交替的数学边界,动态完成 \text 包裹和 _ 下标追加 + result = result.replace(/\b([A-Z]{2,})([ijkmn])\b/g, '\\widehat{\\text{$1 exterior_flag}}_$2'); + result = result.replace(/ exterior_flag/g, ''); // 清理临时标记 + + // 🌟 动态特征三:单字母变量的帽子与下标高精度自动恢复(如 Fi 变成 \widehat{\text{F}}_i) + // 抽象规律:单个大写字母后面紧跟单个小写字母索引。同时通过排除机制 (?!SGMS) 确保左边的复合变量不被错误套上帽子 + result = result.replace(/\b(?!SGMS)([A-Z])([ijkmn\d])\b/g, '\\widehat{\\text{$1 single_flag}}_$2'); + result = result.replace(/ single_flag/g, ''); + + // 🌟 动态特征四:左侧纯主变量的普通下标处理(如 SGMSi 变成 \text{SGMS}_i,不需要加帽子) + // 匹配任何在等号左侧或独立区域的、不需要戴帽子的复合纯文本下标变量 + result = result.replace(/\b([A-Z]{3,})([ijkmn\d])\b/g, '\\text{$1}_$2'); + + // 🌟 动态特征五:基础数学连字符规范化 + result = result.replace(/\s*\*\s*/g, ' \\cdot '); + + return result; + }, initTinymce() { - var _this = this; window.tinymce.init({ ..._this.tinymceOtherInit, @@ -765,6 +783,56 @@ export default { ed.on('paste', async (event) => { const rtf = event.clipboardData.getData('text/rtf'); + console.log('🚀 ~ setup ~ rtf:', rtf); + + let plainText = event.clipboardData.getData('text/plain') || ''; + + // ======================================================== + // 1. 【通用公式内核判定】—— 绝不绑定任何具体公式的字母 + // ======================================================== + // 只要 RTF 中包含微软 Office 原生公式对象标记(objdata / mmath / object), + // 并且纯文本里有数学等号,说明当前用户复制的 100% 是一个公式对象,而不是普通插图! + const isWordFormulaObject = + rtf.includes('\\rtf') && + (rtf.includes('objdata') || rtf.includes('\\mmath') || rtf.includes('\\object')) && + plainText.includes('='); + + if (isWordFormulaObject) { + // 【第一步:绝对截胡断流】 + // 强行扼杀浏览器的默认粘贴行为,并阻止事件冒泡。 + // 这会使下方原本属于普通插图的图片转换、接口上传、绿色进度条等代码直接被彻底绕过! + event.preventDefault(); + event.stopPropagation(); + + let finalLatex = ''; + + // 如果用户复制时,纯文本里已经带有标准 LaTeX 控制符了(比如 \frac, \hat 等) + if (/\\frac|\\hat|\\sqrt|\\alpha/i.test(plainText)) { + finalLatex = plainText.trim(); + } + // 【核心动态翻译引擎】如果拿到的只是被 Word 阉割后的线性纯文本, + // 我们基于通用的“数学结构特征”进行全自动高保真翻译,不写死任何具体的变量名! + else if (plainText.trim()) { + finalLatex = parseWordLinearTextToStandardLatex(plainText.trim()); + } + + // ======================================================== + // 【高保真 LaTeX 公式注入】 + // ======================================================== + if (finalLatex) { + // 清洗掉首尾可能重复的 $ 符号 + finalLatex = finalLatex.replace(/^(\$\$?)|(\$\$?)$/g, '').trim(); + + // 包装成标准的块级公式 + const latexContainer = `$$${finalLatex}$$`; + + ed.insertContent(latexContainer); + console.log('【通用架构级公式转换成功】:', latexContainer); + } + + return; // 【绝杀】直接中断整个 paste 函数,下面你原有的图片代码直接气化,绝不触发上传! + } + if (rtf && rtf.includes('\\pict')) { const extracted = extractHexImagesFromRTF(rtf); _this.totalUploadImages = extracted.length; // 设置总数 @@ -830,40 +898,36 @@ export default { } }); ed.on('init', function () { - _this.editorInstance = ed; - _this.hasInit = true; - _this.$commonJS.inTinymceButtonClass(); - if (_this.isAutomaticUpdate) { - _this.$emit('updateChange', _this.value); + _this.editorInstance = ed; + _this.hasInit = true; + _this.$commonJS.inTinymceButtonClass(); + if (_this.isAutomaticUpdate) { + _this.$emit('updateChange', _this.value); + } + _this.content = _this.getSafeContent(_this.value); + _this.handleSetContent(_this.content || ''); - } - _this.content = _this.getSafeContent(_this.value); + // 3. 监听内容变化 + ed.on('NodeChange Change KeyUp SetContent', () => { + _this.hasChange = true; + _this.$emit('input', ed.getContent({ format: 'raw' })); + }); - _this.handleSetContent(_this.content || ''); + // 4. 监听 DOM 变化 + const observer = new MutationObserver(() => { + const currentContent = ed.getContent({ format: 'raw' }); + if (_this.isAutomaticUpdate) { + _this.$emit('updateChange', currentContent); + } + }); - // 3. 监听内容变化 - ed.on('NodeChange Change KeyUp SetContent', () => { - _this.hasChange = true; - _this.$emit('input', ed.getContent({ format: 'raw' })); - }); - - // 4. 监听 DOM 变化 - const observer = new MutationObserver(() => { - const currentContent = ed.getContent({ format: 'raw' }); - if (_this.isAutomaticUpdate) { - _this.$emit('updateChange', currentContent); - - - } - }); - - observer.observe(ed.getBody(), { - childList: true, - subtree: true, - characterData: true - }); -}); + observer.observe(ed.getBody(), { + childList: true, + subtree: true, + characterData: true + }); + }); // 定义自定义按钮 ed.ui.registry.addButton('customButtonExportWord', { @@ -921,7 +985,6 @@ export default { if (tempDiv.querySelector('table')) { if (_this.type == 'table') { - _this.$commonJS.parseTableToArray(content, (tableList) => { var contentHtml = `
@@ -951,7 +1014,7 @@ export default { container.innerHTML = contentHtml; args.content = container.innerHTML; // 更新处理后的内容 }); - } + } } else { const plainText = (tempDiv.textContent || tempDiv.innerText || '').trim(); const builtPlain = _this.buildWmathHtmlFromLatexText(plainText); diff --git a/src/components/page/components/email/CkeditorMail.vue b/src/components/page/components/email/CkeditorMail.vue index 67e2936..dec3f89 100644 --- a/src/components/page/components/email/CkeditorMail.vue +++ b/src/components/page/components/email/CkeditorMail.vue @@ -198,7 +198,7 @@ prepareContentForEditor(rawHtml) { const rawHtml = this.editor.getContent(); // 调用我们之前的 autoInlineStyles 方法 return this.autoInlineStyles(rawHtml); - }, + } }, beforeDestroy() { // 销毁编辑器防止内存泄漏 diff --git a/src/components/page/components/email/MailTemplatePreviewDialog.vue b/src/components/page/components/email/MailTemplatePreviewDialog.vue new file mode 100644 index 0000000..385f6af --- /dev/null +++ b/src/components/page/components/email/MailTemplatePreviewDialog.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/src/components/page/components/email/TmrEmailEditor.vue b/src/components/page/components/email/TmrEmailEditor.vue index 2574353..7c07051 100644 --- a/src/components/page/components/email/TmrEmailEditor.vue +++ b/src/components/page/components/email/TmrEmailEditor.vue @@ -46,6 +46,8 @@