更新记录
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 插件,支持 Android、HarmonyOS、iOS(有代码但是没有设备进行测试) 三端,请先试用后在购买。
功能特性
| 功能 | Android | HarmonyOS | iOS |
|---|---|---|---|
| 单次扫码 | ✅ | ✅ | ✅ |
| 连续扫码 | ✅ | ✅ | ✅ |
| 生成码(二维码/条码) | ✅ | ✅ | ✅ |
| 生成码带 Logo | ✅ | ✅ | ✅ |
| 多码识别 | ✅ | ✅ | ⚠️ |
⚠️ iOS 端代码已实现,但无真实设备测试,可自行修改源码调试。 ⚠️ HarmonyOS 端生成码请使用真机测试,不支持模拟器。
安装
- 将插件导入到项目的
uni_modules目录 - Android 端需要 自定义基座 后使用(HBuilder X 4.25+)
- 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 属性显示。
注意事项
- Android 端:必须使用自定义基座调试
- HarmonyOS 端:
- 生成码需真机测试,模拟器不支持
- HBuilder X 调试模式下
static目录文件可能无法通过文件系统读取,Logo 使用 HarmonyOS 内置默认 Logo(通过logoBase64传入自定义 Logo 可覆盖)
- iOS 端:代码已实现但无真实设备测试,iOS 用户可自行调试修改
- 码尺寸限制:
- 二维码(QR_CODE):宽高相同,且大于 200px
- 条码(CODE128 等):宽高比 2:1,宽度大于 400px
- 连续扫码:使用系统扫码页面循环调用实现,每次扫码会重新拉起扫码界面,扫码结束后需手动关闭页面
- Logo 优化:建议 logo 尺寸不超过二维码边长的 30%,以保证扫码识别率

收藏人数:
购买源码授权版(
试用
赞赏(4)
下载 70
赞赏 4
下载 12350604
赞赏 1926
赞赏
京公网安备:11010802035340号