uniapp微信小程序图片上传含回显组件(拍照,相册)

效果如下

组件如下

javascript 复制代码
<template>
    <view>
        <view class="upload-box">
            <view class="upload-item" v-for="(item, index) in fileList" :key="index" @click="previewImage(item)">
                <image :src="item.url" mode="aspectFill"></image>
                <text class="delete" @click.stop="deleteImage(index)">×</text>
            </view>
            <view class="upload-btn" v-if="fileList.length < maxCount" @click="uploadShow = true">
                <text class="plus">+</text>
                <text>上传图片</text>
            </view>
        </view>
        <text class="tip">最多上传{{ maxCount }}张图片</text>

        <u-modal :show="uploadShow" @cancel="uploadShow = false" :showConfirmButton="false" :showCancelButton='false'
            @close="uploadShow = false" :closeOnClickOverlay="true">
            <view class="modal_view">
                <view class="modal_photo" style="display:flex">
                    <view class="da" @click="photoUpload(1)">拍照上传</view>
                    <view class="da" @click="photoUpload(2)">从相册导入</view>
                </view>
            </view>
        </u-modal>
    </view>
</template>

<script>
import { getFilePrivate } from '@/api/需引入自己的上传接口'

export default {
    name: 'UploadImage',
    props: {
        // 接收已存在的图片ID字符串,用逗号分隔
        value: {
            type: String,
            default: ''
        },
        // 最大上传数量
        maxCount: {
            type: Number,
            default: 3
        }
    },
    data () {
        return {
            fileList: [],      // 用于页面展示图片 { url: base64 }
            imgUrls: [],       // 存储图片ID数组
            uploadShow: false, // 控制弹窗显示
            baseUrl: uni.getStorageSync('baseUrl')
        }
    },
    watch: {
        value: {
            handler (newVal) {
                if (newVal) {
                    // 防止重复请求:如果当前 imgUrls 已经和传入的一致,则不重新请求
                    const newIds = newVal.split(',')
                    if (JSON.stringify(this.imgUrls) === JSON.stringify(newIds)) return

                    this.imgUrls = newIds
                    this.initFileList()
                } else {
                    this.fileList = []
                    this.imgUrls = []
                }
            },
            immediate: true
        }
    },
    methods: {
        // 初始化图片列表(将ID转为base64展示)
        async initFileList () {
            this.fileList = await Promise.all(
                this.imgUrls.map(async (id) => {
                    const url = await this.getImgUrl(id)
                    return { url }
                })
            )
        },

        // 获取图片Base64
        async getImgUrl (id) {
            try {
                let res = await getFilePrivate(id)
                return 'data:image/jpeg;base64,' + uni.arrayBufferToBase64(res)
            } catch (error) {
                console.error('获取图片失败:', error)
                return ''
            }
        },

        // 选择图片
        photoUpload (type) {
            uni.chooseImage({
                count: this.maxCount - this.fileList.length,
                sourceType: type == 1 ? ['camera'] : ['album'],
                success: async (res) => {
                    this.uploadShow = false
                    for (let filePath of res.tempFilePaths) {
                        await this.uploadImage(filePath)
                    }
                }
            })
        },

        // 上传单张图片到服务器
        uploadImage (filePath) {
            return new Promise((resolve, reject) => {
                uni.uploadFile({
                    url: this.baseUrl + '/filePrivate',
                    filePath: filePath,
                    header: {
                        token: uni.getStorageSync('token')
                    },
                    name: 'file',
                    success: async (res) => {
                        let obj = JSON.parse(res.data)
                        if (obj.data && obj.data.bcode === 0) {
                            const imgId = obj.data.bdata
                            this.imgUrls.push(imgId)

                            const base64Url = await this.getImgUrl(imgId)
                            this.fileList.push({ url: base64Url })

                            // 通知父组件数据更新
                            this.$emit('input', this.imgUrls.join(","))
                        }
                        resolve()
                    },
                    fail: (err) => {
                        reject(err)
                    }
                })
            })
        },

        // 预览图片
        previewImage (item) {
            uni.previewImage({
                urls: [item.url]
            })
        },

        // 删除图片
        deleteImage (index) {
            this.fileList.splice(index, 1)
            this.imgUrls.splice(index, 1)
            this.$emit('input', this.imgUrls.join(","))
        }
    }
}
</script>

