328 lines
12 KiB
TypeScript
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>);
|
|
}
|