更新记录

1.0.6(2026-05-19)

1.安卓端支持自定义扫码 2.支持鸿蒙端原生扫码 3.增加连续扫码设置

1.0.5(2025-12-15)

支持鸿蒙next

1.0.4(2025-04-22)

安卓端生成码bug修复

查看更多

平台兼容性

uni-app(4.87)

Vue2 Vue3 Vue3插件版本 Chrome Safari app-vue app-nvue Android Android插件版本 iOS 鸿蒙 鸿蒙插件版本
- 1.0.2 - - - - 5.0 1.0.5 - 4.0.0(10) 1.0.5
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
- - - - - - - - - - - -

uni-app x(4.87)

Chrome Safari Android Android插件版本 iOS iOS插件版本 鸿蒙 鸿蒙插件版本 微信小程序
- - 5.0 1.0.5 13 1.0.1 4.0.0(10) 1.0.5 -

xmkj-scan

基于华为 ScanKit 的扫码和生成码 uni-app x 插件,支持 AndroidHarmonyOSiOS(有代码但是没有设备进行测试) 三端,请先试用后在购买。

功能特性

功能 Android HarmonyOS iOS
单次扫码
连续扫码
生成码(二维码/条码)
生成码带 Logo
多码识别 ⚠️

⚠️ iOS 端代码已实现,但无真实设备测试,可自行修改源码调试。 ⚠️ HarmonyOS 端生成码请使用真机测试,不支持模拟器。


安装

  1. 将插件导入到项目的 uni_modules 目录
  2. Android 端需要 自定义基座 后使用(HBuilder X 4.25+)
  3. HarmonyOS 端需 HBuilder X 4.61+,使用 run运行到手机 真机调试

导入

import { scanCode, createCode, stopContinuousScan } from '@/uni_modules/xmkj-scan'

或相对路径:

import { scanCode, createCode, stopContinuousScan } from '../../uni_modules/xmkj-scan'

类型定义

ScanCodeOption — 扫码参数

type ScanCodeOption = {
    /** 扫码成功的回调 */
    success?: (result: string, scanType: string) => void,

    /** 扫码失败的回调 */
    fail?: (errMsg: string) => void,

    /** 扫码结束的回调(无论成功/失败/取消都会触发) */
    complete?: () => void,

    /** 是否只能从相机扫码(true=禁止从相册选图) */
    onlyFromCamera?: boolean,

    /** 扫码模式 */
    scanMode?: string,

    /** 是否使用自定义视图扫码 */
    useCustomizedView?: boolean,

    /** 是否启用连续扫码模式,默认 false */
    continuousScan?: boolean,

    /**
     * 连续扫码模式下,每次扫码结果的回调
     * 返回 false 可停止连续扫码,返回 true 继续
     */
    onResult?: (result: string, scanType: string) => boolean | void,
}

CreateCodeOption — 生成码参数

type CreateCodeOption = {
    /** 要编码的内容 */
    content: string,

    /** 码的类型 */
    type: string,

    /** 码的宽度(像素)
     *  二维码宽高相同需 > 200
     *  条码宽高比 2:1,宽度需 > 400 */
    width: number,

    /** 码的高度(像素) */
    height: number,

    /** Logo 图片的 base64 编码(不含 data:image/... 前缀)
     *  传入后在码中心叠加 Logo,不传则无 Logo */
    logoBase64?: string,

    /** 生成成功的回调 */
    success?: (base64: string) => void,

    /** 生成失败的回调 */
    fail?: (errMsg: string) => void,
}

API 说明

1. scanCode — 扫码

scanCode(options: ScanCodeOption): void

扫码打开系统相机扫描界面,识别条码/二维码。

单次扫码示例:

scanCode({
    success: (result, scanType) => {
        console.log(`扫码结果: ${result}`)
        console.log(`码类型: ${scanType}`)
    },
    fail: (errMsg) => {
        console.error(`扫码失败: ${errMsg}`)
    }
})

参数说明:

