Hbuilder 上的水印相机实现方案 (vue3 + vite + hbuilder)

news2025/4/21 22:20:23

效果

在这里插入图片描述

在这里插入图片描述

思路

  • 通过 live-pusher 这个视频推流的组件来获取摄像头
  • 拿到视频的一帧图片之后,跳转到正常的 vue 页面,通过 canvas 来处理图片+水印

源码

live-pusher 这个组件必须是 nvue

至于什么是 nvue,看这个官方文档吧 https://uniapp.dcloud.net.cn/tutorial/nvue-outline.html

// index.nvue
<template>
	<view class="pengke-camera" :style="{ width: windowWidth, height: windowHeight }">
		<!-- live-pusher 显示实时画面 -->
		<live-pusher
			id="livePusher"
			class="livePusher"
			mode="FHD"
			device-position="back"
			:muted="true"
			:enable-camera="true"
			:enable-mic="false"
			:auto-focus="true"
			:zoom="false"
			:style="{ width: windowWidth, height: windowHeight }"
		></live-pusher>

		<!-- 拍照按钮 -->
		<view class="menu">
			<cover-image class="menu-snapshot" @tap="snapshot" src="./icon/snapshot.png"></cover-image>
		</view>
	</view>
</template>

<script>
export default {
	data() {
		return {
			windowWidth: '',
			windowHeight: '',
			livePusher: null
		}
	},
	onLoad() {
		this.initCamera()
	},
	onReady() {
		const pages = getCurrentPages()
		const currentPage = pages[pages.length - 1]
		this.livePusher = uni.createLivePusherContext('livePusher', currentPage)
		this.livePusher.startPreview()
	},
	methods: {
		initCamera() {
			uni.getSystemInfo({
				success: res => {
					this.windowWidth = res.windowWidth
					this.windowHeight = res.windowHeight
				}
			})
		},
		snapshot() {
			this.livePusher.snapshot({
				success: e => {
					const path = e.message.tempImagePath
					uni.navigateTo({
						url: `/pages/camera/preview?img=${encodeURIComponent(path)}`
					})
				},
				fail: err => {
					console.error('拍照失败', err)
				}
			})
		}
	}
}
</script>

<style lang="less">
.pengke-camera {
	position: relative;

	.livePusher {
		position: absolute;
		top: 0;
		left: 0;
		width: 100%;
		height: 100%;
	}

	.menu {
		position: absolute;
		bottom: 60rpx;
		left: 0;
		right: 0;
		display: flex;
		justify-content: center;
		align-items: center;
		z-index: 10;
		pointer-events: auto;

		.menu-snapshot {
			width: 130rpx;
			height: 130rpx;
			background-color: #fff;
			border-radius: 65rpx;
			box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.2);
		}
	}
}

</style>

预览文件 preview.vue

// preview.vue
<template>
  <view class="preview-page">
    <!-- 可见 canvas 显示图 + 水印 -->
    <canvas
      canvas-id="watermarkCanvas"
      id="watermarkCanvas"
      class="watermark-canvas"
      :style="{ width: screenWidth + 'px', height: screenHeight + 'px' }"
    ></canvas>

    <!-- 操作按钮 -->
    <view class="preview-actions">
      <button class="action-btn cancel-btn" @click="goBack">取消</button>
      <button class="action-btn confirm-btn" @click="exportImage">确认</button>
    </view>
  </view>
</template>

