更新记录

1.0.0(2026-07-02)

  • 发布插件

平台兼容性

uni-app(5.14)

Vue2 Vue2插件版本 Vue3 Vue3插件版本 Chrome Safari app-vue app-nvue app-nvue插件版本 Android Android插件版本 iOS 鸿蒙
1.0.0 1.0.0 - - - 1.0.0 5.0 1.0.0 - -
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

功能特性

  • 支持二维码和条形码扫描
  • 支持 OCR 文本识别,自动从文本中提取手机号和身份证号
  • 支持闪光灯控制
  • 支持相机缩放(放大/缩小)
  • 支持自定义扫描线颜色、扫描框、提示文案
  • 支持扫描完成提示音控制

适用范围

适用于 uni-app 非 x 项目的 nvue 页面,支持 vue2vue3。若是uni-app x项目请使用 yuange-scanner插件

本插件需制作自定义基座后运行。

快速开始

1. 导入 NativeScanner

nvue 页面直接导入 UTS 原生模块,无需组件层:

import { NativeScanner } from "@/uni_modules/yuange-scanneruniapp";

2. 放置扫描占位区并测量位置

在模板中放一个 <view ref="scanArea">,扫描器 overlay 会精确覆盖此区域:

<template>
  <view class="container">
    <!-- 扫描占位区:相机预览 overlay 会按测量结果覆盖此区域 -->
    <view ref="scanArea" class="scan-area"></view>

    <!-- 控制按钮 -->
    <view class="controls">
      <button type="primary" size="mini" @tap="startScanner">启动扫描</button>
      <button type="warn" size="mini" @tap="stopScanner">停止扫描</button>
    </view>
  </view>
</template>

3. 测量占位区并初始化

onReady 触发时 flexbox 布局可能尚未完成,需延迟测量(flex:1 的 view 首帧尺寸为 0):

export default {
  data() {
    return {
      scannerWidth: 0,
      scannerHeight: 0,
      scannerLeft: 0,
      scannerTop: 0,
      nativeScanner: null,
      isInitialized: false
    };
  },

  onReady() {
    this.measureAndInit();
  },

  onUnload() {
    this.stopScanner();
    this.destroyScanner();
  },

  onHide() {
    this.stopScanner();
  },

  methods: {
    measureAndInit(retryCount) {
      const retry = retryCount == null ? 0 : retryCount;
      const dom = uni.requireNativePlugin('dom');
      const doMeasure = () => {
        if (dom && this.$refs.scanArea) {
          dom.getComponentRect(this.$refs.scanArea, (res) => {
            if (res && res.size && res.size.width > 0 && res.size.height > 0) {
              this.scannerWidth = Math.floor(res.size.width);
              this.scannerHeight = Math.floor(res.size.height);
              this.scannerLeft = Math.floor(res.size.left || 0);
              this.scannerTop = Math.floor(res.size.top || 0);
              this.initScanner();
            } else if (retry < 5) {
              // 布局未就绪,50ms 后重试
              setTimeout(() => this.measureAndInit(retry + 1), 50);
            } else {
              // 回退到屏幕宽 × 55% 居中
              this.fallbackSize();
              this.initScanner();
            }
          });
        } else {
          this.fallbackSize();
          this.initScanner();
        }
      };
      if (retry === 0) {
        setTimeout(doMeasure, 50); // 首次延迟一帧让布局跑完
      } else {
        doMeasure();
      }
    },

    fallbackSize() {
      const sys = uni.getSystemInfoSync();
      this.scannerWidth = sys.windowWidth;
      this.scannerHeight = Math.floor(sys.windowHeight * 0.55);
      this.scannerLeft = 0;
      this.scannerTop = Math.floor((sys.windowHeight - this.scannerHeight) / 2);
    },

    initScanner() {
      // 创建实例(参数为宽高,Double 类型)
      this.nativeScanner = new NativeScanner(this.scannerWidth, this.scannerHeight);

      // 设置 overlay 位置和尺寸(参数为 left, top, width, height)
      this.nativeScanner.setViewFrame(
        this.scannerLeft, this.scannerTop,
        this.scannerWidth, this.scannerHeight
      );

      // 绑定视图到 Activity
      this.nativeScanner.bindView();

      // 设置扫描视图样式
      this.nativeScanner.setScanViewOptions({
        scanFrameWidth: 0.7,
        scanFrameHeight: 0.5,
        frameBorderColor: '#FFFFFF',
        frameBorderWidth: 2,
        scanLineStyle: { color: '#00FF00', width: 2, height: 4 },
        hintText: '请将二维码/条形码置于框内',
        hintColor: '#FFFFFF',
        hintTextSize: 14,
        maskOpacity: 0.5,
        maskColor: '#000000'
      });

      // 设置扫描反馈
      this.nativeScanner.setScanFeedbackOptions({ playSound: true, autoStop: false });

      // 设置扫描结果回调
      this.setupScanResultCallback();

      this.isInitialized = true;
    }
  }
};