参数 类型 必填 说明
success (result, scanType) => void 扫码成功回调,result 为码内容,scanType 为码类型
fail (errMsg) => void 扫码失败回调
complete () => void 扫码结束(无论成功/失败/用户取消)
onlyFromCamera boolean 限制仅从相机扫码,禁止相册选择
continuousScan boolean 开启连续扫码模式
onResult (result, scanType) => boolean \| void 连续扫码每次结果的回调

2. 连续扫码

scanCode({
    continuousScan: true,
    onResult: (result, scanType) => {
        // 每次扫码结果实时回调
        console.log(`第 ${results.length} 条: ${result}`)
        // 返回 false 停止,返回 true 继续
        return true
    },
    success: (result, scanType) => {
        // 每次扫码结果也会走此回调
    },
    complete: () => {
        console.log('连续扫码已停止')
    }
})

// 主动停止连续扫码
stopContinuousScan()

连续扫码流程:

开始 → 打开扫码页 → 扫码成功 → 回调 onResult → 扫码页自动重开 → 继续扫码
                                                                       │
                    用户点 X 关闭扫码页  ←──────────────────────────────┘
                    (自动停止,触发 complete)

                    用户调用 stopContinuousScan()
                    (设置停止标志,当前扫码完成后停止)

关键说明:

行为 效果
onResult 返回 true 或不返回 继续下一轮扫码
onResult 返回 false 停止连续扫码,触发 complete
用户手动关闭扫码页 自动停止,触发 complete
调用 stopContinuousScan() 设置停止标志,当前扫码完成后停止

完整示例:

import { scanCode, stopContinuousScan } from '@/uni_modules/xmkj-scan'

const results: Array<{ content: string, type: string }> = []
let isScanning = false

// 开始连续扫码
function startContinuousScan() {
    results.length = 0
    isScanning = true

    scanCode({
        continuousScan: true,
        onResult: (result, scanType) => {
            results.push({ content: result, type: scanType })
            console.log(`已扫码 ${results.length} 条`)
            return true // 继续扫码
        },
        fail: (errMsg) => {
            isScanning = false
            console.error(`扫码失败: ${errMsg}`)
        },
        complete: () => {
            isScanning = false
            console.log(`扫码结束,共 ${results.length} 条`)
        }
    })
}

// 停止连续扫码
function stopScan() {
    if (isScanning) {
        stopContinuousScan()
        isScanning = false
    }
}

3. createCode — 生成码

createCode(options: CreateCodeOption): void

生成二维码或条码,返回 base64 格式的图片数据。

支持的码类型:

类型 说明 示例场景
QR_CODE 二维码 网址、文本、名片
AZTEC Aztec 码 交通票务
CODABAR Codabar 码 血库、物流
CODE39 Code 39 工业标识
CODE93 Code 93 零售
CODE128 Code 128 物流、供应链
DATAMATRIX Data Matrix 电子元件标识
EAN8 EAN-8 小商品
EAN13 EAN-13 商品条码
ITF14 ITF-14 物流包装
PDF417 PDF417 证件、驾照
UPC_A UPC-A 北美商品
UPC_E UPC-E 北美小商品

基础示例(无 Logo):

createCode({
    content: 'https://www.example.com',
    type: 'QR_CODE',
    width: 300,
    height: 300,
    success: (base64) => {
        // base64 为完整 data:image/bmp;base64,... 格式
        imageSrc.value = base64
    },
    fail: (errMsg) => {
        console.error(`生成失败: ${errMsg}`)
    }
})

4. 生成带 Logo 的二维码

static 目录下放置 qr_logo.png 文件,然后在调用时通过 logoBase64 参数传入 base64 编码:

方式一:页面自动读取 static 目录(推荐)

import { createCode } from '@/uni_modules/xmkj-scan'

function generateWithLogo() {
    let logoBase64: string | undefined = undefined
    try {
        const fs = uni.getFileSystemManager()
        logoBase64 = fs.readFileSync('static/qr_logo.png', 'base64') as string
    } catch (e) {
        console.log('未找到 logo 文件,生成无 logo 二维码')
    }

    createCode({
        content: 'https://www.example.com',
        type: 'QR_CODE',
        width: 300,
        height: 300,
        logoBase64: logoBase64,
        success: (base64) => {
            imageSrc.value = base64
        },
        fail: (errMsg) => {
            console.error(`生成失败: ${errMsg}`)
        }
    })
}

