预览
Upload | Vue3xy (chengbotao.github.io)
介绍
文件上传
基于axios和type="file" input实现
Props 属性
| 属性名 | 属性类型 | 说明 | 默认值 |
|---|---|---|---|
action | string | 上传文件的服务器地址 | - |
drag | boolean | 是否支持拖放文件 | false |
className | string | 自定义CSS类名 | - |
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 | axios headers 配置 | - |
data | DuckTyping | formData data 添加 | - |
withCredentials | boolean | 是否需要忽略响应中的 cookie | - |
name | string | 上传文件类型设置 | file |
accept | string | 文件上传控件中预期文件类型的提示 | - |
multiple | boolean | 是否允许多选 | - |
源码呈现
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
后记
感谢阅读,敬请斧正!