首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
【架构师(第一篇)】整体需求分析和架构设计
2
【架构师(第二篇)】脚手架架构设计和框架搭建
3
【架构师(第三篇)】脚手架开发之掌握Lerna操作流程
4
【架构师(第四篇)】脚手架开发之Lerna源码分析
5
【架构师(第五篇)】脚手架之import-local执行流程及简历设计
6
【架构师(第六篇)】脚手架之需求分析和架构设计
7
【架构师(第七篇)】脚手架之准备阶段编写
8
【架构师(第八篇)】脚手架之 commander 框架使用方法
9
【架构师(第九篇)】如何让 Node 环境支持 ES Module
10
【架构师(第十篇)】脚手架之注册命令及架构优化
11
【架构师(第十一篇)】脚手架之命令注册和执行过程开发
12
【架构师(第十二篇)】脚手架之命令行交互工具 inquirer.js 使用方法
13
【架构师(第十三篇)】脚手架之创建项目准备阶段开发
14
【架构师(第十四篇)】脚手架之 egg.js 和 mongodb 的使用
15
【架构师(第十五篇)】脚手架之创建项目模板开发
16
【架构师(第十六篇)】脚手架之创建项目模板的下载与更新
17
【架构师(第十七篇)】脚手架之 ejs 和 glob 的使用
18
【架构师(第十八篇)】脚手架之项目模板的安装
19
【架构师(第十九篇)】脚手架之组件库模板开发
20
【架构师(第二十篇)】脚手架之自定义模板及第一阶段总结
21
【架构师(第二十一篇)】编辑器开发之需求分析和架构设计
22
【架构师(第二十二篇)】编辑器开发之项目整体搭建
23
【架构师(第二十三篇)】编辑器开发之画布区域组件的渲染
24
【架构师(第二十四篇)】编辑器开发之添加模版到画布
25
【架构师(第二十五篇)】编辑器开发之属性编辑区域表单渲染
26
【架构师(第二十六篇)】编辑器开发之属性编辑同步渲染
27
【架构师(第二十七篇)】前端单元测试框架 Jest 基础知识入门
28
【架构师(第二十八篇)】 测试工具 Vue-Test-Utils 基础语法
29
【架构师(第二十九篇)】Vue-Test-Utils 触发事件和异步请求
30
【架构师(第三十篇)】Vue-Test-Utils 全局组件和第三方库 vuex | vue-router
31
【架构师(第三十一篇)】前端测试之 TDD 的开发方式
32
【架构师(第三十二篇)】 通用上传组件开发及测试用例
33
【架构师(第三十三篇)】 Vue 中的实例及本地图片预览
34
【架构师(第三十四篇)】 业务组件库开发之 vue3 的插件系统
35
【架构师(第三十五篇)】 业务组件库开发之使用 Rollup 进行打包
36
【架构师(第三十六篇)】 业务组件库开发之发布到 NPM
37
【架构师(第三十七篇)】 服务端开发之后端框架与数据库技术选型
38
【架构师(第三十八篇)】 服务端开发之本地安装最新版 MySQL 数据库
39
【架构师(第三十九篇)】 服务端开发之连接 MySQL 数据库
40
【架构师(第四十篇)】 服务端开发之连接 Mongodb 数据库
41
【架构师(第四十一篇)】 服务端开发之安装并连接 Redis数据库
42
【架构师(第四十二篇)】 服务端开发之常用的登录鉴权方式
43
【架构师(第四十三篇)】 服务端开发之单元测试和接口测试
44
【架构师(第四十四篇)】 服务端开发之 pm2 和 nginx 介绍
45
【架构师(第四十五篇)】 服务端开发之认识 Github actions
46
【架构师(第四十六篇)】 服务端开发之安装 Docker
47
【架构师(第四十七篇)】 服务端开发之认识 Docker
48
【架构师(第四十八篇)】 服务端开发之 Dockerfile
49
【架构师(第四十九篇)】 服务端开发之认识 Docker-compose
50
【架构师(第五十篇)】 服务端开发之自动发布到测试机
清单首页架构文章详情

【架构师(第三十二篇)】 通用上传组件开发及测试用例


主要内容

  • 使用 TDD 的开发方式,一步步开发一个上传组件
  • 分析 Element Plus 中的 uploader 组件的源码
  • 将上传组件应用到编辑器中
  • 对于知识点的发散和总结
    • Vue3 中实例的类型
    • Vue3 中组件通讯方法
    • 预览本地图片的两种方法
    • HtmlImgElement 家族的一系列关系
    • JSDOM 是什么? Jest 是怎么使用它来模拟浏览器环境的

