diff --git a/npminstall-debug.log b/npminstall-debug.log index 8d0f56e..4dc5b2f 100644 --- a/npminstall-debug.log +++ b/npminstall-debug.log @@ -3,7 +3,14 @@ registry: 'https://registry.npmmirror.com', pkgs: [ { - name: 'spellchecker', + name: 'tinymce', + version: 'latest', + type: 'tag', + alias: undefined, + arg: [Result] + }, + { + name: 'tinymce-kityformula-editor', version: 'latest', type: 'tag', alias: undefined, @@ -15,7 +22,7 @@ cacheDir: 'C:\\Users\\Administrator\\.npminstall_tarball', env: { npm_config_registry: 'https://registry.npmmirror.com', - npm_config_argv: '{"remain":[],"cooked":["--fix-bug-versions","--china","--userconfig=C:\\\\Users\\\\Administrator\\\\.cnpmrc","--disturl=https://cdn.npmmirror.com/binaries/node","--registry=https://registry.npmmirror.com","spellchecker"],"original":["--fix-bug-versions","--china","--userconfig=C:\\\\Users\\\\Administrator\\\\.cnpmrc","--disturl=https://cdn.npmmirror.com/binaries/node","--registry=https://registry.npmmirror.com","spellchecker"]}', + npm_config_argv: '{"remain":[],"cooked":["--fix-bug-versions","--china","--userconfig=C:\\\\Users\\\\Administrator\\\\.cnpmrc","--disturl=https://cdn.npmmirror.com/binaries/node","--registry=https://registry.npmmirror.com","tinymce","tinymce-kityformula-editor"],"original":["--fix-bug-versions","--china","--userconfig=C:\\\\Users\\\\Administrator\\\\.cnpmrc","--disturl=https://cdn.npmmirror.com/binaries/node","--registry=https://registry.npmmirror.com","tinymce","tinymce-kityformula-editor"]}', npm_config_user_agent: 'npminstall/7.12.0 npm/? node/v16.14.1 win32 x64', npm_config_cache: 'C:\\Users\\Administrator\\.npminstall_tarball', NODE: 'C:\\Program Files\\nodejs\\node.exe', @@ -119,7 +126,10 @@ }, fsevents: { host: 'https://cdn.npmmirror.com/binaries/fsevents' }, nodejieba: { host: 'https://cdn.npmmirror.com/binaries/nodejieba' }, - canvas: { host: 'https://cdn.npmmirror.com/binaries/canvas' }, + canvas: { + host: 'https://cdn.npmmirror.com/binaries/canvas', + remote_path: 'v{version}' + }, 'skia-canvas': { host: 'https://cdn.npmmirror.com/binaries/skia-canvas' }, 'flow-bin': { replaceHost: 'https://github.com/facebook/flow/releases/download/v', diff --git a/package.json b/package.json index 4783b06..83f36aa 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "html2canvas": "^1.4.1", "install": "^0.13.0", "jszip": "^3.10.1", + "katex": "^0.16.21", "mammoth": "^1.5.1", + "mathlive": "^0.104.0", "mavon-editor": "^2.6.17", "multi-items-input": "^0.2.0", "pizzip": "^3.1.7", diff --git a/public/index.html b/public/index.html index f588847..b185a6f 100644 --- a/public/index.html +++ b/public/index.html @@ -15,7 +15,8 @@
- + + diff --git a/public/js/global-math.js b/public/js/global-math.js new file mode 100644 index 0000000..9aa76a9 --- /dev/null +++ b/public/js/global-math.js @@ -0,0 +1,108 @@ +// 确保 MathJax 配置正确 +window.MathJax = window.MathJax || { + loader: { + load: ['[tex]/ams', '[tex]/newcommand'] + }, + tex: { + inlineMath: [['$', '$'], ['\\(', '\\)']], + displayMath: [['$$', '$$'], ['\\[', '\\]']] + }, + a11y: { + // 启用 MathJax 可访问性功能,确保使用 aria-label + texHints: true, + screenReader: true, + mathml: { + enable: true, + }, + label: { + // 公式的 aria-label 配置 + enable: true, + texClass: 'MathJax-Label', + form: 'LaTeX' + } + }, + svg: { fontCache: 'global' } +}; + +// **定义全局渲染方法** +window.renderMathJax = function (tinymceId) { + if (window.MathJax && typeof window.MathJax.typesetPromise === "function") { + console.log("正在渲染 MathJax 公式..."); + + // 如果提供了 TinyMCE 编辑器 ID + if (tinymceId) { + const editorInstance = window.tinymce.get(tinymceId); // 根据 ID 获取 TinyMCE 实例 + if (!editorInstance) return; + + const editorBody = editorInstance.getBody(); + + // 获取所有 元素 + const wmathElements = editorBody.querySelectorAll('wmath'); + + if (wmathElements.length > 0) { + wmathElements.forEach((element) => { + const latexContent = element.getAttribute('data-latex'); + if (latexContent) { + // 将公式内容填入标签内部 + element.innerHTML = latexContent; + + // 使用 MathJax 渲染该元素 + window.MathJax.typesetPromise([element]).catch((err) => { + console.warn("TinyMCE MathJax 渲染失败:", err); + }); + } + }); + } + + // 渲染 (MathML)标签,如果有的话 + const mathElements = editorBody.querySelectorAll('math'); + if (mathElements.length > 0) { + window.MathJax.typesetPromise(Array.from(mathElements)).catch((err) => { + console.warn("MathML 渲染失败:", err); + }); + } + } + else { + // 处理全局文档中的 标签 + const wmathElements = document.querySelectorAll('wmath'); + wmathElements.forEach((element) => { + // 检查 标签是否包含有效的 data-latex 属性值 + const latexContent = element.getAttribute('data-latex'); + if (latexContent) { + // 将元素的内部内容替换为 data-latex 的值 + element.innerHTML = latexContent; + + // 渲染 MathJax 公式 + window.MathJax.typesetPromise([element]).catch((err) => { + console.warn("MathJax 渲染失败:", err); + }); + } + }); + + // 处理全局文档中的 标签(MathML 内容) + const mathElements = document.querySelectorAll('math'); + mathElements.forEach((element) => { + // 渲染 MathJax 公式 + window.MathJax.typesetPromise([element]).catch((err) => { + console.warn("MathJax 渲染失败:", err); + }); + }); + } + + } else { + console.warn("MathJax 未正确加载!"); + } +} + + + + + + + + + + + + + diff --git a/src/assets/css/main.css b/src/assets/css/main.css index 7a52444..956a84b 100644 --- a/src/assets/css/main.css +++ b/src/assets/css/main.css @@ -1115,10 +1115,10 @@ a { /* 自动调整列宽 */ text-align: left; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 12px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .wordTableHtml table td, @@ -1131,10 +1131,10 @@ a { word-break: normal; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 12px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .wordTableHtml table tbody tr td { @@ -1160,38 +1160,38 @@ a { align-items: center; margin: 0; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 12px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .wordTableHtml table span { color: #000000; text-align: left !important; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; - mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + font-size: 12px !important; + + line-height: 20px !important; + } .wordTableHtml table .color-highlight { color: rgb(0, 130, 170) !important; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 12px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .wordTableHtml table blue { color: rgb(0, 130, 170) !important; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 12px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .wordTableHtml table tr:first-child td { @@ -1213,7 +1213,7 @@ a { /* 给最后一个 table-header-row(第二行)加样式 */ .wordTableHtml table tr.table-header-row:nth-of-type(2) td { - border-bottom: 1px solid #000 !important; + border-bottom: 1px solid #000 !important; } .word-container b span { @@ -1258,10 +1258,10 @@ a { /* 自动调整列宽 */ text-align: left; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 14px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .word-container table td, @@ -1274,10 +1274,10 @@ a { /* 长单词自动换行 */ word-break: normal; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 14px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .word-container table tbody tr td { @@ -1294,7 +1294,7 @@ a { border-left: 1px dashed #dcdfe6 !important; border-right: 1px dashed #dcdfe6 !important; word-break: keep-all !important; - white-space: pre-wrap !important; + white-space: pre-wrap !important; /* text-align: justify !important; */ } @@ -1303,29 +1303,29 @@ a { margin: 0; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 14px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .word-container table span { color: #000000; text-align: left !important; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 14px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .word-container table .color-highlight { color: rgb(0, 130, 170) !important; font-family: 'Charis SIL' !important; - font-size: 7.5pt !important; + font-size: 14px !important; mso-font-kerning: 1pt !important; - line-height: 10pt !important; - mos-line-height: 10pt !important; + line-height: 20px !important; + mos-line-height: 20px !important; } .word-container table tr:first-child td { @@ -1433,4 +1433,20 @@ a { /* 设置字体颜色 */ text-decoration: line-through !important; /* 设置字体颜色 */ +} + +mjx-container { + font-size: 14px !important; +} + +wmath { + width: 100%; + display: block; + display: flex; +} + +/* 强制 MathLive 虚拟键盘浮在最顶层 */ +.ML__keyboard { + z-index: 99999 !important; + position: fixed !important; } \ No newline at end of file diff --git a/src/assets/img/ai.png b/src/assets/img/ai.png new file mode 100644 index 0000000..cfdc0b6 Binary files /dev/null and b/src/assets/img/ai.png differ diff --git a/src/assets/img/repeat.png b/src/assets/img/repeat.png new file mode 100644 index 0000000..709024b Binary files /dev/null and b/src/assets/img/repeat.png differ diff --git a/src/common/js/commonJS.js b/src/common/js/commonJS.js index 6ff51a2..8ae2e47 100644 --- a/src/common/js/commonJS.js +++ b/src/common/js/commonJS.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import katex from 'katex'; import JSZip from 'jszip'; import Common from '@/components/common/common' import Tiff from 'tiff.js'; @@ -18,16 +19,106 @@ const capitalizeFirstLetter = function (text) { }); }; export default { + extractLatexFromMathJax() { + // 获取所有 MathJax 渲染的公式容器 + const mathContainers = document.querySelectorAll('mjx-container'); + + mathContainers.forEach(container => { + // 查找每个渲染公式对应的 MathML 部分 + const mathElement = container.querySelector('mjx-math'); + console.log('mathElement at line 28:', mathElement) + + if (mathElement) { + // 获取 MathJax 渲染的公式对象 + const jax = window.MathJax.getJaxFor(mathElement); + console.log('jax at line 32:', jax) + + if (jax) { + // 使用 MathJax API 获取公式的 LaTeX 代码 + const latex = jax.getLiteral(); // 获取 LaTeX 表达式 + console.log('提取到的 LaTeX 公式:', latex); // 输出 LaTeX 代码 + } else { + console.warn('MathJax 对象未找到'); + } + } + }); + }, + + + + + replaceWMathContent(inputHtml, callback) { + // 使用正则表达式查找所有 标签,并提取 data-latex 的内容 + var str = inputHtml.replace(/[^<]*<\/wmath>/g, function (match, latexContent) { + // 返回 标签,内容替换为 data-latex 的值 + return `${latexContent}`; + }); + + // 调用回调函数并传递处理后的结果 + callback(str); + + // 输出结果到控制台 + console.log('Processed HTML:', str); + } + + + , + // **解析 MathJax 公式,获取 LaTeX** + async extractMathJaxLatex(cell, callback) { + console.log('cell at line 67:', cell) + return new Promise((resolve, reject) => { + // Step 1: First, process the math content and extract LaTeX from tags + let updatedContent = cell.innerHTML; // Start with the cell's inner HTML + console.log('cell content at the start:', updatedContent); + + // Find all elements + const wmathElements = cell.querySelectorAll('wmath'); + wmathElements.forEach((element) => { + // Get the LaTeX content from the data-latex attribute + const latexContent = element.getAttribute('data-latex'); + console.log('LaTeX content from data-latex:', latexContent); + + // Replace the tag with its LaTeX content wrapped in $$...$$ + updatedContent = updatedContent.replace(element.outerHTML, `${latexContent}`); + }); + + console.log('updatedContent after processing wmath tags:', updatedContent); + + // Step 2: Now extract content without the outer tags + updatedContent = this.extractContentWithoutOuterSpan(updatedContent); + console.log('updatedContent after extractContentWithoutOuterSpan:', updatedContent); + + // Step 3: Call the callback function with the final updated content + // callback(updatedContent); + + // Resolve the promise with the final content + resolve(updatedContent); + }); + } + + + , + renderLatex(latexString) { + return katex.renderToString(latexString, { + throwOnError: false + }); + }, decodeHtml(html) { var txt = document.createElement('textarea'); txt.innerHTML = html; return txt.value; - }, + }, //去掉最外层自定义的span标签 extractContentWithoutOuterSpan(cell) { + console.log('cell at line 90:', cell) var str = '' + if (!cell) { + return '' + } // 获取单元格的 HTML 内容 - let htmlContent = cell.innerHTML.trim(); + let htmlContent = cell.trim(); + + console.log('htmlContent at line 94:', htmlContent) str = this.transformHtmlString(htmlContent) @@ -52,12 +143,12 @@ export default { const regex = /\[(\d+(?:–\d+)?(?:, ?\d+(?:–\d+)?)*)\]/g; str = str.replace(//g, '').replace(/<\/blue>/g, ''); // 先去掉所有的 标签 - + if (regex.test(str)) { - str = str.replace(regex, function(match) { + str = str.replace(regex, function (match) { // 提取出方括号中的内容,并进行匹配 const content = match.slice(1, match.length - 1); // 去掉方括号 - + // 判断是否符合条件,纯数字、逗号后有空格、连字符 if (/^\d+$/.test(content) || /, ?/.test(content) || /–/.test(content)) { return `${match}`; // 如果符合条件则加上蓝色标签 @@ -65,12 +156,13 @@ export default { return match; // 如果不符合条件,则保持原样 }); } - - + + console.log('str at line 141:', str) // 如果没有 标签,直接返回原始 HTML 内容 return str; + }, async extractPastedWordTablesToArrays(pastedHtml, callback) { try { @@ -161,90 +253,140 @@ export default { callback([]); } }, - async extractWordTablesToArrays(file, callback) { - const Zip = new JSZip(); + const namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; try { - // 解压 Word 文档 + const Zip = new JSZip(); const zip = await Zip.loadAsync(file); - const documentXml = await zip.file("word/document.xml").async("string"); - // 解析 XML + console.log("解压后的文件:", Object.keys(zip.files)); + + const documentFile = zip.file("word/document.xml"); + if (!documentFile) { + console.error("❌ 找不到 word/document.xml,无法解析 Word 文件"); + return; + } + + const documentXml = await documentFile.async("string"); const parser = new DOMParser(); const documentDoc = parser.parseFromString(documentXml, "application/xml"); - // 获取命名空间 - const namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; - const numberingXml = await zip.file("word/numbering.xml").async("string"); - let numberingDoc = null; - if (numberingXml) { - numberingDoc = parser.parseFromString(numberingXml, "application/xml"); + console.log("解析的 XML 结构:", new XMLSerializer().serializeToString(documentDoc)); + + const numberingFile = zip.file("word/numbering.xml"); + let numberingMap = {}; + if (numberingFile) { + const numberingXml = await numberingFile.async("string"); + const numberingDoc = parser.parseFromString(numberingXml, "application/xml"); + numberingMap = this.parseNumbering(numberingDoc); + } else { + console.warn("⚠️ word/numbering.xml 不存在,跳过编号解析"); } - // 获取编号定义 - const numberingMap = this.parseNumbering(numberingDoc); - - // 获取所有表格 const tables = documentDoc.getElementsByTagNameNS(namespace, "tbl"); - const allTables = []; // 存储所有表格的二维数组 + const allTables = []; if (!tables || tables.length === 0) { console.warn("未找到表格内容,请检查 XML 结构"); return []; } - for (const table of tables) { - - const prevParagraph = table.previousElementSibling; - if (prevParagraph) { - console.log(`表格前的段落: ${prevParagraph.textContent}`); - } const rows = table.getElementsByTagNameNS(namespace, "tr"); - const tableArray = []; // 当前表格的二维数组 + const tableArray = []; - for (const row of rows) { + let rowSpanMap = []; + + for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) { + const row = rows[rowIndex]; const cells = row.getElementsByTagNameNS(namespace, "tc"); - const rowArray = []; // 当前行的数据 + const rowArray = []; - // 存储已合并的单元格 - let colSpanInfo = []; + if (!rowSpanMap[rowIndex]) { + rowSpanMap[rowIndex] = []; + } + + let cellIndex = 0; for (let i = 0; i < cells.length; i++) { + while (rowSpanMap[rowIndex][cellIndex]) { + rowArray.push(null); + cellIndex++; + } + const cell = cells[i]; let cellText = ""; - // const paragraphs = cell.getElementsByTagNameNS(namespace, "p"); const paragraphs = cell.getElementsByTagName("w:p"); - // 检查合并单元格属性 + const gridSpan = cell.getElementsByTagNameNS(namespace, "gridSpan")[0]; const vMerge = cell.getElementsByTagNameNS(namespace, "vMerge")[0]; - // 获取合并信息 - let colspan = gridSpan ? parseInt(gridSpan.getAttribute("w:val"), 10) : 1; - let rowspan = 1; + var colspan = gridSpan ? parseInt(gridSpan.getAttribute("w:val"), 10) : 1; + var rowspan = 1; + if (vMerge) { + if (vMerge.getAttribute("w:val") === "restart") { + rowspan = 1; // 初始化 rowspan + let nextRowIdx = rowIndex + 1; + let maxRowspan = rows.length - rowIndex; // 确保 rowspan 不会超过剩余行数 - if (vMerge && vMerge.getAttribute("w:val") === "restart") { - rowspan = 2; // 假设合并两行 - } else if (vMerge && !vMerge.getAttribute("w:val")) { - continue; // 如果是合并单元格的继续部分,则跳过 + while (nextRowIdx < rows.length) { + const nextRowCells = rows[nextRowIdx].getElementsByTagNameNS(namespace, "tc"); + // console.log(`🔍 检查下一行单元格 at row ${nextRowIdx}, col ${cellIndex}:`, nextRowCells); + + if (nextRowCells.length > cellIndex) { + const nextCell = nextRowCells[cellIndex]; + + if (!nextCell) { + // console.warn(`⚠️ nextCell 未定义 at row ${nextRowIdx}, col ${cellIndex}`); + break; + } + + const nextVMerge = nextCell.getElementsByTagNameNS(namespace, "vMerge")[0]; + // console.log(`🔍 检查 nextVMerge at row ${nextRowIdx}, col ${cellIndex}:`, nextVMerge); + + // **如果 nextVMerge 为空,则不应继续增长 rowspan** + if (!nextVMerge) { + // console.log(`⚠️ nextVMerge 为空 at row ${nextRowIdx}, col ${cellIndex} - 停止扩展`); + break; + } + + // **解析 nextVMerge 的值** + const vMergeVal = nextVMerge.getAttribute("w:val"); + + if (!vMergeVal || vMergeVal === "continue") { + if (rowspan < maxRowspan) { // 限制 rowspan 最大值 + rowspan++; + // console.log(`✅ rowspan 扩展到: ${rowspan} (row: ${nextRowIdx}, col: ${cellIndex})`); + nextRowIdx++; + } else { + // console.log(`⛔ 最大 rowspan 限制 ${rowspan},在 row ${nextRowIdx} 停止`); + break; + } + } else if (vMergeVal === "restart") { + // console.log(`⛔ 停止 rowspan 扩展 at row ${nextRowIdx}, 因为 w:val="restart"`); + break; + } else { + // console.log(`⚠️ 未知 w:val="${vMergeVal}" at row ${nextRowIdx},停止合并`); + break; + } + } else { + // console.warn(`⚠️ Row ${nextRowIdx} 没有足够的列 cellIndex ${cellIndex}`); + break; + } + } + } else { + continue; + } } - // 处理单元格内容 - // let cellText = ""; - - - // 为当前单元格初始化计数器 + console.log('rowspan at line 265:', rowspan) const currentLevelNumbers = {}; for (const paragraph of paragraphs) { - // 提取段落编号 let listPrefix = ""; const numPr = paragraph.getElementsByTagName("w:numPr")[0]; if (numPr) { - console.log('numPr at line 54:', numPr) const numIdElement = numPr.getElementsByTagName("w:numId")[0]; - console.log('numIdElement at line 56:', numIdElement) const ilvlElement = numPr.getElementsByTagName("w:ilvl")[0]; - console.log('ilvlElement at line 58:', ilvlElement) if (numIdElement && ilvlElement) { const numId = numIdElement.getAttribute("w:val"); const ilvl = ilvlElement.getAttribute("w:val"); @@ -252,7 +394,6 @@ export default { } } - // 初始化当前段落文本 let paragraphText = listPrefix ? `${listPrefix} ` : ""; const runs = paragraph.getElementsByTagName("w:r"); @@ -263,7 +404,6 @@ export default { textContent += text.textContent; } - // 检查格式 const rPr = run.getElementsByTagName("w:rPr")[0]; let formattedText = textContent; @@ -288,90 +428,308 @@ export default { } } - // 替换负号 formattedText = replaceNegativeSign(formattedText); - - // 首字母大写 formattedText = capitalizeFirstLetter(formattedText); - // 添加蓝色标签 - - - const regex = /\[(\d+(?:–\d+)?(?:, ?\d+(?:–\d+)?)*)\]/g; + formattedText = formattedText.replace(//g, '').replace(/<\/blue>/g, ''); - formattedText = formattedText.replace(//g, '').replace(/<\/blue>/g, ''); // 先去掉所有的 标签 - if (regex.test(formattedText)) { - formattedText = formattedText.replace(regex, function(match) { - // 提取出方括号中的内容,并进行匹配 - const content = match.slice(1, match.length - 1); // 去掉方括号 - - // 判断是否符合条件,纯数字、逗号后有空格、连字符 + formattedText = formattedText.replace(regex, function (match) { + const content = match.slice(1, match.length - 1); + if (/^\d+$/.test(content) || /, ?/.test(content) || /–/.test(content)) { - return `${match}`; // 如果符合条件则加上蓝色标签 + return `${match}`; } - return match; // 如果不符合条件,则保持原样 + return match; }); } - console.log("After replacement:", formattedText); // 调试:查看替换后的文本 - - - + console.log("After replacement:", formattedText); + paragraphText += formattedText; } - // 处理换行符 const breaks = paragraph.getElementsByTagName("w:br"); for (const br of breaks) { - paragraphText += "
"; + paragraphText += "
"; } - cellText += paragraphText; // 将段落文本添加到单元格文本 + cellText += paragraphText; + console.log('cellText at line 366:', cellText) } - // 更新合并单元格的信息 - if (colSpanInfo[i]) { - colspan = colSpanInfo[i].colspan; - } - - // 保存当前单元格信息 rowArray.push({ text: cellText, colspan: colspan, rowspan: rowspan }); - // 记录跨列合并 - if (colspan > 1) { - for (let j = 1; j < colspan; j++) { - colSpanInfo[i + j] = { colspan: 0 }; // 用 0 填充后续的列合并 + if (rowspan > 1) { + for (let j = 1; j < rowspan; j++) { + if (!rowSpanMap[rowIndex + j]) { + rowSpanMap[rowIndex + j] = []; + } + rowSpanMap[rowIndex + j][cellIndex] = true; } } + + cellIndex++; } - tableArray.push(rowArray); // 添加当前行到表格数组 + tableArray.push(rowArray.filter(item => item !== null)); + } - allTables.push(tableArray); // 添加当前表格到所有表格数组 + allTables.push(tableArray); } console.log("解析后的二维数组:", allTables); - callback(allTables); // 返回处理后的 HTML + callback(allTables); } catch (error) { console.error("解析 Word 文件失败:", error); - return []; + callback([]); } }, - transformHtmlString(inputHtml) { - - inputHtml = inputHtml.replace(/(<[^>]+) style="[^"]*"/g, '$1'); // 移除style属性 - inputHtml = inputHtml.replace(/(<[^>]+) class="[^"]*"/g, '$1'); // 移除class属性 + // async extractWordTablesToArrays(file, callback) { + + // const namespace = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; + // try { + // const Zip = new JSZip(); + // // 解压 ZIP + // const zip = await Zip.loadAsync(file); + + // // **检查解压的文件列表** + // console.log("解压后的文件:", Object.keys(zip.files)); + + // const documentFile = zip.file("word/document.xml"); + // if (!documentFile) { + // console.error("❌ 找不到 word/document.xml,无法解析 Word 文件"); + // return; + // } + + // const documentXml = await documentFile.async("string"); + // const parser = new DOMParser(); + // const documentDoc = parser.parseFromString(documentXml, "application/xml"); + + // // **打印 XML 结构以检查表格** + // console.log("解析的 XML 结构:", new XMLSerializer().serializeToString(documentDoc)); + + // // **检查 word/numbering.xml 是否存在** + // const numberingFile = zip.file("word/numbering.xml"); + // let numberingMap = {}; + // if (numberingFile) { + // const numberingXml = await numberingFile.async("string"); + // const numberingDoc = parser.parseFromString(numberingXml, "application/xml"); + // numberingMap = parseNumbering(numberingDoc); // 解析编号信息 + // } else { + // console.warn("⚠️ word/numbering.xml 不存在,跳过编号解析"); + // } + + // const tables = documentDoc.getElementsByTagNameNS(namespace, "tbl"); + // const allTables = []; // 存储所有表格的二维数组 + + // if (!tables || tables.length === 0) { + // console.warn("未找到表格内容,请检查 XML 结构"); + // return []; + // } + + + // for (const table of tables) { + + // const prevParagraph = table.previousElementSibling; + // if (prevParagraph) { + // console.log(`表格前的段落: ${prevParagraph.textContent}`); + // } + // const rows = table.getElementsByTagNameNS(namespace, "tr"); + // const tableArray = []; // 当前表格的二维数组 + + // for (const row of rows) { + // const cells = row.getElementsByTagNameNS(namespace, "tc"); + // const rowArray = []; // 当前行的数据 + + // // 存储已合并的单元格 + // let colSpanInfo = []; + + // for (let i = 0; i < cells.length; i++) { + // const cell = cells[i]; + // let cellText = ""; + // // const paragraphs = cell.getElementsByTagNameNS(namespace, "p"); + // const paragraphs = cell.getElementsByTagName("w:p"); + // // 检查合并单元格属性 + // const gridSpan = cell.getElementsByTagNameNS(namespace, "gridSpan")[0]; + // const vMerge = cell.getElementsByTagNameNS(namespace, "vMerge")[0]; + + // // 获取合并信息 + // let colspan = gridSpan ? parseInt(gridSpan.getAttribute("w:val"), 10) : 1; + // let rowspan = 1; + + // if (vMerge && vMerge.getAttribute("w:val") === "restart") { + // rowspan = 4; // 假设合并两行 + // } else if (vMerge && !vMerge.getAttribute("w:val")) { + // continue; // 如果是合并单元格的继续部分,则跳过 + // } + + // // 处理单元格内容 + // // let cellText = ""; + + + // // 为当前单元格初始化计数器 + // const currentLevelNumbers = {}; + // for (const paragraph of paragraphs) { + // // 提取段落编号 + // let listPrefix = ""; + // const numPr = paragraph.getElementsByTagName("w:numPr")[0]; + // if (numPr) { + // console.log('numPr at line 54:', numPr) + // const numIdElement = numPr.getElementsByTagName("w:numId")[0]; + // console.log('numIdElement at line 56:', numIdElement) + // const ilvlElement = numPr.getElementsByTagName("w:ilvl")[0]; + // console.log('ilvlElement at line 58:', ilvlElement) + // if (numIdElement && ilvlElement) { + // const numId = numIdElement.getAttribute("w:val"); + // const ilvl = ilvlElement.getAttribute("w:val"); + // listPrefix = this.getListNumber(numId, ilvl, numberingMap, currentLevelNumbers); + // } + // } + + // // 初始化当前段落文本 + // let paragraphText = listPrefix ? `${listPrefix} ` : ""; + + // const runs = paragraph.getElementsByTagName("w:r"); + // for (const run of runs) { + // let textContent = ""; + // const texts = run.getElementsByTagName("w:t"); + // for (const text of texts) { + // textContent += text.textContent; + // } + + // // 检查格式 + // const rPr = run.getElementsByTagName("w:rPr")[0]; + // let formattedText = textContent; + + // if (rPr) { + // const bold = rPr.getElementsByTagName("w:b").length > 0; + // const italic = rPr.getElementsByTagName("w:i").length > 0; + // const vertAlignElement = rPr.getElementsByTagName("w:vertAlign")[0]; + + // if (bold) { + // formattedText = `${formattedText}`; + // } + // if (italic) { + // formattedText = `${formattedText}`; + // } + // if (vertAlignElement) { + // const vertAlign = vertAlignElement.getAttribute("w:val"); + // if (vertAlign === "superscript") { + // formattedText = `${formattedText}`; + // } else if (vertAlign === "subscript") { + // formattedText = `${formattedText}`; + // } + // } + // } + + // // 替换负号 + // formattedText = replaceNegativeSign(formattedText); + + // // 首字母大写 + // formattedText = capitalizeFirstLetter(formattedText); + + // // 添加蓝色标签 + + + + // const regex = /\[(\d+(?:–\d+)?(?:, ?\d+(?:–\d+)?)*)\]/g; + + // formattedText = formattedText.replace(//g, '').replace(/<\/blue>/g, ''); // 先去掉所有的 标签 + + // if (regex.test(formattedText)) { + // formattedText = formattedText.replace(regex, function (match) { + // // 提取出方括号中的内容,并进行匹配 + // const content = match.slice(1, match.length - 1); // 去掉方括号 + + // // 判断是否符合条件,纯数字、逗号后有空格、连字符 + // if (/^\d+$/.test(content) || /, ?/.test(content) || /–/.test(content)) { + // return `${match}`; // 如果符合条件则加上蓝色标签 + // } + // return match; // 如果不符合条件,则保持原样 + // }); + // } + // console.log("After replacement:", formattedText); // 调试:查看替换后的文本 + + + + + // paragraphText += formattedText; + // } + + // // 处理换行符 + // const breaks = paragraph.getElementsByTagName("w:br"); + // for (const br of breaks) { + // paragraphText += "
"; + // } + + // cellText += paragraphText; // 将段落文本添加到单元格文本 + // } + + // // 更新合并单元格的信息 + // if (colSpanInfo[i]) { + // colspan = colSpanInfo[i].colspan; + // } + + // // 保存当前单元格信息 + // rowArray.push({ + // text: cellText, + // colspan: colspan, + // rowspan: rowspan + // }); + + // // 记录跨列合并 + // if (colspan > 1) { + // for (let j = 1; j < colspan; j++) { + // colSpanInfo[i + j] = { colspan: 0 }; // 用 0 填充后续的列合并 + // } + // } + // } + + // tableArray.push(rowArray); // 添加当前行到表格数组 + // } + + // allTables.push(tableArray); // 添加当前表格到所有表格数组 + // } + + // console.log("解析后的二维数组:", allTables); + // callback(allTables); // 返回处理后的 HTML + + // } catch (error) { + // console.error("解析 Word 文件失败:", error); + // return []; + // } + + // }, + transformHtmlString(inputHtml) { + + // inputHtml = inputHtml.replace(/(<[^>]+) style="[^"]*"/g, '$1'); // 移除style属性 + // inputHtml = inputHtml.replace(/(<[^>]+) class="[^"]*"/g, '$1'); // 移除class属性 + // inputHtml = inputHtml.replace(/<([a-zA-Z0-9]+)[^>]*>/g, '<$1>'); // 删除标签上的所有属性 + + inputHtml = inputHtml.replace(/<([a-zA-Z0-9]+)([^>]*)>/g, function (match, tag, attributes) { + // 使用正则表达式删除属性(保留 data-latex) + let updatedAttributes = attributes.replace(/\s([a-zA-Z0-9-]+)(="[^"]*")?/g, function (attrMatch, attrName) { + // 只保留 data-latex 属性,其他属性删除 + if (attrName === "data-latex") { + return attrMatch; + } + return ''; // 删除其他属性 + }); + + // 返回标签,保留 data-latex 属性 + return `<${tag}${updatedAttributes}>`; + }); // 2. 删除所有不需要的标签 (除 `strong`, `em`, `sub`, `sup`, `b`, `i` 外的所有标签) - inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue))[^>]+>/g, ''); // 删除不需要的标签 + inputHtml = inputHtml.replace(/<(?!\/?(strong|em|sub|sup|b|i|blue|wmath))[^>]+>/g, ''); // 删除不需要的标签 // 3. 如果有 `` 和 `` 标签,去掉内部样式并保留内容 inputHtml = inputHtml.replace(/]*>/g, '').replace(/<\/span>/g, ''); // 去除span标签 @@ -386,26 +744,79 @@ export default { }, + cleanAndParseWordContent(content) { + // 1️⃣ 解析成

段落数组 + let tempDiv = document.createElement('div'); + tempDiv.innerHTML = content; // 解析 HTML 内容 + let paragraphs = tempDiv.querySelectorAll("p"); // 选取所有

作为数据项 + + // 2️⃣ 将

内容转换为数组,并处理内容 + let parsedData = Array.from(paragraphs).map(p => { + let text = p.innerHTML.trim(); // 获取内容,去除两端空格 + text = this.transformHtmlString(text) + // 3️⃣ **正确移除 (Word 复制的无效标签)** + text = text.replace(/<\/?o:p[^>]*>/g, ""); + + // 4️⃣ **移除所有 style="..."** + text = text.replace(/\s*style="[^"]*"/gi, ""); + + // 5️⃣ **修正标签替换** + text = text.replace(//gi, "").replace(/<\/strong>/gi, ""); + text = text.replace(//gi, "").replace(/<\/em>/gi, ""); + + // 6️⃣ **移除空的 span、b、i 标签** + text = text.replace(/\s*<\/span>/gi, ""); + text = text.replace(/\s*<\/b>/gi, ""); + text = text.replace(/\s*<\/i>/gi, ""); + + // 7️⃣ **确保不移除半个标签(修复匹配规则)** + text = text.replace(/<[^\/>]+>\s*<\/[^>]+>/gi, match => { + return match.trim() === "" ? "" : match; + }); + + // 8️⃣ **返回最终内容** + return text.trim() === "" ? "" : text; + }); + + console.log(parsedData); // 输出数组,方便调试 + return parsedData; + } - parseTableToArray(tableString, callback) { + + , + + + async parseTableToArray(tableString, callback) { const parser = new DOMParser(); const doc = parser.parseFromString(tableString, 'text/html'); const rows = doc.querySelectorAll('table tr'); // 获取所有的行() - callback(Array.from(rows).map(row => { - const cells = row.querySelectorAll('th, td'); // 获取每个行中的单元格(包括 和 ) - return Array.from(cells).map(cell => ({ + // 使用 Promise 来处理异步的 MathJax 解析 + const result = await Promise.all( + Array.from(rows).map(async (row) => { + const cells = row.querySelectorAll('th, td'); // 获取每个行中的单元格(包括 和 ) + return await Promise.all( + Array.from(cells).map(async (cell) => { + const text = await this.extractMathJaxLatex(cell); + return { + text, + colspan: cell.getAttribute('colspan') ? parseInt(cell.getAttribute('colspan')) : 1, // 提取 colspan,默认值为 1 + rowspan: cell.getAttribute('rowspan') ? parseInt(cell.getAttribute('rowspan')) : 1 // 提取 rowspan,默认值为 1 + }; + }) + ); + }) + ); + console.log('result at line 78611:', result) + callback(result) + // 返回处理后的数组 - text: this.extractContentWithoutOuterSpan(cell), - colspan: cell.getAttribute('colspan') ? parseInt(cell.getAttribute('colspan')) : 1, // 提取 colspan,默认值为 1 - rowspan: cell.getAttribute('rowspan') ? parseInt(cell.getAttribute('rowspan')) : 1 // 提取 rowspan,默认值为 1 - })); - })); }, + parseNumbering(numberingDoc) { const numberingMap = new Map(); if (!numberingDoc) return numberingMap; @@ -1240,7 +1651,7 @@ export default { } }); ed.ui.registry.addButton('addRow', { - icon:'duplicate-row', + icon: 'duplicate-row', text: 'Add Row', // 下拉框标题 onAction: function () { var edSelection = ed.selection; @@ -1257,7 +1668,7 @@ export default { vueInstance.$emit('onAddRow', dataId); } } - + }); // 添加自定义菜单项 ed.ui.registry.addButton('Save', { @@ -1300,7 +1711,7 @@ export default { emTags[i].parentNode.replaceChild(iTag, emTags[i]); } content = div.innerHTML; - + vueInstance.$emit('saveContent', content, dataId); } } @@ -1390,8 +1801,8 @@ export default { } } }); - - + + ed.ui.registry.addButton('commentAdd', { icon: 'comment-add', text: 'Comment Add', @@ -1519,7 +1930,7 @@ export default { ed.setContent(''); } }); - + ed.ui.registry.addButton('customBlue', { text: 'Blue', // 按钮文本 @@ -1537,80 +1948,112 @@ export default { } } }); - + let latexEditorBookmark = null; // 用于记录插入点 + let activeEditorId = null; // 当前激活的编辑器 ID + + // 在编辑器工具栏中添加 "LateX" 按钮 + ed.ui.registry.addButton('LateX', { + text: 'LateX', // 按钮文本 + onAction: function () { + // 1. 获取当前光标位置 + const latexEditorBookmark = ed.selection.getBookmark(2); // 获取光标位置 + const editorId = ed.id; // 保存当前编辑器 ID + console.log('activeEditorId:', editorId); + + // 2. 生成一个随机的 ID,用于 wmath 标签 + const uid = 'wmath-' + Math.random().toString(36).substr(2, 9); + + // 3. 创建一个 标签并插入到光标处 + const wmathHtml = ``; + ed.insertContent(wmathHtml); // 在光标位置插入 wmath 标签 + + // 4. 打开公式编辑器窗口,并传递光标位置、编辑器 ID 和 wmathId + const url = `/LateX?editorId=${editorId}&wmathId=${uid}`; + // vueInstance.openLatexEditor({ + // editorId:editorId, + // wmathId:uid, + // }); + window.open(url, '_blank', 'width=1000,height=800,scrollbars=no,resizable=no'); + } + }); + + + + + ed.ui.registry.addButton('myuppercase', { text: 'A', // 按钮文本 - + onAction: function () { // 在选中的文本周围包裹 标签 var selectedText = ed.selection.getContent({ format: 'html' }); // 确保选中的文本是单个单词,包括空格 if (selectedText.trim() && /^[\s\w]+$/.test(selectedText)) { - // 使用正则将选中的文本中的第一个字母大写 - var capitalizedText = selectedText.replace(/(^|\s)([a-zA-Z])/g, function(match, p1, p2) { - return p1 + p2.toUpperCase(); - }); - - // 替换选中的文本,保持空格和其他内容 - ed.selection.setContent(capitalizedText); - } else { - vueInstance.$message.error(vueInstance.$t('commonTable.selectWord')); - - } + // 使用正则将选中的文本中的第一个字母大写 + var capitalizedText = selectedText.replace(/(^|\s)([a-zA-Z])/g, function (match, p1, p2) { + return p1 + p2.toUpperCase(); + }); + + // 替换选中的文本,保持空格和其他内容 + ed.selection.setContent(capitalizedText); + } else { + vueInstance.$message.error(vueInstance.$t('commonTable.selectWord')); + + } } }); ed.ui.registry.addButton('myuppercase', { text: 'A', // 按钮文本 - + onAction: function () { // 在选中的文本周围包裹 标签 var selectedText = ed.selection.getContent({ format: 'html' }); // 确保选中的文本是单个单词,包括空格 if (selectedText.trim() && /^[\s\w]+$/.test(selectedText)) { - // 使用正则将选中的文本中的第一个字母大写 - var capitalizedText = selectedText.replace(/(^|\s)([a-zA-Z])/g, function(match, p1, p2) { - return p1 + p2.toUpperCase(); - }); - - // 替换选中的文本,保持空格和其他内容 - ed.selection.setContent(capitalizedText); - } else { - vueInstance.$message.error(vueInstance.$t('commonTable.selectWord')); - - } + // 使用正则将选中的文本中的第一个字母大写 + var capitalizedText = selectedText.replace(/(^|\s)([a-zA-Z])/g, function (match, p1, p2) { + return p1 + p2.toUpperCase(); + }); + + // 替换选中的文本,保持空格和其他内容 + ed.selection.setContent(capitalizedText); + } else { + vueInstance.$message.error(vueInstance.$t('commonTable.selectWord')); + + } } }); ed.ui.registry.addButton('myuppercasea', { text: 'a', // 按钮文本(小写字母) onAction: function () { - // 获取选中的文本,保留 HTML 格式 - var selectedText = ed.selection.getContent({ format: 'html' }); + // 获取选中的文本,保留 HTML 格式 + var selectedText = ed.selection.getContent({ format: 'html' }); - // 确保选中的文本是单个有效的单词,包括空格 - if (selectedText.trim() && /^[\s\w]+$/.test(selectedText)) { - // 使用正则将选中的文本中的第一个字母转换为小写 - var lowercasedText = selectedText.replace(/(^|\s)([a-zA-Z])/g, function(match, p1, p2) { - return p1 + p2.toLowerCase(); - }); - - // 替换选中的文本,保持空格和其他内容 - ed.selection.setContent(lowercasedText); - } else { - vueInstance.$message.error(vueInstance.$t('commonTable.selectWord')); - } + // 确保选中的文本是单个有效的单词,包括空格 + if (selectedText.trim() && /^[\s\w]+$/.test(selectedText)) { + // 使用正则将选中的文本中的第一个字母转换为小写 + var lowercasedText = selectedText.replace(/(^|\s)([a-zA-Z])/g, function (match, p1, p2) { + return p1 + p2.toLowerCase(); + }); + + // 替换选中的文本,保持空格和其他内容 + ed.selection.setContent(lowercasedText); + } else { + vueInstance.$message.error(vueInstance.$t('commonTable.selectWord')); + } } - }); - ed.ui.registry.addButton('Line', { - text: '−', // 按钮文本 - onAction: function () { - // 插入 `−` 符号到当前光标位置 - ed.insertContent('−'); - } -}); + }); + ed.ui.registry.addButton('Line', { + text: '−', // 按钮文本 + onAction: function () { + // 插入 `−` 符号到当前光标位置 + ed.insertContent('−'); + } + }); + - ed.ui.registry.addButton('removeBlue', { text: 'Blue', // 按钮文本 onAction: function () { @@ -1660,7 +2103,7 @@ export default { { selector: '.tox-tbtn[data-mce-name="customblue"]', className: 'tinymce-custom-button-blue' }, { selector: '.tox-tbtn[data-mce-name="removeblue"]', className: 'tinymce-custom-button-removeblue' } ]; - + // 遍历每个按钮并为每个按钮添加类 buttons.forEach(item => { const buttonElements = document.querySelectorAll(item.selector); @@ -1672,7 +2115,7 @@ export default { }); }, 100); // 延迟执行,确保按钮渲染完成 } - + // 通用递归方法 diff --git a/src/components/common/Home.vue b/src/components/common/Home.vue index 97382ae..865907c 100644 --- a/src/components/common/Home.vue +++ b/src/components/common/Home.vue @@ -1,31 +1,42 @@