import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import BindScreen from "./components/BindScreen.jsx"; import { isDeviceBound } from "./utils/tenant"; import ErrorBoundary from "./components/ErrorBoundary"; import { ThemeProvider } from "@appica/ui-react/providers/theme-provider"; 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)★ 2. 后端 GET /api/config(MQTT 地址/账号,后端 .env 为准) 3. 内置默认(Tauri 打包回退 192.168.1.9 / 浏览器用当前主机) 结果写入 window.__DPM_*__,api.js / mqtt.js 惰性读取生效。 ========================================================= */ async function bootstrapRuntimeConfig() { // 1) exe 旁 config.json(最高优先):{api_base, mqtt_url, mqtt_username, mqtt_password} try { const raw = await invoke("read_runtime_config"); if (raw) { const cfg = JSON.parse(raw); if (cfg.api_base) window.__DPM_API__ = cfg.api_base; if (cfg.mqtt_url) window.__DPM_MQTT__ = cfg.mqtt_url; if (cfg.mqtt_username) window.__DPM_MQTT_USER__ = cfg.mqtt_username; if (cfg.mqtt_password) window.__DPM_MQTT_PASS__ = cfg.mqtt_password; if (cfg.voice_url) window.__DPM_VOICE_WS__ = cfg.voice_url; // eslint-disable-next-line no-console if (cfg.mqtt_url) console.info(`[bootstrap] config.json MQTT -> ${cfg.mqtt_url}`); } } catch { /* 浏览器开发环境或未注册命令:跳过 */ } // AudioWorklet 兼容:打包环境(WebView2)无法从 tauri.localhost 自定义协议加载 // worklet 模块,统一改从后端真实 HTTP 服务加载(backend/static/voice-worklets/) if (!window.VOICE_WORKLETS_BASE) { window.VOICE_WORKLETS_BASE = `${getApiBase()}/static/voice-worklets/`; } // 2) 后端 /api/config(MQTT 地址/账号;API 基址以前端实际可达地址为准) // 优先级保护:若构建期已用 .env.local 烘焙了 VITE_MQTT_URL, // 则后端 /api/config 不再覆盖(避免后端默认 localhost 冲掉正确地址)。 const bakedMqtt = import.meta.env.VITE_MQTT_URL; try { const res = await fetch(`${getApiBase()}/api/config`, { cache: 'no-store' }); if (res.ok) { const cfg = await res.json(); if (!bakedMqtt && cfg.mqtt_url) window.__DPM_MQTT__ = cfg.mqtt_url; if (!bakedMqtt && cfg.mqtt_username) window.__DPM_MQTT_USER__ = cfg.mqtt_username; if (!bakedMqtt && cfg.mqtt_password) window.__DPM_MQTT_PASS__ = cfg.mqtt_password; if (!import.meta.env.VITE_VOICE_WS && cfg.voice_url) window.__DPM_VOICE_WS__ = cfg.voice_url; // 视频识别:独立 park-vision 地址(enabled 才下发;空=禁用) if (cfg.vision_api) window.__DPM_VISION__ = cfg.vision_api; // eslint-disable-next-line no-console if (cfg.mqtt_url) console.info(`[bootstrap] /api/config MQTT -> ${cfg.mqtt_url}${bakedMqtt ? '(已烘焙,忽略)' : ''}`); } } catch { /* 后端不可达:使用内置默认地址 */ } } bootstrapRuntimeConfig().then(() => { ReactDOM.createRoot(document.getElementById("root")).render( {/* 多租户:未绑定大屏 → 待绑定页(连接码+二维码);已绑定 → 大屏应用 */} {isDeviceBound() ? : } , ); });