首页前端组件库Vue3 + VueOffice 全家桶进行 多格式文件预览

Vue3 + VueOffice 全家桶进行 多格式文件预览

分类前端组件库时间2025-11-25 15:04:43发布RustStream浏览332
摘要: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版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

从文件名中提取文件类型后缀 Videojs 使用 Hls 对视频加密解码

游客 回复需填写必要信息
召唤伊斯特瓦尔