问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

Vue3+Element Plus+el-upload文件上传组件二次封装

创作时间:
作者:
@小白创作中心

Vue3+Element Plus+el-upload文件上传组件二次封装

引用
CSDN
1.
https://m.blog.csdn.net/mark885/article/details/144715894

本文将介绍如何使用Vue3、Element Plus和el-upload组件进行文件上传功能的二次封装。通过本教程,你将能够创建一个支持图片和视频上传、预览、删除等功能的自定义上传组件。

最终效果图

该组件支持限制上传大小、数量、预览查看、删除以及自动更新父组件值。

使用方法

<el-form-item label="附件上传" prop="imagesAndVideos">
  <uploadView
    v-model:img-list="formData.imagesAndVideos"
    :boxStyle="{ width: '80px', height: '80px' }"
    :limit="5"
    :imageSize="300"
    :videoSize="50 * 1024"
  ></uploadView>
  <div class="input-tip">支持图片和视频上传,图片大小不超过300KB,视频大小不超过50M</div>
</el-form-item>

组件代码

<template>
  <div class="custom-uploader-img-box">
    <div v-for="(item, i) in imageUrls" :key="item" class="image-container">
      <!-- 图片 -->
      <template v-if="getUploadFileType(item) == 'image'">
        <el-image class="image" :style="props.boxStyle" :src="item" :preview-src-list="imageUrls" fit="cover" />
      </template>
      <!-- 视频 -->
      <template v-if="getUploadFileType(item) == 'video'">
        <video class="image" :src="item" controls :style="props.boxStyle"></video>
      </template>
      <div class="icon-container">
        <el-icon @click="handlePictureCardPreview(item)"><zoom-in /></el-icon>
        <el-icon @click="handleRemove(i)"><Delete /></el-icon>
      </div>
    </div>
    <el-upload
      v-if="imageUrls.length < Number(props.limit)"
      ref="elUploadViewRef"
      class="avatar-uploader"
      action="#"
      :headers="uploadHeaders"
      :http-request="handleUploadForm"
      :on-success="handleAvatarSuccess"
      :before-upload="beforeAvatarUpload"
      :on-change="handleUploadChange"
      :file-list="fileList"
      :show-file-list="false"
      :limit="Number(props.limit)"
      :multiple="Number(props.limit) > 1"
    >
      <el-icon class="custom-uploader-icon" :style="props.boxStyle">
        <Plus />
      </el-icon>
      <template v-for="(value, name) in slots" #[name]="scope">
        <slot :name="name" v-bind="scope || {}"></slot>
      </template>
    </el-upload>
  </div>
  <el-dialog v-model="dialogVisible" title="预览" width="50%" align-center>
    <template v-if="dialogViewData.type == 'image'">
      <img class="dialog-view-obj" w-full :src="dialogViewData.url" />
    </template>
    <template v-if="dialogViewData.type == 'video'">
      <video class="dialog-view-obj" :src="dialogViewData.url" controls></video>
    </template>
  </el-dialog>
