Vue3实战Easy云盘(四):使用空间+文件预览+文件分享+文件下载

news2024/11/19 17:39:50

一、空间使用

Framework.vue中

(1)引入接口

const api = {
    getUseSpace: "/getUseSpace",
    logout: "/logout",
};

(2)回调

// 使用空间
const useSpaceInfo = ref({ useSpace: 0, totalSpace: 1 });
const getUseSpace = async () => {
    // 存储请求信息
    let result = await proxy.Request({
        // 请求路径
        url: api.getUseSpace,
        // 不显示加载
        showLoading: false,
    });
    if (!result) {
        return;
    }
    // 把请求到的信息存到使用空间
    useSpaceInfo.value = result.data;
};
// 调用
getUseSpace();

上传文件结束后,更新使用空间:

// 上传文件回调
const uploadCallbackHandler = () => {
    nextTick(() => {
...........................
        // 并最后调用一个函数来获取空间使用情况。
        getUseSpace();
    });
};

(3)结构中使用

 <!-- 下方空间使用 -->
                    <div class="space-info">
                        <div>空间使用</div>
                        <div class="percent">
                            <!-- 占用空间进度条 -->
                            <!-- 结果除以 100 是为了将前面乘以的 10000 还原为百分比形式 -->
                            <el-progress
                                :percentage="Math.floor(
                                (useSpaceInfo.useSpace / useSpaceInfo.totalSpace) * 10000
                                )/100"
                                color="#409eff"/>
                        </div>
                        <!-- 文字说明和图标 -->
                        <div class="space-use">
                            <div class="use">
                               {{ proxy.Utils.size2Str(useSpaceInfo.useSpace) }}/
                               {{ proxy.Utils.size2Str(useSpaceInfo.totalSpace) }}
                            </div>
                            <div class="iconfont icon-refresh" @click="getUseSpace"></div>
                        </div>
                    </div>

效果:

二、文件预览(难点)

1.封装Preview.vue组件

  • 根据文件分类类型传入不同的width给Window.vue组件,以决定展示的宽度,而Window.vue组件中,又使用计算属性根据此传递的width值与当前窗口宽度作比较(width值不允许超过当前窗口宽度),返回作为windowContentWidth
  • showPreview方法( 入口)暴露给父组件Main.vue调用(其实,要展示弹框有2种方法,要么在父组件中定义一个响应式数据,然后以prop传给子组件,子组件根据此响应式数据作出对应展示。要么子组件暴露一个方法给外界调用,让外界通过此方法传入数据。很显然,这里用的是第二种方式)
  • 如何展示多种不同类型的文件?Main组件中使用Preview组件,调用<Preview ref=“previewRef”>组件的previewRef.value.showPreview(row, 0)方法,将文件数据传递了过去,并且指定url使用FILE_URL_MAP[0],然后,在<Preview>组件中,根据文件数据中的文件类型使用不同的组件作展示(不然,所有的根据文件类型展示不同的组件,都要写在Main.vue组件中,那这样的话,Main.vue组件就过于复杂了)
  • <Preview>组件用到了Window.vue(用于模拟弹窗)配合展示不同文件类型的组件(不包括图片类型,PreviewXXX组件) 和 <el-image-viewer>组件(专门展示图片)
  • 不同文件类型请求路径(后端处理这些请求的详细代码在上面已贴出)

非视频文件类型文件 预览的url
0 - fileUrl: "/file/getFile"
1 - fileUrl: "/admin/getFile"
2 - fileUrl: "/showShare/getFile"
视频文件类型文件的url取
0 - videoUrl: /file/ts/getVideoInfo"
1 - videoUrl: /admin/ts/getVideoInfo"
2 - videoUrl: /showShare/ts/getVideoInfo"

components/preview/Preview.vue

<template>
  <PreviewImage
    ref="imageViewerRef"
    :imageList="[imageUrl]"
    v-if="fileInfo.fileCategory == 3"
  ></PreviewImage>
  <Window
    :show="windowShow"
    @close="closeWindow"
    :width="fileInfo.fileCategory == 1 ? 1300 : 900"
    :title="fileInfo.fileName"
    :align="fileInfo.fileCategory == 1 ? 'center' : 'top'"
    v-else
  >
    <!--  `file_type` 1:视频 2:音频  3:图片 4:pdf 5:doc 6:excel 7:txt 8:code 9:zip 10:其他', -->
    <PreviewVideo :url="url" v-if="fileInfo.fileCategory == 1"></PreviewVideo>
    <PreviewExcel :url="url" v-if="fileInfo.fileType == 6"></PreviewExcel>
    <PreviewDoc :url="url" v-if="fileInfo.fileType == 5"></PreviewDoc>
    <PreviewPdf :url="url" v-if="fileInfo.fileType == 4"></PreviewPdf>
    <PreviewTxt
      :url="url"
      v-if="fileInfo.fileType == 7 || fileInfo.fileType == 8"
    ></PreviewTxt>
    <PreviewMusic
      :url="url"
      v-if="fileInfo.fileCategory == 2"
      :fileName="fileInfo.fileName"
    ></PreviewMusic>
    <PreviewDownload
      :createDownloadUrl="createDownloadUrl"
      :downloadUrl="downloadUrl"
      :fileInfo="fileInfo"
      v-if="fileInfo.fileCategory == 5 && fileInfo.fileType != 8"
    ></PreviewDownload>
  </Window>