上传组件需求分析

  • 基本上传流程
    • 点击按钮选择文件,完成上传
  • 支持查看上传文件列表
    • 文件名称
    • 上传状态
    • 上传进度
    • 删除按钮
    • 其它更丰富的显示
  • 自定义模板
    • 初始容器自定义
    • 上传完毕自定义
  • 支持一系列的生命周期钩子函数,上传事件
    • beforeUpload
    • onSuccess
    • onError
    • onChange
    • onProgress
      • 使用 aixos 内置 Api
      • 设置事件的参数
  • 支持拖拽上传
    • dargoverdargLeave 添加或者删除对应的 class
    • drop 事件拿到正在拖拽的文件,删除 class 并且触发上传
    • 事件是可选的,只有在属性 dargtrue 的时候才会生效
  • 支持手动上传
  • 等等
    • 支持自定义 headers
    • 自定义 file 的表单名称
    • 更多需要发送的数据
    • input 原生属性 multiple
    • input 原生属性 accept
    • with-credentials 发送时是否支持发送 cookie

上传文件的原理

enctype

  • 表单默认: application/x-www-form-urlencoded
  • 二进制数据: multipart/form-data

传统模式

通过 input type="file", 然后触发 formsubmit 上传。

代码语言:javascript
复制
    <from method="post"
          action="http://api/upload"
          enctype="multipart/form-data">
      <input type="file">
      <button type="submit">Submit </button>
    </from>

使用 js 模拟

代码语言:javascript
复制
    <input type="file"
           name="file"
           @change="handleFileChange">

Input 获取 Files

  • e.target.filesFileList 对象,它是一个类数组,并不是真正的数组。
  • 可以通过 files[index] 拿到对应的文件,它是 File 对象。
  • FormData 是针对 XHR2 设计的数据结构,可以完美模拟 HTMLform 标签。
代码语言:javascript
复制
import axios from 'axios';
const handleFileChange = (e: Event) => {
  // 获取文件列表
  const target = e.target as HTMLInputElement
  const files = target.files
  if (files) {
    // 获取文件
    const uploadedFile = files[0]
    // 创建 FormData 数据结构
    const formData = new FormData()
    // 往 FormData 中 添加数据
    formData.append(uploadedFile.name, uploadedFile)
    // 发送请求
    axios.post('/api/upload', formData, {
      headers: {
        // 需要在请求头中设置类型
        'Content-Type': "multipart/form-data"
      }
    }).then((resp) => {
      console.log(resp.data);
    })
  }
}

编写测试用例

基础结构

代码语言:javascript
复制
import type { VueWrapper } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Uploader from '@/components/Uploader.vue';
import axios from 'axios';
import flushPromises from 'flush-promises';

jest.mock('axios');
//将 mock 对象断言为特定类型 使用 jest.Mocked<T>
const mockAxios = axios as jest.Mocked<typeof axios>;
// 定义 wrapper
let wrapper: VueWrapper<any>;
// 定义测试文件
const testFile = new File(['xyz'], 'test.png', { type: 'image/png' });
// 测试 UserProfile.vue
describe('UserProfile.vue', () => {
  beforeAll(() => {
    // 获取组件
    wrapper = shallowMount(Uploader, {
      // 传入到组件内部的属性
      props: { action: 'https://jsonplaceholder.typicode.com/posts/' },
    });
  });
  afterEach(() => {
    // 重置 post 请求
    mockAxios.post.mockReset();
  });
});

测试初始界面渲染

代码语言:javascript
复制
  it('basic layout before uploading', async () => {
    // 存在上传按钮
    expect(wrapper.find('button').exists()).toBeTruthy();
    // 按钮文字是点击上传
    expect(wrapper.get('button').text()).toBe('点击上传');
    // input 是隐藏的
    expect(wrapper.get('input').isVisible()).toBeFalsy();
  });

测试上传成功

