|
|
@@ -0,0 +1,315 @@
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, watch, onMounted } from 'vue'
|
|
|
+import { message } from 'ant-design-vue'
|
|
|
+import {
|
|
|
+ MobileOutlined,
|
|
|
+ DisconnectOutlined,
|
|
|
+ SaveOutlined,
|
|
|
+ ReloadOutlined,
|
|
|
+ ClearOutlined,
|
|
|
+ CheckCircleFilled,
|
|
|
+ CloseCircleFilled,
|
|
|
+ LoadingOutlined,
|
|
|
+} from '@ant-design/icons-vue'
|
|
|
+import { useBleDevice } from '@/composables/useBleDevice'
|
|
|
+
|
|
|
+const {
|
|
|
+ connectionState,
|
|
|
+ deviceName,
|
|
|
+ currentMode,
|
|
|
+ config,
|
|
|
+ logs,
|
|
|
+ connect,
|
|
|
+ disconnect,
|
|
|
+ setMode,
|
|
|
+ saveConfig,
|
|
|
+ restartDevice,
|
|
|
+ clearLogs,
|
|
|
+} = useBleDevice()
|
|
|
+
|
|
|
+const isMobile = ref(window.innerWidth < 768)
|
|
|
+
|
|
|
+const wifiForm = reactive({
|
|
|
+ ssid: '',
|
|
|
+ password: '',
|
|
|
+})
|
|
|
+
|
|
|
+const mqttForm = reactive({
|
|
|
+ broker: '',
|
|
|
+ port: 1883,
|
|
|
+ client: 'AI-Light',
|
|
|
+ username: '',
|
|
|
+ password: '',
|
|
|
+ topic: 'opencode/status',
|
|
|
+ statusTopic: 'openCodeLight/status',
|
|
|
+})
|
|
|
+
|
|
|
+watch(config, (cfg) => {
|
|
|
+ if (cfg) {
|
|
|
+ wifiForm.ssid = cfg.wifi_ssid || ''
|
|
|
+ mqttForm.broker = cfg.mqtt_broker || ''
|
|
|
+ mqttForm.port = cfg.mqtt_port || 1883
|
|
|
+ mqttForm.client = cfg.mqtt_client || 'AI-Light'
|
|
|
+ mqttForm.username = cfg.mqtt_user || ''
|
|
|
+ mqttForm.topic = cfg.mqtt_topic || ''
|
|
|
+ mqttForm.statusTopic = cfg.mqtt_status || ''
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+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' },
|
|
|
+]
|
|
|
+
|
|
|
+async function handleSaveConfig() {
|
|
|
+ const cfg = {
|
|
|
+ wifi_ssid: wifiForm.ssid,
|
|
|
+ wifi_pass: wifiForm.password,
|
|
|
+ mqtt_broker: mqttForm.broker,
|
|
|
+ mqtt_port: mqttForm.port,
|
|
|
+ mqtt_user: mqttForm.username,
|
|
|
+ mqtt_pass: mqttForm.password,
|
|
|
+ mqtt_client: mqttForm.client,
|
|
|
+ mqtt_topic: mqttForm.topic,
|
|
|
+ mqtt_status: mqttForm.statusTopic,
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await saveConfig(cfg)
|
|
|
+ message.success('配置已保存,设备将重启')
|
|
|
+ } catch {
|
|
|
+ message.error('保存失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleRestart() {
|
|
|
+ try {
|
|
|
+ await restartDevice()
|
|
|
+ message.success('重启指令已发送')
|
|
|
+ } catch {
|
|
|
+ message.error('重启失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function onResize() {
|
|
|
+ isMobile.value = window.innerWidth < 768
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => window.addEventListener('resize', onResize))
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="ble-device-page">
|
|
|
+ <!-- Connection Bar -->
|
|
|
+ <a-alert
|
|
|
+ v-if="connectionState === 'disconnected'"
|
|
|
+ message="未连接设备"
|
|
|
+ type="warning"
|
|
|
+ show-icon
|
|
|
+ :banner="isMobile"
|
|
|
+ class="conn-bar"
|
|
|
+ />
|
|
|
+ <a-alert
|
|
|
+ v-else-if="connectionState === 'scanning' || connectionState === 'connecting'"
|
|
|
+ :message="connectionState === 'scanning' ? '正在扫描...' : '正在连接...'"
|
|
|
+ type="info"
|
|
|
+ show-icon
|
|
|
+ :banner="isMobile"
|
|
|
+ class="conn-bar"
|
|
|
+ >
|
|
|
+ <template #icon><LoadingOutlined spin /></template>
|
|
|
+ </a-alert>
|
|
|
+ <a-alert
|
|
|
+ v-else
|
|
|
+ :message="'已连接: ' + deviceName"
|
|
|
+ type="success"
|
|
|
+ show-icon
|
|
|
+ :banner="isMobile"
|
|
|
+ class="conn-bar"
|
|
|
+ />
|
|
|
+
|
|
|
+ <a-button
|
|
|
+ :type="connectionState === 'connected' ? 'default' : 'primary'"
|
|
|
+ block
|
|
|
+ size="large"
|
|
|
+ class="conn-btn"
|
|
|
+ :danger="connectionState === 'connected'"
|
|
|
+ @click="connect"
|
|
|
+ >
|
|
|
+ <template #icon>
|
|
|
+ <DisconnectOutlined v-if="connectionState === 'connected'" />
|
|
|
+ <MobileOutlined v-else />
|
|
|
+ </template>
|
|
|
+ {{ connectionState === 'connected' ? '断开连接' : '连接设备' }}
|
|
|
+ </a-button>
|
|
|
+
|
|
|
+ <template v-if="connectionState === 'connected'">
|
|
|
+ <!-- Device Status -->
|
|
|
+ <a-card title="设备状态" size="small" class="section-card">
|
|
|
+ <a-descriptions :column="1" size="small" :label-style="{ width: '80px' }">
|
|
|
+ <a-descriptions-item label="WiFi">
|
|
|
+ <a-tag v-if="config?.wifi_ssid" color="success">{{ config.wifi_ssid }}</a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="MQTT">
|
|
|
+ <a-tag v-if="config?.mqtt_broker" color="success">{{ config.mqtt_broker }}</a-tag>
|
|
|
+ <a-tag v-else color="error">未配置</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="模式">
|
|
|
+ <a-tag color="processing">{{ currentMode || '-' }}</a-tag>
|
|
|
+ </a-descriptions-item>
|
|
|
+ <a-descriptions-item label="通信">
|
|
|
+ {{ config?.comm_mode === 1 ? 'MQTT' : 'BLE-only' }}
|
|
|
+ </a-descriptions-item>
|
|
|
+ </a-descriptions>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <!-- Light Mode -->
|
|
|
+ <a-card title="灯效模式" size="small" class="section-card">
|
|
|
+ <div class="mode-grid">
|
|
|
+ <a-button
|
|
|
+ v-for="m in LIGHT_MODES"
|
|
|
+ :key="m.key"
|
|
|
+ :type="currentMode === m.key ? 'primary' : 'default'"
|
|
|
+ class="mode-btn"
|
|
|
+ @click="setMode(m.key)"
|
|
|
+ >
|
|
|
+ {{ m.label }}
|
|
|
+ </a-button>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <!-- WiFi Config -->
|
|
|
+ <a-card title="WiFi 配置" size="small" class="section-card">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-form-item label="SSID">
|
|
|
+ <a-input v-model:value="wifiForm.ssid" placeholder="WiFi 名称" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="密码">
|
|
|
+ <a-input-password v-model:value="wifiForm.password" placeholder="WiFi 密码" />
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <!-- MQTT Config -->
|
|
|
+ <a-card title="MQTT 配置" size="small" class="section-card">
|
|
|
+ <a-form layout="vertical">
|
|
|
+ <a-form-item label="Broker">
|
|
|
+ <a-input v-model:value="mqttForm.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="mqttForm.port" :min="1" :max="65535" style="width: 100%" />
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ <a-col :span="12">
|
|
|
+ <a-form-item label="Client ID">
|
|
|
+ <a-input v-model:value="mqttForm.client" placeholder="AI-Light" />
|
|
|
+ </a-form-item>
|
|
|
+ </a-col>
|
|
|
+ </a-row>
|
|
|
+ <a-form-item label="用户名">
|
|
|
+ <a-input v-model:value="mqttForm.username" placeholder="可选" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="密码">
|
|
|
+ <a-input-password v-model:value="mqttForm.password" placeholder="可选" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="订阅主题">
|
|
|
+ <a-input v-model:value="mqttForm.topic" placeholder="opencode/status" />
|
|
|
+ </a-form-item>
|
|
|
+ <a-form-item label="状态发布主题">
|
|
|
+ <a-input v-model:value="mqttForm.statusTopic" placeholder="openCodeLight/status" />
|
|
|
+ </a-form-item>
|
|
|
+ </a-form>
|
|
|
+ </a-card>
|
|
|
+
|
|
|
+ <!-- Actions -->
|
|
|
+ <a-space direction="vertical" style="width: 100%" :size="12">
|
|
|
+ <a-button type="primary" block size="large" @click="handleSaveConfig">
|
|
|
+ <template #icon><SaveOutlined /></template>
|
|
|
+ 保存配置并重启
|
|
|
+ </a-button>
|
|
|
+ <a-button block size="large" @click="handleRestart">
|
|
|
+ <template #icon><ReloadOutlined /></template>
|
|
|
+ 重启设备
|
|
|
+ </a-button>
|
|
|
+ </a-space>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- Log -->
|
|
|
+ <a-card title="日志" size="small" class="section-card" v-if="logs.length > 0">
|
|
|
+ <template #extra>
|
|
|
+ <a-button type="text" size="small" @click="clearLogs">
|
|
|
+ <ClearOutlined /> 清空
|
|
|
+ </a-button>
|
|
|
+ </template>
|
|
|
+ <div class="log-container">
|
|
|
+ <div v-for="(line, i) in logs" :key="i" class="log-line">{{ line }}</div>
|
|
|
+ </div>
|
|
|
+ </a-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.ble-device-page {
|
|
|
+ max-width: 520px;
|
|
|
+ margin: 0 auto;
|
|
|
+ color: var(--text-color);
|
|
|
+}
|
|
|
+
|
|
|
+.conn-bar {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.conn-btn {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.section-card {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.mode-grid {
|
|
|
+ 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;
|
|
|
+ max-height: 200px;
|
|
|
+ overflow-y: auto;
|
|
|
+ line-height: 1.8;
|
|
|
+}
|
|
|
+
|
|
|
+.log-line {
|
|
|
+ color: #a5d6a7;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+
|
|
|
+@media (max-width: 767px) {
|
|
|
+ .ble-device-page {
|
|
|
+ max-width: 100%;
|
|
|
+ }
|
|
|
+
|
|
|
+ .mode-grid {
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|