当前位置: 首页 > article >正文

uniapp - 小程序实现摄像头拍照 + 水印绘制 + 反转摄像头 + 拍之前显示时间+地点 + 图片上传到阿里云服务器

前言

uniapp,碰到新需求,反转摄像头,需要在打卡的时候对上传图片加上水印,拍照前就显示当前时间日期+地点,拍摄后在呈现刚才拍摄的图加上水印,最好还需要将图片上传到阿里云。

声明

水印部分代码是借鉴的这位博主的博客,剩下的是我根据自己的需求加上的。水印部分看原博主博客就行。
小晗同学 - 原小程序拍照+水印绘制博主博客链接跳转

效果预览

拍摄前预览
右上角切换前后摄像头
底部时间和位置信息,这里位置替换掉真实位置了,代码里没变
在这里插入图片描述
拍摄后效果
在这里插入图片描述

水印组件代码

<template>
    <view class="camera-wrapper">
        <!-- 拍照 -->
        <template v-if="!snapSrc">
            <!-- 相机 -->
            <camera :device-position="cameraPosition" flash="off" @error="handleError" class="image-size">
                <view class="photo-btn" @click="handleTakePhoto">拍照</view>
                <view class="iconfont icon-qiehuanshexiangtou switch-camera-btn" @click="handleSwitchCamera"></view>
                <view class="time-wrap">
                    <view>{{ new Date().toLocaleString() }}</view>
                    <view>{{ location_data }}</view>
                    <!-- <view>这里是位置信息,</view> -->
                </view>
            </camera>
            <!-- 水印 -->
            <canvas canvas-id="photoMarkCanvas" id="photoMarkCanvas" class="mark-canvas"
                :style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }" />
        </template>
        <!-- 预览 -->
        <template v-else>
            <!-- <view class="re-photo-btn btn" @click="handleRephotograph">重拍</view> -->
            <!-- <view class="re-fanhui-btn btn" @click="fanhui">返回</view> -->
            <image class="image-size" :src="snapSrc"></image>
        </template>
    </view>
