⌈Vue3&React18⌋动手实践构建组件库之❣️Upload组件❣️

197 阅读1分钟

预览

Upload | Vue3xy (chengbotao.github.io)

介绍

文件上传
基于 axiostype="file" input 实现

Props 属性

属性名属性类型说明默认值
actionstring上传文件的服务器地址-
dragboolean是否支持拖放文件false
classNamestring自定义CSS类名-
defaultFileListUploadFile[]默认已上传文件列表-
beforeUpload(file: File) => boolean | Promise<File>上传前的回调函数-
onProgress(percentage: number, file: UploadFile) => void上传中的回调函数-
onSuccess(data: any, file: UploadFile) => void上传成功的回调函数-
onError(err: any, file: UploadFile) => void上传失败的回调函数-
onChange(file: UploadFile) => void选择文件的回调函数-
onRemove(file: UploadFile) => void移除上传列中的文件回调函数-
headersDuckTypingaxios headers 配置-
dataDuckTypingformData data 添加-
withCredentialsboolean是否需要忽略响应中的 cookie-
namestring上传文件类型设置file
acceptstring文件上传控件中预期文件类型的提示-
multipleboolean是否允许多选-

源码呈现

Vue3 实现

Upload

  <template>
  <div v-bind="$attrs" :class="classes">
    <div class="upload-input" style="display: inline-block" @click="handleClick">
      <template v-if="drag">
        <xyDragger key="dragger" :onFile="uploadFiles">
          <slot></slot>
        </xyDragger>
      </template>
      <slot v-else key="slot"></slot>
      <input ref="fileInput" type="file" class="file-input" style="display: none" @change="handleFileChange"
        :accept="accept" :multiple="multiple" />
    </div>
    <xyUploadList :fileList="fileList" :onRemove="handleRemove"></xyUploadList>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'XyUpload',
  inheritAttrs: false,
})

</script>

<script setup lang="ts">
import { computed, ref } from "vue";
import classNames from "classnames";
import axios from 'axios';

type UploadFileStatus = 'ready' | 'uploading' | 'success' | 'error';

interface UploadFile {
  uid: string;
  size: number;
  name: string;
  status?: UploadFileStatus;
  percent?: number;
  raw?: File;
  response?: any;
  error?: any;
}

interface DuckTyping {
  [key: string]: any;
}

interface UploadProps {
  className?: string;
  action: string;
  defaultFileList?: UploadFile[];
  beforeUpload?: (file: File) => boolean | Promise<File>;
  onProgress?: (percentage: number, file: UploadFile) => void;
  onSuccess?: (data: any, file: UploadFile) => void;
  onError?: (err: any, file: UploadFile) => void;
  onChange?: (file: UploadFile) => void;
  onRemove?: (file: UploadFile) => void;
  headers?: DuckTyping;
  name?: string;
  data?: DuckTyping;
  withCredentials?: boolean;
  accept?: string;
  multiple?: boolean;
  drag?: boolean;
}

// defineOptions({
//     name: 'XyUpload',
//     inheritAttrs: false,
// })

// ref
const fileInput = ref<HTMLInputElement>()

// Props
const props = withDefaults(defineProps<UploadProps>(), {
  name: 'file',
});

// Emits

// data
const fileList = ref<UploadFile[]>(props.defaultFileList || []);

// computed
const classes = computed(() => {
  return classNames("xy-upload", props.className);
});

// methods
const updateFileList = (
  updateFile: UploadFile,
  updateObj: Partial<UploadFile>
) => {
  fileList.value = fileList.value.map((file) => {
    if (file.uid === updateFile.uid) {
      return { ...file, ...updateObj };
    } else {
      return file;
    }
  });

};
const handleClick = () => {
  if (fileInput.value) {
    fileInput.value.click();
  }
};
const handleRemove = (file: UploadFile) => {
  fileList.value = fileList.value.filter((item) => item.uid !== file.uid);
  props.onRemove && props.onRemove(file);
};
const handleFileChange = (event: Event) => {
  const files = (event.target as HTMLInputElement).files;
  if (!files) {
    return;
  }
  uploadFiles(files);
  if (fileInput.value) {
    fileInput.value.value = '';
  }
};

