邮件预览

This commit is contained in:
2026-05-09 10:29:34 +08:00
parent 67a4875b01
commit f67d8d5600
3 changed files with 158 additions and 16 deletions

View File

@@ -74,7 +74,7 @@
</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 +123,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';
@@ -159,9 +160,29 @@ export default {
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: {
escapeHtml(text) {
if (text == null) return '';
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
},
scrollToAttachments() {
// 1. 获取目标元素的 DOM
const target = this.$el.querySelector('.attachment-section');
@@ -441,6 +462,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;

View File

@@ -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, '&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.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;
});
},

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