Uniapp 使用 plus.barcode.create 实现自定义扫码功能
引言
- 简述 Uniapp 跨平台开发的优势及扫码功能的常见场景
- 介绍
plus.barcode.create的作用及适用平台(通常为 5+ App 或 Web 环境)
https://www.html5plus.org/doc/zh_cn/barcode.html
环境准备
- 确保项目基于 HBuilderX 开发并运行在 5+ Runtime 环境
- 检查
manifest.json中是否配置了扫码模块权限(如Barcode)
核心 API 解析
plus.barcode.create参数说明:id:扫码控件容器的 DOM 元素 IDfilters:支持的条码类型(如 QR、EAN13 等)styles:自定义扫码界面样式(位置、颜色等)
实现步骤
创建扫码控件
- 在页面中预留容器(如
<view id="barcode"></view>) - 通过
plus.barcode.create初始化扫码实例并绑定容器
配置扫码参数
- 设置条码类型:
barcodeInstance.setSupportedFormats(['qr']) - 自定义界面样式:通过
styles调整扫描框大小、边框颜色等
监听扫码事件
- 注册成功回调:
barcodeInstance.onmarked = function(res) { ... } - 错误处理:捕获扫描失败或权限异常情况
高级功能扩展
- 动态切换扫码类型(如从二维码切换到条形码)
- 结合相机权限管理,处理用户拒绝授权场景
- 实现连续扫描或批量扫描逻辑
性能优化与兼容性
- 避免频繁创建/销毁扫码实例
- 处理 Android/iOS 平台的差异(如权限申请流程)
- 真机调试时的常见问题排查(如黑屏、无响应)
示例代码片段
// 初始化扫码
const barcode = plus.barcode.create('barcode', 'qr', {
frameColor: '#00FF00',
scanbarColor: '#0000FF'
});
barcode.onmarked = function(res) {
console.log('扫码结果:', res.result);
};
barcode.start();
示例代码
<template>
<view class="barcode-scanner-container" v-show="isShow">
<!-- 原生扫码容器(必须是块级元素,且有明确宽高) -->
<view
class="scan-native-container"
ref="scanContainerRef"
id="scanNativeContainer"
></view>
<!-- 自定义扫码遮罩层(仅视觉展示,不影响原生扫码) -->
<view class="scan-mask">
<view class="scan-frame" ref="scanFrameRef">
<view class="scan-line" v-if="isScanning"></view>
<view class="corner top-left"></view>
<view class="corner top-right"></view>
<view class="corner bottom-left"></view>
<view class="corner bottom-right"></view>
</view>
<text class="scan-tips">{{ scanTips }}</text>
</view>
<view class="close-btn" @click="closeScanner">×</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
// 响应式数据
const isShow = ref(false);
const isScanning = ref(false);
const scanFrameRef = ref(null);
const scanContainerRef = ref(null); // 原生扫码容器Ref
// 扫码对象
let barcodeScanner = null;
let currentWebview = null; // 当前页面的原生Webview
let uniPlus = null;
// Props
const props = defineProps({
show: { type: Boolean, default: false },
scanTips: { type: String, default: '请将条码/二维码对准扫描框' },
scanTypes: {
type: Array,
default: () => ['QR_CODE', 'EAN_13', 'CODE_128']
}
});
const emit = defineEmits(['scanSuccess', 'scanFail', 'close']);
/**
* 转换码型为 plus.barcode 常量
*/
const convertScanTypes = (typeNames) => {
if (!uniPlus || !uniPlus.barcode) return [uniPlus.barcode.QR, uniPlus.barcode.EAN13, uniPlus.barcode.CODE128];
const typeMap = {
QR_CODE: uniPlus.barcode.QR,
EAN_13: uniPlus.barcode.EAN13,
EAN_8: uniPlus.barcode.EAN8,
CODE_128: uniPlus.barcode.CODE128,
CODE_39: uniPlus.barcode.CODE39,
ITF: uniPlus.barcode.ITF
};
return typeNames.map(name => typeMap[name] || uniPlus.barcode.QR);
};
/**
* 检查并申请相机权限(完整版本)
*/
const requestCameraPermission = () => {
return new Promise((resolve, reject) => {
if (!uniPlus) {
reject(new Error('仅支持App端使用'));
return;
}
// Android 动态权限申请
if (uniPlus.os.name === 'Android') {
const main = uniPlus.android.runtimeMainActivity();
const Permission = uniPlus.android.importClass('android.Manifest');
const ActivityCompat = uniPlus.android.importClass('androidx.core.app.ActivityCompat');
// 检查权限
if (ActivityCompat.checkSelfPermission(main, Permission.CAMERA) === 0) {
resolve(true);
return;
}
// 申请权限
ActivityCompat.requestPermissions(main, [Permission.CAMERA], 1001);
uniPlus.android.requestPermissions(
['android.permission.CAMERA'],
(res) => {
const granted = res.granted || [];
if (granted.includes('android.permission.CAMERA')) {
resolve(true);
} else {
// 引导用户去设置开启权限
uni.showModal({
title: '权限不足',
content: '请前往设置开启相机权限,否则无法使用扫码功能',
showCancel: false,
confirmText: '知道了'
});
reject(new Error('相机权限被拒绝'));
}
},
(e) => reject(new Error(`权限申请失败: ${e.message || JSON.stringify(e)}`))
);
} else if (uniPlus.os.name === 'iOS') {
// iOS 检查权限(需在info.plist配置 NSCameraUsageDescription)
const authStatus = uniPlus.barcode.checkPermission();
if (authStatus === 0) { // 0=允许,1=拒绝,2=未决定
resolve(true);
} else {
uni.showModal({
title: '权限不足',
content: '请前往设置-隐私-相机,允许本App访问相机',
showCancel: false
});
reject(new Error('iOS相机权限被拒绝'));
}
} else {
reject(new Error('不支持的操作系统'));
}
});
};
/**
* 初始化扫码器(核心修复)
*/
const initScanner = async () => {
try {
// 仅App端执行
// #ifdef APP-PLUS
if (typeof plus === 'undefined') {
throw new Error('当前环境不支持原生扫码(仅支持App端)');
}
uniPlus = plus;
// 获取当前页面的原生Webview
const pages = getCurrentPages();
const page = pages[pages.length - 1];
currentWebview = page.$getAppWebview();
// #endif
if (!uniPlus || !currentWebview) {
throw new Error('无法获取原生Webview实例');
}
// 1. 等待DOM完全渲染
await nextTick();
if (!scanContainerRef.value) {
throw new Error('扫码容器DOM未渲染');
}
// 2. 申请相机权限
await requestCameraPermission();
// 3. 获取扫码容器的位置和尺寸(关键!)
const rect = uni.createSelectorQuery().in(page).select('#scanNativeContainer').boundingClientRect();
const domRect = await new Promise(resolve => rect.exec(resolve));
if (!domRect[0]) {
throw new Error('无法获取扫码容器尺寸');
}
const { width, height, left, top } = domRect[0];
// 4. 创建扫码实例(指定位置和样式)
const scanTypes = convertScanTypes(props.scanTypes);
barcodeScanner = uniPlus.barcode.create('scanNativeContainer', scanTypes, {
top: `${top}px`,
left: `${left}px`,
width: `${width}px`,
height: `${height}px`,
scanbarColor: '#00FF00', // 扫描线颜色
frameColor: '#00FF00', // 扫码框颜色
position: 'static' // 绝对定位
});
if (!barcodeScanner) {
throw new Error('创建扫码实例失败');
}
// 5. 绑定事件
barcodeScanner.onmarked = onScanMarked;
barcodeScanner.onerror = onScanError;
// 6. 将扫码实例添加到当前Webview
currentWebview.append(barcodeScanner);
let view = new plus.nativeObj.View('nbutton', {
bottom: '28%',
left: '30%',
width: '40%',
height: '44px'
}, [{
tag: 'font',
id: 'text',
text: '请扫描二维码',
textStyles: {
color: '#FFFFFF'
}
}]);
currentWebview.append(view);
// 7. 启动扫码 filename:"lnnpath",
barcodeScanner.start({
conserve: true, // 不保存截图
vibrate: true, // 扫码成功震动
sound: 'none' // 关闭提示音
});
isScanning.value = true;
console.log('扫码器启动成功,相机已打开');
} catch (error) {
console.error('初始化扫码器失败:', error);
emit('scanFail', { message: error.message || '扫码器初始化失败' });
closeScanner();
}
};
/**
* 扫码成功回调
*/
const onScanMarked = (type, result,file) => {
console.log('扫码成功:', type, result,file);
let a = plus.io.convertLocalFileSystemURL( file )
console.log(a,"path")
// 震动反馈
if (uniPlus && uniPlus.device?.vibrate) {
uniPlus.device.vibrate(100);
}
// 获取类型名称
const typeName = getTypeName(type);
// 停止扫描
// stopScanning();
// 延迟返回结果
setTimeout(() => {
emit('scanSuccess', {
type: typeName,
result: result
});
}, 300);
};
/**
* 扫码错误回调
*/
const onScanError = (err) => {
console.error('扫码异常:', err);
const errorMsg = err.message || JSON.stringify(err);
emit('scanFail', { message: `扫码异常:${errorMsg}` });
if (errorMsg.includes('permission') || errorMsg.includes('权限')) {
closeScanner();
}
};
/**
* 转换码型常量为名称
*/
const getTypeName = (typeCode) => {
if (!uniPlus) return 'UNKNOWN';
const codeMap = {
[uniPlus.barcode.QR]: 'QR_CODE',
[uniPlus.barcode.EAN13]: 'EAN_13',
[uniPlus.barcode.EAN8]: 'EAN_8',
[uniPlus.barcode.CODE128]: 'CODE_128',
[uniPlus.barcode.CODE39]: 'CODE_39',
[uniPlus.barcode.ITF]: 'ITF'
};
return codeMap[typeCode] || 'UNKNOWN';
};
/**
* 停止扫描
*/
const stopScanning = () => {
if (barcodeScanner && isScanning.value) {
try {
barcodeScanner.cancel();
isScanning.value = false;
} catch (e) {
console.warn('停止扫描时出错:', e);
}
}
};
/**
* 关闭扫码器
*/
const closeScanner = () => {
console.log('关闭扫码器');
// 停止扫描
stopScanning();
// 销毁扫码实例
if (barcodeScanner) {
try {
barcodeScanner.close();
currentWebview?.remove(barcodeScanner); // 从Webview移除
} catch (e) {
console.warn('关闭扫码器时出错:', e);
}
barcodeScanner = null;
}
isShow.value = false;
emit('close');
};
/**
* 销毁扫码实例
*/
const destroyScanner = () => {
closeScanner();
};
// 监听 props.show(修复异步渲染问题)
watch(
() => props.show,
async (newVal) => {
isShow.value = newVal;
if (newVal) {
// 确保DOM完全渲染后初始化
await nextTick();
initScanner();
} else {
destroyScanner();
}
},
{ immediate: true }
);
// 组件卸载时清理
onUnmounted(() => {
destroyScanner();
currentWebview = null;
});
</script>
<style scoped>
.barcode-scanner-container {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.85);
z-index: 9999;
}
/* 原生扫码容器(必须占满全屏,否则相机画面被裁剪) */
.scan-native-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1; /* 低于遮罩层,仅渲染相机画面 */
}
.scan-mask {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: absolute;
background: red;
z-index: 23333; /* 遮罩层在相机画面上方 */
pointer-events: none; /* 不拦截点击事件 */
}
.scan-frame {
width: 65vw;
height: 65vw;
max-width: 300px;
max-height: 300px;
position: relative;
border: none;
}
/* 自定义扫码框角标 */
.corner {
position: absolute;
width: 20px;
height: 20px;
border-color: #00FF00;
}
.corner.top-left {
top: 0;
left: 0;
border-left: 3px solid;
border-top: 3px solid;
}
.corner.top-right {
top: 0;
right: 0;
border-right: 3px solid;
border-top: 3px solid;
}
.corner.bottom-left {
bottom: 0;
left: 0;
border-left: 3px solid;
border-bottom: 3px solid;
}
.corner.bottom-right {
bottom: 0;
right: 0;
border-right: 3px solid;
border-bottom: 3px solid;
}
.scan-line {
position: absolute;
top: 0;
left: 10%;
width: 80%;
height: 2px;
background: linear-gradient(to right, transparent, #00FF00, transparent);
box-shadow: 0 0 10px #00FF00;
animation: scanMove 2s linear infinite;
}
@keyframes scanMove {
0% {
top: 0;
opacity: 0;
}
5% {
opacity: 1;
}
95% {
opacity: 1;
}
100% {
top: 100%;
opacity: 0;
}
}
.scan-tips {
color: #fff;
font-size: 14px;
margin-top: 40px;
text-align: center;
padding: 10px 20px;
background: rgba(0, 0, 0, 0.5);
border-radius: 20px;
z-index: 3;
pointer-events: none;
}
.close-btn {
position: absolute;
top: 40px;
right: 20px;
width: 44px;
height: 44px;
background: rgba(0, 0, 0, 0.7);
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
font-size: 28px;
font-weight: 300;
border: 1px solid rgba(255, 255, 255, 0.2);
pointer-events: auto; /* 允许点击 */
}
.close-btn:active {
background: rgba(0, 0, 0, 0.9);
}
</style>
8603

被折叠的 条评论
为什么被折叠?



