uniapp相机扫码和图片识别两种方式(适配鸿蒙系统)
扫码功能文档(含源码+常见权限问题)
效果
常见问题
概述
本文档介绍了uni-app应用中的扫码功能实现,包括相机扫码和图片识别两种方式,支持二维码和条形码的识别。
功能特性
1. 多种扫码方式
- 相机实时扫码:使用设备相机进行实时二维码/条形码扫描
- 图片识别:从相册选择图片进行二维码/条形码识别
2. 智能状态管理
- 扫描状态指示:动态显示当前扫描状态(等待、扫描中、权限被拒绝等)
- 动画效果控制:扫描线动画仅在扫描过程中显示
- 权限状态管理:智能处理相机权限授权状态
3. 跨平台支持
- APP-PLUS:完整功能支持
- H5:部分功能支持(相册选择暂不支持)
- 鸿蒙系统:特殊权限处理适配
技术实现
核心文件
- 文件路径:
pages/scan/index.vue
- 主要组件:Vue单文件组件
关键数据字段
data() { return { scanResult: \'\', // 扫描结果 isScanning: false, // 是否正在扫描 scanStatus: \'waiting\', // 扫描状态:waiting/scanning/permission_denied isInitialized: false, // 页面是否已初始化 permissionDenied: false // 权限是否被拒绝 }}
扫描状态说明
waiting
scanning
permission_denied
主要功能方法
1. 相机扫码 (startScan
)
startScan() { // 设置扫描状态 this.isScanning = true; this.scanStatus = \'scanning\'; // 调用uni.scanCode进行扫码 uni.scanCode({ scanType: [\'qrCode\', \'barCode\'], autoDecodeCharSet: true, success: (res) => { // 处理扫码成功 this.scanResult = res.result; this.handleScanResult(res.result); }, fail: (err) => { // 处理扫码失败 this.scanStatus = \'waiting\'; }, complete: () => { this.isScanning = false; } });}
2. 图片识别 (openAlbum
)
openAlbum() { // 选择图片 uni.chooseImage({ count: 1, sourceType: [\'album\'], success: (res) => { const imagePath = res.tempFilePaths[0]; // 双重识别机制 try { // 优先使用plus.barcode.scan plus.barcode.scan(imagePath, (type, result) => { if (result) { this.handleScanResult(result); } }); } catch (e) { // 降级使用uni.scanCode uni.scanCode({ filePath: imagePath, success: (scanRes) => { this.handleScanResult(scanRes.result); } }); } } });}
3. 权限检查 (checkCameraPermission
)
支持Android和鸿蒙系统的相机权限检查:
checkCameraPermission() { return new Promise((resolve, reject) => { if (this.isHarmonyOS()) { // 鸿蒙系统权限处理 plus.android.requestPermissions([\'ohos.permission.CAMERA\'], (result) => { resolve(result.granted.length > 0); }, (error) => { reject(error); } ); } else { // Android系统权限处理 plus.android.requestPermissions([\'android.permission.CAMERA\'], (result) => { resolve(result.granted.length > 0); }, (error) => { reject(error); } ); } });}
4. 结果处理 (handleScanResult
)
handleScanResult(result) { if (result.startsWith(\'http\')) { // URL类型结果处理 const idMatch = result.match(/[\\?&]id=([^&]*)/i); const id = idMatch ? idMatch[1] : null; if (id) { // 跳转到指定页面 uni.navigateTo({ url: `/pages/workbench/inspection/list?AS_ID=${id}` }); } } else { // 其他类型结果处理 uni.showToast({ title: `扫码结果:${result}`, icon: \'none\' }); }}
UI组件说明
扫描框架构
将二维码放入框内,即可自动扫描 相机权限被拒绝,请授权后重试 准备扫描中...
样式特性
- 扫描线动画:仅在
scan-line-active
状态下执行 - 状态指示颜色:
tips-waiting
:正常状态(蓝色)tips-error
:错误状态(红色)
- 响应式设计:适配不同屏幕尺寸
使用方法
1. 相机扫码
- 进入扫码页面
- 授权相机权限(首次使用)
- 将二维码/条形码对准扫描框
- 系统自动识别并处理结果
2. 图片识别
- 点击\"从相册选择\"按钮
- 选择包含二维码/条形码的图片
- 系统自动识别图片中的码
- 显示识别结果
常见问题
1、打包时未添加barcode模块,请参考https://ask.dcloud.net.cn/article/283
解决方案:在manifest.json中添加配置
\"app-plus\" : { \"usingComponents\" : true, \"compilerVersion\" : 3, \"modules\" : { \"Push\" : {}, \"Geolocation\" : {}, \"Maps\" : {}, \"Camera\" : {}, \"Barcode\" : {} },
全部代码
<template><view class=\"scan-container\"><!-- 扫一扫 --><view class=\"scan-content\"><view class=\"scan-area\"><view class=\"scan-frame\"><view class=\"scan-corner scan-corner-tl\"></view><view class=\"scan-corner scan-corner-tr\"></view><view class=\"scan-corner scan-corner-bl\"></view><view class=\"scan-corner scan-corner-br\"></view><view class=\"scan-line\" :class=\"{ \'scan-line-active\': scanStatus === \'scanning\' }\"></view></view></view><view class=\"scan-tips\"><text class=\"tips-text\" v-if=\"scanStatus === \'scanning\'\">将二维码放入框内,即可自动扫描</text><text class=\"tips-text tips-waiting\" v-else-if=\"scanStatus === \'waiting\'\">点击\"重新扫描\"开始扫描</text><text class=\"tips-text tips-error\" v-else-if=\"scanStatus === \'permission_denied\'\">相机权限被拒绝,请授权后重试</text><text class=\"tips-text tips-waiting\" v-else>准备扫描中...</text></view><view class=\"scan-actions\"><button class=\"scan-btn scan-btn-half\" @click=\"retryPermissionAndScan\">重新扫描</button><button class=\"scan-btn scan-btn-half scan-btn-secondary\" @click=\"openAlbum\">从相册选择</button></view></view><!-- 扫描结果:{{ scanResult }} --></view></template><script>export default {data() {return {scanResult: \'\',isShowCheckPermissionDialog: false,permissionChecked: false, // 权限是否已检查过permissionGranted: false, // 权限是否已授予pageInitialized: false, // 页面是否已初始化isScanning: false, // 是否正在扫描scanStatus: \'waiting\', // 扫描状态:waiting(等待), scanning(扫描中), permission_denied(权限被拒绝)}},onLoad() {// 页面加载时自动开始扫描// this.startScan();},onHide() {console.log(\'页面隐藏了\');// 页面隐藏时重置初始化状态,确保下次显示时能重新初始化this.pageInitialized = false;},onShow() {console.log(\'页面显示了,pageInitialized:\', this.pageInitialized);// 只在页面首次显示时执行权限检查if (!this.pageInitialized) {this.pageInitialized = true;console.log(\'页面首次初始化,开始权限检查\');this.scanStatus = \'waiting\'; // 设置初始状态this.checkPermissionsAndScan();} else {console.log(\'页面已初始化,跳过自动权限检查\');// 如果权限已授予,可以直接开始扫描if (this.permissionChecked && this.permissionGranted) {console.log(\'权限已授予,直接开始扫描\');this.startScan();} else if (this.permissionChecked && !this.permissionGranted) {// 权限被拒绝,显示相应状态this.scanStatus = \'permission_denied\';}}},methods: {// 重置权限状态resetPermissionStatus() {this.permissionChecked = false;this.permissionGranted = false;this.pageInitialized = false;this.isScanning = false;this.scanStatus = \'waiting\';console.log(\'权限状态已重置\');},// 重试权限检查并扫描retryPermissionAndScan() {console.log(\'用户点击重新扫描\');this.resetPermissionStatus();this.checkPermissionsAndScan();},// 检查权限并开始扫描checkPermissionsAndScan() {// 防止重复检查if (this.isShowCheckPermissionDialog) {return;}// 如果权限已经检查过且已授予,直接开始扫描if (this.permissionChecked && this.permissionGranted) {this.startScan();return;}// 如果权限已经检查过但被拒绝,不再重复弹窗if (this.permissionChecked && !this.permissionGranted) {console.log(\'权限已被拒绝,不再重复检查\');return;}this.isShowCheckPermissionDialog = true;// #ifdef APP-PLUSthis.checkCameraPermission().then(() => {this.permissionChecked = true;this.permissionGranted = true;this.startScan();}).catch(() => {this.permissionChecked = true;this.permissionGranted = false;this.scanStatus = \'permission_denied\';this.showPermissionDeniedAlert();}).finally(() => {this.isShowCheckPermissionDialog = false;});// #endif// #ifdef H5this.permissionChecked = true;this.permissionGranted = true;this.startScan();this.isShowCheckPermissionDialog = false;// #endif},// 检测是否为鸿蒙系统isHarmonyOS() {try {// #ifdef APP-PLUS-ANDROIDconst main = plus.android.runtimeMainActivity();const Build = plus.android.importClass(\'android.os.Build\');// 检查系统属性const brand = Build.BRAND;const manufacturer = Build.MANUFACTURER;const model = Build.MODEL;console.log(\'设备信息 - Brand:\', brand, \'Manufacturer:\', manufacturer, \'Model:\', model);// 检查是否包含华为/荣耀相关标识const isHuawei = brand && (brand.toLowerCase().includes(\'huawei\') || brand.toLowerCase().includes(\'honor\'));const isHuaweiManufacturer = manufacturer && (manufacturer.toLowerCase().includes(\'huawei\') || manufacturer.toLowerCase().includes(\'honor\'));// 尝试检测鸿蒙系统特有的API或属性try {const SystemProperties = plus.android.importClass(\'android.os.SystemProperties\');const harmonyVersion = SystemProperties.get(\'hw_sc.build.platform.version\', \'\');const isHarmony = harmonyVersion && harmonyVersion.length > 0;console.log(\'鸿蒙版本信息:\', harmonyVersion, \'是否为鸿蒙:\', isHarmony);return isHarmony || isHuawei || isHuaweiManufacturer;} catch (e) {console.log(\'无法获取鸿蒙版本信息,使用品牌判断:\', isHuawei || isHuaweiManufacturer);return isHuawei || isHuaweiManufacturer;}// #endifreturn false;} catch (error) {console.error(\'检测鸿蒙系统失败:\', error);return false;}},// 检查相机权限checkCameraPermission() {return new Promise((resolve, reject) => {// #ifdef APP-PLUS// 运行时判断平台类型const platform = uni.getSystemInfoSync().platform;console.log(\'当前平台:\', platform);if (platform === \'android\') {// Android 和 鸿蒙系统处理try {const main = plus.android.runtimeMainActivity();const Context = plus.android.importClass(\'android.content.Context\');const PackageManager = plus.android.importClass(\'android.content.pm.PackageManager\');const permission = \'android.permission.CAMERA\';// 检测是否为鸿蒙系统const isHarmonyOS = this.isHarmonyOS();console.log(\'是否为鸿蒙系统:\', isHarmonyOS);const result = main.checkSelfPermission(permission);console.log(\'Android/鸿蒙权限检查结果:\', result);if (result === PackageManager.PERMISSION_GRANTED) {resolve();} else {// 请求权限plus.android.requestPermissions([permission], (resultObj) => {const granted = resultObj.granted && resultObj.granted.length > 0;if (granted) {resolve();} else {reject();}}, (error) => {console.error(\'Android/鸿蒙权限请求失败:\', error);reject();});}} catch (error) {console.error(\'Android/鸿蒙权限检查失败:\', error);reject();}} else if (platform === \'ios\') {// iOS 系统处理const AVCaptureDevice = plus.ios.importClass(\'AVCaptureDevice\');const AVMediaTypeVideo = \'vide\';try {const authStatus = AVCaptureDevice.authorizationStatusForMediaType(AVMediaTypeVideo);if (authStatus === 3) { // AVAuthorizationStatusAuthorizedresolve();} else if (authStatus === 0) { // AVAuthorizationStatusNotDetermined// 请求权限AVCaptureDevice.requestAccessForMediaTypeCompletionHandler(AVMediaTypeVideo, (granted) => {if (granted) {resolve();} else {reject();}});} else {// 权限被拒绝或受限reject();}} catch (error) {console.error(\'iOS权限检查失败:\', error);reject();}} else {console.log(\'未知平台:\', platform);reject();}// #endif// #ifdef H5resolve();// #endif});},// 显示权限被拒绝的提示showPermissionDeniedAlert() {// #ifdef APP-PLUSconst platform = uni.getSystemInfoSync().platform;if (platform === \'android\') {// Android 和 鸿蒙系统处理const isHarmonyOS = this.isHarmonyOS();const systemName = isHarmonyOS ? \'鸿蒙\' : \'Android\';uni.showModal({title: \'相机权限\',content: `扫码功能需要相机权限,请在${systemName}系统设置中开启相机权限`,confirmText: \'去设置\',cancelText: \'稍后再说\',success: (res) => {if (res.confirm) {this.openAppSettings();}}});} else if (platform === \'ios\') {// iOS 系统处理uni.showModal({title: \'相机权限\',content: \'扫码功能需要相机权限,请在\"设置 > 隐私与安全性 > 相机\"中开启权限\',confirmText: \'去设置\',cancelText: \'取消\',success: (res) => {if (res.confirm) {this.openAppSettings();}}});}// #endif},// 打开应用设置openAppSettings() {// #ifdef APP-PLUSconst platform = uni.getSystemInfoSync().platform;if (platform === \'android\') {// Android 和 鸿蒙系统处理const Intent = plus.android.importClass(\'android.content.Intent\');const Settings = plus.android.importClass(\'android.provider.Settings\');const Uri = plus.android.importClass(\'android.net.Uri\');const main = plus.android.runtimeMainActivity();const intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);const uri = Uri.fromParts(\'package\', main.getPackageName(), null);intent.setData(uri);main.startActivity(intent);} else if (platform === \'ios\') {// iOS 系统处理const UIApplication = plus.ios.importClass(\'UIApplication\');const NSURL = plus.ios.importClass(\'NSURL\');const settingsUrl = NSURL.URLWithString(\'app-settings:\');const application = UIApplication.sharedApplication();application.openURL(settingsUrl);}// #endif},startScan() {console.log(\'开始扫描\');this.isScanning = true;this.scanStatus = \'scanning\';// #ifdef APP-PLUSuni.scanCode({success: (res) => {console.log(\'扫码成功:\', res.result);this.isScanning = false; uni.showToast({title: `扫码成功`,icon: \'success\'});this.scanResult = res.result;this.handleScanResult(res.result);},fail: (err) => {console.log(\'扫码失败:\', err);this.isScanning = false;this.scanStatus = \'waiting\';uni.showToast({title: \'扫码失败\',icon: \'none\'});}});// #endif// #ifdef H5this.isScanning = false;this.scanStatus = \'waiting\';uni.showToast({title: \'H5环境暂不支持扫码功能\',icon: \'none\'});// #endif},openAlbum() {console.log(\'从相册选择图片进行扫码\');// #ifdef APP-PLUSuni.chooseImage({count: 1,sourceType: [\'album\'],success: (res) => {const imagePath = res.tempFilePaths[0];console.log(\'选择的图片路径:\', imagePath);// 使用plus.barcode识别图片中的二维码// #ifdef APP-PLUStry {plus.barcode.scan(imagePath, (type, result, file) => {console.log(\'图片扫码成功 - 类型:\', type, \'结果:\', result);if (result) {uni.showToast({title: `扫码成功`,icon: \'success\'});this.scanResult = result;this.handleScanResult(result);} else {uni.showToast({title: \'未识别到二维码\',icon: \'none\'});}}, (error) => {console.log(\'图片扫码失败:\', error);uni.showToast({title: \'未识别到二维码\',icon: \'none\'});});} catch (e) {console.log(\'扫码API调用失败:\', e);// 降级使用uni.scanCodeuni.scanCode({scanType: [\'qrCode\', \'barCode\'],autoDecodeCharSet: true,filePath: imagePath,success: (scanRes) => {console.log(\'图片扫码成功:\', scanRes.result);uni.showToast({title: `扫码成功`,icon: \'success\'});this.scanResult = scanRes.result;this.handleScanResult(scanRes.result);},fail: (scanErr) => {console.log(\'图片扫码失败:\', scanErr);uni.showToast({title: \'未识别到二维码\',icon: \'none\'});}});}// #endif},fail: (err) => {console.log(\'选择图片失败:\', err);uni.showToast({title: \'选择图片失败\',icon: \'none\'});}});// #endif// #ifdef H5uni.showToast({title: \'H5环境暂不支持此功能\',icon: \'none\'});// #endif},handleScanResult(result) {// 处理扫描结果if (result.startsWith(\'http\')) {console.log(\'开始处理URL:\', result);try {// 使用正则表达式提取URL参数const idMatch = result.match(/[\\?&]id=([^&]*)/i);const id = idMatch ? idMatch[1] : null;console.log(\'解析到的ID:\', id);// 检查是否成功提取到IDif (!id) {throw new Error(\'未找到ID参数\');}console.log(\'准备跳转到:\', `/pages/workbench/inspection/list?AS_ID=${id}`);// 添加延迟确保页面准备就绪setTimeout(() => {uni.navigateTo({url: `/pages/workbench/inspection/list?AS_ID=${id}`,success: (res) => {console.log(\'跳转成功:\', res);},fail: (err) => {console.error(\'跳转失败:\', err);uni.showToast({title: \'页面跳转失败\',icon: \'none\'});// 尝试使用reLaunch作为备选方案uni.reLaunch({url: `/pages/workbench/inspection/list?AS_ID=${id}`,success: (res) => {console.log(\'reLaunch成功:\', res);},fail: (err) => {console.error(\'reLaunch也失败:\', err);}});}});}, 100);} catch (error) {console.error(\'URL解析错误:\', error);uni.showToast({title: \'无法识别\',icon: \'none\'});}} else {uni.showModal({title: \'扫描结果\',content: result,showCancel: false});}}}}</script><style lang=\"scss\" scoped>.scan-container {min-height: 100vh;background-color: #121212;display: flex;flex-direction: column;}.scan-header {padding: 30rpx 20rpx;text-align: center;background-color: rgba(18, 18, 18, 0.95);border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);}.scan-title {color: #fff;font-size: 36rpx;font-weight: bold;letter-spacing: 2rpx;}.scan-content {flex: 1;display: flex;flex-direction: column;align-items: center;justify-content: center;padding: 40rpx;}.scan-area {position: relative;width: 550rpx;height: 550rpx;margin-bottom: 30rpx;margin-top: 20rpx;}.scan-frame {position: relative;width: 100%;height: 100%;border: 2rpx solid rgba(255, 255, 255, 0.2);box-shadow: 0 0 0 4000rpx rgba(0, 0, 0, 0.5);backdrop-filter: blur(5px);}.scan-corner {position: absolute;width: 60rpx;height: 60rpx;border: 4rpx solid #1677FF;box-shadow: 0 0 10rpx rgba(22, 119, 255, 0.5);}.scan-corner-tl {top: -2rpx;left: -2rpx;border-right: none;border-bottom: none;}.scan-corner-tr {top: -2rpx;right: -2rpx;border-left: none;border-bottom: none;}.scan-corner-bl {bottom: -2rpx;left: -2rpx;border-right: none;border-top: none;}.scan-corner-br {bottom: -2rpx;right: -2rpx;border-left: none;border-top: none;}.scan-line {position: absolute;top: 0;left: 0;width: 100%;height: 4rpx;background: linear-gradient(90deg, transparent, #1677FF, transparent);box-shadow: 0 0 8rpx rgba(22, 119, 255, 0.8);opacity: 0.3;}.scan-line-active {animation: scanMove 2.5s ease-in-out infinite;opacity: 1;}@keyframes scanMove {0% {top: 0;opacity: 0.6;}50% {opacity: 1;}100% {top: calc(100% - 4rpx);opacity: 0.6;}}.scan-tips {margin-bottom: 80rpx;margin-top: 40rpx;background-color: rgba(0, 0, 0, 0.3);padding: 16rpx 30rpx;border-radius: 30rpx;}.tips-text {color: #fff;font-size: 28rpx;text-align: center;letter-spacing: 1rpx;}.tips-waiting {color: #ffa500;}.tips-error {color: #ff6b6b;}.scan-actions {display: flex;flex-direction: row;justify-content: space-between;gap: 20rpx;width: 100%;padding: 0 30rpx;}.scan-btn {height: 90rpx;background-color: #1677FF;color: #fff;border: none;border-radius: 45rpx;font-size: 32rpx;font-weight: bold;line-height: 90rpx;padding: 0;box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);transition: all 0.3s ease;}.scan-btn:active {transform: scale(0.98);box-shadow: 0 2rpx 6rpx rgba(0, 0, 0, 0.2);}.scan-btn-half {width: 45%;display: flex;align-items: center;justify-content: center;}.scan-btn-secondary {background-color: rgba(255, 255, 255, 0.15);border: 1rpx solid rgba(255, 255, 255, 0.3);color: #fff;}.scan-result {position: fixed;bottom: 0;left: 0;right: 0;background-color: rgba(0, 0, 0, 0.85);padding: 40rpx;border-top-left-radius: 30rpx;border-top-right-radius: 30rpx;box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.3);animation: slideUp 0.3s ease-out;}@keyframes slideUp {from {transform: translateY(100%);}to {transform: translateY(0);}}.result-title {color: #fff;font-size: 30rpx;font-weight: bold;margin-bottom: 20rpx;display: block;}.result-content {color: #e0e0e0;font-size: 26rpx;word-break: break-all;display: block;line-height: 1.5;}</style>
错误处理
权限相关
- 权限被拒绝:显示授权提示,提供设置跳转
- 权限检查失败:提供重试机制
扫码相关
- 识别失败:显示\"未识别到二维码\"提示
- API调用失败:自动降级到备用方案
平台兼容
- H5环境:部分功能不支持时显示相应提示
- 不同Android版本:适配不同的权限API
注意事项
- 权限管理:首次使用需要用户授权相机权限
- 平台差异:H5环境功能受限,建议在APP中使用
- 图片格式:支持常见图片格式的二维码识别
- 网络要求:扫码结果处理可能需要网络连接
- 性能优化:大图片识别可能耗时较长
更新日志
v1.0.0
- 基础扫码功能实现
- 支持相机扫码和图片识别
- 权限管理和状态控制
- 跨平台兼容性处理
本文档基于uni-app框架编写,适用于移动端应用开发。