merchapp/java-mall-app-shop-admin/components/tui-upload/tui-upload.vue
2025-07-11 00:19:56 +08:00

613 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="tui-upload__container">
<view class="tui-upload-box">
<view class="tui-image-item" :style="{width:width+'rpx',height:height+'rpx',borderRadius:radius+'rpx'}"
v-for="(item,index) in imageList" :key="index">
<image :src="item" class="tui-item-img"
:style="{width:width+'rpx',height:height+'rpx',borderRadius:radius+'rpx'}"
@tap.stop="previewImage(index)" mode="aspectFill"></image>
<view v-if="!forbidDel" class="tui-img-del" :style="{background:getDelColor}"
@tap.stop="delImage(index)">
</view>
<view v-if="statusArr[index]!=1" class="tui-upload-mask">
<view class="tui-upload-loading" v-if="statusArr[index]==2"></view>
<text class="tui-tips">{{statusArr[index]==2?'上传中...':'上传失败'}}</text>
<view class="tui-mask-btn" v-if="statusArr[index]==3" @tap.stop="reUpLoad(index)"
hover-class="tui-btn-hover" :hover-stay-time="150">重新上传</view>
</view>
</view>
<view v-if="isShowAdd" class="tui-upload-add"
:class="[borderColor!=='transparent'?'tui-upload__border':'tui-upload__unborder']"
:style="{width:width+'rpx',height:height+'rpx',background:background,borderRadius:radius+'rpx',borderColor:borderColor,borderStyle:borderSytle}"
@tap="chooseImage">
<slot>
<view class="tui-upload-icon tui-icon-plus" :style="{color:addColor,fontSize:addSize+'rpx'}"></view>
</slot>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'tuiUpload',
emits: ['remove', 'complete', 'reupload'],
props: {
//展示图片宽度
width: {
type: [Number, String],
default: 218
},
//展示图片高度
height: {
type: [Number, String],
default: 218
},
//初始化图片路径
value: {
type: Array,
default () {
return []
}
},
//2.3.0+
radius: {
type: [Number, String],
default: 0
},
//2.3.0+
background: {
type: String,
default: '#F7F7F7'
},
//2.3.0+
borderColor: {
type: String,
default: 'transparent'
},
//2.3.0+
//solid、dashed、dotted
borderSytle: {
type: String,
default: 'dashed'
},
//2.3.0+
delColor: {
type: String,
default: ''
},
//删除图片前是否弹框确认
delConfirm: {
type: Boolean,
default: false
},
//禁用删除
forbidDel: {
type: Boolean,
default: false
},
//V2.9.6+ 删除图片是否触发 @complete 事件
delTrigger:{
type: Boolean,
default: true
},
//2.3.0+
addColor: {
type: String,
default: '#888'
},
//2.3.0+
addSize: {
type: [Number, String],
default: 68
},
//禁用添加
forbidAdd: {
type: Boolean,
default: false
},
//服务器接口地址。当接口地址为空时,直接返回本地图片地址
serverUrl: {
type: String,
default: ""
},
//限制数
limit: {
type: Number,
default: 9
},
//original 原图compressed 压缩图,默认二者都有
sizeType: {
type: Array,
default () {
return ['original', 'compressed']
}
},
//album 从相册选图camera 使用相机,默认二者都有。如需直接开相机或直接选相册,请只使用一个选项
sourceType: {
type: Array,
default () {
return ['album', 'camera']
}
},
//可上传图片类型,默认为空,不限制 Array<String> ['jpg','png','gif']
imageFormat: {
type: Array,
default () {
return []
}
},
//单张图片大小限制 MB
size: {
type: Number,
default: 4
},
//文件对应的key默认为 file
fileKeyName: {
type: String,
default: "file"
},
//HTTP 请求 Header, header 中不能设置 Referer。
header: {
type: Object,
default () {
return {}
}
},
//HTTP 请求中其他额外的 form data
formData: {
type: Object,
default () {
return {}
}
},
//自定义参数
params: {
type: [Number, String],
default: 0
}
},
data() {
return {
//图片地址
imageList: [],
tempFiles: [],
//上传状态1-上传成功 2-上传中 3-上传失败
statusArr: [],
//传入回调函数上传
callUpload: false
}
},
created() {
this.initImages()
},
watch: {
value(val) {
if (val) {
this.initImages()
}
}
},
computed: {
isShowAdd() {
let isShow = true;
if (this.forbidAdd || (this.limit && this.imageList.length >= this.limit)) {
isShow = false;
}
return isShow
},
getDelColor() {
return this.delColor || (uni && uni.$tui && uni.$tui.color.danger) || '#EB0909';
}
},
methods: {
initImages() {
this.statusArr = [];
this.imageList = [...this.value];
let tempFiles = []
for (let item of this.imageList) {
this.statusArr.push("1")
tempFiles.push({
path: item
})
}
this.tempFiles = tempFiles;
},
// 重新上传
reUpLoad(index) {
this.$set(this.statusArr, index, "2")
this.$emit('reupload', {
index
})
if (!this.callUpload) {
this.uploadImage(index, this.imageList[index]).then(() => {
this.change()
}).catch(() => {
this.change()
})
}
},
/**
* @param manual 是否手动上传
**/
change(manual = false) {
let status = ~this.statusArr.indexOf("2") ? 2 : 1
if (status != 2 && ~this.statusArr.indexOf("3")) {
// 上传失败
status = 3
}
this.$emit('complete', {
status: status,
imgArr: this.imageList,
params: this.params,
manual: manual
})
},
toast(text) {
text && uni.showToast({
title: text,
icon: "none"
});
},
chooseImage: function() {
let _this = this;
uni.chooseImage({
count: _this.limit - _this.imageList.length,
sizeType: _this.sizeType,
sourceType: _this.sourceType,
success: function(e) {
let imageArr = [];
for (let i = 0; i < e.tempFiles.length; i++) {
let len = _this.imageList.length;
if (len >= _this.limit) {
_this.toast(`最多可上传${_this.limit}张图片`);
break;
}
//过滤图片类型
let path = e.tempFiles[i].path;
if (_this.imageFormat.length > 0) {
let format = ""
// #ifdef H5
let type = e.tempFiles[i].type;
format = type.split('/')[1]
// #endif
// #ifndef H5
format = path.split(".")[(path.split(".")).length - 1];
// #endif
if (_this.imageFormat.indexOf(format) == -1) {
let text = `只能上传 ${_this.imageFormat.join(',')} 格式图片!`
_this.toast(text);
continue;
}
}
//过滤超出大小限制图片
let size = e.tempFiles[i].size;
if (_this.size * 1024 * 1024 < size) {
let err = `单张图片大小不能超过:${_this.size}MB`
_this.toast(err);
continue;
}
imageArr.push(path)
_this.imageList.push(path)
_this.tempFiles.push(e.tempFiles[i])
_this.statusArr.push("2")
}
_this.change()
let start = _this.imageList.length - imageArr.length
for (let j = 0; j < imageArr.length; j++) {
let index = start + j
//服务器地址
if (_this.serverUrl) {
_this.uploadImage(index, imageArr[j]).then(() => {
_this.change()
}).catch(() => {
_this.change()
})
} else {
//无服务器地址则直接返回成功
_this.$set(_this.statusArr, index, "1")
_this.change()
}
}
}
})
},
uploadImage: function(index, url, serverUrl) {
let _this = this;
return new Promise((resolve, reject) => {
uni.uploadFile({
url: this.serverUrl || serverUrl,
name: this.fileKeyName,
header: this.header,
formData: this.formData,
filePath: url,
success: function(res) {
if (res.statusCode == 200) {
//返回结果 此处需要按接口实际返回进行修改
let d = JSON.parse(res.data.replace(/\ufeff/g, "") || "{}")
//判断code以实际接口规范判断
if (d.code % 100 === 0) {
// 上传成功 d.url 为上传后图片地址,以实际接口返回为准
d.url && (_this.imageList[index] = d.url)
_this.$set(_this.statusArr, index, d.url ? "1" : "3")
} else {
// 上传失败
_this.$set(_this.statusArr, index, "3")
}
resolve(index)
} else {
_this.$set(_this.statusArr, index, "3")
reject(index)
}
},
fail: function(res) {
_this.$set(_this.statusArr, index, "3")
reject(index)
}
})
})
},
delImage: function(index) {
let that = this
if (this.delConfirm) {
uni.showModal({
title: '提示',
content: '确认删除该图片吗?',
showCancel: true,
cancelColor: "#555",
confirmColor: "#eb0909",
confirmText: "确定",
success(res) {
if (res.confirm) {
that.imageList.splice(index, 1)
that.tempFiles.splice(index, 1)
that.statusArr.splice(index, 1)
that.$emit("remove", {
index: index,
params: that.params
})
that.delTrigger && that.change()
}
}
})
} else {
that.imageList.splice(index, 1)
that.tempFiles.splice(index, 1)
that.statusArr.splice(index, 1)
that.$emit("remove", {
index: index,
params: that.params
})
that.delTrigger && that.change()
}
},
previewImage: function(index) {
if (!this.imageList.length) return;
uni.previewImage({
current: this.imageList[index],
loop: true,
urls: this.imageList
})
},
/**
* 当属性serverUrl传空时父级调用该方法一次性上传所有图片
* @param serverUrl 服务器接口地址
**/
uploadAllImage(serverUrl) {
if (!serverUrl) {
this.toast('服务器接口地址不能为空!');
return;
}
let imageArr = [...this.imageList]
const len = imageArr.length
for (let i = 0; i < len; i++) {
//如果是服务器地址图片则无需再次上传
if (imageArr[i].startsWith('https')) {
continue;
} else {
this.$set(this.statusArr, i, "2")
this.uploadImage(i, imageArr[i], serverUrl).then(() => {
if (i === len - 1) {
this.change(true)
}
}).catch(() => {
if (i === len - 1) {
this.change(true)
}
})
}
}
},
upload(callback, index) {
// 传入一个返回Promise的文件上传的函数
//上传状态1-上传成功 2-上传中 3-上传失败
this.callUpload = true;
if (index === undefined || index === null) {
let urls = [...this.imageList]
const len = urls.length
for (let i = 0; i < len; i++) {
if (urls[i].startsWith('https')) {
continue;
} else {
this.$set(this.statusArr, i, "2")
if (typeof callback === 'function') {
callback(this.tempFiles[i]).then(res => {
this.$set(this.statusArr, i, '1')
this.imageList[i] = res
this.change(true)
}).catch(err => {
this.$set(this.statusArr, i, '3')
})
}
}
}
} else {
//如果传入index则是重新上传时调用
this.$set(this.statusArr, index, "2")
if (typeof callback === 'function') {
callback(this.tempFiles[index]).then(res => {
this.$set(this.statusArr, index, '1')
this.imageList[index] = res
this.change(true)
}).catch(err => {
this.$set(this.statusArr, index, '3')
})
}
}
}
}
}
</script>
<style scoped>
@font-face {
font-family: 'tuiUpload';
src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAATcAA0AAAAAByQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAAEwAAAABoAAAAciR52BUdERUYAAASgAAAAHgAAAB4AKQALT1MvMgAAAaAAAABCAAAAVjxvR/tjbWFwAAAB+AAAAEUAAAFK5ibpuGdhc3AAAASYAAAACAAAAAj//wADZ2x5ZgAAAkwAAADXAAABAAmNjcZoZWFkAAABMAAAAC8AAAA2FpiS+WhoZWEAAAFgAAAAHQAAACQH3QOFaG10eAAAAeQAAAARAAAAEgwAACBsb2NhAAACQAAAAAwAAAAMAEoAgG1heHAAAAGAAAAAHwAAACABEgA2bmFtZQAAAyQAAAFJAAACiCnmEVVwb3N0AAAEcAAAACgAAAA6OMUs4HjaY2BkYGAAYo3boY/i+W2+MnCzMIDAzb3qdQj6fwPzf+YGIJeDgQkkCgA/KAtvAHjaY2BkYGBu+N/AEMPCAALM/xkYGVABCwBZ4wNrAAAAeNpjYGRgYGBl0GJgZgABJiDmAkIGhv9gPgMADTABSQB42mNgZGFgnMDAysDA1Ml0hoGBoR9CM75mMGLkAIoysDIzYAUBaa4pDA7PGJ9xMjf8b2CIYW5gaAAKM4LkANt9C+UAAHjaY2GAABYIVmBgAAAA+gAtAAAAeNpjYGBgZoBgGQZGBhBwAfIYwXwWBg0gzQakGRmYnjE+4/z/n4EBQksxSf6GqgcCRjYGOIeRCUgwMaACRoZhDwCiLwmoAAAAAAAAAAAAAAAASgCAeNpdjkFKw0AARf/vkIR0BkPayWRKQZtYY90ohJju2kOIbtz0KD1HVm50UfEmWXoAr9ADOHFARHHzeY//Fx8Ci+FJfIgdJFa4AhgiMshbrCuIsLxhFJZVs+Vl1bT1GddtbXTC3OhohN4dg4BJ3zMJAnccyfm468ZzHXddrH9ZKbHzdf9n/vkY/xv9sPQXgGEvBrHHwst5kTbXLE+YpYVPkxepPmW94W16UbdNJd6f3SAzo5W7m1jaKd+8ZZIvk5nlKw9SK6Wle7BLS3f/bTzQLmfAF2T1NsQAeNp9kD1OAzEQhZ/zByQSQiCoXVEA2vyUKRMp9Ailo0g23pBo1155nUg5AS0VB6DlGByAGyDRcgpelkmTImvt6PObmeexAZzjGwr/3yXuhBWO8ShcwREy4Sr1F+Ea+V24jhY+hRvUf4SbuFUD4RYu1BsdVO2Eu5vSbcsKZxgIV3CKJ+Eq9ZVwjfwqXMcVPoQb1L+EmxjjV7iFa2WpDOFhMEFgnEFjig3jAjEcLJIyBtahOfRmEsxMTzd6ETubOBso71dilwMeaDnngCntPbdmvkon/mDLgdSYbh4FS7YpjS4idCgbXyyc1d2oc7D9nu22tNi/a4E1x+xRDWzU/D3bM9JIbAyvkJI18jK3pBJTj2hrrPG7ZynW814IiU68y/SIx5o0dTr3bmniwOLn8owcfbS5kj33qBw+Y1kIeb/dTsQgil2GP5PYcRkAAAB42mNgYoAALjDJyIAOWMGiTIxMjMxsKak5qSWpbFmZiRmJ+QAmgAUIAAAAAf//AAIAAQAAAAwAAAAWAAAAAgABAAMABAABAAQAAAACAAAAAHjaY2BgYGQAgqtL1DlA9M296nUwGgA+8QYgAAA=) format('woff');
font-weight: normal;
font-style: normal;
}
.tui-upload-icon {
font-family: "tuiUpload" !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
padding: 10rpx;
}
.tui-icon-delete:before {
content: "\e601";
}
.tui-icon-plus:before {
content: "\e609";
}
.tui-upload-box {
width: 100%;
display: flex;
flex-wrap: wrap;
}
.tui-upload-add {
font-weight: 100;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
overflow: hidden;
box-sizing: border-box;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.tui-upload__unborder {
border-width: 0;
}
.tui-upload__border {
border-width: 1px;
}
.tui-image-item {
position: relative;
margin-right: 20rpx;
margin-bottom: 20rpx;
flex-shrink: 0;
}
.tui-item-img {
display: block;
}
.tui-img-del {
width: 36rpx;
height: 36rpx;
position: absolute;
right: -12rpx;
top: -12rpx;
border-radius: 50%;
color: white;
font-size: 34rpx;
z-index: 5;
/* #ifdef H5 */
cursor: pointer;
/* #endif */
}
.tui-img-del::before {
content: '';
width: 16rpx;
height: 1px;
position: absolute;
left: 10rpx;
top: 18rpx;
background-color: #fff;
}
.tui-upload-mask {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx 0;
box-sizing: border-box;
background-color: rgba(0, 0, 0, 0.6);
z-index: 3;
}
.tui-upload-loading {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
border: 2px solid;
border-color: #B2B2B2 #B2B2B2 #B2B2B2 #fff;
animation: tui-rotate 0.7s linear infinite;
}
@keyframes tui-rotate {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.tui-tips {
font-size: 26rpx;
color: #fff;
}
.tui-mask-btn {
padding: 4rpx 16rpx;
border-radius: 40rpx;
text-align: center;
font-size: 24rpx;
color: #fff;
border: 1px solid #fff;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 26rpx;
}
.tui-btn-hover {
opacity: 0.8;
}
</style>