uni-app Vue3+TS 微信小程序扫码核销功能实现
·
uni-app Vue3+TS 微信小程序扫码核销功能实现
基于uniapp camera组件实现微信小程序扫码页面,包含相机权限校验、动态扫描动画、扫码防抖、异常错误处理
1. 实现逻辑
1.1 权限管控逻辑
- 页面进入自动检测手机相机权限,区分未申请、申请中、已授权、已拒绝四种状态
- 首次进入自动唤起相机权限申请,用户拒绝后弹窗引导跳转系统设置开启权限
- 权限状态动态切换页面UI,无权限隐藏相机组件展示权限提示
1.2 页面布局逻辑
- 全屏黑色背景铺满页面,扫码相机区域居中固定定位
- 自定义扫码框四角高亮边框,替代原生简陋扫码框
- 上层叠加遮罩层级,保证扫描动画、提示文字置顶显示
- 底部放置扫码引导文案,提升用户使用体验
1.3 扫描动画逻辑
- 使用定时器实现扫描线上下往复滑动效果
- 动态计算扫描线透明度,上下两端渐隐、中间高亮
- 页面显示启动动画,页面隐藏/销毁清空定时器,杜绝内存泄漏
1.4 扫码业务逻辑
- 增加扫码防抖锁,防止短时间重复多次触发扫码事件
- 扫码成功触发手机短震动反馈,弹出成功提示并跳转业务页面
- 扫码识别失败统一错误提示,设置冷却时间自动重置扫码状态
- 相机启动失败自动重新校验权限,完善异常兜底处理
1.5 生命周期优化
- onShow:页面显示重置扫码状态、重新校验相机权限
- onHide:页面隐藏停止扫描动画、清空延时定时器
- 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>
更多推荐

所有评论(0)