uni-app Vue3+TS 微信小程序扫码核销功能实现

发布时间:2026/5/19 2:05:12

uni-app Vue3+TS 微信小程序扫码核销功能实现 uni-app Vue3TS 微信小程序扫码核销功能实现基于uniapp camera组件实现微信小程序扫码页面包含相机权限校验、动态扫描动画、扫码防抖、异常错误处理1. 实现逻辑1.1 权限管控逻辑页面进入自动检测手机相机权限区分未申请、申请中、已授权、已拒绝四种状态首次进入自动唤起相机权限申请用户拒绝后弹窗引导跳转系统设置开启权限权限状态动态切换页面UI无权限隐藏相机组件展示权限提示1.2 页面布局逻辑全屏黑色背景铺满页面扫码相机区域居中固定定位自定义扫码框四角高亮边框替代原生简陋扫码框上层叠加遮罩层级保证扫描动画、提示文字置顶显示底部放置扫码引导文案提升用户使用体验1.3 扫描动画逻辑使用定时器实现扫描线上下往复滑动效果动态计算扫描线透明度上下两端渐隐、中间高亮页面显示启动动画页面隐藏/销毁清空定时器杜绝内存泄漏1.4 扫码业务逻辑增加扫码防抖锁防止短时间重复多次触发扫码事件扫码成功触发手机短震动反馈弹出成功提示并跳转业务页面扫码识别失败统一错误提示设置冷却时间自动重置扫码状态相机启动失败自动重新校验权限完善异常兜底处理1.5 生命周期优化onShow页面显示重置扫码状态、重新校验相机权限onHide页面隐藏停止扫描动画、清空延时定时器onUnmounted页面彻底销毁清除所有定时器优化性能2. 具体代码2.1 页面完整代码template view classcontent !-- 相机组件 -- camera v-if!showPopup hasCameraPermission modescanCode classcamera device-positionback flashauto scancodeonScanCode erroronCameraError /camera !-- 相机权限被拒绝时的提示 -- view v-if!hasCameraPermission !showPopup permissionStatus denied classpermission-denied text classpermission-text需要相机权限才能扫描二维码/text button classpermission-btn clickrequestCameraPermission申请相机权限/button /view !-- 首次进入页面正在申请权限的加载状态 -- view v-if!hasCameraPermission !showPopup permissionStatus requesting classpermission-loading text classpermission-text正在申请相机权限.../text /view !-- 遮罩层 UI - 修改为使用四个方向的遮罩块 -- view v-ifhasCameraPermission classscan-overlay !-- .中间扫描区域 (绝对定位居中) -- view classscan-area !-- 四个角标 -- view classcorner corner-tl/view view classcorner corner-tr/view view classcorner corner-bl/view view classcorner corner-br/view !-- 扫描线 - 使用 JS 控制动画 -- view classscan-line :stylescanLineStyle/view /view /view !-- 提示文本 -- view v-ifhasCameraPermission classtip_text将二维码/条形码放入框内即可自动扫描/view /view /template script setup langts import { ref, computed, onUnmounted } from vue; import { onShow, onHide } from dcloudio/uni-app; // 防抖标记 const isScanning ref(false); // 用于存储冷却期定时器 const scanCooldownTimer refany(null); // 相机权限状态 const hasCameraPermission ref(false); // 权限状态unknown未知requesting申请中denied已拒绝granted已授权 const permissionStatus ref(unknown); const showPopup ref(false); // 扫描线动画相关 const scanLineTop ref(0); const scanLineOpacity ref(0); const scanAnimationTimer refany(null); const scanAreaHeight 267; // 扫描区域高度与 CSS 中保持一致 // 扫描线样式 const scanLineStyle computed(() { return { top: ${scanLineTop.value}px, opacity: scanLineOpacity.value }; }); // 开始扫描线动画 const startScanAnimation () { let direction 1; // 1 向下-1 向上 let position 0; const speed 2; // 每帧移动的像素 scanAnimationTimer.value setInterval(() { position speed * direction; // 计算透明度 if (position scanAreaHeight * 0.1) { scanLineOpacity.value position / (scanAreaHeight * 0.1); } else if (position scanAreaHeight * 0.9) { scanLineOpacity.value 1 - (position - scanAreaHeight * 0.9) / (scanAreaHeight * 0.1); } else { scanLineOpacity.value 1; } // 边界检测 if (position scanAreaHeight) { position scanAreaHeight; direction -1; } else if (position 0) { position 0; direction 1; } scanLineTop.value position; }, 16); }; // 停止扫描线动画 const stopScanAnimation () { if (scanAnimationTimer.value) { clearInterval(scanAnimationTimer.value); scanAnimationTimer.value null; } }; // onShow 生命周期确保页面显示时重置状态 onShow(() { // 页面显示时确保没有在进行冷却可以立即开始新的扫描 clearScanCooldown(); // 检查相机权限 checkCameraPermission(); }); // onHide 生命周期页面隐藏时清理状态 onHide(() { if (scanCooldownTimer.value) { clearTimeout(scanCooldownTimer.value); scanCooldownTimer.value null; } stopScanAnimation(); }); // 页面卸载时清理 onUnmounted(() { stopScanAnimation(); if (scanCooldownTimer.value) { clearTimeout(scanCooldownTimer.value); scanCooldownTimer.value null; } }); // 检查相机权限 function checkCameraPermission() { uni.getSystemInfo({ success: (res) { // 检查平台是否支持权限API if (res.platform android || res.platform ios) { uni.getSetting({ success: (settingRes) { // 检查相机权限 if (settingRes.authSetting[scope.camera]) { hasCameraPermission.value true; permissionStatus.value granted; startScanAnimation(); } else if (settingRes.authSetting[scope.camera] false) { // 用户已经明确拒绝过 hasCameraPermission.value false; permissionStatus.value denied; } else { // 权限未知首次进入自动申请权限 hasCameraPermission.value false; permissionStatus.value requesting; requestCameraPermission(true); // 传入true表示是自动申请 } }, fail: () { // 获取设置失败尝试申请权限 hasCameraPermission.value false; permissionStatus.value requesting; requestCameraPermission(true); } }); } else { // 非移动平台默认有权限 hasCameraPermission.value true; permissionStatus.value granted; startScanAnimation(); } }, fail: () { // 获取系统信息失败尝试申请权限 hasCameraPermission.value false; permissionStatus.value requesting; requestCameraPermission(true); } }); } // 申请相机权限 function requestCameraPermission(isAutoRequest false) { // 如果已经在申请中避免重复申请 if (permissionStatus.value requesting !isAutoRequest) return; permissionStatus.value requesting; uni.authorize({ scope: scope.camera, success: () { hasCameraPermission.value true; permissionStatus.value granted; startScanAnimation(); }, fail: () { hasCameraPermission.value false; permissionStatus.value denied; // 只有在自动申请失败时才显示弹窗 if (isAutoRequest) { uni.showModal({ title: 提示, content: 您已拒绝相机权限将无法使用扫码功能。是否前往设置开启权限, success: (modalRes) { if (modalRes.confirm) { uni.openSetting({ success: (settingRes) { if (settingRes.authSetting[scope.camera]) { hasCameraPermission.value true; permissionStatus.value granted; startScanAnimation(); } } }); } } }); } } }); } // 清除冷却期并重置状态 function clearScanCooldown() { if (scanCooldownTimer.value) { clearTimeout(scanCooldownTimer.value); scanCooldownTimer.value null; } isScanning.value false; } /** * 扫码成功回调 */ function onScanCode(e: any) { console.log(扫码结果, e.detail); if (isScanning.value) return; isScanning.value true; const result e.detail.result; if (result ! null result.length 0) { try { // 触发手机震动反馈 uni.vibrateShort(); uni.showToast({ icon: success, title: 扫码成功, duration: 1500, }); uni.navigateTo({ url: /packageB/scanInfo/index ?id${result}, }); } catch (e: any) { uni.vibrateShort(); uni.showToast({ icon: error, title: e?.description ?? 识别失败, duration: 1500 }); scanCooldownTimer.value setTimeout(() { clearScanCooldown(); }, 1500); } } else { isScanning.value false; } } /** * 相机错误回调 */ function onCameraError(e: any) { console.error(相机错误, e.detail); // 相机错误时重新检查权限 checkCameraPermission(); if (hasCameraPermission.value) { // 有权限但仍然出错可能是其他问题 uni.showToast({ title: 相机开启失败请重试, icon: none }); } // 如果没有权限checkCameraPermission函数会处理权限申请 }; /script style scoped langscss /* 确保页面全屏 */ .content { width: 100vw; height: 100vh; position: relative; background-color: #000; } .camera { z-index: 1; position: absolute; width: 266px; height: 266px; left: 50%; top: 25%; transform: translateX(-50%); } /* 遮罩层容器 */ .scan-overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; z-index: 2; /* 确保遮罩层在上层 */ } /* 扫描区域容器 - 绝对定位居中 */ .scan-area { position: absolute; width: 267px; height: 267px; left: 50%; top: 25%; transform: translateX(-50%); border: 1px solid rgba(255, 255, 255, 0.1); background-color: transparent; z-index: 4; overflow: visible; } /* 四个角的样式 */ .corner { position: absolute; width: 35px; height: 35px; border-color: #ffc783; /* 绿色角标 */ border-style: solid; border-width: 0; z-index: 199; } .corner-tl { top: -3px; left: -3px; border-top-width: 5px; border-left-width: 5px; border-radius: 3px; border-top-left-radius: 8px; } .corner-tr { top: -3px; right: -3px; border-top-width: 5px; border-right-width: 5px; border-top-right-radius: 8px; } .corner-bl { bottom: -3px; left: -3px; border-bottom-width: 5px; border-left-width: 5px; border-bottom-left-radius: 8px; } .corner-br { bottom: -3px; right: -3px; border-bottom-width: 5px; border-right-width: 5px; border-bottom-right-radius: 8px; } /* 扫描线样式 - 动画由 JS 控制 */ .scan-line { width: 100%; height: 2px; background: linear-gradient(90deg, rgba(255, 199, 131, 0.1) 0%, #ffc783 54.15%, #737373 100%); position: absolute; z-index: 5; transition: opacity 0.1s ease; } .footer-tip { color: #cccccc; font-size: 14px; text-align: center; width: 100%; } .tip_text { width: 100%; position: absolute; left: 50%; transform: translateX(-50%); top: 62%; font-family: PingFang SC, PingFang SC; font-weight: bold; font-size: 14px; color: #ffffff; z-index: 18; text-align: center; } .permission-denied { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; padding: 40rpx; } .permission-loading { display: flex; flex-direction: column; justify-content: center; align-items: center; height: 100%; padding: 40rpx; } .camera-icon { width: 120rpx; height: 120rpx; margin-bottom: 30rpx; } .permission-text { font-size: 32rpx; color: #333; text-align: center; margin-bottom: 40rpx; line-height: 1.5; } .permission-btn { background-color: #c1a37e; color: white; border-radius: 8rpx; padding: 20rpx 40rpx; font-size: 30rpx; } /style

相关新闻