</template>
<script setup>
import { ref, watch, useSlots, getCurrentInstance, defineProps, defineEmits, reactive } from 'vue';
import { uploadImageApi, uploadFileApi } from '@apis/system.js';
import { useAdminStore } from '@/store';
const adminStore = useAdminStore();
const { proxy } = getCurrentInstance();
const elUploadViewRef = ref(null);
const slots = useSlots();
const props = defineProps({
  //列表数据 ['http://1111.jpg', 'http://222.jpg']
  imgList: {
    type: Array,
    required: true,
    default: () => {
      return [];
    },
  },
  //样式大小限制
  boxStyle: {
    type: Object,
    default: () => {
      return { width: '80px', height: '80px' };
    },
  },
  //上传数量限制
  limit: {
    type: [Number, String],
    default: () => {
      return 1;
    },
  },
  //上传图片 大小限制   0为不限制 -   50 * 1024 =  50MB,300 = 300KB
  imageSize: {
    type: [Number, String],
    default: () => {
      return 0;
    },
  },
  //上传视频 大小限制   0为不限制 -   50 * 1024 =  50MB,300 = 300KB
  videoSize: {
    type: [Number, String],
    default: () => {
      return 0;
    },
  },
  //上传文件 大小限制   0为不限制 -   50 * 1024 =  50MB,300 = 300KB
  fileSize: {
    type: [Number, String],
    default: () => {
      return 0;
    },
  },
});
const imageUrl = ref('');
const imageUrls = ref([]);
const fileList = ref([]);
const emit = defineEmits(['update:imgList']);
watch(
  () => props.imgList,
  (newVal, oldVal) => {
    if (Array.isArray(newVal) && newVal.length > 0) {
      imageUrls.value = newVal;
    }
  },
);
//上传文件需要 token
const merToken = computed(() => adminStore.merToken);
const uploadHeaders = reactive({
  'Authori-zation': merToken,
});
//上传成功
const handleAvatarSuccess = (response, uploadFile) => {
  const URL = window.URL || window.webkitURL;
  imageUrl.value = URL.createObjectURL(uploadFile.raw);
};
//上传前判断
const beforeAvatarUpload = (rawFile) => {
  //根据不同的文件类型判断
  const fType = getUploadFileType(rawFile.name);
  if (!fType) {
    proxy.$MsgBox.MessageError('不支持上传文件:' + rawFile.name);
    return false;
  }
  //上传图片
  if (fType == 'image') {
    if (Number(props.imageSize) > 0 && rawFile.size > Number(props.imageSize) * 1024) {
      proxy.$MsgBox.MessageError(`图片大小超过上传限制`);
      return false;
    }
  }
  //上传视频
  if (fType == 'video') {
    if (Number(props.videoSize) > 0 && rawFile.size > Number(props.videoSize) * 1024) {
      proxy.$MsgBox.MessageError(`视频大小超过上传限制`);
      return false;
    }
  }
  //上传文件
  if (fType == 'file') {
    if (Number(props.fileSize) > 0 && rawFile.size > Number(props.fileSize) * 1024) {
      proxy.$MsgBox.MessageError(`文件大小超过上传限制`);
      return false;
    }
  }
  return true;
};
const dialogViewData = reactive({ type: '', url: '' });
const dialogVisible = ref(false);
const handlePictureCardPreview = (uploadFile) => {
  dialogViewData.type = getUploadFileType(uploadFile);
  dialogViewData.url = uploadFile;
  dialogVisible.value = true;
};
const handleRemove = (i) => {
  // 从 fileList 中移除被删除的文件
  // fileList.value = fileList.value.filter(file => file.uid !== uploadFile.uid)
  imageUrls.value.splice(i, 1);
  console.log(i, imageUrls);
  emit('update:imgList', imageUrls.value);
};
//清空文件
const handleUploadChange = () => {
  if (Number(props.limit) <= 1) {
    elUploadViewRef.value && elUploadViewRef.value.clearFiles();
  }
};
// 上传
const handleUploadForm = (param) => {
  const formData = new FormData();
  formData.append('model', 'chat_img');
  formData.append('pid', 7);
  formData.append('multipart', param.file);
  //图片上传
  if (param.file.type.startsWith('image/')) {
    return uploadImage(formData); // 上传视频 与  上传图片  接口一样
  }
  //视频上传
  if (param.file.type.startsWith('video/')) {
    return uploadImage(formData); // 上传视频 与  上传图片  接口一样
  }
  return proxy.$MsgBox.MessageError(`上传文件类型错误,请重新选择`);
};
//图片上传
const uploadImage = (formData) => {
  const loading = proxy.$loading({
    lock: true,
    text: '上传中,请稍候...',
    spinner: 'el-icon-loading',
    background: 'rgba(0, 0, 0, 0.7)',
  });
  uploadImageApi(formData)
    .then((res) => {
      imageUrls.value.push(res.data.url);
      console.log('图片提交返回', res, imageUrls, fileList);
      emit('update:imgList', imageUrls.value);
      loading.close();
    })
    .catch((res) => {
      loading.close();
      proxy.$MsgBox.MessageError(res.message || '上传失败');
    })
    .finally(() => {
      loading.close();
    });
};
//文件上传
const uploadFile = (formData) => {
  const loading = proxy.$loading({
    lock: true,
    text: '上传中,请稍候...',
    spinner: 'el-icon-loading',
    background: 'rgba(0, 0, 0, 0.7)',
  });
  uploadFileApi(formData)
    .then((res) => {
      imageUrls.value.push(res.data.url);
      emit('update:imgList', imageUrls.value);
      loading.close();
    })
    .catch((res) => {
      loading.close();
      proxy.$MsgBox.MessageError(res.message || '上传失败');
    })
    .finally(() => {
      loading.close();
    });
};
//判断连接地址类型
const checkUrlPath = (path = '', ext = ['mp4']) => {
  const fileExtension = path.split('.').pop().toLowerCase();
  return ext.includes(fileExtension);
};
//根据文件名 得到文件类型
const getUploadFileType = (fileName = '') => {
  //视频
  if (checkUrlPath(fileName, ['mp4', 'flv', 'avi'])) {
    return 'video';
  }
  //图片
  if (checkUrlPath(fileName, ['jpg', 'jpeg', 'png', 'gif', 'bmp'])) {
    return 'image';
  }
  //文件
  if (checkUrlPath(fileName, ['txt', 'word', 'excel', 'pdf', 'ppt', 'zip', 'rar', '7z'])) {
    return 'file';
  }
  return '';
};
</script>
<style lang="scss" scoped>
.custom-uploader-img-box {
  width: 100%;
  display: flex;
  flex-wrap: wrap;
  .image-container {
    position: relative;
    display: inline-block;
    cursor: pointer;
    margin: 0 5px;
    img {
      border: 1px dashed var(--el-border-color);
      font-size: 28px;
      color: #8c939d;
      width: 80px;
      height: 80px;
      text-align: center;
      border-radius: 10px;
      object-fit: cover;
      margin: 0 1px;
    }
  }
  .image-container:first-child {
    margin-left: 0;
  }
}
.icon-container {
  position: absolute;
  right: 0;
  bottom: 0;
  transform: translate(0, -11px);
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: rgba(0, 0, 0, 0.5);
  color: #fff;
  height: 30px;
  border-radius: 10px 0 0 0;
  opacity: 0;
  transition: opacity 0.3s;
  .el-icon {
    font-size: 15px;
    margin: 0 6px;
  }
}
.image-container:hover .icon-container {
  opacity: 1;
}
</style>
<style lang="scss">
.avatar-uploader .el-upload {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
  border-color: var(--el-color-primary);
}
.el-icon.custom-uploader-icon {
  font-size: 24px;
  color: #8c939d;
  width: 80px;
  height: 80px;
  text-align: center;
}
.el-upload-list__item {
  align-items: center;
}
.el-dialog__body {
  margin-top: 20px;
  text-align: center;
  img {
    min-height: 50vh;
  }
}
.dialog-view-obj {
  max-width: 400px;
  height: auto;
}
</style>
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号