antd vue3 图片上传组件扩展,支持多图上传 图片拖拽排序等

组件涉及到 vue3.2、vite、Ant Design Vue 3.2.16、Windi CSS样式库、vuedraggable-es拖拽库等

组件功能
  1. 图片拖拽
  2. 多图上传
  3. 自定义图片加载样式
  4. 自定义图片加载失败样式

注意:此处使用vuedraggable-es 而不是vuedraggablevue.draggable.next 官方版本,因为vuedraggableantd vue3会存在不可预知的bug问题,
报错如下:

TypeError: isFunction is not a function in vue3 esm environment

所以我们采用另一位大佬封装的vuedraggable-es
组件如下:
getAssetsFile()方法是根据图片名称assets文件中的获取静态资源图片


<template>
  <div class="component-upload-image">
    <div class="flex items-center">
      <Draggable
        v-if="fileList.length > 0"
        :list="fileList"
        :animation="100"
        item-key="url"
        class="list-group"
        :forceFallback="true"
        @end="onEnd"
      >
        <template #item="{ element }">
          <div class="items">
            <img
              class="img"
              :src="getAssetsFile('loading.gif')"
              :onload="onLoadImg(element.url)"
              @error.once="useDefaultImage"
            />
            <div class="img-mask">
              <a-space>
                <c-icon type="EyeOutlined" @click="handlePreview(element)" />
                <c-icon type="DeleteOutlined" @click="removeFn(element)" />
              a-space>
            div>
          div>
        template>
      Draggable>
      <a-upload
        class="custom-ant-upload"
        :multiple="multiple"
        :maxCount="count"
        :file-list="[]"
        :list-type="type"
        :before-upload="beforeUpload"
        :customRequest="uploadImage"
      >
        <div v-if="fileList.length < count">
          <svg-icon icon-class="icon-uploadFile" :style="{ fontSize: '32px' }" />
        div>
      a-upload>
    div>
    
    <div class="text-xs text-gray-400" v-if="showTip">
      <template v-if="fileSize">
        大小不超过
        <b class="text-red-400">{{ fileSize }}MBb>
      template>
      <template v-if="fileType">
        格式为
        <b class="text-red-400">{{ fileType.join('/') }}b>
      template>
      的文件
    div>
    <a-modal :visible="previewVisible" :title="previewTitle" width="400px" :footer="null" @cancel="handleCancel">
      <img
        :src="previewImage"
        alt="example"
        style="display: block; max-width: 100%; margin: 0 auto"
        v-realImg="previewImage"
      />
    a-modal>
  div>
template>

<script setup>
import Draggable from 'vuedraggable-es';
import { getToken } from '@/utils/auth';
import axios from 'axios';
const props = defineProps({
  // 图片数据
  listValue: [String, Object, Array],
  // 上传列表的内建样式暂只支持card样式, text, picture待定
  type: {
    type: String,
    default: 'picture-card',
  },
  // 是否支持多文件上传
  multiple: {
    type: Boolean,
    default: false,
  },
  // 预览标题
  previewTitle: {
    type: String,
    default: '预览',
  },
  // 数量限制
  count: {
    type: Number,
    default: 5,
  },
  // 大小限制(MB)
  fileSize: {
    type: Number,
    default: 5,
  },
  // 上传类型
  uploadType: {
    type: Number,
    default: 0,
  },
  // 文件类型, 例如['png', 'jpg', 'jpeg']
  fileType: {
    type: Array,
    default: () => ['png', 'jpg', 'jpeg'],
  },
  // 是否显示提示
  isShowTip: {
    type: Boolean,
    default: true,
  },
});
const emit = defineEmits(['change']);
const { proxy } = getCurrentInstance();
const fileList = ref([]);
const uploadList = ref([]);
const number = ref(0);
const previewImage = ref('');
const previewVisible = ref(false);
const fileUrl = ref('');// 图片前缀
const uploadApi = ref('') // 上传的图片服务器地址
const headers = ref({ Authorization: 'Bearer ' + getToken() });
const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));

watch(
  () => props.listValue,
  val => {
    if (val) {
      // 首先将值转为数组
      const list = Array.isArray(val) ? val : props.listValue.split(',');
      // 然后将数组转为对象数组
      fileList.value = list.map(item => {
        if (typeof item === 'string') {
          if (item.indexOf(fileUrl) === -1) {
            item = { name: fileUrl + item, url: fileUrl + item };
          } else {
            item = { name: item, url: item };
          }
        }
        return item;
      });
    } else {
      fileList.value = [];
      return [];
    }
  },
  { deep: true, immediate: true }
);

/**
 * @description: 图片加载中,在加载完成时触发
 * @param {*} url 上传的图片地址
 * @return {*} 上传的图片地址
 */
