前言:使用的是若依的框架+element ui+vue2封装的。如果有不对的地方欢迎指出。后台管理使用,文件需要上传。回显列表,详情也需要回显+预览
// 开始封装组件:封装在 src/components/FileUpload/index.vue中
<template>
<div class="upload-file">
<el-upload
multiple
name="multipartFile"
:action="uploadFileUrl"
:data="{ 上传时附带的额外参数 }"
:limit="limit"
:file-list="fileList"
:before-upload="handleBeforeUpload"
:on-exceed="handleExceed"
:on-error="handleUploadError"
:on-success="handleUploadSuccess"
:show-file-list="false"
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
>
<!-- 上传按钮 -->
<el-button size="mini" type="primary" plain v-if="!disabled">选取文件</el-button>
<!-- 上传提示 -->
<div class="el-upload__tip" slot="tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
</template>
<template v-if="fileType">
格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
</template>
的文件
</div>
</el-upload>
<!-- 文件列表,我们功能需要 删除文件、下载文件、预览文件功能 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul" style="min-width: 300px">
<li class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList" :key="file.fileId">
<el-link class="link" :href="`${baseUrl}${file.filePath}`" :underline="false" target="_blank">
<span class="el-icon-document" style="padidng-left: 10px">
{{ getFileName(file.fileName) }}
</span>
</el-link>
<div class="controls">
<div class="ele-upload-list__item-content-action" style="width: 50px; text-align: center">
<el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled">删除</el-link>
</div>
<div class="ele-upload-list__item-content-action" style="width: 50px; text-align: center">
<el-link :underline="false" @click="handlePreview(file)" type="danger">预览</el-link>
</div>
<div class="ele-upload-list__item-content-action" style="width: 50px; text-align: center">
<el-link :underline="false" @click="handleDownload(file)" type="danger">下载</el-link>
</div>
</div>
</li>
</transition-group>
<!-- 文件预览功能,点击文件列表后的预览按钮,需要预览文档,pdf,excel,照片,视频,音频。其他没有封装,所以就不支持 -->
<el-dialog title="文件预览" :visible.sync="preview.open" append-to-body :before-close="previewCancel">
<vue-office-docx v-if="fileSuffix == 'docx'" :src="preview.url" style="height: 100vh" />
<vue-office-excel v-else-if="fileSuffix == 'xlsx'" :src="preview.url" style="width: auto; height: 100vh" />
<vue-office-pdf v-else-if="fileSuffix == 'pdf'" :src="preview.url" style="height: 100vh" />
<div style="text-align: center" v-else-if="fileSuffix == 'img'">
<el-image style="width: 500px" :src="preview.url" fit="fill" :preview-src-list="[preview.url]"></el-image>
</div>
<div style="text-align: center" v-else-if="fileSuffix == 'mp3'">
<audio controls loop ref="myAudio" autoplay class="my-audio">
<source :src="preview.url" />
</audio>
</div>
<div style="text-align: center" v-else-if="fileSuffix == 'mp4'">
<video-app :src="preview.url" :second="1"></video-app>
</div>
<div style="text-align: center" v-else>暂不支持该文件预览,请下载预览</div>
</el-dialog>
</div>
</template>
<script>
import { getToken } from "@/utils/auth";
import { 查看和下载的接口,后端给的 } from "";
//引入VueOfficeDocx组件,需要npm安装
import VueOfficeDocx from "@vue-office/docx";
import "@vue-office/docx/lib/index.css";
//引入VueOfficeExcel组件,需要npm安装
import VueOfficeExcel from "@vue-office/excel";
import "@vue-office/excel/lib/index.css";
//引入VueOfficePdf组件,需要npm安装
import VueOfficePdf from "@vue-office/pdf";
import VideoApp from "./video.vue"; // 这个是我封装的视频预览的组件
export default {
name: "FileUpload",
props: {
// 数量限制
limit: {
type: Number,
default: 5,
},
// 大小限制(MB)
fileSize: {
type: Number,
default: 20,
},
// 文件类型, 例如['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["docx", "pptx", "pdf"],
},
// 是否显示提示
isShowTip: {
type: Boolean,
default: true,
},
formFileList: {
type: Array,
default: () => [],
},
disabled: {
type: Boolean,
default: false,
},
// 培训记录附件的类型
busiType: {
type: String,
},
},
components: {
VueOfficeDocx,
VueOfficeExcel,
VueOfficePdf,
VideoApp,
},
data() {
return {
number: 0,
uploadList: [],
baseUrl: // 地址 ,
uploadFileUrl: , // 上传文件服务器地址
headers: {
Authorization: "Bearer " + getToken(),
},
fileList: [],
lookFile: false,
url: "",
// 文件预览
preview: {
open: false,
url: "",
},
fileSuffix: "",
};
},
watch: {
// 编辑和详情的回显fileList
formFileList: {
handler(val) {
if (val !== undefined) {
this.fileList = val;
}
if (val == null) {
this.fileList = [];
return;
}
},
deep: true,
immediate: true,
},
},
computed: {
// 是否显示提示
showTip() {
return this.isShowTip && (this.fileType || this.fileSize);
},
},
methods: {
// 上传前校检格式和大小
handleBeforeUpload(file) {
// 校检文件类型
if (this.fileType) {
const fileName = file.name.split(".");
const fileExt = fileName[fileName.length - 1];
const isTypeOk = this.fileType.length ? this.fileType.indexOf(fileExt) >= 0 : [];
if (!isTypeOk) {
this.$modal.msgError(`文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`);
return false;
}
}
// 校检文件大小
if (this.fileSize) {
const isLt = file.size / 1024 / 1024 < this.fileSize;
if (!isLt) {
this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`);
return false;
}
}
this.$modal.loading("正在上传文件,请稍候...");
this.number++;
return true;
},
// 文件个数超出
handleExceed() {
this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
},
// 上传失败
handleUploadError(err) {
this.number--;
this.$modal.msgError("上传文件失败,请重试");
this.$modal.closeLoading();
},
// 上传成功回调
handleUploadSuccess(res, file) {
if (res.code === 200) {
this.uploadList.push(res.data);
this.$modal.closeLoading();
this.uploadedSuccessfully();
} else {
this.number--;
this.$modal.closeLoading();
this.$modal.msgError(res.msg);
this.$refs.fileUpload.handleRemove(file);
this.uploadedSuccessfully();
}
},
// 删除文件
handleDelete(index) {
this.fileList.splice(index, 1);
this.$emit("input", this.listToString(this.fileList));
},
// 上传结束处理
uploadedSuccessfully() {
if (this.number > 0 && this.uploadList.length === this.number) {
this.fileList = this.fileList.concat(this.uploadList);
this.uploadList = [];
this.number = 0;
this.$emit("input", this.listToString(this.fileList));
this.$modal.closeLoading();
this.$emit("fileUploadSuccess", this.fileList);
}
},
// 获取文件名称
getFileName(name) {
if (name?.lastIndex/Of("/") > -1) {
return name.slice(name.lastIndexOf("/") + 1);
} else {
return name;
}
},
// 对象转成指定字符串分隔
listToString(list, separator) {
let strs = "";
separator = separator || ",";
for (let i in list) {
strs += list[i].url + separator;
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
},
resetFileList() {
this.fileList = [];
},
// 下载
handleDownload(file) {
downloadFile(file.fileId).then((res) => {
this.exportFunction(res, file.fileName, file.fileType);
});
},
exportFunction(response, name, type) {
const link = document.createElement("a");
const blob = new Blob([response], { type });
link.style.display = "none";
link.href = URL.createObjectURL(blob);
link.setAttribute("download", name, type);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
},
// 预览
handlePreview(file) {
this.preview.url = "";
if (file.fileType.indexOf("image") !== -1) {
this.fileSuffix = "img";
} else {
this.fileSuffix = file.suffix;
}
showFileURL(file.fileId).then((res) => {
this.preview.url = defaultSettings.minioUrl + res;
if (file.suffix == "mp3") {
this.$nextTick((res) => {
this.$refs.myAudio.load();
this.$refs.myAudio.play();
});
}
this.preview.open = true;
});
},
previewCancel() {
this.preview.open = false;
this.preview.url = "";
if (this.fileSuffix == "mp3") {
this.$refs.myAudio.pause();
}
},
},
};
</script>
<style scoped lang="scss">
::v-deep .el-button--primary.is-plain {
color: var(--select-selected-color);
background: var(--table-content-bColor);
border-color: var(--search-back);
}
::v-deep .el-button--primary.is-plain:hover,
::v-deep .el-button--primary.is-plain:focus {
border-color: var(--select-selected-color);
background-color: var(--select-selected-color);
color: #fff !important;
}
.upload-file-uploader {
margin-bottom: 5px;
}
.upload-file-list .el-upload-list__item {
border: 1px solid #e4e7ed;
line-height: 2;
margin-bottom: 10px;
position: relative;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
color: inherit;
.link {
flex: 1;
display: block;
margin-left: 2px;
::v-deep .el-icon-document {
width: 310px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.controls {
display: flex;
}
}
.ele-upload-list__item-content-action .el-link {
margin-right: 10px;
}
::v-deep {
.x-spreadsheet-table {
width: auto !important;
}
}
.my-audio {
width: 100%;
}
</style>
video组件封装:
<template>
<div class="m-video" :class="{'u-video-hover': !hidden}" :style="`width: ${width}px; height: ${height}px;`">
<video
ref="veo"
:style="`object-fit: ${zoom};`"
:src="src"
:poster="veoPoster"
:width="width"
:height="height"
:autoplay="autoplay"
:controls="!originPlay&&controls"
:loop="loop"
:muted="autoplay || muted"
:preload="preload"
crossorigin="anonymous"
@loadeddata="poster ? () => false : getPoster()"
@pause="showPlay ? onPause() : () => false"
@playing="showPlay ? onPlaying() : () => false"
@click.prevent.once="onPlay"
v-bind="$attrs">
您的浏览器不支持video标签。
</video>
<svg v-show="originPlay || showPlay" class="u-play" :class="{'hidden': hidden}" :style="`width: ${playWidth}px; height: ${playWidth}px;`" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4.75 6.75C4.75 5.64543 5.64543 4.75 6.75 4.75H17.25C18.3546 4.75 19.25 5.64543 19.25 6.75V17.25C19.25 18.3546 18.3546 19.25 17.25 19.25H6.75C5.64543 19.25 4.75 18.3546 4.75 17.25V6.75Z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M15.25 12L9.75 8.75V15.25L15.25 12Z"></path>
</svg>
</div>
</template>
<script>
export default {
name: 'Video',
props: {
src: { // 视频文件url,必传,支持网络地址 https 和相对地址 require('@/assets/files/Bao.mp4')
type: String,
required: true,
default: ''
},
poster: { // 视频封面url,支持网络地址 https 和相对地址 require('@/assets/images/Bao.jpg')
type: String,
default: ''
},
second: { // 在未设置封面时,自动截取视频第 second 秒对应帧作为视频封面
type: Number,
default: 0.5
},
width: { // 视频播放器宽度
type: Number,
default: 800
},
height: { // 视频播放器高度
type: Number,
default: 450
},
autoplay: { // 视频就绪后是否马上播放
type: Boolean,
default: false
},
controls: { // 是否向用户显示控件,比如进度条,全屏
type: Boolean,
default: true
},
loop: { // 视频播放完成后,是否循环播放
type: Boolean,
default: false
},
muted: { // 是否静音
type: Boolean,
default: false
},
preload: { // 是否在页面加载后载入视频,如果设置了autoplay属性,则preload将被忽略;
type: String,
default: 'auto' // auto:一旦页面加载,则开始加载视频; metadata:当页面加载后仅加载视频的元数据 none:页面加载后不应加载视频
},
showPlay: { // 播放暂停时是否显示播放器中间的暂停图标
type: Boolean,
default: true
},
playWidth: { // 中间播放暂停按钮的边长
type: Number,
default: 96
},
zoom: { // video的poster默认图片和视频内容缩放规则
type: String,
default: 'contain' // none:(默认)保存原有内容,不进行缩放; fill:不保持原有比例,内容拉伸填充整个内容容器; contain:保存原有比例,内容以包含方式缩放; cover:保存原有比例,内容以覆盖方式缩放
}
},
data () {
return {
veoPoster: this.poster,
originPlay: true,
hidden: false
}
},
mounted () {
if (this.autoplay) {
this.hidden = true
this.originPlay = false
}
/*
自定义设置播放速度,经测试:
在vue2中需设置:this.$refs.veo.playbackRate = 2
在vue3中需设置:veo.value.defaultPlaybackRate = 2
*/
// this.$refs.veo.playbackRate = 2
},
methods: {
/*
loadeddata 事件在媒体当前播放位置的视频帧(通常是第一帧)加载完成后触发
preload为none时不会触发
*/
getPoster () { // 在未设置封面时,自动获取视频0.5s对应帧作为视频封面
// 由于不少视频第一帧为黑屏,故设置视频开始播放时间为0.5s,即取该时刻帧作为封面图
this.$refs.veo.currentTime = this.second
// 创建canvas元素
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// canvas画图
canvas.width = this.$refs.veo.videoWidth
canvas.height = this.$refs.veo.videoHeight
ctx.drawImage(this.$refs.veo, 0, 0, canvas.width, canvas.height)
// 把canvas转成base64编码格式
this.veoPoster = canvas.toDataURL('image/png')
},
onPlay () {
if (this.originPlay) {
this.$refs.veo.currentTime = 0
this.originPlay = false
}
if (this.autoplay) {
this.$refs.veo.pause()
} else {
this.hidden = true
this.$refs.veo.play()
}
},
onPause () {
this.hidden = false
},
onPlaying () {
this.hidden = true
}
}
}
</script>
<style lang="scss" scoped>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.m-video {
display: inline-block;
position: relative;
background: #000;
cursor: pointer;
.u-play {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
fill: none;
color: #FFF;
pointer-events: none;
opacity: 0.7;
transition: opacity .3s;
path {
stroke: #FFF;
}
}
.hidden {
opacity: 0;
}
}
.u-video-hover {
&:hover {
.u-play {
opacity: 0.9;
}
}
}
</style>
使用组件:
import FileUpload from "@/components/FileUpload";
<el-form-item label="文件上传">
<file-upload
ref="fileResetRef"
@fileUploadSuccess="fileUploadSuccessHandle"
:formFileList="form.files" // 回显的数据文件列表
:disabled="isReadonly" // 区分编辑还是查看
:file-type="[ // 支持的类型
'png',
'jpg',
'docx',
'xlsx',
'pptx',
'pdf',
'mp3',
'mp4',
'zip',
]"
>
</file-upload>
</el-form-item>
// 文件上传成功的展示
fileUploadSuccessHandle1(fileList) {
this.form.files = fileList;
},
上传组件效果图:
上传的文件列表: