918 lines
24 KiB
Vue
918 lines
24 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>
|
||
|
||
<!-- ========== 蓝牙操作按钮 ========== -->
|
||
<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="disconnectScale">
|
||
断开连接
|
||
</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" v-else>
|
||
<view class="section-header">
|
||
<text class="section-title">设备配置</text>
|
||
<text class="device-name-tag">{{ connectedDeviceName }}</text>
|
||
</view>
|
||
|
||
<!-- 服务配置区域 -->
|
||
<view class="service-config-section" v-if="serviceList.length > 0">
|
||
<!-- 当前配置显示 -->
|
||
<view class="current-config">
|
||
<view class="config-item">
|
||
<text class="config-label">服务UUID:</text>
|
||
<text class="config-value" :class="{ active: notifyServiceId }">{{ notifyServiceId || '未选择' }}</text>
|
||
</view>
|
||
<view class="config-item">
|
||
<text class="config-label">通知特征值:</text>
|
||
<text class="config-value" :class="{ active: notifyCharacteristicId }">{{ notifyCharacteristicId || '未选择' }}</text>
|
||
</view>
|
||
<view class="config-item">
|
||
<text class="config-label">写入特征值:</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: selectedServiceId === service.uuid }"
|
||
@click="selectService(service)">
|
||
<text class="service-index">{{ index + 1 }}</text>
|
||
<text class="service-uuid">{{ service.uuid }}</text>
|
||
<text class="service-type" v-if="service.isPrimary">主</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 特征值列表(优先显示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: notifyCharacteristicId === char.uuid || writeCharacteristicId === char.uuid,
|
||
notifiable: isNotifiable(char),
|
||
writable: isWritable(char)
|
||
}"
|
||
@click="selectCharacteristic(char)">
|
||
<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.read">read</text>
|
||
<text class="prop-tag notify" v-if="char.properties.notify">notify</text>
|
||
<text class="prop-tag indicate" v-if="char.properties.indicate">indicate</text>
|
||
</view>
|
||
</view>
|
||
<view class="char-actions">
|
||
<text class="select-btn notify-btn" v-if="notifyCharacteristicId === char.uuid">✓通知</text>
|
||
<text class="select-btn write-btn" v-else-if="writeCharacteristicId === char.uuid">✓写入</text>
|
||
<text class="select-btn" v-else @click.stop="selectCharacteristic(char)">选择</text>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
|
||
<!-- 保存配置按钮 -->
|
||
<view class="save-btn-wrap">
|
||
<button type="primary" @click="saveCurrentConfig" :disabled="!notifyCharacteristicId">
|
||
保存配置
|
||
</button>
|
||
<button type="default" @click="goToTestPage" v-if="notifyCharacteristicId">
|
||
去测试页面
|
||
</button>
|
||
</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. 连接选定的设备,获取并配置蓝牙服务和特征值(notify类型)
|
||
* 3. 支持保存/加载配置到 Vuex store
|
||
* 4. 显示操作日志便于调试
|
||
*/
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
// ========== 蓝牙适配器状态 ==========
|
||
adapterState: {
|
||
available: false,
|
||
discovering: false
|
||
},
|
||
isInitializing: false,
|
||
|
||
// ========== 设备列表 ==========
|
||
deviceList: [],
|
||
connectingDeviceId: '',
|
||
|
||
// ========== 连接状态 ==========
|
||
connectedDeviceId: '',
|
||
connectedDeviceName: '',
|
||
connected: false,
|
||
|
||
// ========== 服务和特征值 ==========
|
||
notifyCharacteristicId: '',
|
||
notifyServiceId: '',
|
||
writeCharacteristicId: '',
|
||
writeServiceId: '',
|
||
serviceList: [],
|
||
characteristicList: [],
|
||
selectedServiceId: '',
|
||
|
||
// ========== 操作日志 ==========
|
||
logList: [],
|
||
|
||
}
|
||
},
|
||
computed: {
|
||
...mapGetters('scale', ['scaleServiceId', 'scaleCharacteristicId'])
|
||
},
|
||
onLoad() {
|
||
this.initBluetooth()
|
||
this.setupListeners()
|
||
this.loadSavedConfig()
|
||
},
|
||
onUnload() {
|
||
this.cleanup()
|
||
},
|
||
methods: {
|
||
...mapActions('scale', ['saveScaleConfig']),
|
||
|
||
/**
|
||
* 添加日志记录
|
||
*/
|
||
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 Scale] ${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.getDeviceServices()
|
||
},
|
||
fail: (err) => {
|
||
this.connectingDeviceId = ''
|
||
this.addLog(`连接失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 断开连接
|
||
*/
|
||
disconnectScale() {
|
||
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')
|
||
}
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 获取设备服务列表
|
||
*/
|
||
getDeviceServices() {
|
||
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.selectedServiceId = service.uuid
|
||
this.addLog(`选择服务: ${service.uuid}`)
|
||
uni.getBLEDeviceCharacteristics({
|
||
deviceId: this.connectedDeviceId,
|
||
serviceId: service.uuid,
|
||
success: (res) => {
|
||
this.characteristicList = res.characteristics
|
||
// 优先查找notify或indicate特征值(称重设备通常用通知方式传输数据)
|
||
const notifyChar = res.characteristics.find(c => c.properties.notify)
|
||
if (notifyChar) {
|
||
this.notifyCharacteristicId = notifyChar.uuid
|
||
this.notifyServiceId = service.uuid
|
||
this.addLog(`notify特征值: ${notifyChar.uuid}`)
|
||
} else {
|
||
const indicateChar = res.characteristics.find(c => c.properties.indicate)
|
||
if (indicateChar) {
|
||
this.notifyCharacteristicId = indicateChar.uuid
|
||
this.notifyServiceId = service.uuid
|
||
this.addLog(`indicate特征值: ${indicateChar.uuid}`)
|
||
} else {
|
||
this.addLog(`未找到notify特征值,特征值数: ${res.characteristics.length}`, 'warn')
|
||
}
|
||
}
|
||
|
||
// 同时查找可写特征值(用于发送命令)
|
||
const writeChar = res.characteristics.find(c => c.properties.write || c.properties.writeNoResponse)
|
||
if (writeChar) {
|
||
this.writeCharacteristicId = writeChar.uuid
|
||
this.writeServiceId = service.uuid
|
||
this.addLog(`写入特征值: ${writeChar.uuid}`)
|
||
}
|
||
},
|
||
fail: (err) => {
|
||
this.addLog(`获取特征值失败: ${err.errMsg}`, 'error')
|
||
}
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 选择特征值(点击切换通知/写入模式)
|
||
*/
|
||
selectCharacteristic(char) {
|
||
// 如果当前已选择此特征值,切换模式
|
||
if (this.notifyCharacteristicId === char.uuid) {
|
||
// 如果是可写的,切换到写入模式
|
||
if (this.isWritable(char)) {
|
||
this.notifyCharacteristicId = ''
|
||
this.writeCharacteristicId = char.uuid
|
||
this.writeServiceId = this.selectedServiceId
|
||
this.addLog(`切换为写入特征值: ${char.uuid}`)
|
||
uni.showToast({ title: '已切换为写入模式', icon: 'none' })
|
||
} else {
|
||
this.addLog('此特征值不可写入', 'warn')
|
||
}
|
||
} else if (this.writeCharacteristicId === char.uuid) {
|
||
// 切换回通知模式
|
||
this.writeCharacteristicId = ''
|
||
this.notifyCharacteristicId = char.uuid
|
||
this.notifyServiceId = this.selectedServiceId
|
||
this.addLog(`切换为通知特征值: ${char.uuid}`)
|
||
uni.showToast({ title: '已切换为通知模式', icon: 'none' })
|
||
} else {
|
||
// 默认选择为通知模式
|
||
if (this.isNotifiable(char)) {
|
||
this.notifyCharacteristicId = char.uuid
|
||
this.notifyServiceId = this.selectedServiceId
|
||
this.addLog(`已选择通知特征值: ${char.uuid}`)
|
||
} else if (this.isWritable(char)) {
|
||
this.writeCharacteristicId = char.uuid
|
||
this.writeServiceId = this.selectedServiceId
|
||
this.addLog(`已选择写入特征值: ${char.uuid}`)
|
||
} else {
|
||
this.notifyCharacteristicId = char.uuid
|
||
this.notifyServiceId = this.selectedServiceId
|
||
this.addLog(`已选择特征值: ${char.uuid}`, 'warn')
|
||
}
|
||
uni.showToast({ title: '特征值已选择', icon: 'success' })
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 检查特征值是否为notify/indicate类型
|
||
*/
|
||
isNotifiable(char) {
|
||
return char.properties && (char.properties.notify || char.properties.indicate)
|
||
},
|
||
|
||
/**
|
||
* 检查特征值是否可写
|
||
*/
|
||
isWritable(char) {
|
||
return char.properties && (char.properties.write || char.properties.writeNoResponse)
|
||
},
|
||
|
||
/**
|
||
* 跳转到测试页面
|
||
*/
|
||
goToTestPage() {
|
||
uni.navigateTo({
|
||
url: '/pages/settings/devices/scaleTest'
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 保存当前配置
|
||
*/
|
||
async saveCurrentConfig() {
|
||
if (!this.connected) {
|
||
uni.showToast({ title: '请先连接设备', icon: 'none' })
|
||
return
|
||
}
|
||
if (!this.notifyServiceId || !this.notifyCharacteristicId) {
|
||
uni.showToast({ title: '请先选择服务和特征值', icon: 'none' })
|
||
return
|
||
}
|
||
const config = {
|
||
deviceId: this.connectedDeviceId,
|
||
deviceName: this.connectedDeviceName,
|
||
serviceId: this.notifyServiceId,
|
||
characteristicId: this.notifyCharacteristicId,
|
||
writeCharacteristicId: this.writeCharacteristicId || '',
|
||
connected: true,
|
||
lastConfigured: new Date().toISOString()
|
||
}
|
||
try {
|
||
await this.saveScaleConfig(config)
|
||
this.addLog(`配置已保存: ${this.connectedDeviceName}`, 'success')
|
||
uni.showToast({
|
||
title: '配置已保存',
|
||
icon: 'success',
|
||
duration: 2000
|
||
})
|
||
} catch (error) {
|
||
this.addLog(`保存配置失败: ${error.message}`, 'error')
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 加载保存的配置
|
||
*/
|
||
loadSavedConfig() {
|
||
if (this.scaleServiceId) {
|
||
this.addLog(`已保存配置: 服务=${this.scaleServiceId}`)
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 清理资源
|
||
*/
|
||
cleanup() {
|
||
if (this.connected) {
|
||
this.disconnectScale()
|
||
}
|
||
if (this.adapterState.discovering) {
|
||
this.stopDiscovery()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</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;
|
||
}
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16rpx;
|
||
}
|
||
.section-title {
|
||
font-size: 30rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
.device-name-tag {
|
||
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;
|
||
}
|
||
|
||
/* ========== 预设设备提示 ========== */
|
||
.preset-tip {
|
||
background: #fffbe6;
|
||
border: 1rpx solid #ffe58f;
|
||
border-radius: 8rpx;
|
||
padding: 16rpx;
|
||
margin-bottom: 20rpx;
|
||
}
|
||
.preset-title {
|
||
font-size: 24rpx;
|
||
color: #d48806;
|
||
display: block;
|
||
margin-bottom: 12rpx;
|
||
}
|
||
.preset-item {
|
||
padding: 8rpx 0;
|
||
border-bottom: 1rpx dashed #ffe58f;
|
||
}
|
||
.preset-item:last-child {
|
||
border-bottom: none;
|
||
}
|
||
.preset-name {
|
||
font-size: 24rpx;
|
||
color: #333;
|
||
font-weight: bold;
|
||
display: block;
|
||
}
|
||
.preset-uuid {
|
||
font-size: 20rpx;
|
||
color: #666;
|
||
font-family: 'Courier New', monospace;
|
||
display: block;
|
||
word-break: break-all;
|
||
}
|
||
|
||
/* ========== 服务配置区域 ========== */
|
||
.service-config-section {
|
||
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.notifiable {
|
||
border-left: 4rpx solid #007aff;
|
||
}
|
||
.characteristic-item.writable {
|
||
border-left: 4rpx solid #ff9500;
|
||
}
|
||
.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;
|
||
}
|
||
.prop-tag.notify {
|
||
background: #e6f7ff;
|
||
color: #007aff;
|
||
}
|
||
.prop-tag.indicate {
|
||
background: #f6ffed;
|
||
color: #52c41a;
|
||
}
|
||
.char-actions {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.select-btn {
|
||
font-size: 24rpx;
|
||
color: #007aff;
|
||
padding: 8rpx 16rpx;
|
||
}
|
||
.notify-btn {
|
||
color: #34c759;
|
||
font-weight: bold;
|
||
}
|
||
.write-btn {
|
||
color: #ff9500;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* ========== 保存按钮 ========== */
|
||
.save-btn-wrap {
|
||
margin-top: 20rpx;
|
||
display: flex;
|
||
gap: 20rpx;
|
||
}
|
||
.save-btn-wrap button {
|
||
flex: 1;
|
||
}
|
||
|
||
/* ========== 日志区域 ========== */
|
||
.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;
|
||
}
|
||
</style>
|