This commit is contained in:
2026-06-02 15:28:25 +08:00
commit b271b84f70
249 changed files with 42240 additions and 0 deletions
+570
View File
@@ -0,0 +1,570 @@
<template>
<view class="container">
<!-- 状态栏 -->
<view class="status-section">
<view class="status-item">
<text class="status-label">蓝牙适配器</text>
<text class="status-value" :class="adapterState.available ? 'available' : ''">
{{ adapterState.available ? '就绪' : '未就绪' }}
</text>
</view>
<view class="status-item">
<text class="status-label">打印机</text>
<text class="status-value" :class="{ connected: connected }">
{{ connected ? '已连接' : '未连接' }}
</text>
</view>
<view class="status-item">
<text class="status-label">连接方式</text>
<text class="status-value auto">自动</text>
</view>
</view>
<!-- 连接信息 -->
<view class="section" v-if="savedConfig">
<view class="section-header">
<text class="section-title">已保存的配置</text>
</view>
<view class="config-info">
<view class="config-row">
<text class="config-label">设备名称</text>
<text class="config-value">{{ savedConfig.deviceName }}</text>
</view>
<view class="config-row">
<text class="config-label">服务UUID</text>
<text class="config-value uuid">{{ savedConfig.serviceId }}</text>
</view>
<view class="config-row">
<text class="config-label">特征值UUID</text>
<text class="config-value uuid">{{ savedConfig.writeCharId }}</text>
</view>
<view class="config-row" v-if="savedConfig.lastUpdated">
<text class="config-label">保存时间</text>
<text class="config-value">{{ formatDate(savedConfig.lastUpdated) }}</text>
</view>
</view>
<view class="config-actions">
<button type="warn" size="mini" @click="clearConfig">清除配置</button>
</view>
</view>
<!-- 无配置提示 -->
<view class="section empty-section" v-else>
<text class="empty-text">暂无保存的配置</text>
<text class="empty-hint">请先在蓝牙打印页面连接打印机并保存配置</text>
<button type="primary" size="mini" @click="goToBlePrint">前往配置</button>
</view>
<!-- 打印控制区 -->
<view class="section print-section" v-if="connected">
<view class="section-header">
<text class="section-title">打印控制</text>
<text class="printer-name">{{ connectedDeviceName }}</text>
</view>
<!-- 指令类型选择 -->
<view class="command-type">
<view class="type-label">指令类型:</view>
<view class="type-switch">
<view
class="type-btn"
:class="{ active: commandType === 'CPCL' }"
@click="commandType = 'CPCL'">
CPCL
</view>
<view
class="type-btn"
:class="{ active: commandType === 'TSPL' }"
@click="commandType = 'TSPL'">
TSPL
</view>
</view>
</view>
<!-- 打印内容 -->
<view class="print-content">
<view class="content-header">
<text class="content-title">打印内容</text>
<view class="content-actions">
<button size="mini" @click="clearContent">清空</button>
<button size="mini" type="primary" @click="doPrint">打印</button>
</view>
</view>
<textarea
class="content-textarea"
v-model="printContent"
:placeholder="contentPlaceholder">
</textarea>
</view>
<!-- 快捷模板 -->
<view class="template-section">
<text class="template-title">快捷模板</text>
<scroll-view class="template-list" scroll-x>
<view class="template-tags">
<view class="template-tag" @click="useTemplate('qrcode')">二维码</view>
<view class="template-tag" @click="useTemplate('barcode')">条码</view>
<view class="template-tag" @click="useTemplate('label')">标签</view>
</view>
</scroll-view>
</view>
</view>
<!-- 连接中状态 -->
<view class="section connecting-section" v-else-if="savedConfig && isConnecting">
<view class="connecting-content">
<view class="loading-icon"></view>
<text class="connecting-text">正在自动连接...</text>
<text class="connecting-hint">{{ savedConfig.deviceName }}</text>
</view>
<button type="default" size="mini" @click="cancelConnect">取消</button>
</view>
</view>
</template>
<script>
import * as iconv from 'iconv-lite';
export default {
data() {
return {
adapterState: {
available: false,
discovering: false
},
isConnecting: false,
savedConfig: null,
connectedDeviceId: '',
connectedDeviceName: '',
connected: false,
writeServiceId: '',
writeCharacteristicId: '',
commandType: 'CPCL',
printContent: ''
}
},
computed: {
contentPlaceholder() {
const placeholders = {
'TSPL': '输入TSPL指令,如:\nSIZE 50 mm 30 mm\nGAP 2 mm\nCLS\nTEXT 50 50 "4" 0 1 1 "Hello"\nPRINT',
'CPCL': '输入CPCL指令,如:\n! 0 200 200 240 1\nSIZE 50mm 30mm\nGAP 2mm\nTEXT 4 0 10 10 EN Hello\nPRINT'
}
return placeholders[this.commandType]
}
},
onLoad() {
this.loadConfig()
},
onUnload() {
this.cleanup()
},
methods: {
formatDate(dateStr) {
const date = new Date(dateStr)
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
},
loadConfig() {
const config = uni.getStorageSync('blePrintAutoConfig')
if (config) {
this.savedConfig = config
setTimeout(() => {
this.autoConnect()
}, 500)
}
},
clearConfig() {
uni.removeStorageSync('blePrintAutoConfig')
this.savedConfig = null
uni.showToast({ title: '配置已清除', icon: 'success' })
},
goToBlePrint() {
uni.navigateTo({
url: '/pages/testpages/bleprint'
})
},
async autoConnect() {
if (!this.savedConfig) return
this.isConnecting = true
try {
await this.initBluetooth()
await this.createConnection()
this.connected = true
this.connectedDeviceId = this.savedConfig.deviceId
this.connectedDeviceName = this.savedConfig.deviceName
this.writeServiceId = this.savedConfig.serviceId
this.writeCharacteristicId = this.savedConfig.writeCharId
this.isConnecting = false
uni.showToast({ title: '连接成功', icon: 'success' })
} catch (err) {
this.isConnecting = false
uni.showToast({ title: '连接失败', icon: 'none' })
}
},
initBluetooth() {
return new Promise((resolve, reject) => {
uni.openBluetoothAdapter({
success: () => {
this.adapterState.available = true
uni.onBLEConnectionStateChange((res) => {
if (!res.connected && this.connectedDeviceId === res.deviceId) {
this.connected = false
this.connectedDeviceId = ''
this.connectedDeviceName = ''
}
})
resolve()
},
fail: (err) => {
this.adapterState.available = false
// 检测蓝牙是否未打开
if (err.errMsg && (err.errMsg.includes('not available') || err.errMsg.includes('available'))) {
uni.showModal({
title: '蓝牙未开启',
content: '请先开启手机蓝牙,然后重试',
confirmText: '重试',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 延迟重试
setTimeout(() => {
this.autoConnect()
}, 300)
}
}
})
}
reject(err)
}
})
})
},
createConnection() {
return new Promise((resolve, reject) => {
uni.createBLEConnection({
deviceId: this.savedConfig.deviceId,
timeout: 15000,
success: () => {
resolve()
},
fail: (err) => {
reject(err)
}
})
})
},
cancelConnect() {
this.isConnecting = false
},
disconnectPrinter() {
if (!this.connectedDeviceId) return
uni.closeBLEConnection({
deviceId: this.connectedDeviceId,
success: () => {
this.connected = false
this.connectedDeviceId = ''
this.connectedDeviceName = ''
}
})
},
clearContent() {
this.printContent = ''
},
useTemplate(type) {
const templates = {
qrcode: {
CPCL: '! 0 200 200 240 1\nSIZE 50mm 30mm\nGAP 2mm\nCENTER\nQR 300 10 M 2 A 0\nMA,https://www.example.com\nENDQR\nPRINT',
TSPL: 'SIZE 50 mm 30 mm\nGAP 2 mm\nCLS\nQRCODE 50 50 L 5 0 A 0 "https://www.example.com"\nPRINT'
},
barcode: {
CPCL: '! 0 200 200 240 1\nSIZE 50mm 30mm\nGAP 2mm\nCENTER\nBARCODE 128 1 1 60 1 0 10 40 1234567890\nPRINT',
TSPL: 'SIZE 50 mm 30 mm\nGAP 2 mm\nCLS\nBARCODE 50 50 "128" 50 1 0 2 2 "1234567890"\nPRINT'
},
label: {
CPCL: '! 0 200 200 400 1\nSIZE 50mm 80mm\nGAP 2mm\nCENTER\nTEXT 4 0 10 20 EN Product Name\nTEXT 4 0 10 60 EN Price: $99.00\nQR 300 120 M 2 A 0\nMA,https://example.com\nENDQR\nBARCODE 128 1 1 60 1 0 10 200 12345678\nPRINT',
TSPL: 'SIZE 50 mm 80 mm\nGAP 2 mm\nCLS\nTEXT 20 20 "4" 0 1 1 "Product Name"\nTEXT 20 60 "4" 0 1 1 "Price: $99.00"\nQRCODE 50 100 L 5 0 A 0 "https://example.com"\nBARCODE 20 160 "128" 50 1 0 2 2 "12345678"\nPRINT'
}
}
this.printContent = templates[type][this.commandType]
},
stringToBuffer(str) {
try {
const uint8Array = iconv.encode(str, 'gbk');
return uint8Array.buffer;
} catch (e) {
console.warn('编码失败,使用回退方案', e)
const buffer = new ArrayBuffer(str.length)
const dataView = new DataView(buffer)
for (let i = 0; i < str.length; i++) {
dataView.setUint8(i, str.charCodeAt(i) & 0xFF)
}
return buffer
}
},
doPrint() {
if (!this.connected) {
uni.showToast({ title: '请先连接打印机', icon: 'none' })
return
}
if (!this.printContent.trim()) {
uni.showToast({ title: '请输入打印内容', icon: 'none' })
return
}
if (!this.writeCharacteristicId) {
uni.showToast({ title: '未配置特征值', icon: 'none' })
return
}
const buffer = this.stringToBuffer(this.printContent)
uni.writeBLECharacteristicValue({
deviceId: this.connectedDeviceId,
serviceId: this.writeServiceId,
characteristicId: this.writeCharacteristicId,
value: buffer,
writeType: 'writeNoResponse',
success: () => {
uni.showToast({ title: '打印成功', icon: 'success' })
},
fail: (err) => {
uni.showToast({ title: '发送失败', icon: 'none' })
}
})
},
cleanup() {
if (this.connectedDeviceId) {
uni.closeBLEConnection({
deviceId: this.connectedDeviceId
})
}
}
}
}
</script>
<style scoped>
.container {
padding: 20rpx;
background: #f5f5f5;
min-height: 100vh;
}
.status-section {
display: flex;
background: #fff;
padding: 20rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
}
.status-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.status-label {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.status-value {
font-size: 28rpx;
font-weight: bold;
color: #666;
}
.status-value.available {
color: #007aff;
}
.status-value.connected {
color: #34c759;
}
.status-value.auto {
color: #ff9500;
}
.section {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.printer-name {
font-size: 24rpx;
color: #34c759;
}
.config-info {
background: #f8f8f8;
border-radius: 8rpx;
padding: 16rpx;
margin-bottom: 16rpx;
}
.config-row {
display: flex;
margin-bottom: 12rpx;
}
.config-row:last-child {
margin-bottom: 0;
}
.config-label {
font-size: 24rpx;
color: #666;
min-width: 140rpx;
}
.config-value {
font-size: 24rpx;
color: #333;
flex: 1;
}
.config-value.uuid {
font-family: 'Courier New', monospace;
font-size: 22rpx;
word-break: break-all;
}
.config-actions {
display: flex;
justify-content: flex-end;
}
.empty-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 20rpx;
}
.empty-text {
font-size: 30rpx;
color: #666;
margin-bottom: 16rpx;
}
.empty-hint {
font-size: 24rpx;
color: #999;
margin-bottom: 32rpx;
text-align: center;
}
.print-section {
min-height: 400rpx;
}
.command-type {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.type-label {
font-size: 26rpx;
color: #666;
margin-right: 16rpx;
}
.type-switch {
display: flex;
border: 1rpx solid #007aff;
border-radius: 8rpx;
overflow: hidden;
}
.type-btn {
padding: 12rpx 24rpx;
font-size: 24rpx;
color: #007aff;
background: #fff;
}
.type-btn.active {
background: #007aff;
color: #fff;
}
.print-content {
margin-bottom: 20rpx;
}
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.content-title {
font-size: 26rpx;
color: #666;
}
.content-actions {
display: flex;
gap: 12rpx;
}
.content-textarea {
width: 100%;
height: 180rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 16rpx;
font-size: 26rpx;
font-family: 'Courier New', monospace;
box-sizing: border-box;
}
.template-section {
margin-bottom: 20rpx;
}
.template-title {
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
.template-list {
width: 100%;
}
.template-tags {
display: flex;
gap: 12rpx;
padding: 4rpx;
}
.template-tag {
padding: 12rpx 24rpx;
background: #f0f0f0;
border-radius: 8rpx;
font-size: 24rpx;
color: #333;
}
.template-tag:active {
background: #e0e0e0;
}
.connecting-section {
display: flex;
flex-direction: column;
align-items: center;
padding: 60rpx 20rpx;
}
.connecting-content {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 32rpx;
}
.loading-icon {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #007aff;
border-top-color: transparent;
border-radius: 50%;
animation: rotate 1s linear infinite;
margin-bottom: 24rpx;
}
@keyframes rotate {
to { transform: rotate(360deg); }
}
.connecting-text {
font-size: 28rpx;
color: #333;
margin-bottom: 8rpx;
}
.connecting-hint {
font-size: 24rpx;
color: #999;
}
</style>
File diff suppressed because it is too large Load Diff
+171
View File
@@ -0,0 +1,171 @@
<template>
<view class="container">
<!-- 步骤1扫描设备 -->
<button @click="startScan">1. 扫描蓝牙设备</button>
<view v-if="devices.length">
<view v-for="device in devices" :key="device.deviceId" class="device-item" @click="selectDevice(device)">
<text>{{ device.name || '未知设备' }}</text>
<text class="mac">{{ device.deviceId }}</text>
</view>
</view>
<!-- 步骤2连接后展示服务 -->
<view v-if="services.length">
<text>请选择服务</text>
<view v-for="svc in services" :key="svc.uuid" class="service-item" @click="selectService(svc)">
<text>{{ svc.uuid }}</text>
</view>
</view>
<!-- 步骤3选择特征值 -->
<view v-if="characteristics.length">
<text>请选择写入特征值</text>
<view v-for="char in characteristics" :key="char.uuid" class="char-item" @click="selectWriteChar(char)">
<text>{{ char.uuid }} ({{ char.properties.write ? '可写' : '' }})</text>
</view>
<text>请选择通知特征值可选</text>
<view v-for="char in characteristics" :key="char.uuid" class="char-item" @click="selectNotifyChar(char)">
<text>{{ char.uuid }} ({{ char.properties.notify ? '可通知' : '' }})</text>
</view>
</view>
<!-- 步骤4保存配置 -->
<button v-if="selectedWriteChar" @click="saveConfiguration">4. 保存配置并测试连接</button>
</view>
</template>
<script>
import bluetoothManager from '@/utils/BluetoothManager.js';
export default {
data() {
return {
devices: [], // 扫描到的设备列表
selectedDeviceId: null,
services: [], // 选中设备的服务列表
characteristics: [], // 选中服务的特征值列表
selectedServiceId: null,
selectedWriteChar: null,
selectedNotifyChar: null,
};
},
methods: {
async startScan() {
await bluetoothManager.init();
this.devices = [];
bluetoothManager.setDeviceFoundCallback((device) => {
// 避免重复添加
if (!this.devices.find(d => d.deviceId === device.deviceId)) {
this.devices.push(device);
}
});
await bluetoothManager.startScan();
uni.showToast({ title: '扫描中...', icon: 'none' });
// 30秒后停止
setTimeout(() => bluetoothManager.stopScan(), 30000);
},
async selectDevice(device) {
bluetoothManager.stopScan();
uni.showLoading({ title: '连接设备中...' });
try {
const { services, deviceId } = await bluetoothManager.connectForDiscovery(device.deviceId);
this.selectedDeviceId = deviceId;
this.services = services;
this.characteristics = [];
this.selectedServiceId = null;
this.selectedWriteChar = null;
this.selectedNotifyChar = null;
uni.hideLoading();
uni.showToast({ title: '连接成功,请选择服务', icon: 'success' });
} catch (err) {
uni.hideLoading();
uni.showToast({ title: '连接失败', icon: 'none' });
}
},
async selectService(service) {
this.selectedServiceId = service.uuid;
uni.showLoading({ title: '获取特征值...' });
try {
const chars = await this._getCharacteristics(this.selectedDeviceId, service.uuid);
this.characteristics = chars;
uni.hideLoading();
} catch (err) {
uni.hideLoading();
uni.showToast({ title: '获取特征值失败', icon: 'none' });
}
},
async _getCharacteristics(deviceId, serviceId) {
return new Promise((resolve, reject) => {
uni.getBLEDeviceCharacteristics({
deviceId,
serviceId,
success: (res) => resolve(res.characteristics),
fail: reject,
});
});
},
selectWriteChar(char) {
if (!char.properties.write && !char.properties.writeNoResponse) {
uni.showToast({ title: '该特征值不支持写入', icon: 'none' });
return;
}
this.selectedWriteChar = char.uuid;
},
selectNotifyChar(char) {
if (!char.properties.notify && !char.properties.indicate) {
uni.showToast({ title: '该特征值不支持通知', icon: 'none' });
return;
}
this.selectedNotifyChar = char.uuid;
},
async saveConfiguration() {
if (!this.selectedWriteChar) {
uni.showToast({ title: '请先选择写入特征值', icon: 'none' });
return;
}
try {
await bluetoothManager.saveAndConnect(
this.selectedDeviceId,
this.selectedServiceId,
this.selectedWriteChar,
this.selectedNotifyChar
);
// 保存配置到本地
const config = {
deviceId: this.selectedDeviceId,
serviceId: this.selectedServiceId,
writeCharId: this.selectedWriteChar,
notifyCharId: this.selectedNotifyChar,
deviceName: this.devices.find(d => d.deviceId === this.selectedDeviceId)?.name || '',
};
uni.setStorageSync('bleDeviceConfig', config);
uni.showToast({ title: '配置已保存,可返回使用', icon: 'success' });
// 可选:跳转到主页面
setTimeout(() => uni.navigateBack(), 1500);
} catch (err) {
console.error('保存失败', err);
uni.showToast({ title: '保存失败', icon: 'none' });
}
},
},
};
</script>
<style>
.device-item, .service-item, .char-item {
padding: 10px;
border-bottom: 1px solid #ddd;
margin: 5px 0;
}
.mac {
font-size: 12px;
color: gray;
margin-left: 10px;
}
</style>
File diff suppressed because it is too large Load Diff
+339
View File
@@ -0,0 +1,339 @@
<template>
<view class="container">
<view class="status-bar">
<text>打印机: {{ printerConnected ? '已连接' : '未连接' }}</text>
<text v-if="lastPrinterMac && !printerConnected">上次设备: {{ lastPrinterMac }}</text>
</view>
<view class="status-bar">
<text>电子秤: {{ scaleConnected ? '已连接' : '未连接' }}</text>
<text>重量: {{ weightValue }} g</text>
</view>
<button type="primary" @click="initAndAutoConnect">启动蓝牙并自动连接</button>
<button @click="manualScanPrinter">手动扫描打印机</button>
<button @click="manualScanScale">手动扫描电子秤</button>
<button @click="printTest">打印测试</button>
<button @click="disconnectAll">断开所有连接</button>
</view>
</template>
<script>
import bluetoothManager from '@/utils/BluetoothManager.js';
export default {
data() {
return {
// 打印机相关
printerDeviceId: null,
printerConnected: false,
lastPrinterMac: '',
// 电子秤相关
scaleDeviceId: null,
scaleConnected: false,
weightValue: '--',
// UUID 配置(请替换为实际值)
printerServiceUUID: '0000EEE2-0000-1000-8000-00805F9B34FB',
printerWriteUUID: '0000EEE3-0000-1000-8000-00805F9B34FB',
scaleServiceUUID: 'FFF0',
scaleNotifyUUID: 'FFF1',
};
},
onLoad() {
const config = uni.getStorageSync('bleDeviceConfig');
if (config && config.deviceId) {
this.printerServiceUUID = config.serviceId;
this.printerWriteUUID = config.writeCharId;
this.lastPrinterMac = config.deviceId;
// 如果配置了 notify UUID,也可用于电子秤等
this.initAndAutoConnect();
} else {
// 没有配置,引导用户去配置页面
uni.showModal({
title: '未配置设备',
content: '请先配置蓝牙设备',
success: () => {
uni.navigateTo({ url: '/pages/testpages/bleconfig' });
},
});
}
},
onUnload() {
// 页面卸载时释放所有连接(可选,也可保持连接)
bluetoothManager.closeAll();
},
methods: {
/**
* 初始化蓝牙并自动连接上次的打印机
*/
async initAndAutoConnect() {
try {
await bluetoothManager.init();
console.log('蓝牙初始化成功');
// 设置全局回调
bluetoothManager.setConnectionChangeCallback(this.onConnectionChange);
bluetoothManager.setDataReceivedCallback(this.onDataReceived);
if (this.lastPrinterMac) {
uni.showLoading({ title: '正在连接上次打印机...' });
try {
const info = await bluetoothManager.connectKnownDevice(
this.lastPrinterMac,
this.printerServiceUUID,
this.printerWriteUUID
);
this.printerDeviceId = this.lastPrinterMac;
this.printerConnected = true;
uni.hideLoading();
uni.showToast({ title: '打印机已自动连接', icon: 'success' });
} catch (err) {
uni.hideLoading();
console.error('自动连接失败', err);
uni.showModal({
title: '连接失败',
content: `上次使用的打印机 (${this.lastPrinterMac}) 未找到,是否重新扫描?`,
success: (res) => {
if (res.confirm) this.manualScanPrinter();
},
});
}
}
} catch (err) {
console.error('蓝牙初始化失败', err);
uni.showToast({ title: '请打开蓝牙并授权', icon: 'none' });
}
},
/**
* 手动扫描打印机(用于首次或自动连接失败后的补救)
*/
async manualScanPrinter() {
try {
await bluetoothManager.init();
bluetoothManager.setDeviceFoundCallback(this.onPrinterFound);
await bluetoothManager.stopScan();
await bluetoothManager.startScan([this.printerServiceUUID]);
uni.showToast({ title: '扫描打印机中...', icon: 'none' });
// 20秒后自动停止扫描
setTimeout(() => {
bluetoothManager.stopScan();
}, 20000);
} catch (err) {
console.error('扫描启动失败', err);
}
},
/**
* 手动扫描电子秤
*/
async manualScanScale() {
try {
await bluetoothManager.init();
bluetoothManager.setDeviceFoundCallback(this.onScaleFound);
await bluetoothManager.stopScan();
await bluetoothManager.startScan([this.scaleServiceUUID]);
uni.showToast({ title: '扫描电子秤中...', icon: 'none' });
setTimeout(() => {
bluetoothManager.stopScan();
}, 20000);
} catch (err) {
console.error('扫描电子秤失败', err);
}
},
/**
* 打印机发现回调
*/
onPrinterFound(device) {
const name = device.name || '';
// 根据名称过滤器(可修改为你的打印机名称关键字)
if (name.includes('QR380A-15E0_Ble')) {
bluetoothManager.stopScan(); // 发现后停止扫描
uni.showModal({
title: '发现打印机',
content: `设备: ${name}\nMAC: ${device.deviceId}\n是否连接?`,
success: (res) => {
if (res.confirm) {
this.connectAndSavePrinter(device.deviceId, name);
} else {
// 用户取消,可以继续扫描(可选)
this.manualScanPrinter();
}
},
});
}
},
/**
* 电子秤发现回调
*/
onScaleFound(device) {
const name = device.name || '';
if (name.includes('Scale') || name.includes('Weight')) {
bluetoothManager.stopScan();
uni.showModal({
title: '发现电子秤',
content: `设备: ${name}\nMAC: ${device.deviceId}\n是否连接?`,
success: (res) => {
if (res.confirm) {
this.connectScale(device.deviceId);
}
},
});
}
},
/**
* 连接打印机并保存 MAC
*/
async connectAndSavePrinter(deviceId, deviceName) {
try {
uni.showLoading({ title: '连接打印机中...' });
const info = await bluetoothManager.connectKnownDevice(
deviceId,
this.printerServiceUUID,
this.printerWriteUUID
);
this.printerDeviceId = deviceId;
this.printerConnected = true;
// 保存 MAC
uni.setStorageSync('savedPrinterMac', deviceId);
this.lastPrinterMac = deviceId;
uni.hideLoading();
uni.showToast({ title: `打印机 ${deviceName} 已连接`, icon: 'success' });
} catch (err) {
uni.hideLoading();
console.error('连接打印机失败', err);
uni.showToast({ title: '连接失败', icon: 'none' });
}
},
/**
* 连接电子秤(不需要保存,按需连接)
*/
async connectScale(deviceId) {
try {
uni.showLoading({ title: '连接电子秤...' });
const info = await bluetoothManager.connectKnownDevice(
deviceId,
this.scaleServiceUUID,
null, // 写入特征值可选
this.scaleNotifyUUID
);
this.scaleDeviceId = deviceId;
this.scaleConnected = true;
uni.hideLoading();
uni.showToast({ title: '电子秤已连接', icon: 'success' });
} catch (err) {
uni.hideLoading();
console.error('连接电子秤失败', err);
uni.showToast({ title: '连接失败', icon: 'none' });
}
},
/**
* 全局连接状态变化回调
*/
onConnectionChange(deviceId, connected) {
if (deviceId === this.printerDeviceId) {
this.printerConnected = connected;
if (!connected) {
uni.showToast({ title: '打印机断开', icon: 'none' });
// 可选:自动重连(这里不做,避免频繁请求)
}
}
if (deviceId === this.scaleDeviceId) {
this.scaleConnected = connected;
if (!connected) {
uni.showToast({ title: '电子秤断开', icon: 'none' });
}
}
},
/**
* 全局数据接收回调(接收电子秤重量数据)
*/
onDataReceived(deviceId, value) {
if (deviceId === this.scaleDeviceId) {
// 将 ArrayBuffer 转为字符串(根据电子秤协议解析)
const data = this.ab2str(value);
console.log('电子秤原始数据:', data);
// 示例:假设秤返回 "1234g" 或 "12.34kg"
const weight = this.parseWeight(data);
if (weight !== null) {
this.weightValue = weight;
}
}
},
/**
* 打印测试
*/
async printTest() {
if (!this.printerConnected) {
uni.showToast({ title: '打印机未连接', icon: 'none' });
return;
}
try {
// 示例:发送纯文本(实际需组装打印机指令,如 ESC/POS 或 CPCL
const text = '测试打印\n自动连接成功\n时间: ' + new Date().toLocaleString() + '\n\n';
const buffer = this.str2ab(text);
await bluetoothManager.writeData(this.printerDeviceId, buffer);
uni.showToast({ title: '打印指令已发送', icon: 'success' });
} catch (err) {
console.error('打印失败', err);
uni.showToast({ title: '打印失败', icon: 'none' });
}
},
/**
* 断开所有连接
*/
async disconnectAll() {
await bluetoothManager.closeAll();
this.printerDeviceId = null;
this.printerConnected = false;
this.scaleDeviceId = null;
this.scaleConnected = false;
this.weightValue = '--';
uni.showToast({ title: '已断开所有连接', icon: 'success' });
},
// ---------- 辅助函数 ----------
str2ab(str) {
const buf = new ArrayBuffer(str.length);
const view = new Uint8Array(buf);
for (let i = 0; i < str.length; i++) {
view[i] = str.charCodeAt(i);
}
return buf;
},
ab2str(buffer) {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
},
parseWeight(dataStr) {
// 修改为你的电子秤实际解析规则
const match = dataStr.match(/(\d+(?:\.\d+)?)/);
return match ? match[1] : null;
},
},
};
</script>
<style>
.container {
padding: 20px;
display: flex;
flex-direction: column;
gap: 15px;
}
.status-bar {
background: #f5f5f5;
padding: 10px;
border-radius: 8px;
margin-bottom: 10px;
}
button {
margin: 5px 0;
}
</style>
+239
View File
@@ -0,0 +1,239 @@
<template>
<div>
<button type="primary" @click="initScanFunc()">初始化扫描</button>
<br/>
<button type="primary" @click="closeScanFunc()">结束扫描</button>
<br/>
<button type="primary" @click="scanStartFunc()">开启扫描</button>
<br/>
<button type="primary" @click="scanStopFunc()">停止扫描</button>
<br/>
<p>设置连续扫描时间间隔</p><input type="number" v-model="invTime" placeholder="时间间隔(单位ms)">
<button type="primary" @click="continueScanFunc(true)">连续扫描开启</button>
<br/>
<button type="primary" @click="continueScanFunc(false)">连续扫描关闭</button>
<br/>
<button type="primary" @click="setCenterModeFunc(0)">区域解码</button>
<br/>
<button type="primary" @click="setCenterModeFunc(1)">中心解码</button>
<br/>
<p>设置扫描输出模式</p>
<button type="primary" @click="setOutputModeFunc(0)">焦点</button>
<button type="primary" @click="setOutputModeFunc(1)">广播</button>
<button type="primary" @click="setOutputModeFunc(2)">模拟按键</button>
<p>部分旧版扫描软件不支持以下模式设置后会导致软件异常</p>
<button type="primary" @click="setOutputModeFunc(3)">复制</button>
<button type="primary" @click="setOutputModeFunc(4)">焦点&广播</button>
<br/>
<button type="primary" @click="setLockScanKeyFunc()">启用扫码功能</button>
<br/>
<button type="primary" @click="setUnLockScanKeyFunc()">禁用扫码功能</button>
<br/>
<button type="primary" @click="setMultiBarcodeEnableFunc()">启用多条码识别功能</button>
<br/>
<button type="primary" @click="setMultiBarcodeDisableFunc()">禁用多条码识别功能</button>
<br/>
<p>设置多条码识别数量</p><input type="number" v-model="mulNumber" placeholder="个数">
<button type="primary" @click="setMultiBarcodePreciseStatusEnableFunc()">启用多条码精准识别功能</button>
<br/>
<button type="primary" @click="setMultiBarcodePreciseStatusDisableFunc()">禁用多条码精准识别功能</button>
<br/>
<button type="primary" @click="goTestFunc()">打开新页面</button>
<text id="testcontent" class="intro">\n测试步骤\n1.点击初始化扫描\n2.按设备黄色扫描按键扫描到条码后弹窗提示条码数据\n3.页面关闭前点击结束扫描或调用closeScan()\n4.如需使用连续扫描功能请点击连续扫描开启或关闭按钮建议设置连续扫描时间间隔</text>
</div>
</template>
<script>
//获取module
var barcodeModel = uni.requireNativePlugin("iData-BarcodePlugin-BarcodeModule")
const modal = uni.requireNativePlugin('modal');
var globalEvent = uni.requireNativePlugin('globalEvent');
export default {
onLoad() {
globalEvent.addEventListener('iDataBarcodeEvent', function(e) {
console.log("收到条码:" + JSON.stringify(e));
modal.toast({
message: "收到条码:" + JSON.stringify(e),
duration: 1.5
});
//连续扫描如果间隔时间比较短,会出现toast不提示的情况,数据可以正常接收到,建议查看控制台输出
console.log(JSON.stringify(e));
});
barcodeModel.initScan((ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
onUnload() {
barcodeModel.closeScan((ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
globalEvent.removeEventListener('iDataBarcodeEvent');
},
data(){
return{
invTime:"500",
mulNumber:"3"
}
},
methods: {
goTestFunc(){
uni.navigateTo({
url: './scantest2'
});
},
initScanFunc() {
barcodeModel.initScan((ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
scanStartFunc() {
barcodeModel.scanStart((ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
scanStopFunc() {
barcodeModel.scanStop((ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
setCenterModeFunc(mode) {
// 区域/中心扫码设置
barcodeModel.setCenterMode(mode,
(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
setOutputModeFunc(mode) {
// 扫描输出模式设置
barcodeModel.setOutputMode(mode,
(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
continueScanFunc(enable) {
// 连续扫描设置:先设置扫描间隔时间,然后开启扫描开关
if(enable){
//设置连续扫描间隔时间
barcodeModel.intervalSet(this.invTime,
(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
}
//设置连续扫描
barcodeModel.continueScan(enable,
(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
closeScanFunc() {
barcodeModel.closeScan((ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
globalEvent.removeEventListener('iDataBarcodeEvent');
},
setLockScanKeyFunc() {
//启用扫码按键扫码功能,启用后按扫描键会自动触发扫码功能
barcodeModel.setScanKeyEnable(true,(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
setUnLockScanKeyFunc() {
//禁用扫码按键扫码功能,禁用后按扫描键不会自动触发扫码功能,支持自行监听按键触发功能
barcodeModel.setScanKeyEnable(false,(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
setMultiBarcodeEnableFunc() {
//启用多条码识别功能,启用后按扫描键会一次性识别多个条码并返回对应的条码数据,每个数据一行,分隔符为'\n'
barcodeModel.setMultiBarcodeEnable(true,(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
setMultiBarcodeDisableFunc() {
//禁用多条码识别功能,禁用后按扫描键不会识别多个条码,仅识别一个条码
barcodeModel.setMultiBarcodeEnable(false,(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
setMultiBarcodePreciseStatusEnableFunc() {
//启用多条码精准模式,配合多条码数据接口,识别到配置的多条码数量的条码后才会返回条码数据
barcodeModel.setMultiBarcodePreciseStatus(true,(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
barcodeModel.setMultiBarcodeNumber(this.mulNumber,(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
},
setMultiBarcodePreciseStatusDisableFunc() {
//禁用多条码精准模式
barcodeModel.setMultiBarcodePreciseStatus(false,(ret) => {
modal.toast({
message: ret,
duration: 1.5
});
});
}
}
}
</script>
<style>
</style>
+64
View File
@@ -0,0 +1,64 @@
<template>
<view>
<view class="uni-section">
<text>扫描测试统一扫码模块</text>
</view>
<view class="uni-form-item uni-column">
<button class="btn btn-primary" @click="startScan">开始扫描</button>
<button class="btn" @click="stopScan">停止扫描</button>
</view>
<view v-if="scanResult">
<text>最近扫描结果: {{ scanResult }}</text>
</view>
</view>
</template>
<script>
import scan from '@/utils/scan.js'
export default {
data() {
return {
scanResult: ''
}
},
onLoad() {
// 初始化(默认自动检测模式,也可手动指定)
// scan.setMode('idata') // 强制 iData 模式
scan.setMode('android') // 强制 Android 系统模式
scan.init()
// 注册扫码结果回调
scan.register(this.onScanResult)
},
onUnload() {
scan.unregister()
},
methods: {
/**
* 扫码结果回调
* @param {string} codeStr - 解码后的扫描字符串
*/
onScanResult(codeStr) {
console.log('codeStr:', codeStr)
this.scanResult = codeStr
uni.showToast({
title: codeStr,
duration: 2000
})
},
startScan() {
scan.startScan()
},
stopScan() {
scan.stopScan()
}
}
}
</script>
<style>
</style>