方式二:使用自定义 Logo

// 将任意图片转为 base64 后传入
const myLogoBase64 = 'iVBORw0KGgoAAAANSUhEUgAA...' // 纯 base64 字符串

createCode({
    content: 'https://www.example.com',
    type: 'QR_CODE',
    width: 300,
    height: 300,
    logoBase64: myLogoBase64,
    success: (base64) => { ... }
})

Logo 大小建议:二维码宽度的 20%\~30%,过大会影响扫码识别率。


完整示例页面

示例一:扫码/生成码综合测试页

<template>
    <scroll-view class="scroll-container" :scroll-y="true">
        <view class="container">
            <view class="header">
                <text class="title">扫码/生成码测试</text>
            </view>

            <view class="section">
                <text class="section-title">生成码</text>

                <view class="form-item">
                    <text class="label">内容</text>
                    <input class="input" v-model="codeContent" placeholder="请输入要生成的内容" />
                </view>

                <view class="form-item">
                    <text class="label">宽度</text>
                    <input class="input" type="number" v-model="codeWidth" />
                </view>

                <view class="form-item">
                    <text class="label">高度</text>
                    <input class="input" type="number" v-model="codeHeight" />
                </view>

                <button class="btn btn-primary" @click="handleCreateCode">生成码</button>

                <view class="code-preview" v-if="codeImage">
                    <text class="preview-label">预览</text>
                    <image class="code-image" :src="codeImage" mode="aspectFit"></image>
                </view>

                <view class="error-message" v-if="createError">
                    <text class="err-msg">{{createError}}</text>
                </view>
            </view>

            <view class="section">
                <text class="section-title">自定义页面扫码</text>

                <button class="btn btn-success" @click="handleScanCode">开始扫码</button>

                <view class="scan-result" v-if="scanResult">
                    <text class="result-label">扫码结果</text>
                    <view class="result-item">
                        <text class="result-key">内容:</text>
                        <text class="result-value">{{scanResult?.content}}</text>
                    </view>
                    <view class="result-item">
                        <text class="result-key">类型:</text>
                        <text class="result-value">{{scanResult?.type}}</text>
                    </view>
                </view>

                <view class="error-message" v-if="scanError">
                    <text class="err-msg">{{scanError}}</text>
                </view>
            </view>

            <view class="section">
                <text class="section-title">默认页面扫码</text>
                <button class="btn" :class="scanningContinuously ? 'btn-danger' : 'btn-warning'" @click="handleContinuousScan">
                    {{scanningContinuously ? '停止扫码' : '开始扫码'}}
                </button>
                <text class="scan-status" v-if="scanningContinuously">扫码中,扫描结果实时更新...</text>
                <view class="scan-result" v-if="continuousScanResults.length > 0">
                    <text class="result-label">扫码结果列表(共 {{continuousScanResults.length}} 条)</text>
                    <scroll-view class="result-list" :scroll-y="true">
                        <view class="result-item" v-for="(item, index) in continuousScanResults" :key="index">
                            <text class="result-index">{{index + 1}}.</text>
                            <view class="result-content">
                                <text class="result-value-text">{{item.content}}</text>
                                <text class="result-type-text">{{item.type}}</text>
                            </view>
                        </view>
                    </scroll-view>
                </view>
            </view>
        </view>
    </scroll-view>
</template>

