diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 4e2c344..1c5892a 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -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; } diff --git a/src/common/js/commonJS.js b/src/common/js/commonJS.js index 6889ecc..d950ff3 100644 --- a/src/common/js/commonJS.js +++ b/src/common/js/commonJS.js @@ -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 = - '' + + '' + ''; @@ -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 = `
@@ -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); } diff --git a/src/components/common/langs/en.js b/src/components/common/langs/en.js index b08797b..2ae466b 100644 --- a/src/components/common/langs/en.js +++ b/src/components/common/langs/en.js @@ -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', diff --git a/src/components/common/langs/zh.js b/src/components/common/langs/zh.js index 25d1c1f..d53ff6c 100644 --- a/src/components/common/langs/zh.js +++ b/src/components/common/langs/zh.js @@ -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:'多列', diff --git a/src/components/page/GenerateCharts.vue b/src/components/page/GenerateCharts.vue index 8361481..bf75d3f 100644 --- a/src/components/page/GenerateCharts.vue +++ b/src/components/page/GenerateCharts.vue @@ -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" >