feat: 实现设备注册和精准控制功能,支持双屏管理;更新相关逻辑以优化设备状态跟踪
This commit is contained in:
@@ -7,6 +7,23 @@ import { getApiBase } from "./config";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import "./styles/global.css";
|
||||
|
||||
/* 全局致命错误捕获:任何模块加载/运行/异步错误 → 显示到页面(替代白屏),便于副屏等定位 */
|
||||
function installFatalErrorDiagnostics() {
|
||||
const show = (label, err) => {
|
||||
const el = document.getElementById("fatal-error");
|
||||
if (!el || el.style.display === "block") return;
|
||||
const text = document.getElementById("fatal-error-text");
|
||||
if (text) {
|
||||
const msg = (err && (err.stack || err.message)) || String(err);
|
||||
text.textContent = `${label}\n${msg}`;
|
||||
}
|
||||
el.style.display = "block";
|
||||
};
|
||||
window.addEventListener("error", (e) => show("加载错误:", e.error || e.message));
|
||||
window.addEventListener("unhandledrejection", (e) => show("未处理异常:", e.reason));
|
||||
}
|
||||
installFatalErrorDiagnostics();
|
||||
|
||||
/* =========================================================
|
||||
启动引导(打包部署关键)—— 地址解析优先级:
|
||||
1. exe 旁 config.json(Tauri 命令读取,部署免重打包改 IP)★
|
||||
|
||||
+3
-1
@@ -46,7 +46,9 @@ export function connectMqtt() {
|
||||
client.on('connect', () => {
|
||||
status = 'connected';
|
||||
notifyStatus();
|
||||
client.subscribe([MQTT_TOPIC_COMMAND, MQTT_TOPIC_TICK], { qos: 1 });
|
||||
// 客户端只监听自己唯一的频道 opc/display/command/<clientId>(+ 数据 tick)。
|
||||
// 不再订阅全局命令频道 → 后端按客户端路由时,他屏的指令绝收不到。
|
||||
client.subscribe([MQTT_TOPIC_TICK, `${MQTT_TOPIC_COMMAND}/${clientId}`], { qos: 1 });
|
||||
});
|
||||
client.on('reconnect', () => {
|
||||
status = 'connecting';
|
||||
|
||||
@@ -6,20 +6,29 @@
|
||||
import { getApiBase } from '../config';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
// 当前版本:发布新版本时同步 package.json 的 version
|
||||
export const APP_VERSION = '0.1.0';
|
||||
/** 读取真实安装的应用版本(Tauri getVersion = package.json/tauri.conf.json version);失败时兜底。 */
|
||||
async function getCurrentVersion() {
|
||||
try {
|
||||
const { getVersion } = await import('@tauri-apps/api/app');
|
||||
const v = await getVersion();
|
||||
return v || '0.1.0';
|
||||
} catch {
|
||||
return '0.1.0';
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkForUpdate() {
|
||||
try {
|
||||
const current = await getCurrentVersion();
|
||||
const res = await fetch(`${getApiBase()}/api/update/check`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ current: APP_VERSION }),
|
||||
body: JSON.stringify({ current }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (data && data.ok) {
|
||||
return {
|
||||
current: APP_VERSION,
|
||||
current,
|
||||
latest: data.latest_version,
|
||||
available: !!data.update_available,
|
||||
url: `${getApiBase()}${data.download_url || '/download'}`,
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { onMqttMessage, connectMqtt, publishMqtt, getClientId, useMqttStatus } from './mqtt';
|
||||
import { MQTT_TOPIC_HEARTBEAT } from '../config';
|
||||
import { MQTT_TOPIC_HEARTBEAT, MQTT_TOPIC_COMMAND, getApiBase as API_BASE } from '../config';
|
||||
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'(浏览器开发环境回退 'main')
|
||||
// 当前窗口标识:主屏 'main' / 双屏副窗 'secondary'
|
||||
// 优先用 Rust 注入的 __DPM_WINDOW_LABEL__(副窗初始化脚本),再回退 getCurrentWindow().label
|
||||
const WINDOW_LABEL = (() => {
|
||||
try { return getCurrentWindow().label || 'main'; } catch { return 'main'; }
|
||||
try {
|
||||
if (window.__DPM_WINDOW_LABEL__) return window.__DPM_WINDOW_LABEL__;
|
||||
return getCurrentWindow().label || 'main';
|
||||
} catch { return 'main'; }
|
||||
})();
|
||||
|
||||
/* =========================================================
|
||||
@@ -42,6 +46,20 @@ export function useMqttControl() {
|
||||
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]);
|
||||
|
||||
// 大屏在线心跳(后端据此统计在线大屏数)
|
||||
useEffect(() => {
|
||||
connectMqtt();
|
||||
@@ -75,9 +93,14 @@ export function useMqttControl() {
|
||||
useEffect(() => {
|
||||
connectMqtt();
|
||||
const off = onMqttMessage((topic, cmd) => {
|
||||
if (topic !== 'opc/display/command' || !cmd || !cmd.action) return;
|
||||
// 双屏独立控制:cmd.screen = main|secondary|both(缺省视为 both)
|
||||
// 接收全局控制 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) {
|
||||
@@ -97,10 +120,10 @@ export function useMqttControl() {
|
||||
break;
|
||||
}
|
||||
case 'dual_screen':
|
||||
// 双屏:admin 控制第二块屏全屏展示(Tauri 命令)——仅主窗口执行
|
||||
// 双屏:仅主窗口执行(只有主屏能开启/关闭副屏),把主屏 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 })
|
||||
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' });
|
||||
|
||||
Reference in New Issue
Block a user