【微信小程序】连续拍照功能实现
前言:
最近在使用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>
欢迎各位大佬有意见的话评论区留言,互相交流学习~