说明
文件预览一直是前端很麻烦但是又必须要实现的一个功能,我根据工作中的经验,整合了常见类型文件的预览功能实现,并整合在一起
环境
json
"dependencies": {
"@google/model-viewer": "^4.0.0",
"@videojs-player/vue": "^1.0.0",
"axios": "^1.8.4",
"babylonjs": "^7.54.0",
"babylonjs-loaders": "^7.54.0",
"cesium": "^1.127.0",
"dxf-viewer": "^1.0.42",
"file-type": "^20.4.1",
"mavon-editor": "^3.0.2",
"naive-ui": "^2.41.0",
"pangju-utils": "^2.3.2",
"three": "^0.169.0",
"video.js": "^7.21.6",
"vue": "^3.5.13",
"vue-json-viewer": "^3.0.4",
"vue-router": "^4.5.0"
}
- @google/model-viewer 谷歌模型预览器(只支持glb)
- @videojs-player/vue 视频播放器(videojs vue3实现)
- babylonjs 3d模型库(微软出品的)
- babylonjs-loaders 模型加载器(用于支持各种模型文件类型)
- cesium 3维和2维库(主要用来加载kml)
- dxf-viewer dxf预览器(dwg的兄弟类型)
- file-type 文件类型解析器(解析文件mime-type)
- mavon-editor markdown编辑器(用于md文件预览)
- naive-ui vue3 ui 库(我个人喜欢的一个ui库)
- pangju-utils 自己写的工具库,没写文档(目前是我自己用的,后边有空会完善文档)
- axios axios(http请求库)
- three threejs(@google/model-viewer、dxf-viewer 依赖)
- video.js videojs(@videojs-player/vue 依赖)
- vue-json-viewer json文件查看器
代码仓库
- Github github.com/pangju666/f...
- Gitee gitee.com/pangju666/f...
实现方法
音频(Audio)
支持类型
- audio/mpeg(mp3)
- audio/wav(wav)
- audio/ogg(ogg)
- audio/x-aac(acc)
- audio/x-flac(flac)
实现原理
使用原生audio标签实现
html
<script setup>
defineProps({
fileUrl: {
type: String,
required: true,
},
});
defineEmits(["finished"]);
</script>
<template>
<div class="audio-viewer">
<audio
controls
autoplay
:src="fileUrl"
@loadeddata="$emit('finished')"
></audio>
</div>
</template>
<style scoped lang="less">
.audio-viewer {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
</style>
视频(Video)
支持类型
- video/mp4(mp4)
- video/ogg(ogg)
- video/webm(webm)
实现原理
使用VideoPlayer组件实现
html
<script setup>
import { VideoPlayer } from "@videojs-player/vue";
import videojs from "video.js";
import "video.js/dist/video-js.css";
videojs.addLanguage("zh-CN", {
Play: "播放",
Pause: "暂停",
"Current Time": "当前时间",
Duration: "时长",
"Remaining Time": "剩余时间",
"Stream Type": "媒体流类型",
LIVE: "直播",
Loaded: "加载完成",
Progress: "进度",
Fullscreen: "全屏",
"Non-Fullscreen": "退出全屏",
"Picture-in-Picture": "画中画",
"Exit Picture-in-Picture": "退出画中画",
Mute: "静音",
Unmute: "开启音效",
"Playback Rate": "播放速度",
Subtitles: "字幕",
"subtitles off": "关闭字幕",
Captions: "内嵌字幕",
"captions off": "关闭内嵌字幕",
Chapters: "节目段落",
"Close Modal Dialog": "关闭弹窗",
Descriptions: "描述",
"descriptions off": "关闭描述",
"Audio Track": "音轨",
"You aborted the media playback": "视频播放被终止",
"A network error caused the media download to fail part-way.":
"网络错误导致视频下载中途失败。",
"The media could not be loaded, either because the server or network failed or because the format is not supported.":
"视频因格式不支持或者服务器或网络的问题无法加载。",
"The media playback was aborted due to a corruption problem or because the media used features your browser did not support.":
"由于视频文件损坏或是该视频使用了你的浏览器不支持的功能,播放终止。",
"No compatible source was found for this media.":
"无法找到此视频兼容的源。",
"The media is encrypted and we do not have the keys to decrypt it.":
"视频已加密,无法解密。",
"Play Video": "播放视频",
Close: "关闭",
"Modal Window": "弹窗",
"This is a modal window": "这是一个弹窗",
"This modal can be closed by pressing the Escape key or activating the close button.":
"可以按ESC按键或启用关闭按钮来关闭此弹窗。",
", opens captions settings dialog": ", 开启标题设置弹窗",
", opens subtitles settings dialog": ", 开启字幕设置弹窗",
", opens descriptions settings dialog": ", 开启描述设置弹窗",
", selected": ", 选择",
"captions settings": "字幕设定",
"Audio Player": "音频播放器",
"Video Player": "视频播放器",
Replay: "重新播放",
"Progress Bar": "进度条",
"Volume Level": "音量",
"subtitles settings": "字幕设定",
"descriptions settings": "描述设定",
Text: "文字",
White: "白",
Black: "黑",
Red: "红",
Green: "绿",
Blue: "蓝",
Yellow: "黄",
Magenta: "紫红",
Cyan: "青",
Background: "背景",
Window: "窗口",
Transparent: "透明",
"Semi-Transparent": "半透明",
Opaque: "不透明",
"Font Size": "字体尺寸",
"Text Edge Style": "字体边缘样式",
None: "无",
Raised: "浮雕",
Depressed: "压低",
Uniform: "均匀",
Dropshadow: "下阴影",
"Font Family": "字体库",
"Proportional Sans-Serif": "比例无细体",
"Monospace Sans-Serif": "单间隔无细体",
"Proportional Serif": "比例细体",
"Monospace Serif": "单间隔细体",
Casual: "舒适",
Script: "手写体",
"Small Caps": "小型大写字体",
Reset: "重置",
"restore all settings to the default values": "恢复全部设定至预设值",
Done: "完成",
"Caption Settings Dialog": "字幕设定窗口",
"Beginning of dialog window. Escape will cancel and close the window.":
"打开对话窗口。Escape键将取消并关闭对话窗口",
"End of dialog window.": "结束对话窗口",
"Seek to live, currently behind live": "尝试直播,当前为延时播放",
"Seek to live, currently playing live": "尝试直播,当前为实时播放",
"progress bar timing: currentTime={1} duration={2}": "{1}/{2}",
"{1} is loading.": "正在加载 {1}。",
"No content": "无内容",
});
defineProps({
fileUrl: {
type: String,
required: true,
},
});
defineEmits(["finished"]);
</script>
<template>
<div class="video-viewer">
<video-player
:src="fileUrl"
playsinline
controls
class="video-player vjs-big-play-centered"
language="zh-CN"
@mounted="$emit('finished')"
/>
</div>
</template>
<style scoped lang="less">
.video-viewer {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
:deep(.video-player) {
width: 100%;
height: 100%;
}
}
</style>
图片(Image)
支持类型
image/jpeg(jpg、jpeg) image/png(png) image/gif(git) image/webp(webp) image/svg+xml(svg)
实现原理
使用NImage组件实现
html
<script setup>
defineProps({
fileUrl: {
type: String,
required: true,
},
});
const emits = defineEmits(["finished"]);
</script>
<template>
<div class="image-viewer">
<n-image
:src="fileUrl"
object-fit="scale-down"
:on-load="emits('finished')"
/>
</div>
</template>
<style scoped lang="less">
.image-viewer {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
</style>
DXF
支持类型
image/vnd.dxf(dxf)
实现原理
使用DxfViewer组件实现
html
<script setup>
import { ref, toRaw, onMounted } from "vue";
import { DxfViewer } from "dxf-viewer";
import * as THREE from "three";
import SimFangFont from "@/assets/fonts/simfang.ttf";
const props = defineProps({
fileUrl: {
type: String,
required: true,
},
});
const emits = defineEmits(["finished"]);
const dxfContainerRef = ref();
const showAll = ref(true);
const dxfViewer = ref(null);
const layers = ref([]);
onMounted(() => {
dxfViewer.value = new DxfViewer(dxfContainerRef.value, {
clearColor: new THREE.Color("white"),
autoResize: true,
colorCorrection: true,
// 一般是用gbk,也可以换成别的,比如:utf-8
fileEncoding: "gbk",
sceneOptions: {
wireframeMesh: true,
},
});
for (const eventName of ["loaded"]) {
dxfViewer.value.Subscribe(eventName, () => {
const dxfLayers = dxfViewer.value.GetLayers();
dxfLayers.forEach((lyr) => (lyr["isVisible"] = true));
layers.value = dxfLayers;
});
}
toRaw(dxfViewer.value)
.Load({
url: props.fileUrl,
fonts: [SimFangFont],
})
.then(() => emits("finished"));
});
const onToggleLayer = (layer, newState) => {
layer.isVisible = newState;
toRaw(dxfViewer.value).ShowLayer(layer.name, newState);
};
const onToggleAll = (newState) => {
showAll.value = newState;
for (const layer of layers.value) {
if (layer.isVisible !== newState) {
onToggleLayer(layer, newState);
}
}
};
</script>
<template>
<div class="dxf-viewer">
<n-split
direction="horizontal"
:default-size="0.9"
:max="0.9"
:min="0.1"
>
<template #1>
<div ref="dxfContainerRef" class="dxf-container"></div>
</template>
<template #2>
<div class="layer-list">
<n-scrollbar>
<div
v-show="layers.length > 0"
class="mb-10 display-flex"
>
<n-checkbox
v-model:checked="showAll"
label="全部图层"
@update:checked="onToggleAll"
/>
</div>
<n-flex vertical align="start">
<div v-for="layer in layers" :key="layer.name">
<n-checkbox
v-model:checked="layer.isVisible"
@update:checked="
(e) => onToggleLayer(layer, e)
"
>
<n-ellipsis>
{{ layer.displayName }}
</n-ellipsis>
</n-checkbox>
</div>
</n-flex>
</n-scrollbar>
</div>
</template>
</n-split>
</div>
</template>
<style lang="less" scoped>
.dxf-viewer {
width: 100%;
height: 100%;
.layer-list {
padding: 10px;
}
.dxf-container {
width: 100%;
height: 100%;
}
}
</style>
JSON
支持类型
- application/json(json)
- application/json;charset=utf-8(json)
实现原理
使用vue-json-viewer组件实现
html
<script setup>
import { onMounted, ref } from "vue";
import axios from "axios";
import JsonViewer from "vue-json-viewer";
import "vue-json-viewer/style.css";
const props = defineProps({
fileUrl: {
type: String,
required: true,
},
});
const emits = defineEmits(["finished"]);
const content = ref(null);
onMounted(async () => {
content.value = (
await axios.get(props.fileUrl, {
responseType: "json",
})
).data;
emits("finished");
});
</script>
<template>
<div class="json-viewer">
<json-viewer
:value="content"
copyable
:expand-depth="10"
expanded
show-double-quotes
show-array-index
/>
</div>
</template>
<style scoped lang="less">
.json-viewer {
width: 100%;
height: 100%;
text-align: left;
}
</style>
Markdown
支持类型
- text/x-web-markdown(md)
实现原理
使用mavonEditor组件实现
html
<script setup>
import { mavonEditor } from "mavon-editor";
import { onMounted, ref } from "vue";
import axios from "axios";
import "mavon-editor/dist/css/index.css";
const props = defineProps({
fileUrl: {
type: String,
required: true,
},
});
const emits = defineEmits(["finished"]);
const content = ref(null);
onMounted(async () => {
content.value = (
await axios.get(props.fileUrl, {
responseType: "text",
})
).data;
emits("finished");
});
</script>
<template>
<div class="markdown-viewer">
<mavon-editor
v-model="content"
:subfield="false"
default-open="preview"
:editable="false"
:toolbars-flag="false"
/>
</div>
</template>
<style scoped lang="less">
.markdown-viewer {
width: 100%;
height: 100%;
}
</style>
纯文本
支持类型
- text/plain(txt)
- ...
实现原理
使用pre标签实现
html
<script setup>
import { onMounted, ref } from "vue";
import axios from "axios";
const props = defineProps({
fileUrl: {
type: String,
required: true,
},
});
const emits = defineEmits(["finished"]);
const content = ref(null);
onMounted(async () => {
content.value = (
await axios.get(props.fileUrl, {
responseType: "text",
})
).data;
emits("finished");
});
</script>
<template>
<div class="plain-text-viewer">
<pre>{{ content }}</pre>
</div>
</template>
<style scoped lang="less">
.plain-text-viewer {
padding: 10px;
width: 100%;
height: 100%;
> pre {
font-size: 16px;
text-align: left;
}
}
</style>
KML
支持类型
- application/vnd.google-earth.kml+xml(kml)
实现原理
使用Cesium库实现
html
<script setup>
import { Ellipsoid, Ion, KmlDataSource, Viewer } from "cesium";
import { onMounted } from "vue";
import "cesium/Build/Cesium/Widgets/widgets.css";
const props = defineProps({
fileUrl: {
type: String,
required: true,
},
});
const emits = defineEmits(["finished"]);
onMounted(() => {
Ion.defaultAccessToken = import.meta.env.VITE_CESIUM_TOKEN;
const viewer = new Viewer("cesium-container", {
animation: false, //左下角的动画仪表盘
baseLayerPicker: false, //右上角的图层选择按钮
homeButton: false, //home按钮
sceneModePicker: false, //模式切换按钮
timeline: false, //底部的时间轴
geocoder: false,
navigationHelpButton: false, //右上角的帮助按钮,
fullscreenButton: false, //右下角的全屏按钮
selectionIndicator: false, //原生自带绿色选择框,双击显示的绿框
infoBox: false, //点击要素之后显示的信息窗口
//sceneMode: SceneMode.SCENE2D,
ellipsoid: Ellipsoid.WGS84,
});
viewer.dataSources
.add(
KmlDataSource.load(props.fileUrl, {
camera: viewer.scene.camera,
canvas: viewer.scene.canvas,
screenOverlayContainer: viewer.container,
}),
)
.then((dataSource) => {
viewer.flyTo(dataSource.entities);
});
emits("finished");
});
</script>
<template>
<div id="cesium-container" class="kml-viewer"></div>
</template>
<!--suppress Stylelint -->
<style scoped lang="less">
.kml-viewer {
width: 100%;
height: 100%;
:deep(.cesium-credit-logoContainer) {
display: none !important;
}
}
</style>
注意事项
需要将.env文件中的VITE_CESIUM_TOKEN 值,配置为自己Cesium账号的TOKEN
3D模型
支持类型
- model/obj(obj)
- model/gltf-binary(glb)
- model/x.stl-binary(stl)
实现原理
使用 babylonjs 库和 @google/model-viewer 组件实现
html
<script setup>
import { onMounted, nextTick, ref } from "vue";
import "@google/model-viewer";
import * as BABYLON from "babylonjs";
import "babylonjs-loaders";
const props = defineProps({
fileUrl: {
type: String,
required: true,
},
mimeType: {
type: String,
required: true,
},
filename: {
type: String,
required: true,
},
});
const emits = defineEmits(["finished"]);
const canvasRef = ref();
onMounted(() => {
if (props.mimeType === "model/gltf-binary") {
emits("finished");
} else {
nextTick(async () => {
// 获取画布元素
const canvas = canvasRef.value;
// 创建引擎
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
// 创建相机
const camera = new BABYLON.ArcRotateCamera(
"camera1",
Math.PI / 2,
Math.PI / 2,
5,
BABYLON.Vector3.Zero(),
scene,
);
camera.attachControl(canvas, true);
// 创建光源
const light = new BABYLON.HemisphericLight(
"light1",
BABYLON.Vector3.Up(),
scene,
);
light.intensity = 0.7;
try {
// 加载模型
const result = await BABYLON.SceneLoader.ImportMeshAsync(
"",
props.fileUrl, // 替换为模型路径
props.filename, // 替换为模型文件名
scene,
);
// 模型加载完成后的操作
const meshes = result.meshes;
// 合并所有网格的边界框
let minimum = new BABYLON.Vector3(
Number.MAX_VALUE,
Number.MAX_VALUE,
Number.MAX_VALUE,
);
let maximum = new BABYLON.Vector3(
-Number.MAX_VALUE,
-Number.MAX_VALUE,
-Number.MAX_VALUE,
);
meshes.forEach((mesh) => {
const boundingBox = mesh.getBoundingInfo().boundingBox;
minimum = BABYLON.Vector3.Minimize(
minimum,
boundingBox.minimumWorld,
);
maximum = BABYLON.Vector3.Maximize(
maximum,
boundingBox.maximumWorld,
);
});
// 计算模型的中心和大小
const center = minimum.add(maximum).scale(0.5);
const size = maximum.subtract(minimum);
const maxSize = Math.max(size.x, size.y, size.z);
// 设置相机目标和距离
camera.target = center;
camera.radius = maxSize * 2; // 调整相机的距离
// 设置相机裁剪平面
camera.minZ = maxSize / 100; // 根据模型大小动态调整
camera.maxZ = maxSize * 10;
// 启动渲染循环
engine.runRenderLoop(() => {
scene.render();
});
emits("finished");
} catch (error) {
console.error("模型加载失败:", error);
}
// 窗口尺寸变化时调整画布大小
window.addEventListener("resize", () => {
engine.resize();
});
});
}
});
</script>
<template>
<model-viewer
v-if="mimeType === 'model/gltf-binary'"
class="model-viewer"
:src="fileUrl"
shadow-intensity="1"
camera-controls
></model-viewer>
<div v-else class="model-viewer">
<canvas ref="canvasRef" style="width: 100%; height: 100vh"></canvas>
</div>
</template>
<style scoped lang="less">
.model-viewer {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
</style>
注意事项
如果为obj模型,则文件地址必须为根路径,如:xxxxx.com/{文件md5}
OFFICE
支持类型
- application/vnd.openxmlformats-officedocument.wordprocessingml.document(docx)
- application/vnd.openxmlformats-officedocument.spreadsheetml.sheet(xlsx)
- application/vnd.openxmlformats-officedocument.presentationml.presentation(pptx)
- application/msword(doc)
- application/vnd.ms-excel(xls)
- application/vnd.ms-powerpoint(ppt)
实现原理
使用only-office 库或微软提供的在线预览服务实现
html
<script setup>
import { onMounted } from "vue";
import { RandomStringUtils, StringUtils } from "pangju-utils";
const props = defineProps({
fileUrl: {
type: String,
required: true,
},
filename: {
type: String,
required: true,
},
mimeType: {
type: String,
required: true,
},
});
const emits = defineEmits(["finished"]);
onMounted(async () => {
if (!window.DocsAPI) {
location.href = `${
import.meta.env.VITE_OFFICE_VIEW_URL
}?src=${encodeURIComponent(props.fileUrl)}`;
} else {
// 配置文档查看器
const baseName = StringUtils.substringBeforeLast(props.filename, ".");
const extension = StringUtils.substringAfterLast(props.filename, ".");
let documentType = null;
switch (props.mimeType) {
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
case "application/msword":
documentType = "word";
break;
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
case "application/vnd.ms-excel":
documentType = "cell";
break;
case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
case "application/vnd.ms-powerpoint":
documentType = "slide";
break;
}
// eslint-disable-next-line no-undef
window.docEditor = new DocsAPI.DocEditor("only-office-container", {
documentType: documentType,
type: "desktop",
height: "100%",
width: "100%",
document: {
fileType: extension,
key: RandomStringUtils.randomLetter(10),
title: baseName,
url: props.fileUrl,
permissions: {
copy: true,
download: true,
print: true,
},
},
editorConfig: {
customization: {
anonymous: {
request: false,
},
chat: false,
comments: false,
},
lang: "zh",
mode: "view",
},
});
}
emits("finished");
});
</script>
<template>
<div id="only-office-container"></div>
</template>
注意事项
如果使用onlyoffice,则需要在index.html中添加
html
<script type="text/javascript" src="http://xxxx/web-apps/apps/api/documents/api.js">
总结
后边我会更新更多的文件类型预览实现,如果有错误请在评论区指正,谢谢!