说明:上传是将信息(文件、图片、视频等)通过上传工具发布到远程服务器上的过程。

引入

1、引入文件夹UFileUpload
2、裁剪功能需要安装插件 vue-cropper
版本"vue-cropper": “^1.1.1”

主要功能

最大尺寸以及限制上传数量、文件样式、大文件上传视频、裁剪尺寸、文案配置、按钮配置可查看、删除、下载、禁用和回显查看

主要代码
<template>
  <div class="clearfix">
    <!-- 视频预览 -->
    <div class="video-wrapper" v-if="reviewUrl">
      <video :src="reviewUrl" width="320" height="200" controls>
        <source :src="reviewUrl" type="video/mp4" />
        <source :src="reviewUrl" type="video/ogg" />
        <source :src="reviewUrl" type="video/webm" />
        <object :data="reviewUrl" width="320" height="240">
          <embed :src="reviewUrl" width="320" height="240" />
        </object>
      </video>
      <DeleteOutlined v-if="!disabled" @click="deleteHandle" class="delete_icon" />
    </div>
    <Upload
      v-else
      v-model:file-list="localFileList"
      :action="action"
      :customRequest="crop || largeVideo ? () => {} : undefined"
      :headers="headers"
      :accept="accept"
      :list-type="listType"
      @preview="handlePreview"
      :maxCount="maxCount"
      @change="handleChange"
      @remove="removeChange"
      @download="handleDownload"
      :before-upload="beforeUpload"
      :multiple="multiple"
      :showUploadList="{
        showDownloadIcon: downLoad,
        showPreviewIcon: preview,
        showRemoveIcon: !disabled && remove,
      }"
      :disabled="disabled"
    >
      <template v-if="!disabled">
        <!-- 图片上传卡片 -->
        <template v-if="listType === ListTypeEnum.PICTURE_CARD">
          <div v-if="!maxCount || localFileList.length < maxCount">
            <PlusOutlined />
            <p>{{ uploadText }}</p>
          </div>
        </template>
        <a-button v-else>
          <upload-outlined />
          {{ uploadText }}
        </a-button>
      </template>
    </Upload>
    <!-- 信息描述 -->
    <p v-if="extraInfo">{{ extraInfo }}</p>
    <!-- 预览弹窗 -->
    <Modal :visible="previewVisible" :footer="null" @cancel="previewVisible = false">
      <img alt="无法查看" style="width: 100%" :src="previewImage" />
    </Modal>
    <!-- 裁剪弹窗 -->
    <imageCut
      ref="imageCutRef"
      :autoCropWidth="autoCropWidth"
      :autoCropHeight="autoCropHeight"
      @change="cropChange"
      :action="action"
      :headers="headers"
      @cancel="localFileList.pop()"
    />
  </div>