<script setup lang="uts">
    import { createCode, scanCode, stopContinuousScan } from '@/uni_modules/xmkj-scan'

    type ScanResultType = {
        content : string
        type : string
    }

    const codeContent = ref('https://example.com')
    const codeTypes = ref(['QR_CODE', 'CODE128', 'EAN13', 'CODE39', 'AZTEC', 'DATAMATRIX', 'PDF417'])
    const codeTypeIndex = ref(0)
    const codeWidth = ref(300)
    const codeHeight = ref(300)
    const codeImage = ref('')
    const createLoading = ref(false)
    const createError = ref('')

    const scanResult = ref<ScanResultType | null>(null)
    const scanError = ref('')

    const scanningContinuously = ref(false)
    const continuousScanResults = ref<ScanResultType[]>([])

    function handleCreateCode() {
        createError.value = ''
        codeImage.value = ''
        createLoading.value = true

        let logoBase64 : string | null = null
        try {
            const fs = uni.getFileSystemManager()
            logoBase64 = fs.readFileSync('static/qr_logo.png', 'base64') as string
        } catch (e) {
            console.log('Logo file not found, generating without logo')
        }

        createCode({
            content: codeContent.value,
            type: 'QR_CODE',
            width: codeWidth.value,
            height: codeHeight.value,
            logoBase64: logoBase64,
            success: (base64 : string) => {
                codeImage.value = base64
                createLoading.value = false
            },
            fail: (errMsg : string) => {
                createError.value = `生成失败: ${errMsg}`
                createLoading.value = false
            }
        })
    }

    function handleScanCode() {
        scanError.value = ''
        scanResult.value = null

        scanCode({
            success: (result : string, scanType : string) => {
                scanResult.value = {
                    content: result,
                    type: scanType
                }
            },
            fail: (errMsg : string) => {
                scanError.value = `扫码失败: ${errMsg}`
            },
            onlyFromCamera: false,
            scanMode: 'default',
            useCustomizedView: true
        })
    }

    function handleContinuousScan() {
        if (scanningContinuously.value) {
            stopContinuousScan()
            scanningContinuously.value = false
            return
        }

        continuousScanResults.value = []
        scanningContinuously.value = true

        scanCode({
            onlyFromCamera: false,
            continuousScan: true,
            onResult: (result : string, scanType : string) : boolean | void => {
                continuousScanResults.value.push({
                    content: result,
                    type: scanType
                } as ScanResultType)
                return true
            },
            success: (result : string, scanType : string) => {
                continuousScanResults.value.push({
                    content: result,
                    type: scanType
                } as ScanResultType)
            },
            fail: (errMsg : string) => {
                scanningContinuously.value = false
                scanError.value = `连续扫码失败: ${errMsg}`
            },
            complete: () => {
                scanningContinuously.value = false
            }
        })
    }
</script>

<style>
    .scroll-container {
        flex: 1;
    }

    .container {
        display: flex;
        flex-direction: column;
        padding: 20px;
        background-color: #f5f5f5;
    }

    .header {
        display: flex;
        justify-content: center;
        padding: 20px 0;
    }

    .title {
        font-size: 24px;
        font-weight: bold;
        color: #333;
    }

    .section {
        background-color: #fff;
        border-radius: 12px;
        padding: 20px;
        margin-bottom: 20px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
    }

    .section-title {
        font-size: 18px;
        font-weight: bold;
        color: #333;
        margin-bottom: 20px;
        padding-bottom: 10px;
        border-bottom: 1px solid #eee;
    }

    .form-item {
        margin-bottom: 15px;
    }

    .label {
        font-size: 14px;
        color: #666;
        margin-bottom: 8px;
    }

    .input {
        height: 44px;
        padding: 0 12px;
        border: 1px solid #ddd;
        border-radius: 8px;
        font-size: 16px;
        background-color: #fafafa;
    }

    .btn {
        width: 100%;
        height: 48px;
        border-radius: 8px;
        font-size: 16px;
        border: none;
        margin-top: 10px;
    }

    .btn-primary {
        background-color: #1890ff;
        color: #fff;
    }

    .btn-success {
        background-color: #52c41a;
        color: #fff;
    }

    .btn-warning {
        background-color: #faad14;
        color: #fff;
    }

    .btn-danger {
        background-color: #ff4d4f;
        color: #fff;
    }

    .scan-status {
        font-size: 13px;
        color: #52c41a;
        margin-top: 8px;
        text-align: center;
    }

    .result-list {
        max-height: 300px;
        margin-top: 10px;
    }

    .result-index {
        font-size: 14px;
        font-weight: bold;
        color: #1890ff;
        min-width: 30px;
    }

    .result-content {
        display: flex;
        flex-direction: column;
        flex: 1;
    }

    .result-value-text {
        font-size: 14px;
        color: #333;
    }

    .result-type-text {
        font-size: 12px;
        color: #999;
        margin-top: 2px;
    }

    .code-preview {
        margin-top: 20px;
        display: flex;
        flex-direction: column;
        align-items: center;
    }

    .preview-label {
        font-size: 14px;
        color: #666;
        margin-bottom: 10px;
    }

    .code-image {
        width: 100%;
        max-width: 300px;
        height: 300px;
        border-radius: 8px;
    }

    .scan-result {
        margin-top: 20px;
        background-color: #f9f9f9;
        border-radius: 8px;
        padding: 15px;
    }

    .result-label {
        font-size: 14px;
        font-weight: bold;
        color: #333;
        margin-bottom: 10px;
    }

    .result-item {
        display: flex;
        flex-direction: row;
        margin-bottom: 8px;
    }

    .result-key {
        font-size: 14px;
        color: #666;
        min-width: 60px;
    }

    .result-value {
        font-size: 14px;
        color: #333;
        flex: 1;
    }

    .error-message {
        margin-top: 15px;
        padding: 12px;
        background-color: #fff2f0;
        border: 1px solid #ffccc7;
        border-radius: 8px;
    }

    .err-msg {
        font-size: 14px;
        color: #ff4d4f;
    }
