0 相关介绍
基于element-plus框架,视频播放器使用西瓜视频播放器组件
相关能力
- 提供图片、音频、视频的预览功能
- 提供是否为空、文件类型、文件大小、文件数量、图片宽高校验
- 提供图片回显功能,并保证回显的文件不会重新上传
- 提供达到数量限制不显示element自带的加号
相关文档
1 效果展示
2 组件主体
<template>
<el-upload
list-type="picture-card"
:auto-upload="false"
:on-change="onChange"
:on-remove="onRemove"
:multiple="props.multiple"
:limit="props.limit"
:accept="accept"
ref="elUploadElement"
:file-list="fileList"
:id="fileUploadId"
:disabled="disabled"
>
<el-icon><Plus /></el-icon>
<template #file="{ file }">
<div>
<img
class="el-upload-list__item-thumbnail"
:src="file.viewUrl"
alt=""
v-if="isShow"
/>
<span class="el-upload-list__item-actions">
<span
class="el-upload-list__item-preview"
@click="handlePictureCardPreview(file)"
>
<el-icon><zoom-in /></el-icon>
</span>
<span
class="el-upload-list__item-delete"
@click="handleRemove(file)"
v-if="!disabled"
>
<el-icon><Delete /></el-icon>
</span>
</span>
</div>
</template>
<template #tip>
<div class="el-upload__tip">
{
{
tip }}
</div>
</template>
</el-upload>
<!-- 图片预览弹窗 -->
<el-dialog
v-model="dialogVisible"
style="width: 800px"
@close="close"
@open="open"
>
<el-image
v-if="previewFile.type === 'image'"
style="width: 100%; height: 400px"
:src="previewFile.url"
fit="contain"
alt="Preview Image"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="[previewFile.url]"
:initial-index="0"
/>
<videoComponent
ref="videoRef"
v-if="previewFile.type === 'video'"
style="width: 100%; height: 400px"
:url="previewFile.url"
:poster="previewFile.viewUrl"
/>
<audio
ref="audioRef"
v-if="previewFile.type === 'audio'"
:src="previewFile.url"
controls
style="width: 100%; height: 400px"
/>
</el-dialog>
</template>
<script lang="ts" setup>
import {
ref } from "vue";
import {
Delete, Plus, ZoomIn } from "@element-plus/icons-vue";
import type {
UploadFile } from "element-plus";
import {
buildUUID } from "@pureadmin/utils";
import {
message } from "@/utils/message";
const fileUploadId = ref("ID" + buildUUID().slice(0, 12));
type UploadFileNew = {
[Property in keyof UploadFile]: UploadFile[Property];
} & {
type: string;
viewUrl: string | undefined;
needUpload: boolean;
duration: number;
width: number;
height: number;
process: boolean;
};
import {
imageRegex, audioRegex, videoRegex } from "@/utils/preview";
import videoComponent from "./videoComponent.vue";
const props = defineProps({
// 数量限制
limit: {
type: Number,
default: 1
},
minLimit: {
type: Number,
default: -1
},
multiple: {
type: Boolean,
default: false
},
fileType: {
type: String,
required: true,
validator(value: string) {
return ["audio", "image", "video"].includes(value);
}
},
appendTip: {
type: String,
default: ""
},
widthLimit: {
type: Number,
default: 0
},
heightLimit: {
type: Number,
default: 0
},
maxSize: {
type: Number,
default: 0
},
accept: {
type: String,
default: ""
},
disabled: {
type: Boolean,
default: false
},
required: {
type: Boolean,
default: true
}
});
const emits = defineEmits(["changeFileList"]);
// 可上传文件类型
const accept = ref(props.accept);
// 最大上传文件大小
const maxSize = ref(props.maxSize);
const tip = ref("");
// 根据类型设置默认值
if (props.fileType) {
switch (props.fileType) {
case "image":
accept.value = accept.value || ".png, .jpg, .jpeg, .webp";
maxSize.value = maxSize.value || 20;
tip.value = `请上传${
accept.value}格式的文件,图片大小不能超过${
maxSize.value}MB。${
props.appendTip}`;
break;
case "audio":
accept.value = accept.value || ".mp3, .wma, .aac, .flac, .ape";
maxSize.value = maxSize.value || 500;
tip.value = `请上传${
accept.value}格式的文件,音频大小不能超过${
maxSize.value}MB。${
props.appendTip}`;
break;
case "video":
accept.value = accept.value || ".mp4, .rmvb, .avi, .mov";
maxSize.value = maxSize.value || 500;
tip.value = `请上传${
accept.value}格式的文件,视频大小不能超过${
maxSize.value}MB。${
props.appendTip}`;
break;
case "musiVideo":
accept.value =
accept.value ||
".mp4, .rmvb, .avi, .mov, .mp3, .wma, .aac, .flac, .ape";
maxSize.value = maxSize.value || 500;
tip.value = `请上传${
accept.value}格式的文件,音视频大小不能超过${
maxSize.value}MB。${
props.appendTip}`;
break;
default:
throw new Error("类型错误");
}
}
const isShow = ref(true);
const elUploadElement = ref();
// 控制图片预览的路径
const previewFile = ref();
// 控制是否显示图片预览的弹窗
const dialogVisible = ref(false);
// 双向绑定的文件列表
const fileList = ref<UploadFileNew[]>([]);
// 定义组件ref
const videoRef = ref(),
audioRef = ref();
function onRemove(uploadFile: UploadFileNew, uploadFiles: UploadFileNew[]) {
// console.log("onRemove", uploadFile, uploadFiles);
verifyLength();
fileList.value = uploadFiles;
emits("changeFileList", uploadFile, "remove");
}
async function onChange(
uploadFileC: UploadFileNew, uploadFiles: UploadFileNew[]
) {
console.log("onChange", uploadFileC, uploadFiles);
const uploadFile = uploadFileC;
try {
// 如果是远程原件不需要任何处理
if (!uploadFile.name) return;
// 如果被处理过则不再处理
if (uploadFile.process) return;
isShow.value = false;
const suffix = uploadFile.name.split(".").at(-1);
if (videoRegex.test(suffix)) {
const res = (await findvideodetail(uploadFile.url)) as {
viewUrl: string;
duration: number;
};
uploadFile.type = "video";
uploadFile.viewUrl = res.viewUrl;
uploadFile.duration = res.duration;
} else if (imageRegex.test(suffix)) {
uploadFile.type = "image";
uploadFile.viewUrl = uploadFile.url;
const res = (await findImageDetail(uploadFile.url)) as {
width: number;
height: number;
};
uploadFile.width = res.width;
uploadFile.height = res.height;
} else if (audioRegex.test(suffix)) {
uploadFile.type = "audio";
uploadFile.viewUrl = new URL(
"@/assets/goods/audio.svg",
import.meta.url
).href;
const res = (await findAudioDetail(uploadFile.url)) as {
duration: number;
};
uploadFile.duration = res.duration;
}
uploadFile.needUpload = true;
uploadFile.process = true;
} catch (e) {
console.error(e);
message(e, {
type: "error"
});
}
fileList.value.push(uploadFile);
isShow.value = true;
verifyLength();
emits("changeFileList", uploadFileC);
}
// 删除文件
function handleRemove(uploadFile: UploadFile) {
elUploadElement.value.handleRemove(uploadFile);
verifyLength();
}
// 检验已选择的文件数量是否超过阈值,并做显示/隐藏处理
function verifyLength() {
const element = document.querySelector(
`#${
fileUploadId.value} .el-upload--picture-card`
) as HTMLDivElement;
if (fileList.value.length === props.limit) {
element.style.visibility = "hidden";
} else {
element.style.visibility = "visible";
}
}
// 预览文件
const handlePictureCardPreview = (file: UploadFile) => {
previewFile.value = file;
dialogVisible.value = true;
};
//截取视频第一帧作为播放前默认图片
function findvideodetail(url) {
const video = document.createElement("video"); // 也可以自己创建video
video.src = url; // url地址 url跟 视频流是一样的
const canvas = document.createElement("canvas"); // 获取 canvas 对象
const ctx = canvas.getContext("2d"); // 绘制2d
video.crossOrigin = "anonymous"; // 解决跨域问题,也就是提示污染资源无法转换视频
video.currentTime = 1; // 第一帧
return new Promise((resolve, reject) => {
video.oncanplay = () => {
canvas.width = video.clientWidth || video.width || 320; // 获取视频宽度
canvas.height = video.clientHeight || video.height || 240; //获取视频高度
// 利用canvas对象方法绘图
ctx!.drawImage(video, 0, 0, canvas.width, canvas.height);
// 转换成base64形式
const viewUrl = canvas.toDataURL("image/png"); // 截取后的视频封面
resolve({
viewUrl: viewUrl,
duration: Math.floor(video.duration)
});
video.remove();
canvas.remove();
};
video.onerror = e => {
console.error(e);
reject("文件异常,请检查文件格式,或联系管理员");
};
});
}
//截取视频第一帧作为播放前默认图片
function findAudioDetail(url) {
const audio = document.createElement("audio"); // 也可以自己创建video
audio.src = url; // url地址 url跟 视频流是一样的
audio.crossOrigin = "anonymous"; // 解决跨域问题,也就是提示污染资源无法转换视频
return new Promise((resolve, reject) => {
audio.oncanplay = () => {
resolve({
duration: Math.floor(audio.duration)
});
audio.remove();
};
audio.onerror = e => {
console.error(e);
reject("文件异常,请检查文件格式,或联系管理员");
};
});
}
//
function findImageDetail(url) {
const img = document.createElement("img"); // 也可以自己创建video
img.src = url; // url地址 url跟 视频流是一样的
return new Promise((resolve, reject) => {
img.onload = () => {
resolve({
width: img.width,
height: img.height
});
img.remove();
};
img.onerror = e => {
console.error(e);
reject("文件异常,请检查文件格式,或联系管理员");
};
});
}
type validateReturnValue = {
code: number;
success: boolean;
msg: string;
};
// 验证文件格式
function verification(): validateReturnValue {
if (props.required && fileList.value.length <= 0) {
return {
code: 0,
success: false,
msg: "请选择上传文件"
};
}
if (fileList.value.length > props.limit) {
return {
code: 0,
success: false,
msg: `文件数量超出限制,请上传${
props.limit}个及以内的文件`
};
}
if (props.minLimit > 0 && fileList.value.length < props.minLimit) {
return {
code: 0,
success: false,
msg: `文件数量低于最少限制,请上传${
props.minLimit}个及以上的文件`
};
}
for (let i = 0; i < fileList.value.length; i++) {
const element = fileList.value[i];
if (!element.needUpload) break;
if (element.size / 1024 / 1024 > maxSize.value) {
return {
code: 0,
success: false,
msg: "文件大小超出限制"
};
}
if (element.type === "image") {
if (props.widthLimit && element.width != props.widthLimit) {
return {
code: 0,
success: false,
msg: `图片宽度不等于${
props.widthLimit}像素`
};
}
if (props.heightLimit && element.height != props.heightLimit) {
return {
code: 0,
success: false,
msg: `图片高度不等于${
props.heightLimit}像素`
};
}
}
if (!accept.value.includes(element.name.split(".").at(-1).toLowerCase())) {
return {
code: 0,
success: false,
msg: `文件类型不正确,请上传${
accept.value}类型的文件`
};
}
}
return {
code: 200,
success: true,
msg: "格式正确"
};
}
// 添加远程图片
async function addFileList(url: string, name?: string) {
const uploadFile: any = {
url,
name
};
const suffix = url.split(".").at(-1);
if (videoRegex.test(suffix)) {
const res = (await findvideodetail(url)) as {
viewUrl: string;
};
uploadFile.type = "video";
uploadFile.viewUrl = res.viewUrl;
} else if (imageRegex.test(suffix)) {
uploadFile.type = "image";
uploadFile.viewUrl = uploadFile.url;
} else if (audioRegex.test(suffix)) {
uploadFile.type = "audio";
uploadFile.viewUrl = new URL(
"@/assets/goods/audio.svg",
import.meta.url
).href;
}
uploadFile.needUpload = false;
fileList.value.push(uploadFile);
verifyLength();
}
// 关闭弹窗的时候停止音视频的播放
function close() {
if (previewFile.value.type === "audio") {
audioRef.value.pause();
}
if (previewFile.value.type === "video") {
videoRef.value.pause();
}
}
// 打开弹窗的时候修改视频路径和封面
function open() {
if (previewFile.value.type === "video") {
videoRef.value.changeUrl();
}
}
// 获取文件对象
function getFiles(): UploadFileNew[] {
return fileList.value;
}
// 清理文件
function clearFileList() {
fileList.value = [];
verifyLength();
}
defineExpose({
getFiles,
verification,
addFileList,
clearFileList,
handlePictureCardPreview,
handleRemove
});
</script>
3 视频组件
<script setup lang="ts">
import {
onMounted } from "vue";
import Player from "xgplayer";
import "xgplayer/dist/index.min.css";
const props = defineProps({
// 视频路径
url: {
type: String,
},
// 封皮
poster: {
type: String,
},
});
let player: any;
onMounted(() => {
player = new Player({
id: "mse",
lang: "zh",
// 默认静音
volume: 0,
autoplay: false,
screenShot: true,
videoAttributes: {
crossOrigin: "anonymous",
},
url: props.url,
poster: props.poster,
//传入倍速可选数组
playbackRate: [0.5, 0.75, 1, 1.5, 2],
});
});
// 对外暴露暂停事件
function pause() {
player.pause();
}
// 对外暴露修改视频源事件
function changeUrl() {
player.playNext({
url: props.url,
poster: props.poster,
});
}
defineExpose({
pause, changeUrl });
</script>
<template>
<div id="mse" />
</template>
<style scoped>
#mse {
flex: auto;
margin: 0px auto;
}
</style>
4 Demo
<script setup lang="ts">
import {
ref, onMounted } from "vue";
import FileUpload from "./FileUpload/index.vue";
import type {
FormInstance } from "element-plus";
// el-form实例
const ruleFormRef = ref();
// form绑定参数
const formData = ref({
headImage: undefined,
headImageList: [],
});
// ------上传文件校验用 start-------
const FileUploadRef = ref();
let uploadRules = ref();
const isShowUpLoad = ref(true);
onMounted(() => {
isShowUpLoad.value = false;
uploadRules = ref([
{
validator(rule: any, value: any, callback: any) {
const res = FileUploadRef.value.verification();
if (!res.success) {
callback(new Error(res.msg));
}
setTimeout(() => {
callback();
}, 500);
},
trigger: "blur",
},
// 需要显示出星号,但是由于没有做数据绑定,所以放在后边
{
required: true, message: "请上传头像", trigger: "blur" },
]);
isShowUpLoad.value = true;
});
// ------上传文件校验用 end-------
const submitLoading = ref(false);
function submitForm(ruleFormRef: FormInstance) {
// 避免必需的校验无法通过
formData.value.headImageList = FileUploadRef.value.getFiles();
ruleFormRef.validate(async (valid) => {
if (valid) {
submitLoading.value = true;
const params = {
...formData.value };
// 单文件这么写,多文件需要循环
if (params.headImageList[0].needUpload)
params.headImage = await uploadFile(params.headImageList[0].raw);
delete params.headImageList;
submitLoading.value = false;
}
});
}
function uploadFile(file: File) {
return "路径";
}
</script>
<!-- -->
<template>
<el-form
ref="ruleFormRef"
:model="formData"
label-width="120px"
v-loading="submitLoading"
element-loading-text="数据传输中..."
style="margin-top: 20px;"
>
<el-form-item
label="头像"
prop="headImageList"
:rules="uploadRules"
v-if="isShowUpLoad"
>
<FileUpload ref="FileUploadRef" :multiple="false" fileType="image" />
</el-form-item>
<el-form-item label="">
<el-button>取 消</el-button>
<el-button type="primary" @click="submitForm(ruleFormRef)"
>确 认</el-button
>
</el-form-item>
</el-form>
</template>
<style lang="scss" scoped></style>
文章评论