<template>
<div>
<div class="zk-form__display-text" v-if="_display">
<slot name="tip"></slot>
<div v-if="listType === 'text'" class="zk-upload-display-text">
<template v-for="(file, index) in uploadList">
<div
v-if="file.uploadStatus === 'success'"
:key="file.url + file.name + index"
:class="`zk-upload--${file.uploadStatus}`">
<zk-link
:to="showLink ? file.url : 'javascript:;'"
:target="showLink ? '_blank' : undefined"
@click="preview(file, index)"
:title="file.name">
{{ file.name }}
</zk-link>
</div>
</template>
</div>
<div v-else>
<template v-for="(file, index) in uploadList">
<zk-image
v-if="file.uploadStatus === 'success'"
:key="file.url + file.name + index"
style="margin: 0 8px 0 0;"
:src="file.url"
:preview="false"
v-bind="imageProps"
@preview="preview(file, index)" />
</template>
</div>
</div>
<div v-else class="zk-upload" :class="{'zk-upload--disabled': uploadDisabled}">
<template v-if="$slots.trigger">
<span @click="triggerUpload">
<slot name="trigger"></slot>
</span>
<slot></slot>
</template>
<div v-else-if="triggerType === 'button'" class="zk-upload__button">
<zk-button
v-bind="uploadButton"
:icon="uploadButton.icon || uploadIconClass"
@click="triggerUpload"
:disabled="uploadDisabled">
{{ uploadButton.text || $t.tc('upload.fileListBtn') }}
</zk-button>
<slot></slot>
</div>
<div
v-else-if="triggerType === 'drag'"
:style="previewStyle"
class="zk-upload-drag zk-upload__preview--drag"
@dragover.stop.prevent="dragoverHandler"
@drop.stop.prevent="dropHandler"
@click="triggerUpload">
<div class="zk-upload-drag__content">
<i v-if="previewContent.indexOf('icon') > -1" class="zk-upload-drag__add-icon" :class="uploadIconClass"></i>
<div v-if="previewContent.indexOf('title') > -1" class="zk-upload-drag__title">
<slot name="title">
{{ title }}
</slot>
</div>
<div v-if="previewContent.indexOf('tip') > -1" class="zk-upload-drag__tip">
<slot name="tip">
{{ tip }}
</slot>
</div>
</div>
<div class="zk-upload__finally" v-if="!listType && uploadList.length">
<zk-image
:src="uploadList[0].url"
:preview="false"
:deletable="!deleteDisabled"
:show-preview-icon="showPreviewIcon"
v-bind="imageProps"
@preview="preview(uploadList[0], 0)"
@delete="removeHandler(uploadList[0], 0)" />
</div>
</div>
<ul v-if="listType === 'text' && uploadList.length && showFileList" class="zk-upload__filelist">
<template>
<li
v-for="(file, index) in uploadList"
:key="file.url + file.name + index"
class="zk-upload__file"
:class="`zk-upload--${file.uploadStatus}`">
<div class="zk-upload__file-left">
<slot name="imgBox" :fileName="file.name">
</slot>
<span v-if="file.uploadStatus === 'success'"
class="zk-upload__filename">
<span class="cursor" @click="downloadHandler(file)">{{ file.name }}</span>
</span>
<span v-else class="zk-upload__filename cursor" :title="file.name">{{ file.name }}</span>
</div>
<div class="zk-upload__file-right">
<zk-progress
:show-result-icon="true"
type="circle" size="xs" :percentage="file.percentage"
:status="file.uploadStatus">
</zk-progress>
<zk-button
v-if="file.uploadStatus === 'exception'"
shape="square" size="xs"
icon="zds-icon-refresh"
class="zk-upload__retry"
@click="retryHandler(file, index)">
</zk-button>
<zk-button
v-if="!deleteDisabled"
shape="square" size="xs"
icon="zds-icon-delete-outline"
class="zk-upload__remove"
@click="removeHandler(file, index)">
</zk-button>
</div>
</li>
</template>
</ul>
<div
v-else-if="listType === 'picture' && showFileList"
class="zk-upload-img"
:style="{'marginTop': triggerType === 'picture' ? 0 : '8px'}">
<div class="zk-upload-img__added"
v-for="(file, index) in uploadList"
:class="`zk-upload--${file.uploadStatus}`"
:key="file.url + file.name + index"
:style="imageProps">
<template v-if="file.uploadStatus !== 'success'">
<div>
<zk-progress :show-result-icon="true"
type="circle" size="xs" :percentage="file.percentage"
:status="file.uploadStatus"></zk-progress>
</div>
<span class="zk-upload-img__filename">
{{ file.uploadStatus === 'exception' ? $t.tc('upload.uploadFailed') : $t.tc('upload.uploading') }}
</span>
<div class="zk-upload-img__hover">
<span v-if="file.uploadStatus === 'exception'">
<i
class="zds-icon-refresh"
@click="retryHandler(file)"></i>
</span>
<span @click="removeHandler(file, index)">
<i
class="zds-icon-delete-outline"></i>
</span>
</div>
</template>
<template v-else>
<zk-image
:src="file.url"
:preview="false"
:deletable="!deleteDisabled"
:show-preview-icon="showPreviewIcon"
v-bind="imageProps"
@preview="preview(file, index)"
@delete="removeHandler(file, index)" />
</template>
</div>
<div
v-if="uploadList.length < limit && triggerType === 'picture'"
class="zk-upload-img__add"
:style="imageProps"
@click="triggerUpload">
<i v-if="previewContent.indexOf('icon') > -1" class="zds-icon-add"></i>
<slot v-if="previewContent.indexOf('title') > -1" name="title">
{{ $t.tc('upload.imgBtn') }}
</slot>
</div>
</div>
<input class="zk-upload__input"
type="file"
ref="input"
:name="name"
@change="changeHandler"
:multiple="multiple"
:accept="accept">
</div>
</div>
</template>
<script>
import axios from 'axios';
import formDisplay from 'zeekr-ui/src/mixins/form-display';
export default {
name: 'BillingUpload',
inject: {
zkForm: {
default: ''
}
},
mixins: [formDisplay],
props: {
fileList: {
type: Array,
default: () => ([])
},
accept: {
type: String,
default: '*'
},
action: {
type: String,
default: 'https://gateway-pub-sit.zeekrlife.com/cloud-storage/putObject'
},
multiple: {
type: Boolean,
default: true
},
name: String,
disabled: Boolean,
limit: {
type: Number,
default: 99
},
sizeLimit: {
type: Number,
default: 102400
},
serviceConfig: {
type: Object,
default:() => ({
prefix: 'zeekrui',
serviceName: 'b516fa2c8d874cd38cf46b1ebbf31564',
ossType: 'aliyun'
})
},
triggerType: {
type: String,
default: 'button',
validator: function (value) {
return ['button', 'picture', 'drag'].indexOf(value) !== -1;
}
},
listType: {
type: String,
validator: function (value) {
return ['text', 'picture'].indexOf(value) !== -1;
}
},
token: {
type: String,
default: ''
},
beforeUpload: {
type: Function,
default: null
},
headers: {
type: Object,
default: () => ({})
},
data: {
type: Object
},
getUrl: Function,
previewStyle: {
type: Object,
default: () => ({ width: '', height: '' })
},
previewContent: {
type: Array,
default: () => ['icon', 'title', 'tip']
},
uploadIconClass: {
type: String,
default: 'zds-icon-add'
},
fileKey: {
type: String,
default: 'file'
},
previewModal: {
type: Boolean,
default: true
},
showPreviewIcon: {
type: Boolean,
default: true
},
needTimestamp: {
type: Boolean,
default: true
},
uploadButton: {
type: Object,
default: () => ({})
},
showFileList: {
type: Boolean,
default: true
},
showLink: {
type: Boolean,
default: true
},
httpRequest: {
type: Function,
default: axios
},
previewProps: Object,
imageProps: Object,
quality: {
type: Number,
default: 0.5
},
compress: Boolean,
beforeRemove: Function
},
computed: {
uploadDisabled() {
return this.deleteDisabled || this.uploadList.length >= this.limit;
},
deleteDisabled() {
return (this.zkForm && this.zkForm.disabled) || this.disabled;
},
srcList() {
return this.fileList.map(({ url }) => url);
},
previewTitle() {
return this.fileList[this.previewCurrent]?.name;
}
},
watch: {
fileList (val) {
this.uploadList = val.map((file) => {
if (file.url) {
return {
name: file.name,
url: file.url,
uploadStatus: 'success',
percentage: 100,
...file
};
}
return file;
});
}
},
data () {
return {
uploadList: this.fileList.map((file) => ({
name: file.name,
url: file.url,
uploadStatus: 'success',
percentage: 100,
...file
})),
visible: false,
previewImg: {
name: '',
url: ''
},
title: this.$t.tc('upload.title'),
tip: '',
defaultCurrent: 0,
previewCurrent: 0
};
},
methods: {
messageHandler(type = 'warning', message) {
this.$message({
type,
message,
});
},
changeHandler(e) {
const { files } = e.target;
this.dealFiles(files);
},
async dealFiles(files) {
if ((this.uploadList.length + files.length) > this.limit) {
this.messageHandler('error', this.$t.tc('upload.overLength'));
this.$emit('on-limit', files);
return;
}
if (this.beforeUpload && typeof this.beforeUpload === 'function') {
const res = this.beforeUpload(files);
if (typeof res === 'boolean' && !res) return;
if (res instanceof Promise) {
try {
files = await res;
} catch (e) {
return;
}
} else {
files = res;
}
}
if (files && files.length > 0) {
for (let i = 0, len = files.length; i < len; i++) {
const file = files[i];
const { size } = file;
file.percentage = 0;
if (size / 1024 > this.sizeLimit) {
this.$emit('on-size-limit', file);
return this.messageHandler('error', `${file.name} ${this.$t.tc('upload.overSize')}`);
}
this.uploadList.push(file);
this.upload(file, this.uploadList.length - 1);
}
}
},
triggerUpload() {
if (this.uploadDisabled) return;
this.$refs.input.value = null;
this.$refs.input.click();
},
request (formData, progressCb) {
const url = this.needTimestamp
? `${this.action}?_t=${Date.parse(new Date())}` : this.action;
let headers = {
'Content-Type': 'multipart/form-data',
'Cache-Control': 'no-cache',
...this.headers
};
if (this.token) {
headers.Authorization = this.token;
}
return this.httpRequest({
method: 'post',
url,
headers,
params: this.serviceConfig,
data: formData,
onUploadProgress: (progressEvent) => {
const { total, loaded } = progressEvent;
const percentage = Math.ceil(loaded / total * 100);
this.$emit('on-progress', percentage, progressEvent);
progressCb(percentage);
},
});
},
async upload (file, fileIndex) {
let formData = new FormData();
const { name } = file;
formData.append(this.fileKey, file, name);
if (this.data) {
let { data } = this;
if (typeof data === 'function') {
data = this.data();
}
Object.keys(data).forEach((key) => {
formData.append(key, this.data[key]);
});
}
this.request(formData, (percentage) => {
if (percentage === 100) return;
file.percentage = percentage;
this.$forceUpdate();
}).then(async (res) => {
if (res.status === 200 && (res.data.data || res.data.data?.url)) {
let url;
let fileList = [];
const { data } = res;
if (this.getUrl) {
url = await this.getUrl(data);
} else {
url = typeof data.data === 'string' ? data.data : data.data?.url;
}
if (url) {
file.url = url;
file.percentage = 100;
file.uploadStatus = 'success';
this.$forceUpdate();
fileList = this.updateFileList();
}
this.$emit('on-success', data.data, file, fileList, fileIndex, data);
} else {
throw res.data;
}
}).catch((err) => {
file.uploadStatus = 'exception';
this.$forceUpdate();
this.$emit('on-error', err, file, this.fileList, fileIndex);
});
},
async removeHandler(file, index) {
if (this.beforeRemove) {
try {
const res = await this.beforeRemove(file, index);
if (res === false) {
return;
}
} catch {
return;
}
}
this.uploadList.splice(index, 1);
file.url && this.$emit('update:fileList', this.uploadList);
this.$emit('on-remove', file, index);
},
downloadHandler(file) {
this.$emit('onDownload', file);
},
preview(file, index) {
if (this.showLink && this.listType === 'text') return;
if (this.previewModal) {
this.defaultCurrent = index;
this.visible = true;
}
this.$emit('on-preview', file, this.fileList, index);
},
updateFileList() {
let fileList = [];
for (let i = 0, len = this.uploadList.length; i < len; i++) {
const file = this.uploadList[i];
if (file.uploadStatus === 'success') {
const { name, url } = file;
fileList.push({
name,
url,
...file
});
} else {
fileList.push(file);
}
}
this.$emit('update:fileList', fileList);
return fileList;
},
dragoverHandler(e) {
if (this.uploadDisabled) return;
e.dataTransfer.dropEffect = 'copy';
},
dropHandler(e) {
if (this.uploadDisabled) return;
let filterFiles = [];
const { files } = e.dataTransfer;
if (this.accept === '*') {
filterFiles = files;
} else {
for (let i = 0, len = files.length; i < len; i++) {
const file = files[i];
const arr = file.name.split('.');
const ext = arr[arr.length - 1];
const accepts = this.accept.split(',');
let acceptFlag = false;
for (let i = 0, len = accepts.length; i < len; i++) {
const reg = new RegExp(accepts[i]);
if (reg.test(file.type)) {
acceptFlag = true;
break;
}
}
if (this.accept.includes(ext) || acceptFlag) {
filterFiles.push(file);
}
}
}
this.dealFiles(filterFiles);
},
retryHandler(file, fileIndex) {
file.percentage = 0;
this.$forceUpdate();
this.upload(file, fileIndex);
}
}
};
</script>
<style scoped>
.file-img {
width: 16px;
vertical-align: middle;
margin-right: 4px;
}
.file-name {
vertical-align: middle;
}
.cursor {
cursor: pointer;
}
</style>