Files
uni-pda/pages/testpages/bleprint.vue
T
2026-06-02 15:28:25 +08:00

1120 lines
29 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="container">
<!-- 状态栏 -->
<view class="status-bar">
<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>
<!-- 蓝牙操作 -->
<view class="action-section">
<button type="primary" size="mini" @click="initBluetooth" :disabled="isInitializing">
{{ isInitializing ? '初始化中...' : '初始化蓝牙' }}
</button>
<button v-if="adapterState.available && !adapterState.discovering" type="primary" size="mini" @click="startDiscovery">
搜索打印机
</button>
<button v-if="adapterState.discovering" type="default" size="mini" @click="stopDiscovery">
停止搜索
</button>
<button v-if="connected" type="warn" size="mini" @click="disconnectPrinter">
断开连接
</button>
</view>
<!-- 设备列表 -->
<view class="section" v-if="!connected">
<view class="section-header">
<text class="section-title">可用设备 ({{ deviceList.length }})</text>
</view>
<scroll-view class="device-list" scroll-y>
<view
class="device-item"
v-for="device in deviceList"
:key="device.deviceId"
:class="{ connecting: connectingDeviceId === device.deviceId }"
@click="connectDevice(device)">
<text class="device-name">{{ device.name || device.localName || '未知设备' }}</text>
<text class="device-id">{{ device.deviceId }}</text>
<text class="connect-hint" v-if="connectingDeviceId === device.deviceId">连接中...</text>
</view>
<view class="empty-tip" v-if="deviceList.length === 0 && !adapterState.discovering">
<text>暂无设备请点击搜索</text>
</view>
</scroll-view>
</view>
<!-- 打印控制区 -->
<view class="section print-section" v-else>
<view class="section-header">
<text class="section-title">打印控制</text>
<text class="printer-name">{{ connectedDeviceName }}</text>
</view>
<!-- 服务和特征值选择 -->
<view class="service-select-section" v-if="serviceList.length > 0">
<view class="section-header">
<text class="section-title">蓝牙服务配置</text>
<view class="header-actions">
<switch :checked="showDebugInfo" @change="toggleDebugInfo" size="mini"/>
<button size="mini" type="primary" @click="saveCurrentConfig" :disabled="!connected || !writeCharacteristicId">保存配置</button>
</view>
</view>
<!-- 当前配置状态 -->
<view class="current-config">
<view class="config-item">
<text class="config-label">服务UUID:</text>
<text class="config-value" :class="{ active: writeServiceId }">{{ writeServiceId || '未选择' }}</text>
</view>
<view class="config-item">
<text class="config-label">特征值UUID:</text>
<text class="config-value" :class="{ active: writeCharacteristicId }">{{ writeCharacteristicId || '未选择' }}</text>
</view>
</view>
<!-- 服务列表 -->
<view class="service-list">
<text class="list-title">服务列表 ({{ serviceList.length }})</text>
<scroll-view class="service-scroll" scroll-x>
<view
class="service-tag"
v-for="(service, index) in serviceList"
:key="index"
:class="{ selected: selectedService && selectedService.uuid === service.uuid }"
@click="selectService(service)">
<text class="service-index">{{ index + 1 }}</text>
<text class="service-uuid">{{ service.uuid }}</text>
<text class="service-type">{{ service.isPrimary ? '主' : '从' }}</text>
</view>
</scroll-view>
</view>
<!-- 特征值列表 -->
<view class="characteristic-list" v-if="characteristicList.length > 0">
<text class="list-title">特征值列表 ({{ characteristicList.length }})</text>
<scroll-view class="characteristic-scroll" scroll-y>
<view
class="characteristic-item"
v-for="(char, index) in characteristicList"
:key="index"
:class="{
selected: writeCharacteristicId === char.uuid,
writable: isWritable(char)
}"
@click="selectCharacteristic(char, serviceList.find(s => s.uuid === selectedServiceId))">
<view class="char-info">
<text class="char-uuid">{{ char.uuid }}</text>
<view class="char-props">
<text class="prop-tag" v-if="char.properties.write">write</text>
<text class="prop-tag" v-if="char.properties.writeNoResponse">writeNoResp</text>
<text class="prop-tag" v-if="char.properties.writeWithoutResponse">writeWithoutResp</text>
<text class="prop-tag" v-if="char.properties.read">read</text>
<text class="prop-tag" v-if="char.properties.notify">notify</text>
<text class="prop-tag" v-if="char.properties.indicate">indicate</text>
</view>
</view>
<text class="select-btn">{{ writeCharacteristicId === char.uuid ? '已选择' : '选择' }}</text>
</view>
</scroll-view>
</view>
<!-- 调试信息 -->
<view class="debug-info" v-if="showDebugInfo">
<text class="debug-title">完整服务/特征值信息:</text>
<view class="debug-content">
<text class="debug-json" user-select>{{ debugInfo }}</text>
</view>
</view>
</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"
v-for="(template, index) in commandTemplates"
:key="index"
@click="insertTemplate(template)">
{{ template.name }}
</view>
</view>
</scroll-view>
</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
class="type-btn"
:class="{ active: commandType === 'ESC' }"
@click="commandType = 'ESC'">
ESC/POS
</view>
</view>
</view>
<!-- 指令输入 -->
<view class="command-input">
<view class="input-header">
<text class="input-title">打印指令</text>
<view class="input-actions">
<button size="mini" @click="clearCommand">清空</button>
<button size="mini" type="primary" @click="printCommand">发送打印</button>
</view>
</view>
<textarea
class="command-textarea"
v-model="commandContent"
:placeholder="commandPlaceholder"
:disabled="!connected">
</textarea>
</view>
<!-- 常用参数 -->
<view class="params-section">
<text class="params-title">快速参数</text>
<view class="param-grid">
<view class="param-item" @click="appendCommand(`TEXT 0 0 30 'Hello'`)">
<text>文本</text>
</view>
<view class="param-item" @click="appendCommand('BARCODE 50 50 \'128\' 30 1 0 2 2 \'123456\'')">
<text>条码</text>
</view>
<view class="param-item" @click="appendCommand('QRCODE 50 100 L 5 0 M2 \'https://\'')">
<text>二维码</text>
</view>
<view class="param-item" @click="appendCommand('LINE 20 20 300 20 3')">
<text>线条</text>
</view>
<view class="param-item" @click="appendCommand('BOX 20 20 300 150 3')">
<text>方框</text>
</view>
<view class="param-item" @click="appendCommand('PRINT')">
<text>打印</text>
</view>
</view>
</view>
<!-- 单字节发送 -->
<view class="byte-section">
<text class="byte-title">单字节发送</text>
<view class="byte-row">
<input class="byte-input" v-model="byteInput" placeholder="如: 48 或 0x30"></input>
<button size="mini" type="primary" @click="sendByte">发送</button>
</view>
<text class="byte-hint">输入十进制(48)或十六进制(0x30)格式</text>
</view>
</view>
<!-- 日志 -->
<view class="section log-section">
<view class="section-header">
<text class="section-title">操作日志</text>
<button size="mini" @click="clearLog">清空</button>
</view>
<scroll-view class="log-list" scroll-y>
<text class="log-item"
:class="log.type"
v-for="(log, index) in logList"
:key="index">
[{{ log.time }}] {{ log.message }}
</text>
</scroll-view>
</view>
</view>
</template>
<script>
import * as iconv from 'iconv-lite';
export default {
data() {
return {
adapterState: {
available: false,
discovering: false
},
isInitializing: false,
deviceList: [],
connectingDeviceId: '',
connectedDeviceId: '',
connectedDeviceName: '',
connected: false,
// 服务和特征值
writeCharacteristicId: '',
writeServiceId: '',
serviceList: [],
characteristicList: [],
selectedService: null,
selectedServiceId: '',
showDebugInfo: false,
debugInfo: '',
// 指令相关
commandType: 'TSPL',
commandContent: '',
byteInput: '',
// 模板
commandTemplates: [
{ name: '初始化', command: 'SIZE 50 mm 30 mm\nGAP 2 mm\nDIRECTION 1\n' },
{ name: '文本打印', command: 'CLS\nTEXT 50 50 "4" 0 1 1 "Hello World"\nPRINT' },
{ name: '条码打印', command: 'CLS\nBARCODE 50 50 "128" 50 1 0 2 2 "1234567890"\nPRINT' },
{ name: '二维码打印', command: 'CLS\nQRCODE 50 50 L 5 0 A 0 "https://www.example.com"\nPRINT' },
{ name: '标签排版', command: 'CLS\nTEXT 20 20 "4" 0 1 1 "商品名称"\nTEXT 20 60 "3" 0 1 1 "价格: ¥99.00"\nBARCODE 20 100 "128" 40 1 0 2 2 "12345678"\nPRINT' },
{ name: 'CPCL文本', command: '! 0 200 200 240 1\nSIZE 50mm 30mm\nGAP 2 mm\nCENTER\nTEXT 4 0 10 10 EN Test\nTEXT 4 0 10 50 测试中文\nFORM\nPRINT' },
{ name: 'CPCL条码', command: '! 0 200 200 240 1\nSIZE 50mm 30mm\nGAP 2mm\nENCODING GB18030\nCENTER\nBARCODE 128 1 1 60 1 0 10 40 1234567890\nFORM\nPRINT' },
{ name: 'CPCL二维码', command: '! 0 200 200 240 1\nSIZE 50mm 30mm\nB QR 300 10 M 2 U 6\nMA,https://www.example.com\nENDQR\nFORM\nPRINT' },
],
logList: []
}
},
computed: {
commandPlaceholder() {
const placeholders = {
'TSPL': 'TSPL/TSCL 指令,如:\nSIZE 50 mm 30 mm\nGAP 2 mm\nCLS\nTEXT 50 50 "4" 0 1 1 "Hello"\nPRINT',
'CPCL': 'CPCL 指令,如:\n! U 200\nPAGE-WIDTH 600\nTEXT 0 0 30 "Hello"\nPRINT',
'ESC': 'ESC/POS 指令,如:\n1B 40 (初始化)\n30 80 (打印文字)\n1B 56 00 (打印并切纸)'
}
return placeholders[this.commandType]
}
},
onLoad() {
this.initBluetooth()
this.setupListeners()
},
onUnload() {
this.cleanup()
},
methods: {
addLog(message, type = 'info') {
const now = new Date()
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
this.logList.unshift({ time, message, type })
if (this.logList.length > 100) {
this.logList.pop()
}
console.log(`[BLE Print] ${message}`)
},
clearLog() {
this.logList = []
},
setupListeners() {
uni.onBluetoothAdapterStateChange((res) => {
this.addLog(`适配器: available=${res.available}, discovering=${res.discovering}`)
this.adapterState.available = res.available
this.adapterState.discovering = res.discovering
})
uni.onBluetoothDeviceFound((res) => {
const devices = res.devices || []
devices.forEach(device => {
if (!device.name && !device.localName) return
const index = this.deviceList.findIndex(d => d.deviceId === device.deviceId)
if (index === -1) {
this.deviceList.push(device)
this.addLog(`发现: ${device.name || device.localName}`)
}
})
})
uni.onBLEConnectionStateChange((res) => {
if (!res.connected && this.connectedDeviceId === res.deviceId) {
this.connected = false
this.connectedDeviceId = ''
this.connectedDeviceName = ''
this.addLog('设备已断开', 'warn')
}
})
},
initBluetooth() {
if (this.isInitializing) return
this.isInitializing = true
this.addLog('初始化蓝牙...')
uni.openBluetoothAdapter({
success: (res) => {
this.adapterState.available = true
this.isInitializing = false
this.addLog('蓝牙初始化成功')
this.getKnownDevices()
},
fail: (err) => {
this.isInitializing = false
this.adapterState.available = false
this.addLog(`初始化失败: ${err.errMsg}`, 'error')
}
})
},
getKnownDevices() {
uni.getBluetoothDevices({
success: (res) => {
if (res.devices && res.devices.length > 0) {
this.deviceList = res.devices.filter(d => d.name || d.localName)
}
}
})
},
startDiscovery() {
if (this.adapterState.discovering) return
this.deviceList = []
this.addLog('开始搜索...')
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey: true,
success: () => {
this.adapterState.discovering = true
this.addLog('搜索中...')
setTimeout(() => {
if (this.adapterState.discovering) {
this.stopDiscovery()
}
}, 15000)
},
fail: (err) => {
this.addLog(`搜索失败: ${err.errMsg}`, 'error')
}
})
},
stopDiscovery() {
uni.stopBluetoothDevicesDiscovery({
success: () => {
this.adapterState.discovering = false
this.addLog('已停止搜索')
}
})
},
connectDevice(device) {
if (this.connected) return
this.connectingDeviceId = device.deviceId
this.addLog(`连接 ${device.name || device.localName}...`)
uni.createBLEConnection({
deviceId: device.deviceId,
timeout: 10000,
success: () => {
this.connected = true
this.connectedDeviceId = device.deviceId
this.connectedDeviceName = device.name || device.localName
this.connectingDeviceId = ''
this.addLog('连接成功')
this.getPrinterServices()
},
fail: (err) => {
this.connectingDeviceId = ''
this.addLog(`连接失败: ${err.errMsg}`, 'error')
}
})
},
disconnectPrinter() {
if (!this.connectedDeviceId) return
this.addLog('断开连接...')
uni.closeBLEConnection({
deviceId: this.connectedDeviceId,
success: () => {
this.connected = false
this.connectedDeviceId = ''
this.connectedDeviceName = ''
this.serviceList = []
this.characteristicList = []
this.addLog('已断开')
},
fail: (err) => {
this.addLog(`断开失败: ${err.errMsg}`, 'error')
}
})
},
getPrinterServices() {
this.addLog('获取服务...')
uni.getBLEDeviceServices({
deviceId: this.connectedDeviceId,
success: (res) => {
this.serviceList = res.services
this.addLog(`找到 ${res.services.length} 个服务`)
// 自动选择第一个主服务
const primaryService = res.services.find(s => s.isPrimary)
if (primaryService) {
this.selectService(primaryService)
}
},
fail: (err) => {
this.addLog(`获取服务失败: ${err.errMsg}`, 'error')
}
})
},
selectService(service) {
this.selectedService = service
this.selectedServiceId = service.uuid
this.addLog(`选择服务: ${service.uuid}`)
uni.getBLEDeviceCharacteristics({
deviceId: this.connectedDeviceId,
serviceId: service.uuid,
success: (res) => {
console.log(res)
this.characteristicList = res.characteristics
// 更新调试信息
this.debugInfo = JSON.stringify({
service: service,
characteristics: res.characteristics
}, null, 2)
// 查找可写特征值
const writableChar = res.characteristics.find(c => c.properties.write)
if (writableChar) {
this.writeCharacteristicId = writableChar.uuid
this.writeServiceId = service.uuid
this.addLog(`可写特征值: ${writableChar.uuid}`)
} else {
// 尝试找可写无响应的
const writeNoRespChar = res.characteristics.find(c => c.properties.writeNoResponse)
if (writeNoRespChar) {
this.writeCharacteristicId = writeNoRespChar.uuid
this.writeServiceId = service.uuid
this.addLog(`可写特征值(writeNoResp): ${writeNoRespChar.uuid}`)
} else {
// 尝试 writeWithoutResponse
const writeWithoutRespChar = res.characteristics.find(c => c.properties.writeWithoutResponse)
if (writeWithoutRespChar) {
this.writeCharacteristicId = writeWithoutRespChar.uuid
this.writeServiceId = service.uuid
this.addLog(`可写特征值(writeWithoutResp): ${writeWithoutRespChar.uuid}`)
} else {
// 没有找到可写特征值,列出所有特征值的属性
const props = res.characteristics.map(c => ({
uuid: c.uuid,
props: c.properties
}))
this.addLog(`未找到可写特征值,特征值数: ${res.characteristics.length}`, 'warn')
}
}
}
},
fail: (err) => {
this.addLog(`获取特征值失败: ${err.errMsg}`, 'error')
}
})
},
selectCharacteristic(char, service) {
if (!service) {
this.addLog('请先选择服务', 'warn')
return
}
// 检查是否有任何可写属性
if (!this.isWritable(char)) {
this.addLog('该特征值无可写属性', 'warn')
return
}
this.writeCharacteristicId = char.uuid
this.writeServiceId = service.uuid
this.addLog(`已选择特征值: ${char.uuid}`)
uni.showToast({ title: '特征值已选择', icon: 'success' })
},
isWritable(char) {
return char.properties && (
char.properties.write ||
char.properties.writeNoResponse ||
char.properties.writeWithoutResponse
)
},
toggleDebugInfo(e) {
this.showDebugInfo = e.detail.value
},
insertTemplate(template) {
this.commandContent = template.command
this.addLog(`已插入模板: ${template.name}`)
},
appendCommand(cmd) {
if (this.commandContent) {
this.commandContent += '\n' + cmd
} else {
this.commandContent = cmd
}
},
clearCommand() {
this.commandContent = ''
},
printCommand() {
if (!this.connected) {
uni.showToast({ title: '请先连接打印机', icon: 'none' })
return
}
if (!this.commandContent.trim()) {
uni.showToast({ title: '请输入打印指令', icon: 'none' })
return
}
if (!this.writeCharacteristicId) {
uni.showToast({ title: '未找到可写特征值', icon: 'none' })
return
}
const buffer = this.stringToBuffer(this.commandContent)
this.addLog(`发送指令 (${buffer.byteLength} bytes)`)
uni.writeBLECharacteristicValue({
deviceId: this.connectedDeviceId,
serviceId: this.writeServiceId,
characteristicId: this.writeCharacteristicId,
value: buffer,
writeType: 'writeNoResponse',
success: () => {
this.addLog('发送成功', 'success')
uni.showToast({ title: '发送成功', icon: 'success' })
},
fail: (err) => {
this.addLog(`发送失败: ${err.errMsg}`, 'error')
uni.showToast({ title: '发送失败', icon: 'none' })
}
})
},
sendByte() {
if (!this.connected || !this.writeCharacteristicId) {
uni.showToast({ title: '请先连接打印机', icon: 'none' })
return
}
if (!this.byteInput.trim()) {
uni.showToast({ title: '请输入字节值', icon: 'none' })
return
}
let byteValue
const input = this.byteInput.trim()
if (input.startsWith('0x') || input.startsWith('0X')) {
byteValue = parseInt(input, 16)
} else {
byteValue = parseInt(input, 10)
}
if (isNaN(byteValue) || byteValue < 0 || byteValue > 255) {
uni.showToast({ title: '请输入0-255的有效值', icon: 'none' })
return
}
const buffer = new ArrayBuffer(1)
const dataView = new DataView(buffer)
dataView.setUint8(0, byteValue)
this.addLog(`发送字节: ${byteValue} (0x${byteValue.toString(16).padStart(2, '0')})`)
uni.writeBLECharacteristicValue({
deviceId: this.connectedDeviceId,
serviceId: this.writeServiceId,
characteristicId: this.writeCharacteristicId,
value: buffer,
writeType: 'writeNoResponse',
success: () => {
this.addLog('字节发送成功', 'success')
},
fail: (err) => {
this.addLog(`发送失败: ${err.errMsg}`, 'error')
}
})
},
// stringToBuffer(str) {
// 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))
// }
// return buffer
// },
stringToBuffer(str) {
try {
// 尝试使用 TextEncoderUTF-8 编码)
// const encoder = new TextEncoder()
// const uint8Array = encoder.encode(str)
// return uint8Array.buffer
// 使用 iconv-lite 将字符串编码为 GBK 格式的字节数组
// 这是一个 Uint8Array,需要转换成 writeBLECharacteristicValue 需要的 ArrayBuffer
const uint8Array = iconv.encode(str, 'gbk');
// 核心:转换为 ArrayBuffer
return uint8Array.buffer;
} catch (e) {
console.warn('TextEncoder 不可用,使用回退方案', 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) // 只取低8位
}
return buffer
}
},
cleanup() {
if (this.connected) {
this.disconnectPrinter()
}
if (this.adapterState.discovering) {
this.stopDiscovery()
}
},
saveCurrentConfig() {
if (!this.connected) {
uni.showToast({ title: '请先连接打印机', icon: 'none' })
return
}
if (!this.writeServiceId || !this.writeCharacteristicId) {
uni.showToast({ title: '请先选择服务和特征值', icon: 'none' })
return
}
const config = {
deviceId: this.connectedDeviceId,
serviceId: this.writeServiceId,
writeCharId: this.writeCharacteristicId,
deviceName: this.connectedDeviceName,
lastUpdated: new Date().toISOString()
}
uni.setStorageSync('printerConfig', config)
this.addLog(`配置已保存: ${this.connectedDeviceName}`, 'success')
uni.showToast({
title: '配置已保存',
icon: 'success',
duration: 2000
})
}
}
}
</script>
<style scoped>
.container {
padding: 20rpx;
background: #f5f5f5;
min-height: 100vh;
}
.status-bar {
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;
}
.action-section {
display: flex;
gap: 16rpx;
margin-bottom: 20rpx;
flex-wrap: wrap;
}
.action-section button {
flex: 1;
min-width: 150rpx;
}
.section {
background: #fff;
border-radius: 12rpx;
padding: 20rpx;
margin-bottom: 20rpx;
}
.header-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.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;
}
.device-list {
max-height: 300rpx;
}
.device-item {
padding: 24rpx;
border-bottom: 1rpx solid #eee;
}
.device-item:active {
background: #f5f5f5;
}
.device-item.connecting {
background: #e8f4ff;
}
.device-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
display: block;
}
.device-id {
font-size: 22rpx;
color: #999;
word-break: break-all;
}
.connect-hint {
font-size: 24rpx;
color: #007aff;
}
.empty-tip {
text-align: center;
padding: 40rpx;
color: #999;
}
.print-section {
min-height: 600rpx;
}
.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;
white-space: nowrap;
}
.template-tag:active {
background: #e0e0e0;
}
.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;
}
.command-input {
margin-bottom: 20rpx;
}
.input-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.input-title {
font-size: 26rpx;
color: #666;
}
.input-actions {
display: flex;
gap: 12rpx;
}
.command-textarea {
width: 100%;
height: 200rpx;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 16rpx;
font-size: 26rpx;
font-family: 'Courier New', monospace;
box-sizing: border-box;
}
.params-section {
margin-bottom: 20rpx;
}
.params-title {
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
.param-grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.param-item {
padding: 16rpx 24rpx;
background: #f0f0f0;
border-radius: 8rpx;
}
.param-item:active {
background: #e0e0e0;
}
.param-item text {
font-size: 24rpx;
color: #333;
}
.byte-section {
padding-top: 16rpx;
border-top: 1rpx solid #eee;
}
.byte-title {
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
.byte-row {
display: flex;
gap: 12rpx;
margin-bottom: 8rpx;
}
.byte-input {
flex: 1;
border: 1rpx solid #ddd;
border-radius: 8rpx;
padding: 12rpx 16rpx;
font-size: 26rpx;
}
.byte-hint {
font-size: 22rpx;
color: #999;
}
.log-section {
min-height: 200rpx;
}
.log-list {
height: 250rpx;
background: #1c1c1c;
border-radius: 8rpx;
padding: 16rpx;
}
.log-item {
display: block;
font-size: 22rpx;
font-family: 'Courier New', monospace;
color: #fff;
margin-bottom: 8rpx;
line-height: 1.4;
}
.log-item.success {
color: #34c759;
}
.log-item.warn {
color: #ff9500;
}
.log-item.error {
color: #ff3b30;
}
/* 服务选择区域 */
.service-select-section {
margin-bottom: 20rpx;
padding: 16rpx;
background: #f8f8f8;
border-radius: 8rpx;
}
.current-config {
padding: 16rpx;
background: #fff;
border-radius: 8rpx;
margin-bottom: 16rpx;
}
.config-item {
display: flex;
margin-bottom: 8rpx;
}
.config-label {
font-size: 24rpx;
color: #666;
min-width: 140rpx;
}
.config-value {
font-size: 24rpx;
color: #999;
font-family: 'Courier New', monospace;
word-break: break-all;
flex: 1;
}
.config-value.active {
color: #34c759;
}
.service-list {
margin-bottom: 16rpx;
}
.list-title {
font-size: 26rpx;
color: #333;
margin-bottom: 12rpx;
display: block;
font-weight: bold;
}
.service-scroll {
width: 100%;
white-space: nowrap;
}
.service-tag {
display: inline-flex;
align-items: center;
padding: 12rpx 20rpx;
background: #fff;
border: 2rpx solid #ddd;
border-radius: 8rpx;
margin-right: 16rpx;
white-space: nowrap;
}
.service-tag.selected {
border-color: #007aff;
background: #e8f4ff;
}
.service-index {
font-size: 24rpx;
color: #007aff;
font-weight: bold;
margin-right: 12rpx;
}
.service-uuid {
font-size: 22rpx;
font-family: 'Courier New', monospace;
color: #333;
max-width: 300rpx;
overflow: hidden;
text-overflow: ellipsis;
}
.service-type {
font-size: 20rpx;
color: #fff;
background: #34c759;
padding: 4rpx 8rpx;
border-radius: 4rpx;
margin-left: 12rpx;
}
.characteristic-list {
margin-bottom: 16rpx;
}
.characteristic-scroll {
max-height: 300rpx;
background: #fff;
border-radius: 8rpx;
}
.characteristic-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx;
border-bottom: 1rpx solid #eee;
}
.characteristic-item:last-child {
border-bottom: none;
}
.characteristic-item.selected {
background: #e8f4ff;
}
.characteristic-item.writable {
border-left: 4rpx solid #34c759;
}
.char-info {
flex: 1;
}
.char-uuid {
font-size: 24rpx;
font-family: 'Courier New', monospace;
color: #333;
display: block;
margin-bottom: 8rpx;
word-break: break-all;
}
.char-props {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
}
.prop-tag {
font-size: 20rpx;
padding: 4rpx 8rpx;
background: #f0f0f0;
border-radius: 4rpx;
color: #666;
}
.select-btn {
font-size: 24rpx;
color: #007aff;
padding: 8rpx 16rpx;
}
.debug-info {
margin-top: 16rpx;
}
.debug-title {
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
display: block;
}
.debug-content {
background: #1c1c1c;
border-radius: 8rpx;
padding: 16rpx;
max-height: 300rpx;
overflow-y: auto;
}
.debug-json {
font-size: 20rpx;
font-family: 'Courier New', monospace;
color: #fff;
white-space: pre-wrap;
word-break: break-all;
}
</style>