簡單寫了部分註釋,upload-dragger.vue(拖拽上傳時顯示此組件)、upload-list.vue(已上傳文件列表)源碼暫未添加多少註釋,等有空再補充,先記下來...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/component/upload#yong-hu-tou-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>
<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>
function 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; }
<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>
<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>