212 lines
4.7 KiB
Vue
212 lines
4.7 KiB
Vue
<template>
|
|
<view class="nbd-photo-upload">
|
|
<view class="nbd-photo-upload__grid">
|
|
<view class="nbd-photo-upload__item" v-for="(photo, index) in localPhotos" :key="photo._id">
|
|
<image class="nbd-photo-upload__img" :src="photo.src" mode="aspectFill" @click="handlePreview(index)"></image>
|
|
<view class="nbd-photo-upload__tag" v-if="photo.uploaded">✓</view>
|
|
<view class="nbd-photo-upload__progress" v-if="photo.uploading">
|
|
<view class="nbd-photo-upload__progress-bar" :style="{ width: photo.progress + '%' }"></view>
|
|
</view>
|
|
<view class="nbd-photo-upload__delete" @tap.stop="handleDelete(index)">
|
|
<u-icon name="close" color="#fff" size="14"></u-icon>
|
|
</view>
|
|
</view>
|
|
<view class="nbd-photo-upload__item nbd-photo-upload__add" @click="handleTakePhoto" v-if="localPhotos.length < maxCount">
|
|
<u-icon name="camera" color="#ccc" size="40"></u-icon>
|
|
<text class="nbd-photo-upload__add-text">拍照</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, watch } from 'vue'
|
|
import { uploadAttach } from '@/api/base.js'
|
|
import { baseFileUrl, parseAttachUrls } from '@/utils/request.js'
|
|
|
|
const props = defineProps({
|
|
attachUrl: { type: String, default: '' },
|
|
maxCount: { type: Number, default: 6 },
|
|
typeName: { type: String, default: '' }
|
|
})
|
|
|
|
const emit = defineEmits(['update:attachUrl'])
|
|
|
|
const localPhotos = ref([])
|
|
let photoIdSeed = 0
|
|
|
|
// 回显已有照片
|
|
watch(() => props.attachUrl, (val) => {
|
|
if (val && localPhotos.value.length === 0) {
|
|
localPhotos.value = parseAttachUrls(val, 0)
|
|
photoIdSeed = localPhotos.value.length
|
|
}
|
|
}, { immediate: true })
|
|
|
|
const isLocalPath = (src) => {
|
|
if (!src) return false
|
|
// 已有服务端附件不需要重复上传
|
|
if (src.startsWith(baseFileUrl)) return false
|
|
return true
|
|
}
|
|
|
|
// 同步 emit
|
|
const syncAttachUrl = () => {
|
|
const urls = localPhotos.value
|
|
.filter(p => p.uploaded && p._attachUrl)
|
|
.map(p => p._attachUrl)
|
|
emit('update:attachUrl', urls.join(','))
|
|
}
|
|
|
|
const handleTakePhoto = () => {
|
|
uni.chooseImage({
|
|
count: 1,
|
|
sourceType: ['camera'],
|
|
success: (res) => {
|
|
const tempPath = res.tempFilePaths[0]
|
|
const id = ++photoIdSeed
|
|
localPhotos.value.push({
|
|
_id: id,
|
|
src: tempPath,
|
|
uploaded: false
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
const handlePreview = (index) => {
|
|
const urls = localPhotos.value.map(p => p.src)
|
|
uni.previewImage({ current: index, urls })
|
|
}
|
|
|
|
const handleDelete = (index) => {
|
|
uni.showModal({
|
|
title: '确认删除',
|
|
content: '确定要删除这张照片吗?',
|
|
success: ({ confirm }) => {
|
|
if (confirm) {
|
|
localPhotos.value.splice(index, 1)
|
|
syncAttachUrl()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// 暴露:外部调用批量上传
|
|
const uploadAll = async () => {
|
|
const pending = localPhotos.value.filter(p => !p.uploaded && isLocalPath(p.src))
|
|
for (const photo of pending) {
|
|
photo.uploading = true
|
|
photo.progress = 0
|
|
try {
|
|
const result = await uploadAttach(photo.src, { typeName: props.typeName }, (progress) => {
|
|
photo.progress = progress
|
|
})
|
|
photo.uploading = false
|
|
photo.uploaded = true
|
|
const url = Array.isArray(result) ? result[0] : result
|
|
if (url) {
|
|
photo._attachUrl = url
|
|
photo.src = `${baseFileUrl}${url}`
|
|
}
|
|
} catch (e) {
|
|
photo.uploading = false
|
|
console.error('照片上传失败:', e)
|
|
uni.showToast({ title: '照片上传失败,请重试', icon: 'none' })
|
|
throw e
|
|
}
|
|
}
|
|
syncAttachUrl()
|
|
}
|
|
|
|
// 暴露:是否有待上传的照片
|
|
const hasPending = () => localPhotos.value.some(p => !p.uploaded && isLocalPath(p.src))
|
|
|
|
defineExpose({ uploadAll, hasPending })
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.nbd-photo-upload {
|
|
width: 100%;
|
|
|
|
&__grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
margin: -8rpx;
|
|
}
|
|
|
|
&__item {
|
|
width: 200rpx;
|
|
height: 200rpx;
|
|
margin: 8rpx;
|
|
border-radius: 12rpx;
|
|
overflow: hidden;
|
|
position: relative;
|
|
background: #f5f6f8;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
&__img {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
&__tag {
|
|
position: absolute;
|
|
top: 6rpx;
|
|
left: 6rpx;
|
|
width: 36rpx;
|
|
height: 36rpx;
|
|
border-radius: 50%;
|
|
background: rgba(0, 200, 83, 0.85);
|
|
color: #fff;
|
|
font-size: 22rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
&__progress {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 6rpx;
|
|
background: rgba(0, 0, 0, 0.3);
|
|
|
|
&-bar {
|
|
height: 100%;
|
|
background: #3577ff;
|
|
transition: width 0.3s ease;
|
|
}
|
|
}
|
|
|
|
&__delete {
|
|
position: absolute;
|
|
top: 6rpx;
|
|
right: 6rpx;
|
|
width: 36rpx;
|
|
height: 36rpx;
|
|
border-radius: 50%;
|
|
background: rgba(0, 0, 0, 0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
&__add {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 2rpx dashed #ddd;
|
|
|
|
&-text {
|
|
font-size: 24rpx;
|
|
color: #ccc;
|
|
margin-top: 8rpx;
|
|
}
|
|
}
|
|
}
|
|
</style>
|