更新记录

1.0.1(2026-07-31)

  • 优化
  • 更新文档

1.0.0(2026-07-31)

  • 新版发布

平台兼容性

uni-app(5.01)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× × × × 5.0 × ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

uni-app x(5.01)

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

yt-usbscan

Android USB / 蓝牙 HID 扫码枪后台监听插件(UTS)。

兼容 uni-app 与 uni-app x。无需输入框、无需焦点、无需弹出软键盘。

特别提醒

  • 购买本插件前,请先试用,请先试用,请先试用,确认满足需求之后再行购买。虚拟物品一旦购买之后无法退款。
  • 如有使用上的疑问、bug,可以进交流群联系作者;
  • 请在合法范围内使用,若使用本插件做非法开发,本方概不负责;
  • 插件需先引入再打自定义基座后运行测试

平台

项目类型 Android iOS Harmony
uni-app(vue/nvue) - -
uni-app x - -

建议 HBuilderX 4.27+(原生混编 Kotlin;@UTSJS.keepAlive 需 4.27+)。

API

setOnScanListener(options)

启动后台静默扫码监听。

字段 类型 说明
interCharTimeoutMs number? 字符间隔阈值(ms),默认 100。详见下方说明
finishTimeoutMs number? 无结束符时的静默提交超时(ms),默认 120;传 0 关闭。详见下方说明
onScanSuccess (res) => void 成功回调,res.code 为扫码内容
fail (err) => void 失败回调
complete (res) => void 结束回调

removeOnScanListener()

停止监听并恢复原始 Window.Callback。

isScanListening()

是否正在监听。

拼码超时参数说明

扫码枪在 HID 键盘模式下,会把条码模拟成一串极快的按键。插件用缓冲拼码,依赖下面两个时间参数区分「同一次扫码 / 下一次扫码 / 是否该提交」。

interCharTimeoutMs(字符间隔阈值)

作用: 两次按键间隔若超过该值,丢弃当前缓冲,当作新一次扫码开始。用来区分扫码枪的极速输入和人手慢速打字,也避免半截旧码与下一次扫码粘在一起。

怎么设:

场景 建议值 说明
多数有线 USB 扫码枪 100(默认) 一般无需改
枪很快、码很短 5080 更严格,更不易把慢打字当成扫码
蓝牙 / 无线枪、偶发卡顿 150250 间隔偶发变大时,避免一次扫码被拆成两段
同屏还有实体键盘输入 保持偏小(如 80100 降低把人手输入拼进条码的概率

设错了会怎样:

  • 过大(如 500):人手慢打、误触也可能被拼进缓冲,出现「乱码 / 把键盘输入当扫码」
  • 过小(如 20):无线延迟或个别慢枪会导致一次扫码被拆成多次短结果,或中间被清空丢码

finishTimeoutMs(无结束符时的静默提交)

作用: 每追加一个有效字符后重置定时器;若在超时内没有新字符、也没收到 Enter(或 \n/\r),就把当前缓冲当作一次完整扫码提交。用于兼容未配置扫码后缀 Enter 的设备。传 0 关闭该逻辑,只认结束符才提交。

怎么设:

场景 建议值 说明
扫码枪已开启后缀 Enter(推荐) 120(默认)或 0 有 Enter 时主要靠结束符提交;0 可彻底关掉静默提交
枪未开 Enter / 无法改配置 100200 必须靠静默超时出结果;宜略大于枪的字符间隔
只要「绝对完整、宁可不出」 0 强制要求设备发 Enter,否则永不自动提交

设错了会怎样:

  • 过大(如 800):无 Enter 时结果出来很慢;若用户紧接着再扫一次,还可能把两次码拼在一起
  • 过小(如 30):枪还没打完整个条码就提前提交 → 结果被截断
  • 在未开 Enter 时设为 0:永远等不到结束符 → 一直无回调

两个参数的关系(简记)

参数 超时后做什么
interCharTimeoutMs 间隔太大 → 丢弃旧缓冲,准备接新扫码
finishTimeoutMs 静默一段时间 → 提交当前缓冲

推荐优先在扫码枪上开启 Enter 后缀,超时参数保持默认即可。仅当出现「拆码 / 粘码 / 截断 / 无回调」时,再按上表微调。

更多好用插件推荐


完整示例

以下为 Demo 页完整源码,可直接复制到对应项目使用。

uni-app x(pages/index/index.uvue

<template>
    <view class="page">
        <text class="title">USB 扫码枪监听</text>
        <text class="hint">无需输入框 / 无需焦点</text>
        <text class="hint">请直接用扫码枪扫描</text>

        <view class="actions">
            <button type="primary" :disabled="listening" @click="startListen">开始监听</button>
            <button type="warn" :disabled="!listening" style="margin-top: 16rpx;" @click="stopListen">停止监听</button>
        </view>

        <text class="label">监听状态:{{ listening ? '已开启' : '未开启' }}</text>
        <text class="label">最近一次结果</text>
        <text class="result">{{ lastCode }}</text>

        <view class="divider"></view>
        <text class="label">历史记录({{ history.length }})</text>
        <scroll-view class="history" scroll-y="true">
            <text class="history-item" v-for="(item, index) in history" :key="index">
                {{ index + 1 }}. {{ item }}
            </text>
        </scroll-view>
    </view>
</template>

<script lang="uts">
    import {
        setOnScanListener,
        removeOnScanListener,
        isScanListening,
        SetOnScanListenerOptions,
        ScanSuccessResult,
        UsbScanFail
    } from '@/uni_modules/yt-usbscan'

    export default {
        data() {
            return {
                lastCode: '(等待扫码…)',
                listening: false,
                history: [] as string[]
            }
        },
        onLoad() {
            // 进入页面自动开启后台监听(等价插件 setOnScanListener)
            this.startListen()
        },
        onUnload() {
            this.stopListen()
        },
        methods: {
            startListen() {
                const that = this
                const options : SetOnScanListenerOptions = {
                    onScanSuccess: (res : ScanSuccessResult) => {
                        console.log('onScanSuccess --->', res.code)
                        that.lastCode = res.code
                        const list = that.history.slice()
                        list.unshift(res.code)
                        that.history = list
                        uni.showToast({
                            title: res.code,
                            icon: 'none'
                        })
                    },
                    fail: (err : UsbScanFail) => {
                        console.error('usbscan fail', err.errCode, err.errMsg)
                        uni.showToast({
                            title: err.errMsg,
                            icon: 'none'
                        })
                    },
                    complete: (_ : any) => {
                        that.listening = isScanListening()
                    }
                }
                setOnScanListener(options)
            },
            stopListen() {
                removeOnScanListener()
                this.listening = false
            }
        }
    }
</script>

<style>
    .page {
        flex: 1;
        padding: 10rpx;
        background-color: #ffffff;
    }

    .title {
        font-size: 32rpx;
        font-weight: bold;
        color: #111111;
        text-align: center;
    }

    .hint {
        margin-top: 8rpx;
        font-size: 28rpx;
        color: #757575;
        text-align: center;
    }

    .actions {
        margin-top: 48rpx;
    }

    .label {
        margin-top: 20rpx;
        font-size: 28rpx;
        color: #6200EE;
        font-weight: bold;
    }

    .result {
        margin-top: 16rpx;
        font-size: 36rpx;
        color: #111111;
        text-align: center;
    }

    .divider {
        margin-top: 40rpx;
        margin-bottom: 8rpx;
        height: 1px;
        background-color: #E0E0E0;
    }

    .history {
        flex: 1;
        margin-top: 16rpx;
    }

    .history-item {
        font-size: 28rpx;
        color: #333333;
        margin-bottom: 12rpx;
    }
</style>

uni-app(pages/index/index.vue

<template>
    <view class="page">
        <text class="title">USB 扫码枪监听</text>
        <text class="hint">无需输入框 / 无需焦点</text>
        <text class="hint">请直接用扫码枪扫描</text>

        <view class="actions">
            <button type="primary" :disabled="listening" @click="startListen">开始监听</button>
            <button type="warn" :disabled="!listening" style="margin-top: 16rpx;" @click="stopListen">停止监听</button>
        </view>

        <text class="label">监听状态:{{ listening ? '已开启' : '未开启' }}</text>
        <text class="label">最近一次结果</text>
        <text class="result">{{ lastCode }}</text>

        <view class="divider"></view>
        <text class="label">历史记录({{ history.length }})</text>
        <scroll-view class="history" scroll-y="true">
            <text class="history-item" v-for="(item, index) in history" :key="index">
                {{ index + 1 }}. {{ item }}
            </text>
        </scroll-view>
    </view>
</template>

<script lang="uts">
    import {
        setOnScanListener,
        removeOnScanListener,
        isScanListening,
        SetOnScanListenerOptions,
        ScanSuccessResult,
        UsbScanFail
    } from '@/uni_modules/yt-usbscan'

    export default {
        data() {
            return {
                lastCode: '(等待扫码…)',
                listening: false,
                history: []
            }
        },
        onLoad() {
            // 进入页面自动开启后台监听(等价插件 setOnScanListener)
            this.startListen()
        },
        onUnload() {
            this.stopListen()
        },
        methods: {
            startListen() {
                const that = this
                const options : SetOnScanListenerOptions = {
                    onScanSuccess: (res) => {
                        console.log('onScanSuccess --->', res.code)
                        that.lastCode = res.code
                        const list = that.history.slice()
                        list.unshift(res.code)
                        that.history = list
                        uni.showToast({
                            title: res.code,
                            icon: 'none'
                        })
                    },
                    fail: (err) => {
                        console.error('usbscan fail', err['errCode'], err['errMsg'])
                        uni.showToast({
                            title: err['errMsg'],
                            icon: 'none'
                        })
                    },
                    complete: (_) => {
                        that.listening = isScanListening()
                    }
                }
                setOnScanListener(options)
            },
            stopListen() {
                removeOnScanListener()
                this.listening = false
            }
        }
    }
</script>

<style>
    .page {
        flex: 1;
        padding: 10rpx;
        background-color: #ffffff;
    }

    .title {
        font-size: 32rpx;
        font-weight: bold;
        color: #111111;
        text-align: center;
    }

    .hint {
        margin-top: 8rpx;
        font-size: 28rpx;
        color: #757575;
        text-align: center;
    }

    .actions {
        margin-top: 48rpx;
    }

    .label {
        margin-top: 20rpx;
        font-size: 28rpx;
        color: #6200EE;
        font-weight: bold;
    }

    .result {
        margin-top: 16rpx;
        font-size: 36rpx;
        color: #111111;
        text-align: center;
    }

    .divider {
        margin-top: 40rpx;
        margin-bottom: 8rpx;
        height: 1px;
        background-color: #E0E0E0;
    }

    .history {
        flex: 1;
        margin-top: 16rpx;
    }

    .history-item {
        font-size: 28rpx;
        color: #333333;
        margin-bottom: 12rpx;
    }
</style>

注意事项

  1. 扫码枪需设置为 HID / 键盘楔入 / 自感模式
  2. 建议开启扫码后缀 Enter
  3. 二维码 / 条码内容含中文时,可能扫不出来或结果乱码。 本插件按 HID 键盘按键的 Unicode 拼码;中文通常依赖扫码枪输出编码(如 UTF-8 / GBK)及系统输入法状态,很多枪在键盘模式下对中文支持不佳。业务上尽量使用数字、字母等 ASCII 内容;若必须含中文,请先在扫码枪说明书中确认键盘模式下的中文输出能力,或改用厂商私有协议 / 串口方案
  4. 仅在 App 前台有效;页面销毁请调用 removeOnScanListener
  5. 首次使用需制作自定义基座或云打包后真机调试
  6. Android uni-app(非 x)依赖 @UTSJS.keepAlive,否则第二次扫码会报「回调函数已释放」;请勿频繁重复调用 setOnScanListener。修改插件导出方式后请删除 unpackage 再编译,避免旧产物残留

开发文档

隐私、权限声明

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

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

插件不采集任何数据

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