<style lang="scss" scoped>
.upload-box {
    display: flex;
    flex-wrap: wrap;
    margin-top: 20rpx;

    .upload-item {
        width: 200rpx;
        height: 200rpx;
        margin-right: 20rpx;
        margin-bottom: 20rpx;
        position: relative;

        image {
            width: 100%;
            height: 100%;
            border-radius: 8rpx;
        }

        .delete {
            position: absolute;
            right: -10rpx;
            top: -10rpx;
            width: 40rpx;
            height: 40rpx;
            line-height: 40rpx;
            text-align: center;
            background-color: #f56c6c;
            color: #fff;
            border-radius: 50%;
        }
    }

    .upload-btn {
        width: 200rpx;
        height: 200rpx;
        border: 2rpx dashed #dcdfe6;
        border-radius: 8rpx;
        display: flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;

        .plus {
            font-size: 60rpx;
            color: #909399;
            margin-bottom: 10rpx;
        }

        text {
            font-size: 24rpx;
            color: #909399;
        }
    }
}

.tip {
    font-size: 24rpx;
    color: #909399;
    margin-top: 10rpx;
}

.modal_view {
    padding: 20rpx;
    width: 560rpx;

    .modal_photo {
        display: flex;
        justify-content: space-between;
        height: 90rpx;
        width: 520rpx;
        margin: 40rpx auto;

        &>view {
            height: 88rpx;
            width: 246rpx;
            justify-content: center;
            border-radius: 45rpx;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 28rpx;
        }

        &>view:nth-child(1) {
            background-color: #225AE7;
            color: #fff;
        }

        &>view:nth-child(2) {
            border: 1rpx solid #666;
            color: #666;
        }
    }
}
</style>

使用示例如下

javascript 复制代码
<template>
  <view style="padding: 20px;">
    <!-- 1. 使用组件:v-model绑定数据,maxCount限制最多上传2张 -->
    <UploadImage v-model="imgIds" :maxCount="2" />
    
    <!-- 2. 看看绑定的值变成了啥 -->
    <view style="margin-top: 20px; color: #999;">
      当前绑定的图片ID字符串:{{ imgIds || '暂无' }}
    </view>
  </view>
</template>

<script>
import UploadImage from '@/components/UploadImage/UploadImage.vue'

export default {
  components: { UploadImage },
  data() {
    return {
      // 用来存放图片ID的字符串,多个ID用逗号隔开。新增时为空,编辑时赋值如 "101,102"
      imgIds: '' 
    }
  }
}
</script>

上传文件的接口必须要特殊处理

javascript 复制代码
export function getFilePrivate(id) {
	return new Promise((resolve, reject) => {
		uni.request({
			url: `xxxxxxx/${id}`,
			responseType: 'arraybuffer',
			header: {
				'Content-Type': 'application/x-www-form-urlencoded',
				'token': `${user.state().token}`
			},
			success: res => {
				resolve(res.data)
			},
			fail: err => {
				console.log('request失败fail', err);
				reject(err)
			}
		})
	})
}
相关推荐
YHHLAI1 小时前
[特殊字符] Agent 智能体开发实战 · 第一课:Tool Use —— 让大模型自动干活
前端·人工智能
nuIl1 小时前
我把 5 个编码 Agent 塞进了一个 npm 包
前端·agent·claude
L-影1 小时前
FastAPI 静态文件:Web 页面的“固定展柜”与“加速引擎”
前端·fastapi
陪我去看海2 小时前
受够了 AI 写完代码留下一堆端口?我做了一个端口管理App!
前端·macos·vibecoding
Xuepoo2 小时前
我抛弃了 DOM,但保留了无障碍访问
前端
李明卫杭州2 小时前
Vue2 vs Vue3 的 h 函数:渲染函数完整迁移指南
前端
月光刺眼2 小时前
用LangChain 打造第一个 Agent:LLM 大脑与 Tool 手脚的完整闭环
javascript·人工智能·全栈
星栈2 小时前
LiveView 的认证系统:从登录到权限,我一开始以为有 `current_user` 就算完事了
前端·前端框架·elixir
在因斯坦2 小时前
摸摸鱼之前端自己研究 Jenkins 自动化部署
前端
西西学代码2 小时前
Flutter---底部导航栏(2)
开发语言·javascript·flutter