feat: 园区大屏前端(React+Vite+Tailwind+three.js+MQTT)
- 路由:数据大屏/数字孪生/AI/媒体轮播/语音/企业墙 - 多园区通用,园区账号登录显示本园区
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
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;
|
||||
// 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>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</ErrorBoundary>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user