</template>

<script setup>
// @ 符号表示一个特定路径的别称,这个设置可以在 build/webpack.base.conf.js中设置
import PreviewImage from "@/components/preview/PreviewImage.vue";
import PreviewVideo from "@/components/preview/PreviewVideo.vue";
import PreviewDoc from "@/components/preview/PreviewDoc.vue";
import PreviewExcel from "@/components/preview/PreviewExcel.vue";
import PreviewPdf from "@/components/preview/PreviewPdf.vue";
import PreviewTxt from "@/components/preview/PreviewTxt.vue";
import PreviewMusic from "@/components/preview/PreviewMusic.vue";
import PreviewDownload from "@/components/preview/PreviewDownload.vue";

import { ref, reactive, getCurrentInstance, nextTick, computed } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();

// 计算图片地址(缩略图原图)
const imageUrl = computed(() => {
  return (
    // 区分缩略图和原图
    proxy.globalInfo.imageUrl + fileInfo.value.fileCover.replaceAll("_.", ".")
  );
});

// 默认不展示
const windowShow = ref(false);
// 关闭方法
const closeWindow = () => {
  windowShow.value = false;
};

// 定义接口地址
const FILE_URL_MAP = {
  0: {
    fileUrl: "/file/getFile",
    videoUrl: "/file/ts/getVideoInfo",
    createDownloadUrl: "/file/createDownloadUrl",
    downloadUrl: "/api/file/download",
  },
  1: {
    fileUrl: "/admin/getFile",
    videoUrl: "/admin/ts/getVideoInfo",
    createDownloadUrl: "/admin/createDownloadUrl",
    downloadUrl: "/api/admin/download",
  },
  2: {
    fileUrl: "/showShare/getFile",
    videoUrl: "/showShare/ts/getVideoInfo",
    createDownloadUrl: "/showShare/createDownloadUrl",
    downloadUrl: "/api/showShare/download",
  },
};

// 文件信息
const fileInfo = ref({});
// 视频文件地址
const url = ref(null);

const imageViewerRef = ref();

// 下载地址
const createDownloadUrl = ref(null);
const downloadUrl = ref(null);
// 各种类型预览实现
const showPreview = (data, showPart) => {
  fileInfo.value = data;
  // `file_category`  '1:视频 2:音频  3:图片 4:文档 5:其他',
  // 图片
  if (data.fileCategory == 3) {
    nextTick(() => {
      // 图片预览展示
      imageViewerRef.value.show(0);
    });
  } else {
    // 如果是图片之外的类型,就通过window组件去展示
    windowShow.value = true;
    let _url = FILE_URL_MAP[showPart].fileUrl;
    // 视频地址单独处理
    if (data.fileCategory == 1) {
      _url = FILE_URL_MAP[showPart].videoUrl;
    }
    // 文件下载
    let _createDownloadUrl = FILE_URL_MAP[showPart].createDownloadUrl;
    let _downloadUrl = FILE_URL_MAP[showPart].downloadUrl;

    if (showPart == 0) {
      _url = _url + "/" + data.fileId;
      _createDownloadUrl = _createDownloadUrl + "/" + data.fileId;
    } else if (showPart == 1) {
      _url = _url + "/" + data.uerId + "/" + data.fileId;
      _createDownloadUrl =
        _createDownloadUrl + "/" + data.uerId + "/" + data.fileId;
    } else if (showPart == 2) {
      _url = _url + "/" + data.shareId + "/" + data.fileId;
      _createDownloadUrl =
        _createDownloadUrl + "/" + data.shareId + "/" + data.fileId;
    }
    url.value = _url;
    createDownloadUrl.value = _createDownloadUrl;
    downloadUrl.value = _downloadUrl;
  }
};
// 将此方法暴露出去
defineExpose({ showPreview });
</script>

<style lang="scss" scoped>
</style>

Main.vue 

// 预览
const previewRef = ref();
const preview = (data) => {
    // 如果是文件夹(目录)
    if (data.folderType == 1) {
        // 就调用Navigation组件中的openFolder(打开文件夹)方法,实现预览
        navigationRef.value.openFolder(data);
        return;
    }
    // 如果是文件
    if (data.status != 2) {
        proxy.Message.warning("文件未完成转码,无法预览");
        return;
    }
    // 展示,传入文件类型data,和默认不展示0
    previewRef.value.showPreview(data, 0);
};

2.封装window组件

  • 相当于手动封装一个弹框组件
  • 使用window.innerWidth获取当前窗口宽度作为响应式数据windowWidth的初始值,并使用计算属性绑定给style,并且监听窗口大小变化事件(window.addEventListener('resize',handler),其中handler去修改计算属性中使用的windowWidth响应式数据的值),以此达到此弹框的宽度永远最大不能超过当前窗口的宽度(即使弹框指定的宽度大于当前窗口宽度),并且当窗口变化时,Window组件的宽度能随着窗口变化而变化(最大不超过当前窗口宽度)。
  • 使用计算属性,计算Window组件内容居中时,距离左侧的的left值,绑定给style属性,以此达到让弹框内容永远居中

 components/Window.vue