代码语言:javascript
复制
  it('upload process should works fine', async () => {
    // mock 成功的请求
    mockAxios.post.mockResolvedValueOnce({ status: 'success' });
    // 模拟 input 的 e.target.files
    const fileInput = wrapper.get('input').element as HTMLInputElement;
    const files = [testFile] as any;
    Object.defineProperty(fileInput, 'files', {
      value: files,
      writable: false,
    });
    // 触发 change 事件
    await wrapper.get('input').trigger('change');
    // post 请求被调用一次
    expect(mockAxios.post).toHaveBeenCalledTimes(1);
    // 按钮文字为 正在上传
    expect(wrapper.get('button').text()).toBe('正在上传');
    // 按钮状态为禁用
    expect(wrapper.get('button').attributes()).toHaveProperty('disabled');
    // 列表长度修改, 并且有正确的 class
    expect(wrapper.findAll('li').length).toBe(1);
    // 获取列表第一个元素
    const firstItem = wrapper.get('li:first-child');
    // 元素的类名包含 uploading
    expect(firstItem.classes()).toContain('upload-loading');
    // 清除 promise
    await flushPromises();
    // 按钮文字为点击上传
    expect(wrapper.get('button').text()).toBe('点击上传');
    // 元素的类名包含 upload-success
    expect(firstItem.classes()).toContain('upload-success');
    // 元素的内容正确
    expect(firstItem.get('.filename').text()).toBe(testFile.name);
  });

测试上传失败

代码语言:javascript
复制
  it('should return error text when post is rejected', async () => {
    // mock 失败的请求
    mockAxios.post.mockRejectedValueOnce({ error: 'error' });
    // 触发 change 事件
    await wrapper.get('input').trigger('change');
    // post 请求被调用2次
    expect(mockAxios.post).toHaveBeenCalledTimes(2);
    // 按钮文字为正在上传
    expect(wrapper.get('button').text()).toBe('正在上传');
    // 清除 promise
    await flushPromises();
    // 按钮文字为正在上传
    expect(wrapper.get('button').text()).toBe('点击上传');
    // 列表长度增加 列表的最后一项有正确的class名
    expect(wrapper.findAll('li').length).toBe(2);
    // 获取最后一个元素
    const lastItem = wrapper.get('li:last-child');
    // 元素的类名包含 upload-error
    expect(lastItem.classes()).toContain('upload-error');
    // 点击删除图标,可以删除这一项
    await lastItem.get('.delete-icon').trigger('click');
    // 列表长度减少1
    expect(wrapper.findAll('li').length).toBe(2);
  });

测试自定义插槽

代码语言:javascript
复制
  it('should show current custom slot', async () => {
    // 成功的请求
    mockAxios.post.mockResolvedValueOnce({ data: { url: 'aa.url' } });
    // 获取 wrapper
    const wrapper = shallowMount(Uploader, {
      props: {
        action: 'https://jsonplaceholder.typicode.com/posts/',
      },
      slots: {
        default: '<button>Custom Button</button>',
        loading: "<div class='loading'>Custom Loading</div>",
        uploaded: `<template #uploaded="{ uploadedData }">
          <div class='custom-loaded'>{{uploadedData.url}}</div>
        </template>`,
      },
    });
    // 自定义上传按钮
    expect(wrapper.get('button').text()).toBe('Custom Button');
    // 模拟 input 的 e.target.files
    const fileInput = wrapper.get('input').element as HTMLInputElement;
    const files = [testFile] as any;
    Object.defineProperty(fileInput, 'files', {
      value: files,
      writable: false,
    });
    // 触发 change 事件
    await wrapper.get('input').trigger('change');
    // 自定义loading
    expect(wrapper.get('.loading').text()).toBe('Custom Loading');
    // 清除 promise
    await flushPromises();
    // 自定义文件名称
    expect(wrapper.get('.custom-loaded').text()).toBe('aa.url');
  });

测试上传前检查

代码语言:javascript
复制
  it('before upload check', async () => {
    // 模拟一个回调函数
    const callback = jest.fn();
    // 模拟post请求
    mockAxios.post.mockResolvedValueOnce({ data: { url: 'aa.url' } });
    // 模拟上传前的check
    const checkFileSize = (file: File) => {
      if (file.size > 2) {
        callback();
        return false;
      }
      return true;
    };
    const wrapper = shallowMount(Uploader, {
      props: {
        action: 'https://jsonplaceholder.typicode.com/posts/',
        beforeUpload: checkFileSize,
      },
    });
    // 模拟 input 的 e.target.files
    const fileInput = wrapper.get('input').element as HTMLInputElement;
    const files = [testFile] as any;
    Object.defineProperty(fileInput, 'files', {
      value: files,
      writable: false,
    });
    // 触发 input 的 change 事件
    await wrapper.get('input').trigger('change');
    // post 请求没有被触发
    expect(mockAxios.post).not.toHaveBeenCalled();
    // 页面中没有生成 li
    expect(wrapper.findAll('li').length).toBe(0);
    // 回调函数被触发
    expect(callback).toHaveBeenCalled();
  });