</template>
<script>
const util_two = require('../../static/utils/util_two.js')
const upload = require('../../static/utils/upload.js')
export default {
    name: 'CameraSnap',
    props: {
        // 照片地址(若传递了照片地址,则默认为预览该照片或添加水印后预览)
        photoSrc: {
            type: String,
            default: ""
        },
        // 水印类型
        markType: {
            type: String,
            default: "fixed", // 定点水印 fixed,背景水印 background
        },
        // 水印文本列表(支持多行)
        markList: {
            type: Array,
            default: () => []
        },
        textColor: {
            type: String,
            default: "#FFFFFF"
        },
        textSize: {
            type: Number,
            default: 32
        },
        // 定点水印的遮罩(为了让水印更清楚)
        useTextMask: {
            type: Boolean,
            default: true
        }
    },
    data() {
        return {
            snapSrc: "",
            canvasWidth: "",
            canvasHeight: "",
            cameraPosition: 'back', // 默认为后置摄像头
            inputText: "", // 用户输入的文本
            location: null, // 存储位置信息
            location_data: "",

            photocount: 10,
            hasUserInfo: false,
            productInfo: [],
            fileurl: [],
            prveImgInfo: [],
            imgs: [],
            arrImg: [],
            imgQueRemData: [],//确实上传数据源
            // 位置和时间日期
        }
    },
    watch: {
        photoSrc: {
            handler: function (newValue, oldValue) {
                if (newValue) {
                    this.getWaterMarkImgPath(newValue)
                }
            },
            immediate: true
        }
    },
    mounted() {
        uni.getLocation({
            type: 'wgs84', // 获取经纬度坐标
            success: (res) => {
                this.location = res;
                setTimeout(() => {
                    this.GetMapData();
                }, 1000);
            },
            fail: (err) => {
                console.error('获取位置信息失败', err);
            }
        });
    },
    methods: {

        closeCamera() {
            this.$emit('close'); // 发送一个事件通知父组件关闭 CameraSnap 组件
        },
        handleTakePhoto() {
            // const that = this
            const ctx = uni.createCameraContext();
            ctx.takePhoto({
                quality: 'high',
                success: (res) => {
                    const imgPath = res.tempImagePath
                    if (this.markList.length) {
                        this.getWaterMarkImgPath(imgPath)
                        this.$emit('watermarkPath', this.snapSrc);

                    } else {
                        this.snapSrc = imgPath;
                        this.$emit('complete', imgPath)
                        this.$emit('watermarkPath', this.snapSrc);
                    }
                }
            });
        },
        handleRephotograph() {
            this.snapSrc = ""
        },
        handleSwitchCamera() {
            this.cameraPosition = this.cameraPosition === 'front' ? 'back' : 'front'; // 切换摄像头
        },
        handleError(err) {
            uni.showModal({
                title: '警告',
                content: '若不授权使用摄像头,将无法使用拍照功能!',
                cancelText: '不授权',
                confirmText: '授权',
                success: (res) => {
                    if (res.confirm) {
                        // 允许打开授权页面,调起客户端小程序设置界面,返回用户设置的操作结果
                        uni.openSetting({
                            success: (res) => {
                                res.authSetting = { "scope.camera": true }
                            },
                        })
                    } else if (res.cancel) {
                        // 拒绝打开授权页面
                        uni.showToast({ title: '您已拒绝授权,无法进行拍照', icon: 'error', duration: 2500 });
                    }
                }
            })
        },
        setWaterMark(context, image) {
            const listLength = this.markList?.length
            switch (this.markType) {
                case 'fixed':
                    const spacing = 6 // 行间距
                    const paddingTopBottom = 60 // 整体上下间距
                    // 默认每行的高度 = 字体高度 + 向下间隔
                    const lineHeight = this.textSize + spacing
                    const allLineHeight = lineHeight * listLength
                    // 矩形遮罩的 Y 坐标
                    const maskRectY = image.height - allLineHeight
                    // 绘制遮罩层
                    if (this.useTextMask) {
                        context.setFillStyle('rgba(0,0,0,0.4)');
                        context.fillRect(0, maskRectY - paddingTopBottom, image.width, allLineHeight + paddingTopBottom)
                    }
                    // 文本与 x 轴之间的间隔
                    const textX = 40
                    // 文本一行的最大宽度(减去 20 是为了一行的左右留间隙)
                    const maxWidth = image.width - 20
                    context.setFillStyle(this.textColor)
                    context.setFontSize(this.textSize)
                    this.markList.forEach((item, index) => {
                        // 因为文本的 Y 坐标是指文本基线的 Y 轴坐标,所以要获取文本顶部的 Y 坐标
                        const textY = maskRectY - paddingTopBottom / 2 + this.textSize + lineHeight * index
                        context.fillText(item, textX, textY, maxWidth);
                    })
                    break;
                case 'background':
                    context.translate(0, 0);
                    context.rotate(30 * Math.PI / 180);
                    context.setFillStyle(this.textColor)
                    context.setFontSize(this.textSize)
                    const colSize = parseInt(image.height / 6)
                    const rowSize = parseInt(image.width / 2)
                    let x = -rowSize
                    let y = -colSize
                    // 循环绘制 5 行 6 列 的文字
                    for (let i = 1; i <= 6; i++) {
                        for (let j = 1; j <= 5; j++) {
                            context.fillText(this.markList[0], x, y, rowSize)
                            // 每个水印间隔 20
                            x += rowSize + 20
                        }
                        y += colSize
                        x = -rowSize
                    }
                    break;
            }
            context.save();
        },
        getWaterMarkImgPath(src) {
            const _this = this
            uni.getImageInfo({
                src,
                success: (image) => {
                    this.canvasWidth = image.width
                    this.canvasHeight = image.height
                    const context = uni.createCanvasContext("photoMarkCanvas", this)
                    context.drawImage(src, 0, 0, image.width, image.height)
                    // 设置水印
                    this.setWaterMark(context, image)
                    // 若还需其他操作,可在操作之后叠加保存:context.restore()
                    // 将画布上的图保存为图片
                    context.draw(false, () => {
                        setTimeout(() => {
                            uni.canvasToTempFilePath({
                                destWidth: image.width,
                                destHeight: image.height,
                                canvasId: 'photoMarkCanvas',
                                fileType: 'jpg',
                                success: function (res) {
                                    console.log("将画布上的图保存为图片", JSON.parse(JSON.stringify(res)));
                                    _this.snapSrc = res.tempFilePath
                                    const tempFilePath = res.tempFilePath;
                                    const tempFilePathArray = [tempFilePath];
                                    _this.uploadimg({
                                        path: tempFilePathArray //这里是选取的图片的地址数组
                                    });
                                    _this.$emit('complete', _this.snapSrc)
                                }
                            },
                                _this
                            );
                        }, 200)
                    });
                }
            })
        },
        fanhui() {
            this.closeCamera();
        },
        sendUploadedImagesToParent() {
            this.$emit('imagesUploaded', this.prveImgInfo);
            // prveImgInfo  imgs
        },
        //多张图片上传  服务器
        uploadimg: function (data) {
            // 这两个是对应的,传递的就是这个路径
            var that = this;
            // var orderid = that.XmID;//项目id
            var orderid = '';
            // var gsid = '';
            let photocount = 9;
            var i = data.i ? data.i : 0; //当前上传的哪张图片
            var success = data.success ? data.success : 0; //上传成功的个数
            var fail = data.fail ? data.fail : 0; //上传失败的个数
            //上传到阿里云
            util_two.request(uni.$baseUrlweb + '/api/xcx/oss/fankui').then(function (result) {
                if (result.code == 0) {
                    // var filePath = data.path;
                    var filePath = data.path[i];
                    var filename = result.dir + orderid + upload.calculate_object_name(filePath, '');
                    uni.uploadFile({
                        url: result.host,
                        filePath: filePath,
                        name: "file",
                        /**上传的参数**/
                        formData: {
                            'key': filename, // 文件名
                            'policy': result.policy,
                            'OSSAccessKeyId': result.accessid,
                            'success_action_status': "200",
                            'signature': result.signature,
                            'callback': result.callback
                        },
                        success: (resp) => {
                            if (resp.statusCode == "200") {
                                success++; //图片上传成功,图片上传成功的变量+1
                                photocount--;
                                var show_url = result.host + '/' + filename + result.style1;
                                var productInfo = that.productInfo;
                                productInfo.push(show_url);
                                var prve_url = result.host + '/' + filename + result.style2;
                                var prveImgInfo = that.prveImgInfo;
                                prveImgInfo.push(prve_url);
                                that.sendUploadedImagesToParent();
                                var up_url = filename;
                                var fileurl = that.fileurl;
                                fileurl.push(up_url);
                                let n = i + 1;
                                uni.showLoading({
                                    title: n + '/' + data.path.length + '上传成功', //这里打印出 上传成功
                                })
                            }
                        },
                        fail: (res) => {
                            fail++; //图片上传失败,图片上传失败的变量+1
                            uni.showLoading({
                                title: (i + 1) + '/' + data.path.length + '上传失败', //这里打印出 上传成功
                            })
                        },
                        complete: (res) => {
                            i++; //这个图片执行完上传后,开始上传下一张
                            if (i == data.path.length) { //当图片传完时,停止调用     
                                // 添加上传数据
                                that.imgQueRemData.push(filename)
                                // 添加到展示数组
                                const path = result.host + '/' + filename + result.style1
                                that.arrImg.push(path)
                                that.imgs.push(path)

                                uni.hideLoading();
                                that.closeCamera();
                                if (success == i) {
                                    uni.showToast({
                                        title: '组图上传完成', //这里打印出 上传成功
                                        icon: 'success',
                                        duration: 1000
                                    })
                                } else {
                                    uni.showModal({
                                        title: '组图上传失败', //这里打印出 上传成功
                                        content: '请稍后再试',
                                        showCancel: false
                                    })
                                }
                            } else { //若图片还没有传完,则继续调用函数
                                data.i = i;
                                data.success = success;
                                data.fail = fail;
                                that.uploadimg(data);
                            }
                        }
                    });
                }
            })
        },
        // 获取具体位置信息
        async GetMapData() {
            const res = await this.$axios("work/getMap", {
                lat: this.location.latitude,
                lon: this.location.longitude
            });
            if (res.data.code == 0) {
                this.location_data = res.data.result;
            } else {
                uni.showToast({
                    title: res.data.msg,
                    icon: 'none',
                    duration: 1000
                })
            }
        },
    }
}
</script>
<style lang="scss" scoped>
.icon-qiehuanshexiangtou {
    font-size: 30px;
}

