Files
uni-pda/pages/settings/devices/printer.vue
T
2026-06-02 15:28:25 +08:00

943 lines
25 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-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>
<!-- ========== 蓝牙操作按钮 ========== -->
<!-- 初始化蓝牙搜索设备断开连接 -->
<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>
<!-- ===== 蓝牙服务配置区域 ===== -->
<!-- 用于配置蓝牙服务的UUID和特征值这是蓝牙打印机通信的关键参数 -->
<view class="service-select-section" v-if="serviceList.length > 0">
<view class="section-header">
<text class="section-title">蓝牙服务配置</text>
<view class="header-actions">
<!-- 保存当前配置到本地存储下次可直接使用 -->
<button size="mini" type="primary" @click="saveCurrentConfig" :disabled="!connected || !writeCharacteristicId">保存配置</button>
</view>
</view>
<!-- ===== 打印纸尺寸选择 ===== -->
<view class="paper-size-section">
<text class="section-title">打印纸尺寸</text>
<view class="paper-size-grid">
<view
class="paper-size-item"
:class="{ selected: paperWidth === 100 && paperHeight === 100 }"
@click="selectPaperSize(100, 100)">
<text class="paper-size-name">100mm x 100mm</text>
</view>
<view
class="paper-size-item"
:class="{ selected: paperWidth === 150 && paperHeight === 150 }"
@click="selectPaperSize(150, 150)">
<text class="paper-size-name">150mm x 150mm</text>
</view>
<view
class="paper-size-item"
:class="{ selected: paperWidth === 200 && paperHeight === 200 }"
@click="selectPaperSize(200, 200)">
<text class="paper-size-name">200mm x 200mm</text>
</view>
</view>
</view>
<!-- 当前已选择的服务和特征值UUID -->
<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>
<!-- 特征值列表显示每个特征值的属性write/read/notify等 -->
<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>
</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 { mapGetters, mapActions } from 'vuex'
/**
* 蓝牙标签打印机配置页面
*
* 功能说明:
* 1. 初始化蓝牙适配器,搜索附近的蓝牙打印机设备
* 2. 连接选定的打印机,获取并配置蓝牙服务和特征值
* 3. 支持保存/加载打印机配置到 Vuex store
* 4. 显示操作日志便于调试
*
* 使用说明:
* - 首次使用需要初始化蓝牙并搜索设备
* - 连接设备后选择服务和特征值(通常自动选择可写的)
* - 点击"保存配置"保存当前配置,下次可直接使用
*/
export default {
data() {
return {
// ========== 蓝牙适配器状态 ==========
// available: 蓝牙适配器是否可用
// discovering: 是否正在搜索设备
adapterState: {
available: false,
discovering: false
},
isInitializing: false, // 是否正在初始化蓝牙
// ========== 设备列表 ==========
deviceList: [], // 搜索到的蓝牙设备列表
connectingDeviceId: '', // 正在连接的设备ID
// ========== 连接状态 ==========
connectedDeviceId: '', // 已连接设备ID
connectedDeviceName: '', // 已连接设备名称
connected: false, // 是否已连接打印机
// ========== 服务和特征值 ==========
// writeServiceId: 可写服务UUID(用于发送数据)
// writeCharacteristicId: 可写特征值UUID(数据写入点)
// serviceList: 设备的所有服务列表
// characteristicList: 当前服务的所有特征值列表
// selectedService: 当前选中的服务对象
// selectedServiceId: 当前选中的服务UUID
writeCharacteristicId: '',
writeServiceId: '',
serviceList: [],
characteristicList: [],
selectedService: null,
selectedServiceId: '',
// ========== 操作日志 ==========
// 记录所有操作和调试信息,最多保留100条
logList: []
}
},
computed: {
// 从 Vuex store 映射纸尺寸
...mapGetters('print', ['paperWidth', 'paperHeight'])
},
onLoad() {
// 页面加载时自动初始化蓝牙
this.initBluetooth()
// 设置蓝牙状态变化监听
this.setupListeners()
// 加载保存的配置(从 store 获取)
this.loadSavedConfig()
},
onUnload() {
// 页面卸载时清理资源(断开连接、停止搜索)
this.cleanup()
},
methods: {
// 映射 Vuex actions
...mapActions('print', ['updatePaperSize', 'savePrinterConfig']),
/**
* 添加日志记录
* @param {string} message - 日志消息内容
* @param {string} type - 日志类型:info/success/warn/error
*/
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 })
// 最多保留100条日志
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)
}
}
})
},
/**
* 开始搜索附近的蓝牙设备
* 搜索时长默认15秒
*/
startDiscovery() {
if (this.adapterState.discovering) return
this.deviceList = []
this.addLog('开始搜索...')
uni.startBluetoothDevicesDiscovery({
allowDuplicatesKey: true, // 允许重复发现同一设备
success: () => {
this.adapterState.discovering = true
this.addLog('搜索中...')
// 15秒后自动停止搜索
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('已停止搜索')
}
})
},
/**
* 连接指定的蓝牙设备
* @param {Object} device - 设备对象,包含deviceId和name
*/
connectDevice(device) {
if (this.connected) return
this.connectingDeviceId = device.deviceId
this.addLog(`连接 ${device.name || device.localName}...`)
uni.createBLEConnection({
deviceId: device.deviceId,
timeout: 10000, // 10秒超时
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')
}
})
},
/**
* 选择服务并获取其特征值列表
* @param {Object} service - 选中的服务对象
*/
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
// 查找可写特征值,按优先级:write > writeNoResponse > writeWithoutResponse
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 {
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 {
this.addLog(`未找到可写特征值,特征值数: ${res.characteristics.length}`, 'warn')
}
}
}
},
fail: (err) => {
this.addLog(`获取特征值失败: ${err.errMsg}`, 'error')
}
})
},
/**
* 选择特征值
* @param {Object} char - 选中的特征值对象
* @param {Object} service - 所属的服务对象
*/
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' })
},
/**
* 检查特征值是否可写
* @param {Object} char - 特征值对象
* @returns {boolean} 是否可写
*/
isWritable(char) {
return char.properties && (
char.properties.write ||
char.properties.writeNoResponse ||
char.properties.writeWithoutResponse
)
},
/**
* 选择打印纸尺寸
* @param {number} width - 纸张宽度(mm)
* @param {number} height - 纸张高度(mm)
*/
async selectPaperSize(width, height) {
try {
await this.updatePaperSize({ width, height })
this.addLog(`已选择纸尺寸: ${width}mm x ${height}mm`)
uni.showToast({
title: `已选择 ${width}mm x ${height}mm`,
icon: 'success',
duration: 1500
})
} catch (error) {
this.addLog(`选择纸尺寸失败: ${error.message}`, 'error')
}
},
/**
* 清理资源
* 断开连接并停止搜索
*/
cleanup() {
if (this.connected) {
this.disconnectPrinter()
}
if (this.adapterState.discovering) {
this.stopDiscovery()
}
},
/**
* 保存当前配置到 Vuex store(自动同步到本地存储)
* 包括设备ID、服务UUID、特征值UUID等信息
* 保存后可快速加载使用
*/
async 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,
connected: true,
lastUpdated: new Date().toISOString()
}
try {
await this.savePrinterConfig(config)
this.addLog(`配置已保存: ${this.connectedDeviceName}`, 'success')
uni.showToast({
title: '配置已保存',
icon: 'success',
duration: 2000
})
} catch (error) {
this.addLog(`保存配置失败: ${error.message}`, 'error')
}
},
/**
* 加载保存的配置
* 纸尺寸通过 computed 从 store 获取,无需手动加载
*/
loadSavedConfig() {
// 纸尺寸通过 mapGetters 映射,无需手动加载
this.addLog(`纸尺寸: ${this.paperWidth}mm x ${this.paperHeight}mm`)
}
}
}
</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; /* 绿色 - 已连接 */
}
/* ========== 操作按钮区域 ========== */
.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: 200rpx;
}
/* ========== 打印纸尺寸选择 ========== */
.paper-size-section {
margin-bottom: 20rpx;
}
.paper-size-grid {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 12rpx;
}
.paper-size-item {
flex: 1;
min-width: 200rpx;
padding: 20rpx;
background: #f0f0f0;
border-radius: 8rpx;
border: 2rpx solid transparent;
text-align: center;
}
.paper-size-item.selected {
background: #e8f4ff;
border-color: #007aff;
}
.paper-size-name {
font-size: 26rpx;
color: #333;
}
.paper-size-item.selected .paper-size-name {
color: #007aff;
font-weight: bold;
}
/* ========== 日志区域 ========== */
.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;
}
</style>