更新记录
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 页面,支持 vue2 和 vue3。若是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 文本识别结果
当识别到文本(非条码)时,format 为 TEXT,并额外返回提取的手机号和身份证号:
{
code: 200,
msg: "识别成功",
rawValue: "原始识别文本",
format: "TEXT",
valueType: 0,
textType: "phone", // phone/idcard/phone,idcard/text
phoneNumbers: ["***", "***"], // ArrayList<String>
idCardNumbers: ["110101199001011234"] // ArrayList<String>
}
注意:
phoneNumbers/idCardNumbers是 JavaArrayList<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 条形码 |
注意事项
- 必须制作自定义基座:UTS 插件涉及原生 Kotlin 代码,标准基座无法运行。
- nvue 专用:本插件通过原生 overlay 实现,不适用于 vue 页面。
onReady时 flexbox 布局可能未完成,需延迟测量占位区。 - overlay 机制:扫描器原生视图浮在 nvue 页面之上,靠
setViewFrame精确对齐<view ref="scanArea">的位置。scan-area的flex:1会覆盖显式height,如需固定尺寸请去掉flex:1。 - 数值类型:UTS 在 nvue 下把 JS
number装成BigDecimal/Double,setScanViewOptions等方法已内部转换,JS 侧直接传 number 即可。 maskOpacity:0= 完全透明(无遮罩),0.5= 半透明黑(默认),1= 完全不透明。- 页面生命周期:
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>

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