Files
eacp_webapp/src/App.tsx
T
2026-08-09 16:05:16 +08:00

328 lines
12 KiB
TypeScript

import {Settings} from "./Setting.tsx"; // 保留导入,不修改
import {Leaflet_Map} from "./Map/Leaflet_Map.tsx"
import {Aircraft_List} from "./Map/Aircraft_List.tsx";
import enhance from './core/enhance.tsx';
import {useEffect, useState} from 'react';
// @ts-ignore
import {AppstoreOutlined, MailOutlined, SettingOutlined} from '@ant-design/icons';
import {Flex, Menu, MenuProps, Switch, message} from 'antd';
import {Routes, Route, Link, useNavigate, NavigateFunction, useLocation} from 'react-router-dom';
import React from 'react';
import ReactDOM, {Root} from 'react-dom/client';
import {BrowserRouter} from 'react-router-dom';
import FileList from "./FileList.tsx";
import {DSP} from "./DSP/DSP.tsx";
import {Navigate, Router} from "react-router";
import axios from "axios";
import {baseURL} from "./Global.tsx";
import {is_cesium_webgl_available, webgl_unavailable_message} from "./Map/WebGL_Support.ts";
import {Data_Source_Config} from "./Data_Source/Data_Source_Config.tsx";
import L from 'leaflet';
const Cesium_Map_View = React.lazy(() => import("./Map/Cesium_Map.tsx"));
delete (L.Icon.Default.prototype as any)._getIconUrl;
L.Icon.Default.mergeOptions({
iconUrl: '/ui/images/marker-icon.png',
iconRetinaUrl: '/ui/images/marker-icon-2x.png',
shadowUrl: '/ui/images/marker-shadow.png',
});
class App extends enhance.Base {
// ************************ 第一步:仅声明属性,不即时实例化 ************************
leaflet_map: Leaflet_Map | null = null; // 先赋值为null,延迟实例化
cesium_map: any = null;
setting: Settings | null = null;
aircraft_list: Aircraft_List | null = null;
dsp: DSP | null = null;
data_source_config: Data_Source_Config | null = null;
// @ts-ignore
navigate: NavigateFunction
location: ReturnType<typeof useLocation> | null = null;
selected_key: string = '/map'
version: string = '未加载版本信息';
start_device_time: number;
webgl_supported: boolean = is_cesium_webgl_available();
menuItems: any = []
constructor() {
super();
// ************************ 第二步:在构造函数中延迟实例化(确保依赖已初始化) ************************
this.initSubComponents();
// page 是我自定义的
this.menuItems = [
{
label: '2D地图',
icon: <SettingOutlined/>,
key: '/map',
},
{
label: this.webgl_supported ? '3D地图' : '3D地图(WebGL不可用)',
icon: <SettingOutlined/>,
key: '/map3d',
disabled: !this.webgl_supported,
},
{
label: '设置',
icon: <SettingOutlined/>,
key: '/settings',
},
{
label: '飞机列表',
icon: <SettingOutlined/>,
key: '/aircraftlist',
},
// {
// label: 'DSP',
// icon: <SettingOutlined />,
// key: '/dsp',
// },
];
axios.post(`${baseURL}/version`, {}).then(res => {
this.version = res.data.version
this.flush();
})
axios.post(`${baseURL}/start_device_time`, {}).then(res => {
this.start_device_time = res.data.start_device_time
this.flush();
})
}
// ************************ 新增:子组件初始化方法(统一管理,便于维护) ************************
initSubComponents = () => {
// ************************ 第二步:在构造函数中延迟实例化(确保依赖已初始化) ************************
// 此时所有导入的类已完成初始化,可安全实例化
this.leaflet_map = new Leaflet_Map();
this.aircraft_list = new Aircraft_List();
this.dsp = new DSP();
this.data_source_config = new Data_Source_Config(this.on_data_sources_changed);
// 延迟初始化Settings,避免“初始化前访问”错误
setTimeout(() => {
this.setting = new Settings();
this.flush();
}, 0);
};
on_data_sources_changed = () => {
if (!this.data_source_config) return;
if (this.leaflet_map) {
this.leaflet_map.data_source_config = this.data_source_config;
this.leaflet_map.data_source_show.data_source_config = this.data_source_config;
this.leaflet_map.data_source_show.flush();
this.leaflet_map.flush();
}
this.aircraft_list?.data_sources_changed();
this.cesium_map?.request_sync_data_sources?.();
this.flush();
};
use_hook() {
this.navigate = useNavigate();
this.location = useLocation();
useEffect(() => {
const path = this.location?.pathname || '/map';
this.selected_key = path.startsWith('/ui/') ? path.substring(3) : path;
this.flush()
}, [this.location]);
useEffect(() => {
const on_unavailable = () => {
this.webgl_supported = false;
const item = this.menuItems.find((menu_item: any) => menu_item.key === "/map3d");
if (item) {
item.label = "3D地图(WebGL不可用)";
item.disabled = true;
}
this.flush();
};
window.addEventListener("ecap_cesium_webgl_unavailable", on_unavailable);
return () => window.removeEventListener("ecap_cesium_webgl_unavailable", on_unavailable);
}, []);
};
get_Data_Source(key: string) {
return this.data_source_config?.map(key) ?? null;
}
onClick: MenuProps['onClick'] = (e) => {
if (e.key === "/map3d" && !this.webgl_supported) {
message.warning(webgl_unavailable_message);
this.navigate("/map");
return;
}
this.selected_key = e.key;
this.navigate(this.selected_key); // 跳转到 /about 页面
this.flush();
};
formatTimestamp(ts: number | string): string {
const n = Number(ts);
if (!Number.isFinite(n) || n <= 0) {
return "";
}
// 10 位一般是秒级时间戳,13 位一般是毫秒级时间戳
const ms = n < 1e12 ? n * 1000 : n;
const d = new Date(ms);
const pad = (v: number) => String(v).padStart(2, "0");
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
render(props: any) {
// ************************ 优化:渲染前校验子组件是否初始化完成,避免报错 ************************
if (!this.leaflet_map || !this.setting || !this.aircraft_list || !this.dsp || !this.data_source_config) {
return <div style={{textAlign: 'center', padding: '20px'}}>...</div>;
}
return (
<div style={{
display: 'flex',
flexDirection: 'column',
width: '100vw',
height: '100vh'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
flexShrink: 0,
borderBottom: '1px solid #f0f0f0'
}}>
<Menu
style={{
borderBottom: 0,
flex: '0 0 auto'
}}
selectedKeys={[this.selected_key]}
onClick={this.onClick}
mode="horizontal"
items={this.menuItems.map(item => ({
key: item.key,
icon: item.icon,
label: item.label,
disabled: item.disabled
}))}
/>
<div style={{
marginLeft: 'auto',
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
color: '#666',
fontWeight: 'bold',
padding: '0 20px 0 8px'
}}>
{`服务开启时间:${this.formatTimestamp(this.start_device_time)} 版本:${this.version}`}
</div>
</div>
<Routes>
<Route
path="/map"
element={(
<div style={{width: '100%', height: '100%', position: 'relative'}}>
<this.leaflet_map.x/>
</div>
)}
/>
<Route
path="/ui/map"
element={(
<div style={{width: '100%', height: '100%', position: 'relative'}}>
<this.leaflet_map.x/>
</div>
)}
/>
<Route
path="/settings"
element={(
<this.setting.x/>
)}
/>
<Route
path="/ui/settings"
element={(
<this.setting.x/>
)}
/>
<Route
path="/map3d"
element={(
this.webgl_supported ? <div style={{width: '100%', height: '100%', position: 'relative'}}>
<React.Suspense fallback={<div style={{padding: 20}}>3D地图加载中...</div>}>
<Cesium_Map_View initialSceneMode="3d"/>
</React.Suspense>
</div> : <Navigate to="/map" replace/>
)}
/>
<Route
path="/ui/map3d"
element={(
this.webgl_supported ? <div style={{width: '100%', height: '100%', position: 'relative'}}>
<React.Suspense fallback={<div style={{padding: 20}}>3D地图加载中...</div>}>
<Cesium_Map_View initialSceneMode="3d"/>
</React.Suspense>
</div> : <Navigate to="/ui/map" replace/>
)}
/>
<Route
path="/aircraftlist"
element={(
<div style={{
flex: 1,
}}>
<this.aircraft_list.x/>
</div>
)}
/>
{/*<Route*/}
{/* path="/dsp"*/}
{/* element={(*/}
{/* <div style={{*/}
{/* flex: 1,*/}
{/* }}>*/}
{/* <div style={{ width: '100%', height: '100%', position: 'relative'}}>*/}
{/* <this.dsp.x />*/}
{/* </div>*/}
{/* </div>*/}
{/* )}*/}
{/*/>*/}
<Route path="*" element={<Navigate to="/map"/>}/>
</Routes>
</div>
)
}
}
// ************************ 确认:App实例化在所有导入类完成后 ************************
export let app = new App();
const container = document.getElementById('root');
if (container) {
// 清空容器现有内容(避免热更新重复绑定)
container.innerHTML = '';
const root = ReactDOM.createRoot(container);
root.render(<BrowserRouter>
<app.x/>
</BrowserRouter>);
}