|
|
@@ -1,30 +1,69 @@
|
|
|
<template>
|
|
|
<view :class="classes">
|
|
|
- <nut-icon color="#808080" :name="uploadIcon"></nut-icon>
|
|
|
- <input type="file" :name="name" @change="onChange" />
|
|
|
+ <view class="preview" v-for="item in fileList" :key="item.uid">
|
|
|
+ <view class="preview-img">
|
|
|
+ <nut-icon
|
|
|
+ v-if="isDeletable"
|
|
|
+ color="rgba(0,0,0,0.6)"
|
|
|
+ @click="onDelete(item, index)"
|
|
|
+ class="close"
|
|
|
+ name="mask-close"
|
|
|
+ ></nut-icon>
|
|
|
+ <img v-if="item.type.includes('image') && item.url" :src="item.url" />
|
|
|
+ <view class="tips" v-if="item.status != 'success'">{{
|
|
|
+ item.status
|
|
|
+ }}</view>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="upload" v-if="maxCount - fileList.length">
|
|
|
+ <nut-icon color="#808080" :name="uploadIcon"></nut-icon>
|
|
|
+ <input
|
|
|
+ type="file"
|
|
|
+ :capture="capture"
|
|
|
+ :accept="acceptType"
|
|
|
+ :multiple="multiple"
|
|
|
+ :name="name"
|
|
|
+ :disabled="disabled"
|
|
|
+ @change="onChange"
|
|
|
+ />
|
|
|
+ </view>
|
|
|
</view>
|
|
|
</template>
|
|
|
|
|
|
<script lang="ts">
|
|
|
-import { computed } from 'vue';
|
|
|
+import { computed, reactive } from 'vue';
|
|
|
import { createComponent } from '@/utils/create';
|
|
|
import Icon from '@/packages/icon/index.vue';
|
|
|
+import { Uploader, UploadOptions } from './uploader';
|
|
|
const { componentName, create } = createComponent('uploader');
|
|
|
-
|
|
|
+export type FileItemStatus =
|
|
|
+ | 'ready'
|
|
|
+ | 'uploading'
|
|
|
+ | 'success'
|
|
|
+ | 'error'
|
|
|
+ | 'removed';
|
|
|
+export class FileItem {
|
|
|
+ status: FileItemStatus = 'ready';
|
|
|
+ uid: string = new Date().getTime().toString();
|
|
|
+ name?: string;
|
|
|
+ url?: string;
|
|
|
+ type?: string;
|
|
|
+ formData: FormData = new FormData();
|
|
|
+}
|
|
|
export default create({
|
|
|
props: {
|
|
|
name: { type: String, default: 'file' },
|
|
|
url: { type: String, default: '' },
|
|
|
- defaultFileList: { type: Array, default: [] },
|
|
|
- fileList: { type: Array, default: [] },
|
|
|
+ defaultFileList: { type: Array, default: () => new Array<FileItem>() },
|
|
|
+ fileList: { type: Array, default: () => [] },
|
|
|
isPreview: { type: Boolean, default: true },
|
|
|
isDeletable: { type: Boolean, default: true },
|
|
|
method: { type: String, default: 'post' },
|
|
|
capture: { type: String, default: '' },
|
|
|
- maxSize: { type: [Number, String], default: 1024 * 1024 * 5 },
|
|
|
+ maxSize: { type: [Number, String], default: Number.MAX_VALUE },
|
|
|
maxCount: { type: [Number, String], default: 1 },
|
|
|
clearInput: { type: Boolean, default: false },
|
|
|
- acceptType: { type: String, default: '' },
|
|
|
+ acceptType: { type: String, default: '*' },
|
|
|
headers: { type: Object, default: {} },
|
|
|
formData: { type: Object, default: {} },
|
|
|
uploadIcon: { type: String, default: 'photograph' },
|
|
|
@@ -32,17 +71,35 @@ export default create({
|
|
|
withCredentials: { type: Boolean, default: false },
|
|
|
multiple: { type: Boolean, default: false },
|
|
|
disabled: { type: Boolean, default: false },
|
|
|
- beforeUpload: { type: Function },
|
|
|
- beforeDelete: { type: Function },
|
|
|
+ beforeUpload: {
|
|
|
+ type: Function,
|
|
|
+ default: (files: FileList) => {
|
|
|
+ return files;
|
|
|
+ }
|
|
|
+ },
|
|
|
+ beforeDelete: {
|
|
|
+ type: Function,
|
|
|
+ default: (file: FileItem, files: FileItem[]) => {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ },
|
|
|
onChange: { type: Function },
|
|
|
customRequest: { type: Function }
|
|
|
},
|
|
|
components: {
|
|
|
[Icon.name]: Icon
|
|
|
},
|
|
|
- emits: ['start', 'progress', 'oversize', 'success', 'failure', 'on-change'],
|
|
|
+ emits: [
|
|
|
+ 'start',
|
|
|
+ 'progress',
|
|
|
+ 'oversize',
|
|
|
+ 'success',
|
|
|
+ 'failure',
|
|
|
+ 'on-change',
|
|
|
+ 'on-delete'
|
|
|
+ ],
|
|
|
setup(props, { emit }) {
|
|
|
- console.log(props);
|
|
|
+ const fileList = reactive(props.fileList) as Array<FileItem>;
|
|
|
const classes = computed(() => {
|
|
|
const prefixCls = componentName;
|
|
|
return {
|
|
|
@@ -50,12 +107,142 @@ export default create({
|
|
|
};
|
|
|
});
|
|
|
|
|
|
- const onChange = (event: Event) => {
|
|
|
- emit('on-change', event);
|
|
|
+ const clearInput = (el: HTMLInputElement) => {
|
|
|
+ el.value = '';
|
|
|
+ };
|
|
|
+
|
|
|
+ const executeUpload = (fileItem: FileItem) => {
|
|
|
+ const uploadOption = new UploadOptions();
|
|
|
+ uploadOption.url = props.url;
|
|
|
+ for (const [key, value] of Object.entries(props.formData)) {
|
|
|
+ fileItem.formData.append(key, value);
|
|
|
+ }
|
|
|
+ uploadOption.formData = fileItem.formData;
|
|
|
+ uploadOption.method = props.method;
|
|
|
+ uploadOption.xhrState = props.xhrState as number;
|
|
|
+ uploadOption.headers = props.headers;
|
|
|
+ uploadOption.withCredentials = props.withCredentials;
|
|
|
+ uploadOption.onStart = (option: UploadOptions) => {
|
|
|
+ fileItem.status = 'ready';
|
|
|
+ emit('start', option);
|
|
|
+ };
|
|
|
+ uploadOption.onProgress = (
|
|
|
+ e: ProgressEvent<XMLHttpRequestEventTarget>,
|
|
|
+ option: UploadOptions
|
|
|
+ ) => {
|
|
|
+ fileItem.status = 'uploading';
|
|
|
+ emit('progress', { e, option });
|
|
|
+ };
|
|
|
+
|
|
|
+ uploadOption.onSuccess = (
|
|
|
+ responseText: XMLHttpRequest['responseText'],
|
|
|
+ option: UploadOptions
|
|
|
+ ) => {
|
|
|
+ fileItem.status = 'success';
|
|
|
+ emit('success', {
|
|
|
+ responseText,
|
|
|
+ option
|
|
|
+ });
|
|
|
+ };
|
|
|
+ uploadOption.onFailure = (
|
|
|
+ responseText: XMLHttpRequest['responseText'],
|
|
|
+ option: UploadOptions
|
|
|
+ ) => {
|
|
|
+ fileItem.status = 'error';
|
|
|
+ emit('failure', {
|
|
|
+ responseText,
|
|
|
+ option
|
|
|
+ });
|
|
|
+ };
|
|
|
+ new Uploader(uploadOption).upload();
|
|
|
+ };
|
|
|
+
|
|
|
+ const readFile = (files: File[]) => {
|
|
|
+ files.forEach((file: File) => {
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append(props.name, file);
|
|
|
+
|
|
|
+ const fileItem = new FileItem();
|
|
|
+ fileItem.name = file.name;
|
|
|
+ fileItem.status = 'uploading';
|
|
|
+ fileItem.type = file.type;
|
|
|
+ fileItem.formData = formData;
|
|
|
+ executeUpload(fileItem);
|
|
|
+
|
|
|
+ if (props.isPreview && file.type.includes('image')) {
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (event: ProgressEvent<FileReader>) => {
|
|
|
+ fileItem.url = (event.target as FileReader).result as string;
|
|
|
+ fileList.push(fileItem);
|
|
|
+ };
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+ } else {
|
|
|
+ fileList.push(fileItem);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const filterFiles = (files: File[]) => {
|
|
|
+ const maxCount = (props.maxCount as number) * 1;
|
|
|
+ const maxSize = (props.maxSize as number) * 1;
|
|
|
+ const oversizes = new Array<File>();
|
|
|
+ files = files.filter((file: File) => {
|
|
|
+ if (file.size > maxSize) {
|
|
|
+ oversizes.push(file);
|
|
|
+ return false;
|
|
|
+ } else {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (oversizes.length) {
|
|
|
+ emit('oversize', oversizes);
|
|
|
+ }
|
|
|
+ if (files.length > maxCount) {
|
|
|
+ files.splice(maxCount - 1, files.length - maxCount);
|
|
|
+ }
|
|
|
+ return files;
|
|
|
+ };
|
|
|
+ const onDelete = (file: FileItem, index: number) => {
|
|
|
+ if (props.beforeDelete(file, fileList)) {
|
|
|
+ fileList.splice(index, 1);
|
|
|
+ emit('on-delete', {
|
|
|
+ file,
|
|
|
+ fileList
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ console.log('用户阻止了删除!');
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const onChange = (event: InputEvent) => {
|
|
|
+ if (props.disabled) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const $el = event.target as HTMLInputElement;
|
|
|
+ let { files } = $el;
|
|
|
+
|
|
|
+ if (props.beforeUpload) {
|
|
|
+ files = props.beforeUpload(files);
|
|
|
+ }
|
|
|
+
|
|
|
+ const _files: File[] = filterFiles(new Array<File>().slice.call(files));
|
|
|
+
|
|
|
+ readFile(_files);
|
|
|
+
|
|
|
+ if (props.clearInput) {
|
|
|
+ clearInput($el);
|
|
|
+ }
|
|
|
+
|
|
|
+ emit('on-change', {
|
|
|
+ fileList,
|
|
|
+ event
|
|
|
+ });
|
|
|
};
|
|
|
|
|
|
return {
|
|
|
onChange,
|
|
|
+ onDelete,
|
|
|
+ fileList,
|
|
|
classes
|
|
|
};
|
|
|
}
|