|
|
|
@@ -1,147 +1,399 @@ |
|
|
|
<!-- 通用上传组件 zhao --> |
|
|
|
|
|
|
|
<template> |
|
|
|
<van-uploader v-model="fileList" :multiple="multiple" :after-read="afterRead" :show-upload="showUpload" |
|
|
|
:deletable="deletable" @delete="deleteFile" :accept="accept || null"/> |
|
|
|
<div class="component-upload-image"> |
|
|
|
<!-- 上传区域 --> |
|
|
|
<el-upload |
|
|
|
multiple |
|
|
|
:disabled="disabled" |
|
|
|
:action="uploadImgUrl" |
|
|
|
list-type="picture-card" |
|
|
|
:on-success="handleUploadSuccess" |
|
|
|
:before-upload="handleBeforeUpload" |
|
|
|
:data="data" |
|
|
|
:limit="limit" |
|
|
|
:on-error="handleUploadError" |
|
|
|
:on-exceed="handleExceed" |
|
|
|
ref="imageUpload" |
|
|
|
:on-remove="handleDelete" |
|
|
|
:show-file-list="true" |
|
|
|
:headers="headers" |
|
|
|
:file-list="fileList" |
|
|
|
:on-preview="handlePictureCardPreview" |
|
|
|
:class="{hide: fileList.length >= limit}" |
|
|
|
style="grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); gap: 12px; padding: 0;" |
|
|
|
> |
|
|
|
<!-- 上传按钮 --> |
|
|
|
<div class="upload-btn"> |
|
|
|
<i class="el-icon-plus" style="font-size: 20px;"></i> |
|
|
|
</div> |
|
|
|
</el-upload> |
|
|
|
|
|
|
|
<!-- 上传提示:关键调整 - 减小与上传框的间距 --> |
|
|
|
<div class="el-upload__tip" slot="tip" v-if="showTip && !disabled" style="font-size: 12px; margin-top: 4px; line-height: 1.4;"> |
|
|
|
<template v-if="fileSize"> 大小: ≤ <b style="color: #f56c6c">{{ fileSize }}MB</b> </template> |
|
|
|
<template v-if="fileType && fileSize">,</template> |
|
|
|
<template v-if="fileType"> 格式: <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template> |
|
|
|
</div> |
|
|
|
|
|
|
|
<!-- 预览弹窗 --> |
|
|
|
<el-dialog :visible.sync="dialogVisible" title="预览" :width="dialogWidth" append-to-body :close-on-click-modal="true"> |
|
|
|
<img :src="dialogImageUrl" style="display: block; max-width: 100%; max-height: 70vh; margin: 0 auto;" @load="handleImageLoad"/> |
|
|
|
</el-dialog> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
|
|
|
|
<script> |
|
|
|
|
|
|
|
import {commonUpload} from "@/api/app/index"; |
|
|
|
import { getToken } from "@/utils/auth"; |
|
|
|
import { isExternal } from "@/utils/validate"; |
|
|
|
import Sortable from 'sortablejs'; |
|
|
|
import {Dialog} from "vant"; |
|
|
|
|
|
|
|
export default { |
|
|
|
name: "commonUpload", |
|
|
|
props: { |
|
|
|
name: String, |
|
|
|
value: { // 绑定值 字符串 ,分隔 可监听 |
|
|
|
value: [String, Object, Array], |
|
|
|
action: { |
|
|
|
type: String, |
|
|
|
default: null, |
|
|
|
default: "/common/upload" |
|
|
|
}, |
|
|
|
accept: { // 上传类型限制: 默认图片, * = 任意类型 |
|
|
|
type: String, |
|
|
|
}, |
|
|
|
multiple: { // 多文件上传 |
|
|
|
type: Boolean, |
|
|
|
default: false, |
|
|
|
}, |
|
|
|
deletable: { // 允许删除 |
|
|
|
type: Boolean, |
|
|
|
default: true, |
|
|
|
data: { |
|
|
|
type: Object |
|
|
|
}, |
|
|
|
showUpload: { // 显示上传按钮 |
|
|
|
type: Boolean, |
|
|
|
default: true, |
|
|
|
limit: { |
|
|
|
type: Number, |
|
|
|
default: 5 |
|
|
|
}, |
|
|
|
formData: { // 额外请求参数 |
|
|
|
type: Object, |
|
|
|
default: function () { |
|
|
|
return {}; |
|
|
|
}, |
|
|
|
fileSize: { |
|
|
|
type: Number, |
|
|
|
default: 3 |
|
|
|
}, |
|
|
|
file: { // 上传文件字段名 |
|
|
|
type: String, |
|
|
|
default: 'file', |
|
|
|
fileType: { |
|
|
|
type: Array, |
|
|
|
default: () => ["png", "jpg", "jpeg"] |
|
|
|
}, |
|
|
|
host: { |
|
|
|
type: String, // 文件地址前缀 |
|
|
|
default: '/api', |
|
|
|
isShowTip: { |
|
|
|
type: Boolean, |
|
|
|
default: true |
|
|
|
}, |
|
|
|
}, |
|
|
|
watch: { |
|
|
|
value: function (newVal, oldVal) { |
|
|
|
if (newVal != this.internalValue) |
|
|
|
this.setInternalValue(newVal); |
|
|
|
disabled: { |
|
|
|
type: Boolean, |
|
|
|
default: false |
|
|
|
}, |
|
|
|
}, |
|
|
|
created() { |
|
|
|
this.parseValue(this.value); |
|
|
|
drag: { |
|
|
|
type: Boolean, |
|
|
|
default: false |
|
|
|
} |
|
|
|
}, |
|
|
|
data() { |
|
|
|
return { |
|
|
|
internalValue: this.value, |
|
|
|
fileList: [], |
|
|
|
pathList: [], |
|
|
|
number: 0, |
|
|
|
uploadList: [], |
|
|
|
dialogImageUrl: "", |
|
|
|
dialogVisible: false, |
|
|
|
dialogWidth: "90%", |
|
|
|
baseUrl: process.env.VUE_APP_BASE_API, |
|
|
|
uploadImgUrl: process.env.VUE_APP_BASE_API + this.action, |
|
|
|
headers: { |
|
|
|
Authorization: "Bearer " + getToken(), |
|
|
|
}, |
|
|
|
fileList: [] |
|
|
|
}; |
|
|
|
}, |
|
|
|
mounted() { |
|
|
|
if (this.drag && !this.disabled) { |
|
|
|
this.$nextTick(() => { |
|
|
|
const element = this.$refs.imageUpload?.$el?.querySelector('.el-upload-list'); |
|
|
|
if (element) { |
|
|
|
Sortable.create(element, { |
|
|
|
onEnd: (evt) => { |
|
|
|
const movedItem = this.fileList.splice(evt.oldIndex, 1)[0]; |
|
|
|
this.fileList.splice(evt.newIndex, 0, movedItem); |
|
|
|
this.$emit("input", this.listToString(this.fileList)); |
|
|
|
}, |
|
|
|
handle: ".el-upload-list__item", |
|
|
|
animation: 150 |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
window.addEventListener("resize", this.adjustDialogWidth); |
|
|
|
}, |
|
|
|
beforeDestroy() { |
|
|
|
window.removeEventListener("resize", this.adjustDialogWidth); |
|
|
|
}, |
|
|
|
watch: { |
|
|
|
value: { |
|
|
|
handler(val) { |
|
|
|
if (val) { |
|
|
|
const list = Array.isArray(val) ? val : val.split(','); |
|
|
|
this.fileList = list.map(item => { |
|
|
|
if (typeof item === "string") { |
|
|
|
const imgUrl = !isExternal(item) && item.indexOf(this.baseUrl) === -1 |
|
|
|
? `${this.baseUrl}${item}` |
|
|
|
: item; |
|
|
|
return { |
|
|
|
name: imgUrl, |
|
|
|
url: imgUrl, |
|
|
|
response: { url: imgUrl } |
|
|
|
}; |
|
|
|
} |
|
|
|
return item; |
|
|
|
}); |
|
|
|
} else { |
|
|
|
this.fileList = []; |
|
|
|
} |
|
|
|
}, |
|
|
|
deep: true, |
|
|
|
immediate: true |
|
|
|
} |
|
|
|
}, |
|
|
|
computed: { |
|
|
|
showTip() { |
|
|
|
return this.isShowTip && (this.fileType.length || this.fileSize); |
|
|
|
} |
|
|
|
}, |
|
|
|
methods: { |
|
|
|
setInternalValue(newVal) { |
|
|
|
this.parseValue(newVal); |
|
|
|
this.internalValue = newVal; |
|
|
|
adjustDialogWidth() { |
|
|
|
const screenWidth = window.innerWidth; |
|
|
|
this.dialogWidth = screenWidth < 375 ? "95%" : "90%"; |
|
|
|
}, |
|
|
|
parseValue(data) { |
|
|
|
if (data) { |
|
|
|
this.pathList = data.split(','); |
|
|
|
handleBeforeUpload(file) { |
|
|
|
let isImg = false; |
|
|
|
const fileExtension = file.name.lastIndexOf(".") > -1 |
|
|
|
? file.name.slice(file.name.lastIndexOf(".") + 1).toLowerCase() |
|
|
|
: ""; |
|
|
|
|
|
|
|
if (this.fileType.length) { |
|
|
|
isImg = this.fileType.includes(fileExtension) || file.type.includes(fileExtension); |
|
|
|
} else { |
|
|
|
this.pathList = []; |
|
|
|
isImg = file.type.indexOf("image") > -1; |
|
|
|
} |
|
|
|
this.fileList = this.pathList.map((x) => { |
|
|
|
return { |
|
|
|
url: this.host + x, |
|
|
|
}; |
|
|
|
}); |
|
|
|
}, |
|
|
|
makeFormData() { |
|
|
|
let fd = new FormData(); |
|
|
|
if (this.formData) { |
|
|
|
for (let k of Object.keys(this.formData)) { |
|
|
|
fd.set(k, this.formData[k]); |
|
|
|
|
|
|
|
if (!isImg) { |
|
|
|
this.$modal.msgError(`请上传${this.fileType.join("/")}格式图片`); |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (file.name.includes(',')) { |
|
|
|
this.$modal.msgError("文件名不能包含英文逗号"); |
|
|
|
return false; |
|
|
|
} |
|
|
|
if (this.fileSize) { |
|
|
|
const isLt = file.size / 1024 / 1024 < this.fileSize; |
|
|
|
if (!isLt) { |
|
|
|
this.$modal.msgError(`图片大小不能超过${this.fileSize}MB`); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
return fd; |
|
|
|
}, |
|
|
|
upload(file) { |
|
|
|
let params1 = this.makeFormData(); |
|
|
|
params1.append(this.file, file.file); |
|
|
|
return commonUpload(params1).then((resp) => { |
|
|
|
this.pathList.push(resp.fileName); |
|
|
|
this.updateInternalValue(); |
|
|
|
this.$emit('upload', resp.fileName); |
|
|
|
}); |
|
|
|
}, |
|
|
|
afterRead(file) { |
|
|
|
this.$toast.loading({ |
|
|
|
|
|
|
|
this.$modal.loading({ |
|
|
|
message: "上传中...", |
|
|
|
forbidClick: true, |
|
|
|
duration: 0, |
|
|
|
lock: false |
|
|
|
}); |
|
|
|
// 此时可以自行将文件上传至服务器 |
|
|
|
if (file instanceof Array) {//判断是否为数组,单张图片为array,多张为数组,数组返回true否则为false |
|
|
|
if (file.length > 0) { |
|
|
|
let index = 0; |
|
|
|
const f = () => { |
|
|
|
if (index >= file.length) |
|
|
|
return; |
|
|
|
let up = file[index]; |
|
|
|
//console.log(up); |
|
|
|
//console.log(`上传文件: ${index} -> ${up.file.name}`); |
|
|
|
this.upload(up).then(() => { |
|
|
|
index++; |
|
|
|
if (index < file.length) |
|
|
|
f(); |
|
|
|
}); |
|
|
|
}; |
|
|
|
f(); |
|
|
|
} |
|
|
|
this.number++; |
|
|
|
}, |
|
|
|
handleExceed() { |
|
|
|
this.$modal.msgError(`最多可上传${this.limit}张图片`); |
|
|
|
}, |
|
|
|
handleUploadSuccess(res, file) { |
|
|
|
if (res.code === 200) { |
|
|
|
this.uploadList.push({ |
|
|
|
name: res.fileName, |
|
|
|
url: res.fileName, |
|
|
|
response: { url: `${this.baseUrl}${res.fileName}` } |
|
|
|
}); |
|
|
|
this.uploadedSuccessfully(); |
|
|
|
} else { |
|
|
|
this.upload(file); |
|
|
|
this.number--; |
|
|
|
this.$modal.closeLoading(); |
|
|
|
this.$modal.msgError(res.msg || "上传失败"); |
|
|
|
this.$refs.imageUpload?.handleRemove(file); |
|
|
|
} |
|
|
|
}, |
|
|
|
deleteFile(detail) { |
|
|
|
this.pathList.splice(detail.index, 1); |
|
|
|
this.updateInternalValue(); |
|
|
|
this.$emit('remove', detail.index); |
|
|
|
handleDelete(file, list) { |
|
|
|
Dialog.confirm({ |
|
|
|
title: '提示', |
|
|
|
message: '确定要移除这张图片吗?', |
|
|
|
}) |
|
|
|
.then(() => { |
|
|
|
const findex = this.fileList.map(f => f.name).indexOf(file.name); |
|
|
|
if (findex > -1) { |
|
|
|
this.fileList.splice(findex, 1); |
|
|
|
this.$emit("input", this.listToString(this.fileList)); |
|
|
|
} |
|
|
|
}) |
|
|
|
.catch(() => { |
|
|
|
// on cancel |
|
|
|
}); |
|
|
|
}, |
|
|
|
updateInternalValue() { |
|
|
|
let files = this.pathList.join(','); |
|
|
|
//console.log(files); |
|
|
|
this.internalValue = files; |
|
|
|
if (this.internalValue != this.value) |
|
|
|
this.$emit('input', this.internalValue); |
|
|
|
handleUploadError() { |
|
|
|
this.$modal.closeLoading(); |
|
|
|
this.$modal.msgError("上传失败,请重试"); |
|
|
|
}, |
|
|
|
}, |
|
|
|
} |
|
|
|
uploadedSuccessfully() { |
|
|
|
if (this.number > 0 && this.uploadList.length === this.number) { |
|
|
|
this.fileList = this.fileList.concat(this.uploadList); |
|
|
|
this.uploadList = []; |
|
|
|
this.number = 0; |
|
|
|
this.$emit("input", this.listToString(this.fileList)); |
|
|
|
this.$modal.closeLoading(); |
|
|
|
this.$modal.msgSuccess("全部上传完成"); |
|
|
|
} |
|
|
|
}, |
|
|
|
handlePictureCardPreview(file) { |
|
|
|
this.dialogImageUrl = file.url || file.response?.url; |
|
|
|
this.adjustDialogWidth(); |
|
|
|
this.dialogVisible = true; |
|
|
|
}, |
|
|
|
handleImageLoad(e) { |
|
|
|
const img = e.target; |
|
|
|
const maxHeight = window.innerHeight * 0.7; |
|
|
|
if (img.height > maxHeight) { |
|
|
|
img.style.height = `${maxHeight}px`; |
|
|
|
img.style.width = "auto"; |
|
|
|
} |
|
|
|
}, |
|
|
|
listToString(list, separator) { |
|
|
|
separator = separator || ","; |
|
|
|
return list |
|
|
|
.filter(item => item.url) |
|
|
|
.map(item => item.url.replace(this.baseUrl, "")) |
|
|
|
.join(separator); |
|
|
|
} |
|
|
|
} |
|
|
|
}; |
|
|
|
</script> |
|
|
|
|
|
|
|
<style scoped> |
|
|
|
<style scoped lang="scss"> |
|
|
|
// 上传按钮样式 |
|
|
|
.upload-btn { |
|
|
|
width: 60px; |
|
|
|
height: 60px; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
border: 1px dashed #dcdcdc; |
|
|
|
border-radius: 4px; |
|
|
|
background-color: #fafafa; |
|
|
|
margin: 0; |
|
|
|
transition: all 0.2s; |
|
|
|
|
|
|
|
&:hover { |
|
|
|
border-color: #409eff; |
|
|
|
background-color: #f5faff; |
|
|
|
} |
|
|
|
|
|
|
|
&:active { |
|
|
|
background-color: #f0f7ff; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 已上传图片项样式 |
|
|
|
::v-deep .el-upload-list--picture-card .el-upload-list__item { |
|
|
|
width: 60px; |
|
|
|
height: 60px; |
|
|
|
border-radius: 4px; |
|
|
|
overflow: hidden; |
|
|
|
position: relative; |
|
|
|
border: 1px solid #e5e7eb; |
|
|
|
|
|
|
|
.el-upload-list__item-thumbnail { |
|
|
|
object-fit: cover; |
|
|
|
width: 100%; |
|
|
|
height: 100%; |
|
|
|
} |
|
|
|
|
|
|
|
.el-upload-list__item-delete { |
|
|
|
position: absolute; |
|
|
|
top: -8px; |
|
|
|
right: -8px; |
|
|
|
width: 20px; |
|
|
|
height: 20px; |
|
|
|
font-size: 12px; |
|
|
|
background-color: #f56c6c; |
|
|
|
color: #fff; |
|
|
|
border-radius: 50%; |
|
|
|
display: flex; |
|
|
|
align-items: center; |
|
|
|
justify-content: center; |
|
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1); |
|
|
|
z-index: 10; |
|
|
|
|
|
|
|
&:hover, &:active { |
|
|
|
width: 22px; |
|
|
|
height: 22px; |
|
|
|
background-color: #e4393c; |
|
|
|
top: -9px; |
|
|
|
right: -9px; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
&:hover { |
|
|
|
border-color: #409eff; |
|
|
|
} |
|
|
|
|
|
|
|
&:hover .el-upload-list__item-delete { |
|
|
|
opacity: 1; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 移除外层默认边框 |
|
|
|
::v-deep .el-upload--picture-card { |
|
|
|
border: none !important; |
|
|
|
background: transparent !important; |
|
|
|
margin-bottom: 0 !important; /* 移除上传组件底部默认边距 */ |
|
|
|
} |
|
|
|
|
|
|
|
// 隐藏上传按钮(数量达限时) |
|
|
|
::v-deep.hide .el-upload--picture-card { |
|
|
|
display: none; |
|
|
|
} |
|
|
|
|
|
|
|
// 禁用状态样式 |
|
|
|
::v-deep .el-upload-list--picture-card.is-disabled + .el-upload--picture-card { |
|
|
|
display: none !important; |
|
|
|
} |
|
|
|
|
|
|
|
// 列表动画优化 |
|
|
|
::v-deep .el-list-enter-active, |
|
|
|
::v-deep .el-list-leave-active { |
|
|
|
transition: all 0s; |
|
|
|
} |
|
|
|
|
|
|
|
::v-deep .el-list-enter, |
|
|
|
::v-deep .el-list-leave-active { |
|
|
|
opacity: 0; |
|
|
|
transform: translateY(0); |
|
|
|
} |
|
|
|
|
|
|
|
// 小屏幕适配 |
|
|
|
@media screen and (max-width: 375px) { |
|
|
|
.upload-btn, |
|
|
|
::v-deep .el-upload-list--picture-card .el-upload-list__item { |
|
|
|
width: 55px; |
|
|
|
height: 55px; |
|
|
|
} |
|
|
|
|
|
|
|
.el-upload--picture-card { |
|
|
|
grid-template-columns: repeat(auto-fill, minmax(55px, 1fr)); |
|
|
|
gap: 8px; |
|
|
|
} |
|
|
|
|
|
|
|
// 提示文字进一步缩小间距 |
|
|
|
.el-upload__tip { |
|
|
|
margin-top: 3px !important; |
|
|
|
font-size: 11px !important; |
|
|
|
} |
|
|
|
|
|
|
|
::v-deep .el-upload-list__item-delete { |
|
|
|
width: 18px; |
|
|
|
height: 18px; |
|
|
|
top: -7px; |
|
|
|
right: -7px; |
|
|
|
|
|
|
|
&:hover, &:active { |
|
|
|
width: 20px; |
|
|
|
height: 20px; |
|
|
|
top: -8px; |
|
|
|
right: -8px; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
</style> |