<template>
  <div class="window" v-if="show">
    <div class="window-mask" v-if="show" @click="close"></div>
    <!-- x图标 -->
    <div class="close" @click="close">
      <span class="iconfont icon-close2"> </span>
    </div>
    <!-- 内容 -->
    <div
      class="window-content"
      :style="{
        top: '0px',
        left: windowContentLeft + 'px',
        width: windowContentWidth + 'px',
      }"
    >
      <div class="title">
        {{ title }}
      </div>
      <div class="content-body" :style="{ 'align-items': align }">
        <slot></slot>
      </div>
    </div>
  </div>
</template>

<script setup>
import { computed, onMounted, onUnmounted, ref } from "vue";

// 定义数据类型
const props = defineProps({
  show: {
    type: Boolean,
  },
  width: {
    type: Number,
    default: 1000,
  },
  title: {
    type: String,
  },
  align: {
    type: String,
    default: "top",
  },
});

// 窗口宽度
const windowWidth = ref(window.innerWidth);
// 窗口里面内容宽度
const windowContentWidth = computed(() => {
  return props.width > windowWidth.value ? windowWidth.value : props.width;
});

// 计算窗口到屏幕左边的宽度
const windowContentLeft = computed(() => {
  let left = windowWidth.value - props.width;
  return left < 0 ? 0 : left / 2;
});

const emit = defineEmits(["close"]);
const close = () => {
  emit("close");
};

// 适应屏幕宽度,窗口大小调整
const resizeWindow = () => {
  windowWidth.value = window.innerWidth;
};

// 挂载时,处理窗口大小调整(resize)事件
onMounted(() => {
  // 将resizeWindow函数绑定为window对象的resize事件的事件处理器。每当窗口大小改变时,resizeWindow函数就会被调用
  window.addEventListener("resize", resizeWindow);
});

// 卸载时
onUnmounted(() => {
  // 移除了之前通过addEventListener添加的resize事件监听器。这是非常重要的,因为如果不在组件卸载时移除这个监听器,即使组件已经被销毁,resizeWindow函数仍然可能会在窗口大小改变时被调用,这可能会导致错误或不必要的计算。
  window.removeEventListener("resize", resizeWindow);
});
</script>

<style lang="scss" scoped>
.window {
  .window-mask {
    top: 0px;
    left: 0px;
    width: 100%;
    height: calc(100vh);
    z-index: 200;
    opacity: 0.5;
    background: #000;
    position: fixed;
  }
  .close {
    z-index: 202;
    cursor: pointer;
    position: absolute;
    top: 40px;
    right: 30px;
    width: 44px;
    height: 44px;
    border-radius: 22px;
    background: #606266;
    display: flex;
    justify-content: center;
    align-items: center;
    .iconfont {
      font-size: 20px;
      color: #fff;
      z-index: 100000;
    }
  }
  .window-content {
    top: 0px;
    z-index: 201;
    position: absolute;
    background: #fff;
    .title {
      text-align: center;
      line-height: 40px;
      border-bottom: 1px solid #ddd;
      font-weight: bold;
    }
    .content-body {
      height: calc(100vh - 41px);
      display: flex;
      overflow: auto;
    }
  }
}
</style>

3.图片预览

