290 lines
7.5 KiB
Vue
290 lines
7.5 KiB
Vue
<template>
|
||
<view class="simple-uploader">
|
||
<!-- 图片列表 -->
|
||
<view class="image-list">
|
||
<!-- 已选图片 -->
|
||
<view v-for="(img, index) in innerList" :key="index" class="image-item">
|
||
<image :src="img.url || img" class="img" mode="aspectFill" @tap="preview(index)"></image>
|
||
<!-- 删除按钮 禁用时隐藏 -->
|
||
<view v-if="!props.disabled" class="delete-btn" @tap.stop="remove(index)">
|
||
<uni-icons type="closeempty" size="16" color="#fff"></uni-icons>
|
||
</view>
|
||
<!-- 上传状态 -->
|
||
<view v-if="img.uploading" class="uploading-mask">
|
||
<text>上传中...</text>
|
||
</view>
|
||
</view>
|
||
<!-- 添加按钮 禁用或者达到上限不显示 -->
|
||
<view
|
||
class="add-btn"
|
||
:class="{ 'add-btn-disabled': props.disabled }"
|
||
@tap="choose"
|
||
v-if="innerList.length < props.limit"
|
||
>
|
||
<uni-icons color="#a6a6a6" type="plusempty" size="25"></uni-icons>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
<script setup>
|
||
import { ref, watch } from 'vue'
|
||
import baseUrl from '@/api/BaseUrl.js'
|
||
import { requestAuth } from '@/utils/Auth.js'
|
||
// ======================
|
||
// Props(可配置)
|
||
// ======================
|
||
const props = defineProps({
|
||
// 双向绑定的图片列表
|
||
modelValue: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
/** 多选 最多上传数量 默认9*/
|
||
limit: {
|
||
type: Number,
|
||
default: 9
|
||
},
|
||
// 选图来源:album(相册)/ camera(拍照)
|
||
sourceType: {
|
||
type: Array,
|
||
default: () => ['album', 'camera']
|
||
},
|
||
// 图片大小:original(原图)/ compressed(压缩图)
|
||
sizeType: {
|
||
type: Array,
|
||
default: () => ['compressed']
|
||
},
|
||
// 后端上传接口地址(必填)
|
||
uploadUrl: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
// 后端接收的文件字段名
|
||
fileFieldName: {
|
||
type: String,
|
||
default: 'file'
|
||
},
|
||
// ✅ 新增:上传时携带的额外参数
|
||
extraData: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
// ✅ 新增:禁用组件
|
||
disabled: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
})
|
||
// ======================
|
||
// Emits
|
||
// ======================
|
||
const emit = defineEmits(['update:modelValue', 'success', 'fail', 'remove'])
|
||
// ======================
|
||
// 内部数据
|
||
// ======================
|
||
const innerList = ref([])
|
||
// 监听外部数据变化
|
||
watch(
|
||
() => props.modelValue,
|
||
newVal => {
|
||
innerList.value = newVal.map(item => {
|
||
if (typeof item === 'string') {
|
||
return { url: item }
|
||
}
|
||
return item
|
||
})
|
||
},
|
||
{ immediate: true, deep: true }
|
||
)
|
||
// ======================
|
||
// 核心方法
|
||
// ======================
|
||
// 1. 选择图片
|
||
function choose() {
|
||
// 禁用直接返回
|
||
if (props.disabled) return
|
||
uni.chooseImage({
|
||
count: props.limit - innerList.value.length,
|
||
sourceType: props.sourceType,
|
||
sizeType: props.sizeType,
|
||
success: async res => {
|
||
const tempPaths = res.tempFilePaths
|
||
// 如果没传上传接口,只返回临时路径
|
||
if (!props.uploadUrl) {
|
||
const newItems = tempPaths.map(path => ({ url: path }))
|
||
innerList.value = [...innerList.value, ...newItems]
|
||
emit(
|
||
'update:modelValue',
|
||
innerList.value.map(i => i.url)
|
||
)
|
||
emit('success', { tempFilePaths: tempPaths })
|
||
return
|
||
}
|
||
// 有上传接口,自动上传
|
||
for (const path of tempPaths) {
|
||
await uploadSingle(path)
|
||
}
|
||
},
|
||
fail: err => {
|
||
// 捕获权限拒绝,弹提示引导设置
|
||
if (err.errMsg.includes('auth') || err.errMsg.includes('Permission')) {
|
||
uni.showModal({
|
||
title: '权限不足',
|
||
content: '相册/相机权限已被拒绝,请前往应用设置手动开启',
|
||
confirmText: '去设置',
|
||
cancelText: '取消',
|
||
success: m => {
|
||
if (m.confirm) {
|
||
// #ifdef APP-PLUS
|
||
uni.openAppAuthorizeSetting()
|
||
// #endif
|
||
// #ifdef MP-WEIXIN
|
||
uni.openSetting()
|
||
// #endif
|
||
}
|
||
},
|
||
fail() {
|
||
emit('fail', err)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
})
|
||
}
|
||
// 2. 上传单张图片
|
||
async function uploadSingle(filePath) {
|
||
// 禁用不再新增上传
|
||
if (props.disabled) return
|
||
// 先添加到列表,标记为上传中
|
||
const tempItem = { url: filePath, uploading: true }
|
||
innerList.value = [...innerList.value, tempItem]
|
||
return new Promise((resolve, reject) => {
|
||
uni.uploadFile({
|
||
url: `${baseUrl}${props.uploadUrl}`,
|
||
filePath: filePath,
|
||
name: props.fileFieldName,
|
||
header: {
|
||
Authorization: 'Bearer ' + (uni.getStorageSync('token') || ''),
|
||
clientid: 'butler'
|
||
},
|
||
// ✅ 新增:携带额外参数
|
||
formData: props.extraData,
|
||
success: res => {
|
||
try {
|
||
const data = JSON.parse(res.data)
|
||
// 假设后端返回格式:{ code: 200, url: "https://xxx.com/xxx.png" }
|
||
// 如果你的后端返回格式不同,请在这里修改
|
||
if (data.code === 200 || data.url) {
|
||
const finalUrl = data.url || data.data?.url
|
||
// 更新列表,替换为网络 URL
|
||
const index = innerList.value.findIndex(item => item.url === filePath)
|
||
if (index !== -1) {
|
||
innerList.value[index] = { url: finalUrl, uploading: false }
|
||
}
|
||
emit(
|
||
'update:modelValue',
|
||
innerList.value.map(i => i.url)
|
||
)
|
||
emit('success', { url: finalUrl, tempPath: filePath })
|
||
resolve(finalUrl)
|
||
} else {
|
||
throw new Error(data.msg || '上传失败')
|
||
}
|
||
} catch (e) {
|
||
console.error('解析上传结果失败', e)
|
||
remove(innerList.value.findIndex(item => item.url === filePath))
|
||
emit('fail', e)
|
||
reject(e)
|
||
}
|
||
},
|
||
fail: err => {
|
||
remove(innerList.value.findIndex(item => item.url === filePath))
|
||
emit('fail', err)
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
}
|
||
// 3. 删除图片
|
||
function remove(index) {
|
||
// 禁用禁止删除
|
||
if (props.disabled) return
|
||
const removed = innerList.value.splice(index, 1)[0]
|
||
emit(
|
||
'update:modelValue',
|
||
innerList.value.map(i => i.url)
|
||
)
|
||
emit('remove', { index, url: removed.url })
|
||
}
|
||
// 4. 预览图片(预览保留,禁用也可以看)
|
||
function preview(index) {
|
||
const urls = innerList.value.map(i => i.url).filter(Boolean)
|
||
if (urls.length <= 0) return
|
||
uni.previewImage({
|
||
urls: urls,
|
||
current: index
|
||
})
|
||
}
|
||
</script>
|
||
<style scoped>
|
||
.simple-uploader {
|
||
width: 100%;
|
||
}
|
||
.image-list {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
}
|
||
.image-list > .image-item:not(:last-child) {
|
||
margin-right: 20rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.image-item {
|
||
position: relative;
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
}
|
||
.img {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 8rpx;
|
||
}
|
||
.delete-btn {
|
||
position: absolute;
|
||
top: 0rpx;
|
||
right: 0rpx;
|
||
width: 32rpx;
|
||
height: 32rpx;
|
||
background: rgba(0, 0, 0, 0.6);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 10;
|
||
}
|
||
.uploading-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
border-radius: 8rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: #fff;
|
||
font-size: 24rpx;
|
||
}
|
||
.add-btn {
|
||
width: 160rpx;
|
||
height: 160rpx;
|
||
background-color: #f3f3f3;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
.add-btn-disabled {
|
||
background-color: #eee;
|
||
opacity: 0.6;
|
||
}
|
||
</style>
|