/** * 共享相机 composable (H5 only) * * 用法: const { captured, errorMsg, startCamera, flipCamera, takePhoto, retake, confirm } = useCamera('my-video-id') * * - 自动在 onMounted 启动摄像头 * - 自动在 onBeforeUnmount 停止摄像头 * - 返回 ref 和方法,业务页只需负责 UI 布局 */ import { ref, onMounted, onBeforeUnmount } from 'vue' import { openCamera, stopCamera, captureFrame, describeCameraError, type Facing } from '@/utils/camera' export function useCamera(videoId: string) { const facing = ref('environment') const captured = ref('') const errorMsg = ref('') function getVideoEl(): HTMLVideoElement | null { return document.getElementById(videoId) as HTMLVideoElement | null } async function startCamera() { errorMsg.value = '' const video = getVideoEl() if (!video) { errorMsg.value = '视频元素未找到' return } try { await openCamera(video, facing.value) } catch (err) { errorMsg.value = describeCameraError(err) console.error('[camera]', err) } } function flipCamera() { facing.value = facing.value === 'environment' ? 'user' : 'environment' startCamera() } async function takePhoto() { const video = getVideoEl() if (!video) return try { captured.value = captureFrame(video) } catch (err) { uni.showToast({ title: (err as Error).message || '截图失败', icon: 'none', }) } } function retake() { captured.value = '' } async function confirm() { // POC: 假上传 + loading 给用户完整仪式感 uni.showLoading({ title: '上传中...' }) await new Promise((resolve) => setTimeout(resolve, 800)) uni.hideLoading() uni.showToast({ title: '已保存 (POC 未上传)', icon: 'none', duration: 1500, }) captured.value = '' } onMounted(() => { startCamera() }) onBeforeUnmount(() => { const video = getVideoEl() if (video) { stopCamera(video) } }) return { facing, captured, errorMsg, startCamera, flipCamera, takePhoto, retake, confirm, } }