更新记录
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>

收藏人数:
购买普通授权版(
试用
赞赏(0)
下载 659
赞赏 0
下载 12291788
赞赏 1922
赞赏
京公网安备:11010802035340号