【微信小程序】连续拍照功能实现

前言:
最近在使用uniapp开发微信小程序,遇到这样一个需求,用户想要连续拍照,拍完之后可以删除照片,保留自己想要的照片,然后上传到服务器上。由于原生的方法只能一个个拍照上传,所以只能自己通过视频流截取帧的方式开发一个拍照功能组件,交互和界面都是自己开发,供项目其他需要的地方使用。现做下记录,以下是完整代码:

// Camera.vue 页面,子组件
<template>
  <view v-if="showCamera" class="page-body">
    <!--图片预览-->
    <!-- <template v-if="previewSrc">
      <image @click="handleBack" src="../../static/images/common/back.png" class="close-icon"></image>
      <image :src="previewSrc" class="preview-img"></image>
    </template> -->
    <!--图片上传-->
    <image @click="handleCancel" src="../../static/images/common/close.png" class="close-icon"></image>
    <!--摄像头组件-->
    <camera
      device-position="back"
      flash="off"
      ref="camera"
      class="page-camera"
    ></camera>
    <!--拍照-->
    <image @click="takePhoto" src="../../static/images/common/photo.png" class="photo"></image>
    <!--选择的图片-->
    <view class="select-photo">
      <template v-if="imageList?.length > 0">
        <view class="storage">
          <view v-for="(item, index) in imageList" :key="index" style="position: relative;">
            <image :src="item.tempImagePath" class="select-img" @click="handlePreviewImg(item)"></image>
            <image @click="handleDelete(index)" src="../../static/images/common/cross.png" class="back-icon"></image>
          </view>
        </view>
      </template>
      <view class="finish" @click="handleFinish">确认</view>
    </view>
    <!--添加水印-->
    <view style="position: absolute; top: -999999px;">
			<canvas style="width: 60px; height: 60px" id="uploadCanvas" canvas-id="uploadCanvas"></canvas>
		</view>
  </view>
</template>

<script setup name="MyCamera">
  import { ref, onMounted, getCurrentInstance } from 'vue'
  import { getToken } from '@/utils/auth.js'
  import { useUserStore, useHomeStore } from '@/store/index.js'
  import { $showToast, validateNull, timestampToDateTime } from '@/utils/index.js'

  const userStore = useUserStore()
  const homeStore= useHomeStore()
  const props = defineProps({
    showCamera: {
      type: Boolean,
      default: false
    },
    photos: {
      type: Array,
      default: []
    }
	})
  const { proxy } = getCurrentInstance()
  const imageList = ref([]) // 选择图片
  const uploadFileArr = ref([]) // 已上传的图片
  const previewSrc = ref('') // 图片预览
  const $emit = defineEmits(['handleCancel', 'handleFinish'])

  onMounted(() => {
    imageList.value = []
    uploadFileArr.value = []
  })
  // 添加水印
	const waterMarkerOperate = (filePath) => {
		const address = '江苏省xxxxx'
		uni.getImageInfo({
			src: filePath,
			success: ress => {
				let ctx = uni.createCanvasContext('uploadCanvas', proxy);
				// 将图片绘制到canvas内 60-宽, 60-高
        const cWidth = 40;
        const cHeight = 60;
				ctx.drawImage(filePath, 0, 0, 60, cHeight);
        const fontSize = 2;
				ctx.setFillStyle('rgba(128, 128, 128, 0.9)'); // 设置背景色
				ctx.fillRect(0, 65, cWidth, 34); // 设置背景位置
				ctx.setFontSize(fontSize); // 设置字体大小
				ctx.setFillStyle('#FFFFFF'); // 设置字体颜色
				
        const lineHeight = 2;  // 行高设置
				let textToWidth = (ress.width / 3) * 0.01; // 绘制文本的左下角x坐标位置
				let textToHeight = (ress.height / 3) * 0.1; // 绘制文本的左下角y坐标位置
				const nowTime = timestampToDateTime(); // 当前日期
				ctx.fillText(`日     期:${nowTime}`, textToWidth, textToHeight);
				textToHeight += lineHeight;
				const lines = [];
        let line = '';
        // 遍历字符并拆分行
        for (const char of address) {
					const testLine = line + char;
					const testWidth = ctx.measureText(testLine).width;
					if (testWidth > 24) {
						lines.push(line);
						line = char;
					} else {
						line = testLine;
					}
        }
        // 加入最后一行
        lines.push(line);
				const addressLabel = '地     址:';
				for (let i = 0; i < lines.length; i++) {
					const textLine = lines[i];
					// 仅在第一行添加地址标签
					const lineText = i === 0 ? addressLabel + textLine : textLine;
					ctx.fillText(lineText, textToWidth, textToHeight);
					textToHeight += lineHeight;
        }
				// 绘制完成后,在下一个事件循环将 canvas 内容导出为临时图片地址
				ctx.draw(false, (() => {
					setTimeout(() => {
						uni.canvasToTempFilePath({
							canvasId: 'uploadCanvas',
							success: res1 => {
								// 生成水印
								imageList.value.push({tempImagePath: res1.tempFilePath})
							},
							fail: error => {
								console.log('错误', error);
							},
						}, proxy);
					}, 500);
				})())
			}
		});
	}
  // 拍照
  const takePhoto = () => {
    // 最多拍摄6张
    const totalImages = (imageList.value?.length || 0) + (props.photos?.length || 0);
    if (totalImages > 5) {
      $showToast('已达拍照上限')
      return
    }
    uni.createCameraContext().takePhoto({
      quality: 'high',
      success: (res) => {
        waterMarkerOperate(res.tempImagePath)
      }
    })
  }
  // 拍照完成
  const handleFinish = async () => {
    // 调用上传接口
    const uploadPromises = imageList.value?.map(item => {
      return new Promise((resolve, reject) => {
        uni.uploadFile({
          url: config.baseUrl + '/uploadUrl',
          filePath: item?.tempImagePath,
          name: 'file',
          header: {
            Authorization: 'Bearer ' + getToken()
          },
          success: (res) => {
            try {
              const data = JSON.parse(res.data)
              if (data.fileName) {
                resolve(data.fileName)
              }
            } catch (e) {
              reject(e)
            }
          }
        })
      })
    })
    const imgList = await Promise.all(uploadPromises);
    imgList.forEach(img => {
      uploadFileArr.value.push(img)
    })
    $emit('handleFinish', JSON.stringify({uploadFileArr: uploadFileArr.value}))
    $showToast('上传成功')
    imageList.value = []
    uploadFileArr.value = []
  }
  // 图片预览
  const handlePreviewImg = (item) => {
    previewSrc.value = item?.tempImagePath
  }
  // 返回
  const handleBack = () => {
    previewSrc.value = ''
  }
  // 关闭
  const handleCancel = () => {
    imageList.value = []
    uploadFileArr.value = []
    $emit('handleCancel')
  }
  // 删除图片未上传
  const handleDelete = (index) => {
    imageList.value.splice(index, 1)
  }
