相关性检测

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: 'http://www.tougao.com/', //测试本地 记得切换
// baseURL: 'http://192.168.110.110/tougao/public/index.php/',
baseURL: '/api', //本地
// baseURL: '/', //正式
// baseURL: '/api', //本地
baseURL: '/', //正式
});

View File

@@ -50,6 +50,8 @@ const en = {
plagiarismNotChecked: 'Not checked',
plagiarismChecking: 'Checking…',
plagiarismRecheck: 'Re-check',
plagiarismRecheckConfirm: 'Start a new plagiarism check for this manuscript?',
plagiarismRecheckCancel: 'Cancel',
plagiarismDuplicateCheck: 'Re-check',
plagiarismCheckFailed: 'Failed to start plagiarism check.',
plagiarismStatusFailed: 'Failed to load plagiarism status.',
@@ -62,6 +64,9 @@ const en = {
plagiarismSimilarity: 'Similarity',
plagiarismFile: 'File',
plagiarismPreviewPdf: 'Preview report',
plagiarismDownloadReport: 'Download report',
plagiarismOnlinePreview: 'Online preview',
plagiarismOnlinePreviewFailed: 'Failed to load online preview URL.',
plagiarismReportLink: 'Report',
plagiarismNoPdfLink: 'No link',
plagiarismPreviewClose: 'Close',
@@ -1569,8 +1574,48 @@ const en = {
cancel: 'Cancel'
},
refRelevance: {
startDetect: 'Start relevance check',
detecting: 'Detecting…',
startDetect: 'Batch Audit Reference Context',
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})',
filterModify: 'Needs revision ({count})',
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.',
uncitedTip: 'Remove it from the reference list or add an in-text citation in the manuscript.',
citationN: 'Cite {n}',
citeInParagraphN: 'Occurrence {n} in paragraph',
relevancePct: 'Relevance {score}%',
relevancePctShort: '{score}%',
viewAiAnalysis: 'View AI analysis →',
@@ -1591,7 +1637,7 @@ const en = {
locationMatcher: 'Location context matcher',
manuscriptSection: 'Manuscript section',
highlightedMatch: 'Highlighted match',
excerptEmpty: 'No excerpt available yet.',
excerptEmpty: 'Original cited content not found',
annotationComment: 'Comment',
annotationDelete: 'Suggest removal',
annotationRevision: 'Revision',

View File

@@ -48,6 +48,8 @@ const zh = {
plagiarismNotChecked: '未检测',
plagiarismChecking: '正在检测…',
plagiarismRecheck: '重新查重',
plagiarismRecheckConfirm: '确认要重新发起查重吗?',
plagiarismRecheckCancel: '取消',
plagiarismDuplicateCheck: '重复检查',
plagiarismCheckFailed: '查重任务启动失败。',
plagiarismStatusFailed: '获取查重状态失败。',
@@ -60,6 +62,9 @@ const zh = {
plagiarismSimilarity: '相似度',
plagiarismFile: '文件',
plagiarismPreviewPdf: '预览报告',
plagiarismDownloadReport: '下载报告',
plagiarismOnlinePreview: '在线预览',
plagiarismOnlinePreviewFailed: '获取在线预览地址失败。',
plagiarismReportLink: '报告',
plagiarismNoPdfLink: '无链接',
plagiarismPreviewClose: '关闭',
@@ -1550,8 +1555,47 @@ const zh = {
cancel: '取消'
},
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: '检测中…',
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}',
filterModify: '需修改({count}',
columnTitle: 'AI 智能评定相关性建议',
@@ -1559,6 +1603,7 @@ const zh = {
uncitedDesc: '该文献已列入参考文献列表,但正文中未找到对应的引用上标。',
uncitedTip: '建议:从参考文献列表中删除,或在正文中补充引用。',
citationN: '第{n}处',
citeInParagraphN: '本段第{n}处',
relevancePct: '相关性 {score}%',
relevancePctShort: '{score}%',
viewAiAnalysis: '查看 AI 分析 →',
@@ -1572,7 +1617,7 @@ const zh = {
locationMatcher: '引用位置匹配',
manuscriptSection: '稿件段落',
highlightedMatch: '高亮匹配',
excerptEmpty: '暂无段落原文,待接口返回',
excerptEmpty: '原引用内容未找到',
annotationComment: '批注',
annotationDelete: '建议删除',
annotationRevision: '修订',

View File

@@ -1343,7 +1343,7 @@ export default {
p_article_id: this.p_article_id
})
.then((res) => {
this.chanFerForm = res.data.refers;
this.chanFerForm = (res.data.refers || []).slice();
this.chanFerFormRepeatList = Object.values(res.data.repeat);
for (var i = 0; i < this.chanFerForm.length; i++) {
this.chanFerForm[i].edit_mark = 1;

View File

@@ -1439,7 +1439,7 @@ export default {
p_article_id: this.p_article_id
})
.then((res) => {
this.chanFerForm = res.data.refers;
this.chanFerForm = (res.data.refers || []).slice();
this.chanFerFormRepeatList = Object.values(res.data.repeat);
for (var i = 0; i < this.chanFerForm.length; i++) {
this.chanFerForm[i].edit_mark = 1;

View File

@@ -976,27 +976,29 @@
element-loading-background="transparent"
>
<div class="plagiarism-check-header">
<span class="plagiarism-check-title">{{ $t('articleListEditor.plagiarismListTitle') }}</span>
<div class="plagiarism-check-actions">
<!-- <el-button
type="text"
size="mini"
icon="el-icon-document-checked"
:loading="plagiarismSubmitLoading"
@click="submitPlagiarismCheck"
>
{{ $t('articleListEditor.plagiarismDuplicateCheck') }}
</el-button> -->
<div class="plagiarism-check-header-left">
<span class="plagiarism-check-title">{{ $t('articleListEditor.plagiarismListTitle') }}</span>
<el-button
type="text"
size="mini"
icon="el-icon-refresh"
class="plagiarism-refresh-btn"
:loading="plagiarismListLoading"
@click="fetchPlagiarismList(true)"
>
{{ $t('articleListEditor.plagiarismRefresh') }}
</el-button>
</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 class="plagiarism-check-list-wrap">
<div
@@ -1019,17 +1021,20 @@
</span>
<span class="plagiarism-sim-report">
<a
v-if="hasPlagiarismPdf(row)"
v-if="canPlagiarismOnlinePreview(row)"
href="javascript:;"
class="plagiarism-report-preview"
:title="$t('articleListEditor.plagiarismPreviewOpenTab')"
@click.prevent="openPlagiarismReportPage(row)"
class="plagiarism-report-online"
@click.prevent="openPlagiarismOnlinePreview(row)"
>
{{ $t('articleListEditor.plagiarismPreviewPdf') }}
<i class="el-icon-link"></i>
<i
v-if="isPlagiarismOnlinePreviewLoading(row)"
class="el-icon-loading"
></i>
<i v-else class="el-icon-view"></i>
{{ $t('articleListEditor.plagiarismOnlinePreview') }}
</a>
<span
v-else-if="!isPlagiarismStateLoading(row)"
v-if="!canPlagiarismOnlinePreview(row) && !isPlagiarismStateLoading(row)"
class="plagiarism-check-no-pdf"
>{{ $t('articleListEditor.plagiarismNoPdfLink') }}</span
>
@@ -1870,7 +1875,8 @@ export default {
plagiarismPdfPreviewVisible: false,
plagiarismPdfPreviewUrl: '',
plagiarismPdfPreviewTitle: '',
plagiarismPdfPreviewLoading: false
plagiarismPdfPreviewLoading: false,
plagiarismOnlinePreviewCheckId: null
};
},
async created() {
@@ -3051,6 +3057,22 @@ export default {
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() {
const articleId = String((this.editform && this.editform.articleId) || this.$route.query.id || '').trim();
if (!articleId) {
@@ -3149,12 +3171,31 @@ export default {
if (n < 30) return 'sim-low';
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) {
if (!row || typeof row !== 'object') return '';
const raw =
row.finish_time ||
row.finished_at ||
row.update_time ||
row.utime ||
row.ctime ||
row.create_time ||
row.created_at ||
@@ -3163,27 +3204,26 @@ export default {
const s = String(raw).trim();
if (/^\d+$/.test(s)) {
const num = Number(s);
const ts = num > 1e12 ? num : num * 1000;
try {
return this.formatDate(Math.floor(ts / 1000));
} catch (e) {
return s;
}
const tsSec = num > 1e12 ? Math.floor(num / 1000) : num;
return this.formatPlagiarismDateTime(tsSec);
}
const d = new Date(s.replace(/-/g, '/'));
if (!isNaN(d.getTime())) {
const pad = (v) => String(v).padStart(2, '0');
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
return this.formatPlagiarismDateTime(Math.floor(d.getTime() / 1000));
}
return s.length > 10 ? s.slice(0, 10) : s;
return s.length > 19 ? s.slice(0, 19) : s;
},
resolvePlagiarismPdfUrl(row) {
if (!row || typeof row !== 'object') return '';
const raw = String(row.viewer_url || row.local_pdf_url || row.localPdfUrl || '').trim();
if (!raw) return '';
if (/^https?:\/\//i.test(raw)) return raw;
return this.normalizePlagiarismViewUrl(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)) {
const media = String(this.mediaUrl || '/public/').replace(/\/+$/, '');
if (/^https?:\/\//i.test(media)) {
@@ -3200,14 +3240,66 @@ export default {
}
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) {
return !!this.resolvePlagiarismPdfUrl(row);
},
openPlagiarismReportPage(row) {
const url = this.resolvePlagiarismPdfUrl(row);
if (!url) return;
window.open(url, '_blank', 'noopener,noreferrer');
},
@@ -3371,13 +3463,42 @@ export default {
justify-content: space-between;
margin-bottom: 8px;
}
.plagiarism-check-actions {
display: flex;
.plagiarism-check-header-left {
display: inline-flex;
align-items: center;
gap: 4px;
gap: 12px;
min-width: 0;
flex: 1;
}
.plagiarism-check-actions .el-button {
padding: 0 6px;
.plagiarism-refresh-btn.el-button--text {
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 {
font-size: 13px;
@@ -3437,7 +3558,9 @@ export default {
}
.plagiarism-sim-date {
flex-shrink: 0;
min-width: 132px;
color: #909399;
font-variant-numeric: tabular-nums;
}
.plagiarism-sim-state {
flex: 1;
@@ -3473,20 +3596,22 @@ export default {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 6px;
gap: 16px;
margin-left: auto;
}
.plagiarism-report-preview {
color: #409eff;
.plagiarism-report-online {
color: #006699;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
}
.plagiarism-report-preview:hover {
.plagiarism-report-online:hover {
text-decoration: underline;
color: #66b1ff;
color: #0088bb;
}
.plagiarism-report-preview .el-icon-link {
.plagiarism-report-online .el-icon-view {
font-size: 14px;
}
.plagiarism-check-no-pdf {

View File

@@ -15,13 +15,6 @@
<el-input v-model="AuthorMes.email"></el-input>
</el-form-item>
<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.articleInfor"></p>
<el-input type="textarea" rows="9" v-model="EmailData.substance" @input="btn_ft=false">
@@ -69,13 +62,11 @@
<script>
export default {
data() {
const savedLang = localStorage.getItem('langs');
return {
loading: false,
baseUrl: this.Common.baseUrl,
articleId: this.$route.query.id,
AuthorMes: {},
emailLetterLang: savedLang === 'zh' ? 'zh' : 'en',
authorDisplayName: '',
journalMeta: {
title: '',
@@ -99,14 +90,6 @@
created: function() {
this.getData();
},
watch: {
'$i18n.locale'(val) {
if (val === 'zh' || val === 'en') {
this.emailLetterLang = val;
this.applyEmailLetterBlocks();
}
}
},
computed: {
upload_enclosure: function() {
return this.baseUrl + 'api/Email/up_enclosure_file';
@@ -114,8 +97,8 @@
},
methods: {
letterT(key, params) {
const loc = this.emailLetterLang === 'zh' ? 'zh' : 'en';
return params ? this.$t('articleDetailEmail.' + key, loc, params) : this.$t('articleDetailEmail.' + key, loc);
const path = 'articleDetailEmail.' + key;
return params ? this.$t(path, 'en', params) : this.$t(path, 'en');
},
applyEmailLetterBlocks() {
const name = this.authorDisplayName || '';
@@ -305,20 +288,6 @@ if(this.$route.query.user_id){
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 {
line-height: 26px;
margin: 10px 0 -20px 0;

View File

@@ -79,7 +79,7 @@
<!-- <font style="margin-left: 5px">(Reviewing:{{ scope.row.now }})</font> -->
</template>
</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">
{{ $t('reviewerAdd.total') }}: {{ scope.row.rs_num }}
<br />
@@ -93,6 +93,12 @@
<span style="margin-right: 4px; color: red">{{ scope.row.error_times }}</span>
<span>({{ scope.row.error_rate }}%)</span>
</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> -->
</template>
</el-table-column>
@@ -158,17 +164,7 @@ export default {
methods: {
formatInviteTime(row) {
const raw =
row.invited_time != null && row.invited_time !== ''
? 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;
row.last_invite_time? row.last_invite_time : '1778306077';
if (raw == null || raw === '') {
return '-';
}
@@ -191,7 +187,8 @@ export default {
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
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() {

View File

@@ -130,10 +130,18 @@
></commonMajorTableList>
</template>
</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">
{{ scope.row.rs_num }}
<div>
{{ scope.row.rs_num }}
<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>
</el-table-column>
@@ -270,17 +278,7 @@ export default {
},
formatInviteTime(row) {
const raw =
row.invited_time != null && row.invited_time !== ''
? 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;
row.last_invite_time? row.last_invite_time : '1778306077';
if (raw == null || raw === '') {
return '-';
}
@@ -303,7 +301,8 @@ export default {
const D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
const h = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
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() {
this.$router.push({

View File

@@ -418,6 +418,106 @@ export default {
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
dataURLtoBlob(dataurl) {
const arr = dataurl.split(','),
@@ -625,6 +725,12 @@ export default {
setup(ed) {
let currentPasteImages = [];
_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;
ed.on('click', function (e) {
const wmathElement = e.target.closest('wmath');
@@ -847,16 +953,19 @@ export default {
});
}
} else {
const mathRegex = /\$\$([\s\S]+?)\$\$|\$([\s\S]+?)\$/g;
content = content.replace(mathRegex, function (match, blockFormula, inlineFormula) {
const formula = blockFormula || inlineFormula;
const mode = blockFormula ? 'block' : 'inline';
console.log(`Matched ${mode} formula:`, formula);
return `<wmath data-wrap="${mode}" data-latex="${formula.trim()}">${formula.trim()}</wmath>`;
});
const plainText = (tempDiv.textContent || tempDiv.innerText || '').trim();
const builtPlain = _this.buildWmathHtmlFromLatexText(plainText);
if (builtPlain && _this.isPlainLatexText(plainText)) {
content = builtPlain;
} else {
const mathRegex = /\$\$([\s\S]+?)\$\$|\$([\s\S]+?)\$/g;
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) {

File diff suppressed because it is too large Load Diff

View File

@@ -62,7 +62,8 @@ module.exports = {
devServer: {
// public: 'http://192.168.110.159:8080/', // 你自己本地的ip地址:端口号
port: '8080',
port: 8080,
open: true,
overlay: {
warnings: false,