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

效果

思路

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

源码

live-pusher 这个组件必须是 nvue

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

javascript 复制代码
// 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

javascript 复制代码
// 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>
相关推荐
weixin_466485113 天前
halcon标定助手的使用
数码相机
诸葛务农4 天前
ToF(飞行时间)相机在人形机器人非接触式传感领域内的应用
数码相机·机器人
塞北山巅5 天前
相机自动曝光(AE)核心算法——从参数调节到亮度标定
数码相机·算法
美摄科技5 天前
相机sdk是什么意思?
数码相机
phyit5 天前
全景相机领域,影石何以杀出重围?
数码相机
鄃鳕5 天前
装饰器【Python】
开发语言·python·数码相机
聪明不喝牛奶6 天前
【已解决】海康威视相机如何升级固件
数码相机
PAQQ6 天前
1站--视觉搬运工业机器人工作站 -- 相机部分
数码相机·机器人
诸葛务农6 天前
人形机器人基于视觉的非接触式触觉传感技术
数码相机·机器人
moonsims7 天前
被动式热成像摄像机也称前视红外 (FLIR) 摄像机-Sierra-Olympia Technologies 中波红外摄像机
数码相机