feat(mqtt): 集中式 MQTT 调度器(页面级操作 + vision 开关 + 页面上报)
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)、心跳上报沿用既有通道。
This commit is contained in:
@@ -184,6 +184,22 @@ export default function AiChatPanel() {
|
|||||||
});
|
});
|
||||||
}, [send]);
|
}, [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(() => {
|
const toggleVoice = useCallback(() => {
|
||||||
if (voiceStateRef.current === 'recording') {
|
if (voiceStateRef.current === 'recording') {
|
||||||
const rec = recorderRef.current;
|
const rec = recorderRef.current;
|
||||||
|
|||||||
@@ -85,6 +85,28 @@ export default function ParkSidePanel() {
|
|||||||
|
|
||||||
const list = COMPANIES.filter((c) => c.zone === activeZone);
|
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 (
|
return (
|
||||||
<aside className="bd-twin-side">
|
<aside className="bd-twin-side">
|
||||||
{/* 分区切换 */}
|
{/* 分区切换 */}
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ export default function VoiceAssistant() {
|
|||||||
const [messages, setMessages] = useState([]);
|
const [messages, setMessages] = useState([]);
|
||||||
const [muted, setMuted] = useState(false);
|
const [muted, setMuted] = useState(false);
|
||||||
const [errorMsg, setErrorMsg] = useState('');
|
const [errorMsg, setErrorMsg] = useState('');
|
||||||
|
// 全局人物识别开关(MQTT vision_set 控制)
|
||||||
|
const [visionEnabled, setVisionEnabled] = useState(true);
|
||||||
// 园区数据(页脚概览条;后端 /api/dashboard/snapshot,离线本地模拟兜底)
|
// 园区数据(页脚概览条;后端 /api/dashboard/snapshot,离线本地模拟兜底)
|
||||||
const d = useParkSim();
|
const d = useParkSim();
|
||||||
|
|
||||||
@@ -413,7 +415,31 @@ export default function VoiceAssistant() {
|
|||||||
const visionTriggerRef = useRef(null);
|
const visionTriggerRef = useRef(null);
|
||||||
const gestureRef = useRef(null);
|
const gestureRef = useRef(null);
|
||||||
|
|
||||||
|
// MQTT 调度器 → 语音页操作:启动/关闭对话、刷新页面
|
||||||
|
useEffect(() => {
|
||||||
|
const onVoice = (e) => {
|
||||||
|
const { action } = e.detail || {};
|
||||||
|
if (action === 'start') {
|
||||||
|
if (!clientRef.current) void start();
|
||||||
|
} else if (action === 'stop') {
|
||||||
|
void stop();
|
||||||
|
} else if (action === 'refresh') {
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('dpm:voice-control', onVoice);
|
||||||
|
return () => window.removeEventListener('dpm:voice-control', onVoice);
|
||||||
|
}, [start, stop]);
|
||||||
|
|
||||||
|
// MQTT 调度器 → 全局人物识别开关
|
||||||
|
useEffect(() => {
|
||||||
|
const onVision = (e) => setVisionEnabled(Boolean(e.detail?.enabled));
|
||||||
|
window.addEventListener('dpm:vision-control', onVision);
|
||||||
|
return () => window.removeEventListener('dpm:vision-control', onVision);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const vision = useVisionDetection({
|
const vision = useVisionDetection({
|
||||||
|
enabled: visionEnabled,
|
||||||
onTrigger: () => visionTriggerRef.current?.(),
|
onTrigger: () => visionTriggerRef.current?.(),
|
||||||
onGesture: (g) => gestureRef.current?.(g),
|
onGesture: (g) => gestureRef.current?.(g),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -49,6 +49,18 @@ export function useMqttControl() {
|
|||||||
return () => clearInterval(hb);
|
return () => clearInterval(hb);
|
||||||
}, [mqttStatus, location.pathname]);
|
}, [mqttStatus, location.pathname]);
|
||||||
|
|
||||||
|
// 页面状态即时上报:路由变化即通知服务端当前所在页面
|
||||||
|
useEffect(() => {
|
||||||
|
connectMqtt();
|
||||||
|
if (mqttStatus === 'connected') {
|
||||||
|
publishMqtt('opc/frontend/state', {
|
||||||
|
client_id: getClientId(),
|
||||||
|
page: location.pathname,
|
||||||
|
ts: Date.now(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [mqttStatus, location.pathname]);
|
||||||
|
|
||||||
// 控制指令订阅
|
// 控制指令订阅
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connectMqtt();
|
connectMqtt();
|
||||||
@@ -87,6 +99,33 @@ export function useMqttControl() {
|
|||||||
case 'playlist_changed':
|
case 'playlist_changed':
|
||||||
window.dispatchEvent(new CustomEvent('dpm:media-control', { detail: cmd }));
|
window.dispatchEvent(new CustomEvent('dpm:media-control', { detail: cmd }));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
// ── 全局人物识别开关 ──
|
||||||
|
case 'vision_set':
|
||||||
|
window.dispatchEvent(new CustomEvent('dpm:vision-control', {
|
||||||
|
detail: { enabled: Boolean(cmd.params?.enabled) },
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ── AI 助手页操作 ──
|
||||||
|
case 'ai_input':
|
||||||
|
case 'ai_preset':
|
||||||
|
case 'ai_company':
|
||||||
|
case 'ai_zone':
|
||||||
|
window.dispatchEvent(new CustomEvent('dpm:ai-action', {
|
||||||
|
detail: { type: cmd.action.replace('ai_', ''), ...(cmd.params || {}) },
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ── 语音对话页操作 ──
|
||||||
|
case 'voice_start':
|
||||||
|
case 'voice_stop':
|
||||||
|
case 'voice_refresh':
|
||||||
|
window.dispatchEvent(new CustomEvent('dpm:voice-control', {
|
||||||
|
detail: { action: cmd.action.replace('voice_', '') },
|
||||||
|
}));
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user