import { useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { onMqttMessage, connectMqtt, publishMqtt, getClientId, useMqttStatus } from './mqtt'; import { MQTT_TOPIC_HEARTBEAT, MQTT_TOPIC_COMMAND, MQTT_TOPIC_BIND, getApiBase as API_BASE } from '../config'; import { getTenantId, clearBinding } from './tenant'; import { Sfx } from './sounds'; import { PAGE_ORDER } from './pageNav'; import { aiStatus } from './statusBus'; import { invoke } from '@tauri-apps/api/core'; import { getCurrentWindow } from '@tauri-apps/api/window'; // 当前窗口标识:主屏 'main' / 双屏副窗 'secondary' // 优先用 Rust 注入的 __DPM_WINDOW_LABEL__(副窗初始化脚本),再回退 getCurrentWindow().label const WINDOW_LABEL = (() => { try { if (window.__DPM_WINDOW_LABEL__) return window.__DPM_WINDOW_LABEL__; return getCurrentWindow().label || 'main'; } catch { return 'main'; } })(); /* ========================================================= MQTT 控制 —— 后端通过 opc/display/command 控制大屏 · navigate / navigate_rel:页面切换(与方向键同一通道) · play / pause / next / prev / set_mode:媒体控制 · alert:全局通知 · show_card:展示信息卡片(AI 工具调用 / 管理端指令) · 心跳:定时上报 opc/display/heartbeat(后端统计在线大屏) ========================================================= */ const PAGE_ALIAS = { home: '/', '数据大屏': '/', '首页': '/', twin: '/twin', '数字孪生': '/twin', '3d': '/twin', ai: '/ai', 'ai助手': '/ai', tasks: '/tasks', '任务展示': '/tasks', task: '/tasks', screen: '/screen', '媒体轮播': '/screen', media: '/screen', wall: '/wall', '企业展示墙': '/wall', '企业墙': '/wall', '展示墙': '/wall', }; function resolvePage(page) { if (!page) return null; if (PAGE_ALIAS[String(page).toLowerCase()] !== undefined) return PAGE_ALIAS[String(page).toLowerCase()]; if (PAGE_ORDER.some((p) => p.path === page)) return page; return null; } export function useMqttControl() { const navigate = useNavigate(); const location = useLocation(); const mqttStatus = useMqttStatus(); // 设备注册:MQTT 连接后向后端上报「唯一 device_id + 角色 + 从属主屏」,供后端精准控制 useEffect(() => { if (mqttStatus !== 'connected' || !getClientId()) return; fetch(`${API_BASE()}/api/display/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ device_id: getClientId(), role: WINDOW_LABEL, parent_id: window.__DPM_PARENT_ID__ || '', // 副屏标识从属的主屏 device_id }), }).catch(() => {}); }, [mqttStatus]); // 后端主动解绑:监听绑定频道,收到 unbound 事件 → 清理本地绑定并退出到待绑定页 // (大屏已绑定后仍订阅 bind/,见 mqtt.js subscribe;unbind 后 main.jsx 见 // isDeviceBound()=false 渲染 BindScreen) useEffect(() => { connectMqtt(); const off = onMqttMessage((topic, data) => { if (topic === `${MQTT_TOPIC_BIND}/${getClientId()}` && data && data.event === 'unbound') { clearBinding(); } }); return off; }, []); // 大屏在线心跳(后端据此统计在线大屏数) useEffect(() => { connectMqtt(); const sendHeartbeat = () => { if (mqttStatus === 'connected') { publishMqtt(MQTT_TOPIC_HEARTBEAT, { client_id: getClientId(), tenant_id: getTenantId(), page: location.pathname, ts: Date.now(), }); } }; sendHeartbeat(); const hb = setInterval(sendHeartbeat, 10000); return () => clearInterval(hb); }, [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(() => { connectMqtt(); const off = onMqttMessage((topic, cmd) => { // 接收全局控制 topic 或本客户端独有 topic(后端按屏幕终端分别下发) const isCmd = topic === MQTT_TOPIC_COMMAND || topic.startsWith(`${MQTT_TOPIC_COMMAND}/`); if (!isCmd || !cmd || !cmd.action) return; // 屏幕终端精准控制:后端按 client_id 路由到独有 topic。 // 兜底:命令若带 screen_id,只有本客户端 client_id 匹配才处理(防误收全局)。 const sc = cmd.screen; console.warn(`[dual] 命令 action=${cmd.action} screen_id=${cmd.screen_id || '(无)'} screen_role=${cmd.screen_role || '(无)'} 本窗口=${WINDOW_LABEL} 本client=${getClientId()}`); if (cmd.screen_id && cmd.screen_id !== getClientId()) return; if (sc && sc !== 'both' && sc !== WINDOW_LABEL) return; switch (cmd.action) { case 'navigate': { const path = resolvePage(cmd.params?.page); if (path) { Sfx.page(); aiStatus('正在切换页面', { icon: 'page' }); navigate(path); } break; } case 'navigate_rel': { const delta = Number(cmd.params?.delta ?? 1) || 1; const idx = PAGE_ORDER.findIndex((p) => p.path === location.pathname); if (idx >= 0) { const n = PAGE_ORDER.length; aiStatus('正在切换页面', { icon: 'page' }); navigate(PAGE_ORDER[(idx + delta + n) % n].path); } break; } case 'dual_screen': // 双屏:仅主窗口执行(只有主屏能开启/关闭副屏),把主屏 device_id 传给副窗作 parent_id if (WINDOW_LABEL !== 'main') break; aiStatus(cmd.params?.on === false ? '正在关闭双屏' : '正在启动双屏', { icon: 'page' }); invoke('set_dual_screen', { on: cmd.params?.on !== false, parentId: getClientId() }) .catch((err) => { const msg = String((err && err.message) || err || '操作失败'); aiStatus(`双屏失败:${msg}`, { state: 'error', icon: 'error' }); console.error('[dual]', err); }); break; case 'alert': // 通知本身即展示,无需再弹「正在发送通知」状态提示 window.dispatchEvent(new CustomEvent('dpm:alert', { detail: cmd.params || {} })); break; case 'show_card': aiStatus('正在展示信息卡片', { icon: 'tool' }); window.dispatchEvent(new CustomEvent('dpm:show-card', { detail: cmd.params || {} })); break; case 'play': case 'pause': case 'next': case 'prev': Sfx.tick(); case 'set_mode': case 'play_target': case 'minimize': case 'settings_changed': case 'playlist_changed': window.dispatchEvent(new CustomEvent('dpm:media-control', { detail: cmd })); 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; // ── 任务展示页操作(语音页已暂时关闭,voice_* 指令不再处理) ── case 'task_refresh': window.dispatchEvent(new CustomEvent('dpm:task-control', { detail: { action: 'refresh', ...(cmd.params || {}) }, })); break; // ── 企业展示墙(/wall)操作 ── case 'wall_pause': case 'wall_resume': case 'wall_speed': window.dispatchEvent(new CustomEvent('dpm:wall-control', { detail: { action: cmd.action.replace('wall_', ''), ...(cmd.params || {}) }, })); break; default: break; } }); // 离线兜底:AI 对话返回的工具调用在 MQTT 未连接时本地执行 const onLocal = (e) => { const { action, params } = e.detail || {}; if (action === 'navigate') { const path = resolvePage(params?.page); if (path) { aiStatus('正在切换页面', { icon: 'page' }); navigate(path); } } else if (action === 'control') { aiStatus('正在控制媒体播放', { icon: 'tool' }); window.dispatchEvent(new CustomEvent('dpm:media-control', { detail: { action: params?.action } })); } else if (action === 'alert') { // 通知本身即展示,无需状态提示 window.dispatchEvent(new CustomEvent('dpm:alert', { detail: params || {} })); } else if (action === 'show_card') { aiStatus('正在展示信息卡片', { icon: 'tool' }); window.dispatchEvent(new CustomEvent('dpm:show-card', { detail: params || {} })); } }; window.addEventListener('dpm:mqtt-local', onLocal); return () => { off(); window.removeEventListener('dpm:mqtt-local', onLocal); }; }, [navigate, location.pathname]); return null; }