里奥呦西 發表於 2026-3-17 16:13:00

🔥 手把手教你实现前端邮件预览功能

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<div>
<p>你是否曾经想过,在浏览器中直接点击一个邮件附件,就能预览完整的邮件内容——包括发件人、收件人、抄送、正文甚至内嵌图片?<br>
今天,我们要揭秘一个基于 Vue 3 和 Vant UI 的<strong>邮件预览上传组件</strong>,它不仅能上传 <code>.eml</code> 格式的邮件文件,还能在弹窗中完整渲染邮件内容,甚至支持附件图片的内联展示!</p>
<hr>
<h2 data-id="heading-0">🧩 组件核心功能一览</h2>
<ul>
<li>✅ 支持上传 <code>.eml</code> 格式邮件文件</li>
<li>✅ 限制文件类型、大小、数量</li>
<li>✅ 预览邮件内容(含发件人、收件人、抄送、正文、图片)</li>
<li>✅ 支持附件下载</li>
<li>✅ 响应式栅格布局,适配移动端</li>


</ul>
<hr>
<h2 data-id="heading-1">🧠 技术架构与实现细节</h2>
<h3 data-id="heading-2">1. 文件上传与格式校验</h3>
<p>组件使用 <code>van-uploader</code> 实现文件选择,并在 <code>beforeRead</code> 方法中进行格式和大小校验:</p>

</div>

</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const beforeRead = (file) =&gt; {
if (!props.accept.includes(file.type)) {
    createToast.fail({ getContainer: 'body', message: '文件格式错误' })
    return false
}
// 上传逻辑...
}</pre>
</div>
<h3 data-id="heading-3">2. 邮件内容解析:从二进制到可读 HTML</h3>
<p>这是最核心的部分!组件通过&nbsp;<code>FileReader</code>&nbsp;读取&nbsp;<code>.eml</code>&nbsp;文件内容,并使用&nbsp;<code>emailjs-mime-codec</code>&nbsp;和&nbsp;<code>eml-format</code>&nbsp;库进行解码:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">reader.onload = async (e) =&gt; {
let emlContent = e.target.result;
emlContent = Codec.quotedPrintableDecode(emlContent, "UTF-8");
emlFormat.read(emlContent, (err, data) =&gt; {
    // 解析出邮件主题、发件人、收件人、正文等
});
}</pre>
</div>
<h3 data-id="heading-4">3. 邮件主题解码:处理 MIME 编码</h3>
<p>邮件主题常常是 MIME 编码的,例如:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">=?UTF-8?B?5paw5bm56Zm15a+G?=</pre>
</div>
<div>
<div>
<p>组件使用 <code>Codec.mimeWordDecode</code> 进行解码,确保中文等非 ASCII 字符正确显示。</p>
<h3 data-id="heading-5">4. 内嵌图片处理:Uint8Array → Base64</h3>
<p>邮件中的图片通常以 <code>cid:</code> 引用,附件中以 <code>Uint8Array</code> 格式存储。组件将其转换为 Base64 并替换到 HTML 中:</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const base64String = uint8ArrayToBase64(item.data);
_html = _html.replaceAll(`cid:${cid}`, `data:image/${item.name.split('.').at(-1)};base64,${base64String}`);</pre>
</div>
<h3 data-id="heading-6">5. 弹窗预览与下载</h3>
<p>使用 Vant 的&nbsp;<code>Dialog</code>&nbsp;组件展示邮件内容,并支持一键下载原文件:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">Dialog({
message: concatHeader(data, title),
messageAlign: 'left',
className: "eml-dialog",
showCancelButton: true,
confirmButtonText: "下载"
}).then(async() =&gt; {
await nativeApi.downloadFile(encodeURI(item))
createToast.success({ getContainer: 'body', message: '保存成功' })
})
</pre>
</div>
<p>  </p>
<div>
<div>
<h2 data-id="heading-7">🎨 界面与交互设计</h2>
<ul>
<li>使用 <code>van-grid</code> 实现响应式文件列表</li>
<li>每个文件项显示为附件图标,点击可预览或下载</li>
<li>右上角删除按钮支持编辑模式下移除文件</li>
<li>提示信息友好,限制条件明确</li>
</ul>
<hr>
<h2 data-id="heading-8">🛠 可扩展性与优化建议</h2>
<ul>
<li><strong>类型推断</strong>:可增加一个函数根据 <code>file.type</code> 推断文件后缀名,增强兼容性</li>
<li><strong>错误处理</strong>:增加更多读取失败或格式错误的 fallback 逻辑</li>
<li><strong>性能优化</strong>:大文件分片读取,避免阻塞 UI</li>
</ul>
<hr>
<h2 data-id="heading-9">🚀 总结</h2>
<p>这个组件不仅实现了邮件上传与预览的完整链路,还展示了如何在浏览器中处理复杂的 MIME 格式邮件、解码主题、内联图片等高级功能。<br>
如果你正在开发一个需要邮件附件的管理系统、工单系统或邮件审计工具,这个组件绝对是一个<strong>值得借鉴和复用的技术方案</strong>!</p>
<hr>
<p>如果这篇文章对你有帮助,欢迎点赞、收藏、转发!<br>
我们也欢迎你在评论区留言,分享你在邮件解析或文件上传方面的实战经验!</p>
<hr>
<h2 data-id="heading-10">🚀 源码</h2>

