数字公式优化

This commit is contained in:
2026-06-01 17:59:22 +08:00
parent 4b1a10b20c
commit 08aa3327db
13 changed files with 2526 additions and 89 deletions

View File

@@ -1675,10 +1675,10 @@ wmath {
.tinymce-inline-math-footer {
display: flex;
flex-wrap: nowrap;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 10px;
gap: 8px 10px;
padding: 6px 10px 8px;
background: #fff;
}

View File

@@ -30,6 +30,19 @@ const replaceNegativeSign = function (text) {
};
let activeInlineMathOverlay = null;
let mathEditorVue = null;
function bindMathEditorI18n(vueInstance) {
mathEditorVue = vueInstance || null;
}
function tCommonMath(key, fallback) {
if (mathEditorVue && typeof mathEditorVue.$t === 'function') {
const translated = mathEditorVue.$t(key);
if (translated && translated !== key) return translated;
}
return fallback;
}
function escapeLatexForWmathAttr(latex) {
return String(latex || '')
@@ -113,8 +126,10 @@ function normalizeWmathWrapMode(wrap) {
}
function getOppositeWrapLabel(wrapMode) {
const target = wrapMode === 'inline' ? 'Text above and below' : 'Inline with text';
return 'Change to ' + target;
if (wrapMode === 'inline') {
return tCommonMath('commonTable.latexMathWrapToBlock', 'Change to block display');
}
return tCommonMath('commonTable.latexMathWrapToInline', 'Change to inline with text');
}
function clearEditorMathSelection(ed) {
@@ -179,7 +194,9 @@ function showWmathContextMenu(ed, wmathElement, event) {
menu.id = 'tinymce-wmath-context-menu';
menu.className = 'tinymce-wmath-context-menu';
menu.innerHTML =
'<button type="button" class="tinymce-wmath-context-item" data-action="copy">Copy LaTeX code</button>' +
'<button type="button" class="tinymce-wmath-context-item" data-action="copy">' +
tCommonMath('commonTable.latexMathCopyCode', 'Copy LaTeX code') +
'</button>' +
'<button type="button" class="tinymce-wmath-context-item" data-action="wrap">' +
wrapLabel +
'</button>';
@@ -229,6 +246,69 @@ function openInlineMathEditor(ed, options) {
}
}
/** 将编辑器内元素的 getBoundingClientRect 转为页面视口坐标(兼容 inline / iframe */
function getElementViewportRect(element) {
if (!element) return null;
const rect = element.getBoundingClientRect();
const doc = element.ownerDocument;
const win = doc && doc.defaultView;
if (!win || win === window) {
return rect;
}
const frameEl = win.frameElement;
if (!frameEl) return rect;
const frameRect = frameEl.getBoundingClientRect();
return {
top: frameRect.top + rect.top,
left: frameRect.left + rect.left,
bottom: frameRect.top + rect.bottom,
right: frameRect.left + rect.right,
width: rect.width,
height: rect.height
};
}
/** 将行内公式面板固定在视口内(表格/宽文档右侧单元格也能完整操作) */
function positionInlineMathOverlay(overlay, anchorRect) {
if (!overlay || !anchorRect) return;
const margin = 12;
const gap = 6;
const vw = window.innerWidth || document.documentElement.clientWidth;
const vh = window.innerHeight || document.documentElement.clientHeight;
overlay.style.display = 'block';
overlay.style.right = 'auto';
overlay.style.bottom = 'auto';
overlay.style.maxWidth = Math.max(280, vw - margin * 2) + 'px';
const panelRect = overlay.getBoundingClientRect();
const panelW = panelRect.width || 380;
const panelH = panelRect.height || 160;
let top = anchorRect.bottom + gap;
if (top + panelH + margin > vh) {
top = anchorRect.top - panelH - gap;
}
if (top < margin) {
top = Math.max(margin, Math.min(anchorRect.top, vh - panelH - margin));
}
if (top + panelH + margin > vh) {
top = Math.max(margin, vh - panelH - margin);
}
let left = anchorRect.left;
if (left + panelW + margin > vw) {
left = vw - panelW - margin;
}
if (left < margin) {
left = margin;
}
overlay.style.top = Math.round(top) + 'px';
overlay.style.left = Math.round(left) + 'px';
}
function openInlineMathEditorCore(ed, options) {
const opts = options || {};
const mode = opts.mode === 'edit' ? 'edit' : 'insert';
@@ -236,9 +316,8 @@ function openInlineMathEditorCore(ed, options) {
closeInlineMathOverlay();
const iframe = ed.iframeElement;
const iframeDoc = ed.getDoc();
if (!iframe || !iframeDoc) return;
if (!iframeDoc) return;
const placeholderId = 'math-ph-' + Date.now();
let uid = 'wmath-' + Math.random().toString(36).substr(2, 9);
@@ -254,7 +333,7 @@ function openInlineMathEditorCore(ed, options) {
initialLatex = parseLatexFromWmath(wmathElement);
preservedWrapMode = wmathElement.getAttribute('data-wrap') || null;
originalWmathHtml = stripMceSelectedAttr(wmathElement.outerHTML);
anchorRect = wmathElement.getBoundingClientRect();
anchorRect = getElementViewportRect(wmathElement);
const phWidth = Math.max(Math.round(anchorRect.width), 4);
const phHeight = Math.max(Math.round(anchorRect.height), 4);
ed.dom.setOuterHTML(
@@ -272,12 +351,9 @@ function openInlineMathEditorCore(ed, options) {
currentWrapMode = normalizeWmathWrapMode(preservedWrapMode);
const phRect = placeholder.getBoundingClientRect();
const iframeRect = iframe.getBoundingClientRect();
const anchor = anchorRect || phRect;
const overlayTop = iframeRect.top + anchor.top;
const overlayLeft = iframeRect.left + anchor.left;
const overlayMinWidth = Math.min(Math.max(anchor.width, phRect.width, 360), 560);
const phRect = getElementViewportRect(placeholder);
const anchorViewportRect = anchorRect || phRect;
const overlayMinWidth = Math.min(Math.max(anchorViewportRect.width, phRect.width, 320), 520);
let overlay = document.getElementById('tinymce-inline-math-overlay');
if (!overlay) {
@@ -287,23 +363,24 @@ function openInlineMathEditorCore(ed, options) {
document.body.appendChild(overlay);
}
overlay.style.display = 'block';
overlay.style.top = overlayTop + 'px';
overlay.style.left = overlayLeft + 'px';
overlay.style.minWidth = overlayMinWidth + 'px';
const copyLabel = tCommonMath('commonTable.latexMathCopyCode', 'Copy LaTeX code');
const cancelLabel = tCommonMath('commonTable.latexDataCancel', 'Cancel');
const okLabel = tCommonMath('commonTable.latexMathConfirm', 'OK');
overlay.innerHTML = `
<div class="tinymce-inline-math-panel">
<div class="tinymce-inline-math-field-wrap"></div>
<div class="tinymce-inline-math-footer">
<div class="tinymce-inline-math-tools">
<button type="button" class="tinymce-inline-math-copy">Copy LaTeX code</button>
<button type="button" class="tinymce-inline-math-copy">${copyLabel}</button>
<span class="tinymce-inline-math-tool-divider"></span>
<button type="button" class="tinymce-inline-math-wrap-toggle"></button>
</div>
<div class="tinymce-inline-math-actions">
<button type="button" class="tinymce-inline-math-cancel">Cancel</button>
<button type="button" class="tinymce-inline-math-confirm">OK</button>
<button type="button" class="tinymce-inline-math-cancel">${cancelLabel}</button>
<button type="button" class="tinymce-inline-math-confirm">${okLabel}</button>
</div>
</div>
</div>
@@ -324,12 +401,6 @@ function openInlineMathEditorCore(ed, options) {
e.preventDefault();
e.stopPropagation();
});
wrapToggleBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
currentWrapMode = currentWrapMode === 'inline' ? 'block' : 'inline';
updateWrapToggleLabel();
});
copyBtn.addEventListener('mousedown', (e) => {
e.preventDefault();
@@ -343,10 +414,10 @@ function openInlineMathEditorCore(ed, options) {
e.preventDefault();
e.stopPropagation();
copyTextToClipboard(mf.value, () => {
copyBtn.textContent = 'Copied!';
copyBtn.textContent = tCommonMath('commonTable.latexMathCopied', 'Copied');
copyBtn.classList.add('is-copied');
setTimeout(() => {
copyBtn.textContent = 'Copy LaTeX code';
copyBtn.textContent = copyLabel;
copyBtn.classList.remove('is-copied');
}, 2000);
});
@@ -357,7 +428,10 @@ function openInlineMathEditorCore(ed, options) {
if (mfInitialized) return;
mfInitialized = true;
mf.virtualKeyboardMode = 'onfocus';
mf.placeholder = 'Type formula here.';
mf.placeholder = tCommonMath(
'commonTable.latexMathPlaceholder',
'Enter LaTeX code (e.g., \\frac{a}{b}, x^{2}, \\sqrt{x})'
);
mf.menuItems = [];
try {
if (initialLatex) {
@@ -377,6 +451,16 @@ function openInlineMathEditorCore(ed, options) {
});
setTimeout(initMathfield, 0);
const repositionOverlay = () => {
const livePh = ed.dom.get(placeholderId);
const rect = livePh ? getElementViewportRect(livePh) : anchorViewportRect;
positionInlineMathOverlay(overlay, rect);
};
repositionOverlay();
requestAnimationFrame(repositionOverlay);
window.addEventListener('scroll', repositionOverlay, true);
window.addEventListener('resize', repositionOverlay);
let finalized = false;
let ignoreOutsideClick = true;
const openedAt = Date.now();
@@ -385,6 +469,8 @@ function openInlineMathEditorCore(ed, options) {
document.removeEventListener('mousedown', onDocMouseDown, true);
iframeDoc.removeEventListener('mousedown', onIframeMouseDown, true);
mf.removeEventListener('blur', onMfBlur);
window.removeEventListener('scroll', repositionOverlay, true);
window.removeEventListener('resize', repositionOverlay);
};
const canCancelOutside = () => !ignoreOutsideClick && Date.now() - openedAt > 400;
@@ -467,6 +553,16 @@ function openInlineMathEditorCore(ed, options) {
finalize(false);
});
wrapToggleBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
currentWrapMode = currentWrapMode === 'inline' ? 'block' : 'inline';
updateWrapToggleLabel();
const latex = (mf.value || '').trim() || initialLatex;
if (!latex) return;
finalize(true);
});
activeInlineMathOverlay = {
overlay,
iframeDoc,
@@ -491,12 +587,25 @@ function openInlineMathEditorCore(ed, options) {
}
function insertInlineMathWithMathlive(ed) {
if (activeInlineMathOverlay) {
const mf = activeInlineMathOverlay.mf;
if (mf && typeof mf.focus === 'function') {
mf.focus();
}
return;
}
openInlineMathEditor(ed, { mode: 'insert' });
}
function editInlineMathWithMathlive(ed, wmathElement) {
if (!wmathElement) return;
if (activeInlineMathOverlay) return;
if (activeInlineMathOverlay) {
const mf = activeInlineMathOverlay.mf;
if (mf && typeof mf.focus === 'function') {
mf.focus();
}
return;
}
openInlineMathEditor(ed, { mode: 'edit', wmathElement });
}
@@ -2347,6 +2456,7 @@ str = str.replace(regex, function (match, content, offset, fullString) {
showWmathContextMenu(ed, wmathElement, event);
},
initEditorButton(vueInstance, ed) {
bindMathEditorI18n(vueInstance);
ed.ui.registry.addMenuButton('customDropdown', {
text: 'Set Title', // 下拉框标题
@@ -2726,7 +2836,9 @@ str = str.replace(regex, function (match, content, offset, fullString) {
// 行内公式编辑MathLive类似 Word「在此处键入公式」
ed.ui.registry.addButton('LateX', {
text: 'LateX',
tooltip: 'Type formula here',
tooltip: vueInstance.$t
? vueInstance.$t('commonTable.latexMathEditorTooltip')
: 'Insert or edit a LaTeX formula',
onAction: function () {
insertInlineMathWithMathlive(ed);
}

View File

@@ -1083,6 +1083,33 @@ const en = {
importWordMathLoading: 'Importing Word...',
importWordMathSuccess: 'Word imported. Formulas converted to LaTeX.',
importWordMathFail: 'Word import failed. Please check the file format.',
importWordMathNoFormula: 'No math formulas found in the Word file.',
importWordMathNoNew: 'All formulas already exist. No new rows added.',
importWordMathParsed: 'Word parsed: {parsed} formula(s) found, {added} added.',
importWordMathParsedNone: 'Word parsed: {parsed} formula(s) found; all already exist.',
mathFormula: 'Latex',
mathFormulaMore: 'more formulas',
latexDataCopy: 'Copy',
latexDataCopied: 'Copied',
latexDataClickToCopy: 'Click a formula to copy',
latexDataAdd: 'Add',
latexDataEdit: 'Edit',
latexDataDelete: 'Delete',
latexDataDeleteAll: 'Clear all',
latexDataDeleteAllConfirm: 'Clear all formulas?',
latexDataEmpty: 'No formulas yet. Upload Word or add manually.',
latexDataNoFormulaDetected: 'No math formulas detected.',
latexDataPlaceholder: 'Enter LaTeX formula',
latexMathPlaceholder: 'Enter LaTeX code (e.g., \\frac{a}{b}, x^{2}, \\sqrt{x})',
latexMathCopyCode: 'Copy LaTeX code',
latexMathCopied: 'Copied',
latexMathWrapToInline: 'Change to inline with text',
latexMathWrapToBlock: 'Change to block display',
latexMathEditorTooltip: 'Insert or edit a LaTeX formula',
latexMathConfirm: 'OK',
latexDataCancel: 'Cancel',
latexDataOk: 'Save',
editMathFormulaSuccess: 'Math formulas updated successfully.',
selectOne: 'Please select only a single paragraph',
alreadyCommented: 'There are already annotations in the text, please select again!',
Multicolumn: 'Multicolumn',

View File

@@ -1069,6 +1069,33 @@ const zh = {
importWordMathLoading: '正在导入 Word...',
importWordMathSuccess: 'Word 导入成功,公式已转为 LaTeX',
importWordMathFail: 'Word 导入失败,请检查文件格式',
importWordMathNoFormula: '未在 Word 中识别到数学公式',
importWordMathNoNew: '公式已存在,未新增重复项',
importWordMathParsed: 'Word 解析完成:识别 {parsed} 个公式,新增 {added} 个',
importWordMathParsedNone: 'Word 解析完成:识别 {parsed} 个公式,均已存在,未新增',
mathFormula: 'Latex',
mathFormulaMore: '条公式',
latexDataCopy: '复制',
latexDataCopied: '已复制',
latexDataClickToCopy: '点击数字公式即可复制',
latexDataAdd: '新增',
latexDataEdit: '修改',
latexDataDelete: '删除',
latexDataDeleteAll: '清空',
latexDataDeleteAllConfirm: '确定清空全部公式吗?',
latexDataEmpty: '暂无公式,可上传 Word 或手动新增',
latexDataNoFormulaDetected: '暂未检测到数字公式',
latexDataPlaceholder: '输入 LaTeX 公式',
latexMathPlaceholder: '请输入 LaTeX 代码(如 \\frac{a}{b}、x^{2}、\\sqrt{x}',
latexMathCopyCode: '复制 LaTeX 代码',
latexMathCopied: '已复制',
latexMathWrapToInline: '改为行内公式',
latexMathWrapToBlock: '改为独立成行',
latexMathEditorTooltip: '插入或编辑 LaTeX 公式',
latexMathConfirm: '确定',
latexDataCancel: '取消',
latexDataOk: '保存',
editMathFormulaSuccess: '数字公式更新成功',
selectOne:'请只勾选单个段落!',
alreadyCommented:'文本中已有批注内容请重新选择',
Multicolumn:'多列',

View File

@@ -64,12 +64,14 @@
@onAddComment="onAddComment"
@addImage="handleImageAdd"
@addTable="handleTableAdd"
@importWordMath="handleImportWordMath"
@handlePaperclip="handlePaperclip"
@addComment="addCommentSetting"
@goToComment="goToComment"
@edit="handleFigureAndTableEdit"
@delete="handleFigureAndTableDelete"
@goToListComment="goToListComment"
@mathFormulasChange="onMathFormulasChange"
style="width: 100%; height: 100%; padding: 0 0px; box-sizing: border-box; background-color: #fff"
>
<template slot="catalogue1">
@@ -96,6 +98,7 @@
:contentList="Main_List"
:comments="comments"
:wordStyle="wordStyle"
:mathFormulasList="mathFormulasList"
@onDrop="onDrop"
@onLinkUnbind="handleUnbindLink"
@onLinkConfirm="handleConfirmLink"
@@ -121,6 +124,8 @@
@onEditTitle="onEditTitle"
@onAddRow="onAddRow"
@changeComment="changeComment"
@latexAdded="onLatexAdded"
@latexUpdated="onLatexUpdated"
style="width: calc(100%); height: calc(100%)"
:style="`100%`"
>
@@ -417,7 +422,7 @@
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editVisible = false"> Cancel </el-button>
<el-button @click="addContentVisible = false"> Cancel </el-button>
<el-button type="primary" @click="handleSaveAddContent">
<!-- <i class="el-icon-finished" style="margin-right: 5px"></i> -->
Save
@@ -434,6 +439,7 @@ import bus from '@/components/common/bus';
import { del, isShallow } from 'vue';
import Tiff from 'tiff.js';
import { mediaUrl } from '@/common/js/commonJS.js'; // 引入通用逻辑
import { LATEX_DATA_HTML_DATA, MATH_FORMULA_TABLE_TITLE } from '@/utils/mathFormulaModule';
import Tinymce from '@/components/page/components/Tinymce';
import bottomTinymce from '@/components/page/components/Tinymce';
import catalogue from '@/components/page/components/table/catalogue.vue';
@@ -455,6 +461,7 @@ export default {
editVisible: false,
currentId: null,
ManuscirptContent: [],
mathFormulasList: [],
articleId: this.$route.query.id,
isShowComment: false,
urlList: {
@@ -1526,6 +1533,54 @@ export default {
this.lineStyle.visiTitle = 'Add Table';
this.threeVisible = true;
},
onMathFormulasChange(payload) {
this.mathFormulasList = (payload && payload.list) || [];
},
onLatexAdded(data) {
if (this.$refs.commonWordHtmlTypeSetting) {
this.$refs.commonWordHtmlTypeSetting.onMathFormulaAdded(data);
}
},
onLatexUpdated(data) {
if (this.$refs.commonWordHtmlTypeSetting) {
this.$refs.commonWordHtmlTypeSetting.onMathFormulaUpdated(data);
}
},
async handleImportWordMath(file) {
if (!file) return;
const loading = this.$loading({
lock: true,
text: this.$t('commonTable.importWordMathLoading'),
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
try {
const html = await this.$commonJS.importWordDocumentWithMath(file);
const { tableData } = this.$commonJS.extractWmathTableDataFromHtml(html);
if (!tableData || tableData.length === 0) {
this.$message.warning(this.$t('commonTable.importWordMathNoFormula'));
return;
}
const res = await this.$api.post(this.urlList.addTable, {
article_id: this.articleId,
table_data: JSON.stringify(tableData),
html_data: LATEX_DATA_HTML_DATA,
note: '',
title: MATH_FORMULA_TABLE_TITLE
});
if (res.code == 0) {
this.$message.success(this.$t('commonTable.importWordMathSuccess'));
this.$refs.commonWordHtmlTypeSetting.refresh('addTable', res.data);
} else {
this.$message.error(res.msg || this.$t('commonTable.importWordMathFail'));
}
} catch (err) {
console.error('handleImportWordMath failed', err);
this.$message.error(this.$t('commonTable.importWordMathFail'));
} finally {
loading.close();
}
},
addUploadWordTable(data) {
this.lineStyle = { note: '', table: data.table_data, html_data: data.html_data };
this.lineStyle1 = { note: '', table: data.table_data, html_data: data.html_data };
@@ -1590,6 +1645,26 @@ export default {
this.$message.error(err.msg);
});
}
if (type == 'math') {
this.$api
.post(this.urlList.deleteTable, {
amt_id: data.amt_id,
article_id: this.articleId
})
.then(async (res) => {
if (res.status == 1) {
loading.close();
this.$refs.commonWordHtmlTypeSetting.replacement('math', data.amt_id);
} else {
loading.close();
this.$message.error(res.msg);
}
})
.catch((err) => {
loading.close();
this.$message.error(err.msg);
});
}
})
.catch((err) => {
// this.$message.error(err.msg);
@@ -1602,19 +1677,17 @@ export default {
this.picStyle = { ...data, extension: extension, picUrl: data.url };
this.picStyle.visiTitle = 'Edit Figure';
this.pictVisible = true;
} else if (type == 'table') {
} else if (type == 'table' || type == 'math') {
this.lineStyle = {};
this.lineStyle1 = {};
// 1. 提取处理逻辑
const formattedData = {
...data,
table: JSON.parse(data.table_data)
table: data.table || (data.table_data ? JSON.parse(data.table_data) : [])
};
// 2. 统一赋值
this.lineStyle = formattedData;
this.lineStyle1 = { ...formattedData }; // 使用浅拷贝确保两个变量指向不同引用(如果需要独立修改)
this.lineStyle.visiTitle = 'Edit Table';
this.lineStyle1 = { ...formattedData };
this.lineStyle.visiTitle = type == 'math' ? 'Edit Math Formula' : 'Edit Table';
this.threeVisible = true;
}
},
@@ -2048,7 +2121,7 @@ export default {
strTitle = strTitle.replace(/<br\s*\/?>/gi, '');
var tableStr=JSON.stringify(cleanedTableList)
if (this.lineStyle.visiTitle == 'Edit Table') {
if (this.lineStyle.visiTitle == 'Edit Table' || this.lineStyle.visiTitle == 'Edit Math Formula') {
this.$api
.post(this.urlList.editTable, {
amt_id: this.lineStyle.amt_id,
@@ -2060,7 +2133,11 @@ export default {
.then((res) => {
if (res.code == 0) {
loading.close();
this.$message.success('Successfully edit Table!');
this.$message.success(
this.lineStyle.visiTitle == 'Edit Math Formula'
? this.$t('commonTable.editMathFormulaSuccess')
: 'Successfully edit Table!'
);
this.threeVisible = false;
setTimeout(() => {
this.$refs.commonWordHtmlTypeSetting.refresh(

View File

@@ -74,11 +74,13 @@
@onAddComment="onAddComment"
@addImage="handleImageAdd"
@addTable="handleTableAdd"
@importWordMath="handleImportWordMath"
@handlePaperclip="handlePaperclip"
@addComment="addCommentSetting"
@goToComment="goToComment"
@edit="handleImageEdit"
@goToListComment="goToListComment"
@mathFormulasChange="onMathFormulasChange"
style="width: 100%; height: 100%; padding: 0 0px; box-sizing: border-box; background-color: #fff"
>
</common-word-html-type-setting>
@@ -105,6 +107,8 @@
:contentList="Main_List"
:comments="comments"
:wordStyle="wordStyle"
:articleId="articleId"
:mathFormulasList="mathFormulasList"
@onDrop="onDrop"
@saveContent="saveContent"
@editComment="editComment"
@@ -129,6 +133,8 @@
@onEditTitle="onEditTitle"
@onAddRow="onAddRow"
@changeComment="changeComment"
@latexAdded="onLatexAdded"
@latexUpdated="onLatexUpdated"
style="width: calc(100%); height: calc(100%)"
:style="`100%`"
>
@@ -410,7 +416,7 @@
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editVisible = false"> Cancel </el-button>
<el-button @click="addContentVisible = false"> Cancel </el-button>
<el-button type="primary" @click="handleSaveAddContent">
<!-- <i class="el-icon-finished" style="margin-right: 5px"></i> -->
Save
@@ -427,6 +433,7 @@ import bus from '@/components/common/bus';
import { del, isShallow } from 'vue';
import Tiff from 'tiff.js';
import { mediaUrl } from '@/common/js/commonJS.js'; // 引入通用逻辑
import { LATEX_DATA_HTML_DATA, MATH_FORMULA_TABLE_TITLE } from '@/utils/mathFormulaModule';
import Tinymce from '@/components/page/components/Tinymce';
import bottomTinymce from '@/components/page/components/Tinymce';
import catalogue from '@/components/page/components/OnlineProofreading/catalogue.vue';
@@ -451,6 +458,7 @@ export default {
editVisible: false,
currentId: null,
ManuscirptContent: [],
mathFormulasList: [],
articleId: this.$route.query.id,
isShowComment: false,
urlList: {
@@ -1354,6 +1362,54 @@ export default {
this.lineStyle.visiTitle = 'Add Table';
this.threeVisible = true;
},
onMathFormulasChange(payload) {
this.mathFormulasList = (payload && payload.list) || [];
},
onLatexAdded(data) {
if (this.$refs.commonWordHtmlTypeSetting) {
this.$refs.commonWordHtmlTypeSetting.onMathFormulaAdded(data);
}
},
onLatexUpdated(data) {
if (this.$refs.commonWordHtmlTypeSetting) {
this.$refs.commonWordHtmlTypeSetting.onMathFormulaUpdated(data);
}
},
async handleImportWordMath(file) {
if (!file) return;
const loading = this.$loading({
lock: true,
text: this.$t('commonTable.importWordMathLoading'),
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
try {
const html = await this.$commonJS.importWordDocumentWithMath(file);
const { tableData } = this.$commonJS.extractWmathTableDataFromHtml(html);
if (!tableData || tableData.length === 0) {
this.$message.warning(this.$t('commonTable.importWordMathNoFormula'));
return;
}
const res = await this.$api.post(this.urlList.addTable, {
article_id: this.articleId,
table_data: JSON.stringify(tableData),
html_data: LATEX_DATA_HTML_DATA,
note: '',
title: MATH_FORMULA_TABLE_TITLE
});
if (res.code == 0) {
this.$message.success(this.$t('commonTable.importWordMathSuccess'));
this.$refs.commonWordHtmlTypeSetting.refresh('addTable', res.data);
} else {
this.$message.error(res.msg || this.$t('commonTable.importWordMathFail'));
}
} catch (err) {
console.error('handleImportWordMath failed', err);
this.$message.error(this.$t('commonTable.importWordMathFail'));
} finally {
loading.close();
}
},
addUploadWordTable(data) {
this.lineStyle = { note: '', table: data.table_data, html_data: data.html_data };
@@ -1367,16 +1423,16 @@ export default {
this.picStyle = { ...data, extension: extension, picUrl: data.url };
this.picStyle.visiTitle = 'Edit Figure';
this.pictVisible = true;
} else if (type == 'table') {
} else if (type == 'table' || type == 'math') {
this.lineStyle = {};
this.lineStyle = {
...data,
table: JSON.parse(data.table_data),
table: data.table || (data.table_data ? JSON.parse(data.table_data) : []),
html_data: data.html_data,
note: data.note,
title: data.title
};
this.lineStyle.visiTitle = 'Edit Table';
this.lineStyle.visiTitle = type == 'math' ? 'Edit Math Formula' : 'Edit Table';
this.threeVisible = true;
}
},
@@ -1757,7 +1813,7 @@ export default {
background: 'rgba(0, 0, 0, 0.7)'
});
if (content && cleanedTableList && cleanedTableList.length > 0) {
if (this.lineStyle.visiTitle == 'Edit Table') {
if (this.lineStyle.visiTitle == 'Edit Table' || this.lineStyle.visiTitle == 'Edit Math Formula') {
this.$api
.post(this.urlList.editTable, {
amt_id: this.lineStyle.amt_id,

View File

@@ -1882,12 +1882,12 @@
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
.el-image {
}
.journal-cover.el-image {
width: 100%;
height: 100%;
}
}
.journal-abbreviation {
font-size: 13px;
color: #606266;

View File

@@ -7,7 +7,24 @@
<div class="table-paper-view" @click.stop>
<div class="close-icon-top" @click="close">×</div>
<div v-if="type === 'table' && processedItem" class="thumbnailTableBox wordTableHtml table_Box pMain">
<div v-if="type === 'table' && processedItem && isLatexDataPreview" class="latex-data-preview">
<div class="tableTitle font">
<span v-html="renderText(processedItem.title)"></span>
</div>
<div
v-for="item in latexFormulaItems"
:key="item.index"
class="latex-preview-row"
>
<span class="latex-preview-index">{{ item.index }}</span>
<span class="latex-preview-content" v-html="item.html"></span>
</div>
<div v-if="processedItem.note" class="tableNote font">
<span v-html="renderText(processedItem.note)"></span>
</div>
</div>
<div v-else-if="type === 'table' && processedItem" class="thumbnailTableBox wordTableHtml table_Box pMain">
<div class="tableTitle font">
<span v-html="renderText(processedItem.title)"></span>
</div>
@@ -58,6 +75,7 @@
<script>
import { TableUtils } from '@/common/js/TableUtils';
import { mediaUrl } from '@/common/js/commonJS.js';
import { isMathFormulaTableTitle, parseFormulaRowsFromTableData } from '@/utils/mathFormulaModule';
export default {
name: 'TablePreviewer',
@@ -70,6 +88,20 @@ export default {
mediaUrl,
};
},
computed: {
isLatexDataPreview() {
return this.processedItem && isMathFormulaTableTitle(this.processedItem.title);
},
latexFormulaItems() {
if (!this.isLatexDataPreview || !this.processedItem) return [];
const raw = this.processedItem.table;
const tableList = typeof raw === 'string' ? JSON.parse(raw) : raw;
return parseFormulaRowsFromTableData(tableList).map((row, index) => ({
index: index + 1,
html: row[0] && row[0].text ? row[0].text : ''
}));
}
},
methods: {
async open(type, item,isNoBg) {
this.visible = true;
@@ -79,16 +111,20 @@ export default {
this.loading = true;
setTimeout(() => {
try {
const processed = this.processTableData(item.table);
this.processedItem = Object.freeze({
...item,
table: {
...item.table,
tableHeader: processed.tableHeader,
tableContent: processed.tableContent,
oddRowIds: isNoBg ? [] : processed.oddRowIds
}
});
if (isMathFormulaTableTitle(item.title)) {
this.processedItem = Object.freeze({ ...item });
} else {
const processed = this.processTableData(item.table);
this.processedItem = Object.freeze({
...item,
table: {
...item.table,
tableHeader: processed.tableHeader,
tableContent: processed.tableContent,
oddRowIds: isNoBg ? [] : processed.oddRowIds
}
});
}
} catch (err) {
console.error("解析表格失败", err);
} finally {
@@ -265,4 +301,45 @@ export default {
display: block;
width: 100%;
}
.latex-data-preview {
width: 100%;
}
.latex-preview-row {
display: flex;
align-items: center;
gap: 10px;
padding: 6px 0;
border-bottom: 1px solid #f0f2f5;
}
.latex-preview-row:last-child {
border-bottom: none;
}
.latex-preview-index {
flex-shrink: 0;
width: 24px;
text-align: right;
font-size: 14px;
color: #909399;
}
.latex-preview-content {
flex: 1;
min-width: 0;
font-size: 16px;
line-height: 1.4;
}
.latex-preview-content ::v-deep wmath {
display: inline !important;
width: auto !important;
}
.latex-preview-content ::v-deep mjx-container {
font-size: 16px !important;
margin: 0 !important;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -37,6 +37,35 @@
<ul class="operateBox">
<div class="base-border base-padding-all" :style="{ '--br': '1px', '--p': '0 20px' }">
<ul class="HTitleBox" style="border: none">
<li class="latex-toolbar-item">
<el-popover
v-model="latexPopoverVisible"
placement="bottom-start"
width="720"
trigger="manual"
popper-class="latex-toolbar-popover"
append-to-body
@hide="onLatexPopoverHide"
>
<latex-data-panel
ref="latexDataPanel"
:article-id="articleId"
:list="mathFormulasList"
layout="popover"
@added="onLatexAdded"
@updated="onLatexUpdated"
@close-popover="closeLatexPopover"
/>
<span
slot="reference"
class="latex-toolbar-trigger"
@click.stop="toggleLatexPopover"
>
<i class="el-icon-s-data latex-toolbar-icon"></i>
{{ $t('commonTable.mathFormula') }}
</span>
</el-popover>
</li>
<li
@click="addContent"
class="base-font-size base-bg-imp base-padding-all"
@@ -184,8 +213,8 @@
item.is_h1
? 'isTitleH1 pMainH1'
: item.is_h2
? 'isTitleH2'
: item.is_h3
? 'isTitleH2'
: item.is_h3
? 'isTitleH3'
: ''
"
@@ -202,10 +231,11 @@
<div :class="currentId == item.am_id ? 'glowing-border' : ''" style="position: relative">
<div
class="base-bg base-pos"
class="base-bg base-pos paragraph-select-overlay"
:style="{ '--p-r': '0px', '--p-t': '-40px' }"
v-if="currentId == item.am_id"
style="z-index: 100"
@click.stop="onParagraphSelectOverlayClick(item)"
></div>
<div
@@ -1035,9 +1065,12 @@ const toolbar = 'addImageButton ';
import { TableUtils } from '@/common/js/TableUtils';
import { debounce, throttle } from '@/common/js/debounce';
import { tableStyle, commonWordStyle } from '@/utils/tinymceStyles';
import LatexDataPanel from './LatexDataPanel.vue';
export default {
name: 'tinymce',
components: {},
components: {
LatexDataPanel
},
props: {
id: {
type: String
@@ -1098,6 +1131,12 @@ export default {
wordStyle: {
type: String,
default: ''
},
mathFormulasList: {
type: Array,
default() {
return [];
}
}
},
data() {
@@ -1216,7 +1255,10 @@ export default {
displayList: [],
currentTypeText: '',
tinymceId: this.id || 'vue-tinymce-' + +new Date()
tinymceId: this.id || 'vue-tinymce-' + +new Date(),
latexPopoverVisible: false,
_latexPopoverScrollHandler: null,
_latexPopoverOutsideHandler: null
};
},
@@ -1247,6 +1289,17 @@ export default {
}
},
deep: true // 启用深度监听
},
latexPopoverVisible(val) {
if (val) {
this.$nextTick(() => {
this.bindLatexPopoverScrollListener();
setTimeout(() => this.bindLatexPopoverOutsideClick(), 0);
});
} else {
this.unbindLatexPopoverScrollListener();
this.unbindLatexPopoverOutsideClick();
}
}
},
computed: {
@@ -1362,6 +1415,8 @@ export default {
// window.removeEventListener('resize', this.calcMarkers);
if (this.resizeObs) this.resizeObs.disconnect();
if (this.mutObs) this.mutObs.disconnect();
this.unbindLatexPopoverScrollListener();
this.unbindLatexPopoverOutsideClick();
// 页面销毁前清理定时器和事件监听器
},
@@ -2145,6 +2200,62 @@ export default {
return;
}
},
onLatexAdded(data) {
this.$emit('latexAdded', data);
},
onLatexUpdated(data) {
this.$emit('latexUpdated', data);
},
toggleLatexPopover() {
this.latexPopoverVisible = !this.latexPopoverVisible;
},
closeLatexPopover() {
this.latexPopoverVisible = false;
},
onLatexPopoverHide() {
const panel = this.$refs.latexDataPanel;
if (panel && typeof panel.dismissEditing === 'function') {
panel.dismissEditing();
}
},
bindLatexPopoverScrollListener() {
this.unbindLatexPopoverScrollListener();
this._latexPopoverScrollHandler = () => {
if (this.latexPopoverVisible) {
this.closeLatexPopover();
}
};
const scrollEl = this.$refs.scroll;
if (scrollEl) {
scrollEl.addEventListener('scroll', this._latexPopoverScrollHandler, { passive: true });
}
},
unbindLatexPopoverScrollListener() {
if (!this._latexPopoverScrollHandler) return;
const scrollEl = this.$refs.scroll;
if (scrollEl) {
scrollEl.removeEventListener('scroll', this._latexPopoverScrollHandler);
}
this._latexPopoverScrollHandler = null;
},
bindLatexPopoverOutsideClick() {
this.unbindLatexPopoverOutsideClick();
this._latexPopoverOutsideHandler = (e) => {
if (!this.latexPopoverVisible) return;
const target = e.target;
if (!target) return;
if (target.closest && target.closest('.latex-toolbar-trigger')) return;
if (target.closest && target.closest('.latex-toolbar-popover')) return;
if (target.closest && target.closest('.latex-toolbar-item')) return;
this.closeLatexPopover();
};
document.addEventListener('mousedown', this._latexPopoverOutsideHandler, true);
},
unbindLatexPopoverOutsideClick() {
if (!this._latexPopoverOutsideHandler) return;
document.removeEventListener('mousedown', this._latexPopoverOutsideHandler, true);
this._latexPopoverOutsideHandler = null;
},
cacheSelection() {
const selection = window.getSelection();
@@ -2913,6 +3024,12 @@ export default {
}
});
},
onParagraphSelectOverlayClick(item) {
if (this.isPreview) return;
if (item && item.am_id) {
this.$set(this, 'currentId', item.am_id);
}
},
async handleGeneralClick(event, item, index) {
// 1. 如果有原有的 span 点击逻辑,先执行(或根据条件判断)
if (typeof this.onProofSpanClick === 'function') {
@@ -4078,6 +4195,15 @@ export default {
font-size: 20px;
cursor: pointer;
}
.HTitleBox li.latex-toolbar-item {
padding: 2px 10px;
font-size: 12px;
color: #fff;
background-color: rgb(43, 129, 239);
border: none;
border-radius: 4px;
height: 26px;
}
.operateBox {
width: auto;
display: flex;
@@ -4542,6 +4668,52 @@ font-weight: bold !important;
opacity: 1;
}
}
.latex-toolbar-item {
display: inline-flex;
align-items: center;
list-style: none;
padding: 2px 10px;
margin-left: 10px;
border: none;
border-radius: 4px;
background-color: rgb(43, 129, 239);
height: 26px;
}
.latex-toolbar-trigger {
display: inline-flex;
align-items: center;
cursor: pointer;
font-size: 12px;
font-weight: bold;
color: #fff;
line-height: 1.4;
white-space: nowrap;
}
.latex-toolbar-trigger:hover {
opacity: 0.9;
color: #fff;
}
.latex-toolbar-icon {
margin-right: 4px;
font-size: 13px;
color: #fff;
}
</style>
<style>
.latex-toolbar-popover {
padding: 10px 12px !important;
box-sizing: border-box;
min-width: 520px;
height: 400px;
max-height: 400px;
overflow: hidden;
border-radius: 8px !important;
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.12) !important;
}
</style>
<style scoped lang="less">
.glowing-border {
@@ -4555,6 +4727,18 @@ font-weight: bold !important;
.pMain {
cursor: pointer;
}
.paragraph-select-overlay {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
cursor: pointer;
background: transparent;
}
::v-deep wmath[data-wrap='inline'] {
display: inline-block !important;
width: auto !important;

View File

@@ -473,7 +473,8 @@
<script>
import DynamicTable from './DynamicTable.vue';
import { mediaUrl } from '@/common/js/commonJS.js'; // 引入通用逻辑
import { mediaUrl } from '@/common/js/commonJS.js';
import { isMathFormulaTableTitle, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule';
export default {
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
@@ -498,8 +499,10 @@ export default {
],
images: [],
tables: [],
mathFormulas: [],
imagesList: [],
tablesList: [],
mathFormulasList: [],
tablesHtml: [],
imagesHtml: [],
activeNames: ['images', 'tables']
@@ -620,6 +623,12 @@ export default {
addTable() {
this.$emit('addTable');
},
onMathFormulaAdded(data) {
this.refresh('addTable', normalizeLatexTableApiResponse(data));
},
onMathFormulaUpdated(data) {
this.refresh('editTable', normalizeLatexTableApiResponse(data));
},
handlePaperclip() {
this.$emit('handlePaperclip');
},
@@ -638,11 +647,7 @@ export default {
handleSelectMenu(v) {
this.currentMenu = v;
this.currentSelectType = '0';
// if (v == 1) {
// this.getCommentList();
// } else {
this.filterData();
// }
},
selectType(v) {
this.currentSelectType = v.type;
@@ -663,14 +668,37 @@ export default {
}
this.filterData('table');
} else if (type == 'math') {
const indexInList = this.mathFormulas.findIndex((item) => item.amt_id == id);
if (indexInList !== -1) {
this.mathFormulas.splice(indexInList, 1);
}
this.filterData('math');
}
},
isMathFormulaTable(item) {
return item && isMathFormulaTableTitle(item.title);
},
splitTablesAndMathFormulas(list) {
const tables = [];
const mathFormulas = [];
(list || []).forEach((item) => {
if (this.isMathFormulaTable(item)) {
mathFormulas.push(item);
} else {
tables.push(item);
}
});
return { tables, mathFormulas };
},
filterData(type) {
if (type) {
if (type == 'img') {
this.imagesList = [...this.images];
} else if (type == 'table') {
this.tablesList = [...this.tables];
} else if (type == 'math') {
this.mathFormulasList = [...this.mathFormulas];
}
} else {
if (this.currentMenu == 1) {
@@ -689,7 +717,6 @@ export default {
switch (this.currentSelectType) {
case '0':
this.tablesList = [...this.tables];
// console.log('this.tablesList at line 393:', this.tablesList);
break;
case '1':
this.tablesList = [...this.tables].filter((e) => e.has_selected == 1);
@@ -701,6 +728,13 @@ export default {
} else {
}
}
this.mathFormulasList = [...this.mathFormulas];
this.emitMathFormulasChange();
},
emitMathFormulasChange() {
this.$emit('mathFormulasChange', {
list: [...this.mathFormulasList]
});
},
goToListComment(id, type) {
this.$emit('goToListComment', id, type);
@@ -717,15 +751,22 @@ export default {
})
},
async refresh(type, newData) {
const normalized = normalizeLatexTableApiResponse(newData || {});
var tableIndex;
var mathIndex;
var imgIndex;
var tableData
if(newData.amt_id ) {
tableIndex = this.tables.findIndex((table) => table.amt_id == newData.amt_id);
tableData = newData.table_data?JSON.parse(newData.table_data):[];
var tableData = normalized.table || [];
if (normalized.amt_id) {
mathIndex = this.mathFormulas.findIndex((item) => {
const itemId = item.amt_id || item.ant_id || item.article_table_id;
return itemId != null && String(itemId) === String(normalized.amt_id);
});
if (mathIndex === -1) {
tableIndex = this.tables.findIndex((table) => table.amt_id == normalized.amt_id);
}
}
newData = normalized;
if(newData.ami_id) {
imgIndex = this.images.findIndex((img) => img.ami_id == newData.ami_id);
}
@@ -760,13 +801,38 @@ if(newData.ami_id) {
this.tables.push({
...newData,
article_table_id: newData.amt_id,
table: tableData
});
if (this.isMathFormulaTable(newData)) {
const mathIdx = this.mathFormulas.findIndex(
(item) => item.amt_id == newData.amt_id
);
const payload = {
...newData,
article_table_id: newData.amt_id,
table: tableData
};
if (mathIdx !== -1) {
this.mathFormulas[mathIdx] = payload;
} else if (this.mathFormulas.length > 0) {
this.mathFormulas[0] = payload;
} else {
this.mathFormulas.push(payload);
}
} else {
this.tables.push({
...newData,
article_table_id: newData.amt_id,
table: tableData
});
}
break;
case 'editTable':
if (tableIndex !== -1) {
if (mathIndex !== -1) {
this.mathFormulas[mathIndex] = {
...newData,
article_table_id: newData.amt_id,
table: tableData
};
} else if (tableIndex !== -1) {
this.tables[tableIndex] = {
...newData,
@@ -793,14 +859,18 @@ if(newData.ami_id) {
break;
case 'removeTable':
if (tableIndex !== -1) {
if (mathIndex !== -1) {
this.mathFormulas[mathIndex].has_selected = 0;
} else if (tableIndex !== -1) {
this.tables[tableIndex].has_selected = 0;
}
break;
case 'positioningTable':
if (tableIndex !== -1) {
if (mathIndex !== -1) {
this.mathFormulas[mathIndex].has_selected = 1;
} else if (tableIndex !== -1) {
this.tables[tableIndex].has_selected = 1;
}
@@ -900,8 +970,9 @@ if(newData.ami_id) {
article_id: this.articleId
})
.then(async (res) => {
let parsedList = [];
if (this.urlList) {
this.tables =
parsedList =
res.data.list && res.data.list.length > 0
? res.data.list.map((e) => {
return {
@@ -912,7 +983,7 @@ if(newData.ami_id) {
})
: [];
} else {
this.tables =
parsedList =
res.data.list && res.data.list.length > 0
? res.data.list.map((e) => {
return {
@@ -923,7 +994,11 @@ if(newData.ami_id) {
})
: [];
}
const split = this.splitTablesAndMathFormulas(parsedList);
this.tables = split.tables;
this.mathFormulas = split.mathFormulas;
await this.filterData('table');
await this.filterData('math');
});
}

View File

@@ -0,0 +1,375 @@
import {
importWordDocumentWithMath,
extractWmathTableDataFromHtml,
MATH_FORMULA_TABLE_TITLE,
LEGACY_MATH_FORMULA_TABLE_TITLE,
isMathFormulaTableTitle,
buildWmathCellHtml
} from '@/utils/wordMathImport';
export {
MATH_FORMULA_TABLE_TITLE,
LEGACY_MATH_FORMULA_TABLE_TITLE,
isMathFormulaTableTitle
};
const ADD_MAIN_TABLE_API = 'api/Preaccept/addMainTable';
const EDIT_MAIN_TABLE_API = 'api/Preaccept/editMainTable';
const LATEX_DATA_HEADER = '<b>Latex Data</b>';
/** 清空 Latex Data 时 table_data 传 JSON 空数组字符串 */
export const EMPTY_LATEX_TABLE_DATA = '[]';
/** 数字公式表 html_data 固定为 Latex Data */
export const LATEX_DATA_HTML_DATA = MATH_FORMULA_TABLE_TITLE;
export const EMPTY_LATEX_HTML_DATA = LATEX_DATA_HTML_DATA;
export function buildLatexDataHtmlData() {
return LATEX_DATA_HTML_DATA;
}
export function buildEmptyLatexDataEditPayload(amtId, existingTable) {
return {
amt_id: amtId,
table_data: EMPTY_LATEX_TABLE_DATA,
html_data: EMPTY_LATEX_HTML_DATA,
note: (existingTable && existingTable.note) || '',
title: MATH_FORMULA_TABLE_TITLE
};
}
function getTableAmtId(table) {
if (!table) return null;
return table.amt_id || table.article_table_id || null;
}
/** 从 editMainTable / addMainTable 回参解析 table_data */
export function parseTableDataFromApiField(data) {
if (!data) return [];
if (data.table_data !== undefined && data.table_data !== null && data.table_data !== '') {
const raw = data.table_data;
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : [];
} catch (e) {
return [];
}
}
if (Array.isArray(raw)) return raw;
}
if (Array.isArray(data.table)) return data.table;
return [];
}
/**
* 用接口回显内容同步本地:解析 table_data统一 amt_id兼容 ant_id 拼写)
*/
export function normalizeLatexTableApiResponse(data, existingTable) {
if (!data) return data;
const amtId =
data.amt_id ||
data.ant_id ||
data.article_table_id ||
getTableAmtId(existingTable);
const table = parseTableDataFromApiField(data);
return {
...data,
amt_id: amtId,
article_table_id: amtId,
table,
html_data: data.html_data || buildLatexDataHtmlData(),
title: data.title || MATH_FORMULA_TABLE_TITLE
};
}
/** @deprecated 使用 normalizeLatexTableApiResponse */
export function buildClearedLatexTableResponse(data, existingTable) {
return normalizeLatexTableApiResponse(data, existingTable);
}
function normalizeLatexKey(latex) {
return String(latex || '')
.trim()
.replace(/^\$\$?/, '')
.replace(/\$\$?$/, '')
.replace(/\s+/g, ' ');
}
export function isSameLatexContent(a, b) {
const keyA = normalizeLatexKey(a);
const keyB = normalizeLatexKey(b);
if (!keyA && !keyB) return true;
return keyA === keyB;
}
export function getLatexFromCellHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.innerHTML = String(text);
const wmath = div.querySelector('wmath');
if (wmath) {
return (wmath.getAttribute('data-latex') || wmath.textContent || '').trim();
}
return (div.textContent || text || '').trim();
}
export function getLatexKeyFromCellText(text) {
if (!text) return '';
const div = document.createElement('div');
div.innerHTML = String(text);
const wmath = div.querySelector('wmath');
if (wmath) {
return normalizeLatexKey(wmath.getAttribute('data-latex') || wmath.textContent || '');
}
return normalizeLatexKey(div.textContent || text);
}
export function isLatexDataHeaderRow(row) {
const text = row && row[0] ? row[0].text : '';
return /Latex Data|数字公式/i.test(String(text || ''));
}
export function parseFormulaRowsFromTableData(tableData) {
if (!Array.isArray(tableData)) return [];
return tableData.filter((row) => {
if (!row || !row.length) return false;
const text = row[0] && row[0].text ? row[0].text : '';
if (!text || isLatexDataHeaderRow(row)) return false;
return text.indexOf('wmath') !== -1 || text.indexOf('<wmath') !== -1;
});
}
export function buildLatexDataTableData(formulaRows) {
const rows = formulaRows || [];
if (!rows.length) return [];
return [[{ text: LATEX_DATA_HEADER, colspan: 1, rowspan: 1 }], ...rows];
}
/** 合并公式行incoming 在前,同 key 以先出现的为准(新导入/新数据在列表顶部) */
export function mergeFormulaRows(existingRows, incomingRows) {
const seen = new Set();
const merged = [];
[...(incomingRows || []), ...(existingRows || [])].forEach((row) => {
const key = getLatexKeyFromCellText(row[0] && row[0].text);
if (!key || seen.has(key)) return;
seen.add(key);
merged.push(row);
});
return merged;
}
export function buildHtmlDataFromFormulaRows(formulaRows) {
void formulaRows;
return buildLatexDataHtmlData();
}
export function mergeLatexDataTable(existingTableData, incomingTableData) {
const existingRows = parseFormulaRowsFromTableData(existingTableData);
const incomingRows = parseFormulaRowsFromTableData(incomingTableData);
const mergedRows = mergeFormulaRows(existingRows, incomingRows);
const addedCount = mergedRows.length - existingRows.length;
return {
tableData: buildLatexDataTableData(mergedRows),
html: buildHtmlDataFromFormulaRows(mergedRows),
mergedRows,
addedCount
};
}
export function getLatexDataTableFromList(tables, preferredAmtId) {
const list = (Array.isArray(tables) ? tables : []).filter(
(item) => item && isMathFormulaTableTitle(item.title)
);
if (!list.length) return null;
if (preferredAmtId != null && preferredAmtId !== '') {
const id = String(preferredAmtId);
const hit = list.find((item) => {
const itemId = item.amt_id || item.ant_id || item.article_table_id;
return itemId != null && String(itemId) === id;
});
if (hit) return hit;
}
return list[0];
}
export function buildFormulaItemList(tables, preferredAmtId) {
const tableItem = getLatexDataTableFromList(tables, preferredAmtId);
if (!tableItem) return [];
const tableData = Array.isArray(tableItem.table)
? tableItem.table
: parseTableDataFromApiField(tableItem);
return parseFormulaRowsFromTableData(tableData).map((row, index) => {
const html = row[0] && row[0].text ? row[0].text : '';
return {
index: index + 1,
html,
latex: getLatexFromCellHtml(html)
};
});
}
/** 侧边栏等场景:仅展示一张 Latex Data 表(不按多表合并,避免清空后仍显示其它表的旧公式) */
export function buildMergedLatexDataView(tables, preferredAmtId) {
const tableItem = getLatexDataTableFromList(tables, preferredAmtId);
if (!tableItem) return null;
const tableData = Array.isArray(tableItem.table)
? tableItem.table
: parseTableDataFromApiField(tableItem);
const formulaRows = parseFormulaRowsFromTableData(tableData);
if (!formulaRows.length) {
return {
...tableItem,
table: tableData,
formulaCount: 0
};
}
return {
...tableItem,
table: buildLatexDataTableData(formulaRows),
formulaCount: formulaRows.length
};
}
export async function parseWordMathFormulas(file) {
if (!file) {
throw new Error('No file selected');
}
const html = await importWordDocumentWithMath(file);
if (!html) {
throw new Error('empty content');
}
return extractWmathTableDataFromHtml(html);
}
export function buildMathFormulaTablePayload(articleId, tableData) {
return {
article_id: articleId,
table_data: JSON.stringify(tableData || []),
html_data: buildLatexDataHtmlData(),
note: '',
title: MATH_FORMULA_TABLE_TITLE
};
}
export async function upsertLatexDataTableFromWord(api, articleId, file, existingTable) {
const { tableData: incomingTableData, html: incomingHtml } = await parseWordMathFormulas(file);
const incomingRows = parseFormulaRowsFromTableData(incomingTableData);
const parsedCount = incomingRows.length;
if (!parsedCount) {
return {
empty: true,
allDuplicate: false,
parsedCount: 0,
addedCount: 0,
totalCount: 0,
tableData: [],
html: ''
};
}
const existingTableData = existingTable
? existingTable.table ||
(existingTable.table_data ? JSON.parse(existingTable.table_data) : [])
: [];
const merged = mergeLatexDataTable(existingTableData, incomingTableData);
const amtId = getTableAmtId(existingTable);
const importMeta = {
parsedCount,
addedCount: merged.addedCount,
totalCount: merged.mergedRows.length
};
if (amtId) {
if (merged.addedCount === 0) {
return {
empty: false,
allDuplicate: true,
tableData: merged.tableData,
html: merged.html,
...importMeta
};
}
const res = await api.post(EDIT_MAIN_TABLE_API, {
amt_id: amtId,
table_data: JSON.stringify(merged.tableData),
html_data: buildLatexDataHtmlData(),
note: (existingTable && existingTable.note) || '',
title: MATH_FORMULA_TABLE_TITLE
});
return {
empty: false,
allDuplicate: false,
isUpdate: true,
res,
tableData: merged.tableData,
html: merged.html,
...importMeta
};
}
const payload = buildMathFormulaTablePayload(articleId, merged.tableData);
const res = await api.post(ADD_MAIN_TABLE_API, payload);
return {
empty: false,
allDuplicate: false,
isUpdate: false,
res,
tableData: merged.tableData,
html: merged.html,
...importMeta
};
}
export async function saveLatexDataFormulaRows(api, articleId, existingTable, formulaRows) {
const rows = formulaRows || [];
const amtId = getTableAmtId(existingTable);
if (amtId && !rows.length) {
const res = await api.post(EDIT_MAIN_TABLE_API, buildEmptyLatexDataEditPayload(amtId, existingTable));
return {
res,
tableData: [],
html: EMPTY_LATEX_HTML_DATA,
isUpdate: true,
cleared: true
};
}
const tableData = buildLatexDataTableData(rows);
const html = buildLatexDataHtmlData();
if (amtId) {
const res = await api.post(EDIT_MAIN_TABLE_API, {
amt_id: amtId,
table_data: JSON.stringify(tableData),
html_data: html,
note: (existingTable && existingTable.note) || '',
title: MATH_FORMULA_TABLE_TITLE
});
return { res, tableData, html, isUpdate: true };
}
if (!rows.length) {
return { res: null, tableData: [], html: EMPTY_LATEX_HTML_DATA, isUpdate: false, skipped: true };
}
const payload = buildMathFormulaTablePayload(articleId, tableData);
const res = await api.post(ADD_MAIN_TABLE_API, payload);
return { res, tableData, html, isUpdate: false };
}
export function buildFormulaRowFromLatex(latex) {
const cellHtml = buildWmathCellHtml(latex, 'block');
if (!cellHtml) return null;
return [{ text: cellHtml, colspan: 1, rowspan: 1 }];
}
export async function addMathFormulaTableFromWord(api, articleId, file, existingTable) {
return upsertLatexDataTableFromWord(api, articleId, file, existingTable);
}

View File

@@ -16,6 +16,69 @@ function escapeLatexForAttr(latex) {
.replace(/</g, '&lt;');
}
/** 解码 wmath 标签 data-latex 属性 */
export function decodeLatexFromAttr(value) {
return String(value || '')
.replace(/&quot;/g, '"')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&amp;/g, '&')
.trim();
}
/** 仅从 wmath 的 data-latex 读取公式,无属性则不算公式 */
export function extractLatexFromWmathElement(el) {
if (!el) return '';
const raw = el.getAttribute('data-latex');
if (raw == null || !String(raw).trim()) return '';
return decodeLatexFromAttr(raw);
}
function protectWmathTags(html) {
const placeholders = [];
const safeHtml = String(html).replace(/<wmath\b[\s\S]*?<\/wmath>/gi, (match) => {
const index = placeholders.length;
placeholders.push(match);
return `<!--__WMATH_PLACEHOLDER_${index}__-->`;
});
return { html: safeHtml, placeholders };
}
function restoreWmathPlaceholders(html, placeholders) {
let result = String(html);
placeholders.forEach((original, index) => {
result = result.split(`<!--__WMATH_PLACEHOLDER_${index}__-->`).join(original);
});
return result;
}
/** 规范化 HTML 中已有的 wmath只认 data-latex忽略纯文本内容 */
export function normalizeExistingWmathInHtml(html) {
if (!html) return '';
const div = document.createElement('div');
div.innerHTML = String(html);
div.querySelectorAll('wmath').forEach((el) => {
const latex = extractLatexFromWmathElement(el);
if (!latex) {
el.remove();
return;
}
const wrap = el.getAttribute('data-wrap') || 'block';
const id = el.getAttribute('data-id') || '';
const replacement = buildWmathHtml(latex, wrap, id);
if (!replacement) return;
const temp = document.createElement('div');
temp.innerHTML = replacement;
const node = temp.firstElementChild;
if (node) {
el.replaceWith(node);
}
});
return div.innerHTML;
}
export function createWmathHtml(latex, wrap) {
const raw = String(latex || '').trim();
if (!raw) return '';
@@ -239,10 +302,11 @@ function documentXmlToHtml(documentXml) {
return html;
}
/** 将 HTML 中的 $$...$$ / $...$ 转为 wmath 标签 */
/** 将 HTML 中的 $$...$$ / $...$ 转为 wmath;已有 wmath 仅认 data-latex */
export function parseHtmlToLatex(html) {
if (!html) return '';
let result = String(html);
const { html: safeHtml, placeholders } = protectWmathTags(html);
let result = safeHtml;
result = result.replace(/\$\$([\s\S]+?)\$\$/g, (match, blockFormula) => {
const formula = String(blockFormula || '').trim();
@@ -254,7 +318,8 @@ export function parseHtmlToLatex(html) {
return formula ? `${prefix}${createWmathHtml(formula, 'inline')}` : match;
});
return result;
result = restoreWmathPlaceholders(result, placeholders);
return normalizeExistingWmathInHtml(result);
}
export async function importWordDocumentWithMath(file) {
@@ -276,6 +341,91 @@ export async function importWordDocumentWithMath(file) {
return parseHtmlToLatex(html);
}
export const MATH_FORMULA_TABLE_TITLE = 'Latex Data';
export const LEGACY_MATH_FORMULA_TABLE_TITLE = '数字公式';
export function isMathFormulaTableTitle(title) {
return title === MATH_FORMULA_TABLE_TITLE || title === LEGACY_MATH_FORMULA_TABLE_TITLE;
}
function normalizeWmathLatex(latex) {
let value = String(latex || '').trim();
if (!value) return '';
if (/^\$\$[\s\S]+\$\$$/.test(value) || /^\$[\s\S]+\$$/.test(value)) {
return value;
}
return `$$${value}$$`;
}
function buildWmathHtml(latex, wrap, id) {
const normalized = normalizeWmathLatex(latex);
if (!normalized) return '';
const uid = id || `wmath-${Math.random().toString(36).substr(2, 9)}`;
const mode = wrap === 'inline' || wrap === 'block' ? wrap : 'block';
const safe = escapeLatexForAttr(normalized);
return `<wmath contenteditable="false" data-id="${uid}" data-latex="${safe}" data-wrap="${mode}">${normalized}</wmath>`;
}
export function buildWmathCellHtml(latex, wrap, id) {
return buildWmathHtml(latex, wrap, id);
}
export function getLatexFromCellHtml(html) {
if (!html) return '';
const div = document.createElement('div');
div.innerHTML = String(html);
const wmath = div.querySelector('wmath');
if (wmath) {
const fromAttr = extractLatexFromWmathElement(wmath);
if (fromAttr) return fromAttr;
return (wmath.textContent || '').trim();
}
return (div.textContent || '').trim();
}
/** 将解析后的 HTML 中的 wmath 提取为单个表格 table_data表头 + 公式行) */
export function extractWmathTableDataFromHtml(html) {
if (!html) return { tableData: [], html: '' };
const div = document.createElement('div');
div.innerHTML = String(html);
const formulaRows = [];
const htmlParts = [];
div.querySelectorAll('wmath').forEach((el) => {
const latex = extractLatexFromWmathElement(el);
if (!latex) return;
const wrap = el.getAttribute('data-wrap') || 'block';
const id = el.getAttribute('data-id') || '';
const wmathHtml = buildWmathHtml(latex, wrap, id);
if (!wmathHtml) return;
formulaRows.push([{ text: wmathHtml, colspan: 1, rowspan: 1 }]);
htmlParts.push(wmathHtml);
});
if (!formulaRows.length) {
return { tableData: [], html: '' };
}
return {
tableData: [
[{ text: '<b>Latex Data</b>', colspan: 1, rowspan: 1 }],
...formulaRows
],
html: htmlParts.join('')
};
}
/** @deprecated 保留兼容,请使用 extractWmathTableDataFromHtml */
export function extractWmathTableDataListFromHtml(html) {
const { tableData, html: normalizedHtml } = extractWmathTableDataFromHtml(html);
if (!tableData.length) return [];
const rows = tableData.slice(1);
return rows.map((row) => ({
tableData: [tableData[0], row],
html: row[0] && row[0].text ? row[0].text : ''
}));
}
function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();