.camera-wrapper {
    position: relative;
}

.switch-camera-btn {
    position: absolute;
    top: 20px;
    right: 20px;
    color: #fff;
    font-size: 16px;
    cursor: pointer;
}

.mark-canvas {
    position: absolute;
    /* 将画布移出展示区域 */
    top: -200vh;
    left: -200vw;
}

.image-size {
    width: 100%;
    height: 100vh;
}

.photo-btn {
    position: absolute;
    bottom: 120rpx;
    left: 50%;
    transform: translateX(-50%);
    width: 140rpx;
    height: 140rpx;
    line-height: 140rpx;
    text-align: center;
    background-color: #000000;
    border-radius: 50%;
    border: 10rpx solid #ffffff;
    color: #fff;
}

.btn {
    padding: 10rpx 20rpx;
    background-color: #000000;
    border-radius: 10%;
    border: 6rpx solid #ffffff;
    color: #fff
}

.re-photo-btn {
    position: absolute;
    bottom: 150rpx;
    right: 40rpx;

}

.re-fanhui-btn {
    position: absolute;
    bottom: 150rpx;
    right: 180rpx;


}

.re-fanhui-tijao {
    position: absolute;
    bottom: 150rpx;
    right: 320rpx;

}

.time-wrap {
    position: absolute;
    left: 1rem;
    bottom: 1rem;
    color: white;
}
</style>

