二次封装element-plus上传组件,提供校验、回显等功能

二次封装element-plus上传组件

  • 0 相关介绍
  • 1 效果展示
  • 2 组件主体
  • 3 视频组件
  • 4 Demo

0 相关介绍

基于element-plus框架,视频播放器使用西瓜视频播放器组件

相关能力

  • 提供图片、音频、视频的预览功能
  • 提供是否为空、文件类型、文件大小、文件数量、图片宽高校验
  • 提供图片回显功能,并保证回显的文件不会重新上传
  • 提供达到数量限制不显示element自带的加号

相关文档

  • 西瓜播放器
  • element-plus

1 效果展示

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2 组件主体

<template>
  <el-upload
    list-type="picture-card"
    :auto-upload="false"
    :on-change="onChange"
    :on-remove="onChange"
    :multiple="props.multiple"
    :limit="props.limit"
    :accept="accept"
    ref="elUploadElement"
    :file-list="fileList"
    :id="fileUploadId"
  >
    <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)">
            <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"
    />
    <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";
// 多个组件同时使用的时候做区分
const fileUploadId = ref(`ID${Math.floor(Math.random() * 100000)}`);
type UploadFileNew = {
  [Property in keyof UploadFile]: UploadFile[Property];
} & {
  type: string;
  viewUrl: string | undefined;
  needUpload: boolean;
  duration: number;
  width: number;
  height: number;
};

const imageRegex = RegExp(/(jpg|bmp|gif|ico|pcx|jpeg|tif|png|raw|tga)/);
const audioRegex = RegExp(/(mp3|wav|flac|ogg|aac|wma)/);
const videoRegex = RegExp(
  /(avi|wmv|mpeg|mp4|m4v|mov|asf|flv|f4v|rmvb|rm|3gp|vob)/
);
// 导入视频组件
import videoComponent from "./videoComponent.vue";
const props = defineProps({
  // 数量限制
  limit: {
    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,
  },
});
// 可上传文件类型
const accept = ref("");
// 最大上传文件大小
const maxSize = ref(0);
const tip = ref("");
// 根据类型设置默认值
if (props.fileType) {
  switch (props.fileType) {
    case "image":
      accept.value = ".png, .jpg, .jpeg";
      maxSize.value = 20;
      tip.value = `请上传${accept.value}格式的文件,图片大小不能超过${maxSize.value}MB。${props.appendTip}`;
      break;
    case "audio":
      accept.value = ".mp3, .wma, .aac, .flac, .ape";
      maxSize.value = 500;
      tip.value = `请上传${accept.value}格式的文件,音频大小不能超过${maxSize.value}MB。${props.appendTip}`;
      break;
    case "video":
      accept.value = ".mp4, .rmvb, .avi, .mov";
      maxSize.value = 500;
      tip.value = `请上传${accept.value}格式的文件,视频大小不能超过${maxSize.value}MB。${props.appendTip}`;
      break;
    case "musiVideo":
      accept.value = ".mp4, .rmvb, .avi, .mov, .mp3, .wma, .aac, .flac, .ape";
      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();
async function onChange(
  uploadFile: UploadFileNew,
  uploadFiles: UploadFileNew[]
) {
  // 如果是远程原件不需要任何处理
  if (!uploadFile.name) return;
  isShow.value = false;
  const suffix = uploadFile.name.split(".").at(-1) as string;
  if (videoRegex.test(suffix.toLocaleLowerCase())) {
    const res = (await findvideodetail(uploadFile.url as string)) 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 string)) 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 string)) as {
      duration: number;
    };
    uploadFile.duration = res.duration;
  }
  console.log(uploadFile);
  uploadFile.needUpload = true;
  fileList.value = uploadFiles;
  isShow.value = true;
  verifyLength();
}

// 删除文件
function handleRemove(uploadFile: UploadFile) {
  elUploadElement.value.handleRemove(uploadFile);
  verifyLength();
}

// 检验已选择的文件数量是否超过阈值,并做显示/隐藏处理
function verifyLength() {
  const element = document.querySelector(
    `#${fileUploadId.value} .el-upload--picture-card`
  ) as HTMLDivElement;
  console.log(fileUploadId.value, element);
  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: string) {
  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) => {
    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.ceil(video.duration),
        width: video.width,
        height: video.height,
      });
      video.remove();
      canvas.remove();
    };
  });
}
//截取视频第一帧作为播放前默认图片
function findAudioDetail(url: string) {
  const audio = document.createElement("audio"); // 也可以自己创建video
  audio.src = url; // url地址 url跟 视频流是一样的
  audio.crossOrigin = "anonymous"; // 解决跨域问题,也就是提示污染资源无法转换视频
  return new Promise((resolve) => {
    audio.oncanplay = () => {
      resolve({
        duration: Math.ceil(audio.duration),
      });
      audio.remove();
    };
  });
}
//
function findImageDetail(url: string) {
  const img = document.createElement("img"); // 也可以自己创建video
  img.src = url; // url地址 url跟 视频流是一样的
  return new Promise((resolve) => {
    img.onload = () => {
      resolve({
        width: img.width,
        height: img.height,
      });
      img.remove();
    };
  });
}

