officialAccount/pages/camera/CustomCamera.vue

139 lines
3.7 KiB
Vue
Raw Normal View History

2025-06-04 17:33:25 +08:00
<template>
<view class="page">
<!-- 视频预览层拿到 MediaStream 后才显示 -->
<video
ref="video"
class="video"
playsinline webkit-playsinline x5-playsinline
muted
v-show="started"
/>
<!-- Canvas实时把 video 画上来同时用于拍照 -->
<canvas ref="canvas" class="canvas" v-show="started"></canvas>
<!-- 中间遮罩 -->
<image
class="overlay"
src="@/static/index/nu.png"
:style="{ opacity: started ? 1 : 0 }"
/>
<!-- 按钮区 -->
<view class="btn-bar" v-if="started">
<button class="btn close" @click="close">关闭</button>
<button class="btn shoot" @click="shoot"></button>
</view>
<!-- 首次进入的 开始拍照 按钮触发权限弹窗 -->
<view class="starter" v-if="!started">
<button class="btn start" @click="startCamera">开始拍照</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onBeforeUnmount } from 'vue'
import { useRouter } from 'vue-router'
// ----------------- 视图引用 -----------------
const video = ref<HTMLVideoElement>()
const canvas = ref<HTMLCanvasElement>()
// ----------------- 状态 -----------------
const started = ref(false)
let stream: MediaStream | null = null
let rafID: number
// ----------------- 逻辑 -----------------
async function startCamera() {
// 1. 环境检测
if (!navigator.mediaDevices?.getUserMedia) {
uni.showToast({ title: '当前浏览器不支持实时相机', icon: 'none' })
return
}
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: { ideal: 'environment' } },
audio: false
})
// 2. 让 <video> 播起来
const v = video.value!
v.srcObject = stream
await v.play()
// 3. 启动渲染循环,把视频帧 real-time 画到 Canvas
drawFrame()
started.value = true
} catch (err) {
console.error(err)
uni.showToast({ title: '无法获取摄像头权限', icon: 'none' })
}
}
function drawFrame() {
if (!started.value) return
const v = video.value!, c = canvas.value!
const ctx = c.getContext('2d')!
if (v.videoWidth && v.videoHeight) {
// 每帧同步 canvas 尺寸,否则会变形
if (c.width !== window.innerWidth) {
c.width = window.innerWidth
c.height = window.innerHeight
}
ctx.drawImage(v, 0, 0, c.width, c.height)
}
rafID = requestAnimationFrame(drawFrame)
}
function shoot() {
const dataURL = canvas.value!.toDataURL('image/jpeg', 0.9)
// 这里你可以:直接预览 / 上传 oss / emit 给父页
uni.navigateBack()
uni.$emit('photoTaken', dataURL)
}
function close() {
cleanup()
uni.navigateBack()
}
function cleanup() {
cancelAnimationFrame(rafID)
if (stream) stream.getTracks().forEach(t => t.stop())
}
onBeforeUnmount(cleanup)
</script>
<style scoped>
.page { position:fixed; inset:0; background:#000; overflow:hidden; }
.video { position:absolute; inset:0; object-fit:cover; z-index:1; }
.canvas { position:absolute; inset:0; z-index:1; }
.overlay{
position:absolute; top:50%; left:50%;
width:220px; height:220px;
transform:translate(-50%,-50%); z-index:2;
pointer-events:none;
}
.btn-bar{
position:absolute; bottom:60px; inset-inline:0;
display:flex; justify-content:space-around; z-index:3;
}
.btn{
border:none; font-size:16px; color:#fff;
}
.close{ width:100px; height:40px; border-radius:20px; background:rgba(0,0,0,.5); }
.shoot{ width:80px; height:80px; border-radius:40px; background:#fff; }
.starter{
position:absolute; inset:0; display:flex;
justify-content:center; align-items:center; z-index:3;
background:#000;
}
.start{ width:140px; height:46px; border-radius:23px; background:#1aad19; }
</style>