</style>

示例二:连续扫码专用页

<template>
    <view class="container">
        <view class="header">
            <text class="title">连续扫码测试</text>
        </view>

        <view class="controls">
            <button class="control-btn" type="primary" @click="startScan" :disabled="isScanning">
                {{ isScanning ? '扫码中...' : '开始扫码' }}
            </button>
            <button class="control-btn" type="warn" @click="stopScan" :disabled="!isScanning">关闭扫码</button>
        </view>

        <view class="options">
            <text class="option-label">连续扫码模式</text>
        </view>

        <view class="scan-status">
            <text class="status-text">状态: {{ statusText }}</text>
        </view>

        <view class="result-section">
            <view class="section-header">
                <text class="section-title">扫码记录 ({{ scanRecords.length }})</text>
                <button v-if="scanRecords.length > 0" type="default" size="mini" @click="clearRecords">清空记录</button>
            </view>

            <scroll-view class="record-list" scroll-y="true">
                <view v-for="(record, index) in scanRecords" :key="index" class="record-item">
                    <text class="record-index">{{ index + 1 }}.</text>
                    <view class="record-content">
                        <text class="record-type">{{ record.type }}</text>
                        <text class="record-value">{{ record.content }}</text>
                    </view>
                </view>
                <view v-if="scanRecords.length == 0" class="empty-tip">
                    <text class="empty-text">暂无扫码记录</text>
                </view>
            </scroll-view>
        </view>
    </view>
</template>

<script setup lang="uts">
    import { scanCode } from '@/uni_modules/xmkj-scan'
    import { onUnmounted } from 'vue'

    type ScanRecord = {
        content: string,
        type: string
    }

    const continuousScan = ref<boolean>(true)
    const isScanning = ref<boolean>(false)
    const statusText = ref<string>('待扫码')
    const scanRecords = ref<Array<ScanRecord>>([])

    onUnmounted(() => {
        // 页面卸载时停止扫码
    })

    function startScan() {
        if (isScanning.value) {
            return
        }

        scanRecords.value = []
        statusText.value = '扫码中...'
        isScanning.value = true

        scanCode({
            success: (result : string, scanType : string) => {
                const record : ScanRecord = {
                    content: result,
                    type: scanType
                }
                console.log(record)
                scanRecords.value.push(record)
            },
            fail: (errMsg : string) => {
                statusText.value = `扫码失败: ${errMsg}`
            },
            complete: () => {
                // 扫码页面关闭后的处理
                isScanning.value = false
                if (scanRecords.value.length > 0) {
                    statusText.value = `扫码完成,共 ${scanRecords.value.length} 个`
                } else {
                    statusText.value = '未扫码任何内容'
                }
            },
            onlyFromCamera: false,
            useCustomizedView: true,
            continuousScan: continuousScan.value
        })
    }

    function stopScan() {
        if (!isScanning.value) {
            return
        }

        // 关闭扫码页面
        uni.navigateBack()
    }

    function clearRecords() {
        scanRecords.value = []
        if (continuousScan.value) {
            statusText.value = '连续扫码模式'
        } else {
            statusText.value = '单次扫码模式'
        }
    }
