news 2026/5/19 2:05:09

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
uni-app Vue3+TS 微信小程序扫码核销功能实现

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

基于uniapp camera组件实现微信小程序扫码页面,包含相机权限校验、动态扫描动画、扫码防抖、异常错误处理

1. 实现逻辑

1.1 权限管控逻辑

  1. 页面进入自动检测手机相机权限,区分未申请、申请中、已授权、已拒绝四种状态
  2. 首次进入自动唤起相机权限申请,用户拒绝后弹窗引导跳转系统设置开启权限
  3. 权限状态动态切换页面UI,无权限隐藏相机组件展示权限提示

1.2 页面布局逻辑

  1. 全屏黑色背景铺满页面,扫码相机区域居中固定定位
  2. 自定义扫码框四角高亮边框,替代原生简陋扫码框
  3. 上层叠加遮罩层级,保证扫描动画、提示文字置顶显示
  4. 底部放置扫码引导文案,提升用户使用体验

1.3 扫描动画逻辑

  1. 使用定时器实现扫描线上下往复滑动效果
  2. 动态计算扫描线透明度,上下两端渐隐、中间高亮
  3. 页面显示启动动画,页面隐藏/销毁清空定时器,杜绝内存泄漏

1.4 扫码业务逻辑

  1. 增加扫码防抖锁,防止短时间重复多次触发扫码事件
  2. 扫码成功触发手机短震动反馈,弹出成功提示并跳转业务页面
  3. 扫码识别失败统一错误提示,设置冷却时间自动重置扫码状态
  4. 相机启动失败自动重新校验权限,完善异常兜底处理

1.5 生命周期优化

  1. onShow:页面显示重置扫码状态、重新校验相机权限
  2. onHide:页面隐藏停止扫描动画、清空延时定时器
  3. onUnmounted:页面彻底销毁清除所有定时器,优化性能

2. 具体代码

2.1 页面完整代码

<template> <view class="content"> <!-- 相机组件 --> <camera v-if="!showPopup && hasCameraPermission" mode="scanCode" class="camera" device-position="back" flash="auto" @scancode="onScanCode" @error="onCameraError"> </camera> <!-- 相机权限被拒绝时的提示 --> <view v-if="!hasCameraPermission && !showPopup && permissionStatus === 'denied'" class="permission-denied"> <text class="permission-text">需要相机权限才能扫描二维码</text> <button class="permission-btn" @click="requestCameraPermission">申请相机权限</button> </view> <!-- 首次进入页面,正在申请权限的加载状态 --> <view v-if="!hasCameraPermission && !showPopup && permissionStatus === 'requesting'" class="permission-loading"> <text class="permission-text">正在申请相机权限...</text> </view> <!-- 遮罩层 UI - 修改为使用四个方向的遮罩块 --> <view v-if="hasCameraPermission" class="scan-overlay"> <!-- .中间扫描区域 (绝对定位居中) --> <view class="scan-area"> <!-- 四个角标 --> <view class="corner corner-tl"></view> <view class="corner corner-tr"></view> <view class="corner corner-bl"></view> <view class="corner corner-br"></view> <!-- 扫描线 - 使用 JS 控制动画 --> <view class="scan-line" :style="scanLineStyle"></view> </view> </view> <!-- 提示文本 --> <view v-if="hasCameraPermission" class="tip_text">将二维码/条形码放入框内,即可自动扫描</view> </view> </template> <script setup lang="ts"> import { ref, computed, onUnmounted } from 'vue'; import { onShow, onHide } from "@dcloudio/uni-app"; // 防抖标记 const isScanning = ref(false); // 用于存储冷却期定时器 const scanCooldownTimer = ref<any>(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 = ref<any>(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 lang="scss"> /* 确保页面全屏 */ .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>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/19 2:01:06

推荐靠谱多模型聚合平台生产厂家,技术扎实服务贴心有保障

随着AI大模型应用场景不断拓展&#xff0c;企业对多模型聚合平台的需求持续攀升。行业报告显示&#xff0c;近一年国内企业采购多模型聚合服务的订单量同比增长超60%&#xff0c;如何选择技术扎实、服务贴心的平台生产厂家&#xff0c;成为企业数字化转型的关键决策。一、技术实…

作者头像 李华
网站建设 2026/5/19 1:50:10

别再熬大夜改论文了!okbiye AI 毕业论文写作,帮你把终稿一次焊死

okbiye-免费查重复率aigc检测/开题报告/毕业论文/智能排版/文献综述/AI PPT毕业论文 - Okbiye智能写作https://www.okbiye.com/ai/bylw 论文季的深夜&#xff0c;你是不是也对着空白文档反复刷新浏览器&#xff1f;开题报告被导师打回三次、文献综述东拼西凑还是逻辑不通、终稿…

作者头像 李华
网站建设 2026/5/19 1:50:08

Mac上双开Codex

最近在用Codex 作为主力编程&#xff0c;只开了Plus。一个账号的额度不太够用&#xff0c;于是注册了两个账号。用完一个账号额度&#xff0c;再切换到另外一个账号继续同一个项目。一开始退出当前账号&#xff0c;再重新登录另外一个&#xff0c;总有点麻烦&#xff0c;还要邮…

作者头像 李华
网站建设 2026/5/19 1:47:49

【2026年5月】初级会计师考试真题试卷及答案PDF

2026年5月初级会计师考试真题和答案 考试科目与时间安排 《初级会计实务》 考试日期&#xff1a;2026年5月16日、17日、18日场次安排&#xff1a;上午场&#xff08;9:00-11:30&#xff09;、下午场&#xff08;14:30-17:00&#xff09; 《经济法基础》 考试日期&#xff1a;…

作者头像 李华
网站建设 2026/5/19 1:47:49

compare_exchange_weak 的用法

compare_exchange_weak 是 C 原子库中用于执行比较并交换&#xff08;CAS&#xff09;操作的函数&#xff0c;它原子地比较原子变量的当前值与期望值&#xff0c;如果相等则更新为目标值&#xff0c;否则更新期望值为当前值。bool compare_exchange_weak(T& expected, T de…

作者头像 李华