Files
sh-app/components/nbd-photo-upload.vue
T
2026-06-24 20:50:15 +08:00

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">&#10003;</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>