type validateReturnValue = {
  code: number;
  success: boolean;
  msg: string;
};
// 验证文件格式
function verification(): validateReturnValue {
  if (fileList.value.length <= 0) {
    return {
      code: 0,
      success: false,
      msg: "请选择上传文件",
    };
  }
  if (fileList.value.length > props.limit) {
    return {
      code: 0,
      success: false,
      msg: `文件数量超出限制,请上传${props.limit}以内的文件`,
    };
  }
  for (let i = 0; i < fileList.value.length; i++) {
    const element = fileList.value[i];
    if (!element.needUpload) break;
    const suffix = element.name.split(".").at(-1) as string;
    if (!accept.value.includes(suffix.toLowerCase())) {
      return {
        code: 0,
        success: false,
        msg: `文件类型不正确,请上传${accept.value}类型的文件`,
      };
    }
    if ((element.size as number) / 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}像素`,
        };
      }
    }
  }
  return {
    code: 200,
    success: true,
    msg: "格式正确",
  };
}

// 添加远程图片
async function addFileList(url: string, name?: string) {
  const uploadFile: any = {
    url,
    name,
  };
  const suffix = url.split(".").at(-1) as string;
  if (videoRegex.test(suffix.toLocaleLowerCase())) {
    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() {
  videoRef.value.changeUrl();
}
// 获取文件对象
function getFiles(): UploadFileNew[] {
  return fileList.value;
}
defineExpose({
  getFiles,
  verification,
  addFileList,
  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>

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

盛元广通食品药品检验检测实验室LIMS系统

随着食品与制药行业法规标准的日益提高和国家两化融合的不断推进&#xff0c;为保障检验工作的客观、公正及科学性&#xff0c;确保制药企业对于生产、实验室、物流、管理的信息化和智能化需求越来越明确&#xff0c;为确保新品可及时得到科学准确的检测检验结果&#xff0c;盛…

H5 和小程序的区别

什么是小程序&#xff1f; 从“微信之父” 张小龙的定义里&#xff0c;我们可以了解到&#xff0c;小程序其实就是内嵌在微信&#xff0c;不需要安装和卸载的一种新应用形态。它具备的两个强属性&#xff1a;提高效率&#xff0c;用完即走&#xff01;因此小程序的设计以轻便、…

微服务02-docker

1、Docker架构 1.1 镜像和容器 Docker中有几个重要的概念: 镜像(Image):Docker将应用程序及其所需的依赖、函数库、环境、配置等文件打包在一起,称为镜像。Docker镜像是用于创建 Docker 容器的模板 。就像面向对象编程中的类。 容器(Container):镜像中的应用程序运…

02:STM32--EXTI外部中断

目录 一:中断 1:简历 2:AFIO 3:EXTI ​编辑 4:NVIC基本结构 5:使用步骤 二:中断的应用 A:对外式红外传感计数器 1:连接图​编辑 2:函数介绍 3:硬件介绍 4:计数代码 B;旋转编码计数器 1:连接图 2:硬件介绍 3:旋转编码器代码: 一:中断 1:简历 中断&#xff1a;在主程…

OpenCV基本操作——图像的基础操作

目录 图像的IO操作读取图像显示图像保存图像 绘制几何图形绘制直线绘制圆形绘制矩形向图像中添加文字效果展示 获取并修改图像中的像素点获取图像的属性图像通道的拆分与合并色彩空间的改变 图像的IO操作 读取图像 cv2.imread()import numpy as np import cv2 imgcv2.imread(…

【Java从0到1学习】08 String类

1. 概述 字符串是由多个字符组成的一串数据(字符序列)&#xff0c;字符串可以看成是字符数组。 在实际开发中&#xff0c;字符串的操作是最常见的操作&#xff0c;没有之一。而Java没有内置的字符串类型&#xff0c;所以&#xff0c;就在Java类库中提供了一个类String 供我们…

Python爬虫:单线程、多线程、多进程

前言 在使用爬虫爬取数据的时候&#xff0c;当需要爬取的数据量比较大&#xff0c;且急需很快获取到数据的时候&#xff0c;可以考虑将单线程的爬虫写成多线程的爬虫。下面来学习一些它的基础知识和代码编写方法。 一、进程和线程 进程可以理解为是正在运行的程序的实例。进…

jvs-rules API数据源配置说明(含配置APIdemo视频)

在JVS中&#xff0c;多数据源支持多种形态的数据接入&#xff0c;其中API是企业生产过程中常见的数据形态。使用数据源的集成配置&#xff0c;以统一的方式管理和集成多个API的数据。这些平台通常提供各种数据转换和处理功能&#xff0c;使得从不同数据源获取和处理数据变得更加…

搭建一个能与大家分享的旅游相册网站——“cpolar内网穿透”

如何用piwigo与cpolar结合共同搭建一个能分享的旅行相册网站 文章目录 如何用piwigo与cpolar结合共同搭建一个能分享的旅行相册网站前言1. 使用piwigo这款开源的图片管理软件2. 需要将piwigi网页复制到phpstudy3. “开始安装”进入自动安装程序4. 创建新相册5. 创建一条空白数据…

Spring Gateway+Security+OAuth2+RBAC 实现SSO统一认证平台

背景&#xff1a;新项目准备用SSO来整合之前多个项目的登录和权限&#xff0c;同时引入网关来做后续的服务限流之类的操作&#xff0c;所以搭建了下面这个系统雏形。 关键词&#xff1a;Spring Gateway, Spring Security, JWT, OAuth2, Nacos, Redis, Danymic datasource, Jav…

ansible剧本之role角色模块

role角色 一&#xff1a;Roles 模块1.roles 的目录结构&#xff1a;2.roles 内各目录含义解释3.在一个 playbook 中使用 roles 的步骤&#xff1a;&#xff08;1&#xff09;创建以 roles 命名的目录&#xff08;2&#xff09;创建全局变量目录&#xff08;可选&#xff09;&am…

Java进阶-Oracle(二十一)(2)

&#x1f33b;&#x1f33b; 目录 一、Oracle 数据库的操作(DDL DML DQL DCL TPL)1.1 标识符、关键字、函数等1.1.1 数值类型&#xff1a;1.1.2 字符串类型&#xff1a;1.1.3 日期类型1.1.4 大的数据类型--适合保存更多的数据 1.2 运算符1.3 函数---预定义函数、自定义函数&…

户外组网摆脱布线困扰,工业5G网关实现无人值守、远程实时监控

在物联网通信技术发达的2023&#xff0c;网络覆盖对所及之处的全面覆盖&#xff0c;科技发展的促使下很多高危户外场景也在思考如何利用无线技术提高人员安全及现场无人化管理。 煤矿是我们国家不可缺少的重要能源&#xff0c;其开采过程的危险系数也是众所皆知的&#xff0c;…

【Linux】线程的概念以及与进程的区别

目录 背景知识 什么是线程&#xff1f; 进程和线程的区别 线程的优缺点 背景知识 在了解线程前&#xff0c;我们要首先知道&#xff0c;OS是可以做到让进程进行细粒度划分的! 比如我们所说的进程地址空间中的堆区&#xff0c;它在进程PCB中的mm_structz中有一个start和…

深度学习关键要素:数据集汇总与分享

引言 在深度学习的应用中&#xff0c;数据被认为是最重要的因素之一。因此&#xff0c;选择一个好的数据集对于深度学习的成功至关重要。在选择数据集时&#xff0c;不仅需要关注数据量的大小、多样性以及质量&#xff0c;还要考虑数据集是否代表了所研究问题的真实情况。本文…

SpringBoot对接OpenAI

SpringBoot对接OpenAI 随着人工智能技术的飞速发展&#xff0c;越来越多的开发者希望将智能功能集成到自己的应用中&#xff0c;以提升用户体验和应用的功能。OpenAI作为一家领先的人工智能公司&#xff0c;提供了许多先进的自然语言处理和语言生成模型&#xff0c;其中包括深…

IDEA 设置字体大小无效

设置字体大小&#xff0c;一般都是从file>settings>editor>font>Size里设置&#xff0c;一般都有效。 但是&#xff0c;如果是更换了主体&#xff0c;则需要从主体颜色菜单那里这是&#xff0c;你看这个页面&#xff0c;上面黄色三角也提示你了&#xff0c;要去颜色…

5.内置构造函数

在JavaScript中最主要的数据类型有6种: 1.基本数据类型: 字符串、数值、布尔、undefined、 null 2.引用类型&#xff1a;对象 但是&#xff0c;我们会发现有些特殊情况: //普通字符串 const str andy console.1og(str.length) // 4其实字符串、数值、布尔、等基本类型也都有…

易服客工作室:WordPress 6.3 Lionel发布

WordPress 6.3 Lionel已经发布&#xff0c;它以美国著名爵士乐艺术家莱昂内尔汉普顿 (Lionel Hampton)的名字命名。汉普顿是一位多产的爵士颤音琴演奏家、钢琴家和打击乐演奏家&#xff0c;因与查尔斯明格斯、昆西琼斯等伟大人物合作以及作为同名莱昂内尔汉普顿管弦乐团的乐队领…

【ARM Cache 系列文章 9 -- ARM big.LITTLE技术】

文章目录 big.LITTLE 技术背景big.LITTLE 技术详解big.LITTLE 硬件要求 big.LITTLE 软件模型CPU MigrationGlobal Task SchedulingGlobal Task Scheduling比CPU Migration的优势 转自&#xff1a;https://zhuanlan.zhihu.com/p/630981648 如有侵权&#xff0c;请联系删除 big.L…