4. 设置扫描结果回调

setupScanResultCallback() {
  this.nativeScanner.setScanResultCallback({
    success: (res) => {
      // res 是 HashMap,用 res.get("key") 取值
      const rawValue = res.get("rawValue") || '';
      const format = res.get("format") || '';
      const textType = res.get("textType") || '';
      const phones = res.get("phoneNumbers");     // ArrayList<String>
      const idCards = res.get("idCardNumbers");   // ArrayList<String>

      console.log('内容:', rawValue);
      console.log('格式:', format);
      if (phones != null) {
        // ArrayList 用 size() + get(i) 遍历
        for (let i = 0; i < phones.size(); i++) {
          console.log('手机号:', phones.get(i));
        }
      }
    },
    fail: (err) => {
      console.log('错误:', err.get("msg"));
    }
  });
}

5. 启动/停止扫描

startScanner() {
  this.nativeScanner.start({
    cameraId: 0,            // 0=后摄
    torch: false,           // 是否开启闪光灯
    requestPermission: true // 是否请求相机权限
  }, {
    success: (res) => { console.log('相机启动成功'); },
    fail: (err) => { console.log('启动失败:', err.get("msg")); }
  });
}

stopScanner() {
  this.nativeScanner.stop();
}

destroyScanner() {
  if (this.nativeScanner) {
    this.nativeScanner.destroy();
    this.nativeScanner = null;
  }
}

API 列表

new NativeScanner(width, height)

创建扫描器实例。

参数 类型 说明
width Double 扫描器宽度(像素)
height Double 扫描器高度(像素)

setViewFrame(left, top, width, height)

设置 overlay 的位置和尺寸(相对 Activity contentView 的像素坐标)。可在 bindView 前或后调用,运行时动态调整也会立即生效。

bindView()

将扫描器原生视图绑定到 Activity。必须在 setViewFrame 之后调用。

start(options, callback)

启动相机和扫描。

nativeScanner.start({
  cameraId: 0,            // 摄像头 ID:0=后摄
  torch: false,           // 是否开启闪光灯,默认 false
  requestPermission: true // 是否申请相机权限,默认 true
}, {
  success: (res) => {},
  fail: (err) => { console.log(err.get("msg")); }
});

stop()

停止扫描,相机保持绑定。

destroy()

销毁扫描器并释放资源,从 Activity 移除 overlay 视图。

setTorch(on, callback)

nativeScanner.setTorch(true, {  // true=开启,false=关闭
  success: (res) => {},
  fail: (err) => {}
});

setZoom(zoom, callback)

// zoom: 1.0 ~ maxZoom(通过 getMaxZoom 获取)
nativeScanner.setZoom(2.0, {
  success: (res) => {},
  fail: (err) => {}
});

getMaxZoom(callback)

nativeScanner.getMaxZoom({
  success: (res) => { console.log("最大缩放:", res.get("maxZoom")); },
  fail: () => {}
});

setScanViewOptions(options)

更新扫描视图配置,运行时可随时调用。

nativeScanner.setScanViewOptions({
  scanFrameWidth: 0.7,          // 扫描框宽度比例(0.0~1.0),默认 0.7
  scanFrameHeight: 0.5,         // 扫描框高度比例(0.0~1.0),默认 0.5
  frameBorderColor: '#FFFFFF',  // 边框颜色,默认 '#FFFFFF'
  frameBorderWidth: 2,          // 边框宽度(像素),默认 2
  scanLineStyle: {
    color: '#00FF00',           // 扫描线颜色,默认 '#00FF00'
    width: 2,                   // 扫描线宽度(像素),默认 2
    height: 4                   // 扫描线高度(像素),默认 2
  },
  hintText: '请将二维码/条形码置于框内',  // 提示文案
  hintColor: '#FFFFFF',         // 提示文案颜色,默认 '#FFFFFF'
  hintTextSize: 14,             // 提示文案大小(sp),默认 14
  maskOpacity: 0.5,             // 背景遮罩透明度(0.0~1.0),0=完全透明,默认 0.5
  maskColor: '#000000'          // 背景遮罩颜色,默认 '#000000'
});

