邮件预览
This commit is contained in:
@@ -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, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
},
|
||||||
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;
|
||||||
|
|||||||
@@ -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,6 +172,7 @@ 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 {
|
||||||
@@ -293,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();
|
||||||
@@ -489,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, '&')
|
||||||
|
.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.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;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
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