Files
park-desktop/src/main.jsx
T

94 lines
4.4 KiB
React
Raw Normal View History

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(
<React.StrictMode>
<ErrorBoundary>
<ThemeProvider>
{/* 多租户:未绑定大屏 → 待绑定页(连接码+二维码);已绑定 → 大屏应用 */}
{isDeviceBound() ? <App /> : <BindScreen />}
</ThemeProvider>
</ErrorBoundary>
</React.StrictMode>,
);
});