Files
park-desktop/src/main.jsx
T
Pine 1566477b97 feat(大屏): 视频识别直连 park-vision + 绑定轮询/二维码
- config.js 增 getVisionApiBase(),优先绑定下发的本园区 vision_api(经 tenant info)
- main.jsx bootstrap 读 /park/api/config 的 vision_api
- useVisionDetection 帧 POST 到独立 vision 服务;未配置识别整体禁用
- BindScreen 连接码轮询(无码5s/有码30min) + 渲染二维码(qrcode.react)、绑定后入园区
- 部署文档补 vision_api/识别服务说明
2026-08-25 01:52:53 +08:00

94 lines
4.4 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>,
);
});