相关性检测
This commit is contained in:
@@ -19,8 +19,8 @@ const service = axios.create({
|
|||||||
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
// baseURL: 'https://submission.tmrjournals.com/', //正式 记得切换
|
||||||
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
// baseURL: 'http://www.tougao.com/', //测试本地 记得切换
|
||||||
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
|
||||||
baseURL: '/api', //本地
|
// baseURL: '/api', //本地
|
||||||
// baseURL: '/', //正式
|
baseURL: '/', //正式
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ const en = {
|
|||||||
plagiarismNotChecked: 'Not checked',
|
plagiarismNotChecked: 'Not checked',
|
||||||
plagiarismChecking: 'Checking…',
|
plagiarismChecking: 'Checking…',
|
||||||
plagiarismRecheck: 'Re-check',
|
plagiarismRecheck: 'Re-check',
|
||||||
|
plagiarismRecheckConfirm: 'Start a new plagiarism check for this manuscript?',
|
||||||
|
plagiarismRecheckCancel: 'Cancel',
|
||||||
plagiarismDuplicateCheck: 'Re-check',
|
plagiarismDuplicateCheck: 'Re-check',
|
||||||
plagiarismCheckFailed: 'Failed to start plagiarism check.',
|
plagiarismCheckFailed: 'Failed to start plagiarism check.',
|
||||||
plagiarismStatusFailed: 'Failed to load plagiarism status.',
|
plagiarismStatusFailed: 'Failed to load plagiarism status.',
|
||||||
@@ -62,6 +64,9 @@ const en = {
|
|||||||
plagiarismSimilarity: 'Similarity',
|
plagiarismSimilarity: 'Similarity',
|
||||||
plagiarismFile: 'File',
|
plagiarismFile: 'File',
|
||||||
plagiarismPreviewPdf: 'Preview report',
|
plagiarismPreviewPdf: 'Preview report',
|
||||||
|
plagiarismDownloadReport: 'Download report',
|
||||||
|
plagiarismOnlinePreview: 'Online preview',
|
||||||
|
plagiarismOnlinePreviewFailed: 'Failed to load online preview URL.',
|
||||||
plagiarismReportLink: 'Report',
|
plagiarismReportLink: 'Report',
|
||||||
plagiarismNoPdfLink: 'No link',
|
plagiarismNoPdfLink: 'No link',
|
||||||
plagiarismPreviewClose: 'Close',
|
plagiarismPreviewClose: 'Close',
|
||||||
@@ -1569,8 +1574,48 @@ const en = {
|
|||||||
cancel: 'Cancel'
|
cancel: 'Cancel'
|
||||||
},
|
},
|
||||||
refRelevance: {
|
refRelevance: {
|
||||||
startDetect: 'Start relevance check',
|
startDetect: 'Batch Audit Reference Context',
|
||||||
detecting: 'Detecting…',
|
startDetectTip:
|
||||||
|
'Launch AI semantic consistency verification for all references against in-text citations.',
|
||||||
|
checkComplete: 'Relevance check completed',
|
||||||
|
detectingAi: 'Proofreading',
|
||||||
|
auditProgressTitle: 'Batch reference context audit in progress',
|
||||||
|
queuePositionLabel: 'Queue position:',
|
||||||
|
queuePositionNum: 'No. {n}',
|
||||||
|
queueAheadPrefix: 'Ahead:',
|
||||||
|
queueAheadSuffix: ' article(s) waiting for reference relevance check',
|
||||||
|
progressCount: '{done}/{total}',
|
||||||
|
progressCountShort: '{done} done',
|
||||||
|
startFailed: 'Failed to start proofreading',
|
||||||
|
statusFailed: 'Failed to fetch proofreading status',
|
||||||
|
progressFailed: 'Failed to fetch check progress',
|
||||||
|
detailsFailed: 'Failed to load AI proofreading details',
|
||||||
|
detailsEmpty: 'No AI proofreading details available',
|
||||||
|
noReferId: 'Missing reference ID',
|
||||||
|
noArticleId: 'Missing article ID; cannot run check',
|
||||||
|
detecting: 'Checking',
|
||||||
|
pendingDetect: 'Pending',
|
||||||
|
detectFailed: 'Check failed',
|
||||||
|
notFound: 'Not found',
|
||||||
|
copyInsight: 'Copy analysis',
|
||||||
|
copySuccess: 'Copied',
|
||||||
|
copyFailed: 'Copy failed',
|
||||||
|
copyEmpty: 'Nothing to copy',
|
||||||
|
redetect: 'Re-check',
|
||||||
|
redetectFailed: 'Re-check failed',
|
||||||
|
noReason: '(No details)',
|
||||||
|
aiAnalysis: 'AI analysis',
|
||||||
|
expandReason: 'Expand',
|
||||||
|
collapseReason: 'Collapse',
|
||||||
|
expandChips: 'Show all AI Relevance Assessment Results',
|
||||||
|
collapseChips: 'Hide AI Relevance Assessment Results',
|
||||||
|
expandChipsAll: 'Expand all ({n})',
|
||||||
|
collapseChipsAll: 'Collapse all',
|
||||||
|
summaryPartial: 'Checking {pending}, {done} done',
|
||||||
|
summaryPartialDone: ', {done} done',
|
||||||
|
summaryPartialPending: 'Checking {pending}',
|
||||||
|
summaryChecking: 'Checking',
|
||||||
|
pendingCites: '{n} more checking…',
|
||||||
filterAll: 'All ({count})',
|
filterAll: 'All ({count})',
|
||||||
filterModify: 'Needs revision ({count})',
|
filterModify: 'Needs revision ({count})',
|
||||||
columnTitle: 'AI citation relevance review',
|
columnTitle: 'AI citation relevance review',
|
||||||
@@ -1578,6 +1623,7 @@ const en = {
|
|||||||
uncitedDesc: 'This reference appears in the list but no matching in-text citation was found.',
|
uncitedDesc: 'This reference appears in the list but no matching in-text citation was found.',
|
||||||
uncitedTip: 'Remove it from the reference list or add an in-text citation in the manuscript.',
|
uncitedTip: 'Remove it from the reference list or add an in-text citation in the manuscript.',
|
||||||
citationN: 'Cite {n}',
|
citationN: 'Cite {n}',
|
||||||
|
citeInParagraphN: 'Occurrence {n} in paragraph',
|
||||||
relevancePct: 'Relevance {score}%',
|
relevancePct: 'Relevance {score}%',
|
||||||
relevancePctShort: '{score}%',
|
relevancePctShort: '{score}%',
|
||||||
viewAiAnalysis: 'View AI analysis →',
|
viewAiAnalysis: 'View AI analysis →',
|
||||||
@@ -1591,7 +1637,7 @@ const en = {
|
|||||||
locationMatcher: 'Location context matcher',
|
locationMatcher: 'Location context matcher',
|
||||||
manuscriptSection: 'Manuscript section',
|
manuscriptSection: 'Manuscript section',
|
||||||
highlightedMatch: 'Highlighted match',
|
highlightedMatch: 'Highlighted match',
|
||||||
excerptEmpty: 'No excerpt available yet.',
|
excerptEmpty: 'Original cited content not found',
|
||||||
annotationComment: 'Comment',
|
annotationComment: 'Comment',
|
||||||
annotationDelete: 'Suggest removal',
|
annotationDelete: 'Suggest removal',
|
||||||
annotationRevision: 'Revision',
|
annotationRevision: 'Revision',
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ const zh = {
|
|||||||
plagiarismNotChecked: '未检测',
|
plagiarismNotChecked: '未检测',
|
||||||
plagiarismChecking: '正在检测…',
|
plagiarismChecking: '正在检测…',
|
||||||
plagiarismRecheck: '重新查重',
|
plagiarismRecheck: '重新查重',
|
||||||
|
plagiarismRecheckConfirm: '确认要重新发起查重吗?',
|
||||||
|
plagiarismRecheckCancel: '取消',
|
||||||
plagiarismDuplicateCheck: '重复检查',
|
plagiarismDuplicateCheck: '重复检查',
|
||||||
plagiarismCheckFailed: '查重任务启动失败。',
|
plagiarismCheckFailed: '查重任务启动失败。',
|
||||||
plagiarismStatusFailed: '获取查重状态失败。',
|
plagiarismStatusFailed: '获取查重状态失败。',
|
||||||
@@ -60,6 +62,9 @@ const zh = {
|
|||||||
plagiarismSimilarity: '相似度',
|
plagiarismSimilarity: '相似度',
|
||||||
plagiarismFile: '文件',
|
plagiarismFile: '文件',
|
||||||
plagiarismPreviewPdf: '预览报告',
|
plagiarismPreviewPdf: '预览报告',
|
||||||
|
plagiarismDownloadReport: '下载报告',
|
||||||
|
plagiarismOnlinePreview: '在线预览',
|
||||||
|
plagiarismOnlinePreviewFailed: '获取在线预览地址失败。',
|
||||||
plagiarismReportLink: '报告',
|
plagiarismReportLink: '报告',
|
||||||
plagiarismNoPdfLink: '无链接',
|
plagiarismNoPdfLink: '无链接',
|
||||||
plagiarismPreviewClose: '关闭',
|
plagiarismPreviewClose: '关闭',
|
||||||
@@ -1550,8 +1555,47 @@ const zh = {
|
|||||||
cancel: '取消'
|
cancel: '取消'
|
||||||
},
|
},
|
||||||
refRelevance: {
|
refRelevance: {
|
||||||
startDetect: '开始检测相关性',
|
startDetect: '全量引文相关性核查',
|
||||||
|
startDetectTip: '一键启动对本篇稿件全部参考文献与正文语境的 AI 一致性核查',
|
||||||
|
checkComplete: '相关性检测已完成',
|
||||||
|
detectingAi: '校对中',
|
||||||
|
auditProgressTitle: '全量引文相关性核查中',
|
||||||
|
queuePositionLabel: '当前排队位置:',
|
||||||
|
queuePositionNum: '第 {n} 篇',
|
||||||
|
queueAheadPrefix: '前方有',
|
||||||
|
queueAheadSuffix: ' 篇文章参考文献相关性待检测',
|
||||||
|
progressCount: '{done}/{total}',
|
||||||
|
progressCountShort: '已完成 {done}',
|
||||||
|
startFailed: '启动校对失败',
|
||||||
|
statusFailed: '获取校对状态失败',
|
||||||
|
progressFailed: '获取检测进度失败',
|
||||||
|
detailsFailed: '获取 AI 校对详情失败',
|
||||||
|
detailsEmpty: '暂无 AI 校对详情',
|
||||||
|
noReferId: '缺少参考文献 ID',
|
||||||
|
noArticleId: '缺少稿件 ID,无法检测',
|
||||||
detecting: '检测中…',
|
detecting: '检测中…',
|
||||||
|
pendingDetect: '待检测',
|
||||||
|
detectFailed: '检测失败',
|
||||||
|
notFound: '未找到',
|
||||||
|
copyInsight: '复制分析',
|
||||||
|
copySuccess: '已复制',
|
||||||
|
copyFailed: '复制失败',
|
||||||
|
copyEmpty: '暂无可复制内容',
|
||||||
|
redetect: '重新检测',
|
||||||
|
redetectFailed: '重新检测失败',
|
||||||
|
noReason: '(暂无说明)',
|
||||||
|
aiAnalysis: 'AI 分析',
|
||||||
|
expandReason: '展开',
|
||||||
|
collapseReason: '收起',
|
||||||
|
expandChips: '显示全部 AI 相关性评定结果',
|
||||||
|
collapseChips: '隐藏 AI 相关性评定结果',
|
||||||
|
expandChipsAll: '展开全部({n})',
|
||||||
|
collapseChipsAll: '收起全部',
|
||||||
|
summaryPartial: '正在检测 {pending} 处,已完成 {done} 处',
|
||||||
|
summaryPartialDone: ',已完成 {done} 处',
|
||||||
|
summaryPartialPending: '正在检测 {pending} 处',
|
||||||
|
summaryChecking: '正在检测',
|
||||||
|
pendingCites: '另有 {n} 处检测中…',
|
||||||
filterAll: '全部({count})',
|
filterAll: '全部({count})',
|
||||||
filterModify: '需修改({count})',
|
filterModify: '需修改({count})',
|
||||||
columnTitle: 'AI 智能评定相关性建议',
|
columnTitle: 'AI 智能评定相关性建议',
|
||||||
@@ -1559,6 +1603,7 @@ const zh = {
|
|||||||
uncitedDesc: '该文献已列入参考文献列表,但正文中未找到对应的引用上标。',
|
uncitedDesc: '该文献已列入参考文献列表,但正文中未找到对应的引用上标。',
|
||||||
uncitedTip: '建议:从参考文献列表中删除,或在正文中补充引用。',
|
uncitedTip: '建议:从参考文献列表中删除,或在正文中补充引用。',
|
||||||
citationN: '第{n}处',
|
citationN: '第{n}处',
|
||||||
|
citeInParagraphN: '本段第{n}处',
|
||||||
relevancePct: '相关性 {score}%',
|
relevancePct: '相关性 {score}%',
|
||||||
relevancePctShort: '{score}%',
|
relevancePctShort: '{score}%',
|
||||||
viewAiAnalysis: '查看 AI 分析 →',
|
viewAiAnalysis: '查看 AI 分析 →',
|
||||||
@@ -1572,7 +1617,7 @@ const zh = {
|
|||||||
locationMatcher: '引用位置匹配',
|
locationMatcher: '引用位置匹配',
|
||||||
manuscriptSection: '稿件段落',
|
manuscriptSection: '稿件段落',
|
||||||
highlightedMatch: '高亮匹配',
|
highlightedMatch: '高亮匹配',
|
||||||
excerptEmpty: '暂无段落原文,待接口返回',
|
excerptEmpty: '原引用内容未找到',
|
||||||
annotationComment: '批注',
|
annotationComment: '批注',
|
||||||
annotationDelete: '建议删除',
|
annotationDelete: '建议删除',
|
||||||
annotationRevision: '修订',
|
annotationRevision: '修订',
|
||||||
|
|||||||
@@ -1343,7 +1343,7 @@ export default {
|
|||||||
p_article_id: this.p_article_id
|
p_article_id: this.p_article_id
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.chanFerForm = res.data.refers;
|
this.chanFerForm = (res.data.refers || []).slice();
|
||||||
this.chanFerFormRepeatList = Object.values(res.data.repeat);
|
this.chanFerFormRepeatList = Object.values(res.data.repeat);
|
||||||
for (var i = 0; i < this.chanFerForm.length; i++) {
|
for (var i = 0; i < this.chanFerForm.length; i++) {
|
||||||
this.chanFerForm[i].edit_mark = 1;
|
this.chanFerForm[i].edit_mark = 1;
|
||||||
|
|||||||
@@ -1439,7 +1439,7 @@ export default {
|
|||||||
p_article_id: this.p_article_id
|
p_article_id: this.p_article_id
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.chanFerForm = res.data.refers;
|
this.chanFerForm = (res.data.refers || []).slice();
|
||||||
this.chanFerFormRepeatList = Object.values(res.data.repeat);
|
this.chanFerFormRepeatList = Object.values(res.data.repeat);
|
||||||
for (var i = 0; i < this.chanFerForm.length; i++) {
|
for (var i = 0; i < this.chanFerForm.length; i++) {
|
||||||
this.chanFerForm[i].edit_mark = 1;
|
this.chanFerForm[i].edit_mark = 1;
|
||||||
|
|||||||
@@ -976,27 +976,29 @@
|
|||||||
element-loading-background="transparent"
|
element-loading-background="transparent"
|
||||||
>
|
>
|
||||||
<div class="plagiarism-check-header">
|
<div class="plagiarism-check-header">
|
||||||
<span class="plagiarism-check-title">{{ $t('articleListEditor.plagiarismListTitle') }}</span>
|
<div class="plagiarism-check-header-left">
|
||||||
<div class="plagiarism-check-actions">
|
<span class="plagiarism-check-title">{{ $t('articleListEditor.plagiarismListTitle') }}</span>
|
||||||
<!-- <el-button
|
|
||||||
type="text"
|
|
||||||
size="mini"
|
|
||||||
icon="el-icon-document-checked"
|
|
||||||
:loading="plagiarismSubmitLoading"
|
|
||||||
@click="submitPlagiarismCheck"
|
|
||||||
>
|
|
||||||
{{ $t('articleListEditor.plagiarismDuplicateCheck') }}
|
|
||||||
</el-button> -->
|
|
||||||
<el-button
|
<el-button
|
||||||
type="text"
|
type="text"
|
||||||
size="mini"
|
size="mini"
|
||||||
icon="el-icon-refresh"
|
icon="el-icon-refresh"
|
||||||
|
class="plagiarism-refresh-btn"
|
||||||
:loading="plagiarismListLoading"
|
:loading="plagiarismListLoading"
|
||||||
@click="fetchPlagiarismList(true)"
|
@click="fetchPlagiarismList(true)"
|
||||||
>
|
>
|
||||||
{{ $t('articleListEditor.plagiarismRefresh') }}
|
{{ $t('articleListEditor.plagiarismRefresh') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="mini"
|
||||||
|
icon="el-icon-document-checked"
|
||||||
|
class="plagiarism-recheck-btn"
|
||||||
|
:loading="plagiarismSubmitLoading"
|
||||||
|
@click="confirmPlagiarismRecheck"
|
||||||
|
>
|
||||||
|
{{ $t('articleListEditor.plagiarismRecheck') }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="plagiarism-check-list-wrap">
|
<div class="plagiarism-check-list-wrap">
|
||||||
<div
|
<div
|
||||||
@@ -1019,17 +1021,20 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="plagiarism-sim-report">
|
<span class="plagiarism-sim-report">
|
||||||
<a
|
<a
|
||||||
v-if="hasPlagiarismPdf(row)"
|
v-if="canPlagiarismOnlinePreview(row)"
|
||||||
href="javascript:;"
|
href="javascript:;"
|
||||||
class="plagiarism-report-preview"
|
class="plagiarism-report-online"
|
||||||
:title="$t('articleListEditor.plagiarismPreviewOpenTab')"
|
@click.prevent="openPlagiarismOnlinePreview(row)"
|
||||||
@click.prevent="openPlagiarismReportPage(row)"
|
|
||||||
>
|
>
|
||||||
{{ $t('articleListEditor.plagiarismPreviewPdf') }}
|
<i
|
||||||
<i class="el-icon-link"></i>
|
v-if="isPlagiarismOnlinePreviewLoading(row)"
|
||||||
|
class="el-icon-loading"
|
||||||
|
></i>
|
||||||
|
<i v-else class="el-icon-view"></i>
|
||||||
|
{{ $t('articleListEditor.plagiarismOnlinePreview') }}
|
||||||
</a>
|
</a>
|
||||||
<span
|
<span
|
||||||
v-else-if="!isPlagiarismStateLoading(row)"
|
v-if="!canPlagiarismOnlinePreview(row) && !isPlagiarismStateLoading(row)"
|
||||||
class="plagiarism-check-no-pdf"
|
class="plagiarism-check-no-pdf"
|
||||||
>{{ $t('articleListEditor.plagiarismNoPdfLink') }}</span
|
>{{ $t('articleListEditor.plagiarismNoPdfLink') }}</span
|
||||||
>
|
>
|
||||||
@@ -1870,7 +1875,8 @@ export default {
|
|||||||
plagiarismPdfPreviewVisible: false,
|
plagiarismPdfPreviewVisible: false,
|
||||||
plagiarismPdfPreviewUrl: '',
|
plagiarismPdfPreviewUrl: '',
|
||||||
plagiarismPdfPreviewTitle: '',
|
plagiarismPdfPreviewTitle: '',
|
||||||
plagiarismPdfPreviewLoading: false
|
plagiarismPdfPreviewLoading: false,
|
||||||
|
plagiarismOnlinePreviewCheckId: null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
@@ -3051,6 +3057,22 @@ export default {
|
|||||||
this.plagiarismPollTimer = null;
|
this.plagiarismPollTimer = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async confirmPlagiarismRecheck() {
|
||||||
|
try {
|
||||||
|
await this.$confirm(
|
||||||
|
this.$t('articleListEditor.plagiarismRecheckConfirm'),
|
||||||
|
this.$t('articleListEditor.plagiarismRecheck'),
|
||||||
|
{
|
||||||
|
confirmButtonText: this.$t('articleListEditor.plagiarismRecheck'),
|
||||||
|
cancelButtonText: this.$t('articleListEditor.plagiarismRecheckCancel'),
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
);
|
||||||
|
await this.submitPlagiarismCheck();
|
||||||
|
} catch (e) {
|
||||||
|
/* 用户取消 */
|
||||||
|
}
|
||||||
|
},
|
||||||
async submitPlagiarismCheck() {
|
async submitPlagiarismCheck() {
|
||||||
const articleId = String((this.editform && this.editform.articleId) || this.$route.query.id || '').trim();
|
const articleId = String((this.editform && this.editform.articleId) || this.$route.query.id || '').trim();
|
||||||
if (!articleId) {
|
if (!articleId) {
|
||||||
@@ -3149,12 +3171,31 @@ export default {
|
|||||||
if (n < 30) return 'sim-low';
|
if (n < 30) return 'sim-low';
|
||||||
return 'sim-high';
|
return 'sim-high';
|
||||||
},
|
},
|
||||||
|
formatPlagiarismDateTime(tsSeconds) {
|
||||||
|
const date = new Date(tsSeconds * 1000);
|
||||||
|
if (isNaN(date.getTime())) return '—';
|
||||||
|
const pad = (v) => String(v).padStart(2, '0');
|
||||||
|
return (
|
||||||
|
date.getFullYear() +
|
||||||
|
'-' +
|
||||||
|
pad(date.getMonth() + 1) +
|
||||||
|
'-' +
|
||||||
|
pad(date.getDate()) +
|
||||||
|
' ' +
|
||||||
|
pad(date.getHours()) +
|
||||||
|
':' +
|
||||||
|
pad(date.getMinutes()) +
|
||||||
|
':' +
|
||||||
|
pad(date.getSeconds())
|
||||||
|
);
|
||||||
|
},
|
||||||
formatPlagiarismDate(row) {
|
formatPlagiarismDate(row) {
|
||||||
if (!row || typeof row !== 'object') return '—';
|
if (!row || typeof row !== 'object') return '—';
|
||||||
const raw =
|
const raw =
|
||||||
row.finish_time ||
|
row.finish_time ||
|
||||||
row.finished_at ||
|
row.finished_at ||
|
||||||
row.update_time ||
|
row.update_time ||
|
||||||
|
row.utime ||
|
||||||
row.ctime ||
|
row.ctime ||
|
||||||
row.create_time ||
|
row.create_time ||
|
||||||
row.created_at ||
|
row.created_at ||
|
||||||
@@ -3163,27 +3204,26 @@ export default {
|
|||||||
const s = String(raw).trim();
|
const s = String(raw).trim();
|
||||||
if (/^\d+$/.test(s)) {
|
if (/^\d+$/.test(s)) {
|
||||||
const num = Number(s);
|
const num = Number(s);
|
||||||
const ts = num > 1e12 ? num : num * 1000;
|
const tsSec = num > 1e12 ? Math.floor(num / 1000) : num;
|
||||||
try {
|
return this.formatPlagiarismDateTime(tsSec);
|
||||||
return this.formatDate(Math.floor(ts / 1000));
|
|
||||||
} catch (e) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const d = new Date(s.replace(/-/g, '/'));
|
const d = new Date(s.replace(/-/g, '/'));
|
||||||
if (!isNaN(d.getTime())) {
|
if (!isNaN(d.getTime())) {
|
||||||
const pad = (v) => String(v).padStart(2, '0');
|
return this.formatPlagiarismDateTime(Math.floor(d.getTime() / 1000));
|
||||||
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
|
|
||||||
}
|
}
|
||||||
return s.length > 10 ? s.slice(0, 10) : s;
|
return s.length > 19 ? s.slice(0, 19) : s;
|
||||||
},
|
},
|
||||||
resolvePlagiarismPdfUrl(row) {
|
resolvePlagiarismPdfUrl(row) {
|
||||||
if (!row || typeof row !== 'object') return '';
|
if (!row || typeof row !== 'object') return '';
|
||||||
const raw = String(row.viewer_url || row.local_pdf_url || row.localPdfUrl || '').trim();
|
const raw = String(row.viewer_url || row.local_pdf_url || row.localPdfUrl || '').trim();
|
||||||
if (!raw) return '';
|
return this.normalizePlagiarismViewUrl(raw);
|
||||||
if (/^https?:\/\//i.test(raw)) return raw;
|
},
|
||||||
|
normalizePlagiarismViewUrl(raw) {
|
||||||
|
const url = String(raw || '').trim();
|
||||||
|
if (!url) return '';
|
||||||
|
if (/^https?:\/\//i.test(url)) return url;
|
||||||
|
|
||||||
let path = raw.replace(/^\/+/, '');
|
let path = url.replace(/^\/+/, '');
|
||||||
if (!/^public\//i.test(path)) {
|
if (!/^public\//i.test(path)) {
|
||||||
const media = String(this.mediaUrl || '/public/').replace(/\/+$/, '');
|
const media = String(this.mediaUrl || '/public/').replace(/\/+$/, '');
|
||||||
if (/^https?:\/\//i.test(media)) {
|
if (/^https?:\/\//i.test(media)) {
|
||||||
@@ -3200,14 +3240,66 @@ export default {
|
|||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
},
|
},
|
||||||
|
getPlagiarismCheckId(row) {
|
||||||
|
if (!row || typeof row !== 'object') return null;
|
||||||
|
const id = row.check_id != null ? row.check_id : row.id;
|
||||||
|
return id != null && id !== '' ? id : null;
|
||||||
|
},
|
||||||
|
canPlagiarismOnlinePreview(row) {
|
||||||
|
if (!row || this.isPlagiarismStateLoading(row)) return false;
|
||||||
|
if (!this.getPlagiarismCheckId(row)) return false;
|
||||||
|
const s = Number(row.state);
|
||||||
|
if (s === 1 || s === 4 || s === 5) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
isPlagiarismOnlinePreviewLoading(row) {
|
||||||
|
const id = this.getPlagiarismCheckId(row);
|
||||||
|
return id != null && String(this.plagiarismOnlinePreviewCheckId) === String(id);
|
||||||
|
},
|
||||||
|
async fetchPlagiarismReportUrl(checkId) {
|
||||||
|
const res = await this.$api.post('api/Plagiarism/getReportUrl', { check_id: checkId });
|
||||||
|
if (!res || res.code !== 0) {
|
||||||
|
throw new Error((res && res.msg) || this.$t('articleListEditor.plagiarismOnlinePreviewFailed'));
|
||||||
|
}
|
||||||
|
const data = res.data;
|
||||||
|
let url = '';
|
||||||
|
if (typeof data === 'string') {
|
||||||
|
url = data.trim();
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
url = String(
|
||||||
|
data.view_only_url ||
|
||||||
|
data.url ||
|
||||||
|
data.report_url ||
|
||||||
|
data.viewer_url ||
|
||||||
|
data.online_url ||
|
||||||
|
''
|
||||||
|
).trim();
|
||||||
|
}
|
||||||
|
if (!url) {
|
||||||
|
throw new Error(this.$t('articleListEditor.plagiarismNoReportUrl'));
|
||||||
|
}
|
||||||
|
return this.normalizePlagiarismViewUrl(url);
|
||||||
|
},
|
||||||
|
async openPlagiarismOnlinePreview(row) {
|
||||||
|
const checkId = this.getPlagiarismCheckId(row);
|
||||||
|
if (!checkId) return;
|
||||||
|
this.plagiarismOnlinePreviewCheckId = checkId;
|
||||||
|
try {
|
||||||
|
const url = await this.fetchPlagiarismReportUrl(checkId);
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
} catch (err) {
|
||||||
|
this.$message.error(
|
||||||
|
(err && err.message) || this.$t('articleListEditor.plagiarismOnlinePreviewFailed')
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
this.plagiarismOnlinePreviewCheckId = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
hasPlagiarismPdf(row) {
|
hasPlagiarismPdf(row) {
|
||||||
return !!this.resolvePlagiarismPdfUrl(row);
|
return !!this.resolvePlagiarismPdfUrl(row);
|
||||||
},
|
},
|
||||||
openPlagiarismReportPage(row) {
|
openPlagiarismReportPage(row) {
|
||||||
|
|
||||||
|
|
||||||
const url = this.resolvePlagiarismPdfUrl(row);
|
const url = this.resolvePlagiarismPdfUrl(row);
|
||||||
|
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
window.open(url, '_blank', 'noopener,noreferrer');
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
},
|
},
|
||||||
@@ -3371,13 +3463,42 @@ export default {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.plagiarism-check-actions {
|
.plagiarism-check-header-left {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 12px;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
.plagiarism-check-actions .el-button {
|
.plagiarism-refresh-btn.el-button--text {
|
||||||
padding: 0 6px;
|
padding: 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.plagiarism-refresh-btn.el-button--text:hover,
|
||||||
|
.plagiarism-refresh-btn.el-button--text:focus {
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
.plagiarism-recheck-btn.el-button--mini {
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #2ec4b6 0%, #0d9b8f 45%, #0a7f76 100%);
|
||||||
|
box-shadow: 0 2px 6px rgba(13, 155, 143, 0.35);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.plagiarism-recheck-btn.el-button--primary:hover,
|
||||||
|
.plagiarism-recheck-btn.el-button--primary:focus {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #3dd4c6 0%, #14b0a3 45%, #0e948a 100%);
|
||||||
|
box-shadow: 0 3px 10px rgba(13, 155, 143, 0.45);
|
||||||
|
}
|
||||||
|
.plagiarism-recheck-btn.el-button--primary.is-loading {
|
||||||
|
background: linear-gradient(135deg, #2ec4b6 0%, #0d9b8f 45%, #0a7f76 100%);
|
||||||
}
|
}
|
||||||
.plagiarism-check-title {
|
.plagiarism-check-title {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
@@ -3437,7 +3558,9 @@ export default {
|
|||||||
}
|
}
|
||||||
.plagiarism-sim-date {
|
.plagiarism-sim-date {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
min-width: 132px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.plagiarism-sim-state {
|
.plagiarism-sim-state {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@@ -3473,20 +3596,22 @@ export default {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 6px;
|
gap: 16px;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
.plagiarism-report-preview {
|
.plagiarism-report-online {
|
||||||
color: #409eff;
|
color: #006699;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.plagiarism-report-preview:hover {
|
.plagiarism-report-online:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
color: #66b1ff;
|
color: #0088bb;
|
||||||
}
|
}
|
||||||
.plagiarism-report-preview .el-icon-link {
|
.plagiarism-report-online .el-icon-view {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
.plagiarism-check-no-pdf {
|
.plagiarism-check-no-pdf {
|
||||||
|
|||||||
@@ -15,13 +15,6 @@
|
|||||||
<el-input v-model="AuthorMes.email"></el-input>
|
<el-input v-model="AuthorMes.email"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('articleDetailEmail.content')">
|
<el-form-item :label="$t('articleDetailEmail.content')">
|
||||||
<div class="email-letter-lang">
|
|
||||||
<span class="email-letter-lang-label">{{ $t('articleDetailEmail.letterLang') }}</span>
|
|
||||||
<el-radio-group v-model="emailLetterLang" size="small" @change="applyEmailLetterBlocks">
|
|
||||||
<el-radio-button label="zh">{{ $t('articleDetailEmail.langZh') }}</el-radio-button>
|
|
||||||
<el-radio-button label="en">{{ $t('articleDetailEmail.langEn') }}</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
<p v-html="EmailData.topmail"></p>
|
<p v-html="EmailData.topmail"></p>
|
||||||
<p v-html="EmailData.articleInfor"></p>
|
<p v-html="EmailData.articleInfor"></p>
|
||||||
<el-input type="textarea" rows="9" v-model="EmailData.substance" @input="btn_ft=false">
|
<el-input type="textarea" rows="9" v-model="EmailData.substance" @input="btn_ft=false">
|
||||||
@@ -69,13 +62,11 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
const savedLang = localStorage.getItem('langs');
|
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
baseUrl: this.Common.baseUrl,
|
baseUrl: this.Common.baseUrl,
|
||||||
articleId: this.$route.query.id,
|
articleId: this.$route.query.id,
|
||||||
AuthorMes: {},
|
AuthorMes: {},
|
||||||
emailLetterLang: savedLang === 'zh' ? 'zh' : 'en',
|
|
||||||
authorDisplayName: '',
|
authorDisplayName: '',
|
||||||
journalMeta: {
|
journalMeta: {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -99,14 +90,6 @@
|
|||||||
created: function() {
|
created: function() {
|
||||||
this.getData();
|
this.getData();
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
'$i18n.locale'(val) {
|
|
||||||
if (val === 'zh' || val === 'en') {
|
|
||||||
this.emailLetterLang = val;
|
|
||||||
this.applyEmailLetterBlocks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
upload_enclosure: function() {
|
upload_enclosure: function() {
|
||||||
return this.baseUrl + 'api/Email/up_enclosure_file';
|
return this.baseUrl + 'api/Email/up_enclosure_file';
|
||||||
@@ -114,8 +97,8 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
letterT(key, params) {
|
letterT(key, params) {
|
||||||
const loc = this.emailLetterLang === 'zh' ? 'zh' : 'en';
|
const path = 'articleDetailEmail.' + key;
|
||||||
return params ? this.$t('articleDetailEmail.' + key, loc, params) : this.$t('articleDetailEmail.' + key, loc);
|
return params ? this.$t(path, 'en', params) : this.$t(path, 'en');
|
||||||
},
|
},
|
||||||
applyEmailLetterBlocks() {
|
applyEmailLetterBlocks() {
|
||||||
const name = this.authorDisplayName || '';
|
const name = this.authorDisplayName || '';
|
||||||
@@ -305,20 +288,6 @@ if(this.$route.query.user_id){
|
|||||||
margin: 30px 0 0 0;
|
margin: 30px 0 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-letter-lang {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-letter-lang-label {
|
|
||||||
font-size: 13px;
|
|
||||||
color: #606266;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Email_Data .el-textarea__inner {
|
.Email_Data .el-textarea__inner {
|
||||||
line-height: 26px;
|
line-height: 26px;
|
||||||
margin: 10px 0 -20px 0;
|
margin: 10px 0 -20px 0;
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
<!-- <font style="margin-left: 5px">(Reviewing:{{ scope.row.now }})</font> -->
|
<!-- <font style="margin-left: 5px">(Reviewing:{{ scope.row.now }})</font> -->
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="rs_num" :label="$t('reviewerAdd.colReviewedTimes')" width="140">
|
<el-table-column prop="rs_num" :label="$t('reviewerAdd.colReviewedTimes')" width="240">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ $t('reviewerAdd.total') }}: {{ scope.row.rs_num }}
|
{{ $t('reviewerAdd.total') }}: {{ scope.row.rs_num }}
|
||||||
<br />
|
<br />
|
||||||
@@ -93,6 +93,12 @@
|
|||||||
<span style="margin-right: 4px; color: red">{{ scope.row.error_times }}</span>
|
<span style="margin-right: 4px; color: red">{{ scope.row.error_times }}</span>
|
||||||
<span>({{ scope.row.error_rate }}%)</span>
|
<span>({{ scope.row.error_rate }}%)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span style="font-size: 12px;color: #aaa;"><span style="color: #888;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
">
|
||||||
|
{{ $t('reviewerAdd.latestInvitationLabel') }}
|
||||||
|
</span>{{formatInviteTime(scope.row) }}</span>
|
||||||
<!-- <font style="margin-left: 5px">(Reviewing:{{ scope.row.now }})</font> -->
|
<!-- <font style="margin-left: 5px">(Reviewing:{{ scope.row.now }})</font> -->
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
@@ -158,17 +164,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
formatInviteTime(row) {
|
formatInviteTime(row) {
|
||||||
const raw =
|
const raw =
|
||||||
row.invited_time != null && row.invited_time !== ''
|
row.last_invite_time? row.last_invite_time : '1778306077';
|
||||||
? row.invited_time
|
|
||||||
: row.last_invite_time != null && row.last_invite_time !== ''
|
|
||||||
? row.last_invite_time
|
|
||||||
: row.invite_time != null && row.invite_time !== ''
|
|
||||||
? row.invite_time
|
|
||||||
: row.invitation_time != null && row.invitation_time !== ''
|
|
||||||
? row.invitation_time
|
|
||||||
: row.invit_time != null && row.invit_time !== ''
|
|
||||||
? row.invit_time
|
|
||||||
: row.inviteTime;
|
|
||||||
if (raw == null || raw === '') {
|
if (raw == null || raw === '') {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
@@ -191,7 +187,8 @@ export default {
|
|||||||
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
||||||
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
|
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
|
||||||
const m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
|
const m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
|
||||||
return Y + M + D + ' ' + h + ':' + m;
|
const s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
|
||||||
|
return Y + M + D + ' ' + h + ':' + m + ':' + s;
|
||||||
},
|
},
|
||||||
// 获取编辑列表数据
|
// 获取编辑列表数据
|
||||||
getDate() {
|
getDate() {
|
||||||
|
|||||||
@@ -130,10 +130,18 @@
|
|||||||
></commonMajorTableList>
|
></commonMajorTableList>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="rs_num" :label="$t('reviewerAdd.colReviewedTimes')" width="160">
|
<el-table-column prop="rs_num" :label="$t('reviewerAdd.colReviewedTimes')" width="240">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
{{ scope.row.rs_num }}
|
|
||||||
|
<div>
|
||||||
|
{{ scope.row.rs_num }}
|
||||||
<font style="margin-left: 5px">({{ $t('reviewerAdd.reviewing') }}:{{ scope.row.now }})</font>
|
<font style="margin-left: 5px">({{ $t('reviewerAdd.reviewing') }}:{{ scope.row.now }})</font>
|
||||||
|
</div>
|
||||||
|
<span style="font-size: 12px;color: #aaa;"><span style="color: #888;
|
||||||
|
margin: 0 5px 0 0;
|
||||||
|
">
|
||||||
|
{{ $t('reviewerAdd.latestInvitationLabel') }}
|
||||||
|
</span>{{formatInviteTime(scope.row) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
|
||||||
@@ -270,17 +278,7 @@ export default {
|
|||||||
},
|
},
|
||||||
formatInviteTime(row) {
|
formatInviteTime(row) {
|
||||||
const raw =
|
const raw =
|
||||||
row.invited_time != null && row.invited_time !== ''
|
row.last_invite_time? row.last_invite_time : '1778306077';
|
||||||
? row.invited_time
|
|
||||||
: row.last_invite_time != null && row.last_invite_time !== ''
|
|
||||||
? row.last_invite_time
|
|
||||||
: row.invite_time != null && row.invite_time !== ''
|
|
||||||
? row.invite_time
|
|
||||||
: row.invitation_time != null && row.invitation_time !== ''
|
|
||||||
? row.invitation_time
|
|
||||||
: row.invit_time != null && row.invit_time !== ''
|
|
||||||
? row.invit_time
|
|
||||||
: row.inviteTime;
|
|
||||||
if (raw == null || raw === '') {
|
if (raw == null || raw === '') {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
@@ -303,7 +301,8 @@ export default {
|
|||||||
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
||||||
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
|
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
|
||||||
const m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
|
const m = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
|
||||||
return Y + M + D + ' ' + h + ':' + m;
|
const s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
|
||||||
|
return Y + M + D + ' ' + h + ':' + m + ':' + s;
|
||||||
},
|
},
|
||||||
goBack() {
|
goBack() {
|
||||||
this.$router.push({
|
this.$router.push({
|
||||||
|
|||||||
@@ -418,6 +418,106 @@ export default {
|
|||||||
this.$message.error('Upload failed. Removing temporary placeholder.');
|
this.$message.error('Upload failed. Removing temporary placeholder.');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
escapeLatexForWmathAttr(latex) {
|
||||||
|
return String(latex || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<');
|
||||||
|
},
|
||||||
|
createWmathHtml(latex, wrap) {
|
||||||
|
const uid = 'wmath-' + Math.random().toString(36).substr(2, 9);
|
||||||
|
const safe = this.escapeLatexForWmathAttr((latex || '').trim());
|
||||||
|
if (!safe) return '';
|
||||||
|
const mode = wrap === 'inline' ? 'inline' : 'block';
|
||||||
|
return (
|
||||||
|
'<wmath contenteditable="false" data-id="' +
|
||||||
|
uid +
|
||||||
|
'" data-latex="' +
|
||||||
|
safe +
|
||||||
|
'" data-wrap="' +
|
||||||
|
mode +
|
||||||
|
'">' +
|
||||||
|
safe +
|
||||||
|
'</wmath>'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
normalizeClipboardLatex(text) {
|
||||||
|
let t = String(text || '')
|
||||||
|
.replace(/\u00a0/g, ' ')
|
||||||
|
.trim();
|
||||||
|
if (!t) return '';
|
||||||
|
if (/^\$\$[\s\S]+\$\$$/.test(t)) {
|
||||||
|
t = t.replace(/^\$\$([\s\S]+)\$\$$/, '$1').trim();
|
||||||
|
} else if (/^\$[\s\S]+\$$/.test(t)) {
|
||||||
|
t = t.replace(/^\$([\s\S]+)\$$/, '$1').trim();
|
||||||
|
}
|
||||||
|
return t.replace(/\r\n/g, '\n');
|
||||||
|
},
|
||||||
|
/** 无 $ 包裹的纯 LaTeX,如 Precision_i=\frac{TP_i}{TP_i + FP_i} */
|
||||||
|
isPlainLatexText(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t || t.length > 8000) return false;
|
||||||
|
if (/<[a-z][\s\S]*>/i.test(t)) return false;
|
||||||
|
return /\\[a-zA-Z@]+|[_\^]\{|=\s*\\frac|\\frac\{|\\sqrt|\\sum|\\int|\\alpha|\\beta|\\cdot|\\times|\\leq|\\geq|\\neq|\\pm|\\infty|\\left|\\right/.test(
|
||||||
|
t
|
||||||
|
);
|
||||||
|
},
|
||||||
|
resolveWmathWrapMode(latex) {
|
||||||
|
const t = String(latex || '').trim();
|
||||||
|
if (!t) return 'inline';
|
||||||
|
if (/^\\begin\{/.test(t) || /\n\s*\n/.test(t)) return 'block';
|
||||||
|
if (/\\frac|\\dfrac|\\tfrac|\\sum|\\int|\\prod|\\lim|\\sqrt\{|\\displaystyle/.test(t)) {
|
||||||
|
return 'block';
|
||||||
|
}
|
||||||
|
return 'inline';
|
||||||
|
},
|
||||||
|
buildWmathHtmlFromLatexText(text) {
|
||||||
|
const normalized = this.normalizeClipboardLatex(text);
|
||||||
|
if (!normalized) return '';
|
||||||
|
|
||||||
|
if (/\$[\s\S]*\$/.test(normalized)) {
|
||||||
|
const mathRegex = /\$\$([\s\S]+?)\$\$|\$([\s\S]+?)\$/g;
|
||||||
|
return normalized.replace(mathRegex, (match, blockFormula, inlineFormula) => {
|
||||||
|
const formula = (blockFormula || inlineFormula || '').trim();
|
||||||
|
if (!formula) return '';
|
||||||
|
const mode = blockFormula ? 'block' : 'inline';
|
||||||
|
return this.createWmathHtml(formula, mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isPlainLatexText(normalized)) return '';
|
||||||
|
|
||||||
|
const lines = normalized.split(/\n/).map((l) => l.trim()).filter(Boolean);
|
||||||
|
if (lines.length > 1 && lines.every((l) => this.isPlainLatexText(l))) {
|
||||||
|
return lines.map((l) => this.createWmathHtml(l, this.resolveWmathWrapMode(l))).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.createWmathHtml(normalized, this.resolveWmathWrapMode(normalized));
|
||||||
|
},
|
||||||
|
async pasteClipboardAsMath(ed) {
|
||||||
|
const editor = ed || (window.tinymce && window.tinymce.get(this.tinymceId));
|
||||||
|
if (!editor) return;
|
||||||
|
try {
|
||||||
|
const text = await navigator.clipboard.readText();
|
||||||
|
const html = this.buildWmathHtmlFromLatexText(text);
|
||||||
|
if (!html) return;
|
||||||
|
editor.insertContent(html);
|
||||||
|
const editorId = editor.id || this.tinymceId;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof window.renderMathJax === 'function') {
|
||||||
|
window.renderMathJax(editorId);
|
||||||
|
}
|
||||||
|
}, 10);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('pasteClipboardAsMath failed', err);
|
||||||
|
const isZh = this.$i18n && this.$i18n.locale === 'zh';
|
||||||
|
this.$message.warning(
|
||||||
|
isZh
|
||||||
|
? '无法读取剪贴板。请允许浏览器权限,或复制 LaTeX 后使用 Ctrl+Shift+V(Mac:Cmd+Shift+V)粘贴为公式。'
|
||||||
|
: 'Cannot read clipboard. Allow browser permission, then use Ctrl+Shift+V (Mac: Cmd+Shift+V) to paste as formula.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
// 辅助工具:Base64 转 Blob
|
// 辅助工具:Base64 转 Blob
|
||||||
dataURLtoBlob(dataurl) {
|
dataURLtoBlob(dataurl) {
|
||||||
const arr = dataurl.split(','),
|
const arr = dataurl.split(','),
|
||||||
@@ -625,6 +725,12 @@ export default {
|
|||||||
setup(ed) {
|
setup(ed) {
|
||||||
let currentPasteImages = [];
|
let currentPasteImages = [];
|
||||||
_this.$commonJS.initEditorButton(_this, ed);
|
_this.$commonJS.initEditorButton(_this, ed);
|
||||||
|
ed.on('keydown', function (e) {
|
||||||
|
if (!(e.ctrlKey || e.metaKey) || !e.shiftKey) return;
|
||||||
|
if (e.key !== 'v' && e.key !== 'V') return;
|
||||||
|
e.preventDefault();
|
||||||
|
_this.pasteClipboardAsMath(ed);
|
||||||
|
});
|
||||||
var currentWmathElement = null;
|
var currentWmathElement = null;
|
||||||
ed.on('click', function (e) {
|
ed.on('click', function (e) {
|
||||||
const wmathElement = e.target.closest('wmath');
|
const wmathElement = e.target.closest('wmath');
|
||||||
@@ -847,16 +953,19 @@ export default {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const mathRegex = /\$\$([\s\S]+?)\$\$|\$([\s\S]+?)\$/g;
|
const plainText = (tempDiv.textContent || tempDiv.innerText || '').trim();
|
||||||
content = content.replace(mathRegex, function (match, blockFormula, inlineFormula) {
|
const builtPlain = _this.buildWmathHtmlFromLatexText(plainText);
|
||||||
const formula = blockFormula || inlineFormula;
|
if (builtPlain && _this.isPlainLatexText(plainText)) {
|
||||||
const mode = blockFormula ? 'block' : 'inline';
|
content = builtPlain;
|
||||||
console.log(`Matched ${mode} formula:`, formula);
|
} else {
|
||||||
|
const mathRegex = /\$\$([\s\S]+?)\$\$|\$([\s\S]+?)\$/g;
|
||||||
return `<wmath data-wrap="${mode}" data-latex="${formula.trim()}">${formula.trim()}</wmath>`;
|
content = content.replace(mathRegex, function (match, blockFormula, inlineFormula) {
|
||||||
});
|
const formula = (blockFormula || inlineFormula || '').trim();
|
||||||
|
if (!formula) return '';
|
||||||
|
const mode = blockFormula ? 'block' : 'inline';
|
||||||
|
return _this.createWmathHtml(formula, mode);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.event) {
|
if (args.event) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,8 @@ module.exports = {
|
|||||||
devServer: {
|
devServer: {
|
||||||
|
|
||||||
// public: 'http://192.168.110.159:8080/', // 你自己本地的ip地址:端口号
|
// public: 'http://192.168.110.159:8080/', // 你自己本地的ip地址:端口号
|
||||||
port: '8080',
|
port: 8080,
|
||||||
|
|
||||||
open: true,
|
open: true,
|
||||||
overlay: {
|
overlay: {
|
||||||
warnings: false,
|
warnings: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user