vue3 前端文件预览实现
一 背景
公司制作在线文件预览库,可接受不能预览doc、ppt;
1)图片采用:element-plus的 el-image-viewer
2)vue-office 系列组件可预览 :pdf、docx、excel(xls、xlsx、csv)、pptx
3)bestofdview:预览ofd
4)dplayer 预览音频:mp4,这别后台会校验mp4 前 200 KB 内 有 moov(可边加载边播放)
5)txt:直接读取内容:放入网页显示,但使用 chardet 识别前10k 判断编码模式
其他介绍
html:不推荐预览,加载script 可能会攻击你的网站
若url路径包含文件名:文件名不能包含 [、]、+ 或 %
这里档案库禁止 doc、ppt 上传,除了上面能预览的档案外,档案库还允许上传安全的压缩包类型;
注:为了保证文件的防止传播性,这边后台会为每次预览生成一个临时token,且预览地址会携带该参数;后台会校验token 合法、短时间有效、甚至是单次有效,防止通过预览地址下载到原文件
1.1 预览效果
1 图片:注:不一定要点击图片显示,也可点击按钮直接渲染:el-image-viewer 组件
地址中图片预览 ,https://element-plus.org/zh-CN/component/image
2 pdf、office系列
地址中的:演示效果,https://github.com/501351981/vue-office
3 ofd
地址中的:在线预览地址,https://github.com/besthqs/bestofdview
4 媒体
地址中的媒体:https://github.com/DIYgod/DPlayer
5 文本预览
使用 vue3组件 chardet 进行编码识别,然后用浏览器内置的 TextDecoder 做编码转换:
https://github.com/runk/node-chardet
1.2 安装依赖
# 预览图片
pnpm install element-plus
# 预览pdf
pnpm install @vue-office/pdf
# 预览 docx
pnpm install @vue-office/docx
# 预览 xls、xlsx
pnpm install @vue-office/excel
# 预览ppt
pnpm install @vue-office/pptx
# 预览 ofd
pnpm install bestofdview
# 预览mp4
pnpm install dplayer
# 预览 文件编码识别
pnpm install chardet
二 实现介绍
2.1 图片
element-plus 版本 2.9.9 ,组件名imageDialog
效果,地址中的:图片预览 ,https://element-plus.org/zh-CN/component/image
注:不一定要点击图片显示,也可点击按钮直接渲染:el-image-viewer 组件
<template>
<el-watermark v-if="imgViewerVisible" :content="watermarkContent" :font="watermarkFont " :z-index="3100"
style="position: fixed; inset: 0; z-index: 3000">
<el-image-viewer
:url-list="[fileUrl]"
:initial-index="0"
:z-index="3000"
@close="closeDialog"
/>
</el-watermark>
</template>
<script setup>
const
// 图片预览是否显示
const imgViewerVisible = ref(false)
// 文件url
const fileUrl = ref()
const watermarkContent = ref('水印')
const watermarkFont = ref({
color: 'rgba(0, 0, 0, 0.2)',
fontSize: 16,
fontWeight: 'bold',
fontFamily: 'sans-serif'
})
// 关闭窗口
const closeDialog = () => {
dialogVisible.value = false;
imgViewerVisible.value = false;
fileUrl.value = '';
}
const open = async (url ) => {
fileUrl.value = url
imgViewerVisible.value = true
})
2.2 pdf预览
PdfView.vue 官方样例:https://github.com/501351981/vue-office
注:options 传空即可,这里禁用多线程加载是为了保护服务器
<template>
<div class="pdf-wrapper" v-loading="loading" element-loading-text="加载中...">
<vue-office-pdf v-if="show" :src="currentUrl" :options="options" @rendered="onRendered" @error="handleError"/>
</div>
</template>
<script setup>
import VueOfficePdf from '@vue-office/pdf'
import {ref, nextTick, watch} from 'vue'
const emit = defineEmits(['error'])
const props = defineProps({
fileUrl: {
type: String,
default: undefined
}
})
const loading = ref(false)
const show = ref(false)
const currentUrl = ref('')
const options = {
disableRange: true,
disableStream: true,
disableAutoFetch: true,
}
const onRendered = () => {
loading.value = false
}
const handleError = (error) => {
loading.value = false
emit('error', error)
}
// 加载文件显示
const renderFile = (url) => {
show.value = false
nextTick(() => {
loading.value = true
currentUrl.value = url
show.value = true
})
}
watch(() => props.fileUrl, (newUrl) => {
if (newUrl) {
renderFile(newUrl)
}
}, {immediate: true})
</script>
<style lang="scss" scoped>
.pdf-wrapper {
height: 100%;
overflow: auto;
/* 必加 ——— pdf.js observer 需要稳定布局 */
display: flex;
flex-direction: column;
min-height: 0;
}
</style>
2.3 docx预览
DocView.vue 官方样例:https://github.com/501351981/vue-office
添加了:宽度渲染和不渲染切换效果
<template>
<div :class="[{ 'docx-wrapper-fix-width': viewMode === 'adaptive' }]" v-loading="loading"
element-loading-text="加载中...">
<div class="docx-header">
<el-radio-group v-model="viewMode" @change="handleViewModeChange" size="small">
<el-radio-button label="standard">标准</el-radio-button>
<el-radio-button label="adaptive">自适应</el-radio-button>
</el-radio-group>
</div>
<vue-office-docx
v-if="show"
:src="currentUrl"
:options="options"
@rendered="onRendered"
@error="handleError"
/>
</div>
</template>
<script setup>
import VueOfficeDocx from '@vue-office/docx'
import '@vue-office/docx/lib/index.css'
import {nextTick, ref, watch} from 'vue'
const emit = defineEmits(['error'])
const props = defineProps({
fileUrl: {
type: String,
default: undefined
}
})
const loading = ref(false)
const show = ref(false)
const currentUrl = ref()
const viewMode = ref('standard')
const options = ref({
ignoreWidth: false
})
const onRendered = () => {
loading.value = false
}
const handleError = (error) => {
loading.value = false
emit('error', error)
}
const handleViewModeChange = () => {
options.value.ignoreWidth = viewMode.value === 'adaptive'
reloadView()
}
const reloadView = async () => {
let url = ... 获取新的预览地址
renderFile(url)
}
// 加载文件显示
const renderFile = (url) => {
show.value = false
nextTick(() => {
loading.value = true
currentUrl.value = url
show.value = true
})
}
watch(() => props.fileUrl, (newUrl) => {
if (newUrl) {
renderFile(newUrl)
}
}, {immediate: true})
</script>
<style lang="scss" scoped>
.docx-wrapper-fix-width {
position: relative;
padding: 0 0;
height: 100%;
width: 100%;
overflow: hidden;
:deep(.vue-office-docx-wrapper) {
width: 100% !important;
min-width: fit-content;
max-width: 100%;
overflow: hidden !important;
}
}
.docx-header {
position: fixed;
top: 15px;
left: 50%;
transform: translateX(-50%);
z-index: 100;
}
</style>
2.4 xlxs、csv 预览
ExcelView.vue 官方样例:https://github.com/501351981/vue-office
添加了:xls判断,以及不显示 隐藏sheet页签
<template>
<div class="excel-wrapper" v-loading="loading" element-loading-text="加载中...">
<vue-office-excel
v-if="show"
:src="currentUrl"
:options="excelOptions"
@rendered="onRendered" @error="handleError"
/>
</div>
</template>
<script setup>
import VueOfficeExcel from '@vue-office/excel'
import '@vue-office/excel/lib/index.css'
import {ref, nextTick, watch} from 'vue'
const emit = defineEmits(['error'])
const props = defineProps({
fileUrl: {
type: String,
default: undefined
},
// 是否是xls
xls: {
type: Boolean,
default: false
}
})
const loading = ref(false)
const show = ref(false)
const currentUrl = ref()
const excelOptions = ref({
xls: props.xls,
beforeTransformData(data) {
if (!data._worksheets || data._worksheets.length === 0) {
return data
}
console.log(data._worksheets)
const result = []
for (let i = 0; i < data._worksheets.length; i++) {
const sheet = data._worksheets[i]
console.log(sheet)
// 保留 空 sheet 或 state 为 visible 的 sheet
if (!sheet || sheet.state === 'visible') {
result.push(sheet)
}
}
data._worksheets = result
return data
}
})
const onRendered = () => {
loading.value = false
console.log("渲染完成")
}
const handleError = (error) => {
loading.value = false
emit('error', error)
}
// 加载文件显示
const renderFile = (url) => {
show.value = false
nextTick(() => {
loading.value = true
currentUrl.value = url
show.value = true
})
}
watch(() => props.fileUrl, (newUrl) => {
if (newUrl) {
renderFile(newUrl)
}
}, {immediate: true})
</script>
<style lang="scss" scoped>
.excel-wrapper {
height: 100%;
}
</style>
2.5 pptx 预览
PptxView.vue 官方样例:https://github.com/501351981/vue-office
<template>
<div class="pptx-wrapper" v-loading="loading" element-loading-text="加载中...">
<vue-office-pptx
v-if="show"
:src="currentUrl"
@rendered="onRendered"
@error="handleError"
/>
</div>
</template>
<script setup>
import VueOfficePptx from '@vue-office/pptx'
import {ref, nextTick, watch} from 'vue'
const emit = defineEmits(['error'])
const props = defineProps({
fileUrl: {
type: String,
default: undefined
}
})
const loading = ref(false)
const show = ref(false)
const currentUrl = ref()
const onRendered = () => {
loading.value = false
}
const handleError = (error) => {
loading.value = false
emit('error', error)
}
// 加载文件显示
const renderFile = (url) => {
show.value = false
nextTick(() => {
loading.value = true
currentUrl.value = url
show.value = true
})
}
watch(() => props.fileUrl, (newUrl) => {
if (newUrl) {
renderFile(newUrl)
}
}, {immediate: true})
</script>
<style lang="scss" scoped>
.pptx-wrapper {
height: 100%;
min-width: 350px;
width: 45vw;
margin: 0 auto;
:deep(.pptx-preview-wrapper) {
width: 100% !important;
height: 100% !important;
}
}
</style>
2.6 ofd 预览
OfdView.vue 官方样例:https://github.com/besthqs/bestofdview
<template>
<div class="ofd-view-container" v-loading="loading" element-loading-text="加载中...">
<OfdView
:showOpenFileButton="true"
:ofdLink="currentUrl"
></OfdView>
</div>
</template>
<script setup>
import {OfdView} from "bestofdview";
import "bestofdview/dist/style.css";
import {nextTick, ref, watch} from "vue";
const props = defineProps({
fileUrl: {
type: String,
default: undefined
}
})
const loading = ref(false)
const show = ref(false)
const currentUrl = ref()
// 加载文件显示
const renderFile = (url) => {
show.value = false
nextTick(() => {
loading.value = true
currentUrl.value = url
show.value = true
})
}
watch(() => props.fileUrl, (newUrl) => {
if (newUrl) {
renderFile(newUrl)
}
}, {immediate: true})
</script>
<style scoped>
.ofd-view-container {
width: 100%;
height: 80vh;
}
</style>
2.7 dplayer 预览
VideoPlay.vue 官方样例:https://github.com/DIYgod/DPlayer
添加组件销毁和重新渲染
<template>
<div class="video-wrapper" v-loading="loading" element-loading-text="加载中...">
<div id="dplayer" style="height: 80vh"></div>
</div>
</template>
<script setup>
import {nextTick, onUnmounted, ref, watch} from "vue";
import DPlayer from 'dplayer';
const props = defineProps({
fileUrl: {
type: String,
default: undefined
}
})
const loading = ref(false)
const currentUrl = ref()
let dp = null
const renderFile = (url) => {
nextTick(() => {
// 销毁旧的播放器
if (dp) {
dp.destroy();
dp = null;
}
dp = new DPlayer({
container: document.getElementById('dplayer'),
screenshot: true,
video: {
url: url,
}
});
})
}
watch(() => props.fileUrl, (newUrl) => {
if (newUrl) {
renderFile(newUrl)
}
}, {immediate: true})
onUnmounted(() => {
if (dp) {
dp.destroy();
dp = null;
}
})
</script>
<style lang="scss" scoped>
.video-wrapper {
height: 100%;
}
</style>
2.8 文本预览
- 使用 chardet.detect(Buffer) 识别前10k的文件编码 2)使用浏览器内置的 TextDecoder 做编码转换
<template>
<div class="txt-view">
<div class="txt-toolbar">
<el-select v-model="selectedEncoding" placeholder="选择编码" size="small" style="width: 160px">
<el-option :label="`自动识别${detectedEncoding ? ':' + detectedEncoding : ''}`" value="auto"/>
<el-option v-for="enc in encodingOptions" :key="enc" :label="enc" :value="enc"/>
</el-select>
</div>
<el-scrollbar>
<pre class="txt-content">{{ textContent }}</pre>
</el-scrollbar>
</div>
</template>
<script setup>
import {ref, watch, onMounted} from 'vue'
import axios from 'axios'
import chardet from 'chardet'
const props = defineProps({
fileUrl: {
type: String,
required: true
}
})
const emit = defineEmits(['error'])
const textContent = ref('')
const selectedEncoding = ref('auto')
const detectedEncoding = ref('')
const encodingOptions = ['UTF-8', 'GBK', 'GB2312', 'GB18030', 'BIG5', 'UTF-16']
// chardet 返回的编码名称映射到 TextDecoder 支持的名称
const encodingMap = {
'windows-1252': 'WINDOWS-1252',
'ISO-8859-1': 'ISO-8859-1',
'ascii': 'UTF-8'
}
// 检测编码(只检测前 10KB)
const detectEncoding = (arrayBuffer) => {
return chardet.detect(new Uint8Array(arrayBuffer.slice(0, 10240)))
}
// 加载文本内容
const loadText = async () => {
try {
const response = await axios.get(props.fileUrl, {
responseType: 'arraybuffer'
})
let enc = selectedEncoding.value
if (enc === 'auto') {
enc = detectEncoding(response.data)
detectedEncoding.value = enc
enc = encodingMap[enc] || enc
}
textContent.value = new TextDecoder(enc).decode(response.data)
} catch (error) {
console.error('加载文本文件时出错:', error)
textContent.value = '无法加载文件,请稍后再试。'
emit('error', error)
}
}
// 监听编码变化
watch(selectedEncoding, (newEncoding) => {
if (newEncoding && textContent.value) {
loadText()
}
})
// 监听文件URL变化
watch(() => props.fileUrl, () => {
selectedEncoding.value = 'auto'
loadText()
})
onMounted(() => {
loadText()
})
</script>
<style scoped>
.txt-view {
height: 100%;
display: flex;
flex-direction: column;
}
.txt-toolbar {
padding: 8px 16px;
border-bottom: 1px solid var(--el-border-color-lighter);
display: flex;
justify-content: flex-end;
}
.txt-content {
padding: 0 20px 0 20px;
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
font-family: inherit;
font-size: 14px;
font-weight: 500;
line-height: 1.3;
letter-spacing: 1px;
color: #000;
background: #faf9f7;
}
</style>
三 防止通过预览下载的媒体的方式
# 1 前端改进:
地址是传递的方法:每次预览器访问地址会生成1个新token 预览地址
# 2 后端校验
1)后台对资源预览禁用缓存:防止用户直接新页签打开走缓存直接保存文件
response.setHeader("Cache-Control", "no-store");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
2)校验是通过安全域名,访问本文件
String referer = request.getHeader("Referer");
referer.contains("网站域名")
3)token 合法校验
token 放入 redis,校验存在,访问1次就删除;且校验token用户在登录用户,且token未过期
4)每次访问记录日志,记录访问的ip,用于追溯
更多推荐


所有评论(0)