2 Commits

Author SHA1 Message Date
723ec0d190 提交 2026-05-13 13:25:05 +08:00
b10de50fdf 提交 2026-05-13 09:47:36 +08:00
7 changed files with 897 additions and 18 deletions

View File

@@ -2,14 +2,14 @@
//记得切换 //记得切换
//正式 //正式
const mediaUrl = '/public/'; // const mediaUrl = '/public/';
const baseUrl = '/'; // const baseUrl = '/';
//正式环境 //正式环境
// const mediaUrl = 'https://submission.tmrjournals.com/public/'; const mediaUrl = 'https://submission.tmrjournals.com/public/';
// // const mediaUrl = 'http://zmzm.tougao.dev.com/public/'; // const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
// const baseUrl = '/api' const baseUrl = '/api'
//测试环境 //测试环境

View File

@@ -47,6 +47,13 @@ const en = {
status: 'Status', status: 'Status',
delete: 'Delete', delete: 'Delete',
deleteInfo: 'Are you sure you want to delete this journal installment?', deleteInfo: 'Are you sure you want to delete this journal installment?',
plagiarismNotChecked: 'Not checked',
plagiarismChecking: 'Checking…',
plagiarismRecheck: 'Re-check',
plagiarismCheckFailed: 'Failed to start plagiarism check.',
plagiarismStatusFailed: 'Failed to load plagiarism status.',
plagiarismNoReportUrl: 'Report link is not available yet.',
plagiarismReportDetailFailed: 'Could not load manuscript details. Please try again.',
}, },
menu: { menu: {
main: 'Personal Center', main: 'Personal Center',
@@ -650,6 +657,15 @@ colTitle: 'Template title',
printBtn: 'Print', printBtn: 'Print',
previewNotSupported: 'This file format cannot be previewed online', previewNotSupported: 'This file format cannot be previewed online',
downloadToView: 'Download to view locally', downloadToView: 'Download to view locally',
registerAuthorBtn: 'Create author account',
registerAuthorConfirm:
'Create an account via the same admin API as User Management: login name "{account}", display name "{realname}", email "{email}", initial password 123456qwe (no captcha). Continue?',
registerAuthorSuccess: 'Author account created.',
registerAuthorFail: 'Creation failed. Try again later or add the user manually in User Management.',
registerAuthorExistsEmail: 'This email is already registered.',
registerAuthorExistsAccount: 'This login name is already taken. Edit the sender display name or add the user manually.',
registerAuthorNeedEmail: 'Sender email is missing; cannot create an account.',
registerAuthorNoQq: 'QQ Mail is not supported for author accounts. Please add the user manually.',
}, },
crawlerKeywords: { crawlerKeywords: {
pageTitle: 'Keyword Configuration', pageTitle: 'Keyword Configuration',

View File

@@ -45,6 +45,13 @@ const zh = {
status: '状态', status: '状态',
delete: '删除', delete: '删除',
deleteInfo: '您确定要删除该期刊分期吗?', deleteInfo: '您确定要删除该期刊分期吗?',
plagiarismNotChecked: '未检测',
plagiarismChecking: '正在检测…',
plagiarismRecheck: '重新查重',
plagiarismCheckFailed: '查重任务启动失败。',
plagiarismStatusFailed: '获取查重状态失败。',
plagiarismNoReportUrl: '报告链接暂不可用。',
plagiarismReportDetailFailed: '获取稿件详情失败,请稍后重试。',
}, },
menu: { menu: {
main: '个人中心', main: '个人中心',
@@ -639,6 +646,15 @@ const zh = {
printBtn: '打印', printBtn: '打印',
previewNotSupported: '该文件格式无法在线预览', previewNotSupported: '该文件格式无法在线预览',
downloadToView: '下载到本地查看', downloadToView: '下载到本地查看',
registerAuthorBtn: '创建作者账号',
registerAuthorConfirm:
'将使用推广后台「添加用户」接口创建账号:登录名「{account}」,显示名「{realname}」,邮箱「{email}」,初始密码 123456qwe无需验证码。是否继续',
registerAuthorSuccess: '作者账号已创建。',
registerAuthorFail: '创建失败,请稍后重试或到用户管理中手动添加。',
registerAuthorExistsEmail: '该邮箱已被注册。',
registerAuthorExistsAccount: '该登录名已被占用,请人工处理或修改发件人显示名后重试。',
registerAuthorNeedEmail: '缺少发件人邮箱,无法创建账号。',
registerAuthorNoQq: '本站不支持 QQ 邮箱作为作者账号,请在用户管理中手动添加。',
}, },
crawlerKeywords: { crawlerKeywords: {
pageTitle: '关键词配置', pageTitle: '关键词配置',

View File

@@ -1,5 +1,5 @@
<template> <template>
<div> <div class="article-detail-editor-page">
<div class="crumbs"> <div class="crumbs">
<div class="art_state_message_id" style="padding-left: 18px"> <div class="art_state_message_id" style="padding-left: 18px">
<font <font
@@ -915,9 +915,38 @@
}}</b> }}</b>
<!-- <el-button type="text" @click="testedit" icon="el-icon-edit">Change</el-button> --> <!-- <el-button type="text" @click="testedit" icon="el-icon-edit">Change</el-button> -->
</div> </div>
<div> <div class="detail-plagiarism-row">
<span>Repetition : </span> <span class="detail-plagiarism-main">
<b>{{ form.repetition }}%</b> <span class="detail-plagiarism-lbl">Repetition :</span>
<template v-if="detailPlagiarismUiShowProcessing()">
<span class="detail-plagiarism-loading">
<i class="el-icon-loading"></i>
{{ $t('articleListEditor.plagiarismChecking') }}
</span>
</template>
<template v-else-if="detailPlagiarismUiShowResult()">
<span
class="detail-plagiarism-pct-text"
:style="detailPlagiarismSimilarityStyle()"
@click.stop="detailOpenPlagiarismReport"
>
{{ detailPlagiarismSimilarityNumber() }}%
</span>
</template>
<template v-else>
<span class="detail-plagiarism-not-checked-text" @click.stop="detailTriggerCrossrefPlagiarismCheck">
{{ $t('articleListEditor.plagiarismNotChecked') }}
</span>
</template>
<el-button
v-if="detailPlagiarismShowRecheck()"
type="text"
class="detail-plagiarism-recheck-btn detail-plagiarism-recheck-inline"
@click.stop="detailTriggerCrossrefPlagiarismCheck"
>
{{ $t('articleListEditor.plagiarismRecheck') }}
</el-button>
</span>
</div> </div>
<!--<br clear="both"> <!--<br clear="both">
<el-button type="primary" @click="showResubmit" style="margin: 15px 0 0 0;">Resubmit the manuscript <el-button type="primary" @click="showResubmit" style="margin: 15px 0 0 0;">Resubmit the manuscript
@@ -937,9 +966,38 @@
}}</b> }}</b>
<el-button style="padding: 0" type="text" @click="testedit" icon="el-icon-edit">Change</el-button> <el-button style="padding: 0" type="text" @click="testedit" icon="el-icon-edit">Change</el-button>
</div> </div>
<div> <div class="detail-plagiarism-row detail-plagiarism-row--with-actions">
<span>Repetition : </span> <span class="detail-plagiarism-main">
<b>{{ form.repetition }}%</b> <span class="detail-plagiarism-lbl">Repetition :</span>
<template v-if="detailPlagiarismUiShowProcessing()">
<span class="detail-plagiarism-loading">
<i class="el-icon-loading"></i>
{{ $t('articleListEditor.plagiarismChecking') }}
</span>
</template>
<template v-else-if="detailPlagiarismUiShowResult()">
<span
class="detail-plagiarism-pct-text"
:style="detailPlagiarismSimilarityStyle()"
@click.stop="detailOpenPlagiarismReport"
>
{{ detailPlagiarismSimilarityNumber() }}%
</span>
</template>
<template v-else>
<span class="detail-plagiarism-not-checked-text" @click.stop="detailTriggerCrossrefPlagiarismCheck">
{{ $t('articleListEditor.plagiarismNotChecked') }}
</span>
</template>
<el-button
v-if="detailPlagiarismShowRecheck()"
type="text"
class="detail-plagiarism-recheck-btn detail-plagiarism-recheck-inline"
@click.stop="detailTriggerCrossrefPlagiarismCheck"
>
{{ $t('articleListEditor.plagiarismRecheck') }}
</el-button>
</span>
<a :href="mediaUrl + form.repeurl" v-if="form.repeurl" class="zip_load" target="_blank"> <a :href="mediaUrl + form.repeurl" v-if="form.repeurl" class="zip_load" target="_blank">
<img src="../../assets/img/icon_0.png" /> <img src="../../assets/img/icon_0.png" />
<span>Duplicate check file</span> <span>Duplicate check file</span>
@@ -1426,6 +1484,8 @@
import timetalk from './time_talk'; import timetalk from './time_talk';
import reviewerDetail from '../../components/page/components/articleDetail/reviewerdetail.vue'; import reviewerDetail from '../../components/page/components/articleDetail/reviewerdetail.vue';
import FigureCopyright from '../../components/page/components/articleDetail/FigureCopyright.vue'; import FigureCopyright from '../../components/page/components/articleDetail/FigureCopyright.vue';
import axios from 'axios';
import { getSimilarityStyle } from '../../utils/ithenticateSimilarityStyle';
export default { export default {
components: { components: {
timetalk, timetalk,
@@ -1549,6 +1609,9 @@ export default {
approval_content: '', approval_content: '',
is_figure_copyright: '', is_figure_copyright: '',
repetition: '', repetition: '',
plagiarism_similarity: '',
plagiarism_report_url: '',
plagiarism_job_state: '',
manuscirpt: '', manuscirpt: '',
remarks: '', remarks: '',
state: '', state: '',
@@ -1743,7 +1806,9 @@ export default {
underReview: ['1'], underReview: ['1'],
finalDecision: ['1'], finalDecision: ['1'],
is_figure_copyright: '', is_figure_copyright: '',
figurecopyright_file: '' figurecopyright_file: '',
plagiarismDetailPollTimer: null,
plagiarismDetailPending: false
}; };
}, },
async created() { async created() {
@@ -2541,6 +2606,26 @@ export default {
this.form.approval_file = res.article.approval_file; this.form.approval_file = res.article.approval_file;
this.form.approval_content = res.article.approval_content; this.form.approval_content = res.article.approval_content;
this.form.repetition = res.article.repetition; this.form.repetition = res.article.repetition;
this.$set(
this.form,
'plagiarism_similarity',
res.article.plagiarism_similarity != null && res.article.plagiarism_similarity !== ''
? res.article.plagiarism_similarity
: res.article.crossref_similarity != null && res.article.crossref_similarity !== ''
? res.article.crossref_similarity
: ''
);
this.$set(
this.form,
'plagiarism_report_url',
res.article.plagiarism_report_url || res.article.crossref_report_url || ''
);
this.$set(
this.form,
'plagiarism_job_state',
res.article.plagiarism_job_state || res.article.crossref_status || ''
);
this.detailInitPlagiarismAfterLoad();
this.form.remarks = res.article.remarks; this.form.remarks = res.article.remarks;
this.form.repeurl = res.article.repeurl; this.form.repeurl = res.article.repeurl;
this.repeform.repefen = res.article.repetition; this.repeform.repefen = res.article.repetition;
@@ -2856,11 +2941,188 @@ export default {
// 关闭弹窗 // 关闭弹窗
closeResubmit() { closeResubmit() {
(this.resubmitVisible = false), this.$refs['resubmitJournal'].resetFields(); (this.resubmitVisible = false), this.$refs['resubmitJournal'].resetFields();
},
/* ---------- Crossref plagiarism详情页右侧 ---------- */
detailPlagiarismArticleKey() {
return String(this.form.articleId || this.editform.articleId || this.$route.query.id || '');
},
detailNormalizePlagiarismStatusPayload(body) {
const root = body && typeof body === 'object' ? body : {};
if (root.code != null && Number(root.code) !== 0) {
return { status: 'api_error', similarity: null, reportUrl: '' };
}
let d = root.data != null ? root.data : root;
if (typeof d === 'string') {
try {
d = JSON.parse(d);
} catch (e) {
d = {};
}
}
if (!d || typeof d !== 'object') d = {};
const statusRaw = d.status || d.state || d.job_status || d.plagiarism_status || '';
const status = String(statusRaw).toLowerCase();
let sim =
d.similarity != null
? d.similarity
: d.percent != null
? d.percent
: d.similarity_percent != null
? d.similarity_percent
: d.crossref_similarity != null
? d.crossref_similarity
: null;
if (sim === '' || sim === undefined) sim = null;
const reportUrl = d.report_url || d.reportUrl || d.url || d.report_link || '';
return { status, similarity: sim, reportUrl: String(reportUrl || '') };
},
detailPlagiarismSimilarityRaw() {
const f = this.form;
if (!f) return null;
if (f.plagiarism_similarity != null && f.plagiarism_similarity !== '') return f.plagiarism_similarity;
if (f.crossref_similarity != null && f.crossref_similarity !== '') return f.crossref_similarity;
const st = String(f.plagiarism_job_state || f.crossref_status || '').toLowerCase();
if (['completed', 'done', 'success', 'complete'].includes(st) && (f.plagiarism_similarity === 0 || f.plagiarism_similarity === '0')) {
return 0;
}
const rep = f.repetition;
if (rep != null && rep !== '' && Number(rep) > 0) return rep;
return null;
},
detailPlagiarismSimilarityNumber() {
const n = this.detailPlagiarismSimilarityRaw();
if (n == null || n === '') return 0;
const x = Number(n);
return isNaN(x) ? 0 : Math.round(x * 10) / 10;
},
detailPlagiarismUiShowProcessing() {
if (this.form._plagiarismLocalLoading) return true;
if (this.plagiarismDetailPending) return true;
const st = String(this.form.plagiarism_job_state || this.form.crossref_status || '').toLowerCase();
return ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st);
},
detailPlagiarismUiShowResult() {
if (this.detailPlagiarismUiShowProcessing()) return false;
const st = String(this.form.plagiarism_job_state || this.form.crossref_status || '').toLowerCase();
const done = ['completed', 'done', 'success', 'complete'].includes(st);
const raw = this.detailPlagiarismSimilarityRaw();
if (raw != null && raw !== '' && !isNaN(Number(raw))) {
if (Number(raw) === 0 && !done && !(this.form.plagiarism_report_url || this.form.crossref_report_url)) {
return false;
}
return true;
}
if (done && (this.form.plagiarism_similarity === 0 || this.form.plagiarism_similarity === '0')) return true;
return !!(this.form.plagiarism_report_url || this.form.crossref_report_url);
},
detailPlagiarismSimilarityStyle() {
const n = Number(this.detailPlagiarismSimilarityNumber());
return { color: getSimilarityStyle(n).color };
},
detailPlagiarismShowRecheck() {
if (this.detailPlagiarismUiShowProcessing()) return false;
return Number(this.form.state) === 6;
},
detailInitPlagiarismAfterLoad() {
const st = String(this.form.plagiarism_job_state || '').toLowerCase();
if (['pending', 'processing', 'queued', 'running', 'submitted'].includes(st)) {
this.plagiarismDetailPending = true;
this.detailEnsurePlagiarismPoll();
}
},
detailEnsurePlagiarismPoll() {
if (!this.plagiarismDetailPending) {
this.detailStopPlagiarismPolling();
return;
}
if (!this.plagiarismDetailPollTimer) {
this.detailPollPlagiarismOnce();
this.plagiarismDetailPollTimer = setInterval(() => this.detailPollPlagiarismOnce(), 60000);
}
},
detailStopPlagiarismPolling() {
if (this.plagiarismDetailPollTimer) {
clearInterval(this.plagiarismDetailPollTimer);
this.plagiarismDetailPollTimer = null;
}
},
async detailPollPlagiarismOnce() {
const key = this.detailPlagiarismArticleKey();
if (!key || !this.plagiarismDetailPending) return;
try {
const res = await axios.get('/api/plagiarism/status', { params: { article_id: key } });
const body = res && res.data;
const norm = this.detailNormalizePlagiarismStatusPayload(body);
if (norm.status === 'api_error') {
this.plagiarismDetailPending = false;
this.detailStopPlagiarismPolling();
return;
}
if (norm.similarity != null && norm.similarity !== '') {
this.$set(this.form, 'plagiarism_similarity', norm.similarity);
}
if (norm.reportUrl) this.$set(this.form, 'plagiarism_report_url', norm.reportUrl);
if (norm.status) this.$set(this.form, 'plagiarism_job_state', norm.status);
const active = ['pending', 'processing', 'queued', 'running', 'submitted'];
const terminal = ['completed', 'done', 'success', 'complete', 'failed', 'error', 'fail', 'cancelled'];
const isActive = active.includes(norm.status);
let clearPending =
terminal.includes(norm.status) ||
norm.status === 'error' ||
(!isActive && norm.reportUrl && norm.similarity != null && norm.similarity !== '');
if (clearPending) {
this.plagiarismDetailPending = false;
}
this.detailEnsurePlagiarismPoll();
} catch (e) {
console.error(e);
this.$message.error(this.$t('articleListEditor.plagiarismStatusFailed'));
}
},
async detailTriggerCrossrefPlagiarismCheck() {
const key = this.detailPlagiarismArticleKey();
if (!key) return;
if (this.detailPlagiarismUiShowProcessing()) return;
this.$set(this.form, '_plagiarismLocalLoading', true);
try {
const res = await this.$api.post('api/plagiarism/check', { article_id: key });
if (res && Number(res.code) === 0) {
this.plagiarismDetailPending = true;
this.$set(this.form, 'plagiarism_job_state', 'pending');
this.detailEnsurePlagiarismPoll();
await this.detailPollPlagiarismOnce();
} else {
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismCheckFailed'));
}
} catch (e) {
console.error(e);
this.$message.error(this.$t('articleListEditor.plagiarismCheckFailed'));
} finally {
this.$set(this.form, '_plagiarismLocalLoading', false);
}
},
detailOpenPlagiarismReport() {
const raw = (this.form.plagiarism_report_url || this.form.crossref_report_url || '').trim();
if (!raw) {
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
return;
}
let full = raw;
if (!/^https?:\/\//i.test(raw)) {
const base = (this.mediaUrl || '').replace(/\/+$/, '');
const path = raw.replace(/^\/+/, '');
full = base ? `${base}/${path}` : `/${path}`;
}
window.open(full, '_blank');
} }
}, },
mounted() { mounted() {
this.opname = this.$route.query.mark; this.opname = this.$route.query.mark;
this.resubmitJournal.manuscriptId = this.$route.query.id; this.resubmitJournal.manuscriptId = this.$route.query.id;
},
beforeDestroy() {
this.detailStopPlagiarismPolling();
} }
}; };
</script> </script>
@@ -3345,4 +3607,80 @@ td {
.copyright-declaration-wrapper :deep(.el-radio.is-checked .el-radio__label) { .copyright-declaration-wrapper :deep(.el-radio.is-checked .el-radio__label) {
font-weight: 500; font-weight: 500;
} }
/* 详情页:去掉全局 main.css 中 .art_caozuo_ 的浅蓝底;查重行无额外底色 */
.article-detail-editor-page .art_caozuo_ {
background-color: #fff !important;
border: 1px solid #ebeef5 !important;
}
.detail-plagiarism-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px 8px;
background: transparent !important;
padding: 4px 0 8px 0;
margin: 0 0 10px 0;
}
/* 合并「Repetition :」与数值,避免命中 .art_caozuo_ > div > span 的 min-width:65px 造成巨大空隙 */
.article-detail-editor-page .art_caozuo_ > div > span.detail-plagiarism-main {
min-width: 0 !important;
width: auto !important;
max-width: 100%;
margin-right: 8px !important;
display: inline-flex;
align-items: baseline;
flex-wrap: wrap;
gap: 2px 6px;
vertical-align: middle;
}
.article-detail-editor-page .art_caozuo_ .detail-plagiarism-lbl {
color: #777;
margin: 0 2px 0 0 !important;
min-width: 0 !important;
width: auto !important;
white-space: nowrap;
}
.detail-plagiarism-loading {
color: #409eff;
font-size: 13px;
font-weight: 600;
}
.detail-plagiarism-loading .el-icon-loading {
margin-right: 6px;
}
.detail-plagiarism-pct-text {
cursor: pointer;
font-size: 14px;
font-weight: 700;
background: none !important;
border: none !important;
padding: 0;
line-height: 1.2;
}
.detail-plagiarism-pct-text:hover {
opacity: 0.92;
}
.detail-plagiarism-not-checked-text {
cursor: pointer;
font-size: 13px;
font-weight: 600;
color: #409eff;
text-decoration: underline;
text-underline-offset: 2px;
}
.detail-plagiarism-not-checked-text:hover {
color: #66b1ff;
}
.detail-plagiarism-recheck-btn {
margin-left: 4px !important;
font-weight: 600 !important;
}
/* 紧跟在百分比 / 未检测 文案后,避免继承整行大间距 */
.detail-plagiarism-main .detail-plagiarism-recheck-inline.el-button--text {
margin-left: 0 !important;
padding: 0 2px !important;
vertical-align: baseline;
line-height: inherit;
}
</style> </style>

View File

@@ -116,9 +116,36 @@
<i class="el-icon-data-line"></i> Manuscript Tracking <i class="el-icon-data-line"></i> Manuscript Tracking
</b> </b>
<span style="float: right"> <span style="float: right" class="plagiarism-header-bar">
<span class="labelTitle" style="font-weight: 500; font-size: 13px">Plagiarism Check :</span> <span class="labelTitle" style="font-weight: 500; font-size: 13px">Plagiarism Check :</span>
<font style="margin-right: 16px; font-size: 13px; font-weight: 700"> {{ item.repetition }} % </font> <template v-if="plagiarismUiShowProcessing(item)">
<span class="plagiarism-inline-loading plagiarism-on-blue">
<i class="el-icon-loading"></i>
<span>{{ $t('articleListEditor.plagiarismChecking') }}</span>
</span>
</template>
<template v-else-if="plagiarismUiShowResult(item)">
<span
class="plagiarism-pct-text plagiarism-on-blue"
@click.stop="openPlagiarismReport(item)"
>
{{ plagiarismSimilarityNumber(item) }}%
</span>
</template>
<template v-else>
<span class="plagiarism-not-checked-text plagiarism-on-blue" @click.stop="triggerCrossrefPlagiarismCheck(item)">
{{ $t('articleListEditor.plagiarismNotChecked') }}
</span>
</template>
<el-button
v-if="plagiarismShowRecheck(item)"
type="text"
size="mini"
class="plagiarism-recheck-btn"
@click.native.stop="triggerCrossrefPlagiarismCheck(item)"
>
{{ $t('articleListEditor.plagiarismRecheck') }}
</el-button>
</span> </span>
<span style="margin: 0 10px; float: right">| </span> <span style="margin: 0 10px; float: right">| </span>
@@ -1065,6 +1092,7 @@
</template> </template>
<script> <script>
import axios from 'axios';
import { Loading } from 'element-ui'; import { Loading } from 'element-ui';
import timetalk from './time_talk'; import timetalk from './time_talk';
import commonRemarkList from './articleListEditor_A_list.vue'; import commonRemarkList from './articleListEditor_A_list.vue';
@@ -1432,12 +1460,18 @@ export default {
editVisible1: false, editVisible1: false,
bankVisible: false, bankVisible: false,
majorData: {}, majorData: {},
googleSearchInfo: '' googleSearchInfo: '',
/** Crossref / plagiarism async polling */
plagiarismPollTimer: null,
plagiarismPendingIds: {}
}; };
}, },
created() { created() {
this.getdate(); this.getdate();
}, },
beforeDestroy() {
this.stopPlagiarismPolling();
},
computed: { computed: {
upload_zip: function () { upload_zip: function () {
return this.baseUrl + 'api/Article/up_file/type/repezip'; return this.baseUrl + 'api/Article/up_file/type/repezip';
@@ -2072,6 +2106,7 @@ export default {
this.tableData[i].reportList = this.tableData[i].reportList.slice(0, 3); this.tableData[i].reportList = this.tableData[i].reportList.slice(0, 3);
this.$forceUpdate(); this.$forceUpdate();
} }
this.initPlagiarismFromList();
loading.close(); loading.close();
}) })
@@ -2658,6 +2693,234 @@ export default {
return str; return str;
}, },
/* ---------- Crossref plagiarism (list header) ---------- */
_plagiarismArticleKey(item) {
return item && item.article_id != null ? String(item.article_id) : '';
},
_plagiarismPending(articleKey) {
return !!(articleKey && this.plagiarismPendingIds[articleKey]);
},
plagiarismSimilarityNumber(item) {
const n = this.plagiarismSimilarityRaw(item);
if (n == null || n === '') return 0;
const x = Number(n);
return isNaN(x) ? 0 : Math.round(x * 10) / 10;
},
plagiarismSimilarityRaw(item) {
if (!item) return null;
if (item.plagiarism_similarity != null && item.plagiarism_similarity !== '') return item.plagiarism_similarity;
if (item.crossref_similarity != null && item.crossref_similarity !== '') return item.crossref_similarity;
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
if (['completed', 'done', 'success', 'complete'].includes(st) && (item.plagiarism_similarity === 0 || item.plagiarism_similarity === '0')) {
return 0;
}
const rep = item.repetition;
if (rep != null && rep !== '' && Number(rep) > 0) return rep;
return null;
},
plagiarismUiShowProcessing(item) {
const key = this._plagiarismArticleKey(item);
if (!key) return false;
if (item._plagiarismLocalLoading) return true;
if (this._plagiarismPending(key)) return true;
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
return ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st);
},
plagiarismUiShowResult(item) {
if (this.plagiarismUiShowProcessing(item)) return false;
const st = String(item.plagiarism_job_state || item.crossref_status || '').toLowerCase();
const done = ['completed', 'done', 'success', 'complete'].includes(st);
const raw = this.plagiarismSimilarityRaw(item);
if (raw != null && raw !== '' && !isNaN(Number(raw))) {
if (Number(raw) === 0 && !done && !(item.plagiarism_report_url || item.crossref_report_url)) {
return false;
}
return true;
}
if (done && (item.plagiarism_similarity === 0 || item.plagiarism_similarity === '0')) return true;
return !!(item.plagiarism_report_url || item.crossref_report_url);
},
plagiarismShowRecheck(item) {
if (this.plagiarismUiShowProcessing(item)) return false;
return Number(item.state) === 6;
},
initPlagiarismFromList() {
if (!this.tableData || !this.tableData.length) {
this.reconcilePlagiarismPollTimer();
return;
}
this.tableData.forEach((row) => {
if (row.crossref_report_url && !row.plagiarism_report_url) {
this.$set(row, 'plagiarism_report_url', row.crossref_report_url);
}
if (row.crossref_similarity != null && row.plagiarism_similarity == null) {
this.$set(row, 'plagiarism_similarity', row.crossref_similarity);
}
if (row.crossref_status && !row.plagiarism_job_state) {
this.$set(row, 'plagiarism_job_state', row.crossref_status);
}
const key = this._plagiarismArticleKey(row);
const st = String(row.plagiarism_job_state || '').toLowerCase();
if (key && ['pending', 'processing', 'queued', 'running', 'submitted'].includes(st)) {
this.$set(this.plagiarismPendingIds, key, true);
}
});
this.reconcilePlagiarismPollTimer();
},
reconcilePlagiarismPollTimer() {
const hasPending = Object.keys(this.plagiarismPendingIds).some((k) => this.plagiarismPendingIds[k]);
if (hasPending) {
if (!this.plagiarismPollTimer) {
this.pollPlagiarismPendingOnce();
this.plagiarismPollTimer = setInterval(() => this.pollPlagiarismPendingOnce(), 60000);
}
} else if (this.plagiarismPollTimer) {
clearInterval(this.plagiarismPollTimer);
this.plagiarismPollTimer = null;
}
},
stopPlagiarismPolling() {
if (this.plagiarismPollTimer) {
clearInterval(this.plagiarismPollTimer);
this.plagiarismPollTimer = null;
}
this.plagiarismPendingIds = {};
},
pollPlagiarismPendingOnce() {
Object.keys(this.plagiarismPendingIds).forEach((key) => {
if (this.plagiarismPendingIds[key]) this.fetchPlagiarismStatusByArticleId(key);
});
},
_normalizePlagiarismStatusPayload(body) {
const root = body && typeof body === 'object' ? body : {};
if (root.code != null && Number(root.code) !== 0) {
return { status: 'api_error', similarity: null, reportUrl: '' };
}
let d = root.data != null ? root.data : root;
if (typeof d === 'string') {
try {
d = JSON.parse(d);
} catch (e) {
d = {};
}
}
if (!d || typeof d !== 'object') d = {};
const statusRaw = d.status || d.state || d.job_status || d.plagiarism_status || '';
const status = String(statusRaw).toLowerCase();
let sim =
d.similarity != null
? d.similarity
: d.percent != null
? d.percent
: d.similarity_percent != null
? d.similarity_percent
: d.crossref_similarity != null
? d.crossref_similarity
: null;
if (sim === '' || sim === undefined) sim = null;
const reportUrl = d.report_url || d.reportUrl || d.url || d.report_link || '';
return { status, similarity: sim, reportUrl: String(reportUrl || '') };
},
findTableRowByArticleId(articleId) {
const id = String(articleId);
return (this.tableData || []).find((r) => String(r.article_id) === id) || null;
},
async fetchPlagiarismStatusByArticleId(articleId) {
const key = String(articleId);
try {
const res = await axios.get('/api/plagiarism/status', { params: { article_id: key } });
const body = res && res.data;
const norm = this._normalizePlagiarismStatusPayload(body);
if (norm.status === 'api_error') {
this.$set(this.plagiarismPendingIds, key, false);
this.reconcilePlagiarismPollTimer();
return;
}
const row = this.findTableRowByArticleId(key);
if (row) {
if (norm.similarity != null && norm.similarity !== '') {
this.$set(row, 'plagiarism_similarity', norm.similarity);
}
if (norm.reportUrl) this.$set(row, 'plagiarism_report_url', norm.reportUrl);
if (norm.status) this.$set(row, 'plagiarism_job_state', norm.status);
}
const active = ['pending', 'processing', 'queued', 'running', 'submitted'];
const terminal = ['completed', 'done', 'success', 'complete', 'failed', 'error', 'fail', 'cancelled'];
const isActive = active.includes(norm.status);
let clearPending =
terminal.includes(norm.status) ||
norm.status === 'error' ||
(!isActive && norm.reportUrl && norm.similarity != null && norm.similarity !== '');
if (clearPending) {
this.$set(this.plagiarismPendingIds, key, false);
}
this.reconcilePlagiarismPollTimer();
} catch (e) {
console.error(e);
this.$message.error(this.$t('articleListEditor.plagiarismStatusFailed'));
}
},
async triggerCrossrefPlagiarismCheck(item) {
const key = this._plagiarismArticleKey(item);
if (!key) return;
if (this.plagiarismUiShowProcessing(item)) return;
this.$set(item, '_plagiarismLocalLoading', true);
try {
const res = await this.$api.post('api/plagiarism/check', { article_id: key });
if (res && Number(res.code) === 0) {
this.$set(this.plagiarismPendingIds, key, true);
this.$set(item, 'plagiarism_job_state', 'pending');
this.reconcilePlagiarismPollTimer();
await this.fetchPlagiarismStatusByArticleId(key);
} else {
this.$message.error((res && res.msg) || this.$t('articleListEditor.plagiarismCheckFailed'));
}
} catch (e) {
console.error(e);
this.$message.error(this.$t('articleListEditor.plagiarismCheckFailed'));
} finally {
this.$set(item, '_plagiarismLocalLoading', false);
}
},
async openPlagiarismReport(item) {
const articleId = item && item.article_id;
if (!articleId) {
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
return;
}
let raw = '';
try {
const res = await this.$api.post('api/Article/getArticleDetail', {
articleId,
human: 'editor'
});
console.log("🚀 ~ openPlagiarismReport ~ res:", res);
const a = res && res.article;
if (a) {
raw = String(
a.repeurl || ''
).trim();
}
} catch (e) {
console.error(e);
this.$message.error(this.$t('articleListEditor.plagiarismReportDetailFailed'));
return;
}
const url = String(raw).trim();
if (!url) {
this.$message.warning(this.$t('articleListEditor.plagiarismNoReportUrl'));
return;
}
let full = url;
if (!/^https?:\/\//i.test(url)) {
const base = (this.mediaUrl || '').replace(/\/+$/, '');
const path = url.replace(/^\/+/, '');
full = base ? `${base}/${path}` : `/${path}`;
}
window.open(full, '_blank');
},
//文章类型 //文章类型
@@ -3426,4 +3689,65 @@ td {
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
/* box-shadow: 0 2px 8px rgba(0,0,0,0.1); */ /* box-shadow: 0 2px 8px rgba(0,0,0,0.1); */
} }
/* Crossref plagiarism strip (blue header bar) */
.plagiarism-header-bar {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 6px 8px;
margin-right: 4px;
max-width: 48%;
justify-content: flex-end;
}
.plagiarism-on-blue {
vertical-align: middle;
}
.articleTopBaseInfo .plagiarism-inline-loading {
color: #e6f4ff;
font-size: 12px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
margin-right: 8px;
}
.articleTopBaseInfo .plagiarism-inline-loading .el-icon-loading {
font-size: 14px;
}
.articleTopBaseInfo .plagiarism-pct-text {
cursor: pointer;
margin-right: 4px;
font-size: 13px;
font-weight: 800;
color: #fff;
background: none !important;
border: none !important;
padding: 0;
line-height: 1.2;
}
.articleTopBaseInfo .plagiarism-pct-text:hover {
filter: brightness(1.08);
}
.articleTopBaseInfo .plagiarism-not-checked-text {
cursor: pointer;
margin-right: 4px;
font-size: 13px;
font-weight: 600;
color: #e6f4ff;
text-decoration: underline;
text-underline-offset: 2px;
}
.articleTopBaseInfo .plagiarism-not-checked-text:hover {
color: #fff;
}
.articleTopBaseInfo .plagiarism-recheck-btn {
color: #e6f4ff !important;
padding: 0 4px !important;
font-weight: 600;
}
.articleTopBaseInfo .plagiarism-recheck-btn:hover {
color: #fff !important;
text-decoration: underline;
}
</style> </style>

