|
|
@@ -0,0 +1,692 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import {onMounted, reactive, ref, watch} from 'vue'
|
|
|
+import {message} from 'ant-design-vue'
|
|
|
+import {ClearOutlined, LoadingOutlined, ReloadOutlined, SaveOutlined,} from '@ant-design/icons-vue'
|
|
|
+import {useBleDevice} from '@/composables/useBleDevice'
|
|
|
+import {useDeviceConfig} from '@/composables/useDeviceConfig'
|
|
|
+
|
|
|
+const ble = useBleDevice()
|
|
|
+const mqtt = useDeviceConfig()
|
|
|
+
|
|
|
+const isMobile = ref(window.innerWidth < 768)
|
|
|
+const activeTab = ref('bluetooth')
|
|
|
+const tabList = [
|
|
|
+ {key: 'bluetooth', tab: '蓝牙配置'},
|
|
|
+ {key: 'mqtt', tab: '消息配置'},
|
|
|
+]
|
|
|
+
|
|
|
+function onTabChange(key: string) {
|
|
|
+ activeTab.value = key
|
|
|
+}
|
|
|
+
|
|
|
+const LIGHT_MODES = [
|
|
|
+ {key: 'traffic', label: 'Traffic', color: '#52c41a'},
|
|
|
+ {key: 'thinking', label: 'Thinking', color: '#722ed1'},
|
|
|
+ {key: 'ai', label: 'AI', color: '#1890ff'},
|
|
|
+ {key: 'busy', label: 'Busy', color: '#faad14'},
|
|
|
+ {key: 'success', label: 'Success', color: '#52c41a'},
|
|
|
+ {key: 'error', label: 'Error', color: '#f5222d'},
|
|
|
+ {key: 'alarm', label: 'Alarm', color: '#fa541c'},
|
|
|
+ {key: 'init', label: 'Init', color: '#13c2c2'},
|
|
|
+ {key: 'off', label: 'Off', color: '#8c8c8c'},
|
|
|
+]
|
|
|
+
|
|
|
+const bleWifiForm = reactive({ssid: '', password: ''})
|
|
|
+const bleMqttForm = reactive({
|
|
|
+ broker: '',
|
|
|
+ port: 1883,
|
|
|
+ client: 'AI-Light',
|
|
|
+ username: '',
|
|
|
+ password: '',
|
|
|
+ topic: 'agent/status',
|
|
|
+ statusTopic: 'openCodeLight/status',
|
|
|
+ topicConfig: 'agent/status/config',
|
|
|
+})
|
|
|
+const blePinForm = reactive({red: 4, green: 3, yellow: 2})
|
|
|
+
|
|
|
+const mqttWifiForm = reactive({ssid: '', password: ''})
|
|
|
+const mqttMqttForm = reactive({
|
|
|
+ broker: '',
|
|
|
+ port: 1883,
|
|
|
+ client: 'AI-Light',
|
|
|
+ username: '',
|
|
|
+ password: '',
|
|
|
+ topic: 'agent/status',
|
|
|
+ statusTopic: 'openCodeLight/status',
|
|
|
+ topicConfig: 'agent/status/config',
|
|
|
+})
|
|
|
+const mqttPinForm = reactive({red: 4, green: 3, yellow: 2})
|
|
|
+
|
|
|
+watch(ble.config, (cfg) => {
|
|
|
+ if (cfg) {
|
|
|
+ bleWifiForm.ssid = cfg.wifi_ssid || ''
|
|
|
+ bleMqttForm.broker = cfg.mqtt_broker || ''
|
|
|
+ bleMqttForm.port = cfg.mqtt_port || 1883
|
|
|
+ bleMqttForm.client = cfg.mqtt_client || 'AI-Light'
|
|
|
+ bleMqttForm.username = cfg.mqtt_user || ''
|
|
|
+ bleMqttForm.topic = cfg.mqtt_topic || ''
|
|
|
+ bleMqttForm.statusTopic = cfg.mqtt_status || ''
|
|
|
+ bleMqttForm.topicConfig = cfg.mqtt_topic_config || 'agent/status/config'
|
|
|
+ blePinForm.red = cfg.pin_red ?? 4
|
|
|
+ blePinForm.green = cfg.pin_green ?? 3
|
|
|
+ blePinForm.yellow = cfg.pin_yellow ?? 2
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+watch(mqtt.config, (cfg) => {
|
|
|
+ if (cfg) {
|
|
|
+ mqttWifiForm.ssid = cfg.wifi_ssid || ''
|
|
|
+ mqttMqttForm.broker = cfg.mqtt_broker || ''
|
|
|
+ mqttMqttForm.port = cfg.mqtt_port || 1883
|
|
|
+ mqttMqttForm.client = cfg.mqtt_client || 'AI-Light'
|
|
|
+ mqttMqttForm.username = cfg.mqtt_user || ''
|
|
|
+ mqttMqttForm.topic = cfg.mqtt_topic || ''
|
|
|
+ mqttMqttForm.statusTopic = cfg.mqtt_status || ''
|
|
|
+ mqttMqttForm.topicConfig = cfg.config_topic || 'agent/status/config'
|
|
|
+ mqttPinForm.red = cfg.pin_red ?? 4
|
|
|
+ mqttPinForm.green = cfg.pin_green ?? 3
|
|
|
+ mqttPinForm.yellow = cfg.pin_yellow ?? 2
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+async function handleBleSave() {
|
|
|
+ const cfg = {
|
|
|
+ wifi_ssid: bleWifiForm.ssid,
|
|
|
+ wifi_pass: bleWifiForm.password,
|
|
|
+ mqtt_broker: bleMqttForm.broker,
|
|
|
+ mqtt_port: bleMqttForm.port,
|
|
|
+ mqtt_user: bleMqttForm.username,
|
|
|
+ mqtt_pass: bleMqttForm.password,
|
|
|
+ mqtt_client: bleMqttForm.client,
|
|
|
+ mqtt_topic: bleMqttForm.topic,
|
|
|
+ mqtt_status: bleMqttForm.statusTopic,
|
|
|
+ mqtt_topic_config: bleMqttForm.topicConfig,
|
|
|
+ pin_red: blePinForm.red,
|
|
|
+ pin_green: blePinForm.green,
|
|
|
+ pin_yellow: blePinForm.yellow,
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await ble.saveConfig(cfg)
|
|
|
+ message.success('配置已保存,设备将重启')
|
|
|
+ } catch {
|
|
|
+ message.error('保存失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleBleRestart() {
|
|
|
+ try {
|
|
|
+ await ble.restartDevice()
|
|
|
+ message.success('重启指令已发送')
|
|
|
+ } catch {
|
|
|
+ message.error('重启失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleMqttSave() {
|
|
|
+ const cfg = {
|
|
|
+ wifi_ssid: mqttWifiForm.ssid,
|
|
|
+ wifi_pass: mqttWifiForm.password,
|
|
|
+ mqtt_broker: mqttMqttForm.broker,
|
|
|
+ mqtt_port: mqttMqttForm.port,
|
|
|
+ mqtt_user: mqttMqttForm.username,
|
|
|
+ mqtt_pass: mqttMqttForm.password,
|
|
|
+ mqtt_client: mqttMqttForm.client,
|
|
|
+ mqtt_topic: mqttMqttForm.topic,
|
|
|
+ mqtt_status: mqttMqttForm.statusTopic,
|
|
|
+ mqtt_topic_config: mqttMqttForm.topicConfig,
|
|
|
+ pin_red: mqttPinForm.red,
|
|
|
+ pin_green: mqttPinForm.green,
|
|
|
+ pin_yellow: mqttPinForm.yellow,
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await mqtt.saveConfig(cfg)
|
|
|
+ message.success('配置已保存并推送')
|
|
|
+ } catch {
|
|
|
+ message.error('保存失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleMqttRestart() {
|
|
|
+ try {
|
|
|
+ await mqtt.restartDevice()
|
|
|
+ message.success('重启指令已发送')
|
|
|
+ } catch {
|
|
|
+ message.error('重启失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onResize() {
|
|
|
+ isMobile.value = window.innerWidth < 768
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => window.addEventListener('resize', onResize))
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="device-config-page">
|
|
|
+ <a-card
|
|
|
+ :active-tab-key="activeTab"
|
|
|
+ :body-style="{ padding: '8px' }"
|
|
|
+ :tab-list="tabList"
|
|
|
+ class="config-card"
|
|
|
+ @tabChange="onTabChange"
|
|
|
+ >
|
|
|
+ <!-- 蓝牙配置 Tab -->
|
|
|
+ <template v-if="activeTab === 'bluetooth'">
|
|
|
+ <a-alert
|
|
|
+ v-if="ble.connectionState.value === 'connected'"
|
|
|
+ :message="'已连接: ' + ble.deviceName.value"
|
|
|
+ class="conn-alert"
|
|
|
+ show-icon
|
|
|
+ type="success"
|
|
|
+ >
|
|
|
+ <template #action>
|
|
|
+ <a-button danger size="small" @click="ble.disconnect">断开</a-button>
|
|
|
+ </template>
|
|
|
+ </a-alert>
|
|
|
+ <a-alert
|
|
|
+ v-else-if="ble.connectionState.value === 'scanning' || ble.connectionState.value === 'connecting'"
|
|
|
+ class="conn-alert"
|
|
|
+ message="正在连接设备..."
|
|
|
+ show-icon
|
|
|
+ type="info"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <LoadingOutlined spin/>
|
|
|
+ </template>
|
|
|
+ <template #action>
|
|
|
+ <a-button disabled size="small">连接中</a-button>
|
|
|
+ </template>
|
|
|
+ </a-alert>
|
|
|
+ <a-alert
|
|
|
+ v-else
|
|
|
+ class="conn-alert"
|
|
|
+ description="请先连接蓝牙设备以进行配置"
|
|
|
+ message="未连接设备"
|
|
|
+ show-icon
|
|
|
+ type="warning"
|
|
|
+ >
|
|
|
+ <template #action>
|
|
|
+ <a-button size="small" type="primary" @click="ble.connect">连接设备</a-button>
|
|
|
+ </template>
|
|
|
+ </a-alert>
|
|
|
+
|
|
|
+ <div :class="isMobile ? 'mobile-layout' : 'desktop-layout'">
|
|
|
+ <div class="config-col">
|
|
|
+ <a-card v-if="isMobile" class="section-card" size="small" title="设备状态">
|
|
|
+ <a-descriptions :column="1" :label-style="{ width: '80px' }" size="small">
|
|
|
+ <a-descriptions-item label="WiFi">
|
|
|
+ <a-tag v-if="ble.config.value?.wifi_ssid" color="success">{{ ble.config.value.wifi_ssid }}</a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="MQTT">
|
|
|
+ <a-tag v-if="ble.config.value?.mqtt_broker" color="success">{{ ble.config.value.mqtt_broker }}</a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="模式">
|
|
|
+ <a-tag color="processing">{{ ble.currentMode.value || '-' }}</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="通信">
|
|
|
+ {{ ble.config.value?.comm_mode === 1 ? 'MQTT' : 'BLE-only' }}
|
|
|
+ </a-descriptions-item>
|
|
|
+ </a-descriptions>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card" size="small" title="灯效模式">
|
|
|
+ <div :class="isMobile ? 'mode-grid-mobile' : 'mode-grid-desktop'">
|
|
|
+ <a-button
|
|
|
+ v-for="m in LIGHT_MODES"
|
|
|
+ :key="m.key"
|
|
|
+ :type="ble.currentMode.value === m.key ? 'primary' : 'default'"
|
|
|
+ class="mode-btn"
|
|
|
+ @click="ble.setMode(m.key)"
|
|
|
+ >
|
|
|
+ {{ m.label }}
|
|
|
+ </a-button>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card" size="small" title="WiFi 配置">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-form-item label="SSID">
|
|
|
+ <a-input v-model:value="bleWifiForm.ssid" placeholder="WiFi 名称"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="密码">
|
|
|
+ <a-input-password v-model:value="bleWifiForm.password" placeholder="WiFi 密码"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card" size="small" title="MQTT 配置">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-form-item label="Broker">
|
|
|
+ <a-input v-model:value="bleMqttForm.broker" placeholder="192.168.1.100"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-row :gutter="12">
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-form-item label="端口">
|
|
|
+ <a-input-number v-model:value="bleMqttForm.port" :max="65535" :min="1" style="width: 100%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-form-item label="Client ID">
|
|
|
+ <a-input v-model:value="bleMqttForm.client" placeholder="AI-Light"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ <a-form-item label="用户名">
|
|
|
+ <a-input v-model:value="bleMqttForm.username" placeholder="可选"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="密码">
|
|
|
+ <a-input-password v-model:value="bleMqttForm.password" placeholder="可选"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="订阅主题">
|
|
|
+ <a-input v-model:value="bleMqttForm.topic" placeholder="agent/status"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="状态发布主题">
|
|
|
+ <a-input v-model:value="bleMqttForm.statusTopic" placeholder="openCodeLight/status"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="配置订阅主题">
|
|
|
+ <a-input v-model:value="bleMqttForm.topicConfig" placeholder="agent/status/config"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card" size="small" title="引脚配置(灯序)">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-row :gutter="12">
|
|
|
+ <a-col :span="8">
|
|
|
+ <a-form-item label="红灯引脚">
|
|
|
+ <a-input-number v-model:value="blePinForm.red" :max="21" :min="0" style="width: 100%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="8">
|
|
|
+ <a-form-item label="绿灯引脚">
|
|
|
+ <a-input-number v-model:value="blePinForm.green" :max="21" :min="0" style="width: 100%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="8">
|
|
|
+ <a-form-item label="黄灯引脚">
|
|
|
+ <a-input-number v-model:value="blePinForm.yellow" :max="21" :min="0" style="width: 100%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ <a-alert
|
|
|
+ banner
|
|
|
+ message="默认接线:红=IO4、绿=IO3、黄=IO2。修改引脚后需确认硬件接线对应。"
|
|
|
+ show-icon
|
|
|
+ type="info"
|
|
|
+ />
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-row :gutter="8">
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-button block size="large" type="primary" @click="handleBleSave">
|
|
|
+ <template #icon>
|
|
|
+ <SaveOutlined/>
|
|
|
+ </template>
|
|
|
+ 保存配置
|
|
|
+ </a-button>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-button block danger size="large" @click="handleBleRestart">
|
|
|
+ <template #icon>
|
|
|
+ <ReloadOutlined/>
|
|
|
+ </template>
|
|
|
+ 重启设备
|
|
|
+ </a-button>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="!isMobile" class="side-col">
|
|
|
+ <a-card class="section-card" size="small" title="设备状态">
|
|
|
+ <a-descriptions :column="1" :label-style="{ width: '80px' }" size="small">
|
|
|
+ <a-descriptions-item label="WiFi">
|
|
|
+ <a-tag v-if="ble.config.value?.wifi_ssid" color="success">{{ ble.config.value.wifi_ssid }}</a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="MQTT">
|
|
|
+ <a-tag v-if="ble.config.value?.mqtt_broker" color="success">{{ ble.config.value.mqtt_broker }}</a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="模式">
|
|
|
+ <a-tag color="processing">{{ ble.currentMode.value || '-' }}</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="通信">
|
|
|
+ {{ ble.config.value?.comm_mode === 1 ? 'MQTT' : 'BLE-only' }}
|
|
|
+ </a-descriptions-item>
|
|
|
+ </a-descriptions>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card log-card" size="small" title="日志">
|
|
|
+ <template #extra>
|
|
|
+ <a-button size="small" type="text" @click="ble.clearLogs">
|
|
|
+ <ClearOutlined/>
|
|
|
+ 清空
|
|
|
+ </a-button>
|
|
|
+ </template>
|
|
|
+ <div class="log-container">
|
|
|
+ <div v-for="(line, i) in ble.logs.value" :key="i" class="log-line">{{ line }}</div>
|
|
|
+ <div v-if="ble.logs.value.length === 0" class="log-empty">暂无日志</div>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 消息配置 Tab -->
|
|
|
+ <template v-if="activeTab === 'mqtt'">
|
|
|
+ <a-alert
|
|
|
+ v-if="mqtt.connectionState.value === 'connected'"
|
|
|
+ :message="'已连接: ' + mqtt.deviceName.value"
|
|
|
+ class="conn-alert"
|
|
|
+ show-icon
|
|
|
+ type="success"
|
|
|
+ >
|
|
|
+ <template #action>
|
|
|
+ <a-button danger size="small" @click="mqtt.disconnect">断开</a-button>
|
|
|
+ </template>
|
|
|
+ </a-alert>
|
|
|
+ <a-alert
|
|
|
+ v-else-if="mqtt.connectionState.value === 'connecting'"
|
|
|
+ class="conn-alert"
|
|
|
+ message="正在加载配置..."
|
|
|
+ show-icon
|
|
|
+ type="info"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <LoadingOutlined spin/>
|
|
|
+ </template>
|
|
|
+ <template #action>
|
|
|
+ <a-button disabled size="small">加载中</a-button>
|
|
|
+ </template>
|
|
|
+ </a-alert>
|
|
|
+ <a-alert
|
|
|
+ v-else
|
|
|
+ class="conn-alert"
|
|
|
+ description="请连接后端服务以进行远程配置"
|
|
|
+ message="未连接"
|
|
|
+ show-icon
|
|
|
+ type="warning"
|
|
|
+ >
|
|
|
+ <template #action>
|
|
|
+ <a-button size="small" type="primary" @click="mqtt.connect">连接</a-button>
|
|
|
+ </template>
|
|
|
+ </a-alert>
|
|
|
+
|
|
|
+ <div :class="isMobile ? 'mobile-layout' : 'desktop-layout'">
|
|
|
+ <div class="config-col">
|
|
|
+ <a-card v-if="isMobile" class="section-card" size="small" title="设备状态">
|
|
|
+ <a-descriptions :column="1" :label-style="{ width: '80px' }" size="small">
|
|
|
+ <a-descriptions-item label="WiFi">
|
|
|
+ <a-tag v-if="mqtt.config.value?.wifi_ssid" color="success">{{ mqtt.config.value.wifi_ssid }}</a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="MQTT">
|
|
|
+ <a-tag v-if="mqtt.config.value?.mqtt_broker" color="success">{{
|
|
|
+ mqtt.config.value.mqtt_broker
|
|
|
+ }}
|
|
|
+ </a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="模式">
|
|
|
+ <a-tag color="processing">{{ mqtt.currentMode.value || '-' }}</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="通信">MQTT</a-descriptions-item>
|
|
|
+ </a-descriptions>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card" size="small" title="灯效模式">
|
|
|
+ <div :class="isMobile ? 'mode-grid-mobile' : 'mode-grid-desktop'">
|
|
|
+ <a-button
|
|
|
+ v-for="m in LIGHT_MODES"
|
|
|
+ :key="m.key"
|
|
|
+ :type="mqtt.currentMode.value === m.key ? 'primary' : 'default'"
|
|
|
+ class="mode-btn"
|
|
|
+ @click="mqtt.setMode(m.key)"
|
|
|
+ >
|
|
|
+ {{ m.label }}
|
|
|
+ </a-button>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card" size="small" title="WiFi 配置">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-form-item label="SSID">
|
|
|
+ <a-input v-model:value="mqttWifiForm.ssid" placeholder="WiFi 名称"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="密码">
|
|
|
+ <a-input-password v-model:value="mqttWifiForm.password" placeholder="WiFi 密码"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card" size="small" title="MQTT 配置">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-form-item label="Broker">
|
|
|
+ <a-input v-model:value="mqttMqttForm.broker" placeholder="192.168.1.100"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-row :gutter="12">
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-form-item label="端口">
|
|
|
+ <a-input-number v-model:value="mqttMqttForm.port" :max="65535" :min="1" style="width: 100%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-form-item label="Client ID">
|
|
|
+ <a-input v-model:value="mqttMqttForm.client" placeholder="AI-Light"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ <a-form-item label="用户名">
|
|
|
+ <a-input v-model:value="mqttMqttForm.username" placeholder="可选"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="密码">
|
|
|
+ <a-input-password v-model:value="mqttMqttForm.password" placeholder="可选"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="订阅主题">
|
|
|
+ <a-input v-model:value="mqttMqttForm.topic" placeholder="agent/status"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="状态发布主题">
|
|
|
+ <a-input v-model:value="mqttMqttForm.statusTopic" placeholder="openCodeLight/status"/>
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="配置订阅主题">
|
|
|
+ <a-input v-model:value="mqttMqttForm.topicConfig" placeholder="agent/status/config"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card" size="small" title="引脚配置(灯序)">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-row :gutter="12">
|
|
|
+ <a-col :span="8">
|
|
|
+ <a-form-item label="红灯引脚">
|
|
|
+ <a-input-number v-model:value="mqttPinForm.red" :max="21" :min="0" style="width: 100%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="8">
|
|
|
+ <a-form-item label="绿灯引脚">
|
|
|
+ <a-input-number v-model:value="mqttPinForm.green" :max="21" :min="0" style="width: 100%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="8">
|
|
|
+ <a-form-item label="黄灯引脚">
|
|
|
+ <a-input-number v-model:value="mqttPinForm.yellow" :max="21" :min="0" style="width: 100%"/>
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ <a-alert
|
|
|
+ banner
|
|
|
+ message="默认接线:红=IO4、绿=IO3、黄=IO2。修改引脚后需确认硬件接线对应。"
|
|
|
+ show-icon
|
|
|
+ type="info"
|
|
|
+ />
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-row :gutter="8">
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-button block size="large" type="primary" @click="handleMqttSave">
|
|
|
+ <template #icon>
|
|
|
+ <SaveOutlined/>
|
|
|
+ </template>
|
|
|
+ 保存配置
|
|
|
+ </a-button>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-button block danger size="large" @click="handleMqttRestart">
|
|
|
+ <template #icon>
|
|
|
+ <ReloadOutlined/>
|
|
|
+ </template>
|
|
|
+ 重启设备
|
|
|
+ </a-button>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="!isMobile" class="side-col">
|
|
|
+ <a-card class="section-card" size="small" title="设备状态">
|
|
|
+ <a-descriptions :column="1" :label-style="{ width: '80px' }" size="small">
|
|
|
+ <a-descriptions-item label="WiFi">
|
|
|
+ <a-tag v-if="mqtt.config.value?.wifi_ssid" color="success">{{ mqtt.config.value.wifi_ssid }}</a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="MQTT">
|
|
|
+ <a-tag v-if="mqtt.config.value?.mqtt_broker" color="success">{{
|
|
|
+ mqtt.config.value.mqtt_broker
|
|
|
+ }}
|
|
|
+ </a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="模式">
|
|
|
+ <a-tag color="processing">{{ mqtt.currentMode.value || '-' }}</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="通信">MQTT</a-descriptions-item>
|
|
|
+ </a-descriptions>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <a-card class="section-card log-card" size="small" title="日志">
|
|
|
+ <template #extra>
|
|
|
+ <a-button size="small" type="text" @click="mqtt.clearLogs">
|
|
|
+ <ClearOutlined/>
|
|
|
+ 清空
|
|
|
+ </a-button>
|
|
|
+ </template>
|
|
|
+ <div class="log-container">
|
|
|
+ <div v-for="(line, i) in mqtt.logs.value" :key="i" class="log-line">{{ line }}</div>
|
|
|
+ <div v-if="mqtt.logs.value.length === 0" class="log-empty">暂无日志</div>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </a-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.device-config-page {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ color: var(--text-color);
|
|
|
+}
|
|
|
+
|
|
|
+.conn-alert {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.config-card {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.section-card {
|
|
|
+ margin-bottom: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.mode-grid-mobile {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.mode-grid-desktop {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.mode-btn {
|
|
|
+ height: 40px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.log-container {
|
|
|
+ background: #1a1a2e;
|
|
|
+ border-radius: 8px;
|
|
|
+ padding: 12px;
|
|
|
+ font-family: 'SF Mono', 'Fira Code', monospace;
|
|
|
+ font-size: 11px;
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ line-height: 1.8;
|
|
|
+}
|
|
|
+
|
|
|
+.log-line {
|
|
|
+ color: #a5d6a7;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+.log-empty {
|
|
|
+ color: #666;
|
|
|
+ text-align: center;
|
|
|
+ padding: 20px 0;
|
|
|
+}
|
|
|
+
|
|
|
+.desktop-layout {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ align-items: stretch;
|
|
|
+}
|
|
|
+
|
|
|
+.config-col {
|
|
|
+ flex: 1;
|
|
|
+ min-width: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.side-col {
|
|
|
+ width: 360px;
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.log-card {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.log-card :deep(.ant-card-body) {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 767px) {
|
|
|
+ .mode-grid-mobile {
|
|
|
+ grid-template-columns: repeat(2, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@media (min-width: 1200px) {
|
|
|
+ .mode-grid-desktop {
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|