Vue3 + VueOffice 全家桶进行 多格式文件预览
摘要:vue-office [[1]][1]Vue-Office预览的vue组件库,支持vue2/3。也支持非Vue框架的预览。 准备工作 #docx文档预览组件 npm install @vue-office/docx vue-demi@0.14.6 #excel文档预览组件 npm install @vue-office/excel vue-demi@0.14.6 #pdf文档预览组件 npm install @vue-office/p<!--autointro-->...
vue-office
1Vue-Office预览的vue组件库,支持vue2/3。也支持非Vue框架的预览。
准备工作
#docx文档预览组件
npm install @vue-office/docx vue-demi@0.14.6
#excel文档预览组件
npm install @vue-office/excel vue-demi@0.14.6
#pdf文档预览组件
npm install @vue-office/pdf vue-demi@0.14.6
#pptx文档预览组件
npm install @vue-office/pptx vue-demi@0.14.6
如果是vue2.6版本或以下还需要额外安装 @vue/composition-api
npm install @vue/composition-api
组件代码如下
<template>
<div
class="office-viewer"
:class="{ dragging: isDragging, zoomable: zoomLevel > 1 }"
@wheel="handleWheel"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
@mouseup="handleMouseUp"
@mouseleave="handleMouseLeave"
ref="viewerContainer"
>
<!-- PDF预览 -->
<VueOfficePdf
v-if="trimmedType === 'pdf'"
ref="pdfViewer"
:src="encodedSrc"
:style="{
height: height,
width: width,
}"
@rendered="onRendered"
@error="onError"
/>
<!-- Word文档预览 -->
<VueOfficeDocx
v-else-if="trimmedType === 'word'"
ref="docxViewer"
:src="encodedSrc"
:style="{
height: height,
width: width,
}"
@rendered="onRendered"
@error="onError"
/>
<!-- Excel文档预览 -->
<VueOfficeExcel
v-else-if="trimmedType === 'excel'"
ref="excelViewer"
:src="encodedSrc"
:style="{
height: height,
width: width,
}"
@rendered="onRendered"
@error="onError"
/>
<!-- PPT预览 -->
<VueOfficePptx
v-else-if="trimmedType === 'ppt'"
ref="pptxViewer"
:src="encodedSrc"
:options="pptxOptions"
:style="{
height: height,
width: width,
transformOrigin: 'top left',
}"
@rendered="onPptxRendered"
@error="onPptxError"
/>
<!-- 视频、图片预览 -->
<div class="img_video_box" v-else-if="trimmedType === 'video' || trimmedType === 'img'">
<!-- 视频预览 -->
<video
v-if="trimmedType === 'video'"
:src="encodedSrc"
:style="{ height: height, width: width }"
controls
/>
<!-- 图片预览 -->
<img
v-if="trimmedType === 'img'"
:src="encodedSrc"
:style="{ height: height, width: width / 2 }"
/>
</div>
<!-- 不支持的文件类型 -->
<div v-else class="unsupported-type">
<div class="error-message">
<i class="el-icon-warning"></i>
<p>不支持的文件类型: {{ type }}</p>
<p>支持的格式: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX</p>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-overlay">
<div class="loading-spinner">
<el-icon class="el-icon-loading"><Loading /></el-icon>
<p>文档加载中...</p>
</div>
</div>
<!-- 错误状态 -->
<div v-if="error" class="error-overlay">
<div class="error-message">
<i class="el-icon-warning"></i>
<p>文档加载失败</p>
<p>{{ errorMessage }}</p>
<button @click="retry" class="retry-btn">重试</button>
</div>
</div>
<!-- 缩放控制提示 -->
<div v-if="showZoomTip" class="zoom-tip">
<span>缩放: {{ Math.round(zoomLevel * 100) }}%</span>
<small>Ctrl+滚轮缩放,拖拽移动,双击重置</small>
</div>
<!-- 关闭按钮 -->
<div class="close-btn" @click="emit('close')" title="关闭">
<el-icon><Close /></el-icon>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, readonly, nextTick } from "vue";
//引入VueOfficeDocx组件
import VueOfficeDocx from "@vue-office/docx";
//引入相关样式
import "@vue-office/docx/lib/index.css";
//引入VueOfficeExcel组件
import VueOfficeExcel from "@vue-office/excel";
//引入相关样式
import "@vue-office/excel/lib/index.css";
//引入VueOfficePdf组件
import VueOfficePdf from "@vue-office/pdf";
import VueOfficePptx from "@vue-office/pptx";
// 定义组件属性
const props = defineProps({
// 文件类型: pdf, docx, doc, xlsx, xls, ppt, pptx
type: {
type: String,
required: true,
validator: (value) =>
["pdf", "word", "excel", "ppt", "video", "img"].includes(
value.toLowerCase()
),
},
// 文件源地址
src: {
type: String,
required: true,
},
// 容器高度
height: {
type: String,
default: "100%",
},
// 容器宽度
width: {
type: String,
default: "100%",
},
file: {
type: Object,
default: () => ({
type: '',
src: '',
}),
}
});
// 定义事件
const emit = defineEmits(["rendered", "error", "loading", "close"]);
// 响应式数据
const loading = ref(false);
const error = ref(false);
const errorMessage = ref("");
const zoomLevel = ref(1);
const showZoomTip = ref(false);
const viewerContainer = ref(null);
const pdfViewer = ref(null);
const docxViewer = ref(null);
const excelViewer = ref(null);
const pptxViewer = ref(null);
let zoomTipTimer = null;
// 拖拽相关状态
const isDragging = ref(false);
const dragStart = ref({ x: 0, y: 0 });
const dragOffset = ref({ x: 0, y: 0 });
const lastDragOffset = ref({ x: 0, y: 0 });
// 支持的文件类型
const supportedTypes = ["pdf", "word", "excel", "ppt", "video", "img"];
// PPTX配置选项
const pptxOptions = ref({
// 启用图片渲染
enableImages: true,
// 设置渲染模式
renderMode: "canvas",
// 缩放比例
scale: 1,
// 启用调试模式
debug: true,
});
// 处理去除空格的文件类型
const trimmedType = computed(() => {
return props.type.trim().toLowerCase();
});
// 检查文件类型是否支持
const isSupported = computed(() => {
return supportedTypes.includes(trimmedType.value);
});
// 对src进行URL编码
const encodedSrc = computed(() => {
return props.src;
});
// 文档渲染完成回调
const onRendered = () => {
loading.value = false;
error.value = false;
emit("rendered");
};
// 文档加载错误回调
const onError = (err) => {
loading.value = false;
error.value = true;
errorMessage.value = err.message || "文档加载失败";
emit("error", err);
};
// PPTX文档渲染完成回调
const onPptxRendered = () => {
console.log("PPTX文档渲染完成");
console.log("当前src:", encodedSrc.value);
console.log("文档类型:", trimmedType.value);
loading.value = false;
error.value = false;
emit("rendered");
};
// PPTX文档加载错误回调
const onPptxError = (err) => {
console.error("PPTX文档渲染失败:", err);
console.log("失败时的src:", encodedSrc.value);
console.log("失败时的文档类型:", trimmedType.value);
loading.value = false;
error.value = true;
errorMessage.value = err.message || "PPTX文档加载失败";
emit("error", err);
};
// 重试加载
const retry = () => {
error.value = false;
errorMessage.value = "";
loading.value = true;
};
// 处理鼠标滚轮缩放(需要按住Ctrl键)
const handleWheel = (event) => {
// 只有按住Ctrl键时才进行缩放
if (!event.ctrlKey) {
return;
}
event.preventDefault();
if(props.type == 'img' || props.type == 'video' || props.type == 'other') return
const delta = event.deltaY > 0 ? -0.1 : 0.1;
const newZoomLevel = Math.max(0.5, Math.min(3, zoomLevel.value + delta));
zoomLevel.value = newZoomLevel;
// 显示缩放提示
showZoomTip.value = true
// 清除之前的定时器
if (zoomTipTimer) {
clearTimeout(zoomTipTimer);
}
// 2秒后隐藏提示
zoomTipTimer = setTimeout(() => {
showZoomTip.value = false;
}, 2000);
};
// 双击重置缩放
const handleDoubleClick = () => {
zoomLevel.value = 1;
dragOffset.value = { x: 0, y: 0 };
lastDragOffset.value = { x: 0, y: 0 };
showZoomTip.value = true;
if (zoomTipTimer) {
clearTimeout(zoomTipTimer);
}
zoomTipTimer = setTimeout(() => {
showZoomTip.value = false;
}, 1000);
};
// 重置缩放级别
const resetZoom = () => {
zoomLevel.value = 1;
// 重置拖拽偏移
dragOffset.value = { x: 0, y: 0 };
lastDragOffset.value = { x: 0, y: 0 };
};
// 重置vue-office组件内部滚动位置
const resetOfficeViewerScroll = () => {
// 使用nextTick确保组件已经渲染
nextTick(() => {
// 重置PDF组件滚动位置
if (pdfViewer.value && pdfViewer.value.$el) {
const pdfContainer =
pdfViewer.value.$el.querySelector(".pdf-viewer") || pdfViewer.value.$el;
if (pdfContainer) {
pdfContainer.scrollTop = 0;
}
}
// 重置Word组件滚动位置
if (docxViewer.value && docxViewer.value.$el) {
const docxContainer =
docxViewer.value.$el.querySelector(".docx-viewer") ||
docxViewer.value.$el;
if (docxContainer) {
docxContainer.scrollTop = 0;
}
}
// 重置Excel组件滚动位置
if (excelViewer.value && excelViewer.value.$el) {
const excelContainer =
excelViewer.value.$el.querySelector(".excel-viewer") ||
excelViewer.value.$el;
if (excelContainer) {
excelContainer.scrollTop = 0;
}
}
// 重置PPT组件滚动位置
if (pptxViewer.value && pptxViewer.value.$el) {
const pptxContainer =
pptxViewer.value.$el.querySelector(".pptx-viewer") ||
pptxViewer.value.$el;
if (pptxContainer) {
pptxContainer.scrollTop = 0;
}
}
});
};
// 处理鼠标按下事件(开始拖拽)
const handleMouseDown = (event) => {
if (zoomLevel.value > 1) {
isDragging.value = true;
dragStart.value = {
x: event.clientX - dragOffset.value.x,
y: event.clientY - dragOffset.value.y,
};
event.preventDefault();
}
};
// 处理鼠标移动事件(拖拽中)
const handleMouseMove = (event) => {
if (isDragging.value && zoomLevel.value > 1) {
dragOffset.value = {
x: event.clientX - dragStart.value.x,
y: event.clientY - dragStart.value.y,
};
event.preventDefault();
}
};
// 处理鼠标释放事件(结束拖拽)
const handleMouseUp = () => {
if (isDragging.value) {
isDragging.value = false;
lastDragOffset.value = { ...dragOffset.value };
}
};
// 处理鼠标离开事件
const handleMouseLeave = () => {
if (isDragging.value) {
isDragging.value = false;
lastDragOffset.value = { ...dragOffset.value };
}
};
// 监听src变化,重新加载文档
watch(
() => props.src,
(newSrc) => {
if (newSrc) {
loading.value = true;
error.value = false;
errorMessage.value = "";
// 重置滚动位置到顶部
if (viewerContainer.value) {
viewerContainer.value.scrollTop = 0;
}
// 重置vue-office组件内部滚动位置
resetOfficeViewerScroll();
}
},
{ immediate: true }
);
// 监听type变化
watch(
() => props.type,
(newType) => {
if (newType && props.src) {
loading.value = true;
error.value = false;
errorMessage.value = "";
}
}
);
// 组件挂载时初始化
onMounted(() => {
if (props.src && isSupported.value) {
loading.value = true;
} else {
loading.value = false;
}
if(props.type == 'img' || props.type == 'video'){
nextTick(() => {
loading.value = false
})
}
// 添加双击事件监听
if (viewerContainer.value) {
viewerContainer.value.addEventListener("dblclick", handleDoubleClick);
}
});
// 暴露方法供外部调用
defineExpose({
resetZoom,
resetOfficeViewerScroll,
zoomLevel: readonly(zoomLevel),
handleMouseDown,
handleMouseMove,
handleMouseUp,
handleMouseLeave,
onPptxRendered,
onPptxError,
pptxOptions,
});
const getTransform = computed(() => {
if(props.type == 'excel' || props.type == 'ppt') {
zoomLevel.value = 0.8
}
return `scale(${zoomLevel.value}) translate(${dragOffset.value.x}px, ${dragOffset.value.y}px)`
})
</script>
<style lang="scss" scoped>
.office-viewer {
position: relative;
width: 100%;
height: 100%;
background: transparent;
border-radius: 4px;
overflow: hidden;
box-sizing: border-box;
cursor: grab;
user-select: none;
overflow-y: auto;
scrollbar-width: none;
&:active {
cursor: grabbing;
}
&.dragging {
cursor: grabbing;
}
&.zoomable {
cursor: grab;
&:hover {
cursor: grab;
}
}
&:not(.zoomable) {
cursor: default;
}
// 确保所有子元素都使用border-box
* {
box-sizing: border-box;
}
.img_video_box{
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: rgba(0, 0, 0, .5);
}
// 缩放提示样式
.zoom-tip {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
z-index: 1001;
pointer-events: none;
transition: opacity 0.3s ease;
span {
display: block;
font-weight: 500;
margin-bottom: 2px;
}
small {
opacity: 0.8;
font-size: 10px;
}
}
// 加载状态样式
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent !important;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.loading-spinner {
text-align: center;
color: #409eff;
i {
font-size: 32px;
margin-bottom: 10px;
animation: rotate 2s linear infinite;
}
p {
margin: 0;
font-size: 14px;
letter-spacing: 5px;
color: #f7f8fb;
}
}
}
// 错误状态样式
.error-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: transparent !important;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
.error-message {
text-align: center;
color: #f56c6c;
i {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
margin: 8px 0;
font-size: 14px;
color: #f7f8fb;
&:first-of-type {
font-size: 16px;
font-weight: 500;
color: #f56c6c;
}
}
.retry-btn {
margin-top: 16px;
padding: 8px 16px;
background: #409eff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s;
&:hover {
background: #66b1ff;
}
}
}
}
// 不支持的文件类型样式
.unsupported-type {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, .5);
.error-message {
text-align: center;
color: #e6a23c;
i {
font-size: 48px;
margin-bottom: 16px;
display: block;
}
p {
margin: 8px 0;
font-size: 14px;
color: rgb(241, 241, 241, .7);
&:first-of-type {
font-size: 16px;
font-weight: 500;
color: #e6a23c;
}
}
}
}
.close-btn{
position: absolute;
top: 20px;
left: 20px;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
color: white;
border-radius: 50%;
font-size: 20px;
z-index: 1001;
transition: all 0.2s linear;
cursor: pointer;
}
.close-btn:hover{
background: rgba(0, 0, 0, 0.3);
}
}
// 旋转动画
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
// pdf
::v-deep .vue-office-pdf-wrapper{
background: transparent !important;
scrollbar-width: none !important;
padding: 20px !important;
transform: v-bind(getTransform);
}
:deep(.vue-office-pdf) {
background: rgba(0, 0, 0, .5) !important;
}
// excel
:deep(.vue-office-excel) {
padding: 20px !important;
background: rgba(0, 0, 0, .5) !important;
scrollbar-width: none !important;
}
:deep(.vue-office-excel-main){
width: 100% !important;
height: 100% !important;
overflow: auto !important;
transform: v-bind(getTransform);
}
// word
:deep(.vue-office-docx) {
width: 100% !important;
height: 100% !important;
background: rgba(0, 0, 0, .5) !important;
}
:deep(.docx-wrapper) {
background: transparent !important;
padding: 20px !important;
transform: v-bind(getTransform);
}
// ppt
::v-deep .vue-office-pptx{
scrollbar-width: none !important;
padding: 20px !important;
scrollbar-width: none !important;
background: rgba(0, 0, 0, .5) !important;
}
:deep(.pptx-preview-wrapper){
transform: v-bind(getTransform);
background: transparent !important;
}
</style>
本文链接:https://blog.smallhao.fun/?id=45 转载需授权!
Chen’Blog版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!