简单写了部分注释,upload-dragger.vue(拖拽上传时显示此组件)、upload-list.vue(已上传文件列表)源码暂未添加多少注释,等有空再补充,先记下来... index.vue<script>import UploadList from './upload-list';import Upload from './upload';import ElProgress from 'element-ui/packages/progress';import Migrating from 'element-ui/src/mixins/migrating';function noop() {}export default { name: 'ElUpload', mixins: [Migrating], components: { ElProgress, UploadList, Upload }, provide() { return { uploader: this }; }, inject: { elForm: { default: '' } }, props: { action: { //必选参数,上传的地址 type: String, required: true }, headers: { //设置上传的请求头部 type: Object, default() { return {}; } }, data: Object, //上传时附带的额外参数 multiple: Boolean, //是否支持多选文件 name: { //上传的文件字段名 type: String, default: 'file' }, drag: Boolean, //是否启用拖拽上传 dragger: Boolean, withCredentials: Boolean, //支持发送 cookie 凭证信息 showFileList: { //是否显示已上传文件列表 type: Boolean, default: true }, accept: String, //接受上传的文件类型(thumbnail-mode 模式下此参数无效) type: { type: String, default: 'select' }, beforeUpload: Function, //上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 beforeRemove: Function, //删除文件之前的钩子,参数为上传的文件和文件列表,若返回 false 或者返回 Promise 且被 reject,则停止上传。 onRemove: { //文件列表移除文件时的钩子 type: Function, default: noop }, onChange: { //文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用 type: Function, default: noop }, onPreview: { //点击文件列表中已上传的文件时的钩子 type: Function }, onSuccess: { //文件上传成功时的钩子 type: Function, default: noop }, onProgress: { //文件上传时的钩子 type: Function, default: noop }, onError: { //文件上传失败时的钩子 type: Function, default: noop }, fileList: { //上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] type: Array, default() { return []; } }, autoUpload: { //是否在选取文件后立即进行上传 type: Boolean, default: true }, listType: { //文件列表的类型 type: String, default: 'text' // text,picture,picture-card }, httpRequest: Function, //覆盖默认的上传行为,可以自定义上传的实现 disabled: Boolean, //是否禁用 limit: Number, //最大允许上传个数 onExceed: { //文件超出个数限制时的钩子 type: Function, default: noop } }, data() { return { uploadFiles: [], dragOver: false, draging: false, tempIndex: 1 }; }, computed: { uploadDisabled() { return this.disabled || (this.elForm || {}).disabled; } }, watch: { fileList: { immediate: true, handler(fileList) { this.uploadFiles = fileList.map(item => { item.uid = item.uid || (Date.now() + this.tempIndex++); item.status = item.status || 'success'; return item; }); } } }, methods: { //文件上传之前调用的方法 handleStart(rawFile) { rawFile.uid = Date.now() + this.tempIndex++; let file = { status: 'ready', name: rawFile.name, size: rawFile.size, percentage: 0, uid: rawFile.uid, raw: rawFile }; //判断文件列表类型 if (this.listType === 'picture-card' || this.listType === 'picture') { try { file.url = URL.createObjectURL(rawFile); } catch (err) { console.error('[Element Error][Upload]', err); return; } } this.uploadFiles.push(file); this.onChange(file, this.uploadFiles); }, handleProgress(ev, rawFile) { const file = this.getFile(rawFile); this.onProgress(ev, file, this.uploadFiles); file.status = 'uploading'; file.percentage = ev.percent || 0; }, //文件上传成功后改用该方法,在该方法中调用用户设置的on-success和on-change方法,并将对应的参数传递出去 handleSuccess(res, rawFile) { const file = this.getFile(rawFile); if (file) { file.status = 'success'; file.response = res; this.onSuccess(res, file, this.uploadFiles); this.onChange(file, this.uploadFiles); } }, //文件上传失败后改用该方法,在该方法中调用用户设置的on-error和on-change方法,并将对应的参数传递出去 handleError(err, rawFile) { const file = this.getFile(rawFile); const fileList = this.uploadFiles; file.status = 'fail'; fileList.splice(fileList.indexOf(file), 1); this.onError(err, file, this.uploadFiles); this.onChange(file, this.uploadFiles); }, //文件列表移除文件时调用该方法 handleRemove(file, raw) { if (raw) { file = this.getFile(raw); } let doRemove = () => { this.abort(file); let fileList = this.uploadFiles; fileList.splice(fileList.indexOf(file), 1); this.onRemove(file, fileList); }; if (!this.beforeRemove) { doRemove(); } else if (typeof this.beforeRemove === 'function') { const before = this.beforeRemove(file, this.uploadFiles); if (before && before.then) { before.then(() => { doRemove(); }, noop); } else if (before !== false) { doRemove(); } } }, getFile(rawFile) { let fileList = this.uploadFiles; let target; fileList.every(item => { target = rawFile.uid === item.uid ? item : null; return !target; }); return target; }, abort(file) { this.$refs['upload-inner'].abort(file); }, clearFiles() { this.uploadFiles = []; }, submit() { this.uploadFiles .filter(file => file.status === 'ready') .forEach(file => { this.$refs['upload-inner'].upload(file.raw); }); }, getMigratingConfig() { return { props: { 'default-file-list': 'default-file-list is renamed to file-list.', 'show-upload-list': 'show-upload-list is renamed to show-file-list.', 'thumbnail-mode': 'thumbnail-mode has been deprecated, you can implement the same effect according to this case: http://element.eleme.io/#/zh-CN/ ... u-xiang-shang-chuan } }; } }, beforeDestroy() { this.uploadFiles.forEach(file => { if (file.url && file.url.indexOf('blob:') === 0) { URL.revokeObjectURL(file.url); } }); }, render(h) { let uploadList; //如果用户设置showFileList为true,则显示上传文件列表 if (this.showFileList) { uploadList = ( <UploadList disabled={this.uploadDisabled} listType={this.listType} files={this.uploadFiles} on-remove={this.handleRemove} handlePreview={this.onPreview}> </UploadList> ); } const uploadData = { props: { type: this.type, drag: this.drag, action: this.action, multiple: this.multiple, 'before-upload': this.beforeUpload, 'with-credentials': this.withCredentials, headers: this.headers, name: this.name, data: this.data, accept: this.accept, fileList: this.uploadFiles, autoUpload: this.autoUpload, listType: this.listType, disabled: this.uploadDisabled, limit: this.limit, 'on-exceed': this.onExceed, 'on-start': this.handleStart, 'on-progress': this.handleProgress, 'on-success': this.handleSuccess, 'on-error': this.handleError, 'on-preview': this.onPreview, 'on-remove': this.handleRemove, 'http-request': this.httpRequest }, ref: 'upload-inner' }; const trigger = this.$slots.trigger || this.$slots.default; const uploadComponent = <upload {...uploadData}>{trigger}</upload>; return ( <div> { this.listType === 'picture-card' ? uploadList : ''} { this.$slots.trigger ? [uploadComponent, this.$slots.default] : uploadComponent } {this.$slots.tip} { this.listType !== 'picture-card' ? uploadList : ''} </div> ); }};</script>upload.vue<script>import ajax from './ajax';import UploadDragger from './upload-dragger.vue';export default { inject: ['uploader'], components: { UploadDragger }, props: { type: String, action: { //必选参数,上传的地址 type: String, required: true }, name: { //上传的文件字段名 type: String, default: 'file' }, data: Object, //上传时附带的额外参数 headers: Object, //设置上传的请求头部 withCredentials: Boolean, //支持发送 cookie 凭证信息 multiple: Boolean, //是否支持多选文件 accept: String, //接受上传的文件类型(thumbnail-mode 模式下此参数无效) onStart: Function, onProgress: Function, //文件上传时的钩子 onSuccess: Function, //文件上传成功时的钩子 onError: Function, //文件上传失败时的钩子 beforeUpload: Function, //上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传。 drag: Boolean, //是否启用拖拽上传 onPreview: { //点击文件列表中已上传的文件时的钩子 type: Function, default: function() {} }, onRemove: { //文件列表移除文件时的钩子 type: Function, default: function() {} }, fileList: Array, //上传的文件列表, 例如: [{name: 'food.jpg', url: 'https://xxx.cdn.com/xxx.jpg'}] autoUpload: Boolean, //是否在选取文件后立即进行上传 listType: String, //文件列表的类型 httpRequest: { //覆盖默认的上传行为,可以自定义上传的实现 type: Function, default: ajax }, disabled: Boolean,//是否禁用 limit: Number,//最大允许上传个数 onExceed: Function //文件超出个数限制时的钩子 }, data() { return { mouseover: false, reqs: {} }; }, methods: { isImage(str) { return str.indexOf('image') !== -1; }, handleChange(ev) { const files = ev.target.files; if (!files) return; this.uploadFiles(files); }, uploadFiles(files) { //文件超出个数限制时,调用onExceed钩子函数 if (this.limit && this.fileList.length + files.length > this.limit) { this.onExceed && this.onExceed(files, this.fileList); return; } //将files转成数组 let postFiles = Array.prototype.slice.call(files); if (!this.multiple) { postFiles = postFiles.slice(0, 1); } if (postFiles.length === 0) { return; } postFiles.forEach(rawFile => { this.onStart(rawFile); //选取文件后调用upload方法立即进行上传文件 if (this.autoUpload) this.upload(rawFile); }); }, upload(rawFile) { this.$refs.input.value = null; //beforeUpload 上传文件之前的钩子不存在就直接调用post上传文件 if (!this.beforeUpload) { return this.post(rawFile); } // beforeUpload 上传文件之前的钩子,参数为上传的文件,若返回 false 或者返回 Promise 且被 reject,则停止上传 const before = this.beforeUpload(rawFile); // 在调用beforeUpload钩子后返回的是true,则继续上传 if (before && before.then) { before.then(processedFile => { //processedFile转成对象 const fileType = Object.prototype.toString.call(processedFile); if (fileType === '[object File]' || fileType === '[object Blob]') { if (fileType === '[object Blob]') { processedFile = new File([processedFile], rawFile.name, { type: rawFile.type }); } for (const p in rawFile) { if (rawFile.hasOwnProperty(p)) { processedFile[p] = rawFile[p]; } } this.post(processedFile); } else { this.post(rawFile); } }, () => { this.onRemove(null, rawFile); }); } else if (before !== false) { //调用beforeUpload之后没有返回值,此时before为undefined,继续上传 this.post(rawFile); } else { //调用beforeUpload之后返回值为false,则不再继续上传并移除文件 this.onRemove(null, rawFile); } }, abort(file) { const { reqs } = this; if (file) { let uid = file; if (file.uid) uid = file.uid; if (reqs[uid]) { reqs[uid].abort(); } } else { Object.keys(reqs).forEach((uid) => { if (reqs[uid]) reqs[uid].abort(); delete reqs[uid]; }); } }, //上传文件过程的方法 post(rawFile) { const { uid } = rawFile; const options = { headers: this.headers, withCredentials: this.withCredentials, file: rawFile, data: this.data, filename: this.name, action: this.action, onProgress: e => { //文件上传时的钩子函数 this.onProgress(e, rawFile); }, onSuccess: res => { //文件上传成功的钩子函数 //上传成功调用onSuccess方法,即index.vue中的handleSuccess方法 this.onSuccess(res, rawFile); delete this.reqs[uid]; }, onError: err => { //文件上传失败的钩子函数 this.onError(err, rawFile); delete this.reqs[uid]; } }; //httpRequest可以自定义上传文件,如果没有定义,默认通过ajax文件中的方法上传 const req = this.httpRequest(options); this.reqs[uid] = req; if (req && req.then) { req.then(options.onSuccess, options.onError); } }, handleClick() { //点击组件时调用input的click方法 if (!this.disabled) { this.$refs.input.value = null; this.$refs.input.click(); } }, handleKeydown(e) { if (e.target !== e.currentTarget) return; //如果当前按下的是回车键和空格键,调用handleClick事件 if (e.keyCode === 13 || e.keyCode === 32) { this.handleClick(); } } }, render(h) { let { handleClick, drag, name, handleChange, multiple, accept, listType, uploadFiles, disabled, handleKeydown } = this; const data = { class: { 'el-upload': true }, on: { click: handleClick, keydown: handleKeydown } }; data.class[`el-upload--${listType}`] = true; return ( //判断是否允许拖拽,允许的话显示upload-dragger组件,不允许就显示所有插槽中的节点 <div {...data} tabindex="0" > { drag ? <upload-dragger disabled={disabled} on-file={uploadFiles}>{this.$slots.default}</upload-dragger> : this.$slots.default } <input class="el-upload__input" type="file" ref="input" name={name} on-change={handleChange} multiple={multiple} accept={accept}></input> </div> ); }};</script>ajax.jsfunction getError(action, option, xhr) { let msg; if (xhr.response) { msg = `${xhr.response.error || xhr.response}`; } else if (xhr.responseText) { msg = `${xhr.responseText}`; } else { msg = `fail to post ${action} ${xhr.status}`; } const err = new Error(msg); err.status = xhr.status; err.method = 'post'; err.url = action; return err;}function getBody(xhr) { const text = xhr.responseText || xhr.response; if (!text) { return text; } try { return JSON.parse(text); } catch (e) { return text; }}//默认的上传文件的方法export default function upload(option) { //XMLHttpRequest 对象用于在后台与服务器交换数据。 if (typeof XMLHttpRequest === 'undefined') { return; } //创建XMLHttpRequest对象 const xhr = new XMLHttpRequest(); const action = option.action; //上传的地址 //XMLHttpRequest.upload 属性返回一个 XMLHttpRequestUpload对象,用来表示上传的进度。这个对象是不透明的,但是作为一个XMLHttpRequestEventTarget,可以通过对其绑定事件来追踪它的进度。 if (xhr.upload) { //上传进度调用方法,上传过程中会频繁调用该方法 xhr.upload.onprogress = function progress(e) { if (e.total > 0) { // e.total是需要传输的总字节,e.loaded是已经传输的字节 e.percent = e.loaded / e.total * 100; } //调文件上传时的钩子函数 option.onProgress(e); }; } // 创建一个FormData 对象 const formData = new FormData(); //用户设置了上传时附带的额外参数时 if (option.data) { Object.keys(option.data).forEach(key => { // 添加一个新值到 formData 对象内的一个已存在的键中,如果键不存在则会添加该键。 formData.append(key, option.data[key]); }); } formData.append(option.filename, option.file, option.file.name); //请求出错 xhr.onerror = function error(e) { option.onError(e); }; //请求成功回调函数 xhr.onload = function onload() { if (xhr.status < 200 || xhr.status >= 300) { return option.onError(getError(action, option, xhr)); } //调用upload.vue文件中的onSuccess方法,将上传接口返回值作为参数传递 option.onSuccess(getBody(xhr)); }; //初始化请求 xhr.open('post', action, true); if (option.withCredentials && 'withCredentials' in xhr) { xhr.withCredentials = true; } const headers = option.headers || {}; for (let item in headers) { if (headers.hasOwnProperty(item) && headers[item] !== null) { //设置请求头 xhr.setRequestHeader(item, headers[item]); } } //发送请求 xhr.send(formData); return xhr;}upload-dragger.vue<template> <!--拖拽上传时显示此组件--> <div class="el-upload-dragger" :class="{ 'is-dragover': dragover }" @drop.prevent="onDrop" @dragover.prevent="onDragover" @dragleave.prevent="dragover = false" > <slot></slot> </div></template><script> export default { name: 'ElUploadDrag', props: { disabled: Boolean }, inject: { uploader: { default: '' } }, data() { return { dragover: false }; }, methods: { onDragover() { if (!this.disabled) { this.dragover = true; } }, onDrop(e) { if (this.disabled || !this.uploader) return; //接受上传的文件类型(thumbnail-mode 模式下此参数无效),此处判断该文件是都符合能上传的类型 const accept = this.uploader.accept; this.dragover = false; if (!accept) { this.$emit('file', e.dataTransfer.files); return; } this.$emit('file', [].slice.call(e.dataTransfer.files).filter(file => { const { type, name } = file; //获取文件名后缀,与设置的文件类型进行对比 const extension = name.indexOf('.') > -1 ? `.${ name.split('.').pop() }` : ''; const baseType = type.replace(/\/.*$/, ''); return accept.split(',') .map(type => type.trim()) .filter(type => type) .some(acceptedType => { if (/\..+$/.test(acceptedType)) { //文件名后缀与设置的文件类型进行对比 return extension === acceptedType; } if (/\/\*$/.test(acceptedType)) { return baseType === acceptedType.replace(/\/\*$/, ''); } if (/^[^\/]+\/[^\/]+$/.test(acceptedType)) { return type === acceptedType; } return false; }); })); } } };</script>upload-list.vue<template> <!--这里主要显示已上传文件列表--> <transition-group tag="ul" :class="[ 'el-upload-list', 'el-upload-list--' + listType, { 'is-disabled': disabled } ]" name="el-list"> <li v-for="file in files" :class="['el-upload-list__item', 'is-' + file.status, focusing ? 'focusing' : '']" :key="file.uid" tabindex="0" @keydown.delete="!disabled && $emit('remove', file)" @focus="focusing = true" @blur="focusing = false" @click="focusing = false" > <img class="el-upload-list__item-thumbnail" v-if="file.status !== 'uploading' && ['picture-card', 'picture'].indexOf(listType) > -1" :src="file.url" alt="" > <a class="el-upload-list__item-name" @click="handleClick(file)"> <i class="el-icon-document"></i>{{file.name}} </a> <label class="el-upload-list__item-status-label"> <i :class="{ 'el-icon-upload-success': true, 'el-icon-circle-check': listType === 'text', 'el-icon-check': ['picture-card', 'picture'].indexOf(listType) > -1 }"></i> </label> <i class="el-icon-close" v-if="!disabled" @click="$emit('remove', file)"></i> <i class="el-icon-close-tip" v-if="!disabled">{{ t('el.upload.deleteTip') }}</i> <!--因为close按钮只在li:focus的时候 display, li blur后就不存在了,所以键盘导航时永远无法 focus到 close按钮上--> <el-progress v-if="file.status === 'uploading'" :type="listType === 'picture-card' ? 'circle' : 'line'" :stroke-width="listType === 'picture-card' ? 6 : 2" :percentage="parsePercentage(file.percentage)"> </el-progress> <span class="el-upload-list__item-actions" v-if="listType === 'picture-card'"> <span class="el-upload-list__item-preview" v-if="handlePreview && listType === 'picture-card'" @click="handlePreview(file)" > <i class="el-icon-zoom-in"></i> </span> <span v-if="!disabled" class="el-upload-list__item-delete" @click="$emit('remove', file)" > <i class="el-icon-delete"></i> </span> </span> </li> </transition-group></template><script> import Locale from 'element-ui/src/mixins/locale'; import ElProgress from 'element-ui/packages/progress'; export default { name: 'ElUploadList', mixins: [Locale], data() { return { focusing: false }; }, components: { ElProgress }, props: { files: { type: Array, default() { return []; } }, disabled: { type: Boolean, default: false }, handlePreview: Function, listType: String }, methods: { parsePercentage(val) { return parseInt(val, 10); }, handleClick(file) { this.handlePreview && this.handlePreview(file); } } };</script>
|