setScanFeedbackOptions(options)

nativeScanner.setScanFeedbackOptions({
  playSound: true,   // 扫描成功后是否播放提示音,默认 true
  autoStop: false    // 扫描成功后是否自动停止,默认 false
});

setScanResultCallback(callback)

设置扫描结果回调。扫描到条码或 OCR 文本时触发。

nativeScanner.setScanResultCallback({
  success: (res) => {
    // res 是 HashMap,用 res.get("key") 取值
    const rawValue = res.get("rawValue");        // 扫描内容
    const format = res.get("format");            // 条码格式(QR_CODE/EAN_13/TEXT 等)
    const textType = res.get("textType");        // OCR 文本类型(phone/idcard/text)
    const phoneNumbers = res.get("phoneNumbers");  // ArrayList<String>,提取的手机号
    const idCardNumbers = res.get("idCardNumbers"); // ArrayList<String>,提取的身份证号
  },
  fail: (err) => {
    const msg = err.get("msg");
  }
});

setBarcodeFormats(formats)

设置支持的条码格式(默认支持全部)。

nativeScanner.setBarcodeFormats(['QR_CODE', 'EAN_13', 'CODE_128']);

扫描结果数据结构

条码扫描结果

{
  code: 200,
  msg: "识别成功",
  rawValue: "扫码内容",
  format: "QR_CODE",      // QR_CODE/EAN_13/CODE_128 等
  valueType: 0
}

OCR 文本识别结果

当识别到文本(非条码)时,formatTEXT,并额外返回提取的手机号和身份证号:

{
  code: 200,
  msg: "识别成功",
  rawValue: "原始识别文本",
  format: "TEXT",
  valueType: 0,
  textType: "phone",      // phone/idcard/phone,idcard/text
  phoneNumbers: ["***", "***"],   // ArrayList<String>
  idCardNumbers: ["110101199001011234"]            // ArrayList<String>
}

注意phoneNumbers/idCardNumbers 是 Java ArrayList<String>,不是 JS 数组。遍历用 .size() + .get(i),不能用 forEach/length

OCR 提取规则

  • 手机号:11 位数字,1 开头,第二位 3-9(如 ***)
  • 身份证号:18 位,前 17 位数字,最后一位数字或 X/x(如 110101199001011234)

支持的条码格式

格式 说明
QR_CODE 二维码
AZTEC Aztec 码
EAN_13 EAN-13 条形码
EAN_8 EAN-8 条形码
UPC_A UPC-A 条形码
UPC_E UPC-E 条形码
CODE_128 Code 128 条形码
CODE_39 Code 39 条形码
CODE_93 Code 93 条形码
CODABAR Codabar 条形码
ITF ITF 条形码
DATA_MATRIX Data Matrix 码
PDF_417 PDF 417 条形码

注意事项

  1. 必须制作自定义基座:UTS 插件涉及原生 Kotlin 代码,标准基座无法运行。
  2. nvue 专用:本插件通过原生 overlay 实现,不适用于 vue 页面。onReady 时 flexbox 布局可能未完成,需延迟测量占位区。
  3. overlay 机制:扫描器原生视图浮在 nvue 页面之上,靠 setViewFrame 精确对齐 <view ref="scanArea"> 的位置。scan-areaflex:1 会覆盖显式 height,如需固定尺寸请去掉 flex:1
  4. 数值类型:UTS 在 nvue 下把 JS number 装成 BigDecimal/DoublesetScanViewOptions 等方法已内部转换,JS 侧直接传 number 即可。
  5. maskOpacity0 = 完全透明(无遮罩),0.5 = 半透明黑(默认),1 = 完全不透明。
  6. 页面生命周期onHide 时停止扫描,onUnload 时销毁扫描器,避免后台占用相机。

