101 lines
4.8 KiB
React
101 lines
4.8 KiB
React
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, getMqttUrl, getMqttCredentials, PARK_BOOTSTRAP_TOKEN } 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',
|
||
headers: PARK_BOOTSTRAP_TOKEN ? { 'X-Park-Bootstrap': PARK_BOOTSTRAP_TOKEN } : undefined,
|
||
});
|
||
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(() => {
|
||
// 启动诊断:打印最终生效的 API / MQTT 地址(config.json > 构建烘焙 > /api/config 下发 > 内置默认)
|
||
try {
|
||
console.info(`[bootstrap] API_BASE=${getApiBase()} MQTT_URL=${getMqttUrl()} MQTT_USER=${getMqttCredentials().username || "(空)"}`);
|
||
} catch (e) { /* 忽略 */ }
|
||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||
<React.StrictMode>
|
||
<ErrorBoundary>
|
||
<ThemeProvider>
|
||
{/* 多租户:未绑定大屏 → 待绑定页(连接码+二维码);已绑定 → 大屏应用 */}
|
||
{isDeviceBound() ? <App /> : <BindScreen />}
|
||
</ThemeProvider>
|
||
</ErrorBoundary>
|
||
</React.StrictMode>,
|
||
);
|
||
});
|