相关性检测

This commit is contained in:
2026-05-27 17:09:51 +08:00
parent d4e158105e
commit 7e8d367b82
12 changed files with 3262 additions and 392 deletions

View File

@@ -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: '/', //正式
}); });

View File

@@ -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',

View File

@@ -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: '修订',

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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({

View File

@@ -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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;');
},
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+VMacCmd+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

View File

@@ -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,