</script>

<style lang="scss">
.page-body {
  width: 100%;
  height: 100%;
  position: fixed;
  top: 0;
  left: 0;
  z-index: 99;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  justify-content: center;
  align-items: center;
  .close-icon {
		position: absolute;
		left: 30rpx;
		top: 100rpx;
		width: 44rpx;
		height: 44rpx;
    z-index: 99;
	}
  .page-camera {
    width: 100%;
    height: 90%;
    position: absolute;
    top: 0;
  }
  .photo {
    width: 140rpx;
    height: 140rpx;
    position: absolute;
    bottom: 256rpx;
  }
  .select-photo {
    width: 100%;
    height: 180rpx;
    display: flex;
    flex-direction: row;
    align-items: center;
    margin-bottom: 12rpx;
    background: #000;
    position: absolute;
    bottom: 0;
    .storage {
      max-width: 540rpx; 
      overflow-x: auto;
      display: flex;
      flex-direction: row;
    }
    .finish {
      width: 120rpx;
      height: 60rpx;
      line-height: 60rpx;
      font-size: 28rpx;
      color: #fff;
      text-align: center;
      position: absolute;
      right: 37rpx;
      background: #0fad70;
      border-radius: 10rpx;
    }
    .select-img {
      width: 120rpx;
      height: 120rpx;
      margin-right: 16rpx;
      border-radius: 10rpx;
    }
    .back-icon {
      width: 30rpx;
      height: 30rpx;
      position: absolute;
      right: 0;
      top: -10rpx;
    }
  }
  .preview-img {
    width: 100%;
    height: auto;
    object-fit: contain;
  }
}
</style>

// 时间戳转成时间
const timestampToDateTime = (timestamp) => {
  const date = timestamp ? new Date(timestamp) : /* @__PURE__ */ new Date();
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  const hours = String(date.getHours()).padStart(2, "0");
  const minutes = String(date.getMinutes()).padStart(2, "0");
  const seconds = String(date.getSeconds()).padStart(2, "0");
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
};


// 父组件使用
// index.vue
import Camera from '@/components/Camera.vue'

<my-camera ref="cameraRef" showCamera="true" :photos="form.photo" @handleFinish="handleFinish" @handleCancel="handleCancel"></my-camera>

<script setup>
	const handleFinish = () => {
		// 上传图片完成的逻辑处理
	}
	const handleCancel = () => {
		// 上传图片取消的逻辑处理
	}
</script>

欢迎各位大佬有意见的话评论区留言,互相交流学习~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值