const uploadFiles = (files: FileList) => {
  let postFiles = Array.from(files);
  postFiles.forEach((file) => {
    if (!props.beforeUpload) {
      post(file);
    } else {
      const result = props.beforeUpload(file);
      if (result && result instanceof Promise) {
        // tslint:disable-next-line: no-floating-promises
        result.then((processedFile) => {
          post(processedFile);
        });
      } else if (result !== false) {
        post(file);
      }
    }
  });
};

const post = (file: File) => {
  let _file: UploadFile = {
    uid: `${Date.now()}upload-file`,
    status: 'ready',
    name: file.name,
    size: file.size,
    percent: 0,
    raw: file,
  };
  fileList.value = [_file, ...fileList.value];
  const formData = new FormData();
  formData.append(props.name, file);
  if (props.data) {
    Object.keys(props.data).forEach((key) => {
      formData.append(key, props.data![key]);
    });
  }
  axios
    .post(props.action, formData, {
      headers: {
        ...props.headers,
        'Content-Type': 'multipart/from-data',
      },
      withCredentials: !!props.withCredentials,
      onUploadProgress: (e: any) => {
        let percentage = Math.round((e.loaded * 100) / e.total) || 0;
        if (percentage < 100) {
          updateFileList(_file, { percent: percentage, status: 'uploading' });
          props.onProgress && props.onProgress(percentage, _file);
        }
      },
    })
    .then((resp: any) => {
      updateFileList(_file, { status: 'success', response: resp.data });
      props.onSuccess && props.onSuccess(resp.data, fileList.value.find(file=>file.uid===_file.uid)!);
      props.onChange && props.onChange(fileList.value.find(file=>file.uid===_file.uid)!);
    })
    .catch((err: Error) => {
      updateFileList(_file, { status: 'error', error: err });
      props.onError && props.onError(err, fileList.value.find(file=>file.uid===_file.uid)!);
      props.onChange && props.onChange(fileList.value.find(file=>file.uid===_file.uid)!);
    });
}
// public 方法
defineExpose({})

</script>

UploadList

<template>
  <ul class="xy-upload-list">

    <li v-for="file in fileList" :key="file.uid" class="upload-list-item">
      <span :class="`file-name file-name-${file.status}`">
        <xyIcon icon="file-alt" theme="secondary"></xyIcon>
        {{file.name}}
      </span>
      <span class="file-status">
        <xyIcon v-if="file.status === 'uploading' || file.status === 'ready'" key="uploading" icon="spinner" spin theme="primary">
        </xyIcon>
        <xyIcon v-else-if="file.status === 'success'" key="success" icon="check-circle" theme="success"></xyIcon>
        <xyIcon v-else-if="file.status === 'error'" key="error" icon="times-circle" theme="danger"></xyIcon>
      </span>
      <span class="file-actions">
        <xyIcon icon="times" @click="onRemove(file)"></xyIcon>
      </span>
      <xyProgress v-if="file.status === 'uploading'" :percent="file.percent ?? 0" :strokeHeight="10"></xyProgress>
    </li>
  </ul>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'xyUploadList',
  inheritAttrs: false,
})

</script>

<script setup lang="ts">
type UploadFileStatus = 'ready' | 'uploading' | 'success' | 'error';

interface UploadFile {
  uid: string;
  size: number;
  name: string;
  status?: UploadFileStatus;
  percent?: number;
  raw?: File;
  response?: any;
  error?: any;
}

interface UploadListProps {
  fileList: UploadFile[];
  onRemove: (_file: UploadFile) => void;
}

// defineOptions({
//     name: 'xyUploadList',
//     inheritAttrs: false,
// })

// Props
const props = withDefaults(defineProps<UploadListProps>(), {});

// Emits

// inject

// data

// computed

// methods

// public 方法
defineExpose({})

</script>

Vue3 单元测试

// todo

React 实现

// todo

React 单元测试

// todo

后记

个人博客 | Botaoxy (chengbotao.github.io)


感谢阅读,敬请斧正!