使用水印相机组件代码

<template>
	<view>
	    <view class="qianDao_img">
			<view class="imgs">
				<view style="margin-right: 10px;">
					照片:
				</view>
				<view @click="paizhao" class="paizhao">
					<view class="iconfont icon-paizhao1"></view>
				</view>
			</view>
			<view class="img_wrap">
				<image v-for="(item, index) in shuiyinImg" :key="index" :src="item" mode="scaleToFill"
					@click="SYpreviewImage(index)" @longpress="SYdeleteImage(index)" />
			</view>
		</view>

		<!-- <button @click="paizhao">拍照</button> -->
		<view class="full-screen" v-if="paizhaoType">
			<CameraSnap @imagesUploaded="handleImagesUploaded" @close="paizhaoType = false"
				:mark-list="[new Date().toLocaleString(), location_data]" textSize="24" useTextMask />
		</view>
		<view class="Bom_Btn">
			<view @click="BaoCunAction" class="Bom_Btn_log">
				<view>提交</view>
			</view>
		</view>
	</view>
</template>

<script>


import CameraSnap from '../CameraSnap.vue'

export default {
	data() {
		return {
			paizhaoType: false,
			location: null, // 存储位置信息
			location_data: "",
			// 水印图片
			shuiyinImg: '',
		};
	},
	components: {
		CameraSnap,
	},
	onLoad(options) {
		uni.getLocation({
			type: 'wgs84', // 获取经纬度坐标
			success: (res) => {
				this.location = res;
				setTimeout(() => {
					this.GetMapData();
				}, 1000);
			},
			fail: (err) => {
				console.error('获取位置信息失败', err);
			}
		});
	},

	methods: {
	    // 这里是水印图片
		handleImagesUploaded(images) {
			console.log("成功上传的图片数据:", images);
			this.shuiyinImg = [...this.shuiyinImg, ...images];
		},
		paizhao() {
			this.paizhaoType = true;
		},
		// 获取具体位置信息
		async GetMapData() {
			const res = await this.$axios("work/getMap", {
				lat: this.location.latitude,
				lon: this.location.longitude
			});
			if (res.data.code == 0) {
				this.location_data = res.data.result;
			} else {
				uni.showToast({
					title: res.data.msg,
					icon: 'none',
					duration: 1000
				})
			}
		},
		// 水印图片预览
		SYpreviewImage(index) {
			uni.previewImage({
				urls: this.shuiyinImg,
				current: index, // 当前显示图片的索引
				loop: true // 是否开启图片轮播
			});
		},
		// 长按删除水印图片
		SYdeleteImage(index) {
			uni.showModal({
				title: '提示',
				content: '确定要删除这张图片吗?',
				success: (res) => {
					if (res.confirm) {
						this.shuiyinImg.splice(index, 1);
					}
				}
			});
		},
	
	
		
		
	}
}
</script>

