更新记录

1.0.0(2026-06-21)

  • 发布插件

平台兼容性

uni-app x(5.07)

Chrome Safari Android Android插件版本 iOS 鸿蒙 微信小程序
- - 5.0 1.0.0 × × -

功能特性

  • 支持二维码和条形码扫描
  • 支持OCR文本识别,自动从文本中提取手机号和身份证号以及同时回调原始内容
  • 支持闪光灯控制
  • 支持相机缩放(放大/缩小)
  • 支持自定义扫描线、提示文案UI元素
  • 支持扫描完成提示音控制

快速开始 注意需要在uniapp x项目中使用.uvue创建

1. 在模板中放置扫描组件

<template>
  <view class="container">
    <!-- 使用 id 绑定,用于 CreateNativeScannerContext -->
    <native-scannerpreview
      id="scannerPreview"
      :width="scannerWidth"
      :height="scannerHeight"
      :scanFrameWidthRatio="0.7"
      :scanFrameHeightRatio="0.5"
      :scanLineColor="#00FF00"
      :hintText="请摆好姿势扫描..."
      :playSound="true"
      :showOverlay="true"
      :showScanLine="true"
      :showResult="true"
    ></native-scannerpreview>

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

    <!-- 扫描结果 -->
    <view class="result" v-if="scanResult">
      <text>扫描结果: {{ scanResult }}</text>
    </view>
  </view>
</template>

2. 导入并初始化上下文

import {
  CreateNativeScannerContext,
  INativeScannerContext,
  OnCallBackOptions,
  ScannerStartOptions,
  ScanViewOptions,
  ScanLineStyle,
  ScanFeedbackOptions
} from "@/uni_modules/yuange-scanner";

let context: INativeScannerContext | null = null;

// 在页面 onReady 中初始化(确保组件已渲染)
onReady() {
  setTimeout(() => {
    context = CreateNativeScannerContext("scannerPreview", this);
    if (context != null) {
      // 设置扫描结果回调
      context.setScanResultCallback({
        success: (res) => {
          const rawValue = res.get("rawValue") as string;
          const format = res.get("format") as string;
          console.log("扫描成功:", rawValue, "格式:", format);
        },
        fail: (err) => {
          console.log("扫描失败:", err.get("msg"));
        }
      } as OnCallBackOptions);
    }
  }, 300);
}

3. 启动扫描

function startScanner() {
  if (context == null) return;

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

function stopScanner() {
  context?.stop();
}

4. 页面卸载时销毁

onUnload() {
  context?.stop();
  context?.destroy();
  context = null;
}

API 列表

核心方法(通过 CreateNativeScannerContext 获取)

CreateNativeScannerContext - 创建上下文

import { CreateNativeScannerContext } from "@/uni_modules/yuange-scanner";
context = CreateNativeScannerContext("scannerPreview", this);

start - 启动扫描

context.start({
  cameraId: 0,           // 摄像头 ID:0=后摄
  torch: false,          // 是否开启闪光灯,默认 false
  requestPermission: true // 是否需要申请相机权限,默认 true
} as ScannerStartOptions, {
  success: (res) => {
    console.log("相机启动成功");
  },
  fail: (err) => {
    console.log("启动失败:", err.get("msg"));
  }
} as OnCallBackOptions);

stop - 停止扫描

context.stop();

destroy - 销毁上下文并释放资源

context.destroy();

setTorch - 设置闪光灯

context.setTorch(true, {  // true=开启,false=关闭
  success: (res) => {
    console.log("闪光灯已更新");
  },
  fail: (err) => {
    console.log("设置失败:", err.get("msg"));
  }
} as OnCallBackOptions);

setZoom - 设置缩放倍数

// zoom: 1.0 ~ maxZoom(通过 getMaxZoom 获取最大值)
context.setZoom(2.0 as Float, {
  success: (res) => {
    const zoom = res.get("zoom") as number;
    console.log("缩放已设置:", zoom);
  },
  fail: (err) => {
    console.log("设置失败:", err.get("msg"));
  }
} as OnCallBackOptions);

setScanViewOptions - 更新扫描视图配置

context.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: 2                   // 扫描线高度(像素),默认 2
  } as ScanLineStyle,
  hintText: '请扫描',           // 提示文案,默认 '请扫描'
  hintColor: '#FFFFFF',         // 提示文案颜色,默认 '#FFFFFF'
  hintTextSize: 14,             // 提示文案大小(sp),默认 14
  maskOpacity: 0.5,             // 背景遮罩透明度(0.0~1.0),默认 0.5
  maskColor: '#000000'          // 背景遮罩颜色,默认 '#000000'
} as ScanViewOptions);

