邮件预览
This commit is contained in:
@@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
},
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user