<style lang="scss" scoped>
.full-screen {
	position: fixed;
	top: 0;
	left: 0;
	width: 100vw;
	height: 100vh;
	// background-color: rgba(0, 0, 0, 0.5); /* 半透明背景 */
	z-index: 9999;
	/* 确保在最顶层显示 */
}

// 签到图片
.qianDao_img {
	// padding: 15px;
	border-top: 1px solid #dddddf;
	background-color: white;

	// 图片
	.imgs {
		padding: 15px;
		display: flex;
		align-items: center;
		flex-wrap: wrap;

	}

	.img_wrap {
		padding: 10px;
		display: flex;
		// justify-content: space-around;
		flex-wrap: wrap;

		image {
			width: 80px;
			height: 80px;
			margin-bottom: 5px;
			margin: 4px;
		}
	}

	// 上传图片
	.paizhao {
		width: 80px;
		height: 80px;
		// border: 1px solid red;
		margin: 10px 0;
		background-color: #f7f8fa;

		.icon-paizhao1 {
			// border: 1px solid red;
			width: 50%;
			font-size: 22px;
			padding-left: 25px;
			padding-top: 25px;
		}
	}
}

.Bom_Btn {
	width: 100%;
	padding: 10px;
	position: absolute;
	bottom: 0;
	left: 0;
	border-top: 1px solid #ededed;
	background-color: white;
	z-index: 2;

	.Bom_Btn_log {
		width: 70%;
		// display: flex;
		// align-items: center;
		// justify-content: center;
		// margin: auto;
		padding: 10px 0;
		margin: auto;
	}

	view {
		border-radius: 5px;
		text-align: center;
		color: white;
		background-color: #1989fa;
	}
}
</style>

只能说勉强够用,算不上精细,凑合看把,引入的两个js文件 都是辅助上传阿里云图片的,就是格式啥的进行校验,就没必要上传了,看懂整个思路就行,具体代码肯定还是需要根据自己的情况进行改动的


http://www.kler.cn/a/461164.html

相关文章:

  • NLP 中文拼写检测纠正论文-07-NLPTEA-2020中文语法错误诊断共享任务概述
  • Spring源码分析之事件机制——观察者模式(二)
  • Tailwind CSS 实战:表单设计与验证实现
  • nature reviews genetics | 需要更多的针对不同种族的癌症基因组图谱研究,促进精准治疗和维护治疗公平权益
  • 【网络安全 | 漏洞挖掘】硬编码凭据泄露实现支付系统账户接管
  • 建立一个Macos载入image的实例含界面
  • 线性代数期末复习 [基础篇]
  • Three.js教程004:坐标辅助器与轨道控制器
  • <Uniswap v3 数学洞察>笔记(part 2)
  • 【CSS in Depth 2 精译_099】17.5:基于页面滚动的动画时间线设置(全新)+ 17.6:最后一点建议 + 17.7:本章小结
  • TreeMap
  • 如何使用C#与SQL Server数据库进行交互
  • 【每日学点鸿蒙知识】深色模式、Webview查看版本、window设置亮度、List缓存节点更新、预编译JS
  • 1panel fail2ban助力服务器SSH以及删除SSH登陆日志
  • ubuntu22 安装CUDA
  • 【蓝桥杯——物联网设计与开发】系列前言
  • git clone 超时
  • 吊舱激光测距核心技术详解!
  • 5G终端串口AT命令 FM650 常用命令
  • STM32-笔记24-智能开关垃圾桶盖
  • 数据要素在金融领域如何应用?
  • 深入理解C#的冒泡排序、快速排序、插入排序、选择排序和归并排序
  • v3.0.8- 「S+会员」新增专属运动秀,试试新穿搭吧- 与「好友」
  • 基于SpringBoot的电影购票平台的设计与实现(源码+SQL+LW+部署讲解)
  • PyQt6+OpenCV 项目练习
  • 2024年12月31日Github流行趋势