This commit is contained in:
2026-05-13 09:47:36 +08:00
parent f67d8d5600
commit b10de50fdf
5 changed files with 702 additions and 11 deletions

View File

@@ -47,6 +47,13 @@ const en = {
status: 'Status',
delete: 'Delete',
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: {
main: 'Personal Center',

View File

@@ -45,6 +45,13 @@ const zh = {
status: '状态',
delete: '删除',
deleteInfo: '您确定要删除该期刊分期吗?',
plagiarismNotChecked: '未检测',
plagiarismChecking: '正在检测…',
plagiarismRecheck: '重新查重',
plagiarismCheckFailed: '查重任务启动失败。',
plagiarismStatusFailed: '获取查重状态失败。',
plagiarismNoReportUrl: '报告链接暂不可用。',
plagiarismReportDetailFailed: '获取稿件详情失败,请稍后重试。',
},
menu: {
main: '个人中心',

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="article-detail-editor-page">
<div class="crumbs">
<div class="art_state_message_id" style="padding-left: 18px">
<font
@@ -915,9 +915,38 @@
}}</b>
<!-- <el-button type="text" @click="testedit" icon="el-icon-edit">Change</el-button> -->
</div>
<div>
<span>Repetition : </span>
<b>{{ form.repetition }}%</b>
<div class="detail-plagiarism-row">
<span class="detail-plagiarism-main">
<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>
<!--<br clear="both">
<el-button type="primary" @click="showResubmit" style="margin: 15px 0 0 0;">Resubmit the manuscript
@@ -937,9 +966,38 @@
}}</b>
<el-button style="padding: 0" type="text" @click="testedit" icon="el-icon-edit">Change</el-button>
</div>
<div>
<span>Repetition : </span>
<b>{{ form.repetition }}%</b>
<div class="detail-plagiarism-row detail-plagiarism-row--with-actions">
<span class="detail-plagiarism-main">
<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">
<img src="../../assets/img/icon_0.png" />
<span>Duplicate check file</span>
@@ -1426,6 +1484,8 @@
import timetalk from './time_talk';
import reviewerDetail from '../../components/page/components/articleDetail/reviewerdetail.vue';
import FigureCopyright from '../../components/page/components/articleDetail/FigureCopyright.vue';
import axios from 'axios';
import { getSimilarityStyle } from '../../utils/ithenticateSimilarityStyle';
export default {
components: {
timetalk,
@@ -1549,6 +1609,9 @@ export default {
approval_content: '',
is_figure_copyright: '',
repetition: '',
plagiarism_similarity: '',
plagiarism_report_url: '',
plagiarism_job_state: '',
manuscirpt: '',
remarks: '',
state: '',
@@ -1743,7 +1806,9 @@ export default {
underReview: ['1'],
finalDecision: ['1'],
is_figure_copyright: '',
figurecopyright_file: ''
figurecopyright_file: '',
plagiarismDetailPollTimer: null,
plagiarismDetailPending: false
};
},
async created() {
@@ -2541,6 +2606,26 @@ export default {
this.form.approval_file = res.article.approval_file;
this.form.approval_content = res.article.approval_content;
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.repeurl = res.article.repeurl;
this.repeform.repefen = res.article.repetition;
@@ -2856,11 +2941,188 @@ export default {
// 关闭弹窗
closeResubmit() {
(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() {
this.opname = this.$route.query.mark;
this.resubmitJournal.manuscriptId = this.$route.query.id;
},
beforeDestroy() {
this.detailStopPlagiarismPolling();
}
};
</script>
@@ -3345,4 +3607,80 @@ td {
.copyright-declaration-wrapper :deep(.el-radio.is-checked .el-radio__label) {
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>

View File

@@ -116,9 +116,36 @@
<i class="el-icon-data-line"></i> Manuscript Tracking
</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>
<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 style="margin: 0 10px; float: right">| </span>
@@ -1065,6 +1092,7 @@
</template>
<script>
import axios from 'axios';
import { Loading } from 'element-ui';
import timetalk from './time_talk';
import commonRemarkList from './articleListEditor_A_list.vue';
@@ -1432,12 +1460,18 @@ export default {
editVisible1: false,
bankVisible: false,
majorData: {},
googleSearchInfo: ''
googleSearchInfo: '',
/** Crossref / plagiarism async polling */
plagiarismPollTimer: null,
plagiarismPendingIds: {}
};
},
created() {
this.getdate();
},
beforeDestroy() {
this.stopPlagiarismPolling();
},
computed: {
upload_zip: function () {
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.$forceUpdate();
}
this.initPlagiarismFromList();
loading.close();
})
@@ -2658,6 +2693,234 @@ export default {
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;
/* 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>

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' };
}