测试上传前检查 使用失败的 promise

代码语言:javascript
复制
  it('before upload check using Promise file', async () => {
    // 模拟 post 请求
    mockAxios.post.mockRejectedValueOnce({ data: { url: 'aa.url' } });
    // 失败的情况
    const failedPromise = (file: File) => {
      return Promise.reject('wrong type');
    };
    const wrapper = shallowMount(Uploader, {
      props: {
        action: 'https://jsonplaceholder.typicode.com/posts/',
        beforeUpload: failedPromise,
      },
    });
    // 模拟 input 的 e.target.files
    const fileInput = wrapper.get('input').element as HTMLInputElement;
    const files = [testFile] as any;
    Object.defineProperty(fileInput, 'files', {
      value: files,
      writable: false,
    });
    // 触发 input 的 change 事件
    await wrapper.get('input').trigger('change');
    // 清除 promise
    await flushPromises();
    // post 请求没有被触发
    expect(mockAxios.post).not.toHaveBeenCalled();
    // 页面中没有生成 li
    expect(wrapper.findAll('li').length).toBe(0);
  });

测试上传前检查 使用成功的 promise

代码语言:javascript
复制
  it('before upload check using Promise success', async () => {
    // 模拟 post 请求
    mockAxios.post.mockRejectedValueOnce({ data: { url: 'aa.url' } });
    // 成功的情况
    const successPromise = (file: File) => {
      const newFile = new File([file], 'new_name.docx', { type: file.type });
      return Promise.reject(newFile);
    };
    const wrapper = shallowMount(Uploader, {
      props: {
        action: 'https://jsonplaceholder.typicode.com/posts/',
        beforeUpload: successPromise,
      },
    });
    // 模拟 input 的 e.target.files
    const fileInput = wrapper.get('input').element as HTMLInputElement;
    const files = [testFile] as any;
    Object.defineProperty(fileInput, 'files', {
      value: files,
      writable: false,
    });
    // 触发 input 的 change 事件
    await wrapper.get('input').trigger('change');
    // 清除 promise
    await flushPromises();
    // post 请求被触发
    expect(mockAxios.post).toHaveBeenCalled();
    // 页面中生成了一个 li
    expect(wrapper.findAll('li').length).toBe(1);
    // 获取列表第一个元素
    const firstItem = wrapper.get('li:first-child');
    // 元素的类名包含 upload-success
    expect(firstItem.classes()).toContain('upload-success');
    // 元素的内容正确
    expect(firstItem.get('.filename').text()).toBe('new_name.docx');

    // 成功的情况 返回了错误类型
    const successPromiseWrongType = (file: File) => {
      const newFile = new File([file], 'new_name.docx', { type: file.type });
      return Promise.reject(newFile);
    };
    // 设置 props
    await wrapper.setProps({ beforeUpload: successPromiseWrongType });
    // 触发 input 的 change 事件
    await wrapper.get('input').trigger('change');
    // 清除 promise
    await flushPromises();
    // post 请求没有被触发
    expect(mockAxios.post).not.toHaveBeenCalled();
    // 页面中没有生成 li
    expect(wrapper.findAll('li').length).toBe(0);
  });

测试拖拽功能

代码语言:javascript
复制
 it('test drag and drop function', async () => {
    // 模拟 post 请求
    mockAxios.post.mockResolvedValueOnce({ data: { url: 'aa.url' } });
    const wrapper = shallowMount(Uploader, {
      props: {
        action: 'https://jsonplaceholder.typicode.com/posts/',
        drag: true,
      },
    });
    // 获取上传区域
    const uploadArea = wrapper.get('.upload-area');
    // 触发 dragover 事件
    await uploadArea.trigger('dragover');
    // 存在类名
    expect(uploadArea.classes()).toContain('is-dragover');
    // 触发 dragleave 事件
    await uploadArea.trigger('dragleave');
    // 不存在类名
    expect(uploadArea.classes()).not.toContain('is-dragover');
    // 触发 drop 事件
    await uploadArea.trigger('drop', { dataTransfer: { files: [testFile] } });
    // post 请求被触发
    expect(mockAxios.post).toHaveBeenCalled();
    // 页面中生成了一个 li
    expect(wrapper.findAll('li').length).toBe(1);
  });

测试手动上传

