写在开头点赞 收藏 学会你是否曾经想过在浏览器中直接点击一个邮件附件就能预览完整的邮件内容——包括发件人、收件人、抄送、正文甚至内嵌图片今天我们要揭秘一个基于 Vue 3 和 Vant UI 的邮件预览上传组件它不仅能上传 .eml 格式的邮件文件还能在弹窗中完整渲染邮件内容甚至支持附件图片的内联展示 组件核心功能一览✅ 支持上传 .eml 格式邮件文件✅ 限制文件类型、大小、数量✅ 预览邮件内容含发件人、收件人、抄送、正文、图片✅ 支持附件下载✅ 响应式栅格布局适配移动端 技术架构与实现细节1. 文件上传与格式校验组件使用 van-uploader 实现文件选择并在 beforeRead 方法中进行格式和大小校验const beforeRead (file) { if (!props.accept.includes(file.type)) { createToast.fail({ getContainer: body, message: 文件格式错误 }) return false } // 上传逻辑... }2. 邮件内容解析从二进制到可读 HTML这是最核心的部分组件通过 FileReader 读取 .eml 文件内容并使用 emailjs-mime-codec 和 eml-format 库进行解码reader.onload async (e) { let emlContent e.target.result; emlContent Codec.quotedPrintableDecode(emlContent, UTF-8); emlFormat.read(emlContent, (err, data) { // 解析出邮件主题、发件人、收件人、正文等 }); }3. 邮件主题解码处理 MIME 编码邮件主题常常是 MIME 编码的例如?UTF-8?B?5paw5bm56Zm15aG?组件使用 Codec.mimeWordDecode 进行解码确保中文等非 ASCII 字符正确显示。4. 内嵌图片处理Uint8Array → Base64邮件中的图片通常以 cid: 引用附件中以 Uint8Array 格式存储。组件将其转换为 Base64 并替换到 HTML 中const base64String uint8ArrayToBase64(item.data); _html _html.replaceAll(cid:${cid}, data:image/${item.name.split(.).at(-1)};base64,${base64String});5. 弹窗预览与下载使用 Vant 的 Dialog 组件展示邮件内容并支持一键下载原文件Dialog({ message: concatHeader(data, title), messageAlign: left, className: eml-dialog, showCancelButton: true, confirmButtonText: 下载 }).then(async() { await nativeApi.downloadFile(encodeURI(item)) createToast.success({ getContainer: body, message: 保存成功 }) }) 界面与交互设计使用 van-grid 实现响应式文件列表每个文件项显示为附件图标点击可预览或下载右上角删除按钮支持编辑模式下移除文件提示信息友好限制条件明确 可扩展性与优化建议类型推断可增加一个函数根据 file.type 推断文件后缀名增强兼容性错误处理增加更多读取失败或格式错误的 fallback 逻辑性能优化大文件分片读取避免阻塞 UI 总结这个组件不仅实现了邮件上传与预览的完整链路还展示了如何在浏览器中处理复杂的 MIME 格式邮件、解码主题、内联图片等高级功能。如果你正在开发一个需要邮件附件的管理系统、工单系统或邮件审计工具这个组件绝对是一个值得借鉴和复用的技术方案 源码template div van-grid :borderfalse :column-num4 :gutter10 classemlBox !-- 上传更多图片 -- van-grid-item v-for(item, index) in list :keyindex div classpicBox div click.stoppreview(item) svg-icon icon-classnew-fujian iconnew-fujian class-namefile-svg-icon / /div van-icon click.stoplist.splice(index,1) classcloseIcon nameclear v-ifisEdit / /div /van-grid-item van-grid-item classuploadGrid v-iflist.length maxCount isEdit van-uploader :max-countmaxCount :max-sizemaxSize*1024*1024 :acceptaccept classfile-upload__uploader :preview-imagefalse upload-iconplus :before-readbeforeRead / /van-grid-item /van-grid div classtip v-ifisEdit只能上传{{ acceptFile }}文件不超过{{ maxSize }}M/div /div /template script setup import { useVModel } from vueuse/core import { ref, defineProps, defineEmits } from vue import inspectionApi from /service/apis/modules/inspectionApi.js import { useToast } from /hooks import * as emlFormat from eml-format; import * as Codec from emailjs-mime-codec; import { Dialog } from vant; import nativeApi from /tools/native.js const props defineProps({ maxCount: { type: [Number, String], defaule: 5 }, maxSize: { type: [Number, String], defaule: 20 }, filelList: { type: Array, default: () [] }, accept: { type: String, default: message/rfc822 }, acceptFile: { type: String, default: eml }, isEdit: { type: Boolean, default: true } }) const emit defineEmits([update:filelList]) const list useVModel(props, filelList, emit, { defaultValue: [] }) const { createToast } useToast() const beforeRead (file) { if (!props.accept.includes(file.type)) { createToast.fail({ getContainer: body, message: 文件格式错误 }) return false } let formData new FormData(); formData.append(file, file); inspectionApi.uploadVideoAPI(formData).then(res { list.value.push(res); }) return true } function removeGarbledChars(html) { // 删除最后一个div闭合标签后的多余字符 let content html; let lastDivIndex html.lastIndexOf(/div); if (lastDivIndex ! -1) { content content.substring(0, lastDivIndex 6); } return content } // 拼接邮件内容的发件人/收件人/抄送/附件等信息 function concatHeader (file, title) { const {to, from, cc, attachments, html} file; let header divb主题/b${title}/div; header divb发件人/b${from.name} lt;${from.email}gt;/div; let toList to.map(item ${item.name} lt;${item.email}gt;).join(; ); header divb收件人/b${toList}/div; let ccList cc.map(item ${item.name} lt;${item.email}gt;).join(; ); header divb抄送/b${ccList}/div; header divb邮件内容/b/div; header removeGarbledChars(html); return header; } const preview async (item) { // 邮件预览功能暂时取消直接下载文件附件回显问题无法解决 if (item.split(.).at(-1).toLowerCase() eml) { fetch(encodeURI(item)).then(res res.blob()).then((data) { const blob new Blob([data]); const reader new FileReader(); reader.onload async (e) { let emlContent e.target.result; emlContent Codec.quotedPrintableDecode(emlContent); emlFormat.read(emlContent, (err, data) { let title if (data.subject) { title Codec.mimeWorsdDecode(data.subject); } Dialog({ message: concatHeader(data, title), messageAlign: left, className: eml-dialog, showCancelButton: true, confirmButtonText: 下载 }) }) } reader.readAsText(blob); }) } } // 文件对象中的type和后缀名不一定一致所以需要判断写一个函数根据文件的type返回文件后缀名 /script style langless .eml-dialog { .van-dialog__message { display: flex; flex-direction: column; } .van-dialog__message div { width: fit-content; } } /style style langless scoped .tip { color: #999999; margin-bottom: 12px; padding-left: 16px !important; } .closeIcon { position: absolute; right: 0px; top: 10px; font-size: 20px; } .file-upload__uploader { width: 100%; ::v-deep { .van-uploader__upload { margin: 0; } .van-uploader__upload-icon { display: inline-flex; align-items: center; justify-content: center; color: #999999; font-weight: bold; width: 100%; height: 80px; background: #fdfdfd; border-radius: 6px; border: 1px solid #e5e5e5; overflow: hidden; font-size: 12px; } } } .emlBox { padding-left: 16px !important; padding-right: 6px; .picBox { width: 100%; } .uploadGrid { padding-right: 0 !important; } ::v-deep { .van-grid-item__content { padding: 10px 0; justify-content: start; position: relative; } } } .file-svg-icon { width: 100%; height: 80px; } /style如果对您有所帮助欢迎您点个关注我会定时更新技术文档大家一起讨论学习一起进步。
[特殊字符] 手把手教你实现前端邮件预览功能
写在开头点赞 收藏 学会你是否曾经想过在浏览器中直接点击一个邮件附件就能预览完整的邮件内容——包括发件人、收件人、抄送、正文甚至内嵌图片今天我们要揭秘一个基于 Vue 3 和 Vant UI 的邮件预览上传组件它不仅能上传 .eml 格式的邮件文件还能在弹窗中完整渲染邮件内容甚至支持附件图片的内联展示 组件核心功能一览✅ 支持上传 .eml 格式邮件文件✅ 限制文件类型、大小、数量✅ 预览邮件内容含发件人、收件人、抄送、正文、图片✅ 支持附件下载✅ 响应式栅格布局适配移动端 技术架构与实现细节1. 文件上传与格式校验组件使用 van-uploader 实现文件选择并在 beforeRead 方法中进行格式和大小校验const beforeRead (file) { if (!props.accept.includes(file.type)) { createToast.fail({ getContainer: body, message: 文件格式错误 }) return false } // 上传逻辑... }2. 邮件内容解析从二进制到可读 HTML这是最核心的部分组件通过 FileReader 读取 .eml 文件内容并使用 emailjs-mime-codec 和 eml-format 库进行解码reader.onload async (e) { let emlContent e.target.result; emlContent Codec.quotedPrintableDecode(emlContent, UTF-8); emlFormat.read(emlContent, (err, data) { // 解析出邮件主题、发件人、收件人、正文等 }); }3. 邮件主题解码处理 MIME 编码邮件主题常常是 MIME 编码的例如?UTF-8?B?5paw5bm56Zm15aG?组件使用 Codec.mimeWordDecode 进行解码确保中文等非 ASCII 字符正确显示。4. 内嵌图片处理Uint8Array → Base64邮件中的图片通常以 cid: 引用附件中以 Uint8Array 格式存储。组件将其转换为 Base64 并替换到 HTML 中const base64String uint8ArrayToBase64(item.data); _html _html.replaceAll(cid:${cid}, data:image/${item.name.split(.).at(-1)};base64,${base64String});5. 弹窗预览与下载使用 Vant 的 Dialog 组件展示邮件内容并支持一键下载原文件Dialog({ message: concatHeader(data, title), messageAlign: left, className: eml-dialog, showCancelButton: true, confirmButtonText: 下载 }).then(async() { await nativeApi.downloadFile(encodeURI(item)) createToast.success({ getContainer: body, message: 保存成功 }) }) 界面与交互设计使用 van-grid 实现响应式文件列表每个文件项显示为附件图标点击可预览或下载右上角删除按钮支持编辑模式下移除文件提示信息友好限制条件明确 可扩展性与优化建议类型推断可增加一个函数根据 file.type 推断文件后缀名增强兼容性错误处理增加更多读取失败或格式错误的 fallback 逻辑性能优化大文件分片读取避免阻塞 UI 总结这个组件不仅实现了邮件上传与预览的完整链路还展示了如何在浏览器中处理复杂的 MIME 格式邮件、解码主题、内联图片等高级功能。如果你正在开发一个需要邮件附件的管理系统、工单系统或邮件审计工具这个组件绝对是一个值得借鉴和复用的技术方案 源码template div van-grid :borderfalse :column-num4 :gutter10 classemlBox !-- 上传更多图片 -- van-grid-item v-for(item, index) in list :keyindex div classpicBox div click.stoppreview(item) svg-icon icon-classnew-fujian iconnew-fujian class-namefile-svg-icon / /div van-icon click.stoplist.splice(index,1) classcloseIcon nameclear v-ifisEdit / /div /van-grid-item van-grid-item classuploadGrid v-iflist.length maxCount isEdit van-uploader :max-countmaxCount :max-sizemaxSize*1024*1024 :acceptaccept classfile-upload__uploader :preview-imagefalse upload-iconplus :before-readbeforeRead / /van-grid-item /van-grid div classtip v-ifisEdit只能上传{{ acceptFile }}文件不超过{{ maxSize }}M/div /div /template script setup import { useVModel } from vueuse/core import { ref, defineProps, defineEmits } from vue import inspectionApi from /service/apis/modules/inspectionApi.js import { useToast } from /hooks import * as emlFormat from eml-format; import * as Codec from emailjs-mime-codec; import { Dialog } from vant; import nativeApi from /tools/native.js const props defineProps({ maxCount: { type: [Number, String], defaule: 5 }, maxSize: { type: [Number, String], defaule: 20 }, filelList: { type: Array, default: () [] }, accept: { type: String, default: message/rfc822 }, acceptFile: { type: String, default: eml }, isEdit: { type: Boolean, default: true } }) const emit defineEmits([update:filelList]) const list useVModel(props, filelList, emit, { defaultValue: [] }) const { createToast } useToast() const beforeRead (file) { if (!props.accept.includes(file.type)) { createToast.fail({ getContainer: body, message: 文件格式错误 }) return false } let formData new FormData(); formData.append(file, file); inspectionApi.uploadVideoAPI(formData).then(res { list.value.push(res); }) return true } function removeGarbledChars(html) { // 删除最后一个div闭合标签后的多余字符 let content html; let lastDivIndex html.lastIndexOf(/div); if (lastDivIndex ! -1) { content content.substring(0, lastDivIndex 6); } return content } // 拼接邮件内容的发件人/收件人/抄送/附件等信息 function concatHeader (file, title) { const {to, from, cc, attachments, html} file; let header divb主题/b${title}/div; header divb发件人/b${from.name} lt;${from.email}gt;/div; let toList to.map(item ${item.name} lt;${item.email}gt;).join(; ); header divb收件人/b${toList}/div; let ccList cc.map(item ${item.name} lt;${item.email}gt;).join(; ); header divb抄送/b${ccList}/div; header divb邮件内容/b/div; header removeGarbledChars(html); return header; } const preview async (item) { // 邮件预览功能暂时取消直接下载文件附件回显问题无法解决 if (item.split(.).at(-1).toLowerCase() eml) { fetch(encodeURI(item)).then(res res.blob()).then((data) { const blob new Blob([data]); const reader new FileReader(); reader.onload async (e) { let emlContent e.target.result; emlContent Codec.quotedPrintableDecode(emlContent); emlFormat.read(emlContent, (err, data) { let title if (data.subject) { title Codec.mimeWorsdDecode(data.subject); } Dialog({ message: concatHeader(data, title), messageAlign: left, className: eml-dialog, showCancelButton: true, confirmButtonText: 下载 }) }) } reader.readAsText(blob); }) } } // 文件对象中的type和后缀名不一定一致所以需要判断写一个函数根据文件的type返回文件后缀名 /script style langless .eml-dialog { .van-dialog__message { display: flex; flex-direction: column; } .van-dialog__message div { width: fit-content; } } /style style langless scoped .tip { color: #999999; margin-bottom: 12px; padding-left: 16px !important; } .closeIcon { position: absolute; right: 0px; top: 10px; font-size: 20px; } .file-upload__uploader { width: 100%; ::v-deep { .van-uploader__upload { margin: 0; } .van-uploader__upload-icon { display: inline-flex; align-items: center; justify-content: center; color: #999999; font-weight: bold; width: 100%; height: 80px; background: #fdfdfd; border-radius: 6px; border: 1px solid #e5e5e5; overflow: hidden; font-size: 12px; } } } .emlBox { padding-left: 16px !important; padding-right: 6px; .picBox { width: 100%; } .uploadGrid { padding-right: 0 !important; } ::v-deep { .van-grid-item__content { padding: 10px 0; justify-content: start; position: relative; } } } .file-svg-icon { width: 100%; height: 80px; } /style如果对您有所帮助欢迎您点个关注我会定时更新技术文档大家一起讨论学习一起进步。