完整Demo使用示例 xxx.nvue

<template>
  <view class="container">
    <!-- 扫描区:ScannerView overlay 会按测量结果精确覆盖此区域 -->
    <view class="scan-container">
    <view ref="scanArea" class="scan-area">
      <text class="scan-hint">扫描预览区</text>
    </view>
    </view>
    <!-- 控制条 -->
    <view class="top-bar">
      <button :type="torchOn ? 'warn' : 'default'" size="mini" @tap="toggleTorch">
        <text>{{ torchOn ? '关闪光' : '开闪光' }}</text>
      </button>
      <view class="zoom-control">
        <text class="zoom-label">{{ currentZoom.toFixed(1) }}x</text>
        <slider class="zoom-slider" :min="10" :max="maxZoom * 10" :value="currentZoom * 10" @change="onZoomSliderChange" />
      </view>
      <view class="setting-item">
        <text class="setting-label">音效</text>
        <switch :checked="playSound == true" @change="togglePlaySound" />
      </view>
    </view>

    <!-- 底部控制面板 -->
    <view class="bottom-panel">
      <view class="action-row">
        <button type="primary" size="mini" @tap="startScanner" :disabled="isScanning">启动扫描</button>
        <button type="warn" size="mini" @tap="stopScanner" :disabled="!isScanning">停止扫描</button>
      </view>

      <!-- 扫描线颜色选择区 -->
      <view class="color-section">
        <view class="color-section-header">
          <text class="section-title">扫描线颜色</text>
          <view class="color-preview">
            <view class="color-preview-line" :style="{ backgroundColor: scanLineColor }"></view>
            <text class="color-preview-text">{{ scanLineColor }}</text>
          </view>
        </view>
        <view class="color-row">
          <view v-for="color in lineColors" :key="color" :class="['color-btn', scanLineColor === color ? 'color-btn-active' : '']" :style="{ backgroundColor: color }" @tap="setScanLineColor(color)">
            <text v-if="scanLineColor === color" class="color-btn-check">✓</text>
          </view>
        </view>
      </view>

      <view class="log-section">
        <view class="log-header">
          <text class="section-title">日志</text>
          <button size="mini" type="default" @tap="clearLogs"><text>清空</text></button>
        </view>
        <scroll-view scroll-y class="log-scroll">
          <text v-for="(log, index) in logs" :key="index" class="log-item">{{ log }}</text>
        </scroll-view>
      </view>
    </view>
  </view>
</template>