View File

@@ -5,6 +5,18 @@
<h1 class="mail-subject-top">{{ $t('mailboxCollect.subject') }}{{ mailData.subject }}</h1> <h1 class="mail-subject-top">{{ $t('mailboxCollect.subject') }}{{ mailData.subject }}</h1>
</div> </div>
<div class="toolbar-right"> <div class="toolbar-right">
<el-button
v-if="hasWordAttachment"
type="primary"
size="small"
plain
icon="el-icon-user-solid"
:loading="registerAuthorLoading"
class="register-author-btn"
@click="registerAuthorFromMail"
>
{{ $t('mailboxCollect.registerAuthorBtn') }}
</el-button>
<!-- <i class="el-icon-star-off action-icon"></i> --> <!-- <i class="el-icon-star-off action-icon"></i> -->
<i class="el-icon-close action-icon" @click="$emit('close')"></i> <i class="el-icon-close action-icon" @click="$emit('close')"></i>
</div> </div>
@@ -70,6 +82,18 @@
<el-link type="primary" :underline="false" @click="scrollToAttachments" class="jump-link"> <el-link type="primary" :underline="false" @click="scrollToAttachments" class="jump-link">
{{ $t('mailboxCollect.viewAttachments') }} {{ $t('mailboxCollect.viewAttachments') }}
</el-link> </el-link>
<el-button
v-if="hasWordAttachment"
type="primary"
size="mini"
plain
icon="el-icon-user-solid"
:loading="registerAuthorLoading"
class="register-author-btn-inline"
@click="registerAuthorFromMail"
>
{{ $t('mailboxCollect.registerAuthorBtn') }}
</el-button>
</div> </div>
</div> </div>
</div> </div>
@@ -152,10 +176,20 @@ export default {
mediaUrl: Common.mediaUrl, mediaUrl: Common.mediaUrl,
isDetailExpanded: false, isDetailExpanded: false,
downloadingIndex: -1, downloadingIndex: -1,
packingAll: false packingAll: false,
registerAuthorLoading: false
}; };
}, },
computed: { computed: {
/** 存在 Word 附件(.doc / .docx时显示「创建作者账号」 */
hasWordAttachment() {
const att = (this.mailData && this.mailData.attachments) || [];
if (!att.length) return false;
return att.some((f) => {
const n = (f && f.name) || '';
return /\.(doc|docx)$/i.test(n);
});
},
totalAttachmentSize() { totalAttachmentSize() {
if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B'; if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B';
const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0); const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
@@ -175,6 +209,127 @@ export default {
} }
}, },
methods: { methods: {
stripHtml(html) {
if (html == null || html === '') return '';
const s = String(html);
const d = typeof document !== 'undefined' ? document.createElement('div') : null;
if (d) {
d.innerHTML = s;
return (d.textContent || d.innerText || '').trim();
}
return s.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
},
/** 与 partyList 添加用户一致:从正文尝试提取手机号(国内 11 位或含 + 的国际号) */
extractPhoneFromMailBody() {
const m = this.mailData || {};
let blob = '';
['content_text', 'content', 'body', 'content_html', 'html', 'body_html'].forEach((k) => {
const v = m[k];
if (v != null && String(v).trim() !== '') blob += `\n${String(v)}`;
});
const text = this.stripHtml(blob);
const cn = text.match(/(?:^|\D)(1[3-9]\d{9})(?:\D|$)/);
if (cn) return cn[1];
const intl = text.match(/\+\d{1,3}[\s\-]?\d[\d\s\-]{8,18}\d/);
if (intl) return intl[0].replace(/\s+/g, ' ').trim().slice(0, 32);
const labeled = text.match(/(?:Tel|Phone|Mobile|MW)[\s:]*([+()\d][\d\s\-().]{10,40})/i);
if (labeled) return labeled[1].trim().slice(0, 32);
return '';
},
buildAuthorRegisterPayload() {
const m = this.mailData || {};
const email = String(m.from_email || '')
.trim()
.toLowerCase();
const localPart = email.split('@')[0] || 'user';
let rawName = String(m.from_name || '').trim();
let realname = rawName || localPart;
if (this.$validateString && !this.$validateString(realname)) {
realname = localPart;
if (this.$validateString && !this.$validateString(realname)) {
realname = 'Author';
}
}
let account = rawName.replace(/[^a-zA-Z0-9_-]/g, '');
if (!account || account.length < 2) {
account = localPart.replace(/[^a-zA-Z0-9_-]/g, '');
}
if (!account || account.length < 2) {
account = `u${email.replace(/[^a-zA-Z0-9]/g, '').slice(0, 12) || 'ser'}`;
}
const phone = this.extractPhoneFromMailBody() || '';
return { email, account, realname, phone };
},
async registerAuthorFromMail() {
if (!this.hasWordAttachment) return;
const { email, account, realname, phone } = this.buildAuthorRegisterPayload();
if (!email) {
this.$message.warning(this.$t('mailboxCollect.registerAuthorNeedEmail'));
return;
}
if (email.endsWith('@qq.com')) {
this.$message.warning(this.$t('mailboxCollect.registerAuthorNoQq'));
return;
}
const pwd = '123456qwe';
try {
await this.$confirm(
this.$t('mailboxCollect.registerAuthorConfirm', { email, account, realname }),
this.$t('mailboxCollect.registerAuthorBtn'),
{ type: 'warning', distinguishCancelAndClose: true }
);
} catch (e) {
return;
}
this.registerAuthorLoading = true;
try {
const p = this.buildAuthorRegisterPayload();
const email2 = p.email;
const account2 = p.account;
const realname2 = p.realname;
const phone2 = p.phone;
const ef = { email: email2, account: account2 };
const r1 = await this.$api.post('api/User/checkUserByEmail', ef);
if (!r1 || Number(r1.code) !== 0) {
this.$message.error((r1 && r1.msg) || this.$t('mailboxCollect.registerAuthorFail'));
return;
}
const r2 = await this.$api.post('api/User/checkUserByAccount', ef);
if (!r2 || Number(r2.code) !== 0) {
this.$message.error((r2 && r2.msg) || this.$t('mailboxCollect.registerAuthorFail'));
return;
}
const hasEmail = r1.data && Number(r1.data.has) === 1;
const hasAccount = r2.data && Number(r2.data.has) === 1;
if (hasEmail) {
this.$message.warning(this.$t('mailboxCollect.registerAuthorExistsEmail'));
return;
}
if (hasAccount) {
this.$message.warning(this.$t('mailboxCollect.registerAuthorExistsAccount'));
return;
}
const addForm = {
email: email2,
account: account2,
password: pwd,
repassword: pwd,
realname: realname2,
phone: phone2 || ''
};
const res = await this.$api.post('api/User/addUser', addForm);
if (res && Number(res.code) === 0) {
this.$message.success(this.$t('mailboxCollect.registerAuthorSuccess'));
} else {
this.$message.error((res && res.msg) || this.$t('mailboxCollect.registerAuthorFail'));
}
} catch (err) {
console.error(err);
this.$message.error(this.$t('mailboxCollect.registerAuthorFail'));
} finally {
this.registerAuthorLoading = false;
}
},
escapeHtml(text) { escapeHtml(text) {
if (text == null) return ''; if (text == null) return '';
return String(text) return String(text)
@@ -383,6 +538,19 @@ const res = await this.$api.post('api/email_client/getAttachment', {
color: #606266; color: #606266;
cursor: pointer; cursor: pointer;
} }
.toolbar-right {
display: flex;
align-items: center;
gap: 12px;
flex-shrink: 0;
}
.register-author-btn {
flex-shrink: 0;
}
.register-author-btn-inline {
margin-left: 10px;
vertical-align: middle;
}
.action-icon:hover { .action-icon:hover {
color: #409eff; color: #409eff;
} }
@@ -665,7 +833,9 @@ const res = await this.$api.post('api/email_client/getAttachment', {
} }
.attachment-brief-bar { .attachment-brief-bar {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
gap: 8px 12px;
padding: 8px 0; padding: 8px 0;
font-size: 13px; font-size: 13px;
color: #606266; color: #606266;
@@ -681,7 +851,7 @@ const res = await this.$api.post('api/email_client/getAttachment', {
margin-left: 4px; margin-left: 4px;
} }
.jump-link { .jump-link {
margin-left: 15px; margin-left: 0;
font-size: 13px; font-size: 13px;
} }
.attachment-section { .attachment-section {

View File

@@ -0,0 +1,15 @@
/**
* 基于 iThenticate 标准的颜色映射(与编辑端常见分档一致)
* @param {number|string} score 相似度百分比(可为小数,内部会规范化)
* @returns {{ color: string, label: string }}
*/
export function getSimilarityStyle(score) {
let n = Number(score);
if (!Number.isFinite(n) || n < 0) n = 0;
const s = Math.round(n);
if (s === 0) return { color: '#0000FF', label: 'Blue' };
if (s <= 24) return { color: '#008000', label: 'Green' };
if (s <= 49) return { color: '#EDBD3E', label: 'Yellow' };
if (s <= 74) return { color: '#FF8C00', label: 'Orange' };
return { color: '#FF0000', label: 'Red' };
}