在 Web 应用中,文件预览功能是一个常见且实用的需求。
最下方有源码,大佬可以跳过简单介绍可以直接拉到下方查看。
方案 1 - kkFileView 服务
kkFile 文件预览是一款开源的文档在线预览项目。项目使用流行的 spring boot 搭建,易上手和部署,基本支持主流办公文档的在线预览,如 doc,docx,Excel,pdf,txt,zip,rar,图片等等。。
- 无需前端代码支持:kkFileView 是一个后端服务解决方案,不需要前端大量代码文件支持。
- 支持多种文件类型:kkFileView 支持
图片
、.docx
、.xlsx
和.pdf
等常见文件类型。
方案 2 - 前端预览插件(文后附 vue 代码)
前端预览插件是一个更轻量级的解决方案,它不需要服务端支持,只需在浏览器中加载文件预览组件即可。目前仅支持docx、xlsx、xls、pdf、jpg、jpeg、png、txt、MP4、MP3格式,其他后缀可继续扩展。
- 无需后端支持:前端预览插件不需要服务端支持,只需在浏览器中加载文件预览组件即可。
- 轻量:前端预览插件仅依赖前端框架,不依赖于任何后端服务。
- 灵活:前端预览插件支持各种文件类型,包括图片、文档、音视频等。
方案 2 的实现方式
1. 组件结构
首先,我们定义一个 Vue 组件,用于容纳文件列表和预览对话框。
<template>
<div class="filepreview-container">
<div class="file-list">
<div
v-for="(item, index) in fileList"
:key="index"
class="file-item"
@click="handleShowFile(item)"
>
{{ item.src }}
</div>
</div>
<div v-if="showFile" class="file-preview-dialog" @click="handleClose">
<!-- 这里空白点击事件是为了阻止触发父元素点击事件,避免关闭预览框 -->
<div class="file-preview-content" @click.stop="()=>{}">
<div class="file-box">
<div
v-if="!supportedTypes.includes(extractFileType(targetFileSrc))"
class="empty-box"
>
在线预览服务当前仅支持docx/xlsx/xls/pdf/jpg/jpeg/png/txt/mp4/mp3
</div>
<div
v-else-if="extractFileType(targetFileSrc) === 'xls'"
class="file-content xls-content"
>
<div id="tabs-container">
<div class="tabs-header">
<!-- 动态生成sheet标签页 -->
<div
:class="['tab-item', { active: activeSheetName === itemSheetName }]"
v-for="(item, itemSheetName) in xlsData"
:key="itemSheetName"
@click="activeSheetName = itemSheetName"
>
{{ itemSheetName }}
</div>
</div>
<div class="tabs-content">
<!-- 显示当前选中的表格 -->
<template v-if="activeSheetName && xlsData[activeSheetName]">
<table
v-if="xlsData[activeSheetName].length > 0"
class="data-table"
>
<thead>
<tr>
<!-- 表头 -->
<th
v-for="(item, key) in xlsData[activeSheetName][0]"
:key="key"
>
{{ key }}
</th>
</tr>
</thead>
<tbody>
<!-- 表格内容 -->
<tr v-for="row in xlsData[activeSheetName]" :key="row.id">
<td v-for="(cell, key) in row" :key="key">
{{ cell }}
</td>
</tr>
</tbody>
</table>
<div v-else>当前Sheet页无数据</div>
</template>
<div v-else>请选择一个Sheet页</div>
</div>
</div>
</div>
<VueOfficeDocx
v-else-if="extractFileType(targetFileSrc) === 'docx'"
:src="targetFileSrc"
class="file-content docx-content"
/>
<VueOfficeExcel
v-else-if="extractFileType(targetFileSrc) === 'xlsx'"
:src="targetFileSrc"
class="file-content xlsx-content"
/>
<VueOfficePdf
v-else-if="extractFileType(targetFileSrc) === 'pdf'"
:src="targetFileSrc"
class="file-content pdf-content"
/>
<div
v-else-if="['jpeg', 'png', 'jpg'].includes(extractFileType(targetFileSrc))"
class="file-content img-content"
>
<img :src="targetFileSrc" :alt="targetFileSrc" />
</div>
<div
v-else-if="extractFileType(targetFileSrc) === 'txt'"
class="file-content txt-content"
>
<pre><code>{{ txtData }}</code></pre>
</div>
<div
v-else-if="extractFileType(targetFileSrc) === 'mp3'"
class="file-content audio-content"
>
<!-- 音频文件 -->
<audio :src="targetFileSrc" controls width="100%" height="30px">
您的浏览器不支持 audio 标签。
</audio>
</div>
<div
v-else-if="extractFileType(targetFileSrc) === 'mp4'"
class="file-content video-content"
>
<!-- 视频文件 -->
<video controls width="auto" height="100%">
<source :src="targetFileSrc" type="video/mp4" />
您的浏览器不支持 Video 标签。
</video>
</div>
</div>
<div class="file-box-btns">
<div @click="downLoadFile">下载</div>
<div @click="handleClose">取消</div>
</div>
</div>
</div>
</div>
</template>
2. 使用第三方库
为了处理不同类型的文件预览,我们将使用@vue-office
、xlsx-js-style
。
安装依赖
npm install @vue-office/docx @vue-office/excel @vue-office/pdf xlsx-js-style
引入并注册组件
在 Vue 组件中,我们需要引入并注册这些第三方库提供的组件。
<script setup name="FilePreview">
import { ref, computed, defineProps } from "vue"
import { getToken } from "@/utils/auth"
import VueOfficeDocx from "@vue-office/docx"
import "@vue-office/docx/lib/index.css"
import VueOfficeExcel from "@vue-office/excel"
import "@vue-office/excel/lib/index.css"
import VueOfficePdf from "@vue-office/pdf"
import * as XLSX from "xlsx-js-style"
// 支持的文件类型数组
const supportedTypes = ["docx", "xlsx", "xls", "pdf", "jpg", "jpeg", "png", "txt", "mp4", "mp3"]
// 封装一个fetch方法,通过文件路径获取二进制文件内容
const getFileArrayBufferBySrc = async (src, useLocalToken = false) => {
const headers = new Headers()
if (useLocalToken) {
headers.set("Authorization", `Bearer ${getToken()}`)
}
const res = await fetch(src, { headers })
if (!res.ok) {
throw new Error("请求失败")
}
return await res.arrayBuffer()
}
// 提取文件类型函数,通过文件路径获取文件扩展名
const extractFileType = path => {
const dotIndex = path.lastIndexOf(".")
return dotIndex !== -1 ? path.substring(dotIndex + 1) : "无文件扩展名"
}
// 验证URL是否有效的函数
const isValidUrl = url => {
const pattern = new RegExp('^(https?:\\/\\/)?'+ // 协议
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // 域名
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // IP
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // 端口和路径
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // 查询字符串
'(\\#[-a-z\\d_]*)?$','i'); // 锚点
return !!pattern.test(url);
}
// 定义组件属性
// list可以是字符串数组[''],也可以是object数组[{name: '', src: ''}]
const props = defineProps({
list: {
type: Array,
default: () => []
}
})
// 文件预览对话框显示状态
const showFile = ref(false)
// 当前预览文件的源路径
const targetFileSrc = ref("")
// 当前活动的Sheet名称,用于Excel文件预览
const activeSheetName = ref(undefined)
// Excel文件数据
const xlsData = ref({})
// 文本文件数据
const txtData = ref(undefined)
// 处理文件列表,根据URL或对象生成统一的文件列表格式
const fileList = computed(() => {
return props.list.map(item => {
if (typeof item === "string") {
const lastSlashIndex = item.lastIndexOf("/")
const dotIndex = item.lastIndexOf(".")
if (lastSlashIndex !== -1 && dotIndex !== -1 && lastSlashIndex < dotIndex) {
const fileName = item.substring(lastSlashIndex + 1, dotIndex)
const fileType = item.substring(dotIndex + 1)
return { name: fileName, src: item, filetype: fileType }
} else {
return { name: "未知文件类型", src: item, filetype: "unknown" }
}
} else if (typeof item === "object" && item !== null && "src" in item) {
const fileName = "name" in item ? item.name : "unknown"
const fileType = extractFileType(item.src)
return { name: fileName, src: item.src, filetype: fileType }
} else {
return { name: "unknown", src: "unknown", filetype: "unknown" }
}
})
})
// 处理文件预览
const handleShowFile = async item => {
showFile.value = true
targetFileSrc.value = item.src
if (item.filetype === "xls") {
const arrayBuffer = await getFileArrayBufferBySrc(item.src, false)
const data = new Uint8Array(arrayBuffer)
const workbook = XLSX.read(data, { type: "buffer" })
activeSheetName.value = workbook.SheetNames[0]
workbook.SheetNames.forEach(sheetName => {
const worksheet = workbook.Sheets[sheetName]
xlsData.value[sheetName] = XLSX.utils.sheet_to_json(worksheet)
})
} else if (item.filetype === "txt") {
const arrayBuffer = await getFileArrayBufferBySrc(item.src, false)
const decoder = new TextDecoder("utf-8")
txtData.value = decoder.decode(arrayBuffer)
}
}
// 处理文件下载
const downLoadFile = () => {
if (!isValidUrl(targetFileSrc.value)) {
console.error("下载路径不正确.")
return
}
fetch(targetFileSrc.value, {
method: "GET",
headers: {
Authorization: `Bearer ${getToken()}`,
"Content-Type": "application/x-www-form-urlencoded"
}
})
.then(response => {
if (!response.ok) {
throw new Error("网络连接失败")
}
// 将 Response 转换为 Blob
return response.blob()
})
.then(data => {
const reader = new FileReader()
// 读取 blob 数据
reader.readAsDataURL(data)
// 读取 blob 数据为 Data URL
reader.onloadend = function () {
const a = document.createElement("a")
a.href = reader.result
// 设置为'_blank'以在新窗口或标签页中打开
a.target = "_blank"
// 安全问题
a.rel = "noopenner noreferrer"
const lastSlashIndex = targetFileSrc.value.lastIndexOf("/")
const fileName = targetFileSrc.value.substring(lastSlashIndex + 1)
a.download = fileName // 设置文件名
a.click() // 触发下载
handleClose()
}
})
.catch(error => {
console.error("下载失败:", error)
})
}
// 关闭文件预览对话框
const handleClose = () => {
showFile.value = false
targetFileSrc.value = undefined
activeSheetName.value = undefined
xlsData.value = {}
txtData.value = undefined
}
</script>
3. 样式
最后,我们添加一些 CSS 样式来美化我们的文件预览插件。
<style lang="scss" scoped>
$border-color: #e4e7ed;
$hover-bg: #f5f7fa;
$hover-color: #69b4ff;
$active-color: #409eff;
$file-preview-dialog-width: 100vw;
$file-preview-dialog-height: 100vh;
.filepreview-container {
width: 100%;
max-width: 1000px;
overflow: auto;
.file-list {
.file-item {
border: 1px solid $border-color;
line-height: 2;
margin-bottom: 10px;
position: relative;
// 省略号
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 0 10px;
// hover
transition: background-color 0.3s, color 0.3s;
&:hover {
background-color: $hover-bg;
cursor: pointer;
color: $hover-color;
}
}
}
.file-preview-dialog {
width: $file-preview-dialog-width;
height: $file-preview-dialog-height;
background-color: rgba(0, 0, 0, 0.4);
overflow: hidden;
position: fixed;
left: 0;
top: 0;
z-index: 999;
display: flex;
justify-content: center;
align-items: center;
.file-preview-content {
width: 80vw;
height: 90vh;
background-color: #fff;
border-radius: 5px;
padding: 30px;
display: flex;
flex-flow: column;
.file-box {
flex: 1;
height: calc(90vh - 80px - 60px);
.file-content {
width: 100%;
height: 100%;
display: flex;
flex-flow: column;
overflow: auto;
background-color: #808080;
}
.xls-content {
// 表格table的基础样式
#tabs-container {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
flex-direction: column;
border: 1px solid $border-color;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
.tabs-header {
width: 100%;
display: flex;
gap: 5px;
background-color: #f2f2f2;
padding: 5px 5px 0;
.tab-item {
padding: 8px 16px;
cursor: pointer;
border: 1px solid $border-color;
border-bottom: none;
color: #606266;
font-size: 14px;
line-height: 1.5;
transition: all 0.3s;
&.active {
background-color: white;
position: relative;
top: -1px; /* 下沉效果 */
border-bottom-color: transparent;
color: $active-color;
font-weight: bold;
}
}
}
.tabs-content {
width: 100%;
height: calc(100% - 36px); /* 减去头部高度 */
padding: 10px;
overflow: auto;
border: 1px solid $border-color;
border-top: none;
.data-table {
width: 100%;
border-collapse: collapse;
overflow: auto;
font-size: 14px;
color: #606266;
th,
td {
border: 1px solid $border-color;
padding: 8px;
text-align: left;
min-width: 100px;
min-height: 30px;
background-color: #fff;
vertical-align: middle;
}
th {
background-color: #f2f2f2;
color: #606266;
font-weight: bold;
}
tr:hover {
background-color: $hover-bg;
}
}
}
}
}
.img-content {
> img {
width: 100%;
height: auto;
}
}
.txt-content {
font-size: 15px;
line-height: 1.5;
background-color: #fff;
}
.audio-content {
display: flex;
justify-content: center;
align-items: center;
}
.empty-box {
color: #aaaaaa;
fill: currentColor;
width: 100%;
height: 100%;
vertical-align: top;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
}
.file-box-btns {
display: flex;
flex-flow: row;
justify-content: center;
margin: 20px 0;
> div {
margin: 0 20px;
background-color: $active-color;
color: #fff;
line-height: 40px;
padding: 0 20px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: lighten($active-color, 10%);
color: #fff;
}
}
}
}
}
}
// 响应式设计
@media (max-width: 768px) {
.filepreview-container {
max-width: 100%;
}
.file-preview-dialog {
width: 100%;
height: 100%;
.file-preview-content {
width: 90%;
height: 80%;
padding: 20px;
.file-box {
height: calc(80% - 40px - 40px);
.file-content {
height: 100%;
}
}
}
}
}
</style>
通过以上步骤,我们实现了一个功能齐全的前端文件预览插件。用户可以通过点击文件列表中的文件来查看其内容,并且可以下载文件。这个插件支持.docx
、.xlsx
和.pdf
文件类型,并且可以根据需要轻松扩展以支持更多文件类型。
文件列表
word文档
excel预览(xlsx和xls不一致,xls无空白行数据及样式)
txt预览