Uniapp 使用 `plus.barcode.create` 实现自定义扫码功能

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 元素 ID
    • filters:支持的条码类型(如 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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值