代码语言:javascript
复制
  it('test manual upload process', async () => {
    // 模拟 post 请求
    mockAxios.post.mockResolvedValueOnce({ data: { url: 'aa.url' } });
    const wrapper = shallowMount(Uploader, {
      props: {
        action: 'https://jsonplaceholder.typicode.com/posts/',
        drag: true,
        autoUpload: false,
      },
    });
    // 模拟 input 的 e.target.files
    const fileInput = wrapper.get('input').element as HTMLInputElement;
    const files = [testFile] as any;
    Object.defineProperty(fileInput, 'files', {
      value: files,
      writable: false,
    });
    // 触发 input 的 change 事件
    await wrapper.get('input').trigger('change');
    // 获取列表第一个元素
    const firstItem = wrapper.get('li:first-child');
    // 元素的类名包含 upload-ready
    expect(firstItem.classes()).toContain('upload-ready');
    // 获取组件实例 触发 uploadFiles 方法
    wrapper.vm.uploadFiles();
    // post 请求被触发
    expect(mockAxios.post).toHaveBeenCalled();
    // 清空 promise
    await flushPromises();
    // 元素的类名包含 upload-success
    expect(firstItem.classes()).toContain('upload-success');
  });

测试文件列表展示

代码语言:javascript
复制
it('pictureList mode should works fine', async () => {
    // 模拟 post 请求
    mockAxios.post.mockResolvedValueOnce({ data: { url: 'aa.url' } });
    // 模拟 URL.createObjectURL 方法
    window.URL.createObjectURL = jest.fn(() => {
      return 'test.url';
    });
    const wrapper = shallowMount(Uploader, {
      props: {
        action: 'https://jsonplaceholder.typicode.com/posts/',
        listType: 'picture',
      },
    });
    // 模拟 input 的 e.target.files
    const fileInput = wrapper.get('input').element as HTMLInputElement;
    const files = [testFile] as any;
    Object.defineProperty(fileInput, 'files', {
      value: files,
      writable: false,
    });
    // 元素的类名包含 upload-list-picture
    expect(wrapper.get('ul').classes()).toContain('upload-list-picture');
    // 触发 input 的 change 事件
    await wrapper.get('input').trigger('change');
    // 清空 promise
    await flushPromises();
    // 页面中生成了一个 li
    expect(wrapper.findAll('li').length).toBe(1);
    // 检测 图片是否正确渲染
    expect(wrapper.find('li:first-child img').exists()).toBeTruthy();
    // 图片src是否正确
    const firstImg = wrapper.get('li:first-child img');
    expect(firstImg.attributes('src')).toEqual('test.url');
  });

编写实际代码

代码语言:javascript
复制
<template>
  <div class="file-upload">
    <!-- 使用 button 模拟 input 上传-->
    <div v-on="events"
         class="upload-area"
         :class="{ 'is-dragover': drag &amp;&amp; isDragOver }"
         :disabled="isUploading">
      <slot v-if="isUploading"
            name="loading">
        <button disabled>正在上传</button>
      </slot>
      <slot name="uploaded"
            v-else-if="lastFileData &amp;&amp; lastFileData.loaded">
        <button>点击上传</button>
      </slot>
      <slot v-else
            name="default">
        <button>点击上传</button>
      </slot>
    </div>
    <!-- 隐藏 input 控件 -->
    <input type="file"
           ref="fileInput"
           @change="handleFileChange"
           :style="{ display: 'none' }">
    <!-- 上传文件列表 -->
    <ul class="uploaded-file">
      <li v-for="file in filesList"
          :class="`uploaded-file upload-${file.status}`"
          :key="file.uid">
        <img :src="file.url"
             v-if="file.url &amp;&amp; listType === 'picture'"
             :alt="file.name">
        <span class="filename">{{ file.name }}</span>
        <button class="delete-icon"
                @click="removeFile(file.uid)">del</button>
      </li>
    </ul>
  </div>
</template>
代码语言:javascript
复制
import axios from 'axios';
import { ref, defineProps, reactive, computed, PropType } from 'vue';
import { v4 as uuidv4 } from 'uuid'
import { last } from 'lodash-es'
export type CheckUpload = (file: File) => boolean | Promise<File>
export type UploadStatus = 'ready' | 'success' | "error" | 'loading'
export type FileListType = 'picture' | 'text'
export interface UploadFile {
  uid: string;
  size: number;
  name: string;
  status: UploadStatus;
  raw: File;
  resp?: any;
  url?: string;
}
const props = defineProps({
  action: {
    type: String,
    required: true,
  },
  beforeUpload: {
    type: Function as PropType<CheckUpload>
  },
  drag: {
    type: Boolean,
    default: false
  },
  autoUpload: {
    type: Boolean,
    default: true
  },
  listType: {
    type: String as PropType<FileListType>,
    default: 'text'
  }
})
// 上传文件列表
const filesList = ref<UploadFile[]>([])

