571 lines
14 KiB
Vue
571 lines
14 KiB
Vue
<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>
|