const onLoadImg = url => {
  return 'this.src=' + '"' + url + '";this.οnlοad=null';
};
/**
 * @description: 图片加载失败
 * @param {*} event
 * @return {*} 图片加载失败默认图片
 */
const useDefaultImage = event => {
  event.target.src = proxy.getAssetsFile('loadError.png');
};

/**
 * @description: 拖拽结束的事件
 * @return {*} 返回图片相对路径
 */
const onEnd = () => {
  emit('change', listToString(fileList.value));
};

/**
 * @description: 删除按钮
 * @param {*} file 删除的文件
 * @return {*}
 */
const removeFn = file => {
  const findex = fileList.value.map(f => f.url).indexOf(file.url);
  if (findex > -1 && uploadList.value.length === number.value) {
    fileList.value.splice(findex, 1);
    emit('change', listToString(fileList.value));
    return false;
  }
};

/**
 * @description: 上传前状态
 * @param {*} file 上传文件
 * @return {*}
 */
const beforeUpload = file => {
  return new Promise((resolve, reject) => {
    let fileExtension = '';
    if (file.name.lastIndexOf('.') > -1) {
      fileExtension = file.name.slice(file.name.lastIndexOf('.') + 1);
    }
    // 文件类型(file.type)
    const isTypeOk = props.fileType.some(type => {
      if (file.type.indexOf(type) > -1) return true;
      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
      return false;
    });
    if (!isTypeOk) {
      proxy.$modal.error(`文件格式不正确, 请上传${props.fileType.join('/')}格式文件!`, 3);
      return false;
    }
    // 大小限制(file.size)
    const isLt2M = file.size / 1024 / 1024 < props.fileSize;
    if (!isLt2M) {
      proxy.$modal.error(`图片大小限制${props.fileSize}MB!`, 3);
      return reject(false);
    }
    number.value++;
    resolve(isTypeOk, isLt2M);
  });
};

/**
 * @description: 上传成功
 * @param {*} info  file fileList event
 * @return {*}
 */
const uploadImage = async options => {
  const { onSuccess, onError, file, onProgress } = options;
  let formData = new FormData();
  formData.append('file', file);
  formData.append('uploadType', props.uploadType);
  onProgress({ percent: 80 });
  try {
    axios({
      url: uploadApi.value,
      method: 'post',
      headers: headers.value,
      data: formData,
    }).then(res => {
      if (res.status !== 200) {
        onError(res?.data?.data, file);
        return proxy.$modal.error(`上传失败`, 3);
      }
      onProgress({ percent: 100 });
      onSuccess(res?.data?.data, file);
      const { fileName, filePath } = res?.data?.data;
      uploadList.value.push({ name: fileName, url: fileUrl.value + filePath });
      uploadedSuccessfully();
    });
  } catch (error) {
    console.log(`上传文件出错`, error);
  }
};
/**
 * @description: 上传结束处理
 * @return {*}
 */
const uploadedSuccessfully = () => {
  if (number.value > 0 && uploadList.value.length === number.value) {
    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
    uploadList.value = [];
    number.value = 0;
    emit('change', listToString(fileList.value));
  }
};
/**
 * @description: 预览图片-开启
 * @param {*} file 预览图片
 * @return {*}
 */
const handlePreview = file => {
  previewImage.value = file.url;
  previewVisible.value = true;
};

/**
 * @description: 预览弹窗 关闭
 * @return {*}
 */
const handleCancel = () => {
  previewVisible.value = false;
};

/**
 * @description: 对象转成指定字符串分隔
 * @param {*} files 转换文件数组
 * @param {*} separator 对象转成指定字符串分隔 例如 ","
 * @return {*} 指定分隔符 分隔的字符串
 */
const listToString = (files, separator) => {
  let list = toRaw(files);
  let strs = '';
  separator = separator || ',';
  for (let i in list) {
    if (undefined !== list[i].url && list[i].url.indexOf('blob:') !== 0) {
      strs += list[i].url.replace(fileUrl.value, '') + separator;
    }
  }
  return strs != '' ? strs.substr(0, strs.length - 1) : '';
};
script>
<style lang="scss" scoped>
.custom-ant-upload :deep(.ant-upload.ant-upload-select-picture-card) {
  width: 64px !important;
  height: 64px !important;
  margin: 8 !important;
}
.list-group {
  display: flex;
  padding: 8px;
}
.items {
  position: relative;
  width: 64px;
  height: 64px;
  margin: 0 8px 8px 0;
  border-radius: 8px;
}
.img {
  width: 64px;
  height: 64px;
  border-radius: 8px;
}
.img-mask {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.5);
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.3s;
}
.img-mask:hover {
  opacity: 1;
}
style>

你可能感兴趣的:(antd,javascript,前端,vue3,vite,vuedraggable)