更新记录
1.0.1(2026-10-11)
- 新增 setScanFramePosition:支持动态调整扫描框的竖直排版位置(相对预览高度比例(范围0~1),0.0=顶部、0.5=居中默认、1.0=底部),扫描框边框、扫描线动画、提示文案整体跟随移动,识别带宽中心自动跟随框位置,运行时即时生效;此前扫描框固定绘制在预览竖直中心不可移动
- 支持铺底模式布局:扫描组件绝对定位铺满全屏、页面控件置于组件之后悬浮上层(uni-app x 全原生渲染按元素树叠加,无需原生层级插入),组件新增 scanFramePosition 属性,配合全屏铺底布局可把扫描框放到任意干净预览区
- 组件 overlay 新增识别带宽边界线可视化:传入 ocrLineBandRatio 属性(或运行时调用 setOcrLineBandRatio)后,绘制带宽上下边界线并与扫描框左右边框围成矩形,与原生过滤带宽同一公式,拖动带宽/位置滑块实时跟随
- OCR 升级为中文识别,除中文外也能识别拉丁字母与数字
- 支持条码/二维码和文字同时出现在扫描区域时同时识别并返回,结果通过 results 数组合并返回,新增 ocrText 字段
- 新增 setOcrLineBandRatio:根据扫描线带宽范围精准扫描(OCR/条码按线条位置过滤)
- 新增 setEnableFrameImageCallback:回调扫描的当前帧图片文件(完整相机画面,不裁剪),结果新增 frameImage 字段
1.0.0(2026-06-21)
- 发布插件
平台兼容性
uni-app x(5.07)
| Chrome | Safari | Android | Android插件版本 | iOS | 鸿蒙 | 微信小程序 |
|---|---|---|---|---|---|---|
| - | - | 5.0 | 1.0.0 | × | × | - |
其他
| 多语言 | 暗黑模式 | 宽屏模式 | 蒸汽模式 |
|---|---|---|---|
| × | × | × | √ |
功能特性
- 支持二维码和条形码扫描
- 支持 OCR 中文文本识别,并返回原始识别文本
- 支持条码/二维码和文字同时出现在扫描区域时同时识别并返回
- 支持闪光灯控制
- 支持相机缩放(放大/缩小)
- 支持自定义扫描线颜色、扫描框、提示文案
- 支持扫描完成提示音控制
- 支持根据扫描线带宽范围精准扫描
- 支持回调扫描的当前帧图片文件(完整相机画面,不裁剪)
- 支持手动触发单次扫描(自定义扫描时机)
- 支持动态调整扫描框绝对像素尺寸、竖直排版位置(识别带宽自动跟随)
- 支持铺底模式:相机预览铺满全屏,页面控件悬浮在上层作蒙层
- 支持自动从 OCR 文本中提取手机号和身份证号
适用范围
适用于 uni-app x 项目(.uvue 页面)。若是 uni-app 非 x 项目(nvue 页面)请使用 yuange-scanneruniapp插件。
本插件需制作自定义基座后运行。
快速开始
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();
// 清理帧图片缓存,避免 cacheDir 下 scan_frame_*.jpg 堆积
context?.clearFrameImageCache();
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);
getMaxZoom - 获取最大缩放倍数
context.getMaxZoom({
success: (res) => {
const maxZoom = res.get("maxZoom") as number;
console.log("最大缩放:", maxZoom);
},
fail: (err) => {}
} as OnCallBackOptions);
getTorch - 获取当前闪光灯状态
context.getTorch({
success: (res) => {
const torch = res.get("torch") as boolean;
console.log("闪光灯状态:", torch);
},
fail: (err) => {}
} as OnCallBackOptions);
getZoom - 获取当前缩放倍数
context.getZoom({
success: (res) => {
const zoom = res.get("zoom") as number;
console.log("当前缩放:", zoom);
},
fail: (err) => {}
} as OnCallBackOptions);
setScanViewOptions - 更新扫描视图配置
context.setScanViewOptions({
scanFrameWidth: 0.7, // 扫描框宽度比例(0.0~1.0),默认 0.7
scanFrameHeight: 0.5, // 扫描框高度比例(0.0~1.0),默认 0.5
scanFrameWidthPx: 0, // 扫描框宽度(像素),>0 时优先于比例,0 = 按比例
scanFrameHeightPx: 0, // 扫描框高度(像素),>0 时优先于比例,0 = 按比例
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);
setOcrLineBandRatio - 设置扫描线带宽
设置 OCR/条码按扫描线位置过滤的带宽比例(半宽,相对预览图像对应维度)。
0.12 = 中线 ±12%;1.0 = 不限制(整帧都算命中)。取值夹到 [0.01, 1.0]。
运行时即时生效,下一帧扫描即按新带宽过滤。
context.setOcrLineBandRatio(0.12 as Float);
setScanFramePosition - 设置扫描框竖直排版位置
设置扫描框的竖直排版位置(框中心相对预览高度的比例)。扫描框边框、扫描线动画、提示文案整体跟随移动,识别带宽中心自动跟随框位置。运行时可随时调用,即时生效。
// ratio: 0.0~1.0,默认 0.5(竖直居中)。0.0 = 预览顶部,1.0 = 预览底部
// 例:配合"顶部 20% 干净预览窗"布局,设 0.1 可让扫描框落在顶部窗口中心
// 超出 [0, 1] 会被钳制
context.setScanFramePosition(0.5 as Float);
识别过滤带宽的中心自动跟随
setScanFramePosition设置的扫描框排版位置;组件层同步更新 overlay 视觉位置(也可直接使用组件的scanFramePosition属性声明式控制)。
setScanFeedbackOptions - 设置扫描反馈配置
context.setScanFeedbackOptions({
playSound: true, // 扫描成功后是否播放提示音,默认 true
autoStop: false, // 扫描成功后是否自动停止扫描,默认 false
vibrate: false // 震动反馈(接口保留,当前版本震动功能已禁用,传入不生效)
} as ScanFeedbackOptions);
setEnableFrameImageCallback - 设置是否回调当前帧图片
设置是否回调当前扫描帧(完整相机画面)的图片。
默认 true:回调帧图片。设为 false 后,扫描成功时不保存帧图片、不返回 frameImage 字段。
开启时保存当前完整帧(按屏幕方向旋转到直立,不按扫描线带宽/扫描框裁剪)为 JPEG 文件,
并通过结果中的 frameImage 字段返回文件绝对路径。运行时即时生效,下一次扫描即按新设置处理。
context.setEnableFrameImageCallback(true);
clearFrameImageCache - 清除帧图片缓存
清除所有缓存的扫描帧图片(cacheDir 下的 scan_frame_*.jpg)。
每次开启帧图片回调后,扫描成功都会生成一个 JPEG 缓存文件且不会被自动删除,
长期扫码会导致缓存无限堆积。建议在页面 onUnload 或不再需要帧图片时调用本方法。
context.clearFrameImageCache();
manualScan - 手动触发单次扫描
context.manualScan({
success: (res) => {
console.log("手动扫描已触发");
},
fail: (err) => {}
} as OnCallBackOptions);
setScanResultCallback - 设置扫描结果回调
context.setScanResultCallback({
success: (res) => {
// 扫描成功
const rawValue = res.get("rawValue") as string; // 扫描内容
const format = res.get("format") as string; // 条码格式(如 QR_CODE, EAN_13, TEXT 等)
const ocrText = res.get("ocrText") as string; // 原始 OCR 识别文本
const frameImage = res.get("frameImage") as string; // 当前帧图片路径(开启帧图片回调时返回)
const phoneNumbers = res.get("phoneNumbers") as string[]; // 提取的手机号
const idCardNumbers = res.get("idCardNumbers") as string[]; // 提取的身份证号
// results 数组:同帧识别到的所有条码和文字结果
const results = res.get("results") as ArrayList<any>;
for (let i = 0; i < results.size; i++) {
const item = results.get(i) as HashMap<String, any>;
const type = item.get("type") as string; // "barcode" 或 "text"
const fmt = item.get("format") as string; // "QR_CODE" / "EAN_13" / "TEXT" 等
const val = item.get("rawValue") as string;
console.log(`[${type}] ${fmt}: ${val}`);
}
console.log("扫描成功:", rawValue, "格式:", format);
},
fail: (err) => {
// 扫描失败
const msg = err.get("msg") as string;
console.log("扫描失败:", msg);
}
} as OnCallBackOptions);
setBarcodeFormats - 限定识别的条码格式
传入格式数组(见 支持的条码格式),只识别指定格式;传入空数组恢复识别全部格式。
// 只识别二维码和 EAN-13
context.setBarcodeFormats(['QR_CODE', 'EAN_13']);
// 恢复识别全部格式
context.setBarcodeFormats([]);
扫描结果数据结构
同时扫描结果(条码 + 文字)
当扫描区域内同时存在条码/二维码和文字时,插件会同时执行条码扫描和 OCR 文本识别,并将所有结果合并到一个回调中返回。results 数组包含同一帧识别到的所有条码和文字结果,同时保留顶层兼容字段。
{
code: 200,
msg: "识别成功",
results: [
{ type: "barcode", format: "QR_CODE", rawValue: "二维码内容", valueType: 0 },
{ type: "barcode", format: "EAN_13", rawValue: "6901234567892", valueType: 0 },
{ type: "text", format: "TEXT", rawValue: "原始识别文本", phoneNumbers: [], idCardNumbers: [] }
],
// 以下为向后兼容字段(取第一个结果的值)
rawValue: "二维码内容",
format: "QR_CODE",
valueType: 0,
ocrText: "原始识别文本", // 原始 OCR 识别文本(始终为完整 OCR 结果,不受条码影响)
phoneNumbers: [], // 从 OCR 文本中提取的手机号
idCardNumbers: [], // 从 OCR 文本中提取的身份证号
frameImage: "/data/user/0/xxx/cache/scan_frame_xxx.jpg" // 当前帧图片路径(开启帧图片回调时返回)
}
条码扫描结果
当仅识别到条码时,results 数组包含 barcode 项,顶层兼容字段取第一个条码的值:
{
code: 200,
msg: "识别成功",
results: [
{ type: "barcode", format: "QR_CODE", rawValue: "扫码内容", valueType: 0 }
],
rawValue: "扫码内容",
format: "QR_CODE", // QR_CODE/EAN_13/CODE_128 等
valueType: 0,
frameImage: "/data/user/0/xxx/cache/scan_frame_xxx.jpg"
}
OCR 文本识别结果
当仅识别到文字时,results 数组包含 text 项,顶层兼容字段取该文本的值:
{
code: 200,
msg: "识别成功",
results: [
{ type: "text", format: "TEXT", rawValue: "原始识别文本", phoneNumbers: [...], idCardNumbers: [...] }
],
rawValue: "原始识别文本",
format: "TEXT",
valueType: 0,
ocrText: "原始识别文本", // 原始 OCR 识别文本(完整识别结果)
phoneNumbers: ["***"], // 从 OCR 文本中提取的手机号
idCardNumbers: [], // 从 OCR 文本中提取的身份证号
frameImage: "/data/user/0/xxx/cache/scan_frame_xxx.jpg"
}
组件属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| 尺寸 | |||
| width | Number | - | 扫描器宽度(像素),必填 |
| height | Number | - | 扫描器高度(像素),必填 |
| 扫描框 | |||
| scanFrameWidthRatio | Number | 0.7 | 扫描框宽度占预览宽度的比例(0.0~1.0) |
| scanFrameHeightRatio | Number | 0.5 | 扫描框高度占预览高度的比例(0.0~1.0) |
| scanFrameWidthPx | Number | 0 | 扫描框宽度(像素),>0 时优先于比例 |
| scanFrameHeightPx | Number | 0 | 扫描框高度(像素),>0 时优先于比例 |
| scanFramePosition | Number | 0.5 | 扫描框竖直排版位置(0.0=顶部,0.5=居中,1.0=底部),扫描框/扫描线/提示文案整体跟随,识别带宽自动跟随 |
| ocrLineBandRatio | Number | - | 识别带宽半宽比例(0.01=±1%,0.12=±12%,1.0=不限制);传入后 overlay 绘制带宽上下边界线(与扫描框左右边框围成矩形),运行时经 setOcrLineBandRatio 调整同步跟随 |
| 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 | 是否开启闪光灯 |
| cameraId | Number | 0 | 摄像头 ID(参数保留,当前版本始终使用后置摄像头) |
| showResult | Boolean | true | 是否显示扫描结果 |
| vibrate | Boolean | false | 震动反馈(接口保留,当前版本震动功能已禁用,传入不生效) |
组件事件
scanResult - 扫描成功
@scanResult="onScanResult"
function onScanResult(result) {
console.log("扫描内容:", result.rawValue);
console.log("条码格式:", result.format); // QR_CODE, EAN_13, TEXT 等
console.log("值类型:", result.valueType);
console.log("原始OCR:", result.ocrText);
console.log("帧图片:", result.frameImage);
// results:同帧识别到的所有条码和文字结果
console.log("同帧结果:", result.results);
}
返回数据结构:
{
rawValue: string, // 扫描内容
format: string, // 条码格式
valueType: number, // 值类型
ocrText: string, // 原始 OCR 识别文本
frameImage: string, // 当前帧图片路径(开启帧图片回调时返回)
phoneNumbers: string[], // 提取的手机号数组(OCR时)
idCardNumbers: string[], // 提取的身份证号数组(OCR时)
results: UTSJSONObject[] // 同帧识别的所有条码和文字结果
}
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,
ocrText: "原始识别文本",
phoneNumbers: ["***", "***"], // 提取到的手机号数组
idCardNumbers: ["110101199001011234"], // 提取到的身份证号数组
frameImage: "/data/user/0/xxx/cache/scan_frame_xxx.jpg"
}
支持的条码格式
| 格式 | 说明 |
|---|---|
| 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条形码 |
通过 setBarcodeFormats(['QR_CODE', 'EAN_13', 'CODE_128']) 可限定识别的条码格式(传入空数组恢复识别全部格式)。
铺底模式(预览铺满全屏 + 控件悬浮上层)
适用于"相机预览铺满屏幕、页面控件悬浮在预览上层作蒙层"的场景(与 uniapp 版插件的 bindViewBelowPage 铺底模式效果一致)。
uni-app x 是全原生渲染,视图层级按元素树顺序叠加,无需原生层级插入,直接用布局即可实现:
<template>
<view class="page">
<!-- 1. 扫描组件绝对定位铺满全屏(铺底) -->
<native-scannerpreview
class="full-preview"
:width="screenWidth"
:height="screenHeight"
:scanFramePosition="0.15"
:showOverlay="true"
:showResult="false"
></native-scannerpreview>
<!-- 2. 页面控件写在组件之后,自然悬浮在预览上层 -->
<view class="float-panel">
<button size="mini" @tap="toggleTorch">闪光灯</button>
</view>
</view>
</template>
<style>
.page {
position: relative;
width: 100%;
height: 100%;
}
.full-preview {
position: absolute;
top: 0;
left: 0;
}
.float-panel {
position: absolute;
left: 0;
right: 0;
bottom: 30px;
}
</style>
说明:
- 铺底模式下组件
width/height传屏幕尺寸(uni.getSystemInfoSync()的windowWidth/windowHeight),相机预览即铺满全屏 - 通过
scanFramePosition属性(或setScanFramePosition方法)把扫描框放到实际可用的干净预览区,例如顶部留出控件区时设0.1~0.2 - 页面根元素建议保持透明背景
background-color: rgba(0,0,0,0),相机未启动时组件自身显示黑色底色 - 悬浮控件需放在组件之后(或显式设置更高的
z-index),否则会被预览遮挡
注意事项
- 必须制作自定义基座:UTS 插件涉及原生 Kotlin 代码,标准基座无法运行。
- 页面生命周期:
onHide时停止扫描,onUnload时销毁扫描器并调用clearFrameImageCache(),避免后台占用相机、缓存无限堆积。 - 仅支持后置摄像头:
start的cameraId参数当前保留,插件始终使用后置摄像头。 - 帧图片缓存清理:开启
setEnableFrameImageCallback(true)后,每次扫描成功都会在cacheDir下生成一个scan_frame_*.jpg且不会自动删除。建议在onUnload或切换"帧图"开关关闭时调用clearFrameImageCache(),避免缓存无限堆积。 - 震动反馈已禁用:
ScanFeedbackOptions的vibrate字段接口保留,但当前版本震动功能已禁用,传入不生效,仅playSound/autoStop生效。 - 扫描线带宽钳制:
setOcrLineBandRatio传入值低于0.01会被钳制为0.01,有效范围0.01~1.0。 maskOpacity:0= 完全透明(无遮罩),0.5= 半透明黑(默认),1= 完全不透明。scanFramePosition钳制:传入值超出[0, 1]会被钳制,识别带宽中心自动跟随扫描框排版位置。- 帧图片显示:
frameImage返回的是本地绝对路径,在 uni-app x 的<image>中显示需加file://前缀。
完整 Demo
完整Demo示例(scanner-demo-unix.uvue)
<template>
<view class="container">
<!-- 扫描器组件铺底:绝对定位铺满全屏(对应 nvue 版 bindViewBelowPage 铺底模式),
后续兄弟元素绘制在预览上层作悬浮蒙层 -->
<native-scannerpreview
id="scannerPreview"
class="full-preview"
:width="scannerWidth"
:height="scannerHeight"
:scanFrameWidthRatio="scanFrameWidthRatio"
:scanFrameHeightRatio="scanFrameHeightRatio"
:scanFrameWidthPx="scanFrameWidthPx"
:scanFrameHeightPx="scanFrameHeightPx"
:scanFramePosition="scanFramePosition"
:scanLineColor="scanLineColor"
:scanLineHeight="4"
:frameBorderColor="frameBorderColor"
:hintText="hintText"
:hintColor="hintColor"
:maskOpacity="0"
:playSound="playSound"
:showOverlay="true"
:showScanLine="showScanLine"
:showResult="false"
></native-scannerpreview>
<!-- 顶部27%纯净预览区(扫描窗):不放任何控件,相机画面/扫描线/识别带宽完整清晰露出 -->
<view class="top-gap" :style="{ height: topGapH + 'px' }"></view>
<!-- 控制条(下移到顶部预览区之下,悬浮蒙层:控件绘制在预览上层) -->
<view class="top-bar">
<button :type="torchOn ? 'warn' : 'default'" size="mini" @tap="toggleTorch">
{{ torchOn ? '关闪光' : '开闪光' }}
</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 class="setting-item">
<text class="setting-label">帧图</text>
<switch :checked="enableFrameImage == true" @change="toggleFrameImageCallback" />
</view>
</view>
<!-- 控制面板(可滚动悬浮蒙层,固定在屏幕底部,防止小屏显示不全) -->
<scroll-view scroll-y class="bottom-scroll">
<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="band-section">
<view class="band-header">
<text class="section-title">扫描线带宽</text>
<text class="band-value">{{ (ocrLineBandRatio * 100).toFixed(0) }}%</text>
</view>
<slider class="band-slider" :min="1" :max="100" :value="ocrLineBandRatio * 100" @changing="onBandRatioChanging" @change="onBandRatioChange" />
</view>
<!-- 扫描框排版控制(尺寸 + 竖直位置,位置即 setScanFramePosition) -->
<view class="frame-section">
<view class="frame-header">
<text class="section-title">扫描框排版</text>
<text class="frame-value">{{ scanFrameWidthPx }} × {{ scanFrameHeightPx }} px</text>
</view>
<view class="frame-row">
<text class="frame-label">宽</text>
<slider class="frame-slider" :min="80" :max="maxFrameWidth" :value="scanFrameWidthPx" @change="onFrameWidthChange" />
</view>
<view class="frame-row">
<text class="frame-label">高</text>
<slider class="frame-slider" :min="80" :max="maxFrameHeight" :value="scanFrameHeightPx" @change="onFrameHeightChange" />
</view>
<view class="frame-row">
<text class="frame-label">位置</text>
<slider class="frame-slider" :min="0" :max="100" :value="scanFramePosition * 100" @changing="onFramePositionChanging" @change="onFramePositionChange" />
</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 class="result-section" v-if="hasScanResult">
<view class="result-header">
<text class="section-title">扫描结果</text>
<view class="result-actions">
<button size="mini" type="default" @tap="copyResult"><text>复制</text></button>
<button size="mini" type="default" @tap="clearResult"><text>清除</text></button>
</view>
</view>
<text class="result-value">{{ scanResult }}</text>
<text class="result-format">格式: {{ scanFormat }}</text>
<text class="result-ocr" v-if="ocrText != ''">原始OCR: {{ ocrText }}</text>
<view class="result-list" v-if="resultLines.length > 0">
<text class="section-subtitle">同帧识别结果 ({{ resultLines.length }})</text>
<text v-for="(line, index) in resultLines" :key="index" class="result-item">{{ line }}</text>
</view>
</view>
<!-- 帧图片回调控制 + 当前帧图片预览 -->
<view class="frame-image-section">
<view class="frame-image-header">
<text class="section-title">帧图片</text>
<button size="mini" type="default" @tap="clearFrameCache"><text>清理缓存</text></button>
</view>
<view v-if="frameImage != ''">
<text class="frame-path">{{ frameImage }}</text>
<image class="frame-preview" :src="frameImageSrc()" mode="aspectFit" />
</view>
<text class="frame-hint" v-else>开启"帧图"开关后,扫描成功将回传当前完整帧图片</text>
</view>
</view>
</scroll-view>
</view>
</template>
<script lang="uts">
import {
CreateNativeScannerContext,
INativeScannerContext,
OnCallBackOptions,
ScannerStartOptions,
ScanViewOptions,
ScanLineStyle,
ScanFeedbackOptions
} from "@/uni_modules/yuange-scanner";
import ArrayList from 'java.util.ArrayList';
let context: INativeScannerContext | null = null;
let isContextInitialized = false;
export default {
data() {
return {
// 扫描器组件尺寸(px;铺底模式下 onLoad 直接按全屏赋值)
scannerWidth: 0,
scannerHeight: 0,
// 顶部纯净预览区高度(onLoad 按可视区高度的 27% 换算,控制条下移不遮挡扫描线与识别带宽)
topGapH: 0,
torchOn: false,
currentZoom: 1.0,
maxZoom: 10.0,
isScanning: false,
// 扫描结果
scanResult: '',
scanFormat: '',
ocrText: '',
frameImage: '',
resultLines: [] as string[],
hasScanResult: false,
// 扫描框配置
scanFrameWidthRatio: 0.7,
scanFrameHeightRatio: 0.5,
// 扫描框绝对像素尺寸(>0 时优先于比例);onReady 按预览区较短边 70% 赋初值
scanFrameWidthPx: 0,
scanFrameHeightPx: 0,
// 扫描框竖直排版位置(0=顶部,0.5=居中,1=底部;识别带宽自动跟随框中心)
// 默认 0.25:扫描框落在顶部纯净预览区内(与 demo2 一致,可用滑块调整)
scanFramePosition: 0.1,
maxFrameWidth: 1080,
maxFrameHeight: 1000,
scanLineColor: '#00FF00',
frameBorderColor: '#FFFFFF',
hintText: '请将二维码/条形码置于框内',
hintColor: '#FFFFFF',
playSound: false,
showScanLine: true,
// 是否回调当前扫描帧(完整相机画面)的图片,默认 true
enableFrameImage: true,
// 扫描线带宽比例(OCR/条码按线条位置过滤,0.01 = 中线 ±1%,即最小带宽)
ocrLineBandRatio: 0.01,
logs: [] as string[]
}
},
onLoad() {
const sys = uni.getSystemInfoSync();
// 铺底模式:预览铺满全屏(组件绝对定位铺满页面,无需测量占位区)
this.scannerWidth = sys.windowWidth;
this.scannerHeight = sys.windowHeight;
// 顶部纯净预览区 = 可视区高度的 27%(控制条下移到扫描线/识别带宽之下,不再遮挡)
this.topGapH = Math.floor(sys.windowHeight * 0.27);
this.addLog('页面加载完成: 全屏预览 ' + this.scannerWidth + 'x' + this.scannerHeight);
},
onReady() {
this.addLog('页面就绪,开始初始化扫描器...');
// 计算扫描框默认尺寸:取预览区较短边的 70% 作为正方形边长
const w = this.scannerWidth > 0 ? this.scannerWidth : 300;
const h = this.scannerHeight > 0 ? this.scannerHeight : 400;
this.maxFrameWidth = w;
this.maxFrameHeight = h;
const frameSize = Math.floor(Math.min(w, h) * 0.7);
this.scanFrameWidthPx = frameSize;
this.scanFrameHeightPx = frameSize;
// 延迟初始化上下文,确保 DOM 已经完全准备好
setTimeout(() => {
this.initializeContext();
}, 300);
},
onUnload() {
this.stopScanner();
// 清理帧图片缓存,避免 cacheDir 下 scan_frame_*.jpg 堆积
if (context != null) {
try {
context?.clearFrameImageCache();
} catch (err) {}
context?.destroy();
context = null;
isContextInitialized = false;
}
this.addLog('扫描器已销毁');
},
onHide() {
this.stopScanner();
},
methods: {
// ===== 初始化 =====
initializeContext() {
if (isContextInitialized && context != null) {
this.addLog('上下文已初始化');
return;
}
this.addLog('尝试初始化扫描器上下文...');
// 第一次尝试
if (this.tryCreateContext()) {
return;
}
// 失败重试(延迟等待组件渲染)
this.addLog('首次初始化失败,500ms 后重试...');
setTimeout(() => {
if (this.tryCreateContext()) {
return;
}
this.addLog('第二次初始化失败,1000ms 后再次重试...');
setTimeout(() => {
if (!this.tryCreateContext()) {
this.addLog('扫描器初始化失败,请检查组件是否正确渲染');
}
}, 1000);
}, 500);
},
tryCreateContext(): boolean {
try {
context = CreateNativeScannerContext("scannerPreview", this);
if (context != null) {
this.setupContext();
isContextInitialized = true;
this.addLog('扫描器初始化成功');
return true;
} else {
this.addLog('上下文创建失败,可能组件未准备好');
return false;
}
} catch (e: Exception) {
this.addLog('初始化异常: ' + e.message);
return false;
}
},
// 初始化扫描配置与回调
setupContext() {
if (context == null) return;
// 扫描视图配置
try {
context?.setScanViewOptions({
scanFrameWidth: this.scanFrameWidthRatio.toFloat(),
scanFrameHeight: this.scanFrameHeightRatio.toFloat(),
scanFrameWidthPx: this.scanFrameWidthPx.toInt(),
scanFrameHeightPx: this.scanFrameHeightPx.toInt(),
frameBorderColor: this.frameBorderColor,
frameBorderWidth: 2,
scanLineStyle: {
color: this.scanLineColor,
width: 2,
height: 4
} as ScanLineStyle,
hintText: this.hintText,
hintColor: this.hintColor,
hintTextSize: 14,
maskOpacity: 0.0.toFloat(),
maskColor: '#000000'
} as ScanViewOptions);
} catch (err) {
this.addLog('设置扫描视图配置失败(非致命): ' + (err as Exception).message);
}
// 扫描线带宽(OCR/条码按线条位置过滤)
try {
context?.setOcrLineBandRatio(this.ocrLineBandRatio.toFloat());
} catch (err) {
this.addLog('设置带宽失败(非致命): ' + (err as Exception).message);
}
// 扫描框竖直排版位置(0=顶部,0.5=居中,1=底部;识别带宽自动跟随框中心)
try {
context?.setScanFramePosition(this.scanFramePosition.toFloat());
} catch (err) {
this.addLog('设置扫描框位置失败(非致命): ' + (err as Exception).message);
}
// 反馈配置
try {
context?.setScanFeedbackOptions({
playSound: this.playSound,
autoStop: false
} as ScanFeedbackOptions);
} catch (err) {
this.addLog('设置反馈配置失败(非致命): ' + (err as Exception).message);
}
// 帧图片回调(完整相机画面,不裁剪)
try {
context?.setEnableFrameImageCallback(this.enableFrameImage);
} catch (err) {
this.addLog('设置帧图片回调失败(非致命): ' + (err as Exception).message);
}
// 扫描结果回调
this.setupScanResultCallback();
// 获取最大缩放
this.loadMaxZoom();
},
// ===== 扫描结果回调 =====
setupScanResultCallback() {
if (context == null) return;
context?.setScanResultCallback({
success: (res: HashMap<String, any>) => {
const rawValue = (res.get("rawValue") as string | null) ?? '';
const format = (res.get("format") as string | null) ?? '';
const ocrText = (res.get("ocrText") as string | null) ?? '';
const frameImage = (res.get("frameImage") as string | null) ?? '';
this.scanResult = rawValue;
this.scanFormat = format;
this.ocrText = ocrText;
this.hasScanResult = true;
this.addLog('扫描成功: ' + format);
this.addLog('内容: ' + rawValue);
if (ocrText != '') {
this.addLog('原始OCR: ' + ocrText);
}
if (frameImage != '') {
this.frameImage = frameImage;
this.addLog('帧图片: ' + frameImage);
}
// 遍历 results 数组(同帧识别的所有条码 + 文字结果)
const resultLines: string[] = [];
const resultsRaw = res.get("results");
if (resultsRaw != null) {
try {
const arr = resultsRaw as ArrayList<any>;
if (arr.size > 1) {
this.addLog('共识别到 ' + arr.size + ' 个结果:');
}
for (let i: Int = 0; i < arr.size; i++) {
const item = arr.get(i) as HashMap<String, any>;
const rType = (item.get("type") as string | null) ?? '';
const rFmt = (item.get("format") as string | null) ?? '';
const rVal = (item.get("rawValue") as string | null) ?? '';
if (rType == 'barcode') {
resultLines.push('[条码] ' + rFmt + ': ' + rVal);
if (arr.size > 1) {
this.addLog(' [条码] ' + rFmt + ': ' + rVal);
}
} else if (rType == 'text') {
resultLines.push('[文字]: ' + rVal);
if (arr.size > 1) {
this.addLog(' [文字]: ' + rVal);
}
}
}
} catch (e: Exception) {
this.addLog('解析results失败: ' + e.message);
}
}
this.resultLines = resultLines;
uni.showToast({ title: '扫描成功', icon: 'success', duration: 1500 });
uni.setClipboardData({ data: rawValue, showToast: false });
},
fail: (err: HashMap<String, any>) => {
const msg = (err.get("msg") as string | null) ?? '';
this.addLog('错误: ' + msg);
}
} as OnCallBackOptions);
},
// ===== 缩放 =====
loadMaxZoom() {
if (context == null) return;
context?.getMaxZoom({
success: (res: HashMap<String, any>) => {
const zoom = (res.get("maxZoom") as number | null) ?? 10.0;
this.maxZoom = zoom;
this.addLog('最大缩放: ' + zoom.toFixed(0) + 'x');
},
fail: (err: HashMap<String, any>) => {
this.addLog('获取最大缩放失败,使用默认值');
}
} as OnCallBackOptions);
},
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>) => {
const msg = (err.get("msg") as string | null) ?? '';
this.addLog('设置缩放失败: ' + msg);
}
} as OnCallBackOptions);
}
this.addLog('设置缩放: ' + zoom.toFixed(1) + 'x');
},
// ===== 启动/停止 =====
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 | null) ?? '';
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>) => {},
fail: (err: HashMap<String, any>) => {
const msg = (err.get("msg") as string | null) ?? '';
this.addLog('设置闪光灯失败: ' + msg);
}
} as OnCallBackOptions);
}
this.addLog('闪光灯: ' + (this.torchOn ? '开启' : '关闭'));
},
// ===== 反馈配置 =====
togglePlaySound() {
this.playSound = !this.playSound;
if (context != null) {
context?.setScanFeedbackOptions({
playSound: this.playSound,
autoStop: false
} as ScanFeedbackOptions);
}
this.addLog('提示音: ' + (this.playSound ? '开启' : '关闭'));
},
// ===== 帧图片回调 =====
toggleFrameImageCallback() {
this.enableFrameImage = !this.enableFrameImage;
if (context != null) {
context?.setEnableFrameImageCallback(this.enableFrameImage);
}
this.addLog('帧图片回调: ' + (this.enableFrameImage ? '开启' : '关闭'));
// 关闭时清空当前预览,避免显示过期路径
if (!this.enableFrameImage) {
this.frameImage = '';
}
},
// 清理 cacheDir 下所有 scan_frame_*.jpg 缓存文件
clearFrameCache() {
if (context == null) {
this.addLog('扫描器不可用');
return;
}
try {
context?.clearFrameImageCache();
this.frameImage = '';
this.addLog('已清理帧图片缓存');
} catch (err) {
this.addLog('清理缓存异常');
}
},
// ===== 扫描线带宽 =====
// 拖动过程中实时更新带宽(下一帧扫描即按新带宽过滤),不写日志避免刷屏
onBandRatioChanging(e: UniSliderChangeEvent) {
const ratio = e.detail.value / 100.0;
this.ocrLineBandRatio = ratio;
if (context != null) {
context?.setOcrLineBandRatio(ratio.toFloat());
}
},
// 松手后记录最终值
onBandRatioChange(e: UniSliderChangeEvent) {
const ratio = e.detail.value / 100.0;
this.ocrLineBandRatio = ratio;
if (context != null) {
context?.setOcrLineBandRatio(ratio.toFloat());
}
this.addLog('扫描线带宽: ' + (ratio * 100).toFixed(0) + '%');
},
// ===== 扫描框尺寸 =====
// 拖动实时移动扫描框(组件 overlay 由 scanFramePosition prop 响应式更新,原生带宽中心同步跟随)
onFramePositionChanging(e: UniSliderChangeEvent) {
const ratio = e.detail.value / 100.0;
this.scanFramePosition = ratio;
if (context != null) {
context?.setScanFramePosition(ratio.toFloat());
}
},
// 松手后记录最终值
onFramePositionChange(e: UniSliderChangeEvent) {
const ratio = e.detail.value / 100.0;
this.scanFramePosition = ratio;
if (context != null) {
context?.setScanFramePosition(ratio.toFloat());
}
this.addLog('扫描框位置: ' + (ratio * 100).toFixed(0) + '%');
},
onFrameWidthChange(e: UniSliderChangeEvent) {
this.scanFrameWidthPx = Math.floor(e.detail.value);
this.applyScanViewOptions();
this.addLog('扫描框宽度: ' + this.scanFrameWidthPx + 'px');
},
onFrameHeightChange(e: UniSliderChangeEvent) {
this.scanFrameHeightPx = Math.floor(e.detail.value);
this.applyScanViewOptions();
this.addLog('扫描框高度: ' + this.scanFrameHeightPx + 'px');
},
resetFrameSize() {
const frameSize = Math.floor(Math.min(this.maxFrameWidth, this.maxFrameHeight) * 0.7);
this.scanFrameWidthPx = frameSize;
this.scanFrameHeightPx = frameSize;
this.applyScanViewOptions();
this.addLog('扫描框已重置: ' + this.scanFrameWidthPx + 'x' + this.scanFrameHeightPx);
},
// 运行时更新扫描视图配置(扫描框尺寸即时生效)
applyScanViewOptions() {
if (context == null) return;
try {
context?.setScanViewOptions({
scanFrameWidth: this.scanFrameWidthRatio.toFloat(),
scanFrameHeight: this.scanFrameHeightRatio.toFloat(),
scanFrameWidthPx: this.scanFrameWidthPx.toInt(),
scanFrameHeightPx: this.scanFrameHeightPx.toInt(),
frameBorderColor: this.frameBorderColor,
frameBorderWidth: 2,
scanLineStyle: {
color: this.scanLineColor,
width: 2,
height: 4
} as ScanLineStyle,
hintText: this.hintText,
hintColor: this.hintColor,
hintTextSize: 14,
maskOpacity: 0.0.toFloat(),
maskColor: '#000000'
} as ScanViewOptions);
} catch (err) {
this.addLog('设置扫描视图配置失败(非致命)');
}
},
// ===== 结果操作 =====
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.ocrText = '';
this.resultLines = [] as string[];
this.hasScanResult = false;
this.addLog('清除扫描结果');
},
// 帧图片显示路径:本地绝对路径需加 file:// 前缀
frameImageSrc(): string {
if (this.frameImage == '') return '';
if (this.frameImage.startsWith('file://')) return this.frameImage;
return 'file://' + this.frameImage;
},
// ===== 日志 =====
formatTwoDigits(n: number): string {
const s = Math.floor(n).toString();
return s.length < 2 ? '0' + s : s;
},
addLog(msg: string) {
const now = new Date();
const time = this.formatTwoDigits(now.getHours()) + ':' + this.formatTwoDigits(now.getMinutes()) + ':' + this.formatTwoDigits(now.getSeconds());
this.logs.unshift('[' + time + '] ' + msg);
if (this.logs.length > 50) {
this.logs.pop();
}
console.log('[' + time + '] ' + msg);
},
clearLogs() {
this.logs = [] as string[];
}
}
}
</script>
<style>
/* 容器必须透明,铺底的相机预览组件才能露出(组件自身黑色底) */
.container { background-color: rgba(0,0,0,0); flex: 1; }
/* 扫描组件绝对定位铺满全屏(铺底),写在元素树最前 = 最底层,后续兄弟控件绘制在其上层 */
.full-preview { position: absolute; top: 0; left: 0; }
/* 顶部纯净预览区与中部弹性区均透明,完整透出预览画面 */
.top-gap { background-color: rgba(0,0,0,0); }
.mid-gap { flex: 1; background-color: rgba(0,0,0,0); }
.top-bar { flex-direction: row; align-items: center;padding: 16rpx 20rpx; background-color: rgba(0,0,0,0.45); margin-top: 50rpx}
.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; }
.band-section { background-color: #2c2c2c; padding: 16rpx; border-radius: 8rpx; margin-bottom: 16rpx; }
.band-header { flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 8rpx; }
.band-value { color: #00FF00; font-size: 24rpx; }
.band-slider { flex: 1; }
.frame-section { background-color: #2c2c2c; padding: 16rpx; border-radius: 8rpx; margin-bottom: 16rpx; }
.frame-header { flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 8rpx; }
.frame-value { color: #00FF00; font-size: 24rpx; }
.frame-row { flex-direction: row; align-items: center; margin-bottom: 8rpx; }
.frame-label { color: #aaaaaa; font-size: 22rpx; width: 50rpx; }
.frame-slider { flex: 1; }
.bottom-scroll { height: 1000rpx; }
.bottom-panel { background-color: rgba(0,0,0,0.55); padding: 16rpx 20rpx 48rpx 20rpx; }
.action-row { flex-direction: row; justify-content: space-around; margin-bottom: 16rpx; }
.result-section { background-color: #2c2c2c; padding: 16rpx; border-radius: 8rpx; margin-bottom: 16rpx; }
.result-header { flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 8rpx; }
.result-actions { flex-direction: row; }
.result-value { font-size: 26rpx; color: #00FF00; margin-bottom: 8rpx; }
.result-format { font-size: 24rpx; color: #aaaaaa; margin-bottom: 8rpx; }
.result-ocr { font-size: 24rpx; color: #dddddd; margin-bottom: 8rpx; }
.result-list { margin-top: 8rpx; }
.result-item { font-size: 22rpx; color: #bbbbbb; line-height: 36rpx; }
.frame-image-section { background-color: #2c2c2c; padding: 16rpx; border-radius: 8rpx; margin-bottom: 16rpx; }
.frame-image-header { flex-direction: row; justify-content: space-between; align-items: center; margin-bottom: 8rpx; }
.frame-path { font-size: 20rpx; color: #aaaaaa; margin-bottom: 8rpx; }
.frame-preview { width: 100%; height: 200rpx; border-radius: 4rpx; background-color: #000000; }
.frame-hint { font-size: 22rpx; color: #777777; margin-top: 8rpx; }
.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; }
.section-subtitle { font-size: 22rpx; font-weight: bold; color: #cccccc; margin-bottom: 4rpx; }
.log-scroll { height: 300rpx; }
.log-item { font-size: 20rpx; color: #aaaaaa; line-height: 36rpx; }
</style>

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