-- 创造无限可能

uniapp多图上传,带图片压缩,水印等方法

2026-07-01 15:13:02
101 人浏览 0 人点赞
有用,点赞支持一下
<template>
    <view>
        <!-- 拍照区域 - 多张照片 -->
              <view class="photo-area">
                <view class="photo-grid">
                  <!-- 已上传的照片 -->
                  <view class="photo-item" v-for="(photo, index) in photoList" :key="index">
                    <image :src="domain + photo" mode="aspectFill" class="photo-image"></image>
                    <view class="photo-delete" @click.stop="deletePhoto(index)">
                      <text>✕</text>
                    </view>
                  </view>
                  <!-- 添加照片按钮 -->
                  <view class="photo-add" @click="takePhoto" v-if="photoList.length < maxPhotoCount">
                    <text class="add-icon">+</text>
                    <text class="add-text">添加照片</text>
                    <text class="add-count">{{ photoList.length }}/{{ maxPhotoCount }}</text>
                  </view>
                </view>
                <view class="photo-tip" v-if="photoList.length === 0">
                  <text>请拍摄现场工作照片作为凭证(最多{{ maxPhotoCount }}张)</text>
                </view>
              </view>

        <!-- Canvas 尺寸动态绑定 -->
        <canvas 
            canvas-id="watermarkCanvas" 
            style="position: absolute; left: -9999px; top: -9999px;"
            :style="{
                width: canvasWidth + 'px',
                height: canvasHeight + 'px'
            }"
        ></canvas>
    </view>
</template>

<script>
    import CommonUtils from '../../utils/common.js'
    import http from '@/utils/http.js'
    import config from "@/config.js"
    export default {
        data() {
            return {
                photoList: [], // 改为数组存储多张照片
                domain: config.domain,
                maxPhotoCount: 6, // 最多上传6张照片
                canvasWidth: 300,
                canvasHeight: 300,
            }
        },
        methods: {
            // 拍摄照片 - 支持多张
            takePhoto() {
              if (this.photoList.length >= this.maxPhotoCount) {
                uni.showToast({
                  title: `最多上传${this.maxPhotoCount}张照片`,
                  icon: 'none'
                })
                return
              }

              uni.chooseImage({
                count: this.maxPhotoCount - this.photoList.length, // 剩余可上传数量
                sizeType: ['original', 'compressed'],
                sourceType: ['album', 'camera'],
                success: async (res) => {
                  let tempFilePaths = res.tempFilePaths
                  uni.showLoading({ title: '上传中...' })


                  try {
                    console.log(44545, tempFilePaths)
                    // ✅ 调用时传入回调函数,更新 Canvas 尺寸
                    const watermarkedPath = await CommonUtils.addSingleWatermark(
                        tempFilePaths[0],
                        {
                            text: '现场签到',
                            time: new Date().toLocaleString(),
                            address: this.address || '未知地址',
                        },
                        (width, height) => {
                            // ✅ 回调函数:更新 Canvas 尺寸
                            console.log('???? 更新 Canvas 尺寸:', width, 'x', height)
                            this.canvasWidth = width
                            this.canvasHeight = height
                        }
                    )

                    tempFilePaths[0] = watermarkedPath

                    // 批量上传
                    const uploadPromises = tempFilePaths.map(filePath => 
                      CommonUtils.uploadImage(this.action, filePath)
                    )

                    const results = await Promise.all(uploadPromises)
                    let successCount = 0

                    results.forEach(data => {
                      if (data.code == 0) {
                        let image = data.data || data.data.img || data.data.url
                        this.photoList.push(image)
                        successCount++
                      }
                    })

                    uni.hideLoading()
                    uni.showToast({
                      title: `成功上传${successCount}张照片`,
                      icon: 'success'
                    })
                  } catch (err) {
                    uni.hideLoading()
                    uni.showToast({
                      title: err.message || '上传失败,请重试',
                      icon: 'none'
                    })
                  }
                },
                fail: (err) => {
                  console.log('选择图片失败:', err.errMsg)
                }
              })
            },

            // 删除照片
            deletePhoto(index) {
              uni.showModal({
                title: '提示',
                content: '确定要删除这张照片吗?',
                success: (res) => {
                  if (res.confirm) {
                    this.photoList.splice(index, 1)
                  }
                }
              })
            },
        }
    }
