效果


思路
- 通过
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>