139 lines
3.7 KiB
Vue
139 lines
3.7 KiB
Vue
<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>
|