Files
2026-06-02 15:28:25 +08:00

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>