(1). 引用 Element UI 提供的 el-image-viewer 组件的标签

 :initial-index="previewImgIndex"`:这个属性用于设置初始时显示的图片索引。 
. hide-on-click-modal:这个属性是一个布尔值(默认为 `false`),如果设置为 `true`,则点击模态框时会关闭图片查看器。

. el-image-viewer使用示例
第一种: 使用el-image - 通过点击小图, 然后预览大图, 这是官方文档提供的方法
第二种: 使用el-image-viewer
可以通过这个示例,看下element-ui是怎么做的图片预览

<template>
    <div class="preview-box">
        <!-- 第一种: 使用el-image - 通过点击小图, 然后预览大图, 这是官方文档提供的方法 -->
        <el-image :preview-src-list="['/api/file/getImage/202307/3178033358P0KiZY3YV2.png',
                                      '/api/file/getImage/202307/3178033358bd1LTA0mLK.png']" 
                   :initial-index="0" 
                   src="/api/file/getImage/202307/3178033358P0KiZY3YV2_.png"/>

        <!-- 第二种: 使用el-image-viewer
            1. 必须使用v-if来控制预览效果的显示和隐藏,不能使用v-show(使用v-show无效)
            2. 需要监听close事件, 当点击蒙层 或 关闭按钮时, 会触发close事件, 此时需要手动关闭预览, 否则预览不会关闭
            3. initial-index属性为显示图片的索引
        -->
        <el-button @click="showImage(0)">显示图片0</el-button>
        <el-button @click="showImage(1)">显示图片1</el-button>
        <el-image-viewer 
            v-if="show" 
            :url-list="['/api/file/getImage/202307/3178033358P0KiZY3YV2.png','/api/file/getImage/202307/3178033358bd1LTA0mLK.png']" 
            :initial-index="initialIndex"
            @close="closeImageViewer" 
            :hide-on-click-modal="true" />
    </div>
</template>

<script setup>

import { ref, reactive } from 'vue'

// 预览图片显示的初始索引
const initialIndex = ref(0)

// 是否展示图片预览
const show = ref(false)

// 显示图片预览的方法
function showImage(idx) {
    initialIndex.value = idx
    show.value = true // 展示预览
}

// 关闭图片预览的方法
function closeImageViewer() {
    show.value = false
}
</script>

<style lang="scss"></style>

(2)区分缩略图和原图

不需要存两张图片,只需要计算图片地址
首先,从 fileInfo.value.fileCover 中获取文件名或路径,并将其中的所有 "_." 替换为 "."。
然后,将这个修改后的文件名或路径与 proxy.globalInfo.imageUrl 拼接起来,以形成一个完整的图片URL。
例如,如果:
proxy.globalInfo.imageUrl 是 "https://example.com/images/"
fileInfo.value.fileCover 是 "cover_123_.jpg"
那么,上述代码将返回 "https://example.com/images/cover_123.jpg"。

(3)在使用滚轮缩放预览图片时,禁止页面跟随滚动

components/preview/PreviewImage.vue

<template>
  <div class="image-viewer">
    <!-- * `:initial-index="previewImgIndex"`:这个属性用于设置初始时显示的图片索引。
      `previewImgIndex` 是 Vue 组件中的一个数据属性(data property),它应该是一个数字,表示图片列表中的位置。  
    * `hide-on-click-modal`:这个属性是一个布尔值(默认为 `false`),如果设置为 `true`,则点击模态框时会关闭图片查看器。  
    -->
    <el-image-viewer
      :initial-index="previewImgIndex"
      hide-on-click-modal
      :url-list="imageList"
      @close="closeImgViewer"
      v-if="previewImgIndex != null"
    >
    </el-image-viewer>
  </div>
</template>

<script setup>
import { ref } from "vue";
const props = defineProps({
  imageList: {
    type: Array,
  },
});

const previewImgIndex = ref(null);

const show = (index) => {
  // 缩小放大图片时,禁止页面滚动
  stopScroll();
  previewImgIndex.value = index;
};
defineExpose({ show });

const closeImgViewer = () => {
  // 关闭预览时,允许页面滚动
  startScroll();
  previewImgIndex.value = null;
};

//禁止滚动
const stopScroll = () => {
  document.body.style.overflow = "hidden";
};

// 开始滚动
const startScroll = () => {
  document.body.style.overflow = "auto";
};
</script>

<style lang="scss" scoped>
.image-viewer {
  .el-image-viewer__mask {
    opacity: 0.7;
  }
}
</style>

图片预览效果

3.视频预览

  • 使用DPlayer
  • 引入hls(如果导入hls的包报错的话,可考虑在index.html中直接cdn引入hls.min.js)

components/preview/PreviewVideo.vue

<template>
  <div ref="player" id="player"></div>
</template>

<script setup>
import DPlayer from "dplayer";
import { nextTick, onMounted, ref, getCurrentInstance } from "vue";

const { proxy } = getCurrentInstance();

// 定义数据
const props = defineProps({
  url: {
    type: String,
  },
});

const videoInfo = ref({
  video: null,
});

const player = ref();
const initPlayer = () => {
  // theme	'#b7daff'	主题色
  // screenshot	false	开启截图,如果开启,视频和视频封面需要允许跨域
  // video	-	视频信息
  // video.url	-	视频链接
  // video.type	'auto'	可选值: 'auto', 'hls', 'flv', 'dash', 'webtorrent', 'normal' 或其他自定义类型,
  // video.customType	-	自定义类型
  const dp = new DPlayer({
    element: player.value,
    theme: "#b7daff",
    screenshot: true,
    video: {
      url: `/api${props.url}`,
      type: "customHls",
      customType: {
        customHls: function (video, player) {
          const hls = new Hls();
          hls.loadSource(video.src);
          hls.attachMedia(video);
        },
      },
    },
  });
};

onMounted(() => {
  initPlayer();
});
</script>

<style lang="scss" scoped>
#player {
  width: 100%;
  :deep .dplayer-video-wrap {
    text-align: center;
    .dplayer-video {
      margin: 0px auto;
      max-height: calc(100vh - 41px);
    }
  }
}
</style>

DPlayer使用

<template>
    <div class="preview-box">
        <div id="dplayer"></div>
        <el-button @click="changeVideo">切换视频</el-button>
    </div>
</template>

<script setup>

import { ref, reactive, onMounted } from 'vue'
import Hls from 'hls.js';
import DPlayer from 'dplayer';

// DPlayers实例
let dp = null

// 另一种方式,使用 customType
onMounted(() => {
    dp = new DPlayer({
        container: document.getElementById('dplayer'),
        video: {
            url: '/api/file/ts/getVideoInfo/zwizcojhc7',
            // url: '/api/file/ts/getVideoInfo/PakZTUpyp9',
            type: 'customHls',
            customType: {
                customHls: function (video, player) {
                    let config = {
                        xhrSetup: function (xhr, url) {
                            xhr.withCredentials = true; // 会携带cookie
                            xhr.setRequestHeader('token', "my-token")
                        },
                    }
                    const hls = new Hls(config);
                    hls.loadSource(video.src);
                    hls.attachMedia(video);
                },
            },
        },
    });
})

// 切换视频
function changeVideo() {
    dp.switchVideo({
        // url: '/api/file/ts/getVideoInfo/zwizcojhc7',
        url: '/api/file/ts/getVideoInfo/PakZTUpyp9',
        type: 'customHls',
        customType: {
            customHls: function (video, player) {
                let config = {
                    xhrSetup: function (xhr, url) {
                        xhr.withCredentials = true; // 会携带cookie
                        xhr.setRequestHeader('token', "my-token")
                    },
                }
                const hls = new Hls(config);
                hls.loadSource(video.src);
                hls.attachMedia(video);
            },
        },
    })
}


</script>

<style lang="scss">
#dplayer {
    width: 600px;
    height: 300px;
}
</style>

4.Docx文档预览

PreviewDocx.vue组件

  • 使用docx-preview这个插件(npm i docx-preview -S)
  • axios的responseType配置为blob
  • 后台返回的是二进制数据(后台读取文件流,将流数据写入response),前端接受此流数据,传入给docx-preview插件处理

<template>
  <div ref="docRef" class="doc-content"></div>
</template>

<script setup>
import * as docx from "docx-preview";
import { ref, reactive, getCurrentInstance, onMounted } from "vue";
const { proxy } = getCurrentInstance();

const props = defineProps({
  url: {
    type: String,
  },
});

const docRef = ref();

const initDoc = async () => {
  // 它向 props.url 指定的 URL 发起请求,并设置响应类型为 "blob"。Blob 对象表示一个不可变、原始数据的类文件对象。
  let result = await proxy.Request({
    url: props.url,
    responseType: "blob",
  });
  if (!result) {
    return;
  }
  // 来渲染从服务器获取的 Blob
  docx.renderAsync(result, docRef.value);
};

onMounted(() => {
  initDoc();
});
</script>

<style lang="scss" scoped>
.doc-content {
  margin: 0px auto;
  :deep .docx-wrapper {
    background: #fff;
    padding: 10px 0px;
  }

  :deep .docx-wrapper > section.docx {
    margin-bottom: 0px;
  }
}
</style>

docx-preview使用示例

<template>
   <div class="doc-box">
        <div ref="docRef" id="doc-content"></div>
   </div>
</template>

<script setup>
import { ref,reactive } from 'vue'
import axios from 'axios'
import {renderAsync} from 'docx-preview'

const props = defineProps({
    url:{
        type: String
    },
    fileInfo: {
        type: Object
    }
})
const docRef = ref()
axios({
    url:`${props.url}${props.fileInfo.fileId}`,
    method: 'POST',
    responseType: 'blob',
}).then(res=>{
    console.log(res.data,'res.data');
    renderAsync(res.data, docRef.value)
})


</script>

<style lang="scss" scoped>


.doc-box {
    height: 100%;
    overflow: auto;
}
</style>

 5.Excel文件预览

PreviewExcel.vue组件

  • 安装xlsx这个插件
  • axios的responseType配置为arraybuffer(注意都是小写)
  • 后台返回的是二进制数据(后台读取文件流,将流数据写入response),前端接受此流数据,传入给xlsx插件处理
  • l将插件处理得到的html,使用v-html 插入到 div标签中
<template>
  <div v-html="excelContent" class="talbe-info"></div>
</template>

<script setup>
import * as XLSX from "xlsx";
import { ref, reactive, getCurrentInstance, onMounted } from "vue";
const { proxy } = getCurrentInstance();

const props = defineProps({
  url: {
    type: String,
  },
});

const excelContent = ref();

const initExcel = async () => {
  let result = await proxy.Request({
    url: props.url,
    responseType: "arraybuffer",
  });
  if (!result) {
    return;
  }
  // 使用 XLSX.read 方法来解析一个 Uint8Array,这个 Uint8Array 很可能是从一个 Excel 文件(如 XLSX 格式)的 Blob 数据中得到的。{ type: "array" } 选项告诉 XLSX.read 方法输入数据的类型是一个数组。
  let workbook = XLSX.read(new Uint8Array(result), { type: "array" }); // 解析数据
  // 通过 workbook.SheetNames 获取工作簿中所有工作表的名字数组。然后,通过索引 [0] 获取第一个工作表的名字。最后,使用这个名字从 workbook.Sheets 对象中取出对应的工作表对象。
  var worksheet = workbook.Sheets[workbook.SheetNames[0]]; // workbook.SheetNames 下存的是该文件每个工作表名字,这里取出第一个工作表
  // 将工作表对象转换为 HTML 字符串
  excelContent.value = XLSX.utils.sheet_to_html(worksheet);
};

onMounted(() => {
  initExcel();
});
</script>

<style lang="scss" scoped>
.talbe-info {
  width: 100%;
  padding: 10px;
  :deep table {
    width: 100%;
    border-collapse: collapse;
    td {
      border: 1px solid #ddd;
      border-collapse: collapse;
      padding: 5px;
      height: 30px;
      min-width: 50px;
    }
  }
}
</style>

Xlsx组件使用示例

下面的responseType一定要写成arraybuffer
如果responseType写的是blob的话,那么一定要调用res.data.arraybuffer(),这个调用返回结果是个Promise,把此Promise得到的结果给到new Uint8Array(promise的结果)也可以

<template>
    <div class="xlsx-box">
         <div ref="xlsxRef" id="xlsx-content" v-html="excelContent"></div>
    </div>
 </template>
 
 <script setup>
 import { ref,reactive } from 'vue'
 import axios from 'axios'
 import * as XLSX from 'xlsx'
 
 const props = defineProps({
     url:{
         type: String
     },
     fileInfo: {
         type: Object
     }
 })
const excelContent = ref();
 axios({
     url:`${props.url}${props.fileInfo.fileId}`,
     method: 'POST',
     responseType: 'arraybuffer',
 }).then(res=>{
     console.log(res.data,'res.data');
     let workbook = XLSX.read(new Uint8Array(res.data), { type: "array" });
     var worksheet = workbook.Sheets[workbook.SheetNames[0]];
     excelContent.value = XLSX.utils.sheet_to_html(worksheet);
 })
 
 
 </script>
 
 <style lang="scss" scoped>
 
 
 .xlsx-box {
     height: 100%;
     width: 100%;
     overflow: auto;
     padding: 20px;
     :deep table {
        width: 100%;
        border-collapse: collapse;
        
        td {
            border: 1px solid #ddd;
            line-height: 2;
            padding: 0 5px 0;
            min-width: 30px;
            height: 30px;
        }
     }
 }
 </style>

6.PDF预览

PreviewPDF.vue

  • 须安装VuePdfEmbed 、vue3-pdfjs插件
<template>
  <div class="pdf">
    <vue-pdf-embed
      ref="pdfRef"
      :source="state.url"
      class="vue-pdf-embed"
      width="850"
      :page="state.pageNum"
    ></vue-pdf-embed>
  </div>
</template>

<script setup>
import VuePdfEmbed from "vue-pdf-embed";
import { ref, reactive, getCurrentInstance, onMounted } from "vue";
const { proxy } = getCurrentInstance();

const props = defineProps({
  url: {
    type: String,
  },
});

const state = ref({
  // 预览pdf文件地址
  url: "",
  // 当前页面
  pageNum: 0,
  // 总页数
  numPages: 0,
});

const initPdf = async () => {
  state.value.url = "/api" + props.url;
};

initPdf();
</script>

<style lang="scss" scoped>
.pdf {
  width: 100%;
}
</style>

7.文本预览

PreviewTxt.vue

  • 允许手动选择编码格式(使用了FileReader#readAsText(blob,encode)指定编码,将文件流读取为文本字符串)

  • 如果是代码,允许复制(使用了vue-clipboard3插件)

  • 代码高亮(使用了@highlightjs/vue-plugin插件)

<template>
  <div class="code">
    <div class="top-op">
      <div class="encode-select">
        <el-select
          placeholder="请选择编码"
          v-model="encode"
          @change="changeEncode"
        >
          <el-option value="utf8" label="utf8编码"></el-option>
          <el-option value="gbk" label="gbk编码"></el-option>
        </el-select>

        <div class="tips">乱码了?切换编码试试</div>
      </div>
      <div class="copy-btn">
        <el-button type="primary" @click="copy">复制</el-button>
      </div>
    </div>
    <!-- 代码高亮 -->
    <highlightjs autodetect :code="txtContent" />
  </div>
</template>

<script setup>
// 引入实现复制的文件
import useClipboard from "vue-clipboard3";
const { toClipboard } = useClipboard();

import { ref, reactive, getCurrentInstance, onMounted, nextTick } from "vue";
const { proxy } = getCurrentInstance();

const props = defineProps({
  url: {
    type: String,
  },
});

// 文本内容
const txtContent = ref("");
// 文本流结果
const blobResult = ref();
// 编码类型
const encode = ref("utf8");

const readTxt = async () => {
  let result = await proxy.Request({
    url: props.url,
    responseType: "blob",
  });
  if (!result) {
    return;
  }
  blobResult.value = result;
  showTxt();
};

// 选择编码
const changeEncode = (e) => {
  encode.value = e;
  showTxt();
};

const showTxt = () => {
  const reader = new FileReader();
  // 当读取操作成功完成时调用
  // 2. 再执行该异步操作
  reader.onload = () => {
    let txt = reader.result;
    txtContent.value = txt; //获取的数据data
  };
  // 异步按字符读取文件内容,结果用字符串形式表示
  // 1. 先走这步,获取读取文件操作
  reader.readAsText(blobResult.value, encode.value);
};
onMounted(() => {
  readTxt();
});

const copy = async () => {
  await toClipboard(txtContent.value);
  proxy.Message.success("复制成功");
};
</script>

<style lang="scss" scoped>
.code {
  width: 100%;
  .top-op {
    display: flex;
    align-items: center;
    justify-content: space-around;
  }
  .encode-select {
    flex: 1;
    display: flex;
    align-items: center;
    margin: 5px 10px;
    .tips {
      margin-left: 10px;
      color: #828282;
    }
  }
  .copy-btn {
    margin-right: 10px;
  }
  pre {
    margin: 0px;
  }
}
</style>
// main.js中引入代码高亮

//引入代码高亮
import HljsVuePlugin from '@highlightjs/vue-plugin'
import "highlight.js/styles/atom-one-light.css";
import 'highlight.js/lib/common'

8. 音频预览

PreviewVideo.vue

  • 使用APlayer,官方使用文档:APlayer
  • 使用new URL(`@/assets/music_icon.png`, import.meta.url).href,引入本地图片做封面,这个是写在script标签里用的(而在模板中仍然用的是使用@/assets/music_cover.png去引用)
<template>
  <div class="music">
    <div class="body-content">
      <div class="cover">
        <img src="@/assets/music_cover.png" />
      </div>
      <div ref="playerRef" class="music-player"></div>
    </div>
  </div>
</template>

<script setup>
import APlayer from "APlayer";
import "APlayer/dist/APlayer.min.css";

import {
  ref,
  reactive,
  getCurrentInstance,
  computed,
  onMounted,
  onUnmounted,
} from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();

const props = defineProps({
  url: {
    type: String,
  },
  fileName: {
    type: String,
  },
});

const playerRef = ref();
const player = ref();

onMounted(() => {
  player.value = new APlayer({
    container: playerRef.value,
    audio: {
      url: `/api${props.url}`,
      name: `${props.fileName}`,
      cover: new URL(`@/assets/music_icon.png`, import.meta.url).href,
      artist: "",
    },
  });
});

onUnmounted(() => {
  player.value.destroy();
});
</script>

<style lang="scss" scoped>
.music {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  .body-content {
    text-align: center;
    width: 80%;
    .cover {
      margin: 0px auto;
      width: 200px;
      text-align: center;
      img {
        width: 100%;
      }
    }
    .music-player {
      margin-top: 20px;
    }
  }
}
</style>

9.文件下载 

PreviewDowndload.vue

  • 先获取一个临时的code,再以此code请求另外一个下载链接(直接使用location.href指向下载链接去做下载,如果当前地址栏有地址,则不会地址栏;如果当前地址栏是空的-比如浏览器直接打开一个空白网页,然后在控制台输入location.href=‘下载地址’,此时地址栏就会变成下载地址)
  • 文件列表中的下载也是同样的做法
  • 不支持预览,下载之后查看
<template>
  <div class="others">
    <div class="body-content">
      <div>
        <Icon
          :iconName="fileInfo.fileType == 9 ? 'zip' : 'others'"
          :width="80"
        ></Icon>
      </div>
      <div class="file-name">{{ fileInfo.fileName }}</div>
      <div class="tips">该类型的文件暂不支持预览,请下载后查看</div>
      <div class="download-btn">
        <el-button type="primary" @click="download"
          >点击下载 {{ proxy.Utils.size2Str(fileInfo.fileSize) }}</el-button
        >
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, getCurrentInstance } from "vue";
import { useRouter, useRoute } from "vue-router";
const { proxy } = getCurrentInstance();
const router = useRouter();
const route = useRoute();

const props = defineProps({
  createDownloadUrl: {
    type: String,
  },
  downloadUrl: {
    type: String,
  },
  fileInfo: {
    type: Object,
  },
});

const download = async () => {
  let result = await proxy.Request({
    url: props.createDownloadUrl,
  });
  if (!result) {
    return;
  }
  window.location.href = props.downloadUrl + "/" + result.data;
};
</script>

<style lang="scss" scoped>
.others {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  .body-content {
    text-align: center;
    .file-name {
      font-weight: bold;
    }
    .tips {
      color: #999898;
      margin-top: 5px;
      font-size: 13px;
    }
    .download-btn {
      margin-top: 20px;
    }
  }
}
</style>

 Main.vue中

// 下载文件
const download = async (row) => {
  let result = await proxy.Request({
    url: api.createDownloadUrl + "/" + row.fileId,
  });

  if (!result) {
    return;
  }

  window.location.href = api.download + "/" + result.data;
};

参考:easypan前端学习(二)_easypan源码-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1692076.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入了解 CSS 预处理器 Sass

今天我们来深入探讨一下 CSS 预处理器 Sass。我们将学习什么是 Sass,如何使用它,以及它是如何工作的。 什么是 Sass? Sass 是 syntactically awesome style sheets 的缩写,是一种 CSS 预处理器。它是 CSS 的扩展,为基础 CSS 增加了更多的功能和优雅。普通的 CSS 代码很容…

新手如何收集关键词,拥有源源不断的写作素材资源?分享6种通用方法!

好多个伙伴跟我讲&#xff0c;自己写了一段时间的微信公众号&#xff0c;现在找不到该写什么内容。能写的&#xff0c;自己都写完了&#xff0c;该写的&#xff0c;自己也写了&#xff0c;每天都在为写什么苦恼。 对于新手&#xff0c;如何拥有源源不断的写作素材资源&#xf…

搭建自己的视频通话服务器Janus(WebRTC)

1. 安装janus apt install janus 高版本的Ubuntu已经可以直接安装了&#xff0c;不要编译那么麻烦了 2. 安装nginx janus 是后端&#xff0c;需要nginx或者其他等提供前端页面的服务器&#xff0c;此外因为这里并没有正式的证书&#xff0c;只能用自签名的证书用于https。 …

【竞技宝】英超:足总杯踢完解雇腾帅,曼联管理层心意已决

根据知名媒体《卫报》的报道,足总杯之后曼联将会 解雇滕哈格,哪怕他率领曼联队能够击败强大的曼城夺冠,也无法改变他将下课的事实。因为曼联本赛季的联赛排名只有第8名,已经来到了近30年来的最差成绩,这种情况下滕哈格与曼联的缘分似乎将被终结。 滕哈格上赛季成为曼联的主帅,由…

mysql驱动版本变更导致查询数据结果一直是空

1 引言 最近接手了一个已离职同事的java项目&#xff0c;这个项目中原来使用了自己的mysql驱动版本&#xff0c;并未使用公司公共依赖中的版本号。我想为了统一版本号&#xff0c;就将当前项目中pom文件中mysql的版本号verson给去除了。没怎么自测&#xff0c;就直接发到测试环…

BGP路由优化

一&#xff0c;拓扑 二&#xff0c;要求 用preva1策略确保R4通过R2到达192.168.10.0/24 &#xff0c;用AS Path策略&#xff0c;确保R4通过R3到达192.168.11.0/24 置MED策略&#xff0c;确保R4通过R3到达192.168.12.0/24 .用Local Preference策略&#xff0c;确保R1通过R2到达1…

Excel中Lookup函数

#Excel查找函数最常用的是Vlookup&#xff0c;而且是经常用其精确查找。Lookup函数的强大之处在于其“二分法”的原理。 LOOKUP&#xff08;查找值&#xff0c;查找区域&#xff08;Vector/Array&#xff09;&#xff0c;[返回结果区域]&#xff09; 为什么查找区域必须升序/…

Python 小游戏——贪吃蛇

Python 小游戏——贪吃蛇 项目介绍 贪吃蛇游戏是一款通过上下左右方向键控制贪吃蛇吃到豆豆以获取积分的游戏&#xff0c;该项目使用Python语言进行实现。主要使用了Pygame库来处理图形和用户输入。 环境配置 该项目需要在Pycharm的终端中执行以下指令来安装Pygame库&#…

MySQL的主从复制(主从数据库都是Linux版本)

概述 1.什么是主从复制 主从复制是指将主库的 DDL 和 DML 操作通过二进制日志传到从库服务器中&#xff0c;然后在从库上对这些日志重新执行&#xff08;也叫重做&#xff09;&#xff0c;从而使得从库和主库的数据保持一致。 2.主从复制作用 数据备份&#xff1a;通过主从复…

014_C标准库函数之<stdio.h>

【背景】 今天这个主要说的是<stdio.h>头文件&#xff0c;大家众所周知&#xff0c;这个是我们学习C语言时第一个接触到的头文件了&#xff0c;那么为什么我不一开始就介绍这个头文件呢&#xff1f;我觉得有两个原因&#xff0c;如下&#xff1a; 1.一开始大家的编程思…

【JavaEE进阶】——Spring Web MVC (响应)

目录 &#x1f6a9;学习Spring MVC &#x1f388;返回静态网页 &#x1f388;返回数据ResponseBody &#x1f388;返回html代码片段 &#x1f388;返回JSON &#x1f388;设置状态码 &#x1f388;设置Header &#x1f6a9;学习Spring MVC 既然是 Web 框架, 那么当⽤⼾在…

Linux--10---安装JDK、MySQL

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 安装JDK[Linux命令--03----JDK .Nginx. 数据库](https://blog.csdn.net/weixin_48052161/article/details/108997148) 第一步 查询系统中自带的JDK第二步 卸载系统中…

【医学AI|顶刊精析|05-25】哈佛医学院·告别切片局限:3D病理如何革新癌症预后

小罗碎碎念 先打个预防针&#xff0c;我写这篇推文用了两个多小时&#xff0c;这就意味着要读懂这篇文章不太容易&#xff0c;我已经做好反复阅读的准备了。不过&#xff0c;风险之下&#xff0c;亦是机会&#xff0c;读懂的人少&#xff0c;这个赛道就越值得押宝。 在正式阅…

【浅水模型MATLAB】尝试完成一个数值模拟竞赛题

【浅水模型MATLAB】尝试完成一个数值模拟竞赛题 前言题目描述问题分析理论基础控制方程数值方法边界条件 代码框架与关键代码结果展示写在最后 更新于2024年5月25日 前言 最近看到第四届水科学数值模拟创新大赛的通知&#xff0c;就好奇翻看了前几年的比赛试题。发现去年的一个…

Ajax异步删除

在页面上定义一个按钮 <button type"button" class"btn"><a href"JavaScript:;" class"id" b_id"{{$attachment[id]}}">删除</a></button> js代码 <script>$(.id).click(function (){va…

vs2019 c++ 函数的返回值是对象的值传递时候,将调用对象的移动构造函数

以前倒没有注意过这个问题。但编译器这么处理也符合移动构造的语义。因为本来函数体内的变量也要离开作用域被销毁回收了。测试如下&#xff1a; 谢谢

轻松拿捏C语言——【字符函数】字符分类函数、字符转换函数

&#x1f970;欢迎关注 轻松拿捏C语言系列&#xff0c;来和 小哇 一起进步&#xff01;✊ &#x1f308;感谢大家的阅读、点赞、收藏和关注&#x1f495; &#x1f339;如有问题&#xff0c;欢迎指正 感谢 目录&#x1f451; 一、字符分类函数&#x1f319; 二、字符转换函数…

Java进阶学习笔记1——课程介绍

课程适合学习的人员&#xff1a; 1&#xff09;具备一定java基础的人员&#xff1b; 2&#xff09;想深刻体会Java编程思想&#xff0c;成为大牛的人员&#xff1b; 学完有什么收获&#xff1f; 1&#xff09;掌握完整的Java基础技术体系&#xff1b; 2&#xff09;极强的编…

【算法】前缀和算法——和可被K整除的子数组

题解&#xff1a;和可被K整除的子数组(前缀和算法) 目录 1.题目2.前置知识2.1同余定理2.2CPP中‘%’的计算方式与数学‘%’的差异 及其 修正2.3题目思路 3.代码示例4.总结 1.题目 题目链接&#xff1a;LINK 2.前置知识 2.1同余定理 注&#xff1a;这里的‘/’代表的是数学…