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 = thisconst ctx = uni.createCameraContext();ctx.takePhoto({quality: 'high',success: (res) => {const imgPath = res.tempImagePathif (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?.lengthswitch (this.markType) {case 'fixed':const spacing = 6 // 行间距const paddingTopBottom = 60 // 整体上下间距// 默认每行的高度 = 字体高度 + 向下间隔const lineHeight = this.textSize + spacingconst 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 - 20context.setFillStyle(this.textColor)context.setFontSize(this.textSize)this.markList.forEach((item, index) => {// 因为文本的 Y 坐标是指文本基线的 Y 轴坐标,所以要获取文本顶部的 Y 坐标const textY = maskRectY - paddingTopBottom / 2 + this.textSize + lineHeight * indexcontext.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 = -rowSizelet 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)// 每个水印间隔 20x += rowSize + 20}y += colSizex = -rowSize}break;}context.save();},getWaterMarkImgPath(src) {const _this = thisuni.getImageInfo({src,success: (image) => {this.canvasWidth = image.widththis.canvasHeight = image.heightconst 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.tempFilePathconst 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;//项目idvar 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++; //图片上传成功,图片上传成功的变量+1photocount--;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++; //图片上传失败,图片上传失败的变量+1uni.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.style1that.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.rhkb.cn/news/497957.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Fetch处理大模型流式数据请求与解析

为什么有的大模型可以一次返回多个 data&#xff1f; Server-Sent Events (SSE)&#xff1a;允许服务器连续发送多个 data: 行&#xff0c;每个代表一个独立的数据块。 流式响应&#xff1a;大模型服务通常以流式响应方式返回数据&#xff0c;提高响应速度。 批量处理&#x…

怎么在电脑桌面上设置备忘录,桌面工作提醒小工具哪个好?

在现代的工作和生活中&#xff0c;我们经常需要记录重要的事项和提醒。而在电脑上设置备忘录&#xff0c;无疑是最方便和有效的方法之一。那么&#xff0c;怎么在电脑桌面上设置备忘录&#xff1f;又有哪个工作提醒小工具值得推荐呢&#xff1f; 以Windows系统为例&#xff0c…

EasyExcel简介和读写操作

EasyExcel简介 官网地址&#xff1a;EasyExcel官方文档 - 基于Java的Excel处理工具 | Easy Excel 官网 EasyExcel 的主要特点如下&#xff1a; 1、高性能&#xff1a;EasyExcel 采用了异步导入导出的方式&#xff0c;并且底层使用 NIO 技术实现&#xff0c;使得其在导入导出大…

【网络协议】路由信息协议 (RIP)

未经许可&#xff0c;不得转载。 路由信息协议&#xff08;Routing Information Protocol&#xff0c;简称 RIP&#xff09;是一种使用跳数&#xff08;hop count&#xff09;作为路由度量标准的路由协议&#xff0c;用于确定源网络和目标网络之间的最佳路径。 文章目录 什么是…

MySQL5.7.26-Linux-安装(2024.12)

文章目录 1.下载压缩包1.访问MySQL版本归档2.找到5.7.26并下载3.百度网盘 2.Linux安装1.卸载原来的MySQL8.0.26&#xff08;如果没有则无需在意&#xff09;1.查看所有mysql的包2.批量卸载3.删除残留文件**配置文件**&#xff08;默认路径&#xff09;&#xff1a; 4.**验证卸载…

《云原生安全攻防》-- K8s安全配置:CIS安全基准与kube-bench工具

在本节课程中&#xff0c;我们来了解一下K8s集群的安全配置&#xff0c;通过对CIS安全基准和kube-bench工具的介绍&#xff0c;可以快速发现K8s集群中不符合最佳实践的配置项&#xff0c;及时进行修复&#xff0c;从而来提高集群的安全性。 在这个课程中&#xff0c;我们将学习…

Flink源码解析之:如何根据算法生成StreamGraph过程

Flink源码解析之&#xff1a;如何根据算法生成StreamGraph过程 在我们日常编写Flink应用的时候&#xff0c;会首先创建一个StreamExecutionEnvironment.getExecutionEnvironment()对象&#xff0c;在添加一些自定义处理算子后&#xff0c;会调用env.execute来执行定义好的Flin…

RoboMIND:多体现基准 机器人操纵的智能规范数据

我们介绍了 RoboMIND&#xff0c;这是机器人操纵的多体现智能规范数据的基准&#xff0c;包括 4 个实施例、279 个不同任务和 61 个不同对象类别的 55k 真实世界演示轨迹。 工业机器人企业 埃斯顿自动化 | 埃夫特机器人 | 节卡机器人 | 珞石机器人 | 法奥机器人 | 非夕科技 | C…

sentinel集成nacos启动报[check-update] get changed dataId error, code: 403错误排查及解决

整合nacos报403错误 因为平台写的一个限流代码逻辑有问题&#xff0c;所以准备使用sentinel来限流。平台依赖里面已经引入了&#xff0c;之前也测试过&#xff0c;把sentinel关于nacos的配置加上后&#xff0c;启动一直输出403错误 [fixed-10.0.20.188_8848-test] [check-upda…

【Redis】 数据淘汰策略

面试官询问缓存过多而内存有限时内存被占满的处理办法&#xff0c;引出 Redis 数据淘汰策略。 数据淘汰策略与数据过期策略不同&#xff0c; 过期策略针对设置过期时间的 key 删除&#xff0c; 淘汰策略是在内存不够时按规则删除内存数据。 八种数据淘汰策略介绍 no evision&…

【畅购商城】详情页模块之评论

目录 接口 分析 后端实现&#xff1a;JavaBean 后端实现 前端实现 接口 GET http://localhost:10010/web-service/comments/spu/2?current1&size2 { "code": 20000, "message": "查询成功", "data": { "impressions&q…

Kafka高性能设计

高性能设计概述 Kafka高性能是多方面协同的结果&#xff0c;包括集群架构、分布式存储、ISR数据同步及高效利用磁盘和操作系统特性等。主要体现在消息分区、顺序读写、页缓存、零拷贝、消息压缩和分批发送六个方面。 消息分区 存储不受单台服务器限制&#xff0c;能处理更多数据…

HTML——13.超链接

<!DOCTYPE html> <html><head><meta charset"UTF-8"><title>超链接</title></head><body><!--超链接:从一个网页链接到另一个网页--><!--语法&#xff1a;<a href"淘宝网链接的地址"> 淘宝…

LVS 负载均衡原理 | 配置示例

注&#xff1a;本文为 “ LVS 负载均衡原理 | 配置” 相关文章合辑。 部分内容已过时&#xff0c;可以看看原理实现。 使用 LVS 实现负载均衡原理及安装配置详解 posted on 2017-02-12 14:35 肖邦 linux 负载均衡集群是 load balance 集群的简写&#xff0c;翻译成中文就是负…

Docker 快速搭建 GBase 8s数据库服务

1.查看Gbase 8s镜像版本 可以去到docker hub网站搜索&#xff1a;gbase8s liaosnet/gbase8s如果无法访问到该网站&#xff0c;可以通过docker search搜索 docker search gbase8s2.拉取Gbase 8s镜像 以下演示的版本是目前官网最新版本Gbase8sV8.8_3.5.1 docker pull liaosn…

使用Lodash工具库的orderby和sortby进行排序的区别

简介 _.orderBy 和 _.sortBy 是 Lodash 库中用于排序数组的两个函数。 区别 _.orderBy 允许你指定一个或多个属性来排序&#xff0c;并为每个属性指定排序方向&#xff08;升序或降序&#xff09;。默认所有值为升序排&#xff0c;指定为"desc" 降序&#xff0c…

uniapp中Nvue白屏问题 ReferenceError: require is not defined

uniapp控制台输出如下 exception function:createInstanceContext, exception:white screen cause create instanceContext failed,check js stack ->Uncaught ReferenceError: require is not defined 或者 exception function:createInstanceContext, exception:white s…

STM32-笔记16-定时器中断点灯

一、实验目的 使用定时器 2 进行中断点灯&#xff0c;500ms LED 灯翻转一次。 二&#xff0c;定时器溢出时间计算 Tout&#xff1a;定时器溢出时间 Ft&#xff1a;定时器的时钟源频率 ARR&#xff1a;自动重装载寄存器的值&#xff08;可设置ARR从0开始&#xff0c;但是计数到…

Visual Studio 使用 GitHub Copilot 与 IntelliCode 辅助编码 【AI辅助开发系列】

&#x1f380;&#x1f380;&#x1f380;【AI辅助编程系列】&#x1f380;&#x1f380;&#x1f380; Visual Studio 使用 GitHub Copilot 与 IntelliCode 辅助编码Visual Studio 安装和管理 GitHub CopilotVisual Studio 使用 GitHub Copilot 扩展Visual Studio 使用 GitHu…

【数据结构】数据结构整体大纲

数据结构用来干什么的&#xff1f;很简单&#xff0c;存数据用的。 &#xff08;这篇文章仅介绍数据结构的大纲&#xff0c;详细讲解放在后面的每一个章节中&#xff0c;逐个击破&#xff09; 那为什么不直接使用数组、集合来存储呢 ——> 如果有成千上亿条数据呢&#xff…