officialAccount/pages/camera/CustomCamera.vue

139 lines
3.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>