</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;template&gt;
&lt;div&gt;
    &lt;van-grid :border="false" :column-num="4" :gutter="10" class="emlBox"&gt;
      &lt;!-- 上传更多图片 --&gt;
      &lt;van-grid-item v-for="(item, index) in list" :key="index"&gt;
      &lt;div class="picBox"&gt;
          &lt;div @click.stop="preview(item)"&gt;
            &lt;svg-icon
            icon-class="new-fujian"
            icon="new-fujian"
            class-name="file-svg-icon"
            /&gt;
          &lt;/div&gt;
          &lt;van-icon
            @click.stop="list.splice(index,1)"
            class="closeIcon"
            name="clear"
            v-if="isEdit"
          /&gt;
      &lt;/div&gt;
      &lt;/van-grid-item&gt;

      &lt;van-grid-item class="uploadGrid" v-if="list.length &lt; maxCount &amp;&amp; isEdit"&gt;
      &lt;van-uploader
          :max-count="maxCount"
          :max-size="maxSize*1024*1024"
          :accept="accept"
          class="file-upload__uploader"
          :preview-image="false"
          upload-icon="plus"
          :before-read="beforeRead"
      /&gt;
      &lt;/van-grid-item&gt;
    &lt;/van-grid&gt;
    &lt;div class="tip" v-if="isEdit"&gt;只能上传{{ acceptFile }}文件,不超过{{ maxSize }}M&lt;/div&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;script setup&gt;
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: ,
    defaule: 5
},
maxSize: {
    type: ,
    defaule: 20
},
filelList: {
    type: Array,
    default: () =&gt; []
},
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) =&gt; {
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 =&gt; {
    list.value.push(res);
})
return true
}
function removeGarbledChars(html) {
// 删除最后一个div闭合标签后的多余字符
let content = html;
let lastDivIndex = html.lastIndexOf('&lt;/div&gt;');
if (lastDivIndex !== -1) {
    content = content.substring(0, lastDivIndex + 6);
}
return content
}
// 拼接邮件内容的发件人/收件人/抄送/附件等信息
function concatHeader (file, title) {
const {to, from, cc, attachments, html} = file;
let header = `&lt;div&gt;&lt;b&gt;主题:&lt;/b&gt;${title}&lt;/div&gt;`;
header += `&lt;div&gt;&lt;b&gt;发件人:&lt;/b&gt;${from.name} &lt;${from.email}&gt;&lt;/div&gt;`;
let toList = to.map(item =&gt; `${item.name} &lt;${item.email}&gt;`).join('; ');
header += `&lt;div&gt;&lt;b&gt;收件人:&lt;/b&gt;${toList}&lt;/div&gt;`;
let ccList = cc.map(item =&gt; `${item.name} &lt;${item.email}&gt;`).join('; ');
header += `&lt;div&gt;&lt;b&gt;抄送:&lt;/b&gt;${ccList}&lt;/div&gt;`;
header += '&lt;div&gt;&lt;b&gt;邮件内容:&lt;/b&gt;&lt;/div&gt;';
header += removeGarbledChars(html);
return header;
}
const preview = async (item) =&gt; {
// 邮件预览功能暂时取消,直接下载文件,附件回显问题无法解决
if (item.split(".").at(-1).toLowerCase() === 'eml') {
    fetch(encodeURI(item)).then(res =&gt; res.blob()).then((data) =&gt; {
      const blob = new Blob();
      const reader = new FileReader();
      reader.onload = async (e) =&gt; {
      let emlContent = e.target.result;
      emlContent = Codec.quotedPrintableDecode(emlContent);
      emlFormat.read(emlContent, (err, data) =&gt; {
          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返回文件后缀名


&lt;/script&gt;
&lt;style lang="less"&gt;
.eml-dialog {
.van-dialog__message {
    display: flex;
    flex-direction: column;
}
.van-dialog__message &gt; div {
    width: fit-content;
}
}
&lt;/style&gt;
&lt;style lang="less" scoped&gt;
.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;
}
&lt;/style&gt;</pre>
</div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19730134
頁: [1]
查看完整版本: 🔥 手把手教你实现前端邮件预览功能