</script>

<style>
/* 拍照区域 */
.photo-area {
  background: white;
  border-radius: 24rpx;
  overflow: hidden;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
}

.photo-placeholder {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 60rpx 20rpx;
  min-height: 300rpx;
  background: #f8fafa;
  border: 2rpx dashed #d0dbd8;
}

.photo-icon {
  font-size: 80rpx;
  margin-bottom: 20rpx;
}

.photo-text {
  font-size: 28rpx;
  color: #2d3e3a;
  margin-bottom: 8rpx;
}

.photo-tip {
  font-size: 22rpx;
  color: #b0c4be;
}

.photo-preview {
  position: relative;
}

.photo-image {
  width: 100%;
  height: 400rpx;
  display: block;
}

.photo-actions {
  position: absolute;
  bottom: 20rpx;
  right: 20rpx;
}

.retake-btn {
  background: rgba(0, 0, 0, 0.6);
  color: white;
  padding: 12rpx 28rpx;
  border-radius: 40rpx;
  font-size: 24rpx;
  backdrop-filter: blur(10rpx);
}

/* 照片网格布局 */
.photo-area {
  background: white;
  border-radius: 24rpx;
  padding: 24rpx;
  margin-bottom: 20rpx;
  box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
}

.photo-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 16rpx;
}

.photo-item {
  position: relative;
  width: 200rpx;
  height: 200rpx;
  border-radius: 16rpx;
  overflow: hidden;
  background: #f0f4f3;
}

.photo-item .photo-image {
  width: 100%;
  height: 100%;
}

.photo-delete {
  position: absolute;
  top: 8rpx;
  right: 8rpx;
  width: 44rpx;
  height: 44rpx;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: white;
  font-size: 28rpx;
  backdrop-filter: blur(4rpx);
}

.photo-add {
  width: 200rpx;
  height: 200rpx;
  border-radius: 16rpx;
  border: 2rpx dashed #d0dbd8;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #fafcfc;
}

.add-icon {
  font-size: 60rpx;
  color: #b0c4be;
}

.add-text {
  font-size: 24rpx;
  color: #8aa39b;
  margin-top: 8rpx;
}

.add-count {
  font-size: 20rpx;
  color: #b0c4be;
  margin-top: 4rpx;
}

.photo-tip {
  margin-top: 16rpx;
  font-size: 22rpx;
  color: #b0c4be;
  text-align: center;
}

/* 原有样式保持不变 */
.checkin-page {
  min-height: 100vh;
  background: #f5f8fa;
  padding-bottom: 40rpx;
}
</style>

common.js方法


/**
 * 通用工具库
 * 包含所有公共方法,按功能模块分类
 */
