7 Commits

Author SHA1 Message Date
77c0fac34d 提交 2026-05-14 13:11:54 +08:00
74a6c4b74b 支付信息提示 2026-05-14 11:29:17 +08:00
6050dd365d 作者端预接收按钮 2026-05-13 14:14:50 +08:00
f67d8d5600 邮件预览 2026-05-09 10:29:34 +08:00
67a4875b01 自动化详情页 任务显示 2026-05-08 13:53:37 +08:00
8e59702f0b 申请青年编委单独页面校验显示 2026-05-06 10:49:41 +08:00
ea1564018e 必填项 变成英文提示词 2026-05-06 09:52:32 +08:00
10 changed files with 550 additions and 153 deletions

View File

@@ -995,8 +995,8 @@ colTitle: 'Template title',
step3: 'References', step3: 'References',
step: 'step', step: 'step',
Information: 'Fill in information', Information: 'Fill in information',
startPreAccept: 'Start the pre-acceptance process',
startPreAcceptWithPayment: 'Start the pre-acceptance process and complete your payment',
}, },
Formula: { Formula: {
FormulaTemplate: 'Formula Template' FormulaTemplate: 'Formula Template'
@@ -1243,6 +1243,7 @@ colTitle: 'Template title',
, ,
autoPromotionLogs: { autoPromotionLogs: {
detail: 'Auto Promotion Details', detail: 'Auto Promotion Details',
pipelineHistory: 'PIPELINE HISTORY',
factoryTaskSelectPlaceholder: 'Select promotion task', factoryTaskSelectPlaceholder: 'Select promotion task',
configured: 'Configured', configured: 'Configured',
editConfig: 'Edit auto promotion configuration', editConfig: 'Edit auto promotion configuration',
@@ -1310,6 +1311,7 @@ colTitle: 'Template title',
taskLogState2: 'Failed', taskLogState2: 'Failed',
taskLogState3: 'Bounced', taskLogState3: 'Bounced',
taskLogState4: 'Cancelled', taskLogState4: 'Cancelled',
logColIndex: 'No.',
logColExpert: 'Expert', logColExpert: 'Expert',
logColSendTime: 'Sent at', logColSendTime: 'Sent at',
logColPreparedAt: 'Prepared at', logColPreparedAt: 'Prepared at',

View File

@@ -984,8 +984,8 @@ const zh = {
step3: '参考', step3: '参考',
step: 'step', step: 'step',
Information: 'Fill in information', Information: 'Fill in information',
startPreAccept: '开始预接收流程',
startPreAcceptWithPayment: '开始预接收流程并完成支付',
}, },
Formula:{ Formula:{
FormulaTemplate:'公式模版' FormulaTemplate:'公式模版'
@@ -1228,6 +1228,7 @@ const zh = {
, ,
autoPromotionLogs: { autoPromotionLogs: {
detail: '自动推广详情', detail: '自动推广详情',
pipelineHistory: '流水线历史',
factoryTaskSelectPlaceholder: '选择推广任务', factoryTaskSelectPlaceholder: '选择推广任务',
configured: '已配置', configured: '已配置',
editConfig: '修改期刊自动推广配置', editConfig: '修改期刊自动推广配置',
@@ -1295,6 +1296,7 @@ const zh = {
taskLogState2: '失败', taskLogState2: '失败',
taskLogState3: '退信', taskLogState3: '退信',
taskLogState4: '取消', taskLogState4: '取消',
logColIndex: '序号',
logColExpert: '专家信息', logColExpert: '专家信息',
logColSendTime: '发送时间', logColSendTime: '发送时间',
logColPreparedAt: '预处理完成时间', logColPreparedAt: '预处理完成时间',

View File

@@ -27,14 +27,13 @@
type="text" type="text"
v-model="formData.engName" v-model="formData.engName"
placeholder="" placeholder=""
required
autocomplete="off" autocomplete="off"
/> />
</div> </div>
<div class="form-group"> <div class="form-group">
<label><span class="required-star">*</span> Email (QQ mail is not allowed)</label> <label><span class="required-star">*</span> Email (QQ mail is not allowed)</label>
<input type="email" v-model="formData.email" placeholder="" required autocomplete="off" /> <input type="email" v-model="formData.email" placeholder="" autocomplete="off" />
</div> </div>
<div class="form-group"> <div class="form-group">
@@ -43,7 +42,6 @@
type="text" type="text"
v-model="formData.password" v-model="formData.password"
placeholder="" placeholder=""
required
autocomplete="off" autocomplete="off"
/> />
</div> </div>
@@ -317,7 +315,22 @@ export default {
return; return;
} }
const email = (this.formData.email || '').toLowerCase(); const engName = (this.formData.engName || '').trim();
if (!engName) {
this.alertError('Please enter your English name.');
return;
}
const emailRaw = (this.formData.email || '').trim();
if (!emailRaw) {
this.alertError('Please enter your email address.');
return;
}
const email = emailRaw.toLowerCase();
if (!/^[-._A-Za-z0-9]+@[A-Za-z0-9_-]+(\.[A-Za-z0-9_-]+)+$/.test(email)) {
this.alertError('Please enter a valid email address.');
return;
}
if (email.endsWith('@qq.com')) { if (email.endsWith('@qq.com')) {
this.alertError('Registration failed: QQ email addresses are not accepted.'); this.alertError('Registration failed: QQ email addresses are not accepted.');
return; return;
@@ -340,8 +353,8 @@ export default {
.post('api/Ucenter/submitApplyYboardForExpert', { .post('api/Ucenter/submitApplyYboardForExpert', {
journal_id: this.journalId, journal_id: this.journalId,
expert_id: this.expertId, expert_id: this.expertId,
name: this.formData.engName, name: engName,
email: this.formData.email, email: emailRaw,
cv: this.uploadedCvPath, cv: this.uploadedCvPath,
password: this.formData.password password: this.formData.password
}) })

View File

@@ -2462,6 +2462,7 @@ export default {
background: 'rgba(0, 0, 0, 0.7)' background: 'rgba(0, 0, 0, 0.7)'
}); });
this.$api.post('api/Article/changeRepetition', this.repeform).then((res) => { this.$api.post('api/Article/changeRepetition', this.repeform).then((res) => {
this.repebox = false;
load.close(); load.close();
this.$message.success('success'); this.$message.success('success');
this.initarticle(); this.initarticle();

View File

@@ -107,11 +107,11 @@
</span> </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"> <span @click="goPre_ingested(item.article_id)" class="preButton">
<!-- <el-badge is-dot class="item" > --> <!-- <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> --> <!-- </el-badge> -->
</span> </span>
</span> </span>
@@ -539,6 +539,61 @@
this.getdate(); this.getdate();
}, },
methods: { 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) { formatToHtml(val) {
if (!val) return ''; if (!val) return '';
@@ -612,8 +667,12 @@ return processedText;
1 + '-'; 1 + '-';
let D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate(); let D = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
res.data[i].ctime = Y + M + D; 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 => { .catch(err => {
console.log(err); console.log(err);

View File

@@ -19,19 +19,46 @@
<el-select <el-select
v-if="config.initialized && selectedJournalId" v-if="config.initialized && selectedJournalId"
v-model="headerPromotionFactoryId" v-model="headerPromotionFactoryId"
class="header-factory-task-select" class="header-factory-task-select custom-pipeline-select"
size="small" size="small"
filterable filterable
:loading="factoryTasksHeaderLoading" :loading="factoryTasksHeaderLoading"
:placeholder="$t('autoPromotionLogs.factoryTaskSelectPlaceholder')" :placeholder="$t('autoPromotionLogs.factoryTaskSelectPlaceholder')"
@change="onHeaderFactoryTaskChange" @change="onHeaderFactoryTaskChange"
popper-class="pipeline-popper"
> >
<el-option
v-for="opt in factoryTaskOptions"
:key="opt.value" <el-option-group >
:label="opt.label"
:value="opt.value" <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-select>
<el-tag <el-tag
v-if="config.initialized && selectedJournalId && headerFactoryTaskRunning !== null" v-if="config.initialized && selectedJournalId && headerFactoryTaskRunning !== null"
@@ -45,19 +72,10 @@
{{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }} {{ headerFactoryTaskRunning ? $t('autoPromotion.running') : $t('autoPromotion.stopped') }}
</el-tag> </el-tag>
<!-- <template v-if="config.initialized"> --> <el-button type="text" size="small" style="margin-left: 10px" @click="openFactoryTaskDialogFromLogs">
<!-- <el-tag type="success" size="small" effect="plain" style="margin-left: 10px"> <i class="el-icon-edit"></i>
<i class="el-icon-circle-check"></i> {{ $t('autoPromotionLogs.configured') }} {{ config.initialized ? $t('autoPromotionLogs.editConfig') : $t('autoPromotionLogs.startConfig') }}
</el-tag> --> </el-button>
<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> -->
</div> </div>
<div v-if="config.initialized && selectedJournalId && headerPromotionFactoryId" class="right"> <div v-if="config.initialized && selectedJournalId && headerPromotionFactoryId" class="right">
<el-button <el-button
@@ -100,8 +118,6 @@
<div v-else class="manage-mode"> <div v-else class="manage-mode">
<el-card shadow="never" class="list-card"> <el-card shadow="never" class="list-card">
<div class="filter-header-row"> <div class="filter-header-row">
<div class="tmr-capsule-group"> <div class="tmr-capsule-group">
<el-tabs v-model="query.state" type="card" @tab-click="handleTabClick"> <el-tabs v-model="query.state" type="card" @tab-click="handleTabClick">
@@ -135,7 +151,9 @@
<div class="task-column"> <div class="task-column">
<div class="task-name">{{ scope.row.task_name || '-' }}</div> <div class="task-name">{{ scope.row.task_name || '-' }}</div>
<div class="task-id-tags"> <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> <span class="id-tag">{{ $t('autoPromotionLogs.styleIdLabel') }}: {{ scope.row.style_id }}</span>
</div> </div>
</div> </div>
@@ -224,7 +242,11 @@
:icon="String(scope.row.state) === '5' ? 'el-icon-edit-outline' : 'el-icon-view'" :icon="String(scope.row.state) === '5' ? 'el-icon-edit-outline' : 'el-icon-view'"
@click="previewRow(scope.row)" @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> </el-button>
</div> </div>
</template> </template>
@@ -427,6 +449,36 @@ export default {
const opt = this.factoryTaskOptions.find((o) => String(o.value) === id); const opt = this.factoryTaskOptions.find((o) => String(o.value) === id);
if (opt && typeof opt.running === 'boolean') return opt.running; if (opt && typeof opt.running === 'boolean') return opt.running;
return null; 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: { watch: {
@@ -445,12 +497,33 @@ export default {
this.initPage(); this.initPage();
}, },
methods: { 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) { handleTabClick(tab) {
// tab.name 对应的就是原来的 value ("0", "1" 等) // tab.name 对应的就是原来的 value ("0", "1" 等)
// 注意el-tabs 的 v-model 绑定的是字符串 // 注意el-tabs 的 v-model 绑定的是字符串
this.query.state = tab.name; this.query.state = tab.name;
this.handleStateChange(); // 触发你原有的搜索逻辑 this.handleStateChange(); // 触发你原有的搜索逻辑
}, },
getPercent(row) { getPercent(row) {
if (!row.total_count || row.total_count === 0) return 0; if (!row.total_count || row.total_count === 0) return 0;
// 计算已发送占比 // 计算已发送占比
@@ -554,9 +627,7 @@ export default {
this.hidePage = false; this.hidePage = false;
var journal_id = (this.$route.query && this.$route.query.journal_id) || ''; var journal_id = (this.$route.query && this.$route.query.journal_id) || '';
var pfid = var pfid =
(this.$route.query && this.$route.query.promotion_factory_id) || (this.$route.query && this.$route.query.promotion_factory_id) || (this.$route.query && this.$route.query.taskId) || '';
(this.$route.query && this.$route.query.taskId) ||
'';
this.routePromotionFactoryId = String(pfid || ''); this.routePromotionFactoryId = String(pfid || '');
this.headerPromotionFactoryId = this.routePromotionFactoryId; this.headerPromotionFactoryId = this.routePromotionFactoryId;
this.selectedJournalId = String(journal_id); this.selectedJournalId = String(journal_id);
@@ -662,7 +733,7 @@ export default {
} }
if (!dt || isNaN(dt.getTime())) return String(raw).trim(); if (!dt || isNaN(dt.getTime())) return String(raw).trim();
const pad = (v) => String(v).padStart(2, '0'); 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) { isFactoryHeaderTaskRunning(task) {
if (!task || typeof task !== 'object') return false; if (!task || typeof task !== 'object') return false;
@@ -676,9 +747,12 @@ export default {
}, },
/** 下拉仅展示「类型 - 创建日期」,运行状态单独用 el-tag */ /** 下拉仅展示「类型 - 创建日期」,运行状态单独用 el-tag */
buildFactoryHeaderOptionMainLabel(task, pidFallback) { buildFactoryHeaderOptionMainLabel(task, pidFallback) {
console.log("🚀 ~ buildFactoryHeaderOptionMainLabel ~ task:", task);
const typePart = this.getFactoryHeaderTaskTypeLabel(task) || String(pidFallback || '').trim() || '—'; const typePart = this.getFactoryHeaderTaskTypeLabel(task) || String(pidFallback || '').trim() || '—';
const expertTypePart = this.mapFactoryExpertTypeLabel(task.expert_type);
const datePart = this.formatFactoryHeaderTaskCreateTime(task); 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) { replacePromotionFactoryIdInUrl(promotionFactoryId) {
try { try {
@@ -704,18 +778,20 @@ export default {
}); });
const payload = (res && res.data) || {}; const payload = (res && res.data) || {};
const list = this.findArray(payload) || this.findArray(res) || []; const list = this.findArray(payload) || this.findArray(res) || [];
let opts = (Array.isArray(list) ? list : []).map((task, idx) => { let opts = (Array.isArray(list) ? list : [])
const pid = .map((task, idx) => {
task && task.promotion_factory_id != null const pid =
? String(task.promotion_factory_id) task && task.promotion_factory_id != null
: task && task.id != null ? String(task.promotion_factory_id)
? String(task.id) : task && task.id != null
: ''; ? String(task.id)
if (!pid) return null; : '';
const label = this.buildFactoryHeaderOptionMainLabel(task, pid); if (!pid) return null;
const running = this.isFactoryHeaderTaskRunning(task); const label = this.buildFactoryHeaderOptionMainLabel(task, pid);
return { value: pid, label, running }; const running = this.isFactoryHeaderTaskRunning(task);
}).filter(Boolean); return { value: pid, label, running,task:task }
})
.filter(Boolean);
let cur = String(this.routePromotionFactoryId || this.headerPromotionFactoryId || '').trim(); let cur = String(this.routePromotionFactoryId || this.headerPromotionFactoryId || '').trim();
const ids = new Set(opts.map((o) => o.value)); const ids = new Set(opts.map((o) => o.value));
@@ -799,10 +875,14 @@ export default {
selectedPayload.country_fetch_ids != null selectedPayload.country_fetch_ids != null
? selectedPayload.country_fetch_ids ? selectedPayload.country_fetch_ids
: selectedPayload.country_ids != null : selectedPayload.country_ids != null
? selectedPayload.country_ids ? selectedPayload.country_ids
: ''; : '';
if (typeof raw === 'string' && raw.trim()) { 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 []; return [];
}, },
@@ -825,7 +905,7 @@ export default {
let availableArr = this.findArray(availablePayload); let availableArr = this.findArray(availablePayload);
if (!availableArr) availableArr = Array.isArray(availablePayload) ? availablePayload : []; if (!availableArr) availableArr = Array.isArray(availablePayload) ? availablePayload : [];
this.availableFields = availableArr.map((item, idx) => { 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); const label = item.field || item.title || item.name || item.label || String(id);
return { id: String(id), label }; return { id: String(id), label };
}); });
@@ -898,10 +978,10 @@ export default {
matched.promotion_factory_id != null matched.promotion_factory_id != null
? String(matched.promotion_factory_id) ? String(matched.promotion_factory_id)
: matched.id != null : matched.id != null
? String(matched.id) ? String(matched.id)
: matched.task_id != null : matched.task_id != null
? String(matched.task_id) ? String(matched.task_id)
: ''; : '';
} }
} }
// promotion_factory/getDetail 必须使用地址栏 promotion_factory_id避免列表首行 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 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 state = String(item.state != null ? item.state : '');
const promotionFactoryId = const promotionFactoryId =
item.promotion_factory_id != null item.promotion_factory_id != null ? item.promotion_factory_id : item.id != null ? item.id : item.task_id;
? item.promotion_factory_id
: item.id != null
? item.id
: item.task_id;
return { return {
id: item.id || item.task_id || `task_${idx + 1}`, id: item.id || item.task_id || `task_${idx + 1}`,
promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '', promotion_factory_id: promotionFactoryId != null ? String(promotionFactoryId) : '',
@@ -1206,16 +1282,16 @@ export default {
return Object.prototype.hasOwnProperty.call(stateTextMap, key) ? stateTextMap[key] : '-'; return Object.prototype.hasOwnProperty.call(stateTextMap, key) ? stateTextMap[key] : '-';
}, },
getTaskStatusClass(state) { getTaskStatusClass(state) {
const stateClassMap = { const stateClassMap = {
0: 'status-draft', // Draft - 浅灰色 0: 'status-draft', // Draft - 浅灰色
5: 'status-preparing', // Preparing - 橘色 5: 'status-preparing', // Preparing - 橘色
1: 'status-running', // Running - 深蓝色 1: 'status-running', // Running - 深蓝色
2: 'status-paused', // Paused - 深灰色 2: 'status-paused', // Paused - 深灰色
3: 'status-completed', // Completed - 绿色 3: 'status-completed', // Completed - 绿色
4: 'status-cancelled' // Cancelled - 浅红色 4: 'status-cancelled' // Cancelled - 浅红色
}; };
return stateClassMap[Number(state)] || ''; return stateClassMap[Number(state)] || '';
} }
} }
}; };
</script> </script>
@@ -1229,11 +1305,13 @@ export default {
margin-bottom: 15px; margin-bottom: 15px;
} }
.config-bar { .config-bar {
width: 100%;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
} }
.config-bar .left { .config-bar .left {
width: calc(100% - 100px);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
@@ -1758,96 +1836,96 @@ export default {
} }
.filter-header-row { .filter-header-row {
display: flex; display: flex;
align-items: center; /* 垂直居中对齐 */ align-items: center; /* 垂直居中对齐 */
gap: 16px; /* 胶囊和Search按钮之间的间距 */ gap: 16px; /* 胶囊和Search按钮之间的间距 */
margin-bottom: 16px; margin-bottom: 16px;
background-color: transparent; /* 容器透明,不厚重 */ background-color: transparent; /* 容器透明,不厚重 */
} }
/* 2. 彻底重置 el-tabs 的原生样式 (最丑的地方) */ /* 2. 彻底重置 el-tabs 的原生样式 (最丑的地方) */
.tmr-capsule-group { .tmr-capsule-group {
flex: 1; /* 占据左侧空间 */ flex: 1; /* 占据左侧空间 */
} }
/* 强制隐藏默认灰色横线和卡片灰边 */ /* 强制隐藏默认灰色横线和卡片灰边 */
.tmr-capsule-group .el-tabs__header { .tmr-capsule-group .el-tabs__header {
margin: 0 !important; margin: 0 !important;
border-bottom: none !important; border-bottom: none !important;
} }
.tmr-capsule-group .el-tabs__nav { .tmr-capsule-group .el-tabs__nav {
border: none !important; border: none !important;
} }
/* 3. 重新定义每个 Tab 的样式 (让其变成按钮) */ /* 3. 重新定义每个 Tab 的样式 (让其变成按钮) */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item { .tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
height: 32px !important; /* 紧凑高度 */ height: 32px !important; /* 紧凑高度 */
line-height: 32px !important; line-height: 32px !important;
font-size: 13px; /* 紧凑字体 */ font-size: 13px; /* 紧凑字体 */
border: none !important; /* 彻底隐藏原生卡片边框 */ border: none !important; /* 彻底隐藏原生卡片边框 */
background-color: transparent; /* 默认状态下透明背景 */ background-color: transparent; /* 默认状态下透明背景 */
color: #515a6e; /* 默认状态深灰文字,更专业 */ color: #515a6e; /* 默认状态深灰文字,更专业 */
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
padding: 0 16px !important; /* 适当内边距 */ padding: 0 16px !important; /* 适当内边距 */
margin-right: 8px; /* 每个 Tab 之间的间距 */ margin-right: 8px; /* 每个 Tab 之间的间距 */
border-radius: 6px !important; /* 先统一圆角 */ border-radius: 6px !important; /* 先统一圆角 */
overflow: visible; /* 确保选中的阴影显示完全 */ overflow: visible; /* 确保选中的阴影显示完全 */
} }
/* 首尾 Tab 的圆角处理 (形成整体感) */ /* 首尾 Tab 的圆角处理 (形成整体感) */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:first-child { .tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:first-child {
border-top-left-radius: 6px; border-top-left-radius: 6px;
border-bottom-left-radius: 6px; border-bottom-left-radius: 6px;
} }
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:last-child { .tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:last-child {
border-top-right-radius: 6px; border-top-right-radius: 6px;
border-bottom-right-radius: 6px; border-bottom-right-radius: 6px;
margin-right: 0; margin-right: 0;
} }
/* 中间 Tab 的处理 (去掉左右圆角,紧凑对齐) */ /* 中间 Tab 的处理 (去掉左右圆角,紧凑对齐) */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:first-child):not(:last-child) { .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) - 胶囊浮动核心** */ /* 4. **选中效果 (Active) - 胶囊浮动核心** */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item.is-active { .tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item.is-active {
background-color: #ffffff !important; /* 白色底色 */ background-color: #ffffff !important; /* 白色底色 */
color: #409eff !important; /* 主题蓝色文字 */ color: #409eff !important; /* 主题蓝色文字 */
font-weight: 500; font-weight: 500;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; /* **关键:增加轻微阴影,浮动感** */ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; /* **关键:增加轻微阴影,浮动感** */
border-radius: 6px; /* 选中时恢复圆角 */ border-radius: 6px; /* 选中时恢复圆角 */
position: relative; position: relative;
z-index: 2; /* 确保选中态在最前面,不被覆盖线破坏 */ z-index: 2; /* 确保选中态在最前面,不被覆盖线破坏 */
} }
/* 5. 处理 Tab 之间的断层 (像素级微调) */ /* 5. 处理 Tab 之间的断层 (像素级微调) */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item { .tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item {
position: relative; position: relative;
} }
/* 首尾 Tab 之间的像素级处理 */ /* 首尾 Tab 之间的像素级处理 */
.tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:last-child)::after { .tmr-capsule-group .el-tabs--card > .el-tabs__header .el-tabs__item:not(:last-child)::after {
content: ''; content: '';
position: absolute; position: absolute;
right: -5px; /* 让 Tab 之间紧密无缝 */ right: -5px; /* 让 Tab 之间紧密无缝 */
top: 0; top: 0;
height: 100%; height: 100%;
width: 1px; width: 1px;
background: transparent; /* 移除灰线,不丑 */ background: transparent; /* 移除灰线,不丑 */
} }
/* 6. 搜索按钮对齐微调 */ /* 6. 搜索按钮对齐微调 */
.filter-actions .el-button { .filter-actions .el-button {
height: 32px; /* 与 Tab 高度一致 */ height: 32px; /* 与 Tab 高度一致 */
padding: 0 16px; padding: 0 16px;
border-radius: 6px; border-radius: 6px;
transition: all 0.2s; transition: all 0.2s;
} }
.filter-actions { .filter-actions {
margin-left: auto; margin-left: auto;
} }
/* 基础 Badge 样式 */ /* 基础 Badge 样式 */
.status-badge { .status-badge {
@@ -1894,21 +1972,25 @@ export default {
background-color: #f1f5f9; background-color: #f1f5f9;
color: #64748b; color: #64748b;
} }
.status-draft::before { background-color: #94a3b8; } .status-draft::before {
background-color: #94a3b8;
}
/* 5: Preparing - 橘色 */ /* 5: Preparing - 橘色 */
.status-preparing { .status-preparing {
background-color: #fff7ed; background-color: #fff7ed;
color: #c2410c; color: #c2410c;
} }
.status-preparing::before { background-color: #f97316; } .status-preparing::before {
background-color: #f97316;
}
/* 1: Running - 深蓝色 */ /* 1: Running - 深蓝色 */
.status-running { .status-running {
background-color: #eff6ff; background-color: #eff6ff;
color: #1d4ed8; color: #1d4ed8;
} }
.status-running::before { .status-running::before {
background-color: #3b82f6; background-color: #3b82f6;
box-shadow: 0 0 4px rgba(59, 130, 246, 0.5); /* 模拟运行中的发光感 */ box-shadow: 0 0 4px rgba(59, 130, 246, 0.5); /* 模拟运行中的发光感 */
} }
@@ -1919,19 +2001,104 @@ export default {
color: #334155; color: #334155;
border: 1px solid #e2e8f0; /* 停用状态加个微边框增加分量感 */ border: 1px solid #e2e8f0; /* 停用状态加个微边框增加分量感 */
} }
.status-paused::before { background-color: #475569; } .status-paused::before {
background-color: #475569;
}
/* 3: Completed - 绿色 */ /* 3: Completed - 绿色 */
.status-completed { .status-completed {
background-color: #f0fdf4; background-color: #f0fdf4;
color: #15803d; color: #15803d;
} }
.status-completed::before { background-color: #22c55e; } .status-completed::before {
background-color: #22c55e;
}
/* 4: Cancelled - 浅红色 */ /* 4: Cancelled - 浅红色 */
.status-cancelled { .status-cancelled {
background-color: #fef2f2; background-color: #fef2f2;
color: #b91c1c; 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> </style>

View File

@@ -59,20 +59,22 @@
<div class="drawer-body" v-loading="loading"> <div class="drawer-body" v-loading="loading">
<div class="list-header"> <div class="list-header">
<div class="col-index">{{ $t('autoPromotionLogs.logColIndex') }}</div>
<div class="col-info">{{ $t('autoPromotionLogs.logColExpert') }}</div> <div class="col-info">{{ $t('autoPromotionLogs.logColExpert') }}</div>
<div class="col-send" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColSendTime') }}</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-prepared" style="font-size: 12px;">{{ $t('autoPromotionLogs.logColPreparedAt') }}</div>
<div class="col-status">{{ $t('autoPromotionLogs.logColStatus') }}</div> <div class="col-status">{{ $t('autoPromotionLogs.logColStatus') }}</div>
<div class="col-action">{{ $t('autoPromotionLogs.logColAction') }}</div> <div class="col-action"></div>
</div> </div>
<div class="list-wrapper"> <div class="list-wrapper">
<div <div
v-for="item in fullData" v-for="(item, rowIndex) in fullData"
:key="item.id" :key="item.id"
class="log-row" class="log-row"
:class="{ 'row-error': item.isErrorRow }" :class="{ 'row-error': item.isErrorRow }"
> >
<div class="col-index">{{ (currentPage - 1) * pageSize + rowIndex + 1 }}</div>
<div class="col-info"> <div class="col-info">
<div class="expert-main"> <div class="expert-main">
<span class="name">{{ item.expertName }}</span> <span class="name">{{ item.expertName }}</span>
@@ -782,6 +784,14 @@ export default {
color: #475569; color: #475569;
} }
.col-index {
flex: 0 0 52px;
width: 52px;
text-align: center;
color: #64748b;
font-variant-numeric: tabular-nums;
}
.col-info { .col-info {
flex: 1.8; flex: 1.8;
} }

View File

@@ -74,7 +74,7 @@
</div> </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 v-if="mailData.attachments && mailData.attachments.length" class="attachment-section">
<div class="attachment-header"> <div class="attachment-header">
@@ -123,6 +123,7 @@
<script> <script>
import Common from '@/components/common/common'; import Common from '@/components/common/common';
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
import FilePreviewDialog from './FilePreviewDialog.vue'; import FilePreviewDialog from './FilePreviewDialog.vue';
@@ -159,9 +160,29 @@ export default {
if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B'; if (!this.mailData.attachments || !this.mailData.attachments.length) return '0B';
const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0); const total = this.mailData.attachments.reduce((sum, f) => sum + (Number(f.size) || 0), 0);
return this.formatFileSize(total); 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: { methods: {
escapeHtml(text) {
if (text == null) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
},
scrollToAttachments() { scrollToAttachments() {
// 1. 获取目标元素的 DOM // 1. 获取目标元素的 DOM
const target = this.$el.querySelector('.attachment-section'); const target = this.$el.querySelector('.attachment-section');
@@ -441,6 +462,15 @@ const res = await this.$api.post('api/email_client/getAttachment', {
margin-bottom: 50px; 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 { .attachment-section {
border-top: 1px solid #eee; border-top: 1px solid #eee;

View File

@@ -109,7 +109,11 @@
<div class="mail-content-panel" v-if="selectedAccount" v-loading="detailLoading"> <div class="mail-content-panel" v-if="selectedAccount" v-loading="detailLoading">
<template v-if="activeMailId && !detailLoading"> <template v-if="activeMailId && !detailLoading">
<div class="mail-page"> <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> </div>
</template> </template>
@@ -168,9 +172,11 @@ const API = {
getAllJournal: 'api/Article/getJournal' getAllJournal: 'api/Article/getJournal'
}; };
import MailDetail from '../../components/page/components/email/MailDetail.vue'; import MailDetail from '../../components/page/components/email/MailDetail.vue';
import { normalizeEmailHtmlForInlineDisplay } from '@/utils/emailHtmlView';
export default { export default {
data() { data() {
return { return {
baseUrl: this.Common.baseUrl,
currentFolder: 'inbox', currentFolder: 'inbox',
searchKeyword: '', searchKeyword: '',
syncLoading: false, syncLoading: false,
@@ -292,8 +298,14 @@ import MailDetail from '../../components/page/components/email/MailDetail.vue';
this.$api this.$api
.post(API.getOneEmail, { j_email_id: jEmailId }) .post(API.getOneEmail, { j_email_id: jEmailId })
.then((res) => { .then((res) => {
const email = res && res.data ? res.data.email : null; const raw = res && res.data != null ? res.data : null;
if (res && res.code === 0 && email) { 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.selectedAccount = email;
this.fetchData(); this.fetchData();
this.startInboxSse(); this.startInboxSse();
@@ -488,28 +500,103 @@ fetchLatestSingleMail(jEmailId, journalId) {
this.currentFolder = f; this.currentFolder = f;
this.activeMailId = null; 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
},
/** 解析 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.activeMailId = item.id;
this.detailLoading = true; this.detailLoading = true;
this.detailMail = {};
const inboxId = item.inbox_id || item.id; const inboxId = item.inbox_id || item.id;
this.$api this.$api
.post(API.getEmailDetail, { inbox_id: inboxId }) .post(API.getEmailDetail, { inbox_id: inboxId })
.then((res) => { .then((res) => {
const d = res && res.data; try {
if (res && res.code === 0 && d) { const merged = this.buildDetailMailFromResponse(res, item, inboxId);
this.detailMail = { ...d, inbox_id: String(inboxId), attachments: [] }; this.detailMail = merged;
item.state = 0; if (item) item.state = 0;
this.displayList[index].is_read = d.is_read; const row = this.displayList[index];
if (Number(d.has_attachment) === 1) { if (row && merged && merged.is_read !== undefined && merged.is_read !== null) {
this.fetchAttachments(String(inboxId)); this.$set(row, 'is_read', merged.is_read);
} else {
this.detailLoading = false;
} }
} else { if (Number(merged.has_attachment) === 1) {
this.detailLoading = false; this.fetchAttachments(String(inboxId));
return;
}
} catch (e) {
console.error(e);
this.detailMail = this.buildDetailMailFromResponse(null, item, inboxId);
} }
this.detailLoading = false;
}) })
.catch(() => { .catch(() => {
this.detailMail = this.buildDetailMailFromResponse(null, item, inboxId);
this.detailLoading = false; this.detailLoading = false;
}); });
}, },
@@ -645,7 +732,7 @@ fetchLatestSingleMail(jEmailId, journalId) {
}, },
buildSseUrl(jEmailId) { buildSseUrl(jEmailId) {
// 与现有 axios baseURL=/api + 相对路径 规则一致GET + query 传参 // 与现有 axios baseURL=/api + 相对路径 规则一致GET + query 传参
const base = `/api/${API.inboxSse}`; const base = `${this.baseUrl}${API.inboxSse}`;
const q = new URLSearchParams({ j_email_id: String(jEmailId) }).toString(); const q = new URLSearchParams({ j_email_id: String(jEmailId) }).toString();
return `${base}?${q}`; return `${base}?${q}`;
}, },

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