setScanFeedbackOptions - 设置扫描反馈配置

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

setScanResultCallback - 设置扫描结果回调

context.setScanResultCallback({
  success: (res) => {
    // 扫描成功
    const rawValue = res.get("rawValue") as string;    // 扫描内容
    const format = res.get("format") as string;        // 条码格式(如 QR_CODE, EAN_13 等)
    const textType = res.get("textType") as string;    // OCR 文本类型(phone/idcard)
    const phoneNumbers = res.get("phoneNumbers") as string[];  // 提取的手机号
    const idCardNumbers = res.get("idCardNumbers") as string[]; // 提取的身份证号

    console.log("扫描成功:", rawValue, "格式:", format);
  },
  fail: (err) => {
    // 扫描失败
    const msg = err.get("msg") as string;
    console.log("扫描失败:", msg);
  }
} as OnCallBackOptions);

组件属性

属性 类型 默认值 说明
尺寸
width Number - 扫描器宽度(像素),必填
height Number - 扫描器高度(像素),必填
扫描框
scanFrameWidthRatio Number 0.7 扫描框宽度占预览宽度的比例(0.0~1.0)
scanFrameHeightRatio Number 0.5 扫描框高度占预览高度的比例(0.0~1.0)
frameBorderColor String '#FFFFFF' 扫描框边框颜色
frameBorderWidth Number 2 扫描框边框宽度(像素)
扫描线
scanLineColor String '#00FF00' 扫描线颜色
scanLineWidth Number 2 扫描线宽度(像素)
scanLineHeight Number 2 扫描线高度(像素)
showScanLine Boolean true 是否显示扫描线
提示文案
hintText String '请扫描' 扫描提示文案
hintColor String '#FFFFFF' 提示文案颜色
hintTextSize Number 14 提示文案大小(sp)
背景遮罩
maskOpacity Number 0.5 背景遮罩透明度(0.0~1.0)
maskColor String '#000000' 背景遮罩颜色
showOverlay Boolean true 是否显示扫描框遮罩
扫描反馈
playSound Boolean true 扫描成功后播放提示音
autoStop Boolean false 扫描成功后自动停止扫描
其他
torch Boolean false 是否开启闪光灯
showResult Boolean true 是否显示扫描结果

组件事件

scanResult - 扫描成功

@scanResult="onScanResult"

function onScanResult(result) {
  console.log("扫描内容:", result.rawValue);
  console.log("条码格式:", result.format);  // QR_CODE, EAN_13, TEXT 等
  console.log("值类型:", result.valueType);

  // 当扫描结果为文本(OCR识别)时
  if (result.format === 'TEXT') {
    console.log("提取的手机号:", result.phoneNumbers);
    console.log("提取的身份证号:", result.idCardNumbers);
  }
}

返回数据结构:

{
  rawValue: string,        // 扫描内容
  format: string,          // 条码格式
  valueType: number,       // 值类型
  phoneNumbers: string[],  // 提取的手机号数组(OCR时)
  idCardNumbers: string[]  // 提取的身份证号数组(OCR时)
}

scanError - 扫描错误

@scanError="onScanError"

function onScanError(error) {
  console.log("错误代码:", error.code);
  console.log("错误信息:", error.msg);
}

返回数据结构:

{
  code: number,  // 错误代码
  msg: string    // 错误信息
}

torchChange - 闪光灯状态改变

@torchChange="onTorchChange"

function onTorchChange(on) {
  console.log("闪光灯:", on ? "开启" : "关闭");
}

zoomChange - 缩放倍数改变

@zoomChange="onZoomChange"

function onZoomChange(zoom) {
  console.log("缩放倍数:", zoom);
}

OCR文本识别功能

当扫描到的文本内容(非条形码/二维码)时,插件会自动调用OCR文本识别,并从识别的文本中提取:

  • 手机号:11位数字,1开头(如:***)
  • 身份证号:18位数字,最后一位可以是X(如:110101199001011234)

OCR扫描结果格式

当格式为 TEXT 时(OCR识别),扫描结果会包含以下额外字段:

{
  code: 200,
  msg: "识别成功",
  rawValue: "原始识别文本",
  format: "TEXT",
  valueType: 0,
  phoneNumbers: ["***", "***"],  // 提取到的手机号数组
  idCardNumbers: ["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条形码

Demo完整示例 xxxxx.uvue

<template>
  <view class="container">
    <!-- 扫描器组件 -->
    <native-scannerpreview
      id="scannerPreview"
      :width="scannerWidth"
      :height="scannerHeight"
      :scanFrameWidthRatio="scanFrameWidthRatio"
      :scanFrameHeightRatio="scanFrameHeightRatio"
      :scanLineColor="scanLineColor"
      :scanLineHeight="4"
      :frameBorderColor="frameBorderColor"
      :hintText="hintText"
      :hintColor="hintColor"
      :maskOpacity="0.5"
      :playSound="playSound"
      :showOverlay="true"
      :showScanLine="showScanLine"
      :showResult="true"
    ></native-scannerpreview>

    <!-- 控制区 -->
    <view class="controls">
      <view class="control-row">
        <button :type="torchOn ? 'warn' : 'default'" size="mini" @tap="toggleTorch">
          {{ torchOn ? '💡 关闭闪光' : '💡 开启闪光' }}
        </button>
      </view>

      <view class="zoom-control">
        <text class="label">缩放: {{ currentZoom.toFixed(1) }}x</text>
        <slider class="zoom-slider" :min="10" :max="maxZoom * 10" :value="currentZoom * 10" @change="onZoomSliderChange" />
        <text class="max-label">{{ maxZoom.toFixed(0) }}x</text>
      </view>

      <view class="control-row">
        <button type="primary" size="mini" @tap="startScanner" :disabled="isScanning">▶️ 启动扫描</button>
        <button type="warn" size="mini" @tap="stopScanner" :disabled="!isScanning">⏹️ 停止扫描</button>
      </view>

      <view class="settings-row">
        <view class="setting-item">
          <text class="setting-label">提示音</text>
          <switch :checked="playSound == true" @change="togglePlaySound" />
        </view>
        <view class="setting-item">
          <text class="setting-label">扫描线</text>
          <switch :checked="showScanLine == true" @change="toggleScanLine" />
        </view>
      </view>

      <view class="color-row">
        <text class="label">扫描线颜色:</text>
        <view v-for="color in lineColors" :key="color" class="color-btn" :style="{ backgroundColor: color }" @tap="setScanLineColor(color)"></view>
      </view>
    </view>

    <!-- 扫描结果 -->
    <view class="result" v-if="hasScanResult">
      <text class="result-label">扫描结果:</text>
      <text class="result-value">{{ scanResult }}</text>
      <text class="result-format">格式: {{ scanFormat }}{{ scanTextType != '' ? ' (' + scanTextType + ')' : '' }}</text>
      <view class="result-actions">
        <button type="default" size="mini" @tap="copyResult">📋 复制</button>
        <button type="default" size="mini" @tap="clearResult">🗑️ 清除</button>
      </view>
    </view>

    <!-- 操作日志 -->
    <view class="log-section">
      <view class="log-header">
        <text class="section-title">操作日志</text>
        <button size="mini" type="default" @tap="clearLogs">清空</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>
</template>

  <script lang="uts">
    import {
      CreateNativeScannerContext,
      INativeScannerContext,
      OnCallBackOptions,
      ScannerStartOptions,
      ScanViewOptions,
      ScanLineStyle,
      ScanFeedbackOptions
    } from "@/uni_modules/yuange-scanner";

    let context: INativeScannerContext | null = null;
    let isContextInitialized = false;

    export default {
      data() {
        return {
          scannerWidth: 0,
          scannerHeight: 0,
          torchOn: false,
          currentZoom: 1.0,
          maxZoom: 10.0,
          isScanning: false,
          scanResult: '',
          scanFormat: '',
          scanTextType: '',
          hasScanResult: false,
          scanFrameWidthRatio: 0.7,
          scanFrameHeightRatio: 0.5,
          scanLineColor: '#FFFFFF',
          frameBorderColor: '#FFFFFF',
          hintText: '请摆好姿势扫描...',
          hintColor: '#FFFFFF',
          playSound: true,
          showScanLine: true,
          lineColors: ['#FFFFFF', '#00FF00', '#FF0000', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF'],
          logs: [] as string[],
          isPageReady: false
        }
      },

      onLoad() {
        const systemInfo = uni.getSystemInfoSync();
        this.scannerWidth = systemInfo.windowWidth;
        this.scannerHeight = Math.floor(systemInfo.windowHeight * 0.5);
        this.addLog('页面加载完成');
        this.addLog('等待组件渲染...');
      },

      onReady() {
        this.isPageReady = true;
        this.addLog('页面就绪,组件已渲染');
        // 延迟初始化上下文,确保 DOM 已经完全准备好
        setTimeout(() => {
          this.initializeContext();
        }, 300);
      },

      onUnload() {
        this.stopScanner();
        if (context != null) {
          context?.destroy();
          context = null;
          isContextInitialized = false;
        }
        this.addLog('扫描器已销毁');
      },

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

      methods: {
        // 初始化上下文
        initializeContext() {
          if (isContextInitialized && context != null) {
            this.addLog('上下文已初始化');
            return;
          }

          this.addLog('尝试初始化扫描器上下文...');

          // 第一次尝试
          let result = this.tryCreateContext();
          if (result) {
            return;
          }

          // 如果第一次失败,延迟后再次尝试
          this.addLog('首次初始化失败,500ms 后重试...');
          setTimeout(() => {
            let retryResult = this.tryCreateContext();
            if (retryResult) {
              return;
            }

            // 如果第二次也失败,再延迟尝试一次
            this.addLog('第二次初始化失败,1000ms 后再次重试...');
            setTimeout(() => {
              let finalResult = this.tryCreateContext();
              if (!finalResult) {
                this.addLog('❌ 扫描器初始化失败,请检查组件是否正确渲染');
              }
            }, 1000);
          }, 500);
        },

        // 尝试创建上下文
        tryCreateContext(): boolean {
          try {
            context = CreateNativeScannerContext("scannerPreview", this);
            if (context != null) {
              this.setupContextCallbacks();
              isContextInitialized = true;
              this.addLog('✅ 扫描器初始化成功');
              return true;
            } else {
              this.addLog('⚠️ 上下文创建失败,可能组件未准备好');
              return false;
            }
          } catch (e: Exception) {
            this.addLog('❌ 初始化异常: ' + e);
            return false;
          }
        },

        // 设置上下文回调
        setupContextCallbacks() {
          if (context == null) return;

          context?.setScanResultCallback({
            success: (res: HashMap<String, any>) => {
              const rawValue = (res.get("rawValue") as string) ?? '';
              const format = (res.get("format") as string) ?? '';
              const textType = (res.get("textType") != null) ? (res.get("textType") as string) : '';
              this.scanResult = rawValue;
              this.scanFormat = format;
              this.scanTextType = textType;
              this.hasScanResult = true;
              const typeInfo = textType != '' ? ' [' + textType + ']' : '';
              this.addLog('扫描成功: ' + format + typeInfo);
              this.addLog('内容: ' + rawValue);
              console.log("扫描结果 "+res)
              uni.showToast({ title: '扫描成功', icon: 'success', duration: 1500 });
            },
            fail: (err: HashMap<String, any>) => {
              const msg = (err.get("msg") as string) ?? '';
              this.addLog('❌ 错误: ' + msg);
            }
          } as OnCallBackOptions);
        },

        addLog(msg: string) {
          const time = new Date().toLocaleTimeString();
          this.logs.unshift(`[${time}] ${msg}`);
          if (this.logs.length > 50) {
            this.logs.pop();
          }
        },

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

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

          // 确保上下文已初始化
          if (!isContextInitialized || context == null) {
            this.addLog('⚠️ 上下文未初始化,尝试重新初始化...');
            this.initializeContext();
            return;
          }

          this.addLog('启动相机...');
          context?.start({
            cameraId: 0,
            torch: this.torchOn,
            requestPermission: true
          } as ScannerStartOptions, {
            success: (res: HashMap<String, any>) => {
              this.addLog('✅ 相机启动成功');
            },
            fail: (err: HashMap<String, any>) => {
              const msg = (err.get("msg") as string) ?? '';
              this.addLog('❌ 启动失败: ' + msg);
            }
          } as OnCallBackOptions);
          this.isScanning = true;
          this.addLog('启动扫描...');
        },

        stopScanner() {
          if (!this.isScanning) return;
          if (context != null) {
            context?.stop();
          }
          this.isScanning = false;
          this.addLog('停止扫描');
        },

        toggleTorch() {
          this.torchOn = !this.torchOn;
          if (context != null) {
            context?.setTorch(this.torchOn, {
              success: (res: HashMap<String, any>) => {
                this.addLog('闪光灯状态已更新');
              },
              fail: (err: HashMap<String, any>) => {
                const msg = (err.get("msg") as string) ?? '';
                this.addLog('闪光灯设置失败: ' + msg);
              }
            } as OnCallBackOptions);
          }
          this.addLog('闪光灯: ' + (this.torchOn ? '开启' : '关闭'));
        },

        onZoomSliderChange(e: UniSliderChangeEvent) {
          const zoom = e.detail.value / 10.0;
          this.currentZoom = zoom;
          if (context != null) {
            context?.setZoom(zoom.toFloat(), {
              success: (res: HashMap<String, any>) => {},
              fail: (err: HashMap<String, any>) => {}
            } as OnCallBackOptions);
          }
          this.addLog('设置缩放: ' + zoom.toFixed(1) + 'x');
        },

        copyResult() {
          if (this.scanResult.length > 0) {
            uni.setClipboardData({
              data: this.scanResult,
              success: () => {
                this.addLog('已复制到剪贴板');
                uni.showToast({ title: '已复制', icon: 'success' });
              }
            });
          }
        },

        clearResult() {
          this.scanResult = '';
          this.scanFormat = '';
          this.hasScanResult = false;
          this.addLog('清除扫描结果');
        },

        togglePlaySound() {
          this.playSound = !this.playSound;
          if (context != null) {
            context?.setScanFeedbackOptions({
              playSound: this.playSound,
              autoStop: false
            } as ScanFeedbackOptions);
          }
          this.addLog('提示音: ' + (this.playSound ? '开启' : '关闭'));
        },

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

        setScanLineColor(color: string) {
          this.scanLineColor = color;
          if (context != null) {
            context?.setScanViewOptions({
              scanFrameWidth: this.scanFrameWidthRatio.toFloat(),
              scanFrameHeight: this.scanFrameHeightRatio.toFloat(),
              frameBorderColor: this.frameBorderColor,
              frameBorderWidth: 2,
              scanLineStyle: {
                color: this.scanLineColor,
                width: 1,
                height: 1
              } as ScanLineStyle,
              hintText: this.hintText,
              hintColor: this.hintColor,
              hintTextSize: 14,
              maskOpacity: 0.5.toFloat(),
              maskColor: '#000000'
            } as ScanViewOptions);
          }
          this.addLog('扫描线颜色: ' + color);
        }
      }
    }
  </script>

<style>
  .container { background-color: #1a1a1a; min-height: 750px; }
  .controls { background-color: #2c2c2c; padding: 20rpx; }
  .control-row { flex-direction: row; justify-content: space-around; margin-bottom: 20rpx; }
  .zoom-control { flex-direction: row; align-items: center; padding: 10rpx 20rpx; background-color: #3a3a3a; border-radius: 8rpx; margin-bottom: 20rpx; }
  .label { color: #aaaaaa; font-size: 24rpx; margin-right: 20rpx; }
  .zoom-slider { flex: 1; margin: 0 10rpx; }
  .max-label { color: #888888; font-size: 22rpx; width: 60rpx; }
  .settings-row { flex-direction: row; justify-content: space-around; margin-bottom: 20rpx; }
  .setting-item { flex-direction: row; align-items: center; }
  .setting-label { color: #aaaaaa; font-size: 24rpx; margin-right: 10rpx; }
  .color-row { flex-direction: row; align-items: center; padding: 10rpx 20rpx; background-color: #3a3a3a; border-radius: 8rpx; }
  .color-btn { width: 50rpx; height: 50rpx; border-radius: 50%; margin-left: 15rpx; border-width: 2px; border-style: solid; border-color: #ffffff; }
  .result { background-color: #2c2c2c; padding: 20rpx; margin-top: 20rpx; }
  .result-label { font-size: 28rpx; font-weight: bold; color: #eeeeee; margin-bottom: 10rpx; }
  .result-value { font-size: 26rpx; color: #00ff00; margin-bottom: 10rpx; }
  .result-format { font-size: 24rpx; color: #aaaaaa; margin-bottom: 20rpx; }
  .result-actions { flex-direction: row; justify-content: space-around; }
  .log-section { background-color: #1e1e1e; padding: 20rpx; margin-top: 20rpx; }
  .log-header { flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 12rpx; }
  .section-title { font-size: 28rpx; font-weight: bold; color: #eeeeee; }
  .log-scroll { max-height: 300rpx; }
  .log-item { font-size: 22rpx; color: #aaaaaa; line-height: 44rpx; }
</style>

隐私、权限声明

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

android.permission.CAMERA

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

插件不采集任何数据

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

暂无用户评论。