<script setup>
import { ref, nextTick, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import message from '@/utils/message'

const imgUrl = ref('')
const canvasWidth = ref(0)
const canvasHeight = ref(0)
const currentTime = ref('')
const locationText = ref('正在获取位置信息...')

const screenWidth = uni.getSystemInfoSync().windowWidth
const screenHeight = uni.getSystemInfoSync().windowHeight

onLoad((options) => {
  if (options.img) {
    imgUrl.value = decodeURIComponent(options.img)
    initImage()
  }
  updateTime()
  updateLocation()
})

function updateTime() {
  const now = new Date()
  currentTime.value = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`
}

function updateLocation() {
  uni.getLocation({
    type: 'wgs84',
    success: (res) => {
      locationText.value = `经度: ${res.longitude}  纬度: ${res.latitude}`
      drawCanvas()
    },
    fail: () => {
      locationText.value = '位置信息获取失败'
      drawCanvas()
    }
  })
}

function initImage() {
  uni.getImageInfo({
    src: imgUrl.value,
    success(res) {
      canvasWidth.value = res.width
      canvasHeight.value = res.height
      drawCanvas()
    },
    fail(err) {
      console.error('图片信息获取失败:', err)
    }
  })
}

// 绘制预览 canvas
function drawCanvas() {
  if (!imgUrl.value || !canvasWidth.value || !canvasHeight.value) return

  nextTick(() => {
    const ctx = uni.createCanvasContext('watermarkCanvas')

    // 绘制原图
    ctx.drawImage(imgUrl.value, 0, 0, screenWidth, screenHeight)

    // 设置水印样式
    ctx.setFontSize(16)
    ctx.setFillStyle('white')
    ctx.setTextAlign('left')

    ctx.fillText(currentTime.value, 20, screenHeight - 160)
    ctx.setFontSize(16)
    ctx.fillText(locationText.value, 20, screenHeight - 120)

    ctx.draw()
  })
}

// 点击确认导出
function exportImage() {
  // 显示加载提示
  uni.showLoading({
    title: '导出中...',
    mask: true  // 防止点击遮罩层关闭
  })

  uni.canvasToTempFilePath({
    canvasId: 'watermarkCanvas',
    destWidth: canvasWidth.value,
    destHeight: canvasHeight.value,
    success: (res) => {
      // 隐藏加载提示
      uni.hideLoading()
      console.log('导出成功:', res.tempFilePath)
      message.success(`导出成功! 文件路径为 ${res.tempFilePath}`)
      uni.previewImage({ urls: [res.tempFilePath] })
    },
    fail: (err) => {
      // 隐藏加载提示
      uni.hideLoading()
      console.error('导出失败:', err)
      uni.showToast({ title: '导出失败', icon: 'none' })
    }
  })
}


function goBack() {
  uni.navigateBack()
}
</script>

<style scoped lang="scss">
.preview-page {
  width: 100vw;
  height: 100vh;
  position: relative;
  background: #000;
  overflow: hidden;
}

.watermark-canvas {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 1;
}

.preview-actions {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 60rpx;
  display: flex;
  justify-content: center;
  gap: 40rpx;
  z-index: 10;
}

.action-btn {
  padding: 0 40rpx;
  height: 80rpx;
  line-height: 80rpx;
  border-radius: 40rpx;
  font-size: 28rpx;
  background: #fff;
  color: #333;
  border: none;
  opacity: 0.9;
}

.cancel-btn {
  background: #eee;
}

.confirm-btn {
  background: #19be6b;
  color: #fff;
}
</style>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2339698.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

TinyEngine 2.4版本正式发布:文档全面开源,实现主题自定义,体验焕新升级!

本文由体验技术团队李璇原创。 前言 TinyEngine低代码引擎使开发者能够定制低代码平台。它是低代码平台的底座&#xff0c;提供可视化搭建页面等基础能力&#xff0c;既可以通过线上搭配组合&#xff0c;也可以通过cli创建个人工程进行二次开发&#xff0c;实时定制出自己的低…

毕业答辩的PPT应该包括哪些内容?

一、PPT 模板的选择 1. 忌单调的白底黑字&#xff0c;应进行一些艺术设计&#xff0c;使人看着画面舒服&#xff0c;但不必过于花哨。总之&#xff0c;专业制作&#xff0c;符合技术人士的喜好。 2. 去掉不相关信息&#xff0c;如一些下载模板上的LOGO。把学校或部门的LOGO放…

Vscode --- LinuxPrereqs │远程主机可能不符合 glibc 和 libstdc++ Vs code 服务器的先决条件

打开vscode连接远程linux服务器&#xff0c;发现连接失败&#xff0c;并出现如下报错信息&#xff1a; 原因是&#xff1a; vscode 官网公告如下&#xff1a;2025 年 3 月 (版本 1.99) - VSCode 编辑器 版本1.97 官网公告如下&#xff1a;链接 版本1.98 官网公告如下&am…

安装部署RabbitMQ

一、RabbitMQ安装部署 1、下载epel源 2、安装RabbitMQ 3、启动RabbitMQ web管理界面 启用插件 rabbitmq数据目录 创建rabbitmq用户 设置为管理员角色 给用户赋予权限 4、访问rabbitmq

Qt实现文件传输客户端(图文详解+代码详细注释)

Qt实现文件传输客户端 1、 客户端UI界面设计2、客户端2.1 添加网络模块和头文件2.2 创建Tcp对象2.3 连接按钮2.3.1 连接按钮连接信号与槽2.3.2 连接按钮实现 2.4 读取文件2.4.1 连接读取文件的信号与槽2.4.2 读取文件槽函数实现2.5 进度条2.5.1 设置进度条初始值2.5.2 初始化进…

机器学习期末

选择题 以下哪项不是机器学习的类型&#xff1f; A. 监督学习 B.无监督学习 C.半监督学习 D.全监督学习 D 哪一个是机器学习的合理定义? A、机器学习是计算机编程的科学 B、机器学习从标记的数据中学习 C、机器学习是允许机器人智能行动的领域 D、机器学习能使计算机能够在…

Python多任务编程:进程全面详解与实战指南

1. 进程基础概念 1.1 什么是进程&#xff1f; 进程(Process)是指正在执行的程序&#xff0c;是程序执行过程中的一次指令、数据集等的集合。简单来说&#xff0c;进程就是程序的一次执行过程&#xff0c;它是一个动态的概念。 想象你打开电脑上的音乐播放器听歌&#xff0c;…

【QT】 QT中的列表框-横向列表框-树状列表框-表格列表框

QT中的列表框-横向列表框-树状列表框-表格列表框 1.横向列表框(1)主要方法(2)信号(3) 示例代码1:(4) 现象&#xff1a;(5) 示例代码2&#xff1a;加载目录项在横向列表框显示(6) 现象&#xff1a; 2.树状列表框 QTreeWidget(1)使用思路(2)信号(3)常用的接口函数(4) 示例代码&am…

决策树:ID3,C4.5,CART树总结

树模型总结 决策树部分重点关注分叉的指标&#xff0c;多叉还是单叉&#xff0c;处理离散还是连续值&#xff0c;剪枝方法&#xff0c;以及回归还是分类 一、决策树 ID3(Iterative Dichotomiser 3) 、C4.5、CART决策树 ID3:确定分类规则判别指标、寻找能够最快速降低信息熵的方…

easyexcel使用模板填充excel坑点总结

1.单层map设置值是{属性}&#xff0c;那使用两层map进行设置值&#xff0c;是不是可以使用{属性.属性}&#xff0c;以为取出map里字段只用{属性}就可以设置值&#xff0c;那再加个.就可以从里边map取出对应属性&#xff0c;没有两层map写法 填充得到的文件打开报错 was empty (…

C# LINQ基础知识

简介 LINQ(Language Integrated Query)&#xff0c;语言集成查询&#xff0c;是一系列直接将查询功能集成到 C# 语言的技术统称。使用LINQ表达式可以对数据集合进行过滤、排序、分组、聚合、串联等操作。 例子&#xff1a; public class Person {public int Id;public string…

GCoNet+:更强大的团队协作 Co-Salient 目标检测器 2023 GCoNet+(翻译)

摘要 摘要&#xff1a;本文提出了一种新颖的端到端群体协作学习网络&#xff0c;名为GCoNet&#xff0c;它能够高效&#xff08;每秒250帧&#xff09;且有效地识别自然场景中的共同显著目标。所提出的GCoNet通过基于以下两个关键准则挖掘一致性表示&#xff0c;实现了共同显著…

QT常见输入类控件及其属性

Line Edit QLineEdit用来表示单行输入框&#xff0c;可以输入一段文本&#xff0c;但是不能换行 核心属性&#xff1a; 核心信号 信号 说明 void cursorPositionChanged(int old,int new) 当鼠标移动时发出此型号&#xff0c;old为先前位置&#xff0c;new为新位置 void …

Few-shot medical image segmentation with high-fidelity prototypes 论文总结

题目&#xff1a;Few-shot medical image segmentation with high-fidelity prototypes&#xff08;高精确原型&#xff09; 论文&#xff1a;Few-shot medical image segmentation with high-fidelity prototypes - ScienceDirect 源码&#xff1a;https://github.com/tntek/D…

如何使用Node-RED采集西门子PLC数据通过MQTT协议实现数据交互并WEB组态显示

需求概述 本章节主要实现一个流程&#xff1a;使用纵横智控的EG网关通过Node-red&#xff08;可视化编程&#xff09;采集PLC数据&#xff0c;并通过MQTT协议和VISION&#xff08;WEB组态&#xff09;实现数据交互。 以采集西门子PLC为例&#xff0c;要采集的PLC的IP、端口和点…

【cocos creator 3.x】速通3d模型导入, 模型创建,阴影,材质使用,模型贴图绑定

1、右键创建平面&#xff0c;立方体 2、点击场景根节点&#xff0c;shadows勾选enabled3、点击灯光&#xff0c;shadow enabled勾选 4、点击模型&#xff0c;勾选接收阴影&#xff0c;投射阴影&#xff08;按照需要勾选&#xff09; 5、材质创建 6、选中节点&#xff0c;找…

驱动开发硬核特训 · Day 15:电源管理核心知识与实战解析

在嵌入式系统中&#xff0c;电源管理&#xff08;Power Management&#xff09;并不是“可选项”&#xff0c;而是实际部署中影响系统稳定性、功耗、安全性的重要一环。今天我们将以 Linux 电源管理框架 为基础&#xff0c;从理论结构、内核架构&#xff0c;再到典型驱动实战&a…

【零基础】基于DeepSeek-R1与Qwen2.5Max的行业洞察自动化平台

自动生成行业报告,通过调用两个不同的大模型(DeepSeek 和 Qwen),完成从行业趋势分析到结构化报告生成的全过程。 完整代码:https://mp.weixin.qq.com/s/6pHi_aIDBcJKw1U61n1uUg 🧠 1. 整体目的与功能 该脚本实现了一个名为 ReportGenerator 的类,用于: 调用 DeepSe…

C 语言联合与枚举:自定义类型的核心解析

目录 1.联合体 1.1联合体的声明与创建 1.2联合体在内存中的存储 1.3相同成员的结构体与内存比较 1.4联合体内存空间大小的计算 1.5联合体的应用 2.枚举类型 2.1枚举变量的声明 2.2枚举变量的优点 2.3枚举的使用 上篇博客中&#xff0c;我们通过学习了解了C语言中一种自…

基于Canal+Spring Boot+Kafka的MySQL数据变更实时监听实战指南

前期知识背景 binlog 什么是binlog 它记录了所有的DDL和DML(除 了数据查询语句)语句&#xff0c;以事件形式记录&#xff0c;还包含语句所执行的消耗的时间&#xff0c;MySQL 的二进制日志是事务安全型的。一般来说开启二进制日志大概会有1%的性能损耗。 binlog分类 MySQL Bi…