/** * 共享相机 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, captureFrameWithBlur, describeCameraError, type Facing, } from '@/utils/camera' import { uploadCameraPhoto } from '@/utils/upload' /** 条带高斯模糊配置 (签到表脱敏用): 对截图竖直 [start, end] 比例区间做高斯模糊 */ export type BlurConfig = { start: number end: number radius: number } export function useCamera(videoId: string, blur?: BlurConfig) { const facing = ref('environment') const captured = ref('') const blurred = 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 { if (blur) { const r = captureFrameWithBlur(video, blur.start, blur.end, blur.radius) captured.value = r.sharp blurred.value = r.blurred } else { captured.value = captureFrame(video) blurred.value = '' } } catch (err) { uni.showToast({ title: (err as Error).message || '截图失败', icon: 'none', }) } } function retake() { captured.value = '' blurred.value = '' } async function confirm() { if (!captured.value) return uni.showLoading({ title: '上传中...' }) try { // 直传 OSS + 回传 URL 到后端 (公开端点, 无需登录) // 签到表额外上传一张高斯模糊版 (blurred) 作为 extraOssUrl, sponsor 只看这个 await uploadCameraPhoto(captured.value, blurred.value || '') uni.hideLoading() uni.showToast({ title: '已上传', icon: 'success', duration: 1500, }) captured.value = '' blurred.value = '' } catch (err) { uni.hideLoading() uni.showToast({ title: (err as Error).message || '上传失败', icon: 'none', duration: 2500, }) } } onMounted(() => { startCamera() }) onBeforeUnmount(() => { const video = getVideoEl() if (video) { stopCamera(video) } }) return { facing, captured, errorMsg, startCamera, flipCamera, takePhoto, retake, confirm, } }