Compare commits
6 Commits
Editorial-
...
4426077094
| Author | SHA1 | Date | |
|---|---|---|---|
| 4426077094 | |||
| 6050dd365d | |||
| 723ec0d190 | |||
| b10de50fdf | |||
| f67d8d5600 | |||
| 67a4875b01 |
@@ -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: '/', //正式
|
||||
|
||||
});
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
//记得切换
|
||||
|
||||
//正式
|
||||
const mediaUrl = '/public/';
|
||||
const baseUrl = '/';
|
||||
// const mediaUrl = '/public/';
|
||||
// const baseUrl = '/';
|
||||
|
||||
//正式环境
|
||||
|
||||
// const mediaUrl = 'https://submission.tmrjournals.com/public/';
|
||||
// // const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
|
||||
// const baseUrl = '/api'
|
||||
const mediaUrl = 'https://submission.tmrjournals.com/public/';
|
||||
// const mediaUrl = 'http://zmzm.tougao.dev.com/public/';
|
||||
const baseUrl = '/api'
|
||||
|
||||
//测试环境
|
||||
|
||||
|
||||
@@ -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',
|
||||
@@ -650,6 +657,15 @@ colTitle: 'Template title',
|
||||
printBtn: 'Print',
|
||||
previewNotSupported: 'This file format cannot be previewed online',
|
||||
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: {
|
||||
pageTitle: 'Keyword Configuration',
|
||||
@@ -995,8 +1011,8 @@ colTitle: 'Template title',
|
||||
step3: 'References',
|
||||
step: 'step',
|
||||
Information: 'Fill in information',
|
||||
|
||||
|
||||
startPreAccept: 'Start the pre-acceptance process',
|
||||
startPreAcceptWithPayment: 'Start the pre-acceptance process and complete your payment',
|
||||
},
|
||||
Formula: {
|
||||
FormulaTemplate: 'Formula Template'
|
||||
@@ -1243,6 +1259,7 @@ colTitle: 'Template title',
|
||||
,
|
||||
autoPromotionLogs: {
|
||||
detail: 'Auto Promotion Details',
|
||||
pipelineHistory: 'PIPELINE HISTORY',
|
||||
factoryTaskSelectPlaceholder: 'Select promotion task',
|
||||
configured: 'Configured',
|
||||
editConfig: 'Edit auto promotion configuration',
|
||||
@@ -1310,6 +1327,7 @@ colTitle: 'Template title',
|
||||
taskLogState2: 'Failed',
|
||||
taskLogState3: 'Bounced',
|
||||
taskLogState4: 'Cancelled',
|
||||
logColIndex: 'No.',
|
||||
logColExpert: 'Expert',
|
||||
logColSendTime: 'Sent at',
|
||||
logColPreparedAt: 'Prepared at',
|
||||
|
||||
@@ -45,6 +45,13 @@ const zh = {
|
||||
status: '状态',
|
||||
delete: '删除',
|
||||
deleteInfo: '您确定要删除该期刊分期吗?',
|
||||
plagiarismNotChecked: '未检测',
|
||||
plagiarismChecking: '正在检测…',
|
||||
plagiarismRecheck: '重新查重',
|
||||
plagiarismCheckFailed: '查重任务启动失败。',
|
||||
plagiarismStatusFailed: '获取查重状态失败。',
|
||||
plagiarismNoReportUrl: '报告链接暂不可用。',
|
||||
plagiarismReportDetailFailed: '获取稿件详情失败,请稍后重试。',
|
||||
},
|
||||
menu: {
|
||||
main: '个人中心',
|
||||
@@ -639,6 +646,15 @@ const zh = {
|
||||
printBtn: '打印',
|
||||
previewNotSupported: '该文件格式无法在线预览',
|
||||
downloadToView: '下载到本地查看',
|
||||
registerAuthorBtn: '创建作者账号',
|
||||
registerAuthorConfirm:
|
||||
'将使用推广后台「添加用户」接口创建账号:登录名「{account}」,显示名「{realname}」,邮箱「{email}」,初始密码 123456qwe(无需验证码)。是否继续?',
|
||||
registerAuthorSuccess: '作者账号已创建。',
|
||||
registerAuthorFail: '创建失败,请稍后重试或到用户管理中手动添加。',
|
||||
registerAuthorExistsEmail: '该邮箱已被注册。',
|
||||
registerAuthorExistsAccount: '该登录名已被占用,请人工处理或修改发件人显示名后重试。',
|
||||
registerAuthorNeedEmail: '缺少发件人邮箱,无法创建账号。',
|
||||
registerAuthorNoQq: '本站不支持 QQ 邮箱作为作者账号,请在用户管理中手动添加。',
|
||||
},
|
||||
crawlerKeywords: {
|
||||
pageTitle: '关键词配置',
|
||||
@@ -984,8 +1000,8 @@ const zh = {
|
||||
step3: '参考',
|
||||
step: 'step',
|
||||
Information: 'Fill in information',
|
||||
|
||||
|
||||
startPreAccept: '开始预接收流程',
|
||||
startPreAcceptWithPayment: '开始预接收流程并完成支付',
|
||||
},
|
||||
Formula:{
|
||||
FormulaTemplate:'公式模版'
|
||||
@@ -1228,6 +1244,7 @@ const zh = {
|
||||
,
|
||||
autoPromotionLogs: {
|
||||
detail: '自动推广详情',
|
||||
pipelineHistory: '流水线历史',
|
||||
factoryTaskSelectPlaceholder: '选择推广任务',
|
||||
configured: '已配置',
|
||||
editConfig: '修改期刊自动推广配置',
|
||||
@@ -1295,6 +1312,7 @@ const zh = {
|
||||
taskLogState2: '失败',
|
||||
taskLogState3: '退信',
|
||||
taskLogState4: '取消',
|
||||
logColIndex: '序号',
|
||||
logColExpert: '专家信息',
|
||||
logColSendTime: '发送时间',
|
||||
logColPreparedAt: '预处理完成时间',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -107,11 +107,11 @@
|
||||
</span>
|
||||
|
||||
|
||||
<span v-if="item.state == 6" style="text-decoration: none;margin-left: 20px;">
|
||||
<span v-if="item.state == 6 && preAcceptButtonReady(item)" style="text-decoration: none;margin-left: 20px;">
|
||||
|
||||
<span @click="goPre_ingested(item.article_id)" class="preButton">
|
||||
<!-- <el-badge is-dot class="item" > -->
|
||||
<i class="el-icon-bank-card"></i>Start the pre-acceptance process and complete your payment
|
||||
<i class="el-icon-bank-card"></i>{{ preAcceptEntryButtonText(item) }}
|
||||
<!-- </el-badge> -->
|
||||
</span>
|
||||
</span>
|
||||
@@ -539,6 +539,61 @@
|
||||
this.getdate();
|
||||
},
|
||||
methods: {
|
||||
/** 与 Complete_profile.vue getDetail 中订单/支付展示逻辑一致:preOrderDetail → is_buy、期刊费、折后费、isFree */
|
||||
_feeNum(v) {
|
||||
if (v == null || v === '') return 0;
|
||||
const n = Number(String(v).replace(/,/g, ''));
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
},
|
||||
computePreacceptShortButton(articleInfo, journalInfo) {
|
||||
if (!articleInfo || !journalInfo) return false;
|
||||
const journalFee = journalInfo.fee;
|
||||
const articleFee = articleInfo.fee;
|
||||
const isBuy = Number(articleInfo.is_buy) === 1;
|
||||
const tableFee = journalFee ? this._feeNum(articleFee) : 0;
|
||||
const isFree = isBuy && tableFee === 0;
|
||||
const noJournalFee = !journalFee || this._feeNum(journalFee) === 0 || String(journalFee) === '0.00';
|
||||
// Complete_profile: active=1 当已付,或 未付但期刊无 APC
|
||||
if (isFree) return true;
|
||||
if (isBuy) return true;
|
||||
if (!isBuy && noJournalFee) return true;
|
||||
return false;
|
||||
},
|
||||
preAcceptButtonReady(item) {
|
||||
const p = item._preacceptPay;
|
||||
return !!(p && p.loading === false);
|
||||
},
|
||||
async hydratePreacceptPaymentForList(rows) {
|
||||
if (!rows || !rows.length) return;
|
||||
const targets = rows.filter((r) => Number(r.state) === 6);
|
||||
await Promise.all(
|
||||
targets.map(async (row) => {
|
||||
try {
|
||||
const res = await this.$api.post('api/Order/preOrderDetail', {
|
||||
article_id: row.article_id
|
||||
});
|
||||
if (res && Number(res.code) === 0) {
|
||||
const article = res.data.article_detail || {};
|
||||
const journal = res.data.journal_detail || {};
|
||||
const shortButton = this.computePreacceptShortButton(article, journal);
|
||||
this.$set(row, '_preacceptPay', { loading: false, shortButton });
|
||||
} else {
|
||||
this.$set(row, '_preacceptPay', { loading: false, shortButton: false });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$set(row, '_preacceptPay', { loading: false, shortButton: false });
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
preAcceptEntryButtonText(item) {
|
||||
const pay = item._preacceptPay;
|
||||
if (!pay || pay.loading) return '';
|
||||
return pay.shortButton
|
||||
? this.$t('PreAccept.startPreAccept')
|
||||
: this.$t('PreAccept.startPreAcceptWithPayment');
|
||||
},
|
||||
formatToHtml(val) {
|
||||
|
||||
if (!val) return '';
|
||||
@@ -612,8 +667,12 @@ return processedText;
|
||||
1 + '-';
|
||||
let D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
|
||||
res.data[i].ctime = Y + M + D;
|
||||
if (Number(res.data[i].state) === 6) {
|
||||
this.$set(res.data[i], '_preacceptPay', { loading: true, shortButton: false });
|
||||
}
|
||||
}
|
||||
this.tableData = res.data
|
||||
this.tableData = res.data;
|
||||
this.hydratePreacceptPaymentForList(res.data);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,19 +19,46 @@
|
||||
<el-select
|
||||
v-if="config.initialized && selectedJournalId"
|
||||
v-model="headerPromotionFactoryId"
|
||||
class="header-factory-task-select"
|
||||
class="header-factory-task-select custom-pipeline-select"
|
||||
size="small"
|
||||
filterable
|
||||
:loading="factoryTasksHeaderLoading"
|
||||
:placeholder="$t('autoPromotionLogs.factoryTaskSelectPlaceholder')"
|
||||
@change="onHeaderFactoryTaskChange"
|
||||
popper-class="pipeline-popper"
|
||||
>
|
||||
<el-option
|
||||
v-for="opt in factoryTaskOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
/>
|
||||
|
||||
|
||||
<el-option-group >
|
||||
|
||||
<el-option
|
||||
v-for="opt in factoryTaskOptions"
|
||||
:key="opt.value"
|
||||
:label="opt.label"
|
||||
:value="opt.value"
|
||||
class="pipeline-option"
|
||||
>
|
||||
<div class="option-content">
|
||||
<div class="row-top">
|
||||
<span class="task-title">{{ mapFactoryTaskTypeLabel(opt.task.type) }}</span>
|
||||
<el-tag :type="getStatusType(opt.task.start_promotion)" size="mini" effect="plain" class="status-tag">
|
||||
{{ opt.task.start_promotion==1 ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
|
||||
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="row-bottom">
|
||||
<span class="meta-item database">{{ mapFactoryExpertTypeLabel(opt.task.expert_type) }}</span>
|
||||
<template v-if="opt.task.expert_type==5">
|
||||
<span class="separator">•</span>
|
||||
<span class="meta-item region">{{ opt.task.country_scope_label }}</span>
|
||||
</template>
|
||||
|
||||
<span class="separator">•</span>
|
||||
<span class="meta-item time">{{ opt.task.ctime_text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-option-group>
|
||||
</el-select>
|
||||
<el-tag
|
||||
v-if="config.initialized && selectedJournalId && headerFactoryTaskRunning !== null"
|
||||
@@ -45,19 +72,10 @@
|
||||
{{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
|
||||
</el-tag>
|
||||
|
||||
<!-- <template v-if="config.initialized"> -->
|
||||
<!-- <el-tag type="success" size="small" effect="plain" style="margin-left: 10px">
|
||||
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.configured') }}
|
||||
</el-tag> -->
|
||||
<el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
|
||||
<i class="el-icon-edit"></i>
|
||||
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
|
||||
</el-button>
|
||||
<!-- </template> -->
|
||||
|
||||
<!-- <el-tag v-else type="info" size="small" effect="plain" style="margin-left: 10px">
|
||||
<i class="el-icon-info"></i> {{ $t('autoPromotionLogs.notConfigured') }}
|
||||
</el-tag> -->
|
||||
<el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
|
||||
<i class="el-icon-edit"></i>
|
||||
{{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-if="config.initialized && selectedJournalId && headerPromotionFactoryId" class="right">
|
||||
<el-button
|
||||
@@ -100,8 +118,6 @@
|
||||
|
||||
<div v-else class="manage-mode">
|
||||
<el-card shadow="never" class="list-card">
|
||||
|
||||
|
||||
<div class="filter-header-row">
|
||||
<div class="tmr-capsule-group">
|
||||
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
|
||||
@@ -135,7 +151,9 @@
|
||||
<div class="task-column">
|
||||
<div class="task-name">{{ scope.row.task_name || '-' }}</div>
|
||||
<div class="task-id-tags">
|
||||
<span class="id-tag">{{ $t('autoPromotionLogs.templateIdLabel') }}: {{ scope.row.template_id }}</span>
|
||||
<span class="id-tag"
|
||||
>{{ $t('autoPromotionLogs.templateIdLabel') }}: {{ scope.row.template_id }}</span
|
||||
>
|
||||
<span class="id-tag">{{ $t('autoPromotionLogs.styleIdLabel') }}: {{ scope.row.style_id }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,7 +242,11 @@
|
||||
:icon="String(scope.row.state) === '5' ? 'el-icon-edit-outline' : 'el-icon-view'"
|
||||
@click="previewRow(scope.row)"
|
||||
>
|
||||
{{ String(scope.row.state) === '5' ? $t('autoPromotionLogs.editAction') : $t('autoPromotionLogs.previewAction') }}
|
||||
{{
|
||||
String(scope.row.state) === '5'
|
||||
? $t('autoPromotionLogs.editAction')
|
||||
: $t('autoPromotionLogs.previewAction')
|
||||
}}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
@@ -427,6 +449,36 @@ export default {
|
||||
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
|
||||
if (opt && typeof opt.running === 'boolean') return opt.running;
|
||||
return null;
|
||||
},
|
||||
/**
|
||||
* 顶部 select 前缀展示用:与 headerPromotionFactoryId 对应的 option(含 task)。
|
||||
* URL 回退插入的占位项可能没有 task,这里统一成可安全渲染的对象。
|
||||
*/
|
||||
currentSelectedTask() {
|
||||
const emptyTask = { type: '', ctime_text: '' };
|
||||
const id = String(this.headerPromotionFactoryId || '').trim();
|
||||
if (!id || !Array.isArray(this.factoryTaskOptions) || !this.factoryTaskOptions.length) {
|
||||
return { value: '', label: '', running: false, task: { ...emptyTask } };
|
||||
}
|
||||
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
|
||||
if (!opt) {
|
||||
return { value: id, label: id, running: false, task: { ...emptyTask } };
|
||||
}
|
||||
const raw = opt.task && typeof opt.task === 'object' ? opt.task : {};
|
||||
const ctimeText =
|
||||
raw.ctime_text != null && String(raw.ctime_text).trim() !== ''
|
||||
? String(raw.ctime_text).trim()
|
||||
: this.formatFactoryHeaderTaskCreateTime(raw);
|
||||
return {
|
||||
value: opt.value,
|
||||
label: opt.label,
|
||||
running: typeof opt.running === 'boolean' ? opt.running : this.isFactoryHeaderTaskRunning(raw),
|
||||
task: {
|
||||
...raw,
|
||||
type: raw.type != null ? String(raw.type) : '',
|
||||
ctime_text: ctimeText || ''
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -445,12 +497,33 @@ export default {
|
||||
this.initPage();
|
||||
},
|
||||
methods: {
|
||||
mapFactoryTaskTypeLabel(type) {
|
||||
const t = String(type || '');
|
||||
if (t === '1') return this.$t('autoPromotion.factoryScenarioSolicit');
|
||||
if (t === '2') return this.$t('autoPromotion.factoryScenarioPromoteCitation');
|
||||
if (t === '3') return this.$t('autoPromotion.factoryScenarioGeneralThanks');
|
||||
if (t === '4') return this.$t('autoPromotion.autoSolicit');
|
||||
return this.$t('autoPromotion.autoSolicit');
|
||||
},
|
||||
mapFactoryExpertTypeLabel(expertType) {
|
||||
const t = String(expertType || '').trim();
|
||||
if (t === '1') return this.$t('autoPromotion.factoryExpertChief');
|
||||
if (t === '2') return this.$t('autoPromotion.factoryExpertBoard');
|
||||
if (t === '3') return this.$t('autoPromotion.factoryExpertYoungBoard');
|
||||
if (t === '4') return this.$t('autoPromotion.factoryExpertAuthor');
|
||||
if (t === '5') return this.$t('autoPromotion.factoryExpertDb');
|
||||
return '-';
|
||||
},
|
||||
getStatusType(status) {
|
||||
|
||||
return status==1?'success':'info';
|
||||
},
|
||||
handleTabClick(tab) {
|
||||
// tab.name 对应的就是原来的 value ("0", "1" 等)
|
||||
// 注意:el-tabs 的 v-model 绑定的是字符串
|
||||
this.query.state = tab.name;
|
||||
this.handleStateChange(); // 触发你原有的搜索逻辑
|
||||
},
|
||||
// tab.name 对应的就是原来的 value ("0", "1" 等)
|
||||
// 注意:el-tabs 的 v-model 绑定的是字符串
|
||||
this.query.state = tab.name;
|
||||
this.handleStateChange(); // 触发你原有的搜索逻辑
|
||||
},
|
||||
getPercent(row) {
|
||||
if (!row.total_count || row.total_count === 0) return 0;
|
||||
// 计算已发送占比
|
||||
@@ -554,9 +627,7 @@ export default {
|
||||
this.hidePage = false;
|
||||
var journal_id = (this.$route.query && this.$route.query.journal_id) || '';
|
||||
var pfid =
|
||||
(this.$route.query && this.$route.query.promotion_factory_id) ||
|
||||
(this.$route.query && this.$route.query.taskId) ||
|
||||
'';
|
||||
(this.$route.query && this.$route.query.promotion_factory_id) || (this.$route.query && this.$route.query.taskId) || '';
|
||||
this.routePromotionFactoryId = String(pfid || '');
|
||||
this.headerPromotionFactoryId = this.routePromotionFactoryId;
|
||||
this.selectedJournalId = String(journal_id);
|
||||
@@ -662,7 +733,7 @@ export default {
|
||||
}
|
||||
if (!dt || isNaN(dt.getTime())) return String(raw).trim();
|
||||
const pad = (v) => String(v).padStart(2, '0');
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())} ${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
|
||||
return `${dt.getFullYear()}-${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`;
|
||||
},
|
||||
isFactoryHeaderTaskRunning(task) {
|
||||
if (!task || typeof task !== 'object') return false;
|
||||
@@ -676,9 +747,12 @@ export default {
|
||||
},
|
||||
/** 下拉仅展示「类型 - 创建日期」,运行状态单独用 el-tag */
|
||||
buildFactoryHeaderOptionMainLabel(task, pidFallback) {
|
||||
console.log("🚀 ~ buildFactoryHeaderOptionMainLabel ~ task:", task);
|
||||
|
||||
const typePart = this.getFactoryHeaderTaskTypeLabel(task) || String(pidFallback || '').trim() || '—';
|
||||
const expertTypePart = this.mapFactoryExpertTypeLabel(task.expert_type);
|
||||
const datePart = this.formatFactoryHeaderTaskCreateTime(task);
|
||||
return datePart ? `${typePart} - ${datePart}` : typePart;
|
||||
return datePart ? `${typePart} | ${expertTypePart}${task.expert_type==5 ? ` | ${task.country_scope_label} ` :''} | ${datePart}` : typePart;
|
||||
},
|
||||
replacePromotionFactoryIdInUrl(promotionFactoryId) {
|
||||
try {
|
||||
@@ -704,18 +778,20 @@ export default {
|
||||
});
|
||||
const payload = (res && res.data) || {};
|
||||
const list = this.findArray(payload) || this.findArray(res) || [];
|
||||
let opts = (Array.isArray(list) ? list : []).map((task, idx) => {
|
||||
const pid =
|
||||
task && task.promotion_factory_id != null
|
||||
? String(task.promotion_factory_id)
|
||||
: task && task.id != null
|
||||
? String(task.id)
|
||||
: '';
|
||||
if (!pid) return null;
|
||||
const label = this.buildFactoryHeaderOptionMainLabel(task, pid);
|
||||
const running = this.isFactoryHeaderTaskRunning(task);
|
||||
return { value: pid, label, running };
|
||||
}).filter(Boolean);
|
||||
let opts = (Array.isArray(list) ? list : [])
|
||||
.map((task, idx) => {
|
||||
const pid =
|
||||
task && task.promotion_factory_id != null
|
||||
? String(task.promotion_factory_id)
|
||||
: task && task.id != null
|
||||
? String(task.id)
|
||||
: '';
|
||||
if (!pid) return null;
|
||||
const label = this.buildFactoryHeaderOptionMainLabel(task, pid);
|
||||
const running = this.isFactoryHeaderTaskRunning(task);
|
||||
return { value: pid, label, running,task:task }
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
let cur = String(this.routePromotionFactoryId || this.headerPromotionFactoryId || '').trim();
|
||||
const ids = new Set(opts.map((o) => o.value));
|
||||
@@ -799,10 +875,14 @@ export default {
|
||||
selectedPayload.country_fetch_ids != null
|
||||
? selectedPayload.country_fetch_ids
|
||||
: selectedPayload.country_ids != null
|
||||
? selectedPayload.country_ids
|
||||
: '';
|
||||
? selectedPayload.country_ids
|
||||
: '';
|
||||
if (typeof raw === 'string' && raw.trim()) {
|
||||
return raw.split(',').map((s) => s.trim()).filter(Boolean).map(String);
|
||||
return raw
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
.map(String);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
@@ -825,7 +905,7 @@ export default {
|
||||
let availableArr = this.findArray(availablePayload);
|
||||
if (!availableArr) availableArr = Array.isArray(availablePayload) ? availablePayload : [];
|
||||
this.availableFields = availableArr.map((item, idx) => {
|
||||
const id = item.expert_fetch_id || item.fetch_id || item.id || item.field_id || (idx + 1);
|
||||
const id = item.expert_fetch_id || item.fetch_id || item.id || item.field_id || idx + 1;
|
||||
const label = item.field || item.title || item.name || item.label || String(id);
|
||||
return { id: String(id), label };
|
||||
});
|
||||
@@ -898,10 +978,10 @@ export default {
|
||||
matched.promotion_factory_id != null
|
||||
? String(matched.promotion_factory_id)
|
||||
: matched.id != null
|
||||
? String(matched.id)
|
||||
: matched.task_id != null
|
||||
? String(matched.task_id)
|
||||
: '';
|
||||
? String(matched.id)
|
||||
: matched.task_id != null
|
||||
? String(matched.task_id)
|
||||
: '';
|
||||
}
|
||||
}
|
||||
// promotion_factory/getDetail 必须使用地址栏 promotion_factory_id,避免列表首行 id 与路由不一致
|
||||
@@ -1048,11 +1128,7 @@ export default {
|
||||
const runAt = item.run_at || item.run_time || item.plan_time || item.execute_time || item.send_date || '';
|
||||
const state = String(item.state != null ? item.state : '');
|
||||
const promotionFactoryId =
|
||||
item.promotion_factory_id != null
|
||||
? item.promotion_factory_id
|
||||
: item.id != null
|
||||
? item.id
|
||||
: item.task_id;
|
||||
item.promotion_factory_id != null ? item.promotion_factory_id : item.id != null ? item.id : item.task_id;
|
||||
return {
|
||||
id: item.id || item.task_id || `task_${idx + 1}`,
|
||||
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
|
||||
@@ -1206,16 +1282,16 @@ export default {
|
||||
return Object.prototype.hasOwnProperty.call(stateTextMap, key) ? stateTextMap[key] : '-';
|
||||
},
|
||||
getTaskStatusClass(state) {
|
||||
const stateClassMap = {
|
||||
0: 'status-draft', // Draft - 浅灰色
|
||||
5: 'status-preparing', // Preparing - 橘色
|
||||
1: 'status-running', // Running - 深蓝色
|
||||
2: 'status-paused', // Paused - 深灰色
|
||||
3: 'status-completed', // Completed - 绿色
|
||||
4: 'status-cancelled' // Cancelled - 浅红色
|
||||
};
|
||||
return stateClassMap[Number(state)] || '';
|
||||
}
|
||||
const stateClassMap = {
|
||||
0: 'status-draft', // Draft - 浅灰色
|
||||
5: 'status-preparing', // Preparing - 橘色
|
||||
1: 'status-running', // Running - 深蓝色
|
||||
2: 'status-paused', // Paused - 深灰色
|
||||
3: 'status-completed', // Completed - 绿色
|
||||
4: 'status-cancelled' // Cancelled - 浅红色
|
||||
};
|
||||
return stateClassMap[Number(state)] || '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1229,11 +1305,13 @@ export default {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.config-bar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.config-bar .left {
|
||||
width: calc(100% - 100px);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
@@ -1758,96 +1836,96 @@ export default {
|
||||
}
|
||||
|
||||
.filter-header-row {
|
||||
display: flex;
|
||||
align-items: center; /* 垂直居中对齐 */
|
||||
gap: 16px; /* 胶囊和Search按钮之间的间距 */
|
||||
margin-bottom: 16px;
|
||||
background-color: transparent; /* 容器透明,不厚重 */
|
||||
display: flex;
|
||||
align-items: center; /* 垂直居中对齐 */
|
||||
gap: 16px; /* 胶囊和Search按钮之间的间距 */
|
||||
margin-bottom: 16px;
|
||||
background-color: transparent; /* 容器透明,不厚重 */
|
||||
}
|
||||
|
||||
/* 2. 彻底重置 el-tabs 的原生样式 (最丑的地方) */
|
||||
.tmr-capsule-group {
|
||||
flex: 1; /* 占据左侧空间 */
|
||||
flex: 1; /* 占据左侧空间 */
|
||||
}
|
||||
|
||||
/* 强制隐藏默认灰色横线和卡片灰边 */
|
||||
.tmr-capsule-group .el-tabs__header {
|
||||
margin: 0 !important;
|
||||
border-bottom: none !important;
|
||||
margin: 0 !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.tmr-capsule-group .el-tabs__nav {
|
||||
border: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
/* 3. 重新定义每个 Tab 的样式 (让其变成按钮) */
|
||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
|
||||
height: 32px !important; /* 紧凑高度 */
|
||||
line-height: 32px !important;
|
||||
font-size: 13px; /* 紧凑字体 */
|
||||
border: none !important; /* 彻底隐藏原生卡片边框 */
|
||||
background-color: transparent; /* 默认状态下透明背景 */
|
||||
color: #515a6e; /* 默认状态深灰文字,更专业 */
|
||||
transition: all 0.2s ease-in-out;
|
||||
padding: 0 16px !important; /* 适当内边距 */
|
||||
margin-right: 8px; /* 每个 Tab 之间的间距 */
|
||||
border-radius: 6px !important; /* 先统一圆角 */
|
||||
overflow: visible; /* 确保选中的阴影显示完全 */
|
||||
height: 32px !important; /* 紧凑高度 */
|
||||
line-height: 32px !important;
|
||||
font-size: 13px; /* 紧凑字体 */
|
||||
border: none !important; /* 彻底隐藏原生卡片边框 */
|
||||
background-color: transparent; /* 默认状态下透明背景 */
|
||||
color: #515a6e; /* 默认状态深灰文字,更专业 */
|
||||
transition: all 0.2s ease-in-out;
|
||||
padding: 0 16px !important; /* 适当内边距 */
|
||||
margin-right: 8px; /* 每个 Tab 之间的间距 */
|
||||
border-radius: 6px !important; /* 先统一圆角 */
|
||||
overflow: visible; /* 确保选中的阴影显示完全 */
|
||||
}
|
||||
|
||||
/* 首尾 Tab 的圆角处理 (形成整体感) */
|
||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:first-child {
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
border-top-left-radius: 6px;
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
margin-right: 0;
|
||||
border-top-right-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
/* 中间 Tab 的处理 (去掉左右圆角,紧凑对齐) */
|
||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:first-child):not(:last-child) {
|
||||
border-radius: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
/* 4. **选中效果 (Active) - 胶囊浮动核心** */
|
||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
|
||||
background-color: #ffffff !important; /* 白色底色 */
|
||||
color: #409eff !important; /* 主题蓝色文字 */
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; /* **关键:增加轻微阴影,浮动感** */
|
||||
border-radius: 6px; /* 选中时恢复圆角 */
|
||||
position: relative;
|
||||
z-index: 2; /* 确保选中态在最前面,不被覆盖线破坏 */
|
||||
background-color: #ffffff !important; /* 白色底色 */
|
||||
color: #409eff !important; /* 主题蓝色文字 */
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; /* **关键:增加轻微阴影,浮动感** */
|
||||
border-radius: 6px; /* 选中时恢复圆角 */
|
||||
position: relative;
|
||||
z-index: 2; /* 确保选中态在最前面,不被覆盖线破坏 */
|
||||
}
|
||||
|
||||
/* 5. 处理 Tab 之间的断层 (像素级微调) */
|
||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 首尾 Tab 之间的像素级处理 */
|
||||
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:last-child)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -5px; /* 让 Tab 之间紧密无缝 */
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: transparent; /* 移除灰线,不丑 */
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -5px; /* 让 Tab 之间紧密无缝 */
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background: transparent; /* 移除灰线,不丑 */
|
||||
}
|
||||
|
||||
/* 6. 搜索按钮对齐微调 */
|
||||
.filter-actions .el-button {
|
||||
height: 32px; /* 与 Tab 高度一致 */
|
||||
padding: 0 16px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
height: 32px; /* 与 Tab 高度一致 */
|
||||
padding: 0 16px;
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-actions {
|
||||
margin-left: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
/* 基础 Badge 样式 */
|
||||
.status-badge {
|
||||
@@ -1894,21 +1972,25 @@ export default {
|
||||
background-color: #f1f5f9;
|
||||
color: #64748b;
|
||||
}
|
||||
.status-draft::before { background-color: #94a3b8; }
|
||||
.status-draft::before {
|
||||
background-color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 5: Preparing - 橘色 */
|
||||
.status-preparing {
|
||||
background-color: #fff7ed;
|
||||
color: #c2410c;
|
||||
}
|
||||
.status-preparing::before { background-color: #f97316; }
|
||||
.status-preparing::before {
|
||||
background-color: #f97316;
|
||||
}
|
||||
|
||||
/* 1: Running - 深蓝色 */
|
||||
.status-running {
|
||||
background-color: #eff6ff;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
.status-running::before {
|
||||
.status-running::before {
|
||||
background-color: #3b82f6;
|
||||
box-shadow: 0 0 4px rgba(59, 130, 246, 0.5); /* 模拟运行中的发光感 */
|
||||
}
|
||||
@@ -1919,19 +2001,104 @@ export default {
|
||||
color: #334155;
|
||||
border: 1px solid #e2e8f0; /* 停用状态加个微边框增加分量感 */
|
||||
}
|
||||
.status-paused::before { background-color: #475569; }
|
||||
.status-paused::before {
|
||||
background-color: #475569;
|
||||
}
|
||||
|
||||
/* 3: Completed - 绿色 */
|
||||
.status-completed {
|
||||
background-color: #f0fdf4;
|
||||
color: #15803d;
|
||||
}
|
||||
.status-completed::before { background-color: #22c55e; }
|
||||
.status-completed::before {
|
||||
background-color: #22c55e;
|
||||
}
|
||||
|
||||
/* 4: Cancelled - 浅红色 */
|
||||
.status-cancelled {
|
||||
background-color: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
.status-cancelled::before { background-color: #ef4444; }
|
||||
.status-cancelled::before {
|
||||
background-color: #ef4444;
|
||||
}
|
||||
/* 基础 Select 宽度 */
|
||||
.custom-pipeline-select {
|
||||
width: 580px;
|
||||
}
|
||||
|
||||
/* 分组标题(el-option-group,避免在 ul 内放 div) */
|
||||
.pipeline-popper .el-select-group__title {
|
||||
padding: 10px 20px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
color: #909399;
|
||||
letter-spacing: 1px;
|
||||
border-bottom: 1px solid #f0f2f5;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 单个选项容器 */
|
||||
.pipeline-popper .el-select-dropdown__item {
|
||||
height: auto; /* 允许高度自适应 */
|
||||
padding: 12px 20px;
|
||||
line-height: normal;
|
||||
border-left: 3px solid transparent; /* 预留边框位置防止抖动 */
|
||||
}
|
||||
|
||||
/* 选中状态 */
|
||||
.pipeline-popper .el-select-dropdown__item.selected {
|
||||
background-color: #f5f7fa;
|
||||
border-left: 3px solid #409eff; /* 选中时的左侧蓝条 */
|
||||
color: #606266; /* 覆盖 Element 默认高亮色 */
|
||||
}
|
||||
|
||||
/* 选项内容布局 */
|
||||
.pipeline-popper .option-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
/* 第一行:标题 + 状态标签 */
|
||||
.pipeline-popper .option-content .row-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pipeline-popper .option-content .row-top .task-title {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pipeline-popper .option-content .row-top .status-tag {
|
||||
border-radius: 12px;
|
||||
padding: 0 8px;
|
||||
height: 20px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
/* 第二行:元数据信息 */
|
||||
.pipeline-popper .option-content .row-bottom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 11px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.pipeline-popper .option-content .row-bottom .meta-item.database {
|
||||
color: #5856d6;
|
||||
font-weight: bold;
|
||||
/* text-transform: uppercase; */
|
||||
}
|
||||
|
||||
.pipeline-popper .option-content .row-bottom .separator {
|
||||
margin: 0 6px;
|
||||
color: #dcdfe6;
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@@ -59,20 +59,22 @@
|
||||
|
||||
<div class="drawer-body" v-loading="loading">
|
||||
<div class="list-header">
|
||||
<div class="col-index">{{ $t('autoPromotionLogs.logColIndex') }}</div>
|
||||
<div class="col-info">{{ $t('autoPromotionLogs.logColExpert') }}</div>
|
||||
<div class="col-send" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColSendTime') }}</div>
|
||||
<div class="col-prepared" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColPreparedAt') }}</div>
|
||||
<div class="col-status">{{ $t('autoPromotionLogs.logColStatus') }}</div>
|
||||
<div class="col-action">{{ $t('autoPromotionLogs.logColAction') }}</div>
|
||||
<div class="col-action"></div>
|
||||
</div>
|
||||
|
||||
<div class="list-wrapper">
|
||||
<div
|
||||
v-for="item in fullData"
|
||||
v-for="(item, rowIndex) in fullData"
|
||||
:key="item.id"
|
||||
class="log-row"
|
||||
:class="{ 'row-error': item.isErrorRow }"
|
||||
>
|
||||
<div class="col-index">{{ (currentPage - 1) * pageSize + rowIndex + 1 }}</div>
|
||||
<div class="col-info">
|
||||
<div class="expert-main">
|
||||
<span class="name">{{ item.expertName }}</span>
|
||||
@@ -782,6 +784,14 @@ export default {
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.col-index {
|
||||
flex: 0 0 52px;
|
||||
width: 52px;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.col-info {
|
||||
flex: 1.8;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,18 @@
|
||||
<h1 class="mail-subject-top">{{ $t('mailboxCollect.subject') }}:{{ mailData.subject }}</h1>
|
||||
</div>
|
||||
<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-close action-icon" @click="$emit('close')"></i>
|
||||
</div>
|
||||
@@ -70,11 +82,23 @@
|
||||
<el-link type="primary" :underline="false" @click="scrollToAttachments" class="jump-link">
|
||||
{{ $t('mailboxCollect.viewAttachments') }}
|
||||
</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 class="mail-body-content" v-html="mailData.content_html"></div>
|
||||
<div class="mail-body-content" v-html="mailBodyHtml"></div>
|
||||
|
||||
<div v-if="mailData.attachments && mailData.attachments.length" class="attachment-section">
|
||||
<div class="attachment-header">
|
||||
@@ -123,6 +147,7 @@
|
||||
|
||||
<script>
|
||||
import Common from '@/components/common/common';
|
||||
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
||||
import JSZip from 'jszip';
|
||||
import { saveAs } from 'file-saver';
|
||||
import FilePreviewDialog from './FilePreviewDialog.vue';
|
||||
@@ -151,17 +176,168 @@ export default {
|
||||
mediaUrl: Common.mediaUrl,
|
||||
isDetailExpanded: false,
|
||||
downloadingIndex: -1,
|
||||
packingAll: false
|
||||
packingAll: false,
|
||||
registerAuthorLoading: false
|
||||
};
|
||||
},
|
||||
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() {
|
||||
if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B';
|
||||
const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
|
||||
return this.formatFileSize(total);
|
||||
},
|
||||
/** 正文:兼容 content_html / body / html,纯文本时包一层 pre */
|
||||
mailBodyHtml() {
|
||||
const m = this.mailData || {};
|
||||
let raw = m.content_html || m.body_html || m.html || m.body || m.content || '';
|
||||
raw = normalizeEmailHtmlForInlineDisplay(raw);
|
||||
if (raw) return raw;
|
||||
const text = m.content_text;
|
||||
if (text != null && String(text).trim() !== '') {
|
||||
return `<pre class="mail-plain-pre">${this.escapeHtml(String(text))}</pre>`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
},
|
||||
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) {
|
||||
if (text == null) return '';
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
},
|
||||
scrollToAttachments() {
|
||||
// 1. 获取目标元素的 DOM
|
||||
const target = this.$el.querySelector('.attachment-section');
|
||||
@@ -362,6 +538,19 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
color: #606266;
|
||||
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 {
|
||||
color: #409eff;
|
||||
}
|
||||
@@ -441,6 +630,15 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
/* v-html 注入的正文无 scoped 属性,用 deep 命中内部的 pre */
|
||||
.mail-body-content >>> .mail-plain-pre {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 附件卡片 */
|
||||
.attachment-section {
|
||||
border-top: 1px solid #eee;
|
||||
@@ -635,7 +833,9 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
}
|
||||
.attachment-brief-bar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 12px;
|
||||
padding: 8px 0;
|
||||
font-size: 13px;
|
||||
color: #606266;
|
||||
@@ -651,7 +851,7 @@ const res = await this.$api.post('api/email_client/getAttachment', {
|
||||
margin-left: 4px;
|
||||
}
|
||||
.jump-link {
|
||||
margin-left: 15px;
|
||||
margin-left: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.attachment-section {
|
||||
|
||||
@@ -109,7 +109,11 @@
|
||||
<div class="mail-content-panel" v-if="selectedAccount" v-loading="detailLoading">
|
||||
<template v-if="activeMailId && !detailLoading">
|
||||
<div class="mail-page">
|
||||
<mail-detail v-if="detailMail" :mailData="detailMail" @close="closeDetail" />
|
||||
<mail-detail
|
||||
v-if="detailMail && String(detailMail.inbox_id || '') !== ''"
|
||||
:mailData="detailMail"
|
||||
@close="closeDetail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -168,6 +172,7 @@ const API = {
|
||||
getAllJournal: 'api/Article/getJournal'
|
||||
};
|
||||
import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
||||
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
@@ -293,8 +298,14 @@ import MailDetail from '../../components/page/components/email/MailDetail.vue';
|
||||
this.$api
|
||||
.post(API.getOneEmail, { j_email_id: jEmailId })
|
||||
.then((res) => {
|
||||
const email = res && res.data ? res.data.email : null;
|
||||
if (res && res.code === 0 && email) {
|
||||
const raw = res && res.data != null ? res.data : null;
|
||||
const email =
|
||||
raw && typeof raw === 'object' && raw.email && typeof raw.email === 'object'
|
||||
? raw.email
|
||||
: raw && typeof raw === 'object' && (raw.j_email_id != null || raw.smtp_user)
|
||||
? raw
|
||||
: null;
|
||||
if (res && Number(res.code) === 0 && email) {
|
||||
this.selectedAccount = email;
|
||||
this.fetchData();
|
||||
this.startInboxSse();
|
||||
@@ -489,28 +500,103 @@ fetchLatestSingleMail(jEmailId, journalId) {
|
||||
this.currentFolder = f;
|
||||
this.activeMailId = null;
|
||||
},
|
||||
selectMail(item,index) {
|
||||
/** 接口 code 可能是数字 0 或字符串 "0" */
|
||||
isEmailApiSuccess(res) {
|
||||
if (!res || res.code === undefined || res.code === null) return false;
|
||||
return Number(res.code) === 0;
|
||||
},
|
||||
escapeHtml(text) {
|
||||
if (text == null) return '';
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
},
|
||||
/** 解析 getEmailDetail 返回体,统一 content 字段;失败时用列表行兜底 */
|
||||
buildDetailMailFromResponse(res, item, inboxId) {
|
||||
const ok = this.isEmailApiSuccess(res);
|
||||
let d = res && res.data != null ? res.data : null;
|
||||
if (d && typeof d === 'string') {
|
||||
try {
|
||||
d = JSON.parse(d);
|
||||
} catch (e) {
|
||||
d = null;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(d) && d.length) {
|
||||
d = d[0];
|
||||
}
|
||||
if (ok && d && typeof d === 'object' && d.data && typeof d.data === 'object' && !d.content_html && !d.content_text) {
|
||||
d = { ...d, ...d.data };
|
||||
}
|
||||
if (ok && (!d || typeof d !== 'object') && res && (res.content_html != null || res.content_text != null || res.subject != null)) {
|
||||
d = { ...res };
|
||||
delete d.code;
|
||||
delete d.msg;
|
||||
if (Object.prototype.hasOwnProperty.call(d, 'data')) delete d.data;
|
||||
}
|
||||
const inboxKey = String(inboxId);
|
||||
const listSnippet = (item && item.content) || '';
|
||||
if (!ok || !d || typeof d !== 'object') {
|
||||
return {
|
||||
...item,
|
||||
subject: (item && item.subject) || '',
|
||||
from_name: (item && item.from_name) || '',
|
||||
from_email: (item && item.email) || '',
|
||||
email_date: item && item.email_date,
|
||||
content_html: listSnippet,
|
||||
inbox_id: inboxKey,
|
||||
attachments: [],
|
||||
has_attachment: (item && item.has_attachment) || 0
|
||||
};
|
||||
}
|
||||
const html = d.content_html || d.body_html || d.html || d.body || '';
|
||||
const text = d.content_text || '';
|
||||
let content_html =
|
||||
html ||
|
||||
(text ? `<pre class="mail-plain-pre">${this.escapeHtml(text)}</pre>` : '') ||
|
||||
listSnippet;
|
||||
if (content_html && typeof content_html === 'string' && /^<!DOCTYPE|^<\s*html[\s>]/i.test(content_html.trim())) {
|
||||
const normalized = normalizeEmailHtmlForInlineDisplay(content_html);
|
||||
if (normalized && normalized.trim()) content_html = normalized;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
...d,
|
||||
content_html,
|
||||
inbox_id: inboxKey,
|
||||
attachments: []
|
||||
};
|
||||
},
|
||||
selectMail(item, index) {
|
||||
this.activeMailId = item.id;
|
||||
this.detailLoading = true;
|
||||
this.detailMail = {};
|
||||
const inboxId = item.inbox_id || item.id;
|
||||
this.$api
|
||||
.post(API.getEmailDetail, { inbox_id: inboxId })
|
||||
.then((res) => {
|
||||
const d = res && res.data;
|
||||
if (res && res.code === 0 && d) {
|
||||
this.detailMail = { ...d, inbox_id: String(inboxId), attachments: [] };
|
||||
item.state = 0;
|
||||
this.displayList[index].is_read = d.is_read;
|
||||
if (Number(d.has_attachment) === 1) {
|
||||
this.fetchAttachments(String(inboxId));
|
||||
} else {
|
||||
this.detailLoading = false;
|
||||
try {
|
||||
const merged = this.buildDetailMailFromResponse(res, item, inboxId);
|
||||
this.detailMail = merged;
|
||||
if (item) item.state = 0;
|
||||
const row = this.displayList[index];
|
||||
if (row && merged && merged.is_read !== undefined && merged.is_read !== null) {
|
||||
this.$set(row, 'is_read', merged.is_read);
|
||||
}
|
||||
} else {
|
||||
this.detailLoading = false;
|
||||
if (Number(merged.has_attachment) === 1) {
|
||||
this.fetchAttachments(String(inboxId));
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.detailMail = this.buildDetailMailFromResponse(null, item, inboxId);
|
||||
}
|
||||
this.detailLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.detailMail = this.buildDetailMailFromResponse(null, item, inboxId);
|
||||
this.detailLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
26
src/utils/emailHtmlView.js
Normal file
26
src/utils/emailHtmlView.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 将完整 HTML 邮件转为适合 div[v-html] 的片段:
|
||||
* 取 body.innerHTML,并前置 head 内 <style>,避免版式与改版前不一致。
|
||||
*/
|
||||
export function normalizeEmailHtmlForInlineDisplay(html) {
|
||||
if (!html || typeof html !== 'string') return '';
|
||||
const t = html.trim();
|
||||
if (!/^<!DOCTYPE|^<\s*html[\s>]/i.test(t)) return html;
|
||||
try {
|
||||
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||
const body = doc && doc.body;
|
||||
if (!body) return html;
|
||||
const inner = body.innerHTML;
|
||||
if (!inner || !inner.trim()) return html;
|
||||
let headInject = '';
|
||||
const head = doc.head;
|
||||
if (head) {
|
||||
head.querySelectorAll('style').forEach((node) => {
|
||||
headInject += node.outerHTML;
|
||||
});
|
||||
}
|
||||
return headInject + inner;
|
||||
} catch (e) {
|
||||
return html;
|
||||
}
|
||||
}
|
||||
15
src/utils/ithenticateSimilarityStyle.js
Normal file
15
src/utils/ithenticateSimilarityStyle.js
Normal 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' };
|
||||
}
|
||||
Reference in New Issue
Block a user