feat: 添加蓝牙调试页面的初始实现
- 新增 BLVRQPage.json 配置文件,设置导航栏样式。 - 新增 BLVRQPage.wxml 文件,构建蓝牙设备信息展示和操作界面。 - 新增 BLVRQPage.wxss 文件,定义页面样式和布局。
This commit is contained in:
773
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
Normal file
773
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.js
Normal file
@@ -0,0 +1,773 @@
|
||||
const app = getApp()
|
||||
|
||||
function ab2hex(buffer) {
|
||||
const view = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer || [])
|
||||
return Array.from(view).map(b => b.toString(16).padStart(2, '0').toUpperCase()).join(' ')
|
||||
}
|
||||
|
||||
function asciiToBytes(text) {
|
||||
const bytes = []
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
bytes.push(text.charCodeAt(i) & 0xFF)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
function bytesToAscii(bytes) {
|
||||
return String.fromCharCode(...(bytes || []).filter(v => v > 0))
|
||||
}
|
||||
|
||||
function crc16Modbus(bytes) {
|
||||
let crc = 0xFFFF
|
||||
for (let i = 0; i < bytes.length; i += 1) {
|
||||
crc ^= bytes[i]
|
||||
for (let j = 0; j < 8; j += 1) {
|
||||
const lsb = crc & 0x0001
|
||||
crc >>= 1
|
||||
if (lsb) crc ^= 0xA001
|
||||
}
|
||||
}
|
||||
return crc & 0xFFFF
|
||||
}
|
||||
|
||||
function buildPacket(frameType, params = []) {
|
||||
const payload = Array.isArray(params) ? params.slice() : Array.from(params || [])
|
||||
const len = 6 + payload.length
|
||||
const bytes = [0xDD, 0xD0, len & 0xFF, 0x00, 0x00, frameType & 0xFF, ...payload]
|
||||
const crc = crc16Modbus(bytes)
|
||||
bytes[3] = crc & 0xFF
|
||||
bytes[4] = (crc >> 8) & 0xFF
|
||||
return Uint8Array.from(bytes)
|
||||
}
|
||||
|
||||
function parsePacket(buffer) {
|
||||
const bytes = Array.from(new Uint8Array(buffer || []))
|
||||
if (bytes.length < 6) return null
|
||||
if (bytes[0] !== 0xDD || bytes[1] !== 0xD0) return null
|
||||
const len = bytes[2]
|
||||
if (len !== bytes.length) return null
|
||||
const inputCrc = bytes[3] | (bytes[4] << 8)
|
||||
const crcBytes = bytes.slice()
|
||||
crcBytes[3] = 0x00
|
||||
crcBytes[4] = 0x00
|
||||
const calcCrc = crc16Modbus(crcBytes)
|
||||
if (inputCrc !== calcCrc) {
|
||||
return { invalid: true, bytes, reason: 'CRC校验失败' }
|
||||
}
|
||||
return {
|
||||
bytes,
|
||||
len,
|
||||
frameType: bytes[5],
|
||||
params: bytes.slice(6)
|
||||
}
|
||||
}
|
||||
|
||||
function nowText() {
|
||||
const d = new Date()
|
||||
const pad = (n, l = 2) => String(n).padStart(l, '0')
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`
|
||||
}
|
||||
|
||||
function formatBleError(err) {
|
||||
if (!err) return '未知错误'
|
||||
const parts = []
|
||||
if (typeof err.errCode !== 'undefined') parts.push(`errCode=${err.errCode}`)
|
||||
if (err.errMsg) parts.push(err.errMsg)
|
||||
if (err.message && err.message !== err.errMsg) parts.push(err.message)
|
||||
return parts.length ? parts.join(' | ') : String(err)
|
||||
}
|
||||
|
||||
function createFunctionOptions() {
|
||||
const names = {
|
||||
0x00: '不设置',
|
||||
0x01: '普通按键功能',
|
||||
0x02: '清理按键',
|
||||
0x03: '投诉按键'
|
||||
}
|
||||
return Array.from({ length: 16 }, (_, i) => ({
|
||||
code: i,
|
||||
label: `${i}: ${names[i] || '无效'}`
|
||||
}))
|
||||
}
|
||||
|
||||
function createKeys(count) {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
hwIndex: i + 1,
|
||||
mappedKey: i + 1,
|
||||
mappedKeyIndex: i + 1,
|
||||
originalMappedKey: i + 1,
|
||||
functionCode: 0x01,
|
||||
functionIndex: 1
|
||||
}))
|
||||
}
|
||||
|
||||
function createKeySlots(keys, totalSlots = 8) {
|
||||
const list = Array.isArray(keys) ? keys : []
|
||||
return Array.from({ length: totalSlots }, (_, i) => {
|
||||
const key = list[i]
|
||||
if (key) {
|
||||
return {
|
||||
slotIndex: i,
|
||||
isPlaceholder: false,
|
||||
...key
|
||||
}
|
||||
}
|
||||
return {
|
||||
slotIndex: i,
|
||||
isPlaceholder: true,
|
||||
hwIndex: i + 1
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
bleInfo: {
|
||||
devName: '',
|
||||
mac: '',
|
||||
serviceId: '',
|
||||
txCharId: '',
|
||||
rxCharId: '',
|
||||
readCharId: '',
|
||||
writeCharId: '',
|
||||
connected: false,
|
||||
rssi: null,
|
||||
signalText: '-'
|
||||
},
|
||||
bleNameInput: '',
|
||||
canSetBleName: false,
|
||||
deviceInfo: {
|
||||
model: 'BLV_RQ',
|
||||
softwareVersion: '-',
|
||||
hardwareVersion: '-'
|
||||
},
|
||||
keyConfig: {
|
||||
selectedKeyCount: 0,
|
||||
selectedKeyTypeIndex: 0,
|
||||
keyCountOptions: ['请选择按键类型', '1键', '2键', '3键', '4键', '5键', '6键', '7键', '8键'],
|
||||
keyCountValues: [0, 1, 2, 3, 4, 5, 6, 7, 8],
|
||||
keys: [],
|
||||
keySlots: createKeySlots([])
|
||||
},
|
||||
functionOptions: createFunctionOptions(),
|
||||
functionOptionLabels: createFunctionOptions().map(item => item.label),
|
||||
canOperateKeyConfig: false,
|
||||
isConnecting: false,
|
||||
readingVersion: false,
|
||||
readingKeyConfig: false,
|
||||
writingKeyConfig: false,
|
||||
settingBleName: false,
|
||||
keySendingIndex: -1,
|
||||
logs: [],
|
||||
logScrollTo: '',
|
||||
confirmDialog: {
|
||||
visible: false,
|
||||
message: ''
|
||||
}
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const devName = options && options.DevName ? decodeURIComponent(options.DevName) : 'BLV_RQ设备'
|
||||
const mac = options && options.mac ? decodeURIComponent(options.mac) : ''
|
||||
const connected = options && (options.connected === '1' || options.connected === 'true')
|
||||
const serviceId = options && options.serviceId ? decodeURIComponent(options.serviceId) : ''
|
||||
const txCharId = options && options.txCharId ? decodeURIComponent(options.txCharId) : ''
|
||||
const rxCharId = options && options.rxCharId ? decodeURIComponent(options.rxCharId) : ''
|
||||
const readCharId = options && options.readCharId ? decodeURIComponent(options.readCharId) : rxCharId
|
||||
const writeCharId = options && options.writeCharId ? decodeURIComponent(options.writeCharId) : txCharId
|
||||
const resumeConnectedSession = connected || !!(mac && serviceId && txCharId && rxCharId)
|
||||
this.setData({
|
||||
bleInfo: {
|
||||
devName,
|
||||
mac,
|
||||
serviceId,
|
||||
txCharId,
|
||||
rxCharId,
|
||||
readCharId,
|
||||
writeCharId,
|
||||
connected: resumeConnectedSession,
|
||||
rssi: null,
|
||||
signalText: ''
|
||||
},
|
||||
canOperateKeyConfig: false
|
||||
})
|
||||
this.appendLog('CFG', `进入BLV_RQ页面:${devName}`)
|
||||
this.updateSetBleNameState('')
|
||||
if (app.globalData && app.globalData.pendingBleNavigation && app.globalData.pendingBleNavigation.target === 'BLV_RQ') {
|
||||
app.globalData.pendingBleNavigation = null
|
||||
}
|
||||
if (resumeConnectedSession) {
|
||||
this.activateConnectedSession('页面初始化')
|
||||
} else {
|
||||
this.ensureBleReady('页面初始化')
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.bleInfo.connected) {
|
||||
this.bindConnectionStateListener()
|
||||
this.startBleNotifications()
|
||||
this.startStatusPolling()
|
||||
this.refreshBleStatus()
|
||||
return
|
||||
}
|
||||
this.ensureBleReady('页面显示')
|
||||
},
|
||||
|
||||
onUnload() {
|
||||
this.stopStatusPolling()
|
||||
this.teardownBleNotifications()
|
||||
this.teardownConnectionStateListener()
|
||||
},
|
||||
|
||||
appendLog(type, text, extra = {}) {
|
||||
const map = { TX: 'tx', RX: 'rx', WARN: 'warn', ERR: 'err', UI: 'ui', CFG: 'cfg' }
|
||||
let displayText = text
|
||||
let hexText = ''
|
||||
let noteText = text
|
||||
if (type === 'TX' && text.includes('|')) {
|
||||
const parts = text.split('|')
|
||||
noteText = parts[0].trim()
|
||||
hexText = parts.slice(1).join('|').trim()
|
||||
displayText = hexText || noteText
|
||||
} else if (type === 'RX') {
|
||||
hexText = text.replace(/^\[[^\]]+\]\s*/, '').trim()
|
||||
displayText = hexText
|
||||
}
|
||||
const item = {
|
||||
id: Date.now() + Math.random(),
|
||||
time: nowText(),
|
||||
type,
|
||||
typeClass: map[type] || 'cfg',
|
||||
text: extra.cmd ? `[${extra.cmd}] ${text}` : text,
|
||||
displayText,
|
||||
hexText,
|
||||
noteText
|
||||
}
|
||||
const logs = [...this.data.logs, item].slice(-300)
|
||||
this.setData({ logs, logScrollTo: `log-${item.id}` })
|
||||
},
|
||||
|
||||
updateSetBleNameState(value) {
|
||||
const can = /^[A-Za-z0-9]{1,8}$/.test(value || '') && !!this.data.bleInfo.connected
|
||||
this.setData({ bleNameInput: value, canSetBleName: can })
|
||||
},
|
||||
|
||||
onBleNameInput(e) {
|
||||
const raw = (e.detail.value || '').replace(/[^A-Za-z0-9]/g, '').slice(0, 8)
|
||||
this.updateSetBleNameState(raw)
|
||||
},
|
||||
|
||||
onKeyCountChange(e) {
|
||||
const selectedKeyTypeIndex = Number(e.detail.value || 0)
|
||||
const count = this.data.keyConfig.keyCountValues[selectedKeyTypeIndex] || 0
|
||||
const keys = createKeys(count)
|
||||
this.setData({
|
||||
'keyConfig.selectedKeyTypeIndex': selectedKeyTypeIndex,
|
||||
'keyConfig.selectedKeyCount': count,
|
||||
'keyConfig.keys': keys,
|
||||
'keyConfig.keySlots': createKeySlots(keys),
|
||||
canOperateKeyConfig: !!this.data.bleInfo.connected && count > 0
|
||||
})
|
||||
this.appendLog('UI', count ? `选择按键类型:${count}键` : '清空按键类型选择')
|
||||
},
|
||||
|
||||
onFunctionChange(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const value = Number(e.detail.value || 0)
|
||||
const keys = [...this.data.keyConfig.keys]
|
||||
if (!keys[index]) return
|
||||
keys[index].functionIndex = value
|
||||
keys[index].functionCode = this.data.functionOptions[value].code
|
||||
this.setData({ 'keyConfig.keys': keys, 'keyConfig.keySlots': createKeySlots(keys) })
|
||||
},
|
||||
|
||||
async onReadVersion() {
|
||||
if (!this.data.bleInfo.connected) return
|
||||
this.setData({ readingVersion: true })
|
||||
await this.sendCommand(0x01, [0x01], '读取版本')
|
||||
this.setData({ readingVersion: false })
|
||||
},
|
||||
|
||||
async onSetBleName() {
|
||||
if (!this.data.canSetBleName) {
|
||||
wx.showToast({ title: '名称不合法', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const bytes = asciiToBytes(this.data.bleNameInput)
|
||||
this.setData({ settingBleName: true })
|
||||
await this.sendCommand(0x05, [bytes.length, ...bytes], '设置蓝牙名称')
|
||||
this.setData({ settingBleName: false })
|
||||
},
|
||||
|
||||
async onReadKeyConfig() {
|
||||
if (!this.data.bleInfo.connected) {
|
||||
wx.showToast({ title: '请先连接设备', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ readingKeyConfig: true })
|
||||
await this.sendCommand(0x04, [0x01], '读取按键配置')
|
||||
this.setData({ readingKeyConfig: false })
|
||||
},
|
||||
|
||||
onSubmitKeyConfig() {
|
||||
const validation = this.validateKeyConfig()
|
||||
if (validation.blocked) {
|
||||
wx.showModal({ title: '配置校验失败', content: validation.message, showCancel: false })
|
||||
return
|
||||
}
|
||||
const message = `确认下发当前${validation.keyCount}键的按键功能配置?`
|
||||
this._pendingSubmit = validation
|
||||
this.setData({ 'confirmDialog.visible': true, 'confirmDialog.message': message })
|
||||
},
|
||||
|
||||
onCancelSubmit() {
|
||||
this._pendingSubmit = null
|
||||
this.setData({ 'confirmDialog.visible': false, 'confirmDialog.message': '' })
|
||||
},
|
||||
|
||||
async onConfirmSubmit() {
|
||||
const validation = this._pendingSubmit || this.validateKeyConfig()
|
||||
this.onCancelSubmit()
|
||||
const keys = validation.keys
|
||||
const keyCount = validation.keyCount
|
||||
const functionPayload = [keyCount]
|
||||
for (let i = 0; i < keyCount; i += 1) {
|
||||
const item = keys[i]
|
||||
functionPayload.push(item ? item.functionCode : 0x00)
|
||||
}
|
||||
this.setData({ writingKeyConfig: true })
|
||||
const step1 = await this.sendCommand(0x02, functionPayload, '设置按键功能', { expectResponse: true, silentError: true })
|
||||
if (!step1) {
|
||||
this.setData({ writingKeyConfig: false })
|
||||
wx.showToast({ title: '功能设置失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ writingKeyConfig: false })
|
||||
wx.showToast({ title: '设置成功', icon: 'success' })
|
||||
},
|
||||
|
||||
async onTriggerKey(e) {
|
||||
const index = Number(e.currentTarget.dataset.index)
|
||||
const item = this.data.keyConfig.keys[index]
|
||||
if (!item || !item.mappedKey) {
|
||||
wx.showToast({ title: '当前按键未配置映射', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const keyCount = Number(this.data.keyConfig.selectedKeyCount || 0)
|
||||
if (!keyCount) {
|
||||
wx.showToast({ title: '请先选择按键类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const bitmap = 1 << (item.mappedKey - 1)
|
||||
this.setData({ keySendingIndex: index })
|
||||
await this.sendCommand(0x06, [keyCount, bitmap], `触发按键${item.hwIndex} -> 键值${item.mappedKey}(${keyCount}键)`)
|
||||
this.setData({ keySendingIndex: -1 })
|
||||
},
|
||||
|
||||
onClearLogs() {
|
||||
this.setData({ logs: [], logScrollTo: '' })
|
||||
},
|
||||
|
||||
onExportLogs() {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager && wx.getFileSystemManager()
|
||||
if (!fs || !wx.env || !wx.env.USER_DATA_PATH) {
|
||||
wx.showToast({ title: '当前环境不支持导出', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const name = `BLV_RQ_Log_${this.getExportTimestamp()}.txt`
|
||||
const path = `${wx.env.USER_DATA_PATH}/${name}`
|
||||
const content = this.buildLogExportText()
|
||||
fs.writeFileSync(path, content, 'utf8')
|
||||
this.appendLog('UI', `日志已导出:${name}`)
|
||||
if (typeof wx.openDocument === 'function') {
|
||||
wx.openDocument({ filePath: path, showMenu: true })
|
||||
} else {
|
||||
wx.showToast({ title: '日志已导出', icon: 'success' })
|
||||
}
|
||||
} catch (err) {
|
||||
this.appendLog('ERR', `导出日志失败:${(err && err.message) || err}`)
|
||||
wx.showToast({ title: '导出失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
getExportTimestamp() {
|
||||
const d = new Date()
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`
|
||||
},
|
||||
|
||||
buildLogExportText() {
|
||||
const info = this.data.bleInfo
|
||||
const device = this.data.deviceInfo
|
||||
const lines = [
|
||||
'BLV_RQ 通信日志导出',
|
||||
`导出时间:${new Date().toLocaleString()}`,
|
||||
`蓝牙名称:${info.devName || '-'}`,
|
||||
`MAC:${info.mac || '-'}`,
|
||||
`连接状态:${info.connected ? '已连接' : '未连接'}`,
|
||||
`信号值:${info.signalText || '-'}`,
|
||||
`设备型号:${device.model || '-'}`,
|
||||
`软件版本:${device.softwareVersion || '-'}`,
|
||||
`硬件版本:${device.hardwareVersion || '-'}`,
|
||||
'----------------------------------------'
|
||||
]
|
||||
this.data.logs.forEach(item => {
|
||||
if (item.type === 'TX') {
|
||||
lines.push(`[${item.time}] TX -> ${item.displayText || item.text}`)
|
||||
} else if (item.type === 'RX') {
|
||||
lines.push(`[${item.time}] RX <- ${item.displayText || item.text}`)
|
||||
} else {
|
||||
lines.push(`[${item.time}] [${item.type}] ${item.text}`)
|
||||
}
|
||||
})
|
||||
return lines.join('\n')
|
||||
},
|
||||
|
||||
onDisconnect(showToast = false) {
|
||||
const { mac } = this.data.bleInfo
|
||||
if (!mac || typeof wx.closeBLEConnection !== 'function') return
|
||||
this.stopStatusPolling()
|
||||
wx.closeBLEConnection({
|
||||
deviceId: mac,
|
||||
complete: () => {
|
||||
this.setData({
|
||||
'bleInfo.connected': false,
|
||||
'bleInfo.rssi': null,
|
||||
'bleInfo.signalText': '',
|
||||
canOperateKeyConfig: false
|
||||
})
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
this.appendLog('CFG', '蓝牙连接已断开')
|
||||
if (showToast) wx.showToast({ title: '已断开连接', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onConnectionAction() {
|
||||
if (this.data.isConnecting) return
|
||||
if (this.data.bleInfo.connected) {
|
||||
this.onDisconnect(true)
|
||||
return
|
||||
}
|
||||
this.onReconnect()
|
||||
},
|
||||
|
||||
onReconnect() {
|
||||
this.appendLog('UI', '请求重连蓝牙')
|
||||
this.ensureBleReady('手动重连', true)
|
||||
},
|
||||
|
||||
validateKeyConfig() {
|
||||
const keys = (this.data.keyConfig.keys || []).map(item => ({ ...item }))
|
||||
const selectedCount = this.data.keyConfig.selectedKeyCount
|
||||
if (!selectedCount) {
|
||||
return {
|
||||
blocked: true,
|
||||
message: '请先选择按键类型',
|
||||
keys,
|
||||
keyCount: 0
|
||||
}
|
||||
}
|
||||
return {
|
||||
blocked: false,
|
||||
message: '',
|
||||
keys,
|
||||
keyCount: selectedCount
|
||||
}
|
||||
},
|
||||
|
||||
async sendCommand(frameType, params, text, options = {}) {
|
||||
const { mac, serviceId, txCharId, writeCharId } = this.data.bleInfo
|
||||
const targetWriteCharId = writeCharId || txCharId
|
||||
if (!mac || !serviceId || !targetWriteCharId) {
|
||||
wx.showToast({ title: '蓝牙通道未就绪', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
const packet = buildPacket(frameType, params)
|
||||
this.appendLog('TX', `${text} | ${ab2hex(packet)}`, { cmd: `0x${frameType.toString(16).toUpperCase().padStart(2, '0')}` })
|
||||
return new Promise((resolve) => {
|
||||
this._pendingResponse = {
|
||||
frameType,
|
||||
resolve,
|
||||
silentError: !!options.silentError,
|
||||
timer: setTimeout(() => {
|
||||
this._pendingResponse = null
|
||||
this.appendLog('WARN', `${text}超时`, { cmd: `0x${frameType.toString(16).toUpperCase().padStart(2, '0')}` })
|
||||
resolve(false)
|
||||
}, 4000)
|
||||
}
|
||||
wx.writeBLECharacteristicValue({
|
||||
deviceId: mac,
|
||||
serviceId,
|
||||
characteristicId: targetWriteCharId,
|
||||
value: packet.buffer,
|
||||
fail: (err) => {
|
||||
if (this._pendingResponse && this._pendingResponse.timer) clearTimeout(this._pendingResponse.timer)
|
||||
this._pendingResponse = null
|
||||
this.appendLog('ERR', `${text}发送失败:${(err && err.errMsg) || '未知错误'}`)
|
||||
if (!options.silentError) wx.showToast({ title: '发送失败', icon: 'none' })
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
startBleNotifications() {
|
||||
const { mac, serviceId, txCharId, rxCharId } = this.data.bleInfo
|
||||
const notifyTargets = [txCharId, rxCharId].filter(Boolean)
|
||||
if (!mac || !serviceId || !notifyTargets.length || typeof wx.notifyBLECharacteristicValueChange !== 'function') return
|
||||
let pending = notifyTargets.length
|
||||
notifyTargets.forEach((characteristicId) => {
|
||||
wx.notifyBLECharacteristicValueChange({
|
||||
state: true,
|
||||
deviceId: mac,
|
||||
serviceId,
|
||||
characteristicId,
|
||||
complete: () => {
|
||||
pending -= 1
|
||||
if (pending <= 0) {
|
||||
this.bindBleListener()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
ensureBleReady(source = '状态校验', showToast = false) {
|
||||
const { mac } = this.data.bleInfo
|
||||
if (!mac || typeof wx.createBLEConnection !== 'function') return
|
||||
if (this._connecting) return
|
||||
this._connecting = true
|
||||
this.setData({ isConnecting: true })
|
||||
this.appendLog('CFG', `${source}:开始校验蓝牙连接`)
|
||||
wx.createBLEConnection({
|
||||
deviceId: mac,
|
||||
success: () => {
|
||||
this._connecting = false
|
||||
this.activateConnectedSession(source, showToast)
|
||||
},
|
||||
fail: (err) => {
|
||||
this._connecting = false
|
||||
const reason = formatBleError(err)
|
||||
const message = String(reason || '').toLowerCase()
|
||||
if (message.includes('already connect') || message.includes('already connected') || reason === '已连接' || reason === '设备已连接') {
|
||||
this.appendLog('CFG', `${source}检测到设备已连接,复用当前蓝牙会话`)
|
||||
this.activateConnectedSession(source, showToast)
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
isConnecting: false,
|
||||
'bleInfo.connected': false,
|
||||
'bleInfo.rssi': null,
|
||||
'bleInfo.signalText': '',
|
||||
canOperateKeyConfig: false
|
||||
})
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
this.appendLog('ERR', `${source}失败:${reason}`)
|
||||
if (showToast) wx.showToast({ title: '重连失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
activateConnectedSession(source = '状态校验', showToast = false) {
|
||||
this.setData({
|
||||
isConnecting: false,
|
||||
'bleInfo.connected': true,
|
||||
canOperateKeyConfig: this.data.keyConfig.selectedKeyCount > 0
|
||||
})
|
||||
this.bindConnectionStateListener()
|
||||
this.startBleNotifications()
|
||||
this.startStatusPolling()
|
||||
this.refreshBleStatus()
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
this.appendLog('CFG', `${source}成功:蓝牙已连接`)
|
||||
this.autoLoadDeviceData()
|
||||
if (showToast) wx.showToast({ title: '重连成功', icon: 'success' })
|
||||
},
|
||||
|
||||
async autoLoadDeviceData() {
|
||||
await this.onReadVersion()
|
||||
},
|
||||
|
||||
bindConnectionStateListener() {
|
||||
if (this._connectionStateHandler || typeof wx.onBLEConnectionStateChange !== 'function') return
|
||||
this._connectionStateHandler = (res) => {
|
||||
if (!res || res.deviceId !== this.data.bleInfo.mac) return
|
||||
const connected = !!res.connected
|
||||
this.setData({
|
||||
isConnecting: false,
|
||||
'bleInfo.connected': connected,
|
||||
'bleInfo.signalText': connected ? this.data.bleInfo.signalText : '',
|
||||
'bleInfo.rssi': connected ? this.data.bleInfo.rssi : null,
|
||||
canOperateKeyConfig: connected && this.data.keyConfig.selectedKeyCount > 0
|
||||
})
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
if (connected) {
|
||||
this.startStatusPolling()
|
||||
this.refreshBleStatus()
|
||||
this.appendLog('CFG', '蓝牙连接状态:已连接')
|
||||
} else {
|
||||
this.stopStatusPolling()
|
||||
this.appendLog('WARN', '蓝牙连接状态:已断开')
|
||||
}
|
||||
}
|
||||
wx.onBLEConnectionStateChange(this._connectionStateHandler)
|
||||
},
|
||||
|
||||
teardownConnectionStateListener() {
|
||||
if (this._connectionStateHandler && typeof wx.offBLEConnectionStateChange === 'function') {
|
||||
wx.offBLEConnectionStateChange(this._connectionStateHandler)
|
||||
}
|
||||
this._connectionStateHandler = null
|
||||
},
|
||||
|
||||
startStatusPolling() {
|
||||
this.stopStatusPolling()
|
||||
this._statusTimer = setInterval(() => {
|
||||
this.refreshBleStatus()
|
||||
}, 3000)
|
||||
},
|
||||
|
||||
stopStatusPolling() {
|
||||
if (this._statusTimer) {
|
||||
clearInterval(this._statusTimer)
|
||||
this._statusTimer = null
|
||||
}
|
||||
},
|
||||
|
||||
bindBleListener() {
|
||||
if (this._bleChangeHandler || typeof wx.onBLECharacteristicValueChange !== 'function') return
|
||||
this._bleChangeHandler = (res) => {
|
||||
const packet = parsePacket(res && res.value)
|
||||
if (!packet) return
|
||||
if (packet.invalid) {
|
||||
this.appendLog('WARN', `收到异常数据:${ab2hex(packet.bytes)}`)
|
||||
return
|
||||
}
|
||||
this.appendLog('RX', ab2hex(packet.bytes), { cmd: `0x${packet.frameType.toString(16).toUpperCase().padStart(2, '0')}` })
|
||||
this.handlePacket(packet)
|
||||
}
|
||||
wx.onBLECharacteristicValueChange(this._bleChangeHandler)
|
||||
},
|
||||
|
||||
teardownBleNotifications() {
|
||||
if (this._bleChangeHandler && typeof wx.offBLECharacteristicValueChange === 'function') {
|
||||
wx.offBLECharacteristicValueChange(this._bleChangeHandler)
|
||||
}
|
||||
this._bleChangeHandler = null
|
||||
},
|
||||
|
||||
handlePacket(packet) {
|
||||
switch (packet.frameType) {
|
||||
case 0x01:
|
||||
this.handleVersionPacket(packet.params)
|
||||
break
|
||||
case 0x04:
|
||||
this.handleKeyConfigPacket(packet.params)
|
||||
break
|
||||
case 0x05:
|
||||
this.handleSimpleAck(packet.params, '设置蓝牙名称')
|
||||
break
|
||||
case 0x02:
|
||||
this.handleSimpleAck(packet.params, '设置按键配置')
|
||||
break
|
||||
case 0x06:
|
||||
this.handleSimpleAck(packet.params, '按键控制')
|
||||
break
|
||||
default:
|
||||
this.appendLog('CFG', `收到未处理命令:0x${packet.frameType.toString(16).toUpperCase().padStart(2, '0')}`)
|
||||
break
|
||||
}
|
||||
if (this._pendingResponse && this._pendingResponse.frameType === packet.frameType) {
|
||||
clearTimeout(this._pendingResponse.timer)
|
||||
const resolve = this._pendingResponse.resolve
|
||||
this._pendingResponse = null
|
||||
resolve(true)
|
||||
}
|
||||
},
|
||||
|
||||
handleVersionPacket(params = []) {
|
||||
const softwareVersion = params.length > 0 ? String(params[0]) : '-'
|
||||
const hardwareVersion = params.length > 1 ? String(params[1]) : '-'
|
||||
this.setData({
|
||||
'deviceInfo.softwareVersion': softwareVersion,
|
||||
'deviceInfo.hardwareVersion': hardwareVersion
|
||||
})
|
||||
this.appendLog('CFG', `版本更新:软件${softwareVersion},硬件${hardwareVersion}`)
|
||||
},
|
||||
|
||||
handleKeyConfigPacket(params = []) {
|
||||
const count = Number(params[0] || 0)
|
||||
const safeCount = count > 0 && count <= 8 ? count : this.data.keyConfig.selectedKeyCount
|
||||
const keys = createKeys(safeCount)
|
||||
for (let i = 0; i < safeCount; i += 1) {
|
||||
const cfg = params[i + 1] || 0
|
||||
const mappedKey = cfg & 0x0F
|
||||
const functionCode = (cfg >> 4) & 0x0F
|
||||
keys[i].mappedKey = mappedKey || null
|
||||
keys[i].originalMappedKey = mappedKey || null
|
||||
keys[i].mappedKeyIndex = mappedKey || 0
|
||||
keys[i].functionCode = functionCode
|
||||
keys[i].functionIndex = functionCode
|
||||
}
|
||||
const keyTypeIndex = this.data.keyConfig.keyCountValues.indexOf(safeCount)
|
||||
this.setData({
|
||||
readingKeyConfig: false,
|
||||
'keyConfig.selectedKeyTypeIndex': keyTypeIndex >= 0 ? keyTypeIndex : 0,
|
||||
'keyConfig.selectedKeyCount': safeCount,
|
||||
'keyConfig.keys': keys,
|
||||
'keyConfig.keySlots': createKeySlots(keys),
|
||||
canOperateKeyConfig: true
|
||||
})
|
||||
if (count && count !== this.data.keyConfig.selectedKeyCount) {
|
||||
wx.showToast({ title: `已按设备返回${count}键修正`, icon: 'none' })
|
||||
}
|
||||
this.appendLog('CFG', `读取到按键配置:${safeCount}键`)
|
||||
},
|
||||
|
||||
handleSimpleAck(params = [], text) {
|
||||
const ok = Number(params[0] || 0) === 0x01
|
||||
this.appendLog(ok ? 'CFG' : 'WARN', `${text}${ok ? '成功' : '失败'}`)
|
||||
if (text === '设置蓝牙名称' && ok) {
|
||||
this.setData({ 'bleInfo.devName': this.data.bleNameInput })
|
||||
}
|
||||
if (text === '设置蓝牙名称') this.setData({ settingBleName: false })
|
||||
if (text === '按键控制') this.setData({ keySendingIndex: -1 })
|
||||
},
|
||||
|
||||
refreshBleStatus() {
|
||||
const { mac } = this.data.bleInfo
|
||||
if (!mac || typeof wx.getBLEDeviceRSSI !== 'function') return
|
||||
wx.getBLEDeviceRSSI({
|
||||
deviceId: mac,
|
||||
success: (res) => {
|
||||
const rssi = typeof res.RSSI === 'number' ? res.RSSI : null
|
||||
const signalText = rssi == null ? '-' : `${rssi} dBm`
|
||||
this.setData({
|
||||
'bleInfo.rssi': rssi,
|
||||
'bleInfo.signalText': signalText,
|
||||
'bleInfo.connected': true,
|
||||
canOperateKeyConfig: this.data.keyConfig.selectedKeyCount > 0
|
||||
})
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
},
|
||||
fail: (err) => {
|
||||
const reason = formatBleError(err)
|
||||
const connected = !!this.data.bleInfo.connected
|
||||
this.setData({
|
||||
'bleInfo.connected': connected,
|
||||
'bleInfo.rssi': null,
|
||||
'bleInfo.signalText': connected ? '-' : '',
|
||||
canOperateKeyConfig: connected && this.data.keyConfig.selectedKeyCount > 0
|
||||
})
|
||||
if (!connected) {
|
||||
this.stopStatusPolling()
|
||||
}
|
||||
this.appendLog('WARN', `信号刷新失败:${reason}`)
|
||||
this.updateSetBleNameState(this.data.bleNameInput)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
8
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
Normal file
8
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"navigationBarBackgroundColor": "#fff",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {
|
||||
"cu-custom": "/colorui/components/cu-custom"
|
||||
}
|
||||
}
|
||||
129
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
Normal file
129
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxml
Normal file
@@ -0,0 +1,129 @@
|
||||
<view class="container">
|
||||
<cu-custom bgColor="bg-gradual-blue" isBack="true">
|
||||
<view slot="content">{{bleInfo.devName || 'BLV_RQ设备'}}</view>
|
||||
</cu-custom>
|
||||
|
||||
<view class="page-body content">
|
||||
<view class="panel ble-panel device-row">
|
||||
<view class="panel-head inline-head blank-head"></view>
|
||||
|
||||
<view class="console-row name-action-row keep-inline-row">
|
||||
<view class="console-cell device-name-cell">
|
||||
<text class="device-icon">🔵</text>
|
||||
<text class="cell-label">蓝牙名称:</text>
|
||||
<text class="cell-value">{{bleInfo.devName || '-'}}</text>
|
||||
</view>
|
||||
<view class="console-cell action-cell">
|
||||
<button class="action-btn {{bleInfo.connected ? 'warn' : 'primary'}} full-btn" bindtap="onConnectionAction" disabled="{{isConnecting || !bleInfo.mac}}">{{isConnecting ? '连接中...' : (bleInfo.connected ? '断开连接' : '重新连接')}}</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="console-row status-signal-row keep-inline-row">
|
||||
<view class="console-cell status-cell">
|
||||
<text class="cell-label">连接状态:</text>
|
||||
<view class="status-chip {{isConnecting ? 'pending' : (bleInfo.connected ? 'online' : 'offline')}}">{{isConnecting ? '连接中' : (bleInfo.connected ? '已连接' : '未连接')}}</view>
|
||||
</view>
|
||||
<view class="console-cell signal-cell">
|
||||
<text class="cell-label">信号强度:</text>
|
||||
<text class="cell-value">{{bleInfo.signalText || '-'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="console-row mac-row">
|
||||
<view class="console-cell full-line">
|
||||
<text class="cell-label">MAC地址:</text>
|
||||
<text class="cell-value">{{bleInfo.mac || '-'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="console-row rename-console-row keep-inline-row">
|
||||
<view class="console-cell input-cell wide">
|
||||
<text class="cell-label">蓝牙名称:</text>
|
||||
<input class="name-input {{bleInfo.connected ? '' : 'is-disabled'}}" placeholder="输入1-8位英文或数字" maxlength="8" value="{{bleNameInput}}" bindinput="onBleNameInput" disabled="{{!bleInfo.connected || settingBleName}}" />
|
||||
</view>
|
||||
<view class="console-cell button-cell">
|
||||
<button class="action-btn primary full-btn" bindtap="onSetBleName" loading="{{settingBleName}}" disabled="{{!canSetBleName || settingBleName}}">设置名称</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel device-panel cfg-card">
|
||||
<view class="panel-head inline-head blank-head"></view>
|
||||
|
||||
<view class="console-row firmware-row keep-inline-row">
|
||||
<view class="console-cell firmware-cell software-version-cell">
|
||||
<text class="cell-label">软件版本:</text>
|
||||
<text class="cell-value">{{deviceInfo.softwareVersion}}</text>
|
||||
</view>
|
||||
<view class="console-cell firmware-cell hardware-version-cell">
|
||||
<text class="cell-label">硬件版本:</text>
|
||||
<text class="cell-value">{{deviceInfo.hardwareVersion}}</text>
|
||||
</view>
|
||||
<view class="console-cell action-cell-inline read-version-cell">
|
||||
<button class="action-btn default full-btn" bindtap="onReadVersion" loading="{{readingVersion}}" disabled="{{!bleInfo.connected || readingVersion || readingKeyConfig || writingKeyConfig}}">读取版本</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="console-row config-action-row keep-inline-row">
|
||||
<view class="console-cell select-cell key-type-cell">
|
||||
<text class="cell-label">按键类型:</text>
|
||||
<picker class="count-picker" mode="selector" range="{{keyConfig.keyCountOptions}}" value="{{keyConfig.selectedKeyTypeIndex}}" bindchange="onKeyCountChange" disabled="{{!bleInfo.connected || writingKeyConfig}}">
|
||||
<view class="picker-view {{bleInfo.connected ? '' : 'is-disabled'}}">{{keyConfig.keyCountOptions[keyConfig.selectedKeyTypeIndex]}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="console-cell action-cell-inline">
|
||||
<button class="action-btn primary full-btn" bindtap="onReadKeyConfig" loading="{{readingKeyConfig}}" disabled="{{!bleInfo.connected || readingKeyConfig || writingKeyConfig}}">读取配置</button>
|
||||
</view>
|
||||
<view class="console-cell action-cell-inline">
|
||||
<button class="action-btn success full-btn" bindtap="onSubmitKeyConfig" loading="{{writingKeyConfig}}" disabled="{{!canOperateKeyConfig || writingKeyConfig || readingKeyConfig}}">写入配置</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="key-card-wrap">
|
||||
<view class="key-grid strict-grid">
|
||||
<view class="key-card-inline {{item.isPlaceholder ? 'reserve-card' : (bleInfo.connected ? '' : 'is-disabled')}}" wx:for="{{keyConfig.keySlots}}" wx:key="slotIndex">
|
||||
<view wx:if="{{!item.isPlaceholder}}" class="key-card-content">
|
||||
<view class="inline-key-label">按键{{item.hwIndex}}</view>
|
||||
<picker class="inline-key-picker" mode="selector" range="{{functionOptionLabels}}" value="{{item.functionIndex}}" bindchange="onFunctionChange" data-index="{{index}}" disabled="{{!bleInfo.connected || writingKeyConfig}}">
|
||||
<view class="picker-view compact {{bleInfo.connected ? '' : 'is-disabled'}}">{{functionOptionLabels[item.functionIndex] || '请选择功能'}}</view>
|
||||
</picker>
|
||||
<button class="action-btn default inline-send-btn" bindtap="onTriggerKey" data-index="{{index}}" loading="{{keySendingIndex === index}}" disabled="{{!bleInfo.connected || !item.mappedKey || keySendingIndex === index}}">{{keySendingIndex === index ? '发送中...' : '发送'}}</button>
|
||||
</view>
|
||||
<view wx:if="{{item.isPlaceholder}}" class="reserve-card-empty">
|
||||
<text class="reserve-card-text">预留{{item.hwIndex}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel log-panel log-card">
|
||||
<view class="panel-head inline-head log-inline-head keep-inline-head blank-head"></view>
|
||||
<view class="log-toolbar-row keep-inline-actions-row">
|
||||
<view class="log-toolbar-title-spacer"></view>
|
||||
<view class="toolbar-actions right-actions keep-inline-actions">
|
||||
<button class="action-btn default" bindtap="onClearLogs">清空日志</button>
|
||||
<button class="action-btn primary" bindtap="onExportLogs">导出日志</button>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view class="log-scroll" scroll-y="true" scroll-into-view="{{logScrollTo}}">
|
||||
<view class="log-item simple-log {{item.typeClass}}" wx:for="{{logs}}" wx:key="id" id="{{'log-' + item.id}}">
|
||||
<text class="log-time">{{item.time}}</text>
|
||||
<text class="log-dir {{item.typeClass}}">{{item.type === 'TX' ? 'TX ->' : (item.type === 'RX' ? 'RX <-' : '[' + item.type + ']')}}</text>
|
||||
<text class="log-text {{item.type === 'TX' || item.type === 'RX' ? 'packet' : 'plain'}}">{{(item.type === 'TX' || item.type === 'RX') ? (item.displayText || item.hexText || item.text) : item.text}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="dialog-mask" wx:if="{{confirmDialog.visible}}">
|
||||
<view class="dialog-card">
|
||||
<view class="dialog-title">设置确认</view>
|
||||
<view class="dialog-content">{{confirmDialog.message}}</view>
|
||||
<view class="dialog-actions">
|
||||
<button class="action-btn default dialog-btn" bindtap="onCancelSubmit">取消</button>
|
||||
<button class="action-btn primary dialog-btn" bindtap="onConfirmSubmit">继续下发</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
715
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
Normal file
715
pages/basics/BluetoothDebugging/BLVRQPage/BLVRQPage.wxss
Normal file
@@ -0,0 +1,715 @@
|
||||
page {
|
||||
background: #f5f6f8;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100vh;
|
||||
background: #f5f6f8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.page-body,
|
||||
.content {
|
||||
height: calc(100vh - 88rpx);
|
||||
min-height: 0;
|
||||
padding: 8rpx 10rpx 0;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel,
|
||||
.cfg-card,
|
||||
.log-card,
|
||||
.device-row {
|
||||
width: 100%;
|
||||
background: #ffffff;
|
||||
border: none;
|
||||
border-radius: 14rpx;
|
||||
padding: 12rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.ble-panel,
|
||||
.device-panel,
|
||||
.log-panel {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ble-panel,
|
||||
.device-panel {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.log-panel,
|
||||
.log-card {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.blank-head {
|
||||
min-height: 0;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.panel-title.no-margin {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.console-row {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.console-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.console-cell {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.device-name-cell {
|
||||
flex: 0.98;
|
||||
}
|
||||
|
||||
.status-cell,
|
||||
.signal-cell {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-cell,
|
||||
.button-cell,
|
||||
.action-cell-inline {
|
||||
width: 140rpx;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.read-version-cell {
|
||||
width: 108rpx;
|
||||
}
|
||||
|
||||
.full-line {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-cell.wide,
|
||||
.select-cell {
|
||||
flex: 0.88;
|
||||
}
|
||||
|
||||
.firmware-cell {
|
||||
flex: 1.54;
|
||||
}
|
||||
|
||||
.software-version-cell,
|
||||
.hardware-version-cell {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.software-version-cell .cell-label,
|
||||
.hardware-version-cell .cell-label {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.software-version-cell .cell-value,
|
||||
.hardware-version-cell .cell-value {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.key-type-cell {
|
||||
flex: 0.62;
|
||||
}
|
||||
|
||||
.device-icon {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.cell-label {
|
||||
font-size: 26rpx;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cell-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 27rpx;
|
||||
font-weight: 600;
|
||||
color: #333333;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
min-width: 120rpx;
|
||||
height: 100%;
|
||||
min-height: 48rpx;
|
||||
align-self: stretch;
|
||||
padding: 0 12rpx;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.status-chip.online {
|
||||
color: #047857;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border: 1rpx solid rgba(16, 185, 129, 0.28);
|
||||
}
|
||||
|
||||
.status-chip.offline {
|
||||
color: #b45309;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border: 1rpx solid rgba(245, 158, 11, 0.26);
|
||||
}
|
||||
|
||||
.status-chip.pending {
|
||||
color: #1d4ed8;
|
||||
background: rgba(37, 99, 235, 0.12);
|
||||
border: 1rpx solid rgba(37, 99, 235, 0.26);
|
||||
}
|
||||
|
||||
.name-input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
min-height: 48rpx;
|
||||
align-self: stretch;
|
||||
padding: 0 12rpx;
|
||||
background: #f7f8fa;
|
||||
border: 1rpx solid #e5e9f2;
|
||||
border-radius: 10rpx;
|
||||
font-size: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
border-radius: 12rpx;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
padding: 0 12rpx;
|
||||
font-size: 28rpx;
|
||||
height: 100%;
|
||||
min-height: 52rpx;
|
||||
align-self: stretch;
|
||||
line-height: 1.2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.read-version-cell .action-btn {
|
||||
font-size: 26rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
.action-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #0ea5e9;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2rpx 6rpx rgba(14, 165, 233, 0.35);
|
||||
}
|
||||
|
||||
.action-btn.success {
|
||||
background: #21c161;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.warn {
|
||||
background: #ff8f00;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.action-btn.default {
|
||||
background: #ffffff;
|
||||
color: #2bab99;
|
||||
border: 1rpx solid rgba(43, 171, 153, 0.18);
|
||||
}
|
||||
|
||||
.full-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.count-picker,
|
||||
.inline-key-picker {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.picker-view {
|
||||
min-height: 48rpx;
|
||||
height: 100%;
|
||||
align-self: stretch;
|
||||
padding: 0 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f7f8fa;
|
||||
border: 1rpx solid #e5e9f2;
|
||||
border-radius: 10rpx;
|
||||
font-size: 23rpx;
|
||||
color: #0f172a;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.picker-view.compact {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.is-disabled {
|
||||
opacity: 0.48;
|
||||
filter: grayscale(0.2);
|
||||
}
|
||||
|
||||
.name-input.is-disabled,
|
||||
.picker-view.is-disabled {
|
||||
background: #f1f5f9;
|
||||
border-color: #e2e8f0;
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
.key-card-wrap {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.key-grid.strict-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.key-card-inline {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
background: #ffffff;
|
||||
border: none;
|
||||
border-radius: 14rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.reserve-card {
|
||||
border-style: dashed;
|
||||
border-color: rgba(148, 163, 184, 0.5);
|
||||
background: rgba(248, 250, 252, 0.55);
|
||||
min-height: 86rpx;
|
||||
opacity: 0.72;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.key-card-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.reserve-card-empty {
|
||||
width: 100%;
|
||||
min-height: 86rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.reserve-card-text {
|
||||
font-size: 24rpx;
|
||||
color: #94a3b8;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.inline-key-label {
|
||||
min-width: 70rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inline-send-btn {
|
||||
min-width: 104rpx;
|
||||
}
|
||||
|
||||
.dialog-btn {
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.log-inline-head {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.log-toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.log-toolbar-title-spacer {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.log-scroll {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
background: #fdfdfd;
|
||||
border: 1rpx solid #e5e9f2;
|
||||
border-radius: 12rpx;
|
||||
padding: 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.log-item.simple-log {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8rpx;
|
||||
padding: 0 0 8rpx 0;
|
||||
border-bottom: none;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
.log-item.simple-log:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #9aa0a6;
|
||||
font-size: 22rpx;
|
||||
font-family: inherit;
|
||||
white-space: nowrap;
|
||||
min-width: 78rpx;
|
||||
}
|
||||
|
||||
.log-dir {
|
||||
font-size: 22rpx;
|
||||
font-family: inherit;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
min-width: 52rpx;
|
||||
}
|
||||
|
||||
.log-dir.tx {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.log-dir.rx {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
.log-dir.warn {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.log-dir.err {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.log-dir.ui,
|
||||
.log-dir.cfg {
|
||||
color: #c4b5fd;
|
||||
}
|
||||
|
||||
.log-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #333333;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.log-text.packet {
|
||||
color: #333333;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
font-family: inherit;
|
||||
word-break: break-all;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 10rpx;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.log-text.plain {
|
||||
color: #333333;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.45;
|
||||
font-family: inherit;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.dialog-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.dialog-card {
|
||||
width: 78%;
|
||||
background: #ffffff;
|
||||
border-radius: 12rpx;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2d3d;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
font-size: 26rpx;
|
||||
color: #475467;
|
||||
line-height: 1.7;
|
||||
margin-bottom: 16rpx;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.page-body {
|
||||
padding: 12rpx;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.console-row,
|
||||
.panel-head,
|
||||
.toolbar-actions {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.keep-inline-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.keep-inline-head {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.keep-inline-actions {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.keep-inline-row .cell-label {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.keep-inline-row .cell-value,
|
||||
.keep-inline-row .name-input,
|
||||
.keep-inline-row .picker-view,
|
||||
.keep-inline-row .inline-key-label {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.name-action-row .device-name-cell {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.name-action-row .action-cell {
|
||||
flex: 0 0 140rpx;
|
||||
width: 140rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-signal-row .status-cell,
|
||||
.status-signal-row .signal-cell,
|
||||
.firmware-row .firmware-cell,
|
||||
.rename-console-row .input-cell.wide,
|
||||
.rename-console-row .button-cell,
|
||||
.config-action-row .select-cell,
|
||||
.config-action-row .action-cell-inline {
|
||||
flex: 1 1 0;
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rename-console-row .input-cell.wide {
|
||||
flex: 1.08;
|
||||
}
|
||||
|
||||
.rename-console-row .button-cell,
|
||||
.config-action-row .action-cell-inline {
|
||||
flex: 0 0 128rpx;
|
||||
width: 128rpx;
|
||||
}
|
||||
|
||||
.firmware-row .firmware-cell {
|
||||
flex: 1.08;
|
||||
}
|
||||
|
||||
.config-action-row .select-cell {
|
||||
flex: 0.78;
|
||||
}
|
||||
|
||||
.config-action-row .action-cell-inline {
|
||||
flex: 0 0 128rpx;
|
||||
width: 128rpx;
|
||||
}
|
||||
|
||||
.firmware-row .read-version-cell {
|
||||
flex: 0 0 112rpx;
|
||||
width: 112rpx;
|
||||
}
|
||||
|
||||
.status-signal-row .status-cell {
|
||||
flex: 0 0 230rpx;
|
||||
}
|
||||
|
||||
.status-signal-row .signal-cell {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.keep-inline-row .action-btn,
|
||||
.keep-inline-actions .action-btn {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.name-action-row .action-btn,
|
||||
.rename-console-row .action-btn,
|
||||
.config-action-row .action-btn,
|
||||
.firmware-row .action-btn {
|
||||
padding: 0 6rpx;
|
||||
}
|
||||
|
||||
.firmware-row .read-version-cell .action-btn {
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.keep-inline-head .panel-title {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.read-version-cell {
|
||||
width: 96rpx;
|
||||
}
|
||||
|
||||
.read-version-cell .action-btn {
|
||||
font-size: 24rpx;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.keep-inline-actions {
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.key-grid.strict-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.key-card-inline {
|
||||
min-height: 82rpx;
|
||||
}
|
||||
|
||||
.reserve-card-empty {
|
||||
min-height: 82rpx;
|
||||
}
|
||||
|
||||
.inline-key-label {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.log-item.simple-log {
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
min-width: 74rpx;
|
||||
}
|
||||
|
||||
.log-dir {
|
||||
min-width: 50rpx;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user