更新记录

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),否则会被预览遮挡

注意事项

  1. 必须制作自定义基座:UTS 插件涉及原生 Kotlin 代码,标准基座无法运行。
  2. 页面生命周期:onHide 时停止扫描,onUnload 时销毁扫描器并调用 clearFrameImageCache(),避免后台占用相机、缓存无限堆积。
  3. 仅支持后置摄像头:start 的 cameraId 参数当前保留,插件始终使用后置摄像头。
  4. 帧图片缓存清理:开启 setEnableFrameImageCallback(true) 后,每次扫描成功都会在 cacheDir 下生成一个 scan_frame_*.jpg 且不会自动删除。建议在 onUnload 或切换"帧图"开关关闭时调用 clearFrameImageCache(),避免缓存无限堆积。
  5. 震动反馈已禁用:ScanFeedbackOptions 的 vibrate 字段接口保留,但当前版本震动功能已禁用,传入不生效,仅 playSound / autoStop 生效。
  6. 扫描线带宽钳制:setOcrLineBandRatio 传入值低于 0.01 会被钳制为 0.01,有效范围 0.01~1.0。
  7. maskOpacity:0 = 完全透明(无遮罩),0.5 = 半透明黑(默认),1 = 完全不透明。
  8. scanFramePosition 钳制:传入值超出 [0, 1] 会被钳制,识别带宽中心自动跟随扫描框排版位置。
  9. 帧图片显示: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>

隐私、权限声明

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

android.permission.CAMERA

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

插件不采集任何数据

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

无

暂无用户评论。