<script>
  // 直接导入 UTS 原生模块(绕过 .vue 组件层,避免 nvue 下组件不实例化的问题)
  import { NativeScanner } from "@/uni_modules/yuange-scanneruniapp";

  export default {
    data() {
      return {
        // 扫描器 overlay 的位置和尺寸(相对屏幕像素,由 onReady 测量 scanArea 得到)
        scannerWidth: 0,
        scannerHeight: 0,
        scannerLeft: 0,
        scannerTop: 0,
        torchOn: false,
        currentZoom: 1.0,
        maxZoom: 10.0,
        isScanning: false,
        scanFrameWidthRatio: 0.7,
        scanFrameHeightRatio: 0.5,
        scanLineColor: '#00FF00',
        frameBorderColor: '#FFFFFF',
        hintText: '请将二维码/条形码置于框内',
        hintColor: '#FFFFFF',
        playSound: true,
        showScanLine: true,
        lineColors: ['#FFFFFF', '#00FF00', '#FF0000', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF'],
        logs: [],
        // NativeScanner 实例(直接管理,不经过组件层)
        nativeScanner: null,
        isInitialized: false,
        initError: '',
        isPageReady: false,
        initFailed: false
      }
    },

    onLoad() {
      this.addLog('页面加载完成');
    },

    onReady() {
      this.isPageReady = true;
      this.addLog('页面就绪,开始初始化扫描器...');
      this.measureAndInit();
    },

    onUnload() {
      this.stopScanner();
      this.destroyScanner();
      this.addLog('扫描器已销毁');
    },

    onHide() {
      this.stopScanner();
    },

    methods: {
      // 把任意错误对象转成可读字符串(兼容 UTS HashMap)
      toMsg(err) {
        if (err == null) return '未知错误';
        if (typeof err === 'string') return err;
        try {
          if (typeof err.get === 'function') {
            return err.get('msg') || err.get('message') || String(err);
          }
        } catch (e) {}
        return err.message || err.msg || String(err);
      },

      // 测量 scan-area 的实际位置和尺寸,再初始化扫描器。
      // nvue 的 onReady 触发时 flexbox 布局可能尚未完成(尤其 flex:1 的 view),
      // getComponentRect 会返回 0×0。故先延迟一帧再测,并支持失败重试。
      measureAndInit(retryCount) {
        const retry = (retryCount == null) ? 0 : retryCount;
        const dom = uni.requireNativePlugin('dom');
        const doMeasure = () => {
          if (dom && typeof dom.getComponentRect === 'function' && this.$refs.scanArea) {
            dom.getComponentRect(this.$refs.scanArea, (res) => {
              if (res && res.size && res.size.width > 0 && res.size.height > 0) {
                this.scannerWidth = Math.floor(res.size.width);
                this.scannerHeight = Math.floor(res.size.height);
                this.scannerLeft = Math.floor(res.size.left || 0);
                this.scannerTop = Math.floor(res.size.top || 0);
                this.addLog('扫描区: ' + this.scannerWidth + 'x' + this.scannerHeight + ' @(' + this.scannerLeft + ',' + this.scannerTop + ')');
                this.initScanner();
              } else {
                // 布局未就绪,最多重试 5 次(累计约 250ms)
                if (retry < 5) {
                  setTimeout(() => this.measureAndInit(retry + 1), 50);
                } else {
                  this.addLog('多次测量仍为 0,使用回退值');
                  this.fallbackSize();
                  this.initScanner();
                }
              }
            });
          } else {
            this.fallbackSize();
            this.initScanner();
          }
        };
        // 首次延迟一帧,让 flexbox 布局 pass 跑完;重试时无需再延迟
        if (retry === 0) {
          setTimeout(doMeasure, 50);
        } else {
          doMeasure();
        }
      },

      // 测量失败时的回退尺寸(屏幕宽 × 屏幕高 55%,垂直居中)
      fallbackSize() {
        const sys = uni.getSystemInfoSync();
        this.scannerWidth = sys.windowWidth;
        this.scannerHeight = Math.floor(sys.windowHeight * 0.55);
        this.scannerLeft = 0;
        this.scannerTop = Math.floor((sys.windowHeight - this.scannerHeight) / 2);
        this.addLog('测量失败,回退值: ' + this.scannerWidth + 'x' + this.scannerHeight);
      },

      // 直接创建并初始化 NativeScanner 实例
      initScanner() {
        this.addLog('开始创建 NativeScanner...');

        // 检查 UTS 模块是否可用
        if (typeof NativeScanner !== 'function') {
          const msg = 'NativeScanner 未定义 (typeof=' + typeof NativeScanner + ')。请确认已制作自定义基座且 UTS 编译无错误。';
          this.initError = msg;
          this.initFailed = true;
          this.isInitialized = true;
          this.addLog(msg);
          uni.showModal({ title: '初始化失败', content: msg, showCancel: false });
          return;
        }

        try {
          this.nativeScanner = new NativeScanner(this.scannerWidth, this.scannerHeight);
          this.addLog('NativeScanner 已创建');
        } catch (err) {
          this.initError = '创建NativeScanner失败: ' + this.toMsg(err);
          this.initFailed = true;
          this.isInitialized = true;
          this.addLog(this.initError);
          uni.showModal({ title: '初始化失败', content: this.initError, showCancel: false });
          return;
        }

        // 确保尺寸有效
        let w = this.scannerWidth;
        let h = this.scannerHeight;
        if (w <= 0 || h <= 0) {
          w = w > 0 ? w : 300;
          h = h > 0 ? h : 400;
        }

        // 设置 overlay 精确位置和尺寸(让相机预览覆盖 scan-area,而非贴在左上角)
        try {
          this.nativeScanner.setViewFrame(this.scannerLeft, this.scannerTop, w, h);
          this.addLog('视图位置已设置: ' + w + 'x' + h + ' @(' + this.scannerLeft + ',' + this.scannerTop + ')');
        } catch (err) {
          this.addLog('设置视图位置失败(非致命): ' + this.toMsg(err));
        }

        try {
          this.nativeScanner.bindView();
          this.addLog('原生视图已绑定');
        } catch (err) {
          this.initError = '绑定视图失败: ' + this.toMsg(err);
          this.initFailed = true;
          this.isInitialized = true;
          this.addLog(this.initError);
          uni.showModal({ title: '初始化失败', content: this.initError, showCancel: false });
          return;
        }

        // 初始化扫描视图配置
        this.applyScanViewOptions();

        // 初始化反馈配置
        try {
          this.nativeScanner.setScanFeedbackOptions({
            playSound: this.playSound,
            autoStop: false
          });
        } catch (err) {
          this.addLog('设置反馈配置失败(非致命): ' + this.toMsg(err));
        }

        // 设置扫描结果回调
        this.setupScanResultCallback();

        this.isInitialized = true;
        this.initFailed = false;
        this.addLog('扫描器初始化完成');

        // 获取最大缩放
        this.loadMaxZoom();
      },

      // 统一构造扫描视图配置对象(init 与运行时复用)
      applyScanViewOptions() {
        if (!this.nativeScanner) return;
        try {
          this.nativeScanner.setScanViewOptions({
            scanFrameWidth: this.scanFrameWidthRatio,
            scanFrameHeight: this.scanFrameHeightRatio,
            frameBorderColor: this.frameBorderColor,
            frameBorderWidth: 2,
            scanLineStyle: {
              color: this.scanLineColor,
              width: 2,
              height: 4
            },
            hintText: this.hintText,
            hintColor: this.hintColor,
            hintTextSize: 14,
            maskOpacity: 0,
            maskColor: '#000000'
          });
        } catch (err) {
          this.addLog('设置扫描视图配置失败(非致命): ' + this.toMsg(err));
        }
      },

      // 设置扫描结果回调
      setupScanResultCallback() {
        if (!this.nativeScanner) return;

        try {
          this.nativeScanner.setScanResultCallback({
            success: (res) => {
              let rawValue = '';
              let format = '';
              let textType = '';
              let phones = [];
              let idCards = [];

              try {
                if (typeof res.get === 'function') {
                  rawValue = res.get("rawValue") || '';
                  format = res.get("format") || '';
                  textType = res.get("textType") || '';
                  // phoneNumbers / idCardNumbers 在 UTS 侧已转成 ArrayList<String>
                  const phoneList = res.get("phoneNumbers");
                  if (phoneList != null) {
                    try {
                      const size = typeof phoneList.size === 'function' ? phoneList.size() : 0;
                      for (let i = 0; i < size; i++) {
                        phones.push(phoneList.get(i));
                      }
                    } catch (e2) {}
                  }
                  const idCardList = res.get("idCardNumbers");
                  if (idCardList != null) {
                    try {
                      const size = typeof idCardList.size === 'function' ? idCardList.size() : 0;
                      for (let i = 0; i < size; i++) {
                        idCards.push(idCardList.get(i));
                      }
                    } catch (e2) {}
                  }
                } else {
                  rawValue = res["rawValue"] || res.rawValue || '';
                  format = res["format"] || res.format || '';
                  textType = res["textType"] || res.textType || '';
                  phones = res["phoneNumbers"] || res.phoneNumbers || [];
                  idCards = res["idCardNumbers"] || res.idCardNumbers || [];
                }
              } catch (e) {
                rawValue = '';
                format = '';
                textType = '';
              }

              const typeInfo = textType != '' ? ' [' + textType + ']' : '';
              this.addLog('扫描成功: ' + format + typeInfo);
              this.addLog('内容: ' + rawValue);
              if (phones.length > 0) {
                this.addLog('手机号: ' + phones.join(', '));
              }
              if (idCards.length > 0) {
                this.addLog('身份证: ' + idCards.join(', '));
              }

              // 回调出去:通过全局事件通知其他页面或组件
              uni.$emit('scannerResult', {
                rawValue: rawValue,
                format: format,
                textType: textType,
                phoneNumbers: phones,
                idCardNumbers: idCards
              });

              uni.showToast({ title: '扫描成功', icon: 'success', duration: 1500 });
              uni.setClipboardData({ data: rawValue, showToast: false });
            },
            fail: (err) => {
              let msg = '';
              try {
                if (typeof err.get === 'function') {
                  msg = err.get("msg") || '';
                } else {
                  msg = err["msg"] || err.msg || '';
                }
              } catch (e) {
                msg = '未知错误';
              }
              this.addLog('错误: ' + msg);
            }
          });
          this.addLog('扫描结果回调已设置');
        } catch (err) {
          this.addLog('设置回调失败: ' + this.toMsg(err));
        }
      },

      // 获取最大缩放倍数
      loadMaxZoom() {
        if (!this.nativeScanner) return;

        try {
          this.nativeScanner.getMaxZoom({
            success: (res) => {
              let zoom = 10;
              try {
                if (typeof res.get === 'function') {
                  zoom = res.get("maxZoom") || 10;
                } else {
                  zoom = res["maxZoom"] || res.maxZoom || 10;
                }
              } catch (e) {}
              this.maxZoom = zoom;
              this.addLog('最大缩放: ' + zoom.toFixed(0) + 'x');
            },
            fail: () => {
              this.addLog('获取最大缩放失败,使用默认值');
            }
          });
        } catch (err) {
          this.addLog('获取最大缩放异常: ' + this.toMsg(err));
        }
      },

      addLog(msg) {
        const now = new Date();
        const time = now.getHours() + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0');
        const line = '[' + time + '] ' + msg;
        this.logs.unshift(line);
        if (this.logs.length > 50) {
          this.logs.pop();
        }
        // 同步输出到控制台
        console.log(line);
      },

      clearLogs() {
        this.logs = [];
      },

      startScanner() {
        if (this.isScanning) return;

        if (!this.nativeScanner) {
          this.addLog('扫描器不可用');
          return;
        }

        if (this.initFailed) {
          const reason = this.initError || '初始化失败,请重新进入页面';
          this.addLog('启动失败: ' + reason);
          uni.showModal({ title: '无法启动', content: reason, showCancel: false });
          return;
        }

        if (!this.isInitialized) {
          this.addLog('扫描器未初始化完成');
          return;
        }

        this.addLog('启动相机...');

        try {
          this.nativeScanner.start({
            cameraId: 0,
            torch: this.torchOn,
            requestPermission: true
          }, {
            success: (res) => {
              this.addLog('相机启动成功');
            },
            fail: (err) => {
              let msg = '';
              try {
                if (typeof err.get === 'function') {
                  msg = err.get("msg") || '';
                } else {
                  msg = err["msg"] || err.msg || '';
                }
              } catch (e) {
                msg = '未知错误';
              }
              this.addLog('启动失败: ' + msg);
            }
          });
        } catch (err) {
          this.addLog('启动异常: ' + this.toMsg(err));
        }

        this.isScanning = true;
        this.addLog('启动扫描...');
      },

      stopScanner() {
        if (!this.isScanning) return;
        if (this.nativeScanner) {
          try {
            this.nativeScanner.stop();
          } catch (err) {
            this.addLog('停止异常: ' + this.toMsg(err));
          }
        }
        this.isScanning = false;
        this.addLog('停止扫描');
      },

      destroyScanner() {
        if (this.nativeScanner) {
          try {
            this.nativeScanner.destroy();
          } catch (err) {
            this.addLog('销毁异常: ' + this.toMsg(err));
          }
          this.nativeScanner = null;
        }
      },

      toggleTorch() {
        this.torchOn = !this.torchOn;
        if (this.nativeScanner) {
          try {
            this.nativeScanner.setTorch(this.torchOn, {
              success: function(res) {},
              fail: (err) => {
                this.addLog('设置闪光灯失败: ' + this.toMsg(err));
              }
            });
          } catch (err) {
            this.addLog('设置闪光灯异常: ' + this.toMsg(err));
          }
        }
        this.addLog('闪光灯: ' + (this.torchOn ? '开启' : '关闭'));
      },

      onZoomSliderChange(e) {
        // nvue slider 的 change 事件对象结构在不同版本下不一致:
        // 可能是 e.detail.value、e.value,或直接传 number。统一兼容。
        let rawValue = null;
        if (e != null) {
          if (typeof e === 'number') {
            rawValue = e;
          } else if (typeof e.value === 'number') {
            rawValue = e.value;
          } else if (e.detail != null) {
            if (typeof e.detail.value === 'number') rawValue = e.detail.value;
            else if (typeof e.detail === 'number') rawValue = e.detail;
          }
        }
        // 字符串数字也接受
        if (rawValue == null && e != null && typeof e === 'object') {
          const v = e.detail ? e.detail.value : e.value;
          if (v != null) rawValue = Number(v);
        }
        if (rawValue == null || isNaN(rawValue) || !isFinite(rawValue)) {
          this.addLog('缩放值无效,已忽略');
          return;
        }
        const zoom = rawValue / 10.0;
        this.currentZoom = zoom;
        if (this.nativeScanner) {
          try {
            this.nativeScanner.setZoom(zoom, {
              success: function(res) {},
              fail: (err) => {
                this.addLog('设置缩放失败: ' + this.toMsg(err));
              }
            });
          } catch (err) {
            this.addLog('设置缩放异常: ' + this.toMsg(err));
          }
        }
        this.addLog('设置缩放: ' + zoom.toFixed(1) + 'x');
      },

      togglePlaySound() {
        this.playSound = !this.playSound;
        if (this.nativeScanner) {
          try {
            this.nativeScanner.setScanFeedbackOptions({
              playSound: this.playSound,
              autoStop: false
            });
          } catch (err) {
            this.addLog('设置反馈异常: ' + this.toMsg(err));
          }
        }
        this.addLog('提示音: ' + (this.playSound ? '开启' : '关闭'));
      },

      toggleScanLine() {
        this.showScanLine = !this.showScanLine;
        this.addLog('扫描线: ' + (this.showScanLine ? '显示' : '隐藏'));
      },

      setScanLineColor(color) {
        this.scanLineColor = color;
        this.applyScanViewOptions();
        this.addLog('扫描线颜色: ' + color);
      }
    }
  }
</script>

<style>
  .container { background-color: #1a1a1a; flex: 1; }
  .top-bar { flex-direction: row; align-items: center; padding: 16rpx 20rpx; background-color: #2c2c2c; }
  .zoom-control { flex: 1; flex-direction: row; align-items: center; margin-left: 16rpx; padding: 6rpx 16rpx; background-color: #3a3a3a; border-radius: 8rpx; }
  .zoom-label { color: #aaaaaa; font-size: 22rpx; width: 70rpx; }
  .zoom-slider { flex: 1; }
  .setting-item { flex-direction: row; align-items: center; margin-left: 16rpx; }
  .setting-label { color: #aaaaaa; font-size: 22rpx; margin-right: 8rpx; }
  .scan-container { flex: 1;}
  .scan-area { justify-content: center; align-items: center; margin-top: 200px; width: 1080px; height: 1000px;}
  .scan-hint { color: #666666; font-size: 28rpx; }
  .color-section { background-color: #2c2c2c; padding: 16rpx; border-radius: 8rpx; margin-bottom: 16rpx; }
  .color-section-header { flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
  .color-preview { flex-direction: row; align-items: center; }
  .color-preview-line { width: 60rpx; height: 4rpx; margin-right: 10rpx; border-radius: 2rpx; }
  .color-preview-text { color: #aaaaaa; font-size: 22rpx; }
  .color-row { flex-direction: row; align-items: center; flex-wrap: wrap; }
  .color-btn { width: 56rpx; height: 56rpx; border-radius: 28rpx; margin-right: 16rpx; margin-bottom: 8rpx; border-width: 2px; border-style: solid; border-color: #555555; justify-content: center; align-items: center; }
  .color-btn-active { width: 72rpx; height: 72rpx; border-radius: 36rpx; border-width: 3px; border-color: #00FF00; }
  .color-btn-check { font-size: 32rpx; color: #ffffff; font-weight: bold; }
  .bottom-panel { background-color: #1e1e1e; padding: 16rpx 20rpx; }
  .action-row { flex-direction: row; justify-content: space-around; margin-bottom: 16rpx; }
  .log-section { background-color: #252525; padding: 12rpx; border-radius: 8rpx; }
  .log-header { flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 8rpx; }
  .section-title { font-size: 24rpx; font-weight: bold; color: #eeeeee; }
  .log-scroll { height: 200rpx; }
  .log-item { font-size: 20rpx; color: #aaaaaa; line-height: 36rpx; }
</style>

隐私、权限声明

1. 本插件需要申请的系统权限列表:

android.permission.CAMERA

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

插件不采集任何数据

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。