const CommonUtils = {

    // 上传照片
    uploadImage(action, filePath) {
        return new Promise((resolve, reject) => {
          uni.showLoading({
            title: '上传中...',
            mask: true
          });

          // 压缩图片
          const fileMaxSize = 0.3 * 1024 * 1024
          const fileSize = filePath.size;
          if (fileSize > fileMaxSize && filePath.type == 'image') {
            const compressionRatio = this.getCompressionRatio(fileSize);
            filePath = this.compressImg(filePath, compressionRatio)
          }

          uni.uploadFile({
            url: action,
            filePath: filePath,
            name: 'file',
            header: {
              'x-csrf-token': uni.getStorageSync("TOKEN"),
            },
            success: (res) => {
              uni.hideLoading();
              try {
                const data = JSON.parse(res.data);
                console.log(33, data)
                resolve(data); // 成功时 resolve
        //         if (data.code === 0) {
        //           // 根据类型设置对应的表单字段
        //            this.formData.diplomaPhoto = data.data.img || data.data.url || data.data;

        //           uni.showToast({
        //             title: '上传成功',
        //             icon: 'success'
        //           });
        //         } else {
        //           throw new Error(data.message || '上传失败');
        //         }
              } catch (error) {
                console.log('解析响应失败', error);
                uni.showToast({
                  title: '上传失败,请重试',
                  icon: 'none'
                });
              }
            },
            fail: (err) => {
              uni.hideLoading();
              console.log('上传失败', err);
              uni.showToast({
                title: '上传失败',
                icon: 'none'
              });
            }
          });
      })
    },

    // 裁剪图片到指定尺寸(自动居中裁剪)
    getImageInfoWithBlob(filePath) {
      return new Promise((resolve, reject) => {
        // 如果是 blob 路径,需要特殊处理
        if (filePath.startsWith('blob:')) {
          const img = new Image();
          img.onload = () => {
            resolve({
              width: img.width,
              height: img.height,
              path: filePath,
              type: 'blob'
            });
          };
          img.onerror = () => {
            reject(new Error('图片加载失败'));
          };
          img.src = filePath;
        } else {
          // 正常路径,使用 uni.getImageInfo
          uni.getImageInfo({
            src: filePath,
            success: (info) => {
              resolve({
                width: info.width,
                height: info.height,
                path: filePath,
                type: 'normal'
              });
            },
            fail: (err) => {
              reject(new Error('获取图片信息失败:' + JSON.stringify(err)));
            }
          });
        }
      });
    },

    // 裁剪图片到指定尺寸(支持 blob 路径)
// 裁剪图片到指定尺寸(支持 blob 路径)
async cropImageToSize(filePath, targetWidth, targetHeight) {
  return new Promise(async (resolve, reject) => {
    try {
      // 1. 获取图片信息
      const imageInfo = await this.getImageInfoWithBlob(filePath);
      const { width, height, path } = imageInfo;

      console.log('原始图片尺寸:', width, 'x', height);

      // 2. 计算裁剪区域(居中裁剪)
      let cropX = 0, cropY = 0, cropWidth = width, cropHeight = height;
      const targetRatio = targetWidth / targetHeight;
      const currentRatio = width / height;

      if (currentRatio > targetRatio) {
        // 原图更宽,裁剪左右
        cropHeight = height;
        cropWidth = height * targetRatio;
        cropX = (width - cropWidth) / 2;
      } else {
        // 原图更高,裁剪上下
        cropWidth = width;
        cropHeight = width / targetRatio;
        cropY = (height - cropHeight) / 2;
      }

      // 3. 创建 Canvas 并绘制图片
      const canvas = document.createElement('canvas');
      const ctx = canvas.getContext('2d');

      // 设置画布尺寸
      canvas.width = targetWidth;
      canvas.height = targetHeight;

      // 创建图片对象
      const img = new Image();
      img.crossOrigin = 'Anonymous'; // 处理跨域问题

      img.onload = () => {
        // 绘制图片(居中裁剪并缩放)
        ctx.drawImage(
          img,
          cropX, cropY, cropWidth, cropHeight,  // 源图裁剪区域
          0, 0, targetWidth, targetHeight       // 目标画布位置和大小
        );

        // 导出为 base64 或 blob
        canvas.toBlob((blob) => {
          // 将 blob 转换为临时文件路径
          const url = URL.createObjectURL(blob);
          resolve(url);
        }, 'image/jpeg', 0.9);
      };

      img.onerror = () => {
        reject(new Error('图片加载失败'));
      };

      // 处理不同类型的图片路径
      if (path.startsWith('blob:')) {
        img.src = path;
      } else {
        // 如果是本地文件路径,需要转换
        uni.getFileSystemManager().readFile({
          filePath: path,
          success: (res) => {
            const blob = new Blob([res.data], { type: 'image/jpeg' });
            img.src = URL.createObjectURL(blob);
          },
          fail: (err) => {
            reject(new Error('读取文件失败:' + JSON.stringify(err)));
          }
        });
      }
    } catch (err) {
      reject(err);
    }
  });
},


    // 压缩图片
    getCompressionRatio(fileSize) {
        let fileMaxSize = 0.3 * 1024 * 1024
      console.log('fileSize',fileSize)
      const multiple = (fileSize / fileMaxSize).toFixed(2);
      let compressionRatio = 1;
      if (multiple > 5) {
        compressionRatio = 0.5
      } else if (multiple > 4) {
        compressionRatio = 0.6
      } else if (multiple > 3) {
        compressionRatio = 0.7
      } else if (multiple > 2) {
        compressionRatio = 0.8
      } else if (multiple > 1) {
        compressionRatio = 0.9
      } else {
        compressionRatio = 2
      }
      return compressionRatio;
    },

    compressImg(source, compressionRatio) {
      let that = this;
      return new Promise((resolve, reject) => {
        image.compressImg(source.url, compressionRatio, source.type, compressRes => {
          resolve(compressRes);
        })
      }).then((res) => {
        source.size = res.size
        // window.URL.revokeObjectURL(source.url) // 删除被压缩的缓存文件,这里注意,如果是相册选择上传,可能会删除相册的图片
        source.url = res.source
        source.thumb = res.source
        return source
      }).catch(err => {
        console.log('图片压缩失败', err)
      })
    },


    // ==================== 时间处理模块 ====================
    /**
       * 格式化时间
       * @param {Date|string} date - 日期
       * @param {string} format - 格式
    */
    formatTime(date, format = 'YYYY-MM-DD HH:mm:ss') {
        if (!date) return ''

        const d = new Date(date)
        const year = d.getFullYear()
        const month = String(d.getMonth() + 1).padStart(2, '0')
        const day = String(d.getDate()).padStart(2, '0')
        const hour = String(d.getHours()).padStart(2, '0')
        const minute = String(d.getMinutes()).padStart(2, '0')
        const second = String(d.getSeconds()).padStart(2, '0')

        return format
          .replace('YYYY', year)
          .replace('MM', month)
          .replace('DD', day)
          .replace('HH', hour)
          .replace('mm', minute)
          .replace('ss', second)
    },


    // ==================== 其他工具模块 ====================
    /**
        * 生成随机ID
        * @param {number} length - 长度
    */
    generateId(length = 8) {
        return Math.random().toString(36).substr(2, length)
    },



    // ==================== 表单验证模块 ====================

      /**
       * 手机号验证
       * @param {string} phone - 手机号
       */
      validatePhone(phone) {
        const reg = /^1[3-9]\d{9}$/
        return reg.test(phone)
      },

      /**
       * 邮箱验证
       * @param {string} email - 邮箱
       */
      validateEmail(email) {
        const reg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
        return reg.test(email)
      },

      /**
       * 身份证验证
       * @param {string} idCard - 身份证号
       */
      validateIdCard(idCard) {
        const reg = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/
        return reg.test(idCard)
      },

      /**
       * 密码强度验证
       * @param {string} password - 密码
       */
      validatePassword(password) {
        // 至少8位,包含字母和数字
        const reg = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/
        return reg.test(password)
      },


    /**
     * 图片压缩
     * @param {string} filePath - 图片路径
     * @param {number} quality - 压缩质量 0-100
     */
    compressImage(filePath, quality = 80) {
      return new Promise((resolve, reject) => {
        uni.compressImage({
          src: filePath,
          quality: quality,
          success: (res) => {
            resolve(res.tempFilePath)
          },
          fail: (error) => {
            reject(error)
          }
        })
      })
    },


    /**
     * 给单张图片添加水印
     */
    // utils/common.js

    /**
     * 给单张图片添加水印
     * @param {String} imagePath - 图片本地路径
     * @param {Object} options - 水印配置
     * @param {Function} onSizeReady - 尺寸就绪回调,用于更新 Canvas 尺寸
     * @returns {Promise<String>} 返回处理后的图片路径
     */
    addSingleWatermark(imagePath, options = {}, onSizeReady = null) {
        console.log('========== addSingleWatermark 开始 ==========')
        console.log('图片路径:', imagePath)

        return new Promise((resolve, reject) => {
            if (!imagePath) {
                reject(new Error('图片路径无效'))
                return
            }

            uni.getImageInfo({
                src: imagePath,
                success: (imageInfo) => {
                    console.log('✅ 图片信息:', imageInfo.width, 'x', imageInfo.height)

                    const width = imageInfo.width
                    const height = imageInfo.height

                    // ✅ 通过回调通知父组件更新 Canvas 尺寸
                    if (typeof onSizeReady === 'function') {
                        onSizeReady(width, height)
                    }

                    // 等待 DOM 更新后再绘制
                    setTimeout(() => {
                        const ctx = uni.createCanvasContext('watermarkCanvas', this)

                        // 清空并绘制原图
                        ctx.clearRect(0, 0, width, height)
                        ctx.drawImage(imagePath, 0, 0, width, height)

                        // ... 水印绘制逻辑(同之前)
                        const fontSize = Math.max(30, Math.min(width, height) / 25)
                        const padding = 30
                        const timeText = options.time || new Date().toLocaleString()
                        const addressText = options.address || '未知地址'
                        let watermarkText = `${timeText}\n${addressText}`

                        if (options.text) {
                            watermarkText = `${watermarkText}`
                            // watermarkText = `${options.text}\n${watermarkText}`
                        }

                        ctx.setFontSize(fontSize)
                        ctx.setFillStyle('rgba(255, 255, 255, 0.9)')
                        ctx.setTextAlign('right')
                        ctx.setTextBaseline('bottom')

                        const lines = watermarkText.split('\n')
                        const lineHeight = fontSize * 1.6
                        let maxWidth = 0
                        lines.forEach(line => {
                            const metrics = ctx.measureText(line)
                            if (metrics.width > maxWidth) {
                                maxWidth = metrics.width
                            }
                        })
                        const totalHeight = lines.length * lineHeight

                        const x = width - padding
                        const y = height - padding

                        const bgPadding = 20
                        ctx.setFillStyle('rgba(0, 0, 0, 0.5)')
                        ctx.fillRect(
                            x - maxWidth - bgPadding, 
                            y - totalHeight - bgPadding, 
                            maxWidth + bgPadding * 2, 
                            totalHeight + bgPadding * 2
                        )

                        ctx.setFillStyle('rgba(255, 255, 255, 0.95)')
                        lines.forEach((line, index) => {
                            const textY = y - (lines.length - 1 - index) * lineHeight
                            ctx.fillText(line, x, textY)
                        })

                        ctx.draw(false, () => {
                            setTimeout(() => {
                                uni.canvasToTempFilePath({
                                    canvasId: 'watermarkCanvas',
                                    width: width,
                                    height: height,
                                    destWidth: width,
                                    destHeight: height,
                                    fileType: 'jpg',
                                    quality: 1,
                                    success: (res) => {
                                        console.log('✅ 水印导出成功')
                                        resolve(res.tempFilePath)
                                    },
                                    fail: (err) => {
                                        console.error('❌ 水印导出失败:', err)
                                        resolve(imagePath)
                                    }
                                }, this)
                            }, 800)
                        })
                    }, 100) // 等待 DOM 更新
                },
                fail: (err) => {
                    console.error('❌ 获取图片信息失败:', err)
                    resolve(imagePath)
                }
            })
        })
    }
}

export default CommonUtils