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