数字公式优化
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:'多列',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,6 +111,9 @@ export default {
|
||||
this.loading = true;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (isMathFormulaTableTitle(item.title)) {
|
||||
this.processedItem = Object.freeze({ ...item });
|
||||
} else {
|
||||
const processed = this.processTableData(item.table);
|
||||
this.processedItem = Object.freeze({
|
||||
...item,
|
||||
@@ -89,6 +124,7 @@ export default {
|
||||
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>
|
||||
1277
src/components/page/components/table/LatexDataPanel.vue
Normal file
1277
src/components/page/components/table/LatexDataPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
@@ -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;
|
||||
|
||||
@@ -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,7 +668,28 @@ 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) {
|
||||
@@ -671,6 +697,8 @@ export default {
|
||||
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
|
||||
var tableData = normalized.table || [];
|
||||
|
||||
if(newData.amt_id ) {
|
||||
tableIndex = this.tables.findIndex((table) => table.amt_id == newData.amt_id);
|
||||
tableData = newData.table_data?JSON.parse(newData.table_data):[];
|
||||
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) {
|
||||
|
||||
|
||||
|
||||
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');
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
375
src/utils/mathFormulaModule.js
Normal file
375
src/utils/mathFormulaModule.js
Normal 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);
|
||||
}
|
||||
@@ -16,6 +16,69 @@ function escapeLatexForAttr(latex) {
|
||||
.replace(/</g, '<');
|
||||
}
|
||||
|
||||
/** 解码 wmath 标签 data-latex 属性 */
|
||||
export function decodeLatexFromAttr(value) {
|
||||
return String(value || '')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/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();
|
||||
|
||||
Reference in New Issue
Block a user