提交
This commit is contained in:
@@ -32,17 +32,21 @@ function stripLatexDelimiters(latex) {
|
||||
.trim();
|
||||
}
|
||||
|
||||
/** 块/行内统一用 $$ 渲染,保证公式大小一致;排版由 data-wrap + CSS 控制 */
|
||||
function buildLatexContentForRender(latex) {
|
||||
/** 按 data-wrap 选择行内 $ 或块级 $$ 分隔符 */
|
||||
function buildLatexContentForRender(latex, wrapMode) {
|
||||
const raw = stripLatexDelimiters(latex);
|
||||
if (!raw) return '';
|
||||
if (wrapMode === 'inline') {
|
||||
return '$' + raw + '$';
|
||||
}
|
||||
return '$$' + raw + '$$';
|
||||
}
|
||||
|
||||
function prepareWmathElement(element) {
|
||||
const latexContent = element.getAttribute('data-latex');
|
||||
if (!latexContent) return;
|
||||
element.innerHTML = buildLatexContentForRender(latexContent);
|
||||
const wrap = element.getAttribute('data-wrap') || 'block';
|
||||
element.innerHTML = buildLatexContentForRender(latexContent, wrap);
|
||||
}
|
||||
|
||||
// **定义全局渲染方法**
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
//记得切换
|
||||
|
||||
//正式
|
||||
// const mediaUrl = '/public/';
|
||||
// const baseUrl = '/';
|
||||
const mediaUrl = '/public/';
|
||||
const baseUrl = '/';
|
||||
|
||||
//正式环境
|
||||
|
||||
const mediaUrl = 'https://submission.tmrjournals.com/public/';
|
||||
// const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
|
||||
const baseUrl = '/api'
|
||||
// const mediaUrl = 'https://submission.tmrjournals.com/public/';
|
||||
// // const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
|
||||
// const baseUrl = '/api'
|
||||
|
||||
//测试环境
|
||||
|
||||
|
||||
@@ -1087,11 +1087,11 @@ const en = {
|
||||
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',
|
||||
mathFormula: 'LaTeX-compliant',
|
||||
mathFormulaMore: 'more formulas',
|
||||
latexDataCopy: 'Copy',
|
||||
latexDataCopied: 'Copied',
|
||||
latexDataClickToCopy: 'Click a formula to copy',
|
||||
latexDataClickToCopy: 'Click a row number or formula to copy',
|
||||
latexDataAdd: 'Add',
|
||||
latexDataEdit: 'Edit',
|
||||
latexDataDelete: 'Delete',
|
||||
@@ -1108,6 +1108,23 @@ const en = {
|
||||
latexMathEditorTooltip: 'Insert or edit a LaTeX formula',
|
||||
latexMathConfirm: 'OK',
|
||||
latexDataCancel: 'Cancel',
|
||||
latexDataRowIssueTip: 'This formula may have recognition or rendering issues; click Edit to review',
|
||||
latexDataIssueSummary: '{count} formula(s) failed to render (rows {indexes}). Row numbers are in red. This notice hides once all are fixed and saved.',
|
||||
latexDataIssueInlinePrefix: '{count} failed to render:',
|
||||
latexDataIssueLocate: 'Locate issues ({count})',
|
||||
latexDataWordDownloadHtml: 'Download source HTML',
|
||||
latexDataWordPreviewTitle: 'Word formula source report',
|
||||
latexDataWordPreviewHint:
|
||||
'Badge number = formula list row. No badge = not in the list. Top links are failed rows only.',
|
||||
latexDataWordReportIssuePrefix: 'Fix row:',
|
||||
latexDataWordPreviewLegendOk: 'Green · OK',
|
||||
latexDataWordPreviewLegendErr: 'Red · needs fix',
|
||||
latexDataWordPreviewLegendDup: 'Orange · duplicate',
|
||||
latexDataWordReportDupPrefix: 'Duplicate rows (same LaTeX):',
|
||||
latexDataWordReportDupGroup: 'Same formula',
|
||||
latexDataWordReportDupTag: 'Dup #{n}',
|
||||
latexDataWordReportDupCount: '×{n}',
|
||||
latexDataWordReportDownloaded: 'Source HTML downloaded. Open it in your browser.',
|
||||
latexDataOk: 'Save',
|
||||
editMathFormulaSuccess: 'Math formulas updated successfully.',
|
||||
selectOne: 'Please select only a single paragraph!',
|
||||
@@ -1591,6 +1608,7 @@ const en = {
|
||||
latestInvitationLabel: 'Latest invitation: ',
|
||||
affiliation: 'Affiliation: ',
|
||||
field: 'Field: ',
|
||||
fieldAi: 'AI Field: ',
|
||||
detail: 'Detail',
|
||||
select: 'Select',
|
||||
reviewing: 'Reviewing',
|
||||
@@ -1605,6 +1623,7 @@ const en = {
|
||||
dialogAffiliation: 'Affiliation :',
|
||||
dialogResearchAreas: 'Research areas :',
|
||||
dialogField: 'Field :',
|
||||
dialogFieldAi: 'AI Field :',
|
||||
dialogIntroduction: 'Introduction :',
|
||||
cancel: 'Cancel'
|
||||
},
|
||||
|
||||
@@ -1073,7 +1073,7 @@ const zh = {
|
||||
importWordMathNoNew: '公式已存在,未新增重复项',
|
||||
importWordMathParsed: 'Word 解析完成:识别 {parsed} 个公式,新增 {added} 个',
|
||||
importWordMathParsedNone: 'Word 解析完成:识别 {parsed} 个公式,均已存在,未新增',
|
||||
mathFormula: 'Latex',
|
||||
mathFormula: 'LaTeX-compliant',
|
||||
mathFormulaMore: '条公式',
|
||||
latexDataCopy: '复制',
|
||||
latexDataCopied: '已复制',
|
||||
@@ -1095,6 +1095,22 @@ const zh = {
|
||||
latexMathConfirm: '确定',
|
||||
latexDataCancel: '取消',
|
||||
latexDataOk: '保存',
|
||||
latexDataRowIssueTip: '该公式可能识别异常或未正确渲染,建议点「修改」检查',
|
||||
latexDataIssueSummary: '共 {count} 处未解析成功(序号 {indexes}),对应序号已标红;全部修正保存后本提示将自动消失',
|
||||
latexDataIssueInlinePrefix: '共 {count} 处未解析:',
|
||||
latexDataIssueLocate: '定位未解析({count})',
|
||||
latexDataWordDownloadHtml: '下载源稿 HTML',
|
||||
latexDataWordPreviewTitle: 'Word 公式源稿报告',
|
||||
latexDataWordPreviewHint: '角标数字=公式列表行号;无角标表示该处公式不在列表中。顶部仅列出需修改的行号。',
|
||||
latexDataWordReportIssuePrefix: '需修改:',
|
||||
latexDataWordPreviewLegendOk: '绿色 · 通过',
|
||||
latexDataWordPreviewLegendErr: '红色 · 需修改',
|
||||
latexDataWordPreviewLegendDup: '橙色 · 重复公式',
|
||||
latexDataWordReportDupPrefix: '重复序号(相同 LaTeX,点击跳转):',
|
||||
latexDataWordReportDupGroup: '相同公式序号',
|
||||
latexDataWordReportDupTag: '重复#{n}',
|
||||
latexDataWordReportDupCount: '×{n}',
|
||||
latexDataWordReportDownloaded: '已生成并下载源稿 HTML,请用浏览器打开查看',
|
||||
editMathFormulaSuccess: '数字公式更新成功',
|
||||
selectOne:'请只勾选单个段落!',
|
||||
alreadyCommented:'文本中已有批注内容请重新选择',
|
||||
@@ -1572,6 +1588,7 @@ const zh = {
|
||||
latestInvitationLabel: '最新邀请:',
|
||||
affiliation: '单位:',
|
||||
field: '领域:',
|
||||
fieldAi: 'AI 领域:',
|
||||
detail: '详情',
|
||||
select: '选择',
|
||||
reviewing: '审稿中',
|
||||
@@ -1586,6 +1603,7 @@ const zh = {
|
||||
dialogAffiliation: '单位:',
|
||||
dialogResearchAreas: '研究领域:',
|
||||
dialogField: '领域:',
|
||||
dialogFieldAi: 'AI 领域:',
|
||||
dialogIntroduction: '简介:',
|
||||
cancel: '取消'
|
||||
},
|
||||
|
||||
@@ -415,6 +415,7 @@
|
||||
type="content"
|
||||
@openLatexEditor="openLatexEditor"
|
||||
v-if="addContentVisible"
|
||||
:enable-import-word-math="true"
|
||||
ref="addContent"
|
||||
style="margin-left: -115px"
|
||||
></common-content>
|
||||
@@ -684,6 +685,12 @@ export default {
|
||||
handleSaveContent() {
|
||||
this.$refs.commonContent.getTinymceContent('content');
|
||||
},
|
||||
clearWordSelectionUI() {
|
||||
const word = this.$refs.commonWord;
|
||||
if (word && typeof word.clearParagraphSelection === 'function') {
|
||||
word.clearParagraphSelection();
|
||||
}
|
||||
},
|
||||
handleSaveAddContent() {
|
||||
this.$refs.addContent.getTinymceContent('addcontent');
|
||||
},
|
||||
@@ -800,6 +807,7 @@ export default {
|
||||
if (res.code == 0) {
|
||||
loading.close();
|
||||
this.editVisible = false;
|
||||
this.clearWordSelectionUI();
|
||||
this.refreshCurrentContent('content', am_id, res.data);
|
||||
this.getCommentList();
|
||||
} else {
|
||||
|
||||
@@ -409,6 +409,7 @@
|
||||
@getContent="getContent"
|
||||
@openLatexEditor="openLatexEditor"
|
||||
v-if="addContentVisible"
|
||||
:enable-import-word-math="true"
|
||||
ref="addContent"
|
||||
style="margin-left: -115px"
|
||||
></common-content>
|
||||
@@ -687,6 +688,12 @@ export default {
|
||||
handleSaveContent() {
|
||||
this.$refs.commonContent.getTinymceContent('content');
|
||||
},
|
||||
clearWordSelectionUI() {
|
||||
const word = this.$refs.commonWord;
|
||||
if (word && typeof word.clearParagraphSelection === 'function') {
|
||||
word.clearParagraphSelection();
|
||||
}
|
||||
},
|
||||
handleSaveAddContent() {
|
||||
this.$refs.addContent.getTinymceContent('addcontent');
|
||||
},
|
||||
@@ -752,6 +759,7 @@ export default {
|
||||
.then(async (res) => {
|
||||
if (res.code == 0) {
|
||||
this.editVisible = false;
|
||||
this.clearWordSelectionUI();
|
||||
this.getDate();
|
||||
this.getCommentList();
|
||||
}
|
||||
|
||||
@@ -1102,6 +1102,10 @@ import ProgressBar from '@/components/page/components/article/progress.vue';
|
||||
import { updateStepStatus, markStepAsSaved } from '@/components/page/components/article/checkStepCompletion.js';
|
||||
import deepEqual from '@/components/page/components/article/deepEqual.js';
|
||||
import { set } from 'vue';
|
||||
import {
|
||||
buildArticleAddTableListFromWordFile,
|
||||
buildArticleAddTableListItems
|
||||
} from '@/utils/mathFormulaModule';
|
||||
export default {
|
||||
components: {
|
||||
JournalSelector,
|
||||
@@ -2750,21 +2754,30 @@ export default {
|
||||
// event.percentage 就是当前进度(百分比,浮点数)
|
||||
this.uploadPercentage = Math.round(event.percent); // 取整数
|
||||
},
|
||||
submitArticleTableList(list) {
|
||||
if (!list || !list.length) return Promise.resolve();
|
||||
return this.$api
|
||||
.post('api/Article/addArticleTable', {
|
||||
article_id: this.stagingID,
|
||||
list
|
||||
})
|
||||
.then(() => {
|
||||
this.isShowCommonWord = true;
|
||||
setTimeout(() => {
|
||||
this.isShowProgress = false;
|
||||
}, 500);
|
||||
});
|
||||
},
|
||||
addWordTablesList(tables) {
|
||||
var data = {
|
||||
article_id: this.stagingID,
|
||||
|
||||
list: tables.map((e) => ({
|
||||
table: JSON.stringify([...e]),
|
||||
type: 0,
|
||||
html_data: ''
|
||||
}))
|
||||
};
|
||||
this.$api.post('api/Article/addArticleTable', data).then((res) => {
|
||||
this.isShowCommonWord = true;
|
||||
setTimeout(() => {
|
||||
this.isShowProgress = false;
|
||||
}, 500);
|
||||
this.submitArticleTableList(buildArticleAddTableListItems(tables, null));
|
||||
},
|
||||
processManuscriptWordTables(file) {
|
||||
if (!file) return;
|
||||
const extractFn = this.$commonJS.extractWordTablesToArrays.bind(this.$commonJS);
|
||||
buildArticleAddTableListFromWordFile(file, extractFn).then((list) => {
|
||||
if (list.length) {
|
||||
this.submitArticleTableList(list);
|
||||
}
|
||||
});
|
||||
},
|
||||
upLoadWordTables() {},
|
||||
@@ -2839,17 +2852,9 @@ export default {
|
||||
window.history.replaceState({}, document.title, url.toString());
|
||||
|
||||
if (this.form.article_id) {
|
||||
if (File) {
|
||||
if (File && File.raw) {
|
||||
loading.close();
|
||||
var that = this;
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
that.$commonJS.extractWordTablesToArrays(File.raw, function (wordTables) {
|
||||
that.addWordTablesList(wordTables);
|
||||
loading.close();
|
||||
},that.form.article_id);
|
||||
};
|
||||
reader.readAsArrayBuffer(File.raw);
|
||||
this.processManuscriptWordTables(File.raw);
|
||||
}
|
||||
await this.onStaging(5);
|
||||
await this.TempoAuthor();
|
||||
|
||||
@@ -525,6 +525,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { buildArticleAddTableListFromWordFile } from '@/utils/mathFormulaModule';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -569,7 +571,8 @@
|
||||
manuscirptFileList: [],
|
||||
supplementaryFileList:[],
|
||||
fileL_supplementary:[],
|
||||
wordTables:[],
|
||||
wordTablePayloadList: [],
|
||||
wordTableParsePromise: null,
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
@@ -894,26 +897,17 @@ if(this.comentDeploy.length){
|
||||
},
|
||||
|
||||
// 上传文件
|
||||
addWordTablesList(tables) {
|
||||
console.log('tables at line 687:', tables)
|
||||
var data = {
|
||||
addWordTablesList() {
|
||||
const list = this.wordTablePayloadList || [];
|
||||
if (!list.length) return Promise.resolve();
|
||||
return this.$api.post('api/Article/addArticleTable', {
|
||||
article_id: this.fileMesForm.articleId,
|
||||
list: tables.map(e=>( {
|
||||
table: JSON.stringify([...e]),
|
||||
type: 0,
|
||||
html_data: ''
|
||||
})),
|
||||
|
||||
|
||||
};
|
||||
|
||||
this.$api.post('api/Article/addArticleTable', data).then((res) => {
|
||||
|
||||
list
|
||||
});
|
||||
},
|
||||
|
||||
async clearFileManuscript() {
|
||||
var that=this
|
||||
var that = this;
|
||||
await this.$api
|
||||
.post('api/Article/reloadArticleImages', {
|
||||
article_id: this.fileMesForm.articleId
|
||||
@@ -924,7 +918,7 @@ if(this.comentDeploy.length){
|
||||
.post('api/Article/reloadArticleTable', {
|
||||
article_id: this.fileMesForm.articleId
|
||||
})
|
||||
.then((res) => {that. addWordTablesList(that.wordTables);});
|
||||
.then((res) => that.addWordTablesList());
|
||||
},
|
||||
async onSubmit() {
|
||||
|
||||
@@ -943,6 +937,9 @@ if(this.comentDeploy.length){
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
if (this.wordTableParsePromise) {
|
||||
await this.wordTableParsePromise;
|
||||
}
|
||||
await this.$api
|
||||
.post('api/Article/RepairBack', this.fileMesForm)
|
||||
.then(async (res) => {
|
||||
@@ -1025,21 +1022,26 @@ if(this.comentDeploy.length){
|
||||
// this.fileMesForm.picturesAndTables.push('picturesAndTables/' + url);
|
||||
// }
|
||||
},
|
||||
parseManuscriptWordTables(file) {
|
||||
if (!file) return Promise.resolve([]);
|
||||
if (this.wordTableParsePromise) return this.wordTableParsePromise;
|
||||
const extractFn = this.$commonJS.extractWordTablesToArrays.bind(this.$commonJS);
|
||||
this.wordTableParsePromise = buildArticleAddTableListFromWordFile(file, extractFn)
|
||||
.then((list) => {
|
||||
this.wordTablePayloadList = list || [];
|
||||
return this.wordTablePayloadList;
|
||||
})
|
||||
.finally(() => {
|
||||
this.wordTableParsePromise = null;
|
||||
});
|
||||
return this.wordTableParsePromise;
|
||||
},
|
||||
upSuccess_manuscirpt(res, File) {
|
||||
if (res.code == 0) {
|
||||
this.fileMesForm.manuscirpt = 'manuscirpt/' + res.upurl;
|
||||
if (File) {
|
||||
var that = this;
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
that.$commonJS.extractWordTablesToArrays(File.raw, function (wordTables) {
|
||||
console.log('tablesHtml at line 61:', wordTables);
|
||||
that.wordTables=wordTables
|
||||
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(File.raw);
|
||||
}
|
||||
if (File && File.raw) {
|
||||
this.parseManuscriptWordTables(File.raw);
|
||||
}
|
||||
} else {
|
||||
this.$message.error('service error:' + res.msg);
|
||||
}
|
||||
@@ -1065,6 +1067,8 @@ that.wordTables=wordTables
|
||||
},
|
||||
removefilemanuscirpt(file, fileList) {
|
||||
this.fileMesForm.manuscirpt = '';
|
||||
this.wordTablePayloadList = [];
|
||||
this.wordTableParsePromise = null;
|
||||
},
|
||||
removefileresponse(file, fileList) {
|
||||
this.fileMesForm.responseFile = '';
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<template slot-scope="scope">
|
||||
<p class="tab_tie_col" v-if="scope.row.company != ''"><span>{{ $t('reviewerAdd.affiliation') }}</span>{{ scope.row.company }}</p>
|
||||
<p class="tab_tie_col" v-if="scope.row.field != ''"><span>{{ $t('reviewerAdd.field') }}</span>{{ scope.row.field }}</p>
|
||||
<p class="tab_tie_col" v-if="scope.row.field_ai"><span>{{ $t('reviewerAdd.fieldAi') }}</span>{{ scope.row.field_ai }}</p>
|
||||
<!-- <p class="tab_tie_col" v-if="scope.row.majorstr != ''"><span>Major: </span>{{ scope.row.majorstr }}</p> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
@@ -117,6 +117,7 @@
|
||||
<template slot-scope="scope">
|
||||
<p class="tab_tie_col" v-if="scope.row.company != ''"><span>{{ $t('reviewerAdd.affiliation') }}</span>{{ scope.row.company }}</p>
|
||||
<p class="tab_tie_col" v-if="scope.row.field != ''"><span>{{ $t('reviewerAdd.field') }}</span>{{ scope.row.field }}</p>
|
||||
<p class="tab_tie_col" v-if="scope.row.field_ai"><span>{{ $t('reviewerAdd.fieldAi') }}</span>{{ scope.row.field_ai }}</p>
|
||||
<!-- <p class="tab_tie_col" v-if="scope.row.majorstr != ''"><span>Major: </span>{{ scope.row.majorstr }}</p> -->
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -213,6 +214,9 @@
|
||||
<el-form-item :label="$t('reviewerAdd.dialogField')">
|
||||
<span>{{ mesOpen.field }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="mesOpen.field_ai" :label="$t('reviewerAdd.dialogFieldAi')">
|
||||
<span>{{ mesOpen.field_ai }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('reviewerAdd.dialogIntroduction')">
|
||||
<span>{{ mesOpen.introduction }}</span>
|
||||
</el-form-item>
|
||||
@@ -386,7 +390,10 @@ export default {
|
||||
uid: row.user_id
|
||||
})
|
||||
.then((res) => {
|
||||
this.mesOpen = res.data;
|
||||
this.mesOpen = {
|
||||
...res.data,
|
||||
field_ai: res.data.field_ai || row.field_ai
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
|
||||
@@ -271,6 +271,12 @@ export default {
|
||||
handleSaveContent() {
|
||||
this.$refs.commonContent.getTinymceContent('content');
|
||||
},
|
||||
clearWordSelectionUI() {
|
||||
const word = this.$refs.commonWord;
|
||||
if (word && typeof word.clearParagraphSelection === 'function') {
|
||||
word.clearParagraphSelection();
|
||||
}
|
||||
},
|
||||
|
||||
async getContent(type, content) {
|
||||
if (type == 'content') {
|
||||
@@ -365,6 +371,7 @@ export default {
|
||||
.then(async (res) => {
|
||||
if (res.code == 0) {
|
||||
this.editVisible = false;
|
||||
this.clearWordSelectionUI();
|
||||
this.getDate();
|
||||
this.getCommentList();
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<script>
|
||||
import { TableUtils } from '@/common/js/TableUtils';
|
||||
import { mediaUrl } from '@/common/js/commonJS.js';
|
||||
import { isMathFormulaTableTitle, parseFormulaRowsFromTableData } from '@/utils/mathFormulaModule';
|
||||
import { isMathFormulaTableRecord, parseFormulaRowsFromTableData } from '@/utils/mathFormulaModule';
|
||||
|
||||
export default {
|
||||
name: 'TablePreviewer',
|
||||
@@ -90,7 +90,7 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
isLatexDataPreview() {
|
||||
return this.processedItem && isMathFormulaTableTitle(this.processedItem.title);
|
||||
return isMathFormulaTableRecord(this.processedItem);
|
||||
},
|
||||
latexFormulaItems() {
|
||||
if (!this.isLatexDataPreview || !this.processedItem) return [];
|
||||
@@ -111,7 +111,7 @@ export default {
|
||||
this.loading = true;
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (isMathFormulaTableTitle(item.title)) {
|
||||
if (isMathFormulaTableRecord(item)) {
|
||||
this.processedItem = Object.freeze({ ...item });
|
||||
} else {
|
||||
const processed = this.processTableData(item.table);
|
||||
|
||||
@@ -16,9 +16,35 @@
|
||||
{{ $t('commonTable.latexDataDeleteAll') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span v-if="displayFormulaItems.length || isAddingNew" class="toolbar-hint">
|
||||
{{ $t('commonTable.latexDataClickToCopy') }}
|
||||
</span>
|
||||
<div v-if="problemIssueCount > 0" class="toolbar-issue-inline">
|
||||
<i class="el-icon-warning-outline"></i>
|
||||
<span class="toolbar-issue-prefix">{{
|
||||
$t('commonTable.latexDataIssueInlinePrefix', { count: problemIssueCount })
|
||||
}}</span>
|
||||
<span class="toolbar-issue-indexes">
|
||||
<template v-for="(rowIndex, idx) in problemIssueIndexesDisplay">
|
||||
<button
|
||||
:key="'issue-' + rowIndex"
|
||||
type="button"
|
||||
class="toolbar-issue-index-link"
|
||||
@click.stop="locateFormulaIssue(rowIndex)"
|
||||
>{{ rowIndex }}</button><span
|
||||
v-if="idx < problemIssueIndexesDisplay.length - 1"
|
||||
class="toolbar-issue-index-sep"
|
||||
>、</span>
|
||||
</template>
|
||||
<span v-if="problemIssueIndexesOverflow" class="toolbar-issue-more">…</span>
|
||||
</span>
|
||||
</div>
|
||||
<el-button
|
||||
v-if="wordPreviewHtml"
|
||||
size="mini"
|
||||
type="text"
|
||||
class="word-report-download-btn"
|
||||
@click="downloadWordImportReport"
|
||||
>
|
||||
{{ $t('commonTable.latexDataWordDownloadHtml') }}
|
||||
</el-button>
|
||||
<el-tooltip :content="$t('commonTable.importWordMathTip')" placement="top">
|
||||
<button
|
||||
type="button"
|
||||
@@ -75,8 +101,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="action-link action-cancel"
|
||||
@mousedown.prevent
|
||||
@click.stop="cancelInlineEdit"
|
||||
@mousedown.prevent.stop="onCancelButtonPointer"
|
||||
>
|
||||
{{ $t('commonTable.latexDataCancel') }}
|
||||
</button>
|
||||
@@ -85,10 +110,18 @@
|
||||
<div
|
||||
v-for="item in displayFormulaItems"
|
||||
:key="item.index"
|
||||
:data-formula-index="item.index"
|
||||
class="latex-formula-row"
|
||||
:class="{ 'latex-formula-row--editing': editingIndex === item.index }"
|
||||
:class="{
|
||||
'latex-formula-row--editing': editingIndex === item.index,
|
||||
'latex-formula-row--highlight': highlightFormulaIndex === item.index
|
||||
}"
|
||||
>
|
||||
<span class="row-index">{{ item.index }}</span>
|
||||
<span
|
||||
class="row-index"
|
||||
:class="{ 'row-index--warn': isRowIndexWarn(item) }"
|
||||
:title="isRowIndexWarn(item) ? $t('commonTable.latexDataRowIssueTip') : ''"
|
||||
>{{ item.index }}</span>
|
||||
<div v-if="editingIndex === item.index" class="row-edit">
|
||||
<div class="tinymce-inline-math-panel latex-data-inline-math latex-data-inline-math--compact">
|
||||
<div class="tinymce-inline-math-field-wrap latex-inline-math-field-wrap"></div>
|
||||
@@ -118,8 +151,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="action-link action-cancel"
|
||||
@mousedown.prevent
|
||||
@click.stop="cancelInlineEdit"
|
||||
@mousedown.prevent.stop="onCancelButtonPointer"
|
||||
>
|
||||
{{ $t('commonTable.latexDataCancel') }}
|
||||
</button>
|
||||
@@ -149,6 +181,12 @@
|
||||
<i :class="emptyHint === 'noFormula' ? 'el-icon-warning-outline' : 'el-icon-document'"></i>
|
||||
<span>{{ emptyMessage }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="displayFormulaItems.length || isAddingNew"
|
||||
class="latex-panel-footer"
|
||||
>
|
||||
<span class="latex-panel-footer-hint">{{ $t('commonTable.latexDataClickToCopy') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -169,6 +207,9 @@ import {
|
||||
normalizeLatexTableApiResponse,
|
||||
getLatexDataTableFromList
|
||||
} from '@/utils/mathFormulaModule';
|
||||
import { normalizeWmathCellHtml, isFormulaDisplayedAsRawSource } from '@/utils/wordMathImport';
|
||||
import { buildWordImportReportHtml, downloadHtmlFile } from '@/utils/wordImportReport';
|
||||
import { buildDuplicateFormulaIndexMap } from '@/utils/mathFormulaModule';
|
||||
import { MathfieldElement } from 'mathlive';
|
||||
import 'mathlive/dist/mathlive-static.css';
|
||||
import 'mathlive/dist/mathlive-fonts.css';
|
||||
@@ -232,6 +273,12 @@ export default {
|
||||
mathFieldRef: null,
|
||||
optimisticRows: null,
|
||||
editOutsideEnabled: false,
|
||||
/** 取消编辑中:阻止 blur/click 竞态触发保存接口 */
|
||||
cancelEditInProgress: false,
|
||||
/** MathJax 渲染失败后标红序号(index -> true) */
|
||||
renderIssueIndexes: {},
|
||||
highlightFormulaIndex: null,
|
||||
_highlightTimer: null,
|
||||
wordImportLoading: false,
|
||||
activeAmtId: null,
|
||||
panelTip: {
|
||||
@@ -239,7 +286,9 @@ export default {
|
||||
type: 'info',
|
||||
text: ''
|
||||
},
|
||||
_panelTipTimer: null
|
||||
_panelTipTimer: null,
|
||||
wordPreviewHtml: '',
|
||||
wordImportFileName: 'word-formulas'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -287,9 +336,33 @@ export default {
|
||||
? tableItem.table
|
||||
: parseTableDataFromApiField(tableItem);
|
||||
return parseFormulaRowsFromTableData(tableData);
|
||||
},
|
||||
problemIssueIndexes() {
|
||||
return (this.displayFormulaItems || [])
|
||||
.filter((item) => this.isRowIndexWarn(item))
|
||||
.map((item) => item.index);
|
||||
},
|
||||
problemIssueCount() {
|
||||
return this.problemIssueIndexes.length;
|
||||
},
|
||||
problemIssueIndexesDisplay() {
|
||||
const max = 12;
|
||||
const list = this.problemIssueIndexes;
|
||||
if (list.length <= max) return list;
|
||||
return list.slice(0, max);
|
||||
},
|
||||
problemIssueIndexesOverflow() {
|
||||
return this.problemIssueIndexes.length > 12;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
problemIssueCount(val) {
|
||||
if (!val) {
|
||||
this._issueLocatePtr = 0;
|
||||
this.clearHighlightTimer();
|
||||
this.highlightFormulaIndex = null;
|
||||
}
|
||||
},
|
||||
formulaItems: {
|
||||
handler(items) {
|
||||
if (this.optimisticRows !== null && !this.optimisticRows.length) {
|
||||
@@ -349,6 +422,7 @@ export default {
|
||||
this.destroyMathField();
|
||||
this.unbindEditOutsideListener();
|
||||
this.clearPanelTipTimer();
|
||||
this.clearHighlightTimer();
|
||||
},
|
||||
methods: {
|
||||
isMathliveUiTarget(target) {
|
||||
@@ -375,9 +449,17 @@ export default {
|
||||
this.editOutsideEnabled = false;
|
||||
this._onEditOutside = (event) => {
|
||||
if (!this.editOutsideEnabled || !this.isEditingActive) return;
|
||||
if (this.isInsideActiveEditArea(event.target)) return;
|
||||
if (this.isMathliveUiTarget(event.target)) return;
|
||||
this.cancelInlineEdit();
|
||||
const target = event.target;
|
||||
if (target && target.closest && target.closest('.action-cancel')) {
|
||||
this.onCancelButtonPointer();
|
||||
return;
|
||||
}
|
||||
if (target && target.closest && target.closest('.action-save')) {
|
||||
return;
|
||||
}
|
||||
if (this.isInsideActiveEditArea(target)) return;
|
||||
if (this.isMathliveUiTarget(target)) return;
|
||||
this.onCancelButtonPointer();
|
||||
};
|
||||
document.addEventListener('mousedown', this._onEditOutside, true);
|
||||
this._editOutsideTimer = setTimeout(() => {
|
||||
@@ -403,7 +485,7 @@ export default {
|
||||
}
|
||||
},
|
||||
dismissEditing() {
|
||||
this.cancelInlineEdit();
|
||||
this.onCancelButtonPointer();
|
||||
},
|
||||
/** 列表内滚动仅退出编辑,不关闭弹窗 */
|
||||
handleListScroll() {
|
||||
@@ -413,7 +495,8 @@ export default {
|
||||
},
|
||||
buildItemsFromRows(rows) {
|
||||
return (rows || []).map((row, index) => {
|
||||
const html = row[0] && row[0].text ? row[0].text : '';
|
||||
const rawHtml = row[0] && row[0].text ? row[0].text : '';
|
||||
const html = normalizeWmathCellHtml(rawHtml);
|
||||
return {
|
||||
index: index + 1,
|
||||
html,
|
||||
@@ -421,12 +504,125 @@ export default {
|
||||
};
|
||||
});
|
||||
},
|
||||
isRowIndexWarn(item) {
|
||||
if (!item) return false;
|
||||
return !!this.renderIssueIndexes[item.index];
|
||||
},
|
||||
clearHighlightTimer() {
|
||||
if (this._highlightTimer) {
|
||||
clearTimeout(this._highlightTimer);
|
||||
this._highlightTimer = null;
|
||||
}
|
||||
},
|
||||
flashFormulaRow(index) {
|
||||
this.clearHighlightTimer();
|
||||
this.highlightFormulaIndex = index;
|
||||
this._highlightTimer = setTimeout(() => {
|
||||
this.highlightFormulaIndex = null;
|
||||
this._highlightTimer = null;
|
||||
}, 2200);
|
||||
},
|
||||
locateFormulaIssue(index) {
|
||||
this.scrollToFormulaRowInList(index);
|
||||
},
|
||||
buildWmathIdToListIndexMap() {
|
||||
const map = {};
|
||||
const list = this.$refs.formulaList;
|
||||
if (!list) return map;
|
||||
list.querySelectorAll('.latex-formula-row:not(.latex-formula-row--new)').forEach((rowEl) => {
|
||||
const index = parseInt(rowEl.getAttribute('data-formula-index'), 10);
|
||||
const w = rowEl.querySelector('wmath');
|
||||
const id = w && w.getAttribute('data-id');
|
||||
if (id && index) map[id] = index;
|
||||
});
|
||||
return map;
|
||||
},
|
||||
downloadWordImportReport() {
|
||||
if (!this.wordPreviewHtml) return;
|
||||
this.scanFormulaRenderIssues();
|
||||
const indexByWmathId = this.buildWmathIdToListIndexMap();
|
||||
const listDup = buildDuplicateFormulaIndexMap(this.currentFormulaRows || []);
|
||||
const html = buildWordImportReportHtml({
|
||||
previewBodyHtml: this.wordPreviewHtml,
|
||||
issueIndexes: this.renderIssueIndexes,
|
||||
indexByWmathId,
|
||||
listDuplicateOf: listDup.duplicateOf || {},
|
||||
listDuplicateCountByFirst: listDup.countByFirst || {},
|
||||
title: this.$t('commonTable.latexDataWordPreviewTitle'),
|
||||
hint: this.$t('commonTable.latexDataWordPreviewHint'),
|
||||
legendOk: this.$t('commonTable.latexDataWordPreviewLegendOk'),
|
||||
legendErr: this.$t('commonTable.latexDataWordPreviewLegendErr'),
|
||||
issueSummaryPrefix: this.$t('commonTable.latexDataWordReportIssuePrefix'),
|
||||
dupTagRepeat: this.$t('commonTable.latexDataWordReportDupTag'),
|
||||
dupTagCount: this.$t('commonTable.latexDataWordReportDupCount')
|
||||
});
|
||||
const base = this.wordImportFileName || 'word-formulas';
|
||||
downloadHtmlFile(html, `${base}-report.html`);
|
||||
},
|
||||
storeWordReportSource(previewHtml, file) {
|
||||
this.wordPreviewHtml = previewHtml || '';
|
||||
const name = file && file.name ? String(file.name) : '';
|
||||
this.wordImportFileName = name ? name.replace(/\.docx$/i, '') : 'word-formulas';
|
||||
},
|
||||
scheduleWordReportDownload() {
|
||||
if (!this.wordPreviewHtml) return;
|
||||
this.$nextTick(() => {
|
||||
if (window.renderMathJax) {
|
||||
window.renderMathJax();
|
||||
}
|
||||
setTimeout(() => {
|
||||
this.scanFormulaRenderIssues();
|
||||
this.downloadWordImportReport();
|
||||
this.$message.success(this.$t('commonTable.latexDataWordReportDownloaded'));
|
||||
}, 700);
|
||||
});
|
||||
},
|
||||
scrollToFormulaRowInList(index) {
|
||||
const list = this.$refs.formulaList;
|
||||
if (!list || !index) return;
|
||||
const rowEl = list.querySelector(`[data-formula-index="${index}"]`);
|
||||
if (!rowEl) return;
|
||||
if (this.isEditingActive) {
|
||||
this.onCancelButtonPointer();
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
rowEl.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
||||
this.flashFormulaRow(index);
|
||||
});
|
||||
},
|
||||
scrollToFormulaRow(index) {
|
||||
this.locateFormulaIssue(index);
|
||||
},
|
||||
/** 与顶部「failed to render」、列表行号标红同一规则 */
|
||||
scanFormulaRenderIssues() {
|
||||
const issues = {};
|
||||
const list = this.$refs.formulaList;
|
||||
const rowEls = list
|
||||
? list.querySelectorAll('.latex-formula-row:not(.latex-formula-row--new)')
|
||||
: this.$el
|
||||
? this.$el.querySelectorAll('.latex-formula-row:not(.latex-formula-row--new)')
|
||||
: [];
|
||||
rowEls.forEach((rowEl) => {
|
||||
const index = parseInt(rowEl.getAttribute('data-formula-index'), 10) || 0;
|
||||
if (!index) return;
|
||||
const wmath = rowEl.querySelector('wmath');
|
||||
if (isFormulaDisplayedAsRawSource(wmath)) {
|
||||
issues[index] = true;
|
||||
}
|
||||
});
|
||||
this.renderIssueIndexes = { ...issues };
|
||||
},
|
||||
refreshFormulaIssueState() {
|
||||
this.scanFormulaRenderIssues();
|
||||
},
|
||||
renderMath() {
|
||||
this.$nextTick(() => {
|
||||
this.$nextTick(() => {
|
||||
if (window.renderMathJax) {
|
||||
window.renderMathJax();
|
||||
}
|
||||
this.$nextTick(() => this.refreshFormulaIssueState());
|
||||
setTimeout(() => this.refreshFormulaIssueState(), 600);
|
||||
});
|
||||
});
|
||||
},
|
||||
@@ -506,6 +702,7 @@ export default {
|
||||
|
||||
if (result.empty) {
|
||||
this.emptyHint = 'noFormula';
|
||||
this.wordPreviewHtml = '';
|
||||
this.setPanelTip(
|
||||
'warning',
|
||||
this.$t('commonTable.importWordMathNoFormula'),
|
||||
@@ -513,12 +710,19 @@ export default {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (result.previewHtml) {
|
||||
this.storeWordReportSource(result.previewHtml, file);
|
||||
}
|
||||
if (result.allDuplicate) {
|
||||
this.setPanelTip(
|
||||
'info',
|
||||
this.$t('commonTable.importWordMathParsedNone', { parsed }),
|
||||
6000
|
||||
);
|
||||
if (result.previewHtml) {
|
||||
this.renderMath();
|
||||
this.scheduleWordReportDownload();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.emptyHint = '';
|
||||
@@ -534,6 +738,9 @@ export default {
|
||||
const list = this.$refs.formulaList;
|
||||
if (list) list.scrollTop = 0;
|
||||
});
|
||||
if (result.previewHtml) {
|
||||
this.scheduleWordReportDownload();
|
||||
}
|
||||
} else if (result.res) {
|
||||
this.setPanelTip(
|
||||
'error',
|
||||
@@ -647,6 +854,7 @@ export default {
|
||||
return this.mathField ? String(this.mathField.value || '').trim() : '';
|
||||
},
|
||||
saveNewInline() {
|
||||
if (this.cancelEditInProgress) return;
|
||||
if (!this.isAddingNew) return;
|
||||
|
||||
const latex = this.getMathFieldLatex();
|
||||
@@ -709,12 +917,21 @@ export default {
|
||||
});
|
||||
});
|
||||
},
|
||||
onCancelButtonPointer() {
|
||||
if (this.cancelEditInProgress) return;
|
||||
this.cancelEditInProgress = true;
|
||||
this.cancelInlineEdit();
|
||||
this.$nextTick(() => {
|
||||
this.cancelEditInProgress = false;
|
||||
});
|
||||
},
|
||||
cancelInlineEdit() {
|
||||
this.destroyMathField();
|
||||
this.editingIndex = null;
|
||||
this.isAddingNew = false;
|
||||
},
|
||||
saveInlineEdit(item) {
|
||||
if (this.cancelEditInProgress) return;
|
||||
if (this.editingIndex !== item.index) return;
|
||||
|
||||
const latex = this.getMathFieldLatex();
|
||||
@@ -892,6 +1109,60 @@ export default {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.toolbar-issue-inline {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px 4px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.toolbar-issue-inline > i {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.toolbar-issue-prefix {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolbar-issue-indexes {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toolbar-issue-index-link {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #f56c6c;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.toolbar-issue-index-link:hover {
|
||||
color: #f78989;
|
||||
}
|
||||
|
||||
.toolbar-issue-index-sep,
|
||||
.toolbar-issue-more {
|
||||
color: #f56c6c;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.word-upload-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -984,6 +1255,20 @@ export default {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.latex-panel-footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 6px 8px 2px;
|
||||
}
|
||||
|
||||
.latex-panel-footer-hint {
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #79bbff;
|
||||
}
|
||||
|
||||
.latex-formula-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
@@ -1016,6 +1301,11 @@ export default {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.latex-formula-row--highlight {
|
||||
background: #fff1f0 !important;
|
||||
box-shadow: inset 0 0 0 1px #fabcbc;
|
||||
}
|
||||
|
||||
.row-index {
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
@@ -1031,11 +1321,22 @@ export default {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.row-index--warn {
|
||||
color: #f56c6c;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.latex-data-panel--popover .row-index {
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.latex-data-panel--popover .row-index.row-index--warn,
|
||||
.latex-data-panel .row-index.row-index--warn {
|
||||
color: #f56c6c !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.row-edit {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -1229,6 +1530,12 @@ export default {
|
||||
.latex-empty--warn i {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.word-report-download-btn {
|
||||
padding: 0 6px;
|
||||
margin-right: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
:value="value"
|
||||
:typesettingType="typesettingType"
|
||||
class="paste-area text-container"
|
||||
:toolbar="!isAutomaticUpdate?['bold italic |customBlue removeBlue|LateX Latex2| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace']:['bold italic |customBlue removeBlue|Latex2| myuppercase myuppercasea Line MoreSymbols|subscript superscript|clearButton|searchreplace']"
|
||||
:toolbar="editorToolbar"
|
||||
style="
|
||||
/* white-space: pre-line; */
|
||||
line-height: 12px;
|
||||
@@ -36,13 +36,44 @@
|
||||
<script>
|
||||
import Tinymce from '@/components/page/components/Tinymce';
|
||||
export default {
|
||||
props: ['value','isAutomaticUpdate','height','id'],
|
||||
props: {
|
||||
value: {},
|
||||
isAutomaticUpdate: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
height: {},
|
||||
id: {},
|
||||
/** 仅「批量添加内容」弹窗开启:Word 导入公式(importWordMath) */
|
||||
enableImportWordMath: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
Tinymce
|
||||
},
|
||||
watch: {
|
||||
lineStyle() {}
|
||||
},
|
||||
computed: {
|
||||
editorToolbar() {
|
||||
const groups = ['bold italic', 'customBlue removeBlue'];
|
||||
if (!this.isAutomaticUpdate) {
|
||||
groups.push('LateX');
|
||||
}
|
||||
// if (this.enableImportWordMath) {
|
||||
// groups.push('importWordMath');
|
||||
// }
|
||||
groups.push(
|
||||
'myuppercase myuppercasea Line MoreSymbols',
|
||||
'subscript superscript',
|
||||
'clearButton',
|
||||
'searchreplace'
|
||||
);
|
||||
return [groups.join(' | ')];
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tableData: [],
|
||||
|
||||
@@ -486,12 +486,7 @@ export default {
|
||||
end_container_on_empty_block: true,
|
||||
content_css: 'default ',
|
||||
setup(ed) {
|
||||
// 禁止输入内容,但工具栏按钮仍然有效
|
||||
ed.on('keydown', function (e) {
|
||||
if (ed.mode.get() === 'readonly') {
|
||||
e.preventDefault(); // 阻止任何输入
|
||||
}
|
||||
});
|
||||
_this.$commonJS.bindReadonlyEditorKeydown(ed);
|
||||
ed.on('keydown', function (e) {
|
||||
// 检查是否按下回车键
|
||||
if (e.keyCode === 13) {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<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">
|
||||
<li v-if="showLatexToolbarButton && isEditComment && !isPreview" class="latex-toolbar-item">
|
||||
<el-popover
|
||||
v-model="latexPopoverVisible"
|
||||
placement="bottom-start"
|
||||
@@ -61,6 +61,7 @@
|
||||
class="latex-toolbar-trigger"
|
||||
@click.stop="toggleLatexPopover"
|
||||
>
|
||||
<i class="el-icon-s-data latex-toolbar-icon"></i>
|
||||
{{ $t('commonTable.mathFormula') }}
|
||||
</span>
|
||||
</el-popover>
|
||||
@@ -229,14 +230,6 @@
|
||||
>
|
||||
|
||||
<div :class="currentId == item.am_id ? 'glowing-border' : ''" style="position: relative">
|
||||
<div
|
||||
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
|
||||
:ref="'row-' + item.am_id"
|
||||
:key="item.am_id"
|
||||
@@ -991,7 +984,13 @@
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div v-if="isMenuVisible || bubbleVisible" class="wps-two-layer-bar selection-bubble no-select" :style="bubbleStyle">
|
||||
<div
|
||||
v-if="isMenuVisible || bubbleVisible"
|
||||
class="wps-two-layer-bar selection-bubble no-select"
|
||||
:style="bubbleStyle"
|
||||
@mousedown.stop.prevent="handleBubblePointerDown"
|
||||
@click.stop
|
||||
>
|
||||
<div class="bar-row" v-if="currentData">
|
||||
<div v-if="currentData.type == 0" class="h-group">
|
||||
<span :class="['h-item', { active: currentData.is_h1 == 1 }]" @click="changeTitle(currentData.is_h1 ? 0 : 1)">H1</span>
|
||||
@@ -1247,6 +1246,8 @@ export default {
|
||||
hasInit: false,
|
||||
selectedIds: [],
|
||||
isMouseSelecting: false,
|
||||
_bubblePointerActive: false,
|
||||
_bubblePointerTimer: null,
|
||||
_selectionSyncToCheckboxesTimer: null,
|
||||
_onDocumentSelectionChange: null,
|
||||
_onDocumentMouseUp: null,
|
||||
@@ -1255,6 +1256,7 @@ export default {
|
||||
displayList: [],
|
||||
currentTypeText: '',
|
||||
tinymceId: this.id || 'vue-tinymce-' + +new Date(),
|
||||
showLatexToolbarButton: false,
|
||||
latexPopoverVisible: false,
|
||||
_latexPopoverScrollHandler: null,
|
||||
_latexPopoverOutsideHandler: null
|
||||
@@ -1299,6 +1301,11 @@ export default {
|
||||
this.unbindLatexPopoverScrollListener();
|
||||
this.unbindLatexPopoverOutsideClick();
|
||||
}
|
||||
},
|
||||
isEditComment(val) {
|
||||
if (!val) {
|
||||
this.closeLatexPopover();
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -1416,6 +1423,19 @@ export default {
|
||||
if (this.mutObs) this.mutObs.disconnect();
|
||||
this.unbindLatexPopoverScrollListener();
|
||||
this.unbindLatexPopoverOutsideClick();
|
||||
if (this._bubblePointerTimer) {
|
||||
clearTimeout(this._bubblePointerTimer);
|
||||
this._bubblePointerTimer = null;
|
||||
}
|
||||
if (this._onDocumentSelectionChange) {
|
||||
document.removeEventListener('selectionchange', this._onDocumentSelectionChange);
|
||||
}
|
||||
if (this._onDocumentMouseUp) {
|
||||
document.removeEventListener('mouseup', this._onDocumentMouseUp);
|
||||
}
|
||||
if (this.$refs.scroll && this._onManuscriptMouseDown) {
|
||||
this.$refs.scroll.removeEventListener('mousedown', this._onManuscriptMouseDown);
|
||||
}
|
||||
|
||||
// 页面销毁前清理定时器和事件监听器
|
||||
},
|
||||
@@ -1459,7 +1479,7 @@ export default {
|
||||
this.isMouseSelecting = false;
|
||||
},
|
||||
handleDocumentSelectionChange() {
|
||||
if (this.isPreview) return;
|
||||
if (this.isPreview || this._bubblePointerActive) return;
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount === 0) return;
|
||||
|
||||
@@ -1487,7 +1507,7 @@ export default {
|
||||
this.currentId = allIds[0];
|
||||
this.currentData = rootItem;
|
||||
}
|
||||
} else {
|
||||
} else if (!this._bubblePointerActive && !this.isMenuVisible) {
|
||||
this.currentTag = '';
|
||||
this.currentTagData = {};
|
||||
this.currentSelection = {
|
||||
@@ -1654,15 +1674,13 @@ export default {
|
||||
this.$forceUpdate();
|
||||
},
|
||||
handleGlobalClick(e) {
|
||||
if(this.isPreview)return;
|
||||
// 如果点在气泡内,不关
|
||||
if (e.target.closest('.bubble-container')) return;
|
||||
if (this.isPreview) return;
|
||||
if (e.target.closest('.selection-bubble, .wps-two-layer-bar, .bubble-container')) return;
|
||||
|
||||
const selection = window.getSelection();
|
||||
const hasSelection = selection && selection.toString().trim() !== '';
|
||||
const isClickOnParagraph = e.target.closest('.pMain');
|
||||
|
||||
// 只有当:既没有选区,也没有点在段落上(点击了背景空白),才真正关闭
|
||||
if (!hasSelection && !isClickOnParagraph) {
|
||||
this.bubbleVisible = false;
|
||||
this.isMenuVisible = false;
|
||||
@@ -1670,9 +1688,16 @@ export default {
|
||||
this.currentData = {};
|
||||
this.currentId = '';
|
||||
|
||||
this.clearActiveGlow(); // 清除高亮
|
||||
this.clearActiveGlow();
|
||||
}
|
||||
},
|
||||
handleBubblePointerDown() {
|
||||
this._bubblePointerActive = true;
|
||||
clearTimeout(this._bubblePointerTimer);
|
||||
this._bubblePointerTimer = setTimeout(() => {
|
||||
this._bubblePointerActive = false;
|
||||
}, 200);
|
||||
},
|
||||
|
||||
// 辅助方法:清除发光边框
|
||||
clearActiveGlow() {
|
||||
@@ -2117,6 +2142,25 @@ export default {
|
||||
|
||||
this.isMenuVisible = false;
|
||||
},
|
||||
/** 保存/取消编辑后关闭段落高亮与浮动工具栏 */
|
||||
clearParagraphSelection() {
|
||||
this.closeMenu();
|
||||
this.currentId = '';
|
||||
this.currentData = {};
|
||||
this.currentTag = '';
|
||||
this.currentTagData = null;
|
||||
this.currentSelection = {
|
||||
label: '',
|
||||
mainId: '',
|
||||
index: 0,
|
||||
content: {}
|
||||
};
|
||||
this.clearActiveGlow();
|
||||
const selection = window.getSelection();
|
||||
if (selection) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
},
|
||||
handleAIProofreading() {
|
||||
if (this.currentId) {
|
||||
this.$api
|
||||
@@ -2175,7 +2219,14 @@ export default {
|
||||
this.onDelete();
|
||||
break;
|
||||
}
|
||||
this.closeMenu();
|
||||
if (!this.shouldKeepMenuOpenAfterAction(action)) {
|
||||
this.closeMenu();
|
||||
}
|
||||
},
|
||||
|
||||
/** 上移/下移后保持工具栏,便于连续操作 */
|
||||
shouldKeepMenuOpenAfterAction(action) {
|
||||
return action === 'up' || action === 'down';
|
||||
},
|
||||
|
||||
changeSort(type) {
|
||||
@@ -3023,26 +3074,24 @@ 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') {
|
||||
this.onProofSpanClick(event);
|
||||
}
|
||||
|
||||
const wmathEl = event.target && event.target.closest && event.target.closest('wmath');
|
||||
if (wmathEl && !this.isPreview) {
|
||||
const wmathId = wmathEl.getAttribute('data-id');
|
||||
await this.initializeEditor(event, item.am_id, 'text', item, index);
|
||||
this.$emit('onEdit', item.am_id, wmathId ? { wmathId } : undefined);
|
||||
const selection = window.getSelection();
|
||||
if (selection && !selection.isCollapsed && selection.toString().trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wmathEl = event.target && event.target.closest && event.target.closest('wmath');
|
||||
// if (wmathEl && !this.isPreview) {
|
||||
// const wmathId = wmathEl.getAttribute('data-id');
|
||||
// await this.initializeEditor(event, item.am_id, 'text', item, index);
|
||||
// this.$emit('onEdit', item.am_id, wmathId ? { wmathId } : undefined);
|
||||
// return;
|
||||
// }
|
||||
|
||||
await this.initializeEditor(event, item.am_id, 'text', item, index);
|
||||
},
|
||||
initializeEditor: throttle(async function (event, id, type, data, index) {
|
||||
@@ -4203,13 +4252,13 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
.HTitleBox li.latex-toolbar-item {
|
||||
padding: 2px 10px;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: rgb(43, 129, 239);
|
||||
color: #333;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
height: 26px;
|
||||
border-radius: 0;
|
||||
height: auto;
|
||||
}
|
||||
.operateBox {
|
||||
width: auto;
|
||||
@@ -4681,12 +4730,11 @@ font-weight: bold !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
list-style: none;
|
||||
padding: 2px 10px;
|
||||
padding: 0 10px;
|
||||
margin-left: 10px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background-color: rgb(43, 129, 239);
|
||||
height: 26px;
|
||||
background: transparent;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.latex-toolbar-trigger {
|
||||
@@ -4695,14 +4743,21 @@ font-weight: bold !important;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.latex-toolbar-trigger:hover {
|
||||
opacity: 0.9;
|
||||
color: #fff;
|
||||
color: #1654f7;
|
||||
}
|
||||
|
||||
.latex-toolbar-icon {
|
||||
margin-right: 4px;
|
||||
font-size: 13px;
|
||||
color: inherit;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
|
||||
@@ -263,6 +263,7 @@
|
||||
|
||||
import { mediaUrl } from '@/common/js/commonJS.js'; // 引入通用逻辑
|
||||
import DynamicTable from './DynamicTable.vue';
|
||||
import { isMathFormulaTableRecord } from '@/utils/mathFormulaModule';
|
||||
export default {
|
||||
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
|
||||
data() {
|
||||
@@ -473,8 +474,9 @@ export default {
|
||||
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 {
|
||||
@@ -485,7 +487,7 @@ export default {
|
||||
})
|
||||
: [];
|
||||
} else {
|
||||
this.tables =
|
||||
parsedList =
|
||||
res.data.list && res.data.list.length > 0
|
||||
? res.data.list.map((e) => {
|
||||
return {
|
||||
@@ -496,6 +498,7 @@ export default {
|
||||
})
|
||||
: [];
|
||||
}
|
||||
this.tables = parsedList.filter((item) => !isMathFormulaTableRecord(item));
|
||||
await this.filterData('table');
|
||||
|
||||
|
||||
|
||||
@@ -474,7 +474,7 @@
|
||||
<script>
|
||||
import DynamicTable from './DynamicTable.vue';
|
||||
import { mediaUrl } from '@/common/js/commonJS.js';
|
||||
import { isMathFormulaTableTitle, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule';
|
||||
import { isMathFormulaTableRecord, normalizeLatexTableApiResponse } from '@/utils/mathFormulaModule';
|
||||
|
||||
export default {
|
||||
props: ['articleId', 'imgWidth', 'imgHeight', 'scale', 'isEdit', 'isShowEdit', 'urlList', 'content'],
|
||||
@@ -677,7 +677,7 @@ export default {
|
||||
}
|
||||
},
|
||||
isMathFormulaTable(item) {
|
||||
return item && isMathFormulaTableTitle(item.title);
|
||||
return isMathFormulaTableRecord(item);
|
||||
},
|
||||
splitTablesAndMathFormulas(list) {
|
||||
const tables = [];
|
||||
|
||||
@@ -192,7 +192,7 @@ If you're still having trouble with Chrome, you could use other browsers to comp
|
||||
<div class="file_sty" v-for="(item,i) in article_response" style="margin-top: 15px;width: 50%;float: left">
|
||||
<img src="../../assets/img/icon_0.png" alt="" class="icon_img" />
|
||||
<a :href="mediaUrl + item.file_url" target="_blank" class="txt_pdf"
|
||||
>Manuscript
|
||||
>Rebuttal Letter
|
||||
<span style="margin-left: 10px; color: #888; font-size: 13px">{{ item.artr_ctime }}</span>
|
||||
<i class="el-icon-download" style="margin-left: 20px; color: #66b1ff; font-weight: bold"></i>
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
</div>
|
||||
|
||||
<div class="container" style="padding-top: 0px;" v-if="showSendEmail">
|
||||
<!-- 阻止回车在收件人/主题等输入框触发表单隐式提交(误点发送) -->
|
||||
<form class="mail-compose-form" @submit.prevent @keydown.enter="preventComposeEnterSubmit">
|
||||
<div class="mail_shuru" style="position: relative; display: flex; align-items: center;">
|
||||
<span class="mail_tit">{{ $t('mailboxSend.to') }}</span>
|
||||
|
||||
@@ -58,7 +60,7 @@
|
||||
</div>
|
||||
<div class="mail_shuru">
|
||||
<span class="mail_tit">{{ $t('mailboxSend.subject') }}</span>
|
||||
<el-input v-model="queryMail.sendtitle" class="mail_inp" ></el-input>
|
||||
<el-input v-model="queryMail.sendtitle" class="mail_inp" @keydown.enter.native.prevent></el-input>
|
||||
</div>
|
||||
<!-- <div class="mail_shuru" style="position: relative; display: flex; align-items: center;">
|
||||
<span class="mail_tit">{{ $t('mailboxSend.cc') }}</span>
|
||||
@@ -122,6 +124,7 @@
|
||||
<div class="action-buttons">
|
||||
<el-button
|
||||
type="primary"
|
||||
native-type="button"
|
||||
icon="el-icon-s-promotion"
|
||||
:loading="sendLoading"
|
||||
:disabled="sendLoading"
|
||||
@@ -133,6 +136,7 @@
|
||||
<!-- <el-button size="medium" :loading="saveDraftLoading" :disabled="saveDraftLoading" @click="handleSaveDraft">{{ $t('mailboxSend.saveDraft') }}</el-button> -->
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- 选择通讯录 -->
|
||||
@@ -285,6 +289,18 @@ import TemplateSelectorDialog from '@/components/page/components/email/TemplateS
|
||||
document.removeEventListener('click', this.handleOutsideAutocompleteClick, true);
|
||||
},
|
||||
methods: {
|
||||
/** 非编辑器区域按回车不触发发送;TinyMCE / 源码区仍正常换行 */
|
||||
preventComposeEnterSubmit(e) {
|
||||
if (!e || e.key !== 'Enter' || e.isComposing) return;
|
||||
const target = e.target;
|
||||
if (!target || typeof target.closest !== 'function') return;
|
||||
if (target.closest('.tox-tinymce') || target.closest('.mail-editor-wrap')) return;
|
||||
if (target.tagName === 'TEXTAREA' || target.isContentEditable) return;
|
||||
if (target.closest('.el-autocomplete-suggestion')) return;
|
||||
const ac = this.$refs.autocompleteTo;
|
||||
if (ac && ac.suggestionVisible) return;
|
||||
e.preventDefault();
|
||||
},
|
||||
closeAllDialogs() {
|
||||
// 点击“去新增模板”后:关闭模板选择弹窗以及可能打开的其它弹窗
|
||||
this.Librarybox = false;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import {
|
||||
importWordDocumentWithMath,
|
||||
extractWmathTableDataFromHtml,
|
||||
buildWordImportPreviewHtml,
|
||||
MATH_FORMULA_TABLE_TITLE,
|
||||
LEGACY_MATH_FORMULA_TABLE_TITLE,
|
||||
isMathFormulaTableTitle,
|
||||
buildWmathCellHtml
|
||||
buildWmathCellHtml,
|
||||
normalizeWmathCellHtml,
|
||||
repairImportedLatex,
|
||||
decodeLatexFromAttr
|
||||
} from '@/utils/wordMathImport';
|
||||
|
||||
export {
|
||||
@@ -86,7 +90,7 @@ export function buildClearedLatexTableResponse(data, existingTable) {
|
||||
return normalizeLatexTableApiResponse(data, existingTable);
|
||||
}
|
||||
|
||||
function normalizeLatexKey(latex) {
|
||||
export function normalizeLatexKey(latex) {
|
||||
return String(latex || '')
|
||||
.trim()
|
||||
.replace(/^\$\$?/, '')
|
||||
@@ -94,6 +98,31 @@ function normalizeLatexKey(latex) {
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
/** 相同 LaTeX 的列表行:duplicateOf[行号] = 首次出现行号;groups 为重复组 */
|
||||
export function buildDuplicateFormulaIndexMap(rows) {
|
||||
const keyToIndexes = {};
|
||||
(rows || []).forEach((row, i) => {
|
||||
const key = getLatexKeyFromCellText(row[0] && row[0].text);
|
||||
if (!key) return;
|
||||
if (!keyToIndexes[key]) keyToIndexes[key] = [];
|
||||
keyToIndexes[key].push(i + 1);
|
||||
});
|
||||
const duplicateOf = {};
|
||||
const groups = [];
|
||||
const countByFirst = {};
|
||||
Object.keys(keyToIndexes).forEach((key) => {
|
||||
const sorted = [...keyToIndexes[key]].sort((a, b) => a - b);
|
||||
if (sorted.length < 2) return;
|
||||
groups.push(sorted);
|
||||
const first = sorted[0];
|
||||
countByFirst[first] = sorted.length;
|
||||
sorted.forEach((idx) => {
|
||||
if (idx !== first) duplicateOf[idx] = first;
|
||||
});
|
||||
});
|
||||
return { duplicateOf, groups, countByFirst };
|
||||
}
|
||||
|
||||
export function isSameLatexContent(a, b) {
|
||||
const keyA = normalizeLatexKey(a);
|
||||
const keyB = normalizeLatexKey(b);
|
||||
@@ -107,9 +136,13 @@ export function getLatexFromCellHtml(text) {
|
||||
div.innerHTML = String(text);
|
||||
const wmath = div.querySelector('wmath');
|
||||
if (wmath) {
|
||||
return (wmath.getAttribute('data-latex') || wmath.textContent || '').trim();
|
||||
const attr = wmath.getAttribute('data-latex');
|
||||
if (attr != null && String(attr).trim()) {
|
||||
return repairImportedLatex(decodeLatexFromAttr(attr));
|
||||
}
|
||||
return repairImportedLatex((wmath.textContent || '').trim());
|
||||
}
|
||||
return (div.textContent || text || '').trim();
|
||||
return repairImportedLatex((div.textContent || text || '').trim());
|
||||
}
|
||||
|
||||
export function getLatexKeyFromCellText(text) {
|
||||
@@ -125,7 +158,10 @@ export function getLatexKeyFromCellText(text) {
|
||||
|
||||
export function isLatexDataHeaderRow(row) {
|
||||
const text = row && row[0] ? row[0].text : '';
|
||||
return /Latex Data|数字公式/i.test(String(text || ''));
|
||||
const plain = String(text || '')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.trim();
|
||||
return /latex\s*data|数字公式/i.test(plain);
|
||||
}
|
||||
|
||||
export function parseFormulaRowsFromTableData(tableData) {
|
||||
@@ -180,7 +216,7 @@ export function mergeLatexDataTable(existingTableData, incomingTableData) {
|
||||
|
||||
export function getLatexDataTableFromList(tables, preferredAmtId) {
|
||||
const list = (Array.isArray(tables) ? tables : []).filter(
|
||||
(item) => item && isMathFormulaTableTitle(item.title)
|
||||
(item) => item && isMathFormulaTableRecord(item)
|
||||
);
|
||||
if (!list.length) return null;
|
||||
if (preferredAmtId != null && preferredAmtId !== '') {
|
||||
@@ -201,11 +237,13 @@ export function buildFormulaItemList(tables, preferredAmtId) {
|
||||
? tableItem.table
|
||||
: parseTableDataFromApiField(tableItem);
|
||||
return parseFormulaRowsFromTableData(tableData).map((row, index) => {
|
||||
const html = row[0] && row[0].text ? row[0].text : '';
|
||||
const rawHtml = row[0] && row[0].text ? row[0].text : '';
|
||||
const html = normalizeWmathCellHtml(rawHtml);
|
||||
return {
|
||||
index: index + 1,
|
||||
html,
|
||||
latex: getLatexFromCellHtml(html)
|
||||
latex: getLatexFromCellHtml(html),
|
||||
hasIssue: false
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -240,7 +278,11 @@ export async function parseWordMathFormulas(file) {
|
||||
if (!html) {
|
||||
throw new Error('empty content');
|
||||
}
|
||||
return extractWmathTableDataFromHtml(html);
|
||||
const extracted = extractWmathTableDataFromHtml(html);
|
||||
return {
|
||||
...extracted,
|
||||
previewHtml: buildWordImportPreviewHtml(html)
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMathFormulaTablePayload(articleId, tableData) {
|
||||
@@ -254,10 +296,11 @@ export function buildMathFormulaTablePayload(articleId, tableData) {
|
||||
}
|
||||
|
||||
export async function upsertLatexDataTableFromWord(api, articleId, file, existingTable) {
|
||||
const { tableData: incomingTableData, html: incomingHtml } = await parseWordMathFormulas(file);
|
||||
const { tableData: incomingTableData, html: incomingHtml, previewHtml } = await parseWordMathFormulas(file);
|
||||
const incomingRows = parseFormulaRowsFromTableData(incomingTableData);
|
||||
|
||||
const parsedCount = incomingRows.length;
|
||||
const preview = previewHtml || '';
|
||||
|
||||
if (!parsedCount) {
|
||||
return {
|
||||
@@ -267,7 +310,8 @@ export async function upsertLatexDataTableFromWord(api, articleId, file, existin
|
||||
addedCount: 0,
|
||||
totalCount: 0,
|
||||
tableData: [],
|
||||
html: ''
|
||||
html: '',
|
||||
previewHtml: preview
|
||||
};
|
||||
}
|
||||
|
||||
@@ -291,6 +335,7 @@ export async function upsertLatexDataTableFromWord(api, articleId, file, existin
|
||||
allDuplicate: true,
|
||||
tableData: merged.tableData,
|
||||
html: merged.html,
|
||||
previewHtml: preview,
|
||||
...importMeta
|
||||
};
|
||||
}
|
||||
@@ -309,6 +354,7 @@ export async function upsertLatexDataTableFromWord(api, articleId, file, existin
|
||||
res,
|
||||
tableData: merged.tableData,
|
||||
html: merged.html,
|
||||
previewHtml: preview,
|
||||
...importMeta
|
||||
};
|
||||
}
|
||||
@@ -322,6 +368,7 @@ export async function upsertLatexDataTableFromWord(api, articleId, file, existin
|
||||
res,
|
||||
tableData: merged.tableData,
|
||||
html: merged.html,
|
||||
previewHtml: preview,
|
||||
...importMeta
|
||||
};
|
||||
}
|
||||
@@ -373,3 +420,94 @@ export function buildFormulaRowFromLatex(latex) {
|
||||
export async function addMathFormulaTableFromWord(api, articleId, file, existingTable) {
|
||||
return upsertLatexDataTableFromWord(api, articleId, file, existingTable);
|
||||
}
|
||||
|
||||
/** 判断 Word 解析出的 table 二维数组是否为数字公式表 */
|
||||
export function isWordTableArrayMathFormula(tableArray) {
|
||||
if (!Array.isArray(tableArray) || !tableArray.length) return false;
|
||||
return isMathFormulaTableRecord({ table: tableArray });
|
||||
}
|
||||
|
||||
/** 投稿/校对相关:统一判断接口记录或本地 table 是否为数字公式表 */
|
||||
export function isMathFormulaTableRecord(item) {
|
||||
if (!item) return false;
|
||||
if (isMathFormulaTableTitle(item.title)) return true;
|
||||
if (item.html_data && isMathFormulaTableTitle(String(item.html_data))) return true;
|
||||
|
||||
let table = item.table;
|
||||
if (!Array.isArray(table)) {
|
||||
const raw = item.table_data != null && item.table_data !== '' ? item.table_data : item.table;
|
||||
if (typeof raw === 'string' && raw) {
|
||||
try {
|
||||
table = JSON.parse(raw);
|
||||
} catch (e) {
|
||||
table = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(table) && table.length && isLatexDataHeaderRow(table[0])) {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(table) && table.length) {
|
||||
const formulaRows = parseFormulaRowsFromTableData(table);
|
||||
const dataRows = table.filter((row) => row && row.length && !isLatexDataHeaderRow(row));
|
||||
if (formulaRows.length > 0 && formulaRows.length === dataRows.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @deprecated 使用 isMathFormulaTableRecord */
|
||||
export const isArticleMathFormulaTable = isMathFormulaTableRecord;
|
||||
|
||||
export function buildArticleRegularTableItem(tableArray) {
|
||||
return {
|
||||
table: JSON.stringify(Array.isArray(tableArray) ? tableArray : []),
|
||||
type: 0,
|
||||
html_data: '',
|
||||
title: ''
|
||||
};
|
||||
}
|
||||
|
||||
export function buildArticleLatexDataTableItem(tableData) {
|
||||
const rows = tableData || [];
|
||||
if (!rows.length) return null;
|
||||
return {
|
||||
table: JSON.stringify(rows),
|
||||
type: 0,
|
||||
html_data: buildLatexDataHtmlData(),
|
||||
title: MATH_FORMULA_TABLE_TITLE
|
||||
};
|
||||
}
|
||||
|
||||
/** 投稿 addArticleTable:普通表格 + 可选 Latex Data 公式表(与校对字段一致) */
|
||||
export function buildArticleAddTableListItems(wordTableArrays, latexTableData) {
|
||||
const list = (wordTableArrays || [])
|
||||
.filter((tableArray) => !isWordTableArrayMathFormula(tableArray))
|
||||
.map((tableArray) => buildArticleRegularTableItem(tableArray));
|
||||
|
||||
const latexItem = buildArticleLatexDataTableItem(latexTableData);
|
||||
if (latexItem) list.push(latexItem);
|
||||
return list;
|
||||
}
|
||||
|
||||
/** 投稿上传 Word:解析物理表格 + 数字公式,生成 addArticleTable 的 list */
|
||||
export async function buildArticleAddTableListFromWordFile(file, extractWordTablesToArrays) {
|
||||
if (!file || typeof extractWordTablesToArrays !== 'function') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const wordTables = await new Promise((resolve) => {
|
||||
extractWordTablesToArrays(file, resolve);
|
||||
});
|
||||
|
||||
// let latexTableData = [];
|
||||
// try {
|
||||
// const parsed = await parseWordMathFormulas(file);
|
||||
// latexTableData = (parsed && parsed.tableData) || [];
|
||||
// } catch (err) {
|
||||
// console.warn('parseWordMathFormulas failed:', err);
|
||||
// }
|
||||
|
||||
return buildArticleAddTableListItems(wordTables, null);
|
||||
}
|
||||
|
||||
324
src/utils/wordImportReport.js
Normal file
324
src/utils/wordImportReport.js
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* Word 公式导入源稿:生成可离线打开的独立 HTML 报告
|
||||
*/
|
||||
|
||||
import { normalizeLatexKey } from './mathFormulaModule';
|
||||
import {
|
||||
extractLatexFromWmathElement,
|
||||
isAttrLatexStructurallyBroken
|
||||
} from './wordMathImport';
|
||||
|
||||
const REPORT_STYLES = `
|
||||
body { margin: 0; font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; font-size: 14px; line-height: 1.6; color: #303133; background: #f5f7fa; }
|
||||
.report-wrap { max-width: 920px; margin: 0 auto; padding: 20px 24px 40px; }
|
||||
.report-header { background: #fff; border: 1px solid #ebeef5; border-radius: 8px; padding: 16px 20px; margin-bottom: 16px; }
|
||||
.report-header h1 { margin: 0 0 10px; font-size: 18px; font-weight: 600; }
|
||||
.report-meta { font-size: 12px; color: #909399; margin-bottom: 12px; }
|
||||
.report-legend { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; font-size: 12px; }
|
||||
.legend-ok { padding: 2px 10px; border-radius: 4px; font-weight: 600; color: #67c23a; background: #f0f9eb; border: 1px solid #b3e19d; }
|
||||
.legend-err { padding: 2px 10px; border-radius: 4px; font-weight: 600; color: #f56c6c; background: #fef0f0; border: 1px solid #fbc4c4; }
|
||||
.legend-dup { padding: 2px 10px; border-radius: 4px; font-weight: 600; color: #e6a23c; background: #fdf6ec; border: 1px solid #f5dab1; }
|
||||
.report-issues-sticky { position: sticky; top: 0; z-index: 30; margin: 0 0 12px; padding: 10px 20px 12px; background: #fff; border: 1px solid #fde2e2; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.08); }
|
||||
.report-issues { margin: 0; font-size: 13px; line-height: 1.5; color: #f56c6c; }
|
||||
.report-issues a { color: #f56c6c; font-weight: 600; margin: 0 4px; text-decoration: none; }
|
||||
.report-issues a:hover { text-decoration: underline; }
|
||||
.report-dup { margin-top: 8px; font-size: 13px; color: #e6a23c; }
|
||||
.report-dup a { color: #e6a23c; font-weight: 600; margin: 0 4px; }
|
||||
.report-dup-group { margin: 4px 0 0; font-size: 12px; color: #909399; }
|
||||
.report-body { background: #fff; border: 1px solid #ebeef5; border-radius: 8px; padding: 20px 24px; }
|
||||
.report-body p { margin: 0 0 0.65em; }
|
||||
.report-body table { border-collapse: collapse; margin: 0.5em 0 1em; width: 100%; }
|
||||
.report-body td, .report-body th { border: 1px solid #dcdfe6; padding: 6px 8px; }
|
||||
.word-import-formula-anchor { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 4px 6px 4px 0; padding: 4px 8px 4px 6px; vertical-align: middle; border-radius: 6px; border: 2px solid transparent; }
|
||||
.word-import-formula-index-badge { display: inline-block; min-width: 1.5em; padding: 1px 5px; font-size: 11px; font-weight: 700; text-align: center; color: #409eff; background: #ecf5ff; border-radius: 4px; }
|
||||
.word-import-dup-tag, .word-import-dup-count-tag { display: inline-block; font-size: 11px; font-weight: 600; line-height: 1.3; padding: 1px 6px; border-radius: 4px; white-space: nowrap; }
|
||||
.word-import-dup-tag { color: #e6a23c; background: #fdf6ec; border: 1px solid #f5dab1; }
|
||||
.word-import-dup-count-tag { color: #fff; background: #e6a23c; border: 1px solid #e6a23c; }
|
||||
.word-import-formula-anchor--nomap { border-color: transparent; background: transparent; padding: 0 2px; }
|
||||
.word-import-formula-anchor--nomap .word-import-formula-index-badge { display: none; }
|
||||
.word-import-formula-anchor--nomap::after { display: none !important; }
|
||||
.word-import-formula-anchor--ok { border-color: #67c23a; background: #f0f9eb; }
|
||||
.word-import-formula-anchor--ok .word-import-formula-index-badge { color: #fff; background: #67c23a; }
|
||||
.word-import-formula-anchor--ok:not(.word-import-formula-anchor--dup):not(.word-import-formula-anchor--dup-first)::after { content: "通过"; font-size: 11px; font-weight: 600; color: #67c23a; }
|
||||
.word-import-formula-anchor--error { border-color: #f56c6c; background: #fef0f0; box-shadow: 0 0 0 1px rgba(245,108,108,.25); }
|
||||
.word-import-formula-anchor--error .word-import-formula-index-badge { color: #fff; background: #f56c6c; }
|
||||
.word-import-formula-anchor--error:not(.word-import-formula-anchor--dup):not(.word-import-formula-anchor--dup-first)::after { content: "需修改"; font-size: 11px; font-weight: 600; color: #f56c6c; }
|
||||
.word-import-formula-anchor--dup { border-color: #e6a23c !important; background: #fdf6ec !important; box-shadow: 0 0 0 1px rgba(230,162,60,.3); }
|
||||
.word-import-formula-anchor--dup .word-import-formula-index-badge { color: #fff !important; background: #e6a23c !important; }
|
||||
.word-import-formula-anchor--dup-first { border-color: #e6a23c; background: #fffbf0; }
|
||||
.word-import-formula-anchor--error.word-import-formula-anchor--dup { border-color: #f56c6c !important; background: linear-gradient(135deg, #fef0f0 0%, #fdf6ec 100%) !important; }
|
||||
.word-import-orphan-latex { margin: 0.35em 0 0.65em; padding: 8px 10px; border-left: 4px solid #f56c6c; background: #fef0f0; color: #c45656; border-radius: 0 6px 6px 0; }
|
||||
wmath { display: inline-block; max-width: 100%; }
|
||||
wmath[data-wrap="block"] { display: block; margin: 0.35em 0; }
|
||||
`;
|
||||
|
||||
function escapeHtml(text) {
|
||||
return String(text || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function latexKeyFromWmath(wmath) {
|
||||
if (!wmath) return '';
|
||||
const latex = extractLatexFromWmathElement(wmath) || (wmath.textContent || '').trim();
|
||||
return normalizeLatexKey(latex);
|
||||
}
|
||||
|
||||
/** Word 源稿内按公式序号检测重复(相同 LaTeX,后者标为重复) */
|
||||
export function computeDuplicateMapFromPreviewHtml(previewHtml) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = previewHtml || '';
|
||||
const keyToIndexes = {};
|
||||
|
||||
div.querySelectorAll('.word-import-formula-anchor').forEach((anchor) => {
|
||||
const idx = parseInt(anchor.getAttribute('data-formula-index'), 10);
|
||||
const key = latexKeyFromWmath(anchor.querySelector('wmath'));
|
||||
if (!idx || !key) return;
|
||||
if (!keyToIndexes[key]) keyToIndexes[key] = [];
|
||||
keyToIndexes[key].push(idx);
|
||||
});
|
||||
|
||||
const duplicateOf = {};
|
||||
const groups = [];
|
||||
const countByFirst = {};
|
||||
|
||||
Object.keys(keyToIndexes).forEach((key) => {
|
||||
const sorted = [...keyToIndexes[key]].sort((a, b) => a - b);
|
||||
if (sorted.length < 2) return;
|
||||
groups.push(sorted);
|
||||
const first = sorted[0];
|
||||
countByFirst[first] = sorted.length;
|
||||
sorted.forEach((idx) => {
|
||||
if (idx !== first) duplicateOf[idx] = first;
|
||||
});
|
||||
});
|
||||
|
||||
return { duplicateOf, groups, countByFirst };
|
||||
}
|
||||
|
||||
/** wmath data-id → Word 稿内序号 */
|
||||
export function buildWmathIdToWordIndexMap(previewHtml) {
|
||||
const map = {};
|
||||
if (!previewHtml) return map;
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = previewHtml;
|
||||
div.querySelectorAll('.word-import-formula-anchor').forEach((anchor) => {
|
||||
const wordIdx = parseInt(anchor.getAttribute('data-formula-index'), 10);
|
||||
const wmath = anchor.querySelector('wmath');
|
||||
const id = wmath && wmath.getAttribute('data-id');
|
||||
if (id && wordIdx) map[id] = wordIdx;
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
/** 列表行号 → Word 稿内序号(仅 data-id 能对上的公式) */
|
||||
export function buildListIndexToWordIndexMap(indexByWmathId, previewHtml) {
|
||||
const wordById = buildWmathIdToWordIndexMap(previewHtml);
|
||||
const listToWord = {};
|
||||
Object.keys(indexByWmathId || {}).forEach((id) => {
|
||||
const listIdx = indexByWmathId[id];
|
||||
const wordIdx = wordById[id];
|
||||
if (listIdx && wordIdx) listToWord[listIdx] = wordIdx;
|
||||
});
|
||||
return listToWord;
|
||||
}
|
||||
|
||||
function removeDupTags(anchor) {
|
||||
anchor.querySelectorAll('.word-import-dup-tag, .word-import-dup-count-tag').forEach((el) => el.remove());
|
||||
}
|
||||
|
||||
function insertDupTag(anchor, className, text) {
|
||||
if (!className || !text) return;
|
||||
const tag = document.createElement('span');
|
||||
tag.className = className;
|
||||
tag.textContent = text;
|
||||
const indexBadge = anchor.querySelector('.word-import-formula-index-badge');
|
||||
if (className === 'word-import-dup-count-tag' && indexBadge) {
|
||||
indexBadge.after(tag);
|
||||
return;
|
||||
}
|
||||
const wmath = anchor.querySelector('wmath');
|
||||
anchor.insertBefore(tag, wmath || null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 标绿/红(列表 renderIssueIndexes)+ 橙(重复序号)
|
||||
* duplicateOf: { 5: 2 } 表示序号 5 与序号 2 重复
|
||||
*/
|
||||
export function applyReportMarksToPreviewHtml(previewHtml, options) {
|
||||
if (!previewHtml) return '';
|
||||
const {
|
||||
issueIndexes = {},
|
||||
indexByWmathId = {},
|
||||
listDuplicateOf = {},
|
||||
listDuplicateCountByFirst = {},
|
||||
dupTagRepeat = '重复#{n}',
|
||||
dupTagCount = '×{n}'
|
||||
} = options || {};
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = previewHtml;
|
||||
const issues = issueIndexes || {};
|
||||
|
||||
div.querySelectorAll('.word-import-formula-anchor').forEach((anchor) => {
|
||||
const wmath = anchor.querySelector('wmath');
|
||||
const wmathId = wmath && wmath.getAttribute('data-id');
|
||||
const listIndex = wmathId && indexByWmathId[wmathId] ? indexByWmathId[wmathId] : null;
|
||||
|
||||
removeDupTags(anchor);
|
||||
anchor.classList.remove(
|
||||
'word-import-formula-anchor--error',
|
||||
'word-import-formula-anchor--ok',
|
||||
'word-import-formula-anchor--dup',
|
||||
'word-import-formula-anchor--dup-first',
|
||||
'word-import-formula-anchor--nomap'
|
||||
);
|
||||
anchor.removeAttribute('data-list-index');
|
||||
anchor.removeAttribute('id');
|
||||
|
||||
const badge = anchor.querySelector('.word-import-formula-index-badge');
|
||||
|
||||
if (!listIndex) {
|
||||
anchor.classList.add('word-import-formula-anchor--nomap');
|
||||
if (badge) badge.textContent = '';
|
||||
if (wmath) wmath.removeAttribute('data-import-error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (badge) badge.textContent = String(listIndex);
|
||||
anchor.id = 'formula-list-' + listIndex;
|
||||
anchor.setAttribute('data-list-index', String(listIndex));
|
||||
|
||||
const bad = !!issues[listIndex];
|
||||
const dupFirst = listDuplicateOf[listIndex];
|
||||
const dupCount = listDuplicateCountByFirst[listIndex];
|
||||
|
||||
if (dupFirst) {
|
||||
anchor.classList.add('word-import-formula-anchor--dup');
|
||||
const label = String(dupTagRepeat).replace('{n}', String(dupFirst));
|
||||
insertDupTag(anchor, 'word-import-dup-tag', label);
|
||||
} else if (dupCount > 1) {
|
||||
anchor.classList.add('word-import-formula-anchor--dup-first');
|
||||
const label = String(dupTagCount).replace('{n}', String(dupCount));
|
||||
insertDupTag(anchor, 'word-import-dup-count-tag', label);
|
||||
}
|
||||
|
||||
if (bad) {
|
||||
anchor.classList.add('word-import-formula-anchor--error');
|
||||
if (wmath) wmath.setAttribute('data-import-error', '1');
|
||||
} else {
|
||||
anchor.classList.add('word-import-formula-anchor--ok');
|
||||
if (wmath) wmath.removeAttribute('data-import-error');
|
||||
}
|
||||
});
|
||||
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/** @deprecated 使用 applyReportMarksToPreviewHtml */
|
||||
export function applyIssueMarksToPreviewHtml(previewHtml, issueIndexes, indexByWmathId) {
|
||||
return applyReportMarksToPreviewHtml(previewHtml, { issueIndexes, indexByWmathId });
|
||||
}
|
||||
|
||||
/** 顶部仅列出有问题的列表行号 */
|
||||
function buildListIssueLinksHtml(issueIndexes, issueSummaryPrefix) {
|
||||
const nums = Object.keys(issueIndexes || {})
|
||||
.filter((k) => issueIndexes[k])
|
||||
.map((k) => parseInt(k, 10))
|
||||
.filter((n) => n > 0)
|
||||
.sort((a, b) => a - b);
|
||||
if (!nums.length) return '';
|
||||
const links = nums.map((listIdx) => `<a href="#formula-list-${listIdx}">${listIdx}</a>`).join('、');
|
||||
return `<div class="report-issues-sticky"><p class="report-issues">${escapeHtml(issueSummaryPrefix)} ${links}</p></div>`;
|
||||
}
|
||||
|
||||
export function buildWordImportReportHtml(options) {
|
||||
const {
|
||||
previewBodyHtml = '',
|
||||
issueIndexes = {},
|
||||
indexByWmathId = {},
|
||||
listDuplicateOf = {},
|
||||
listDuplicateCountByFirst = {},
|
||||
title = 'Word formula report',
|
||||
hint = '',
|
||||
legendOk = 'Green · OK',
|
||||
legendErr = 'Red · needs fix',
|
||||
issueSummaryPrefix = 'Rows to fix:',
|
||||
dupTagRepeat = '重复#{n}',
|
||||
dupTagCount = '×{n}',
|
||||
generatedAt = new Date().toLocaleString()
|
||||
} = options || {};
|
||||
|
||||
const bodyInner = applyReportMarksToPreviewHtml(previewBodyHtml, {
|
||||
issueIndexes,
|
||||
indexByWmathId,
|
||||
listDuplicateOf,
|
||||
listDuplicateCountByFirst,
|
||||
dupTagRepeat,
|
||||
dupTagCount
|
||||
});
|
||||
|
||||
const issueBlock = buildListIssueLinksHtml(issueIndexes, issueSummaryPrefix);
|
||||
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>${escapeHtml(title)}</title>
|
||||
<style>${REPORT_STYLES}</style>
|
||||
<script>
|
||||
window.MathJax = {
|
||||
tex: { inlineMath: [['$','$'], ['\\\\(','\\\\)']], displayMath: [['$$','$$'], ['\\\\[','\\\\]']] },
|
||||
svg: { fontCache: 'global' }
|
||||
};
|
||||
function prepareWmathElements() {
|
||||
document.querySelectorAll('wmath').forEach(function (el) {
|
||||
var latex = el.getAttribute('data-latex');
|
||||
if (!latex) return;
|
||||
var raw = latex.trim().replace(/^\\$\\$/, '').replace(/\\$\\$$/, '').replace(/^\\$/, '').replace(/\\$$/, '').trim();
|
||||
if (raw) el.innerHTML = '$$' + raw + '$$';
|
||||
});
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
prepareWmathElements();
|
||||
if (window.MathJax && MathJax.typesetPromise) {
|
||||
MathJax.typesetPromise().catch(function () {});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="report-wrap">
|
||||
<header class="report-header">
|
||||
<h1>${escapeHtml(title)}</h1>
|
||||
<p class="report-meta">${escapeHtml(generatedAt)}</p>
|
||||
<div class="report-legend">
|
||||
<span class="legend-ok">${escapeHtml(legendOk)}</span>
|
||||
<span class="legend-err">${escapeHtml(legendErr)}</span>
|
||||
</div>
|
||||
${hint ? `<p class="report-meta">${escapeHtml(hint)}</p>` : ''}
|
||||
</header>
|
||||
${issueBlock}
|
||||
<main class="report-body">${bodyInner}</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
export function downloadHtmlFile(html, filename) {
|
||||
const name = filename || `word-formula-report-${Date.now()}.html`;
|
||||
const blob = new Blob([html], { type: 'text/html;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = name;
|
||||
a.rel = 'noopener';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
@@ -97,12 +97,389 @@ export function createWmathHtml(latex, wrap) {
|
||||
|
||||
function firstByLocal(parent, name) {
|
||||
if (!parent) return null;
|
||||
return Array.from(parent.children || []).find((child) => localName(child) === name) || null;
|
||||
const target = String(name || '').toLowerCase();
|
||||
return Array.from(parent.children || []).find((child) => localName(child) === target) || null;
|
||||
}
|
||||
|
||||
function allByLocal(parent, name) {
|
||||
if (!parent) return [];
|
||||
return Array.from(parent.children || []).filter((child) => localName(child) === name);
|
||||
const target = String(name || '').toLowerCase();
|
||||
return Array.from(parent.children || []).filter((child) => localName(child) === target);
|
||||
}
|
||||
|
||||
/** Word OMML m:dPr 上的分隔符(begChr / endChr) */
|
||||
function getDelimCharFromD(node, which) {
|
||||
const dPr = firstByLocal(node, 'dPr');
|
||||
if (!dPr) return null;
|
||||
const el = firstByLocal(dPr, which === 'beg' ? 'begChr' : 'endChr');
|
||||
if (!el) return null;
|
||||
return el.getAttribute('m:val') || el.getAttribute('val') || el.textContent || null;
|
||||
}
|
||||
|
||||
function isNormDelimiterChar(ch) {
|
||||
if (ch == null || ch === '') return false;
|
||||
const c = String(ch).trim();
|
||||
return c === '|' || c === '‖' || c === '∥' || c === '│' || c === '\u2016' || c === '||';
|
||||
}
|
||||
|
||||
/** matrix 内两行均为 with probability */
|
||||
function isMatrixWithProbabilityRows(latex) {
|
||||
const s = String(latex || '');
|
||||
if (!/\\begin\{matrix\}/.test(s)) return false;
|
||||
const n = (s.match(/with\s*probability/gi) || []).length;
|
||||
return n >= 2;
|
||||
}
|
||||
|
||||
/** 分段 matrix:if/otherwise 或 with probability */
|
||||
function isPiecewiseMatrixContent(latex) {
|
||||
const s = String(latex || '');
|
||||
if (!/\\begin\{matrix\}/.test(s)) return false;
|
||||
if (/,\s*(?:&\s*)?if\b/i.test(s) && /\botherwise\b/i.test(s)) return true;
|
||||
return isMatrixWithProbabilityRows(s);
|
||||
}
|
||||
|
||||
/** 已是标准 matrix/cases 分段(无需再修) */
|
||||
function isProperPiecewiseLatex(latex) {
|
||||
const s = String(latex || '');
|
||||
if (isPiecewiseMatrixContent(s)) {
|
||||
const hasDelims = /\\left\s*\\?\{/.test(s) && /\\right\s*[\.\)]?/.test(s) && /\\end\{matrix\}/.test(s);
|
||||
if (!hasDelims) return false;
|
||||
if (isMatrixWithProbabilityRows(s)) {
|
||||
return /,\s*&\s*with\s+probability/gi.test(s);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (/\\begin\{(matrix|cases|aligned)\}/.test(s) && /\\end\{(matrix|cases|aligned)\}/.test(s)) {
|
||||
return true;
|
||||
}
|
||||
if (/\\left\s*\\?\{/.test(s) && /\\right\s*[\.\)]?/.test(s) && /\\\\/.test(s)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Word 将分段压成一行:{…, if …, otherwise} / \atop / \text{if} 等 */
|
||||
function isFlattenedPiecewiseBrace(latex) {
|
||||
const inner = String(latex || '');
|
||||
if (isProperPiecewiseLatex(inner)) return false;
|
||||
if (/\\atop\b/i.test(inner)) return true;
|
||||
if (!/(?:\\{|{)/.test(inner)) return false;
|
||||
const hasIf = /,\s*(?:\\text\{)?if\b/i.test(inner);
|
||||
const hasOtherwise = /\botherwise\b/i.test(inner);
|
||||
return hasIf && hasOtherwise;
|
||||
}
|
||||
|
||||
function stripTrailingPiecewiseGarbage(latex) {
|
||||
return String(latex || '')
|
||||
.trim()
|
||||
.replace(/\)\s*$/, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
function repairUnicodeMathSymbols(latex) {
|
||||
return String(latex || '')
|
||||
.replace(/\u223c/g, ' \\sim ')
|
||||
.replace(/∼/g, ' \\sim ')
|
||||
.replace(/\u2212/g, '-');
|
||||
}
|
||||
|
||||
function repairTildeDistribution(latex) {
|
||||
return repairUnicodeMathSymbols(String(latex || ''))
|
||||
.replace(/([a-zA-Z])\s*\\sim\s*U\s*\(/g, '$1 \\sim \\mathcal{U}(')
|
||||
.replace(/([a-zA-Z])\s*\\sim\s*\\mathcal\{U\}\s*\(/g, '$1 \\sim \\mathcal{U}(')
|
||||
.replace(/([a-zA-Z])\s*~\s*U\s*\(/g, '$1 \\sim \\mathcal{U}(')
|
||||
.replace(/([a-zA-Z])\s*~\s*N\s*\(/g, '$1 \\sim \\mathcal{N}(');
|
||||
}
|
||||
|
||||
/**
|
||||
* Word 导出:={\\begin{matrix}…\\end{matrix}),缺 \\left\\{、仅有 \\right.、缺 &、Unicode ∼ 等
|
||||
*/
|
||||
function repairMatrixPiecewiseDelimiters(latex) {
|
||||
let s = normalizeProbabilityText(repairTildeDistribution(String(latex || '').trim()));
|
||||
s = s.replace(/,\s*otherwise\b/gi, ', & otherwise');
|
||||
s = s.replace(/,with\s+probability/gi, ', & with probability');
|
||||
s = s.replace(/,\s+with\s+probability/gi, ', & with probability');
|
||||
s = s.replace(/\\end\{matrix\}\s*\)\s*$/i, '\\end{matrix} \\right.');
|
||||
|
||||
if (!/\\begin\{matrix\}/.test(s) || !isPiecewiseMatrixContent(s)) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const hasLeft = /\\left\s*\\?\{/.test(s);
|
||||
const hasRight = /\\right\s*[\.\)]?/.test(s);
|
||||
if (hasLeft && hasRight && isProperPiecewiseLatex(s)) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const m = s.match(/^([\s\S]+?)=\s*(?:\\{|{)\s*\\begin\{matrix\}\s*([\s\S]+?)\\end\{matrix\}\s*(?:\\right\.)?\s*$/i);
|
||||
if (!m) return s;
|
||||
|
||||
let inner = m[2].trim();
|
||||
inner = inner.replace(/,with\s+probability/gi, ', & with probability');
|
||||
inner = inner.replace(/,\s+with\s+probability/gi, ', & with probability');
|
||||
return `${m[1].trim()} = \\left\\{ \\begin{matrix} ${inner} \\end{matrix} \\right.`;
|
||||
}
|
||||
|
||||
/** 去掉 Word 在 if/otherwise 两侧插入的 \\quad,便于分段正则匹配 */
|
||||
function normalizePiecewiseSpacing(latex) {
|
||||
return String(latex || '')
|
||||
.replace(/,\s*\\quad\s*\\text\{if\s*\}\s*\\quad\s*/g, ', \\text{if } ')
|
||||
.replace(/,\s*\\quad\s*\\text\{if\s*\}\s*/g, ', \\text{if } ')
|
||||
.replace(/,\s*\\text\{if\s*\}\s*\\quad\s*/g, ', \\text{if } ')
|
||||
.replace(/,\s*\\quad\s*\\text\{otherwise\s*\}\s*/g, ', \\text{otherwise} ')
|
||||
.replace(/,\s*\\text\{otherwise\s*\}\s*\\quad\s*/g, ', \\text{otherwise} ');
|
||||
}
|
||||
|
||||
function normalizeProbabilityText(latex) {
|
||||
return String(latex || '')
|
||||
.replace(/withprobability/gi, ' with probability ')
|
||||
.replace(/with\s+probability\s+/gi, ' with probability ');
|
||||
}
|
||||
|
||||
/** Word 有时输出 \\{^{上}_{下}} 叠字花括号分段 */
|
||||
function repairStackedBracePiecewise(latex) {
|
||||
const s = String(latex || '').trim();
|
||||
const m = s.match(
|
||||
/^([\s\S]+?)=(?:\\{|{)\^\{([\s\S]+?),\s*\\text\{if\s*\}\s*([\s\S]+?)\}_\{([\s\S]+?)(?:,\s*\\text\{otherwise\s*\})?\}\s*\)?\s*$/
|
||||
);
|
||||
if (!m) return s;
|
||||
|
||||
const lhs = m[1].trim();
|
||||
const branch1 = m[2].trim();
|
||||
const condition = repairTildeDistribution(m[3].trim());
|
||||
let branch2 = m[4].trim().replace(/,\s*\\text\{otherwise\s*\}\s*$/i, '').trim();
|
||||
if (!branch1 || !branch2 || !condition) return s;
|
||||
|
||||
return `${lhs} = \\left\\{ \\begin{matrix} ${branch1}, & if ${condition} \\\\ ${branch2}, & otherwise \\end{matrix} \\right.`;
|
||||
}
|
||||
|
||||
/** with probability 分段:\\{ expr, with probability p1) expr2, with probability p2 */
|
||||
function repairProbabilityPiecewise(latex) {
|
||||
const s = normalizeProbabilityText(String(latex || '').trim());
|
||||
const m = s.match(
|
||||
/^([\s\S]+?)=(?:\\{|{)([\s\S]+?),\s*with probability\s+([^,]+?)\)\s*([^,]+?),\s*with probability\s+(\S+)\s*(?:\\}|})?\s*$/
|
||||
);
|
||||
if (!m) return String(latex || '').trim();
|
||||
|
||||
const lhs = m[1].trim();
|
||||
const branch1 = m[2].trim();
|
||||
const prob1 = m[3].trim();
|
||||
const branch2 = m[4].trim();
|
||||
const prob2 = m[5].trim();
|
||||
if (!branch1 || !branch2) return String(latex || '').trim();
|
||||
|
||||
return `${lhs} = \\left\\{ \\begin{matrix} ${branch1}, & with probability ${prob1} \\\\ ${branch2}, & with probability ${prob2} \\end{matrix} \\right.`;
|
||||
}
|
||||
|
||||
/** 修复范数:// → \\|,以及 \\| ... \\| _{2} 间距 */
|
||||
function repairNormDelimiters(latex) {
|
||||
let s = String(latex || '');
|
||||
s = s.replace(/\/\/\s*(?=\\?[a-zA-Z_{\\])/g, '\\| ');
|
||||
s = s.replace(/\/\/\s*_\{/g, '\\|_{');
|
||||
s = s.replace(/,\s*if\s+\/\/\s*/gi, ', & if \\| ');
|
||||
s = s.replace(/,\s*if\s+(?=\\?[a-zA-Z_\\|])/gi, ', & if ');
|
||||
s = s.replace(/\\\|\s+([\s\S]+?)\s+\\\|\s*_\{2\}/g, '\\left\\|$1\\right\\|_2');
|
||||
s = s.replace(/\\\|\s+([\s\S]+?)\s+\\\|_2/g, '\\left\\|$1\\right\\|_2');
|
||||
s = s.replace(/\\\|\s*_\{2\}/g, '\\|_2');
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 未闭合的 \\begin{matrix}:尝试收成单行分式 */
|
||||
function repairIncompleteMatrix(latex) {
|
||||
const s = String(latex || '').trim();
|
||||
if (!/\\begin\{matrix\}/.test(s) || /\\end\{matrix\}/.test(s)) return s;
|
||||
|
||||
const m = s.match(
|
||||
/^([\s\S]+?)=\{\\begin\{matrix\}\s*\\frac\{([\s\S]+?)\}\{([^}]+)\}(?:,\s*(?:&\s*)?if\s+([\s\S]+))?\s*$/
|
||||
);
|
||||
if (!m) return s;
|
||||
|
||||
const lhs = m[1].trim();
|
||||
const num = m[2].trim();
|
||||
const den = m[3].trim();
|
||||
const cond = m[4] ? m[4].trim() : '';
|
||||
if (cond) {
|
||||
return `${lhs} = \\frac{${num}}{${den}}, \\quad \\text{if } ${cond}`;
|
||||
}
|
||||
return `${lhs} = \\frac{${num}}{${den}}`;
|
||||
}
|
||||
|
||||
/** \\min_{i}L、^{\\left(t \\ 1\\right)} 等 */
|
||||
function repairMiscLatexGlitches(latex) {
|
||||
return String(latex || '')
|
||||
.replace(/\bmin_\{1\}L/g, '\\min L')
|
||||
.replace(/\\min_\{([^}]+)\}L/g, '\\min_$1 L')
|
||||
.replace(/\\left\(\s*([a-zA-Z])\s*\\\s*(\d+)\s*\\right\)/g, '($1+$2)')
|
||||
.replace(/\^\{\\left\(([^)]+)\s*\\\s*(\d+)\s*\\right\)\}/g, '^{($1+$2)}')
|
||||
.replace(/(^|[^\\])left\(/g, '$1\\left(')
|
||||
.replace(/(^|[^\\])right\)/g, '$1\\right)');
|
||||
}
|
||||
|
||||
/** Word 导出 \\atop 堆叠分段:{ expr1, if cond \\atop expr2, otherwise } */
|
||||
function repairAtopPiecewise(latex) {
|
||||
let s = stripTrailingPiecewiseGarbage(String(latex || '').trim());
|
||||
if (!/\\atop\b/i.test(s)) return String(latex || '').trim();
|
||||
|
||||
const m = s.match(
|
||||
/^([\s\S]+?)=(?:\\{|{)([\s\S]+?),\s*(?:\\text\{if\s*\}|if)\s+([\s\S]+?)\s+\\atop\s+([\s\S]+?)(?:,\s*(?:\\text\{)?otherwise)?\s*$/i
|
||||
);
|
||||
if (!m) return String(latex || '').trim();
|
||||
|
||||
const lhs = m[1].trim();
|
||||
const branch1 = m[2].trim();
|
||||
const condition = repairTildeDistribution(m[3].trim());
|
||||
const branch2 = m[4].trim().replace(/,\s*otherwise\s*$/i, '').trim();
|
||||
if (!branch1 || !branch2 || !condition) return String(latex || '').trim();
|
||||
|
||||
return `${lhs} = \\left\\{ \\begin{matrix} ${branch1}, & if ${condition} \\\\ ${branch2}, & otherwise \\end{matrix} \\right.`;
|
||||
}
|
||||
|
||||
function buildPiecewiseMatrix(lhs, branch1, condition, branch2) {
|
||||
return `${lhs} = \\left\\{ \\begin{matrix} ${branch1}, & if ${condition} \\\\ ${branch2}, & otherwise \\end{matrix} \\right.`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将压扁的两段式分段函数还原为 matrix + \left\{ \right.
|
||||
* 支持 \\text{if}、裸 if、otherwise
|
||||
*/
|
||||
function repairFlattenedPiecewise(latex) {
|
||||
const original = String(latex || '').trim();
|
||||
let s = stripTrailingPiecewiseGarbage(normalizePiecewiseSpacing(original));
|
||||
if (!isFlattenedPiecewiseBrace(s)) return original;
|
||||
|
||||
const atopFixed = repairAtopPiecewise(s);
|
||||
if (atopFixed !== s && !/\\atop\b/i.test(atopFixed)) return atopFixed;
|
||||
|
||||
const m = s.match(
|
||||
/^([\s\S]+?)=(?:\\{|{)([\s\S]+?),\s*(?:\\text\{if\s*\}|if)\s*([\s\S]+?),\s*(?:\\text\{otherwise\s*\}|otherwise)\s*$/i
|
||||
);
|
||||
if (!m) return original;
|
||||
|
||||
const lhs = m[1].trim();
|
||||
const branch1 = m[2].trim();
|
||||
let mid = repairTildeDistribution(m[3].trim());
|
||||
|
||||
const split = mid.match(/^([\s\S]+?)(<|>|<=|>=)(-?\d*\.?\d+)\s*([\s\S]+)$/);
|
||||
if (!split) return original;
|
||||
|
||||
const condition = `${split[1].trim()}${split[2]}${split[3]}`.trim();
|
||||
const branch2 = split[4].trim();
|
||||
if (!branch1 || !branch2 || !condition) return original;
|
||||
|
||||
return buildPiecewiseMatrix(lhs, branch1, condition, branch2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修复 Word 导入常见 LaTeX 瑕疵(范数变 //、裸 if、重复 $$ 等)
|
||||
*/
|
||||
export function repairImportedLatex(latex) {
|
||||
let s = String(latex || '').trim();
|
||||
if (!s) return '';
|
||||
|
||||
s = s.replace(/^\$+/, '').replace(/\$+$/, '').trim();
|
||||
|
||||
s = repairMatrixPiecewiseDelimiters(s);
|
||||
s = repairAtopPiecewise(s);
|
||||
s = repairStackedBracePiecewise(s);
|
||||
s = repairProbabilityPiecewise(s);
|
||||
s = repairFlattenedPiecewise(s);
|
||||
s = repairNormDelimiters(s);
|
||||
s = repairIncompleteMatrix(s);
|
||||
s = repairTildeDistribution(s);
|
||||
s = repairMiscLatexGlitches(s);
|
||||
|
||||
s = s.replace(/\s+/g, ' ').trim();
|
||||
return s;
|
||||
}
|
||||
|
||||
function normalizeLatexCompareKey(latex) {
|
||||
return String(latex || '')
|
||||
.trim()
|
||||
.replace(/^\$+/, '')
|
||||
.replace(/\$+$/, '')
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
/** Word 导入后仍残留的常见异常片段(如范数变成 //) */
|
||||
export function hasLatexImportArtifacts(latex) {
|
||||
const raw = String(latex || '');
|
||||
if (/\/\//.test(raw)) return true;
|
||||
const inner = normalizeLatexCompareKey(latex);
|
||||
if (!inner) return false;
|
||||
if (/\/\/\s*(?=\\?[a-zA-Z_])/.test(inner)) return true;
|
||||
if (/\/\/\s*_\{/.test(inner)) return true;
|
||||
if (/\bmin_\{1\}L/.test(inner)) return true;
|
||||
if (/(^|[^\\])left\(/.test(inner)) return true;
|
||||
if (/(^|[^\\])right\)/.test(inner)) return true;
|
||||
if (isFlattenedPiecewiseBrace(inner)) return true;
|
||||
if (/[a-zA-Z]\s*~\s*U\s*\(/.test(inner)) return true;
|
||||
if (/[a-zA-Z]\s*\\sim\s*U\s*\(/.test(inner)) return true;
|
||||
if (/^\$\$[\s\S]+\$\$$/.test(String(latex || '').trim())) return true;
|
||||
if (/(?:\\{|{)\^\{[\s\S]*\\text\{if/.test(inner)) return true;
|
||||
if (/withprobability/i.test(inner)) return true;
|
||||
if (/\\begin\{matrix\}/.test(inner) && !/\\end\{matrix\}/.test(inner)) return true;
|
||||
if (/\\left\(\s*[a-zA-Z]\s*\\\s*\d+\s*\\right\)/.test(inner)) return true;
|
||||
if (/\\atop\b/i.test(inner)) return true;
|
||||
if (isPiecewiseMatrixContent(inner) && !/\\left\s*\\?\{/.test(inner)) return true;
|
||||
if (/\\right\s*[\.\)]?/.test(inner) && !/\\left\s*\\?\{/.test(inner) && /\\begin\{matrix\}/.test(inner)) {
|
||||
return true;
|
||||
}
|
||||
if (/with\s*probability/i.test(inner) && !/,\s*&\s*with\s+probability/i.test(inner)) return true;
|
||||
if (/,\s*otherwise\b/i.test(inner) && !/,\s*&\s*otherwise\b/i.test(inner)) return true;
|
||||
if (/∼/.test(raw) || /\u223c/.test(raw)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** data-latex 是否明显损坏(不含合法的 \\left/\\frac 等) */
|
||||
export function isAttrLatexStructurallyBroken(latex) {
|
||||
const t = String(latex || '').trim();
|
||||
if (!t) return true;
|
||||
if (/^\$\$/.test(t) || /\$\$$/.test(t)) return true;
|
||||
if (/\\\\/.test(t) && t.length > 12) return true;
|
||||
if (/\/\/\s*(_|[\\a-zA-Z])/.test(t)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 文本是否像未渲染的 LaTeX 源码(DOM 内可见 $$、\\frac 等) */
|
||||
export function isRawLatexTextProblematic(text) {
|
||||
const t = String(text || '').trim();
|
||||
if (!t) return true;
|
||||
if (isAttrLatexStructurallyBroken(t)) return true;
|
||||
if (/\\(begin|frac|left|right|mathcal|atop|end|sim)\b/i.test(t)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表/预览 DOM:是否应标红(露出源码、MathJax 报错、或导入时已标记)
|
||||
*/
|
||||
export function isFormulaDisplayedAsRawSource(wmath) {
|
||||
if (!wmath) return true;
|
||||
if (wmath.getAttribute('data-import-error') === '1') return true;
|
||||
if (wmath.querySelector('mjx-merror')) return true;
|
||||
|
||||
const mjx = wmath.querySelector('mjx-container');
|
||||
if (mjx) {
|
||||
const text = (wmath.textContent || '').trim();
|
||||
if (text && text !== '1' && isRawLatexTextProblematic(text)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
const text = (wmath.textContent || '').trim();
|
||||
if (!text || text === '1') {
|
||||
const fromAttr = extractLatexFromWmathElement(wmath);
|
||||
return isAttrLatexStructurallyBroken(fromAttr);
|
||||
}
|
||||
|
||||
return isRawLatexTextProblematic(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated 标红请用 isFormulaDisplayedAsRawSource(wmath);此处仅判断缺少 wmath 标签
|
||||
*/
|
||||
export function isProblematicLatexFormula(rawCellHtml) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = String(rawCellHtml || '');
|
||||
return !div.querySelector('wmath');
|
||||
}
|
||||
|
||||
function walkOmml(node) {
|
||||
@@ -157,17 +534,48 @@ function walkOmml(node) {
|
||||
}
|
||||
|
||||
if (name === 'd') {
|
||||
const begChr = node.getAttribute('m:begChr') || node.getAttribute('begChr') || '(';
|
||||
const endChr = node.getAttribute('m:endChr') || node.getAttribute('endChr') || ')';
|
||||
const inner = allByLocal(node, 'e')
|
||||
.map((item) => walkOmmlChildren(item))
|
||||
const begChr = getDelimCharFromD(node, 'beg') || node.getAttribute('m:begChr') || node.getAttribute('begChr') || '(';
|
||||
const endChr = getDelimCharFromD(node, 'end') || node.getAttribute('m:endChr') || node.getAttribute('endChr') || ')';
|
||||
const elems = allByLocal(node, 'e');
|
||||
let inner;
|
||||
if (elems.length > 1) {
|
||||
inner = elems.map((item) => walkOmmlChildren(item)).join(' \\\\ ');
|
||||
} else if (elems.length === 1) {
|
||||
inner = walkOmmlChildren(elems[0]);
|
||||
} else {
|
||||
inner = Array.from(node.children || [])
|
||||
.filter((child) => localName(child) !== 'dpr')
|
||||
.map((child) => walkOmml(child))
|
||||
.join('');
|
||||
}
|
||||
if (isNormDelimiterChar(begChr) && isNormDelimiterChar(endChr)) {
|
||||
return `\\left\\|${inner}\\right\\|`;
|
||||
}
|
||||
if (begChr === '(' && endChr === ')') return `\\left(${inner}\\right)`;
|
||||
if (begChr === '[' && endChr === ']') return `\\left[${inner}\\right]`;
|
||||
if (begChr === '{' && endChr === '}') return `\\left\\{${inner}\\right\\}`;
|
||||
if (begChr === '{' && endChr === '}') {
|
||||
if (elems.length > 1 && !/\\begin\{(matrix|cases)\}/.test(inner)) {
|
||||
inner = `\\begin{matrix} ${inner} \\end{matrix}`;
|
||||
}
|
||||
if (inner.includes('\\\\') || inner.includes('\\begin{matrix}')) {
|
||||
return `\\left\\{ ${inner} \\right.`;
|
||||
}
|
||||
return `\\left\\{${inner}\\right\\}`;
|
||||
}
|
||||
if (begChr === '|' && endChr === '|') return `\\left|${inner}\\right|`;
|
||||
return `${begChr}${inner}${endChr}`;
|
||||
}
|
||||
|
||||
if (name === 'func') {
|
||||
const fNameEl = firstByLocal(node, 'fName');
|
||||
const fname = (fNameEl && (fNameEl.getAttribute('m:val') || fNameEl.getAttribute('val') || fNameEl.textContent)) || '';
|
||||
const fnameTrim = String(fname).trim();
|
||||
const body = walkOmmlChildren(firstByLocal(node, 'e'));
|
||||
if (!fnameTrim) return body;
|
||||
const cmd = fnameTrim.replace(/\s+/g, '');
|
||||
return `\\${cmd}${body ? ` ${body}` : ''}`;
|
||||
}
|
||||
|
||||
if (name === 'nary') {
|
||||
const chr = node.getAttribute('m:chr') || node.getAttribute('chr') || '\\int';
|
||||
const sub = walkOmmlChildren(firstByLocal(node, 'sub'));
|
||||
@@ -184,7 +592,34 @@ function walkOmml(node) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (name === 'eqarr' || name === 'm') {
|
||||
if (name === 'eqarr') {
|
||||
const rows = allByLocal(node, 'e');
|
||||
if (rows.length > 1) {
|
||||
const latexRows = rows.map((row) => walkOmmlChildren(row));
|
||||
return `\\begin{matrix} ${latexRows.join(' \\\\ ')} \\end{matrix}`;
|
||||
}
|
||||
return walkOmmlChildren(node);
|
||||
}
|
||||
|
||||
if (name === 'm') {
|
||||
const matrixRows = allByLocal(node, 'mr');
|
||||
if (matrixRows.length > 0) {
|
||||
const latexRows = matrixRows.map((mr) => {
|
||||
const cols = allByLocal(mr, 'mc');
|
||||
if (cols.length > 0) {
|
||||
return cols.map((mc) => walkOmmlChildren(mc)).join(' & ');
|
||||
}
|
||||
return walkOmmlChildren(mr);
|
||||
});
|
||||
if (latexRows.length > 1) {
|
||||
return `\\begin{matrix} ${latexRows.join(' \\\\ ')} \\end{matrix}`;
|
||||
}
|
||||
return latexRows[0] || walkOmmlChildren(node);
|
||||
}
|
||||
return walkOmmlChildren(node);
|
||||
}
|
||||
|
||||
if (name === 'mc') {
|
||||
return walkOmmlChildren(node);
|
||||
}
|
||||
|
||||
@@ -208,7 +643,7 @@ function walkOmmlChildren(node) {
|
||||
|
||||
function ommlNodeToLatex(node) {
|
||||
if (!node) return '';
|
||||
return walkOmml(node).replace(/\s+/g, ' ').trim();
|
||||
return repairImportedLatex(walkOmml(node).replace(/\s+/g, ' ').trim());
|
||||
}
|
||||
|
||||
function isRunBold(run) {
|
||||
@@ -345,16 +780,14 @@ 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;
|
||||
const t = String(title || '').trim();
|
||||
if (!t) return false;
|
||||
if (t === LEGACY_MATH_FORMULA_TABLE_TITLE) return true;
|
||||
return t.replace(/\s+/g, ' ').toLowerCase() === 'latex data';
|
||||
}
|
||||
|
||||
function normalizeWmathLatex(latex) {
|
||||
let value = String(latex || '').trim();
|
||||
if (!value) return '';
|
||||
if (/^\$\$[\s\S]+\$\$$/.test(value) || /^\$[\s\S]+\$$/.test(value)) {
|
||||
return value;
|
||||
}
|
||||
return `$$${value}$$`;
|
||||
return repairImportedLatex(latex);
|
||||
}
|
||||
|
||||
function buildWmathHtml(latex, wrap, id) {
|
||||
@@ -363,7 +796,7 @@ function buildWmathHtml(latex, wrap, id) {
|
||||
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>`;
|
||||
return `<wmath contenteditable="false" data-id="${uid}" data-latex="${safe}" data-wrap="${mode}">1</wmath>`;
|
||||
}
|
||||
|
||||
export function buildWmathCellHtml(latex, wrap, id) {
|
||||
@@ -377,10 +810,65 @@ export function getLatexFromCellHtml(html) {
|
||||
const wmath = div.querySelector('wmath');
|
||||
if (wmath) {
|
||||
const fromAttr = extractLatexFromWmathElement(wmath);
|
||||
if (fromAttr) return fromAttr;
|
||||
return (wmath.textContent || '').trim();
|
||||
if (fromAttr) return repairImportedLatex(fromAttr);
|
||||
return repairImportedLatex((wmath.textContent || '').trim());
|
||||
}
|
||||
return (div.textContent || '').trim();
|
||||
return repairImportedLatex((div.textContent || '').trim());
|
||||
}
|
||||
|
||||
/** 展示/编辑前:修复 LaTeX 并重建 wmath 单元格 HTML(兼容历史脏数据) */
|
||||
export function normalizeWmathCellHtml(html) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = String(html || '');
|
||||
const wmath = div.querySelector('wmath');
|
||||
if (!wmath) return html || '';
|
||||
const raw = extractLatexFromWmathElement(wmath) || (wmath.textContent || '').trim();
|
||||
const repaired = repairImportedLatex(raw);
|
||||
if (!repaired) return html || '';
|
||||
const wrap = wmath.getAttribute('data-wrap') || 'block';
|
||||
const id = wmath.getAttribute('data-id') || '';
|
||||
return buildWmathHtml(repaired, wrap, id) || html || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 Word 导入预览标注公式序号(与 Latex Data 列表行号一致,仅计有 LaTeX 的 wmath)
|
||||
*/
|
||||
function markOrphanLatexBlocksInPreview(root) {
|
||||
root.querySelectorAll('p, td, th, li').forEach((block) => {
|
||||
if (block.querySelector('wmath')) return;
|
||||
const text = (block.textContent || '').trim();
|
||||
if (!text) return;
|
||||
if (/\$\$[\s\S]+?\$\$/.test(text) || /\\begin\{/.test(text) || /\\frac\s*\{/.test(text)) {
|
||||
block.classList.add('word-import-orphan-latex');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function buildWordImportPreviewHtml(html) {
|
||||
if (!html) return '';
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = String(html);
|
||||
let index = 0;
|
||||
div.querySelectorAll('wmath').forEach((el) => {
|
||||
const latex = extractLatexFromWmathElement(el);
|
||||
if (!latex) return;
|
||||
index += 1;
|
||||
const idx = String(index);
|
||||
el.setAttribute('data-formula-index', idx);
|
||||
const anchor = document.createElement('span');
|
||||
anchor.className = 'word-import-formula-anchor';
|
||||
anchor.setAttribute('data-formula-index', idx);
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'word-import-formula-index-badge';
|
||||
badge.textContent = idx;
|
||||
const parent = el.parentNode;
|
||||
if (!parent) return;
|
||||
parent.insertBefore(anchor, el);
|
||||
anchor.appendChild(badge);
|
||||
anchor.appendChild(el);
|
||||
});
|
||||
markOrphanLatexBlocksInPreview(div);
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/** 将解析后的 HTML 中的 wmath 提取为单个表格 table_data(表头 + 公式行) */
|
||||
@@ -407,8 +895,8 @@ export function extractWmathTableDataFromHtml(html) {
|
||||
}
|
||||
|
||||
return {
|
||||
tableData: [
|
||||
[{ text: '<b>Latex Data</b>', colspan: 1, rowspan: 1 }],
|
||||
tableData: [
|
||||
[{ text: '<b>Latex Data</b>', colspan: 1, rowspan: 1 }],
|
||||
...formulaRows
|
||||
],
|
||||
html: htmlParts.join('')
|
||||
|
||||
Reference in New Issue
Block a user