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 = `
{{ $t('tmrEmailEditor.previewWithVariablesHint') }}
+ + +