</script>

<style>
    .container {
        padding: 20px;
        background-color: #f5f5f5;
        min-height: 800;
    }

    .header {
        padding: 20px 0;
        align-items: center;
    }

    .title {
        font-size: 24px;
        font-weight: bold;
        color: #333333;
    }

    .controls {
        flex-direction: row;
        justify-content: space-between;
        margin-bottom: 20px;
    }

    .control-btn {
        flex: 1;
        margin: 0 5px;
    }

    .options {
        flex-direction: row;
        align-items: center;
        padding: 15px;
        background-color: #ffffff;
        border-radius: 8px;
        margin-bottom: 20px;
    }

    .option-label {
        margin-left: 10px;
        font-size: 16px;
        color: #333333;
    }

    .scan-status {
        padding: 15px;
        background-color: #ffffff;
        border-radius: 8px;
        margin-bottom: 20px;
        align-items: center;
    }

    .status-text {
        font-size: 14px;
        color: #666666;
    }

    .result-section {
        background-color: #ffffff;
        border-radius: 8px;
        padding: 15px;
        flex: 1;
    }

    .section-header {
        flex-direction: row;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 15px;
    }

    .section-title {
        font-size: 16px;
        font-weight: bold;
        color: #333333;
    }

    .record-list {
        max-height: 400;
    }

    .record-item {
        flex-direction: row;
        padding: 10px 0;
        border-bottom-width: 1px;
        border-bottom-color: #f0f0f0;
    }

    .record-index {
        width: 30px;
        font-size: 14px;
        color: #999999;
    }

    .record-content {
        flex: 1;
    }

    .record-type {
        font-size: 12px;
        color: #1890ff;
        font-weight: bold;
    }

    .record-value {
        font-size: 14px;
        color: #333333;
        margin-top: 5px;
    }

    .empty-tip {
        padding: 40px 0;
        align-items: center;
    }

    .empty-text {
        font-size: 14px;
        color: #999999;
    }
</style>

返回值格式

扫码(success 回调)

success(result: string, scanType: string) => void
参数 类型 说明 示例
result string 码内容 "https://www.example.com"
scanType string 码类型 "QR_CODE", "EAN13"

生成码(success 回调)

success(base64: string) => void
参数 类型 说明 示例
base64 string 完整 data URL "data:image/bmp;base64,Qk02..."

可直接赋值给 <image>src 属性显示。


注意事项

  1. Android 端:必须使用自定义基座调试
  2. HarmonyOS 端
    • 生成码需真机测试,模拟器不支持
    • HBuilder X 调试模式下 static 目录文件可能无法通过文件系统读取,Logo 使用 HarmonyOS 内置默认 Logo(通过 logoBase64 传入自定义 Logo 可覆盖)
  3. iOS 端:代码已实现但无真实设备测试,iOS 用户可自行调试修改
  4. 码尺寸限制
    • 二维码(QR_CODE):宽高相同,且大于 200px
    • 条码(CODE128 等):宽高比 2:1,宽度大于 400px
  5. 连续扫码:使用系统扫码页面循环调用实现,每次扫码会重新拉起扫码界面,扫码结束后需手动关闭页面
  6. Logo 优化:建议 logo 尺寸不超过二维码边长的 30%,以保证扫码识别率

隐私、权限声明

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

相机和相册读写

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

插件不采集任何数据

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