template view v-ifcanScan classscanBox !-- 相机区域 -- camera v-ifcameraReady idcamera classcamera device-positionback modescanCode :flashflashMode scancodehandleScan errorhandleCameraError !-- 扫码遮罩层 -- view classscanMask !-- 扫描框 -- view classscanFrame !-- 四个角 -- view classcorner lt/view view classcorner rt/view view classcorner lb/view view classcorner rb/view !-- 扫描线 -- view classscanLine/view /view !-- 扫码提示 -- view classscanTip请将二维码放入框内自动扫描/view /view /camera !-- 亮度提示 -- view classtop-tip text classtip-text 当前亮度{{ brightness.toFixed(1) }} {{ showLightIcon ? 建议补光 : 光线正常 }} /text /view !-- 闪光灯按钮亮度低时显示 -- view v-ifshowLightIcon classlight-icon clickhandleFlashToggle text classlight-icon-text {{ flashEnabled ? : }} /text /view !-- 底部手动输入 -- view classtoolBar view classtoolBtn view styledisplay: flex; align-items: center 多次扫描不成功试试nbsp; text classlink clickhandleManualInput手动输入/text i classiconfont icon-arrow-right/i /view /view /view !-- 顶部查看二维码位置 -- view classfindQrCodeBar view classfindQrCode clickqrCodeGuide view styledisplay: flex; align-items: center 如何找到二维码点击查看 i classiconfont icon-arrow-right/i /view /view /view /view /template script setup import { ref, watch, nextTick, onMounted, onUnmounted } from vue; import useDeviceProductStore from /piniaStore/deviceProduct; const emit defineEmits([success, manual-input, guide]); /* * 1. 基础状态 * */ const canScan ref(false); // 是否允许展示扫码页面 const cameraReady ref(false); // 相机是否已完成初始化 /* * 2. 亮度相关状态 * */ const brightness ref(0); // 当前计算出来的亮度值 const showLightIcon ref(false); // 是否显示闪光灯按钮 /* * 3. 闪光灯状态 * */ const flashEnabled ref(false); // 闪光灯开关状态 const flashMode ref(off); // 相机 flash 属性off / torch / auto /* * 4. 业务数据 * */ const deviceProductStore useDeviceProductStore(); /* * 5. 相机上下文与帧监听 * */ let cameraCtx null; let frameListener null; /* * 6. 亮度阈值 * 低于 SHOW_THRESHOLD显示补光按钮 * 高于 HIDE_THRESHOLD隐藏补光按钮 * 用双阈值避免频繁闪烁 * */ const SHOW_THRESHOLD 90; const HIDE_THRESHOLD 120; /* * 7. 申请摄像头权限 * */ const openScan () { uni.getSetting({ success: ({ authSetting }) { if (authSetting[scope.camera]) { canScan.value true; return; } uni.authorize({ scope: scope.camera, success() { canScan.value true; }, fail() { uni.showModal({ title: 未授权摄像头, content: 请允许摄像头权限后再扫码, }); }, }); }, }); }; /* * 8. 亮度计算 * 从相机帧数据里计算平均亮度 * */ const calcAverageBrightness (arrayBuffer) { const data new Uint8Array(arrayBuffer); let sum 0; let count 0; // 每隔 16 个像素采样一次减少性能开销 for (let i 0; i data.length; i 16 * 4) { const r data[i]; const g data[i 1]; const b data[i 2]; // 灰度亮度公式 const gray 0.299 * r 0.587 * g 0.114 * b; sum gray; count; } return count ? sum / count : 0; }; /* * 9. 根据亮度更新 UI * - 亮度低显示闪光灯按钮 * - 亮度高隐藏闪光灯按钮并关闭闪光灯 * */ const updateLightState (value) { brightness.value value; if (!showLightIcon.value value SHOW_THRESHOLD) { showLightIcon.value true; } else if (showLightIcon.value value HIDE_THRESHOLD) { showLightIcon.value false; // 环境变亮时自动关闭闪光灯 if (flashEnabled.value) { flashEnabled.value false; flashMode.value off; } } }; /* * 10. 开始帧检测 * 只有支持 onCameraFrame 的平台才能使用 * */ const startFrameMonitor () { if (!cameraCtx) { console.warn(cameraCtx 不存在); return; } if (typeof cameraCtx.onCameraFrame ! function) { console.warn(当前平台不支持 onCameraFrame); showLightIcon.value true; // 兜底直接显示按钮 return; } try { frameListener cameraCtx.onCameraFrame((frame) { if (!frame || !frame.data) return; const avg calcAverageBrightness(frame.data); updateLightState(avg); }); frameListener.start(); } catch (err) { console.error(启动帧检测失败, err); showLightIcon.value true; // 兜底显示 } }; /* * 11. 停止帧检测 * */ const stopFrameMonitor () { try { if (frameListener typeof frameListener.stop function) { frameListener.stop(); } } catch (err) { console.error(停止帧检测失败, err); } finally { frameListener null; } }; /* * 12. 初始化相机 * 等待 camera 渲染完成后创建上下文 * */ const initCamera async () { await nextTick(); try { cameraCtx uni.createCameraContext(); cameraReady.value true; startFrameMonitor(); } catch (err) { console.error(创建 cameraCtx 失败, err); } }; /* * 13. 扫码成功 * */ const handleScan (e) { const result e?.detail?.result; if (!result) return; uni.vibrateShort?.(); uni.showToast({ title: 扫码成功, icon: success, }); emit(success, result); }; /* * 14. 相机错误 * */ const handleCameraError (e) { console.log(camera error, e); }; /* * 15. 手动输入 * */ const handleManualInput () { emit(manual-input); }; /* * 16. 查看二维码说明 * */ const qrCodeGuide () { emit(guide); }; /* * 17. 切换闪光灯 * */ const handleFlashToggle () { flashEnabled.value !flashEnabled.value; flashMode.value flashEnabled.value ? torch : off; }; /* * 18. 生命周期 * */ onMounted(() { console.log(设备信息, deviceProductStore.deviceInfo); openScan(); }); /* canScan 变成 true 后再初始化相机上下文 */ watch( () canScan.value, async (val) { if (val) { await initCamera(); } }, ); onUnmounted(() { stopFrameMonitor(); }); /script style scoped .scanBox { position: fixed; inset: 0; background: #000; } .camera { width: 100vw; height: 100vh; } .scanMask { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; } .scanFrame { width: 520rpx; height: 520rpx; position: relative; box-shadow: 0 0 0 9999rpx rgba(0, 0, 0, 0.5); } .corner { position: absolute; width: 60rpx; height: 60rpx; border: 8rpx solid #00ff00; } .lt { left: 0; top: 0; border-right: none; border-bottom: none; } .rt { right: 0; top: 0; border-left: none; border-bottom: none; } .lb { left: 0; bottom: 0; border-right: none; border-top: none; } .rb { right: 0; bottom: 0; border-left: none; border-top: none; } .scanLine { position: absolute; left: 0; right: 0; height: 4rpx; background: #00ff00; animation: scanMove 2s linear infinite; } keyframes scanMove { 0% { top: 0; } 100% { top: 100%; } } .scanTip { margin-top: 40rpx; color: #fff; font-size: 30rpx; } /* 顶部亮度提示 */ .top-tip { position: absolute; top: 40rpx; left: 24rpx; right: 24rpx; z-index: 20; padding: 18rpx 24rpx; border-radius: 16rpx; background: rgba(0, 0, 0, 0.35); } .tip-text { color: #fff; font-size: 28rpx; } /* 闪光灯按钮 */ .light-icon { position: absolute; right: 28rpx; bottom: 140rpx; z-index: 20; width: 96rpx; height: 96rpx; border-radius: 50%; background: rgba(0, 0, 0, 0.45); display: flex; align-items: center; justify-content: center; } .light-icon-text { color: #fff; font-size: 44rpx; line-height: 1; } /* 底部操作区 */ .toolBar { position: absolute; bottom: 277rpx; left: 0; right: 0; display: flex; flex-direction: column; align-items: center; } .findQrCodeBar { position: absolute; top: 277rpx; left: 0; right: 0; display: flex; flex-direction: column; align-items: center; } .toolBtn { width: 520rpx; height: 80rpx; background: rgba(0, 0, 0, 0.5); color: #fff; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; } .findQrCode { width: 520rpx; height: 80rpx; background: rgba(0, 0, 0, 0.5); color: #fff; border-radius: 999rpx; display: flex; align-items: center; justify-content: center; font-size: 28rpx; } .link { color: #007aff; } :deep(.findQrCode .iconfont), :deep(.toolBtn .iconfont) { font-size: 23rpx; margin-left: 15rpx; } :deep(.toolBtn .iconfont) { color: #007aff; } /style