</template>
<script lang="ts" setup>
  import { ref, watch } from 'vue';
  import type { Ref } from 'vue';
  import { Upload, Modal, message } from 'ant-design-vue';
  import { PlusOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons-vue';
  import { ListTypeEnum, UploadResultStatus } from './typing';
  import type { UploadFile, UploadChangeParam } from 'ant-design-vue';
  import { useALinkToDownload, getBase64, simplifyFileList } from './utils';
  import { useUserStoreWithOut } from '/@/store/modules/user';
  import imageCut from './imageCut.vue';
  import { object } from 'vue-types';

  const props = defineProps({
    // 图片地址
    fileList: {
      type: Array,
      defalut: [],
      require: true,
    },
    // 最多数量
    maxCount: {
      type: Number,
      require: false,
    },
    // 最大尺寸
    maxSize: {
      type: Number,
      default: 20,
    },
    // 接受上传的文件类型
    accept: String,
    // 上传样式
    listType: {
      type: String,
      default: 'text',
    },

    // 上传地址
    action: {
      type: String,
      require: true,
      default: '',
    },
    // 请求头
    headers: {
      type: Object,
      default() {
        return { 'zj-unicom-token': useUserStoreWithOut().getToken };
      },
    },
    // 是否可以预览
    preview: {
      type: Boolean,
      default: true,
    },
    // 是否可以下载
    downLoad: {
      type: Boolean,
      default: true,
    },
    // 是否可以删除
    remove: {
      type: Boolean,
      default: true,
    },
    // 是否禁用(查看)
    disabled: {
      type: Boolean,
      default: false,
    },
    // 是否多选
    multiple: {
      type: Boolean,
      default: false,
    },
    // 上传说件说明
    extraInfo: {
      type: String,
      default: '',
    },
    // 上传文案
    uploadText: {
      type: String,
      default: '上传',
    },
    // 是否裁剪
    crop: {
      type: Boolean,
      default: false,
    },
    // 剪切宽度
    autoCropWidth: {
      type: Number,
      default: 300,
    },
    // 剪切高度
    autoCropHeight: {
      type: Number,
      default: 200,
    },
    // 是否展示裁剪预览图
    cropPreview: {
      type: Boolean,
      default: true,
    },
    // 是否大文件上传视频
    largeVideo: {
      type: Boolean,
      default: false,
    },
    // 大文件上传地址
    largeAction: {
      type: String,
      default: '',
    },
  });
  const localFileList: Ref<UploadFile[]> = ref([]);
  const ERRORTEXT = '网络异常';
  // 回显照片
  watch(
    () => props.fileList,
    (val: any) => {
      if (val && val.length && !localFileList.value.length) {
        localFileList.value.push(...val);
      }
    },
    { immediate: true },
  );

  const emit = defineEmits(['update:fileList', 'change']);
  // 判断尺寸大小
  const beforeUpload = (file) => {
    const isLtM = file.size / 1024 / 1024 < props.maxSize;
    if (!isLtM) {
      message.error(`上传内容必须小于${props.maxSize}M`);
    }
    let videoType = true;
    if (props.largeVideo && !props.accept) {
      videoType = file.type.includes('video');
      if (!videoType) {
        message.error('该文件不符合类型!');
      }
    }
    return isLtM && videoType;
  };

  // 裁剪组件
  const imageCutRef = ref();
  // file的change事件
  const handleChange = ({ file, fileList }: UploadChangeParam) => {
    if (!file.status) {
      // beforeUpload返回false,不生效
      // 所以不符合尺寸和类型的change时,没有statue就删除
      fileList.pop();
    }
    emit('update:fileList', [fileList]);
    if (props.crop) {
      file.originFileObj &&
        getBase64(file.originFileObj, (imageUrl) => {
          imageCutRef.value.showEdit({ img: imageUrl }, file.name);
        });
    } else if (props.largeVideo) {
      largeVideoHandle(file);
    } else {
      uplaodHandle({ file, fileList });
    }
  };
  // 普通上传
  const uplaodHandle = ({ file, fileList }) => {
    if (file.status === UploadResultStatus.DONE) {
      if (file.response.code === 200) {
        message.success('上传成功');
      } else {
        fileList.pop();
        message.error(`${file.response.msg || file.response.message || ERRORTEXT}`);
      }
      const formatedFileList = simplifyFileList(fileList as UploadFile[]);
      emit('update:fileList', formatedFileList);
      emit('change', { fileList: formatedFileList });
    }
  };
  // 视频地址
  const reviewUrl = ref('');
  // 视频上传
  const largeVideoHandle = (file) => {
    const params = `?fileName=${encodeURIComponent(file.name)}`;
    fetch(props.largeAction + params, {
      headers: props.headers,
    })
      .then((response) => response.json())
      .then((res) => {
        if (res.code === 200) {
          createUploadHttp(res.data, file.originFileObj);
        } else {
          if (!props.action) {
            message.error(res.msg || res.message || ERRORTEXT);
            return;
          }
          videoOldUpdate(file);
        }
      });
  };
  // 上传的老接口
  const videoOldUpdate = (file) => {
    let formData = new FormData();
    formData.append('file', file.originFileObj, file.name || 'file.png');
    fetch(props.action || '', {
      method: 'POST', // 设置请求方法为POST
      headers: props.headers,
      body: formData,
    })
      .then((response) => response.json())
      .then((res) => {
        if (res.code === 200) {
          reviewUrl.value = res.data;
          const temp = { url: reviewUrl.value, name: file.name };
          emit('change', { fileList: temp });
          localFileList.value.pop();
          message.success('上传成功');
        } else {
          message.error(res.msg || res.message || ERRORTEXT);
        }
      });
  };
  // 视频上传
  const createUploadHttp = (url, file) => {
    let http = new XMLHttpRequest();
    http.onload = () => {
      if (http.status === 200) {
        reviewUrl.value = url.split('?')[0];
        const temp = { url: reviewUrl.value, name: file.name };
        emit('change', { fileList: temp });
        localFileList.value.pop();
        message.success('上传成功');
      } else {
        message.warning('视频上传失败');
        return false;
      }
    };
    http.open('PUT', url, true);
    http.send(file);
  };
  // 剪切回调
  const cropChange = (data, name) => {
    localFileList.value.pop();
    localFileList.value.push({
      url: data,
      name,
      type: 'image/png',
      status: UploadResultStatus.DONE,
      uid: Math.random().toString(),
    });
    const formatedFileList = simplifyFileList(localFileList.value);
    emit('update:fileList', formatedFileList);
    emit('change', { fileList: formatedFileList });
  };
  // 下载
  const handleDownload = (file) => {
    const url = file.url || file.response?.data?.url || file?.response?.data;
    useALinkToDownload(url, file.name);
  };

  // 预览
  const previewVisible = ref<boolean>(false);
  const previewImage = ref<string | undefined>('');

  const handlePreview = (file: any) => {
    console.log('file: ', file);
    const url = file.url || file.response?.data?.url || file?.response?.data;
    // if (file.type?.includes('image') || ) {
    previewImage.value = url;
    previewVisible.value = true;
    // }
  };
  // 移除图片
  const deleteHandle = () => {
    reviewUrl.value = '';
    emit('change', null);
  };

  const removeChange = (file) => {
    localFileList.value = localFileList.value.filter((item) => {
      return item !== file;
    });
    const formatedFileList = simplifyFileList(localFileList.value);
    emit('update:fileList', formatedFileList);
    emit('change', { fileList: formatedFileList });
  };
</script>
<style lang="less" scoped>
  .delete_icon {
    cursor: pointer;
    margin-left: 10px;
    height: 30px;
    width: 30px;
    font-size: 20px;
  }
</style>

裁剪组件代码

<template>
  <Modal
    title="裁剪"
    :visible="visible"
    :confirm-loading="confirmLoading"
    @ok="handleOk()"
    @cancel="handleCancel"
  >
    <div class="cropper-content">
      <div>
        <div class="cropper">
          <vue-cropper
            ref="cropperRef"
            :img="option.img"
            :outputSize="option.outputSize"
            :outputType="option.outputType"
            :info="option.info"
            :canScale="option.canScale"
            :autoCrop="option.autoCrop"
            :autoCropWidth="option.autoCropWidth"
            :autoCropHeight="option.autoCropHeight"
            :fixed="option.fixed"
            :fixedNumber="option.fixedNumber"
            :full="option.full"
            :fixedBox="option.fixedBox"
            :canMove="option.canMove"
            :canMoveBox="option.canMoveBox"
            :original="option.original"
            :centerBox="option.centerBox"
            :height="option.height"
            :infoTrue="option.infoTrue"
            :maxImgSize="option.maxImgSize"
            :enlarge="option.enlarge"
            :mode="option.mode"
            @real-time="realTime"
          />
        </div>
        <!--底部操作工具按钮-->
        <div class="footer-btn">
          <div class="scope-btn">
            <label class="btn" for="uploads">更换图片</label>
            <input
              type="file"
              id="uploads"
              style="position: absolute; clip: rect(0 0 0 0)"
              accept="image/png, image/jpeg, image/gif, image/jpg"
              @change="selectImg($event)"
            />
            <a-button type="dashed" @click="changeScale(1)" class="mr">+ 放大</a-button>
            <a-button type="dashed" @click="changeScale(-1)" class="mr">- 缩小</a-button>
            <a-button type="dashed" @click="rotateLeft" class="mr">↺ 左旋转</a-button>
            <a-button type="dashed" @click="rotateRight" class="mr">↺ 右旋转</a-button>
          </div>
        </div>
      </div>
      <!--预览效果图-->
      <div class="show-preview" v-if="props.cropPreview">
        <div :style="previews.div" class="preview">
          <img :src="previews.url" :style="previews.img" />
        </div>
      </div>
    </div>
  </Modal>
</template>

<script lang="ts" setup>
  import { VueCropper } from 'vue-cropper';
  import 'vue-cropper/dist/index.css';
  import { Modal, message } from 'ant-design-vue';
  import { ref } from 'vue';

  const emit = defineEmits(['change', 'cancel']); // 数组形式定义

  const props = defineProps({
    autoCropWidth: {
      // 默认生成截图框宽度
      type: Number,
      default: 300,
    },
    autoCropHeight: {
      // 默认生成截图框高度
      type: Number,
      default: 200,
    },
    // 是否直接展示预览图
    cropPreview: {
      type: Boolean,
      default: true,
    },
    // 上传地址
    action: {
      type: String,
      require: true,
    },
    headers: {
      type: Object,
    },
  });
  //封面原图
  const originUrl = ref('');
  const confirmLoading = ref(false);
  const visible = ref(false);
  const previews = ref({
    div: '',
    img: '',
    url: '',
  });
  const option = ref({
    img: '', //裁剪图片的地址
    outputSize: 1, //裁剪生成图片的质量(可选0.1 - 1)
    outputType: 'png', //裁剪生成图片的格式(jpeg || png || webp)
    info: true, //图片大小信息
    canScale: true, //图片是否允许滚轮缩放
    autoCrop: true, //是否默认生成截图框
    autoCropWidth: props.autoCropWidth, //默认生成截图框宽度
    autoCropHeight: props.autoCropHeight, //默认生成截图框高度
    fixed: true, //是否开启截图框宽高固定比例
    fixedNumber: [props.autoCropWidth, props.autoCropHeight], //截图框的宽高比例
    full: false, //false按原比例裁切图片,不失真
    fixedBox: true, //固定截图框大小,不允许改变
    canMove: false, //上传图片是否可以移动
    canMoveBox: true, //截图框能否拖动
    original: false, //上传图片按照原始比例渲染
    centerBox: false, //截图框是否被限制在图片里面
    height: true, //是否按照设备的dpr 输出等比例图片
    infoTrue: false, //true为展示真实输出图片宽高,false展示看到的截图框宽高
    maxImgSize: 3000, //限制图片最大宽度和高度
    enlarge: 2, //图片根据截图框输出比例倍数
    mode: 'contain', //图片默认渲染方式
  });
  const cropperRef = ref();
  //初始化函数
  //图片缩放
  const changeScale = (num) => {
    num = num || 1;
    cropperRef.value.changeScale(num);
  };
  //向左旋转
  const rotateLeft = () => {
    cropperRef.value.rotateLeft();
  };
  //向右旋转
  const rotateRight = () => {
    cropperRef.value.rotateRight();
  };
  //选择图片
  const selectImg = (e) => {
    let file = e.target.files[0];
    if (!/\.(jpg|jpeg|png|JPG|PNG|JPEG)$/.test(e.target.value)) {
      message.error('图片类型要求:jpeg、jpg、png');
      return false;
    }
    //转化为blob
    let reader = new FileReader();
    reader.onload = (e: any) => {
      let data;
      if (typeof e.target.result === 'object') {
        data = window.URL.createObjectURL(new Blob([e.target.result]));
      } else {
        data = e.target.result;
      }
      option.value.img = data;
    };
    //转化为base64
    reader.readAsDataURL(file);
  };

  //实时预览函数
  const realTime = (data) => {
    previews.value = data;
  };
  //上传图片
  const handleOk = () => {
    //获取截图的blob数据
    cropperRef.value.getCropBlob(async (data) => {
      let formData = new FormData();
      formData.append('file', data, fileName.value || 'file.png');
      confirmLoading.value = true;
      fetch(props.action || '', {
        method: 'POST', // 设置请求方法为POST
        headers: props.headers,
        body: formData,
      })
        .then((response) => response.json())
        .then((res) => {
          confirmLoading.value = false;
          if (res.code === 200) {
            emit('change', res.data, fileName.value);
            message.success('上传成功');
            visible.value = false;
          } else {
            message.error(res.msg || res.message || '网络异常');
          }
        });
    });
  };
  const fileName = ref('');

  const showEdit = async (record, name) => {
    visible.value = true;
    option.value = Object.assign(option.value, record);
    fileName.value = name;
  };
  const handleCancel = () => {
    originUrl.value = '';
    visible.value = false;
    emit('cancel');
  };
  defineExpose({
    showEdit,
  });
</script>

<style scoped lang="scss">
  .cropper-content {
    .cropper {
      width: auto;
      height: 300px;
    }
    .show-preview {
      flex: 1;
      display: flex;
      justify-content: center;
      margin-top: 20px;

      .preview {
        overflow: hidden;
        background: #ebebeb;
      }
    }
  }

  .footer-btn {
    margin-top: 30px;
    display: flex;
    justify-content: flex-start;

    .scope-btn {
      display: flex;
      justify-content: space-between;
      padding-right: 10px;
    }

    .mr {
      margin-right: 10px;
    }

    .upload-btn {
      flex: 1;
      display: flex;
      justify-content: center;
    }

    .btn {
      cursor: pointer;
      text-align: center;
      padding: 0 15px;
      line-height: 32px;
      font-size: 14px;
      border-radius: 3px;
      color: #fff;
      background-color: #2165f7;
      margin-right: 10px;
    }
  }
</style>
GitHub 加速计划 / vu / vue
207.54 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:2 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 4 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 5 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