From 4d693e895d2af0e4d06cdb1f3d7dce85278a41ef Mon Sep 17 00:00:00 2001 From: Pine Date: Tue, 18 Aug 2026 02:20:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(mqtt):=20=E9=9B=86=E4=B8=AD=E5=BC=8F=20MQT?= =?UTF-8?q?T=20=E8=B0=83=E5=BA=A6=E5=99=A8=EF=BC=88=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=BA=A7=E6=93=8D=E4=BD=9C=20+=20vision=20=E5=BC=80=E5=85=B3?= =?UTF-8?q?=20+=20=E9=A1=B5=E9=9D=A2=E4=B8=8A=E6=8A=A5=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useMqttControl 扩展命令分发(opc/display/command): - vision_set:全局人物识别开/关(dpm:vision-control → 语音页 visionEnabled) - ai_input / ai_preset / ai_company / ai_zone:AI 助手页操作(dpm:ai-action) - voice_start / voice_stop / voice_refresh:语音对话页操作(dpm:voice-control) - 页面状态即时上报 opc/frontend/state(路由变化即发,心跳外补充) 页面监听: - AiChatPanel:输入并发送 / 选择预设问题发送 - ParkSidePanel:切换分区(加速区/国际区/成长区)/ 切换展示企业(next/prev) - VoiceAssistant:启动/关闭对话 / 刷新页面;vision 全局开关 - MediaScreen:play/pause/next/prev 已有覆盖 切页(navigate/navigate_rel)、心跳上报沿用既有通道。 --- src/components/AiChatPanel.jsx | 16 +++++++++++++ src/components/ParkSidePanel.jsx | 22 ++++++++++++++++++ src/pages/VoiceAssistant.jsx | 26 +++++++++++++++++++++ src/utils/useMqttControl.js | 39 ++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) diff --git a/src/components/AiChatPanel.jsx b/src/components/AiChatPanel.jsx index bf7e1f1..3898177 100644 --- a/src/components/AiChatPanel.jsx +++ b/src/components/AiChatPanel.jsx @@ -184,6 +184,22 @@ export default function AiChatPanel() { }); }, [send]); + // MQTT 调度器 → AI 页操作:输入并发送 / 选择预设问题 + useEffect(() => { + const onAiAction = (e) => { + const { type, text, index } = e.detail || {}; + if (type === 'input' && text && String(text).trim()) { + setInput(String(text).trim()); + void send(String(text).trim()); + } else if (type === 'preset') { + const q = QUICK_QUESTIONS[Number(index)]; + if (q) void send(q); + } + }; + window.addEventListener('dpm:ai-action', onAiAction); + return () => window.removeEventListener('dpm:ai-action', onAiAction); + }, [send]); + const toggleVoice = useCallback(() => { if (voiceStateRef.current === 'recording') { const rec = recorderRef.current; diff --git a/src/components/ParkSidePanel.jsx b/src/components/ParkSidePanel.jsx index c0ef7af..d9c72da 100644 --- a/src/components/ParkSidePanel.jsx +++ b/src/components/ParkSidePanel.jsx @@ -85,6 +85,28 @@ export default function ParkSidePanel() { const list = COMPANIES.filter((c) => c.zone === activeZone); + // MQTT 调度器 → 企业面板操作:切换分区 / 切换展示企业 + useEffect(() => { + const onAiAction = (e) => { + const { type, zone, action } = e.detail || {}; + if (type === 'zone' && zone) { + const z = String(zone); + if (COMPANIES.some((c) => c.zone === z)) setActiveZone(z); + return; + } + if (type === 'company' && (action === 'next' || action === 'prev')) { + const cur = COMPANIES.filter((c) => c.zone === activeZone); + if (!cur.length) return; + const idx = cur.findIndex((c) => selected && c.room === selected.room); + const base = idx >= 0 ? idx : 0; + const n = (base + (action === 'next' ? 1 : -1) + cur.length) % cur.length; + setSelected(cur[n]); + } + }; + window.addEventListener('dpm:ai-action', onAiAction); + return () => window.removeEventListener('dpm:ai-action', onAiAction); + }, [activeZone, selected]); + return (