const isDragOver = ref(false)

// 最后一个文件的数据
const lastFileData = computed(() => {
  const lastFile = last(filesList.value)
  if (lastFile) {
    return {
      loaded: lastFile.status === 'success',
      data: lastFile.resp
    }
  }
  return false
})

// 是否正在上传
const isUploading = computed(() => {
  return filesList.value.some((file => file.status === 'loading'))
})

// 删除文件
const removeFile = (id: string) => {
  filesList.value = filesList.value.filter((file) => file.uid === id)
}

// input ref
const fileInput = ref<null | HTMLInputElement>(null)
// 点击 button 触发选择文件弹窗
const triggerUpload = () => {
  fileInput?.value?.click()
}

const postFile = (readyFile: UploadFile) => {
  // 创建 FormData 数据结构
  const formData = new FormData()
  // 往 FormData 中 添加数据
  formData.append(readyFile.name, readyFile.raw)
  readyFile.status = 'loading'
  // 发送请求
  axios.post(props.action, formData, {
    headers: {
      // 需要在请求头中设置类型
      'Content-Type': "multipart/form-data"
    }
  }).then((resp) => {
    console.log(resp.data);
    readyFile.status = 'success'
    readyFile.resp = resp.data
  }).catch(() => {
    readyFile.status = 'error'
  }).finally(() => {
    if (fileInput.value) {
      fileInput.value.value = ''
    }
  })
}

// 添加到文件列表
const addFileToList = (uploadedFile: File) => {
  const fileObj = reactive<UploadFile>({
    uid: uuidv4(),
    size: uploadedFile.size,
    name: uploadedFile.name,
    status: 'ready',
    raw: uploadedFile,
  })
  if (props.listType === 'picture') {
    // try {
    //   fileObj.url = URL.createObjectURL(uploadedFile)
    // } catch (error) {
    //   console.log('upload transform error', error)
    // }
    const fileReader = new FileReader()
    fileReader.readAsDataURL(uploadedFile)
    fileReader.addEventListener('load', () => {
      fileObj.url = fileReader.result as string
    })
    fileReader.addEventListener('error', () => {

      //   console.log('upload transform error', error)
    })
  }

  filesList.value.push(fileObj)
  if (props.autoUpload) {
    postFile(fileObj)
  }
}


// 上传文件
const upLoadFiles = (files: FileList | null) => {
  if (files) {
    // 获取文件
    const uploadedFile = files[0]
    // beforeUpload 钩子
    if (props.beforeUpload) {
      const result = props.beforeUpload(uploadedFile)
      if (result &amp;&amp; result instanceof Promise) {
        result.then((processedFile) => {
          // 判断是否是 file 类型
          if (processedFile instanceof File) {
            addFileToList(processedFile)
          } else {
            throw new Error("beforeUpload Promise should return a file")
          }
        }).catch((e) => console.log(e))
      } else if (result === true) {
        addFileToList(uploadedFile)
      }
    } else {
      addFileToList(uploadedFile)
    }
  }
}

// 上传文件到服务器
const handleFileChange = (e: Event) => {
  // 获取文件列表
  const target = e.target as HTMLInputElement
  const files = target.files
  upLoadFiles(files)
}

/**
 * @description: 上传文件列表
 */
const uploadFiles = () => {
  filesList.value.filter(file => file.status === 'ready').forEach((readFile) => { postFile(readFile) })
}

const handleDrag = (e: DragEvent, over: boolean) => {
  // 取消默认行为
  e.preventDefault()
  isDragOver.value = over
}

const handleDrop = (e: DragEvent) => {
  e.preventDefault()
  isDragOver.value = false
  if (e.dataTransfer) {
    upLoadFiles(e.dataTransfer.files)
  }
}

// 事件列表
let events: { [key: string]: (e: any) => void } = {
  'click': triggerUpload,
}
if (props.drag) {
  events = {
    ...events,
    'dragover': (e: DragEvent) => { handleDrag(e, true) },
    'dragleave': (e: DragEvent) => { handleDrag(e, false) },
    'drop': handleDrop
  }
}
下一篇
举报
领券