缩减代码
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
|
||||
export default [
|
||||
{ ignores: ['dist/**', 'wwwroot/**', 'htdocs/**', 'cesium/**', 'node_modules/**'] },
|
||||
{ ignores: ['dist/**', 'public/**', 'wwwroot/**', 'htdocs/**', 'cesium/**', 'node_modules/**'] },
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx}'],
|
||||
languageOptions: {
|
||||
|
||||
@@ -2,7 +2,7 @@ import {DownloadOutlined, UploadOutlined} from "@ant-design/icons"
|
||||
import {Button, Input, message, Modal, Progress, Select, Space, Upload} from "antd"
|
||||
import axios from "axios"
|
||||
import {useMemo, useState} from "react"
|
||||
export type Archive_Format = {
|
||||
type Archive_Format = {
|
||||
label: string
|
||||
value: string
|
||||
format: string
|
||||
|
||||
@@ -21,6 +21,7 @@ export type Backend_Form_Section = {
|
||||
descriptor_path?: string
|
||||
fields?: string[]
|
||||
layout?: Backend_Field_Layout[]
|
||||
modes?: string[]
|
||||
}
|
||||
|
||||
export type Backend_Field_Layout = {
|
||||
@@ -38,6 +39,22 @@ export type Backend_Disabled_Rule = {
|
||||
}
|
||||
}
|
||||
|
||||
export type Backend_Schema_Node = Backend_Form_Section & {
|
||||
descriptor_fields?: Backend_Field_Descriptor[]
|
||||
rules?: Backend_Disabled_Rule[]
|
||||
sections?: Backend_Schema_Node[]
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export function backend_schema_node(value: unknown, ...path: string[]): Backend_Schema_Node | undefined {
|
||||
let node = value;
|
||||
for (const key of path) {
|
||||
if (!node || typeof node !== "object") return undefined;
|
||||
node = (node as Record<string, unknown>)[key];
|
||||
}
|
||||
return node && typeof node === "object" ? node as Backend_Schema_Node : undefined;
|
||||
}
|
||||
|
||||
type Json_Object = Record<string, any>
|
||||
|
||||
export function get_path(root: Json_Object, path = ""): any {
|
||||
@@ -45,7 +62,7 @@ export function get_path(root: Json_Object, path = ""): any {
|
||||
return path.split(".").reduce((value: any, key) => value?.[key], root);
|
||||
}
|
||||
|
||||
export function fields_at_path(fields: Backend_Field_Descriptor[], path = ""): Backend_Field_Descriptor[] {
|
||||
function fields_at_path(fields: Backend_Field_Descriptor[], path = ""): Backend_Field_Descriptor[] {
|
||||
if (!path) return fields;
|
||||
let current = fields;
|
||||
for (const name of path.split(".")) {
|
||||
|
||||
+32
-98
@@ -5,12 +5,12 @@ import enhance from './core/enhance.tsx';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
import {SettingOutlined} from '@ant-design/icons';
|
||||
import {Menu, MenuProps, message} from 'antd';
|
||||
import {Menu, type MenuProps, message} from 'antd';
|
||||
|
||||
import {
|
||||
BrowserRouter,
|
||||
Navigate,
|
||||
NavigateFunction,
|
||||
type NavigateFunction,
|
||||
Route,
|
||||
Routes,
|
||||
useLocation,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import axios from "axios";
|
||||
import {baseURL} from "./Global.tsx";
|
||||
import {baseURL} from "./Global.ts";
|
||||
import {is_cesium_webgl_available, webgl_unavailable_message} from "./Map/WebGL_Support.ts";
|
||||
import {Data_Source_Config} from "./Data_Source/Data_Source_Config.tsx";
|
||||
|
||||
@@ -37,11 +37,11 @@ L.Icon.Default.mergeOptions({
|
||||
});
|
||||
|
||||
class App extends enhance.Base {
|
||||
leaflet_map: Leaflet_Map | null = null;
|
||||
leaflet_map: Leaflet_Map;
|
||||
cesium_map: any = null;
|
||||
setting: Settings | null = null;
|
||||
aircraft_list: Aircraft_List | null = null;
|
||||
data_source_config: Data_Source_Config | null = null;
|
||||
setting: Settings;
|
||||
aircraft_list: Aircraft_List;
|
||||
data_source_config: Data_Source_Config;
|
||||
|
||||
// @ts-ignore
|
||||
navigate: NavigateFunction
|
||||
@@ -56,7 +56,10 @@ class App extends enhance.Base {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.initSubComponents();
|
||||
this.leaflet_map = new Leaflet_Map();
|
||||
this.aircraft_list = new Aircraft_List();
|
||||
this.setting = new Settings();
|
||||
this.data_source_config = new Data_Source_Config(this.on_data_sources_changed);
|
||||
|
||||
this.menuItems = [
|
||||
{
|
||||
@@ -93,28 +96,12 @@ class App extends enhance.Base {
|
||||
})
|
||||
}
|
||||
|
||||
initSubComponents = () => {
|
||||
this.leaflet_map = new Leaflet_Map();
|
||||
this.aircraft_list = new Aircraft_List();
|
||||
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.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();
|
||||
};
|
||||
@@ -176,18 +163,18 @@ class App extends enhance.Base {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.leaflet_map || !this.setting || !this.aircraft_list || !this.data_source_config) {
|
||||
return <div style={{textAlign: 'center', padding: '20px'}}>组件初始化中...</div>;
|
||||
}
|
||||
|
||||
const map2d = <div style={{width: '100%', height: '100%', position: 'relative'}}><this.leaflet_map.x/></div>;
|
||||
const settings = <this.setting.x/>;
|
||||
const aircraftList = <div style={{flex: 1}}><this.aircraft_list.x/></div>;
|
||||
const map3d = (fallback: string) => 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={fallback} replace/>;
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100vw',
|
||||
height: '100vh'
|
||||
}}>
|
||||
|
||||
<div style={{display: 'flex', flexDirection: 'column', width: '100vw', height: '100vh'}}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -223,64 +210,11 @@ class App extends enhance.Base {
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
{["/map", "/ui/map"].map(path => <Route key={path} path={path} element={map2d}/>)}
|
||||
{["/settings", "/ui/settings"].map(path => <Route key={path} path={path} element={settings}/>)}
|
||||
<Route path="/map3d" element={map3d("/map")}/>
|
||||
<Route path="/ui/map3d" element={map3d("/ui/map")}/>
|
||||
{["/aircraftlist", "/ui/aircraftlist"].map(path => <Route key={path} path={path} element={aircraftList}/>)}
|
||||
<Route path="*" element={<Navigate to="/map"/>}/>
|
||||
</Routes>
|
||||
|
||||
|
||||
+9
-47
@@ -4,13 +4,7 @@ import {Drawer, Switch} from "antd";
|
||||
import {PersistentScroll} from "./PersistentScroll.tsx";
|
||||
|
||||
export class Base_Drawer extends enhance.Base {
|
||||
have_open: boolean = false;
|
||||
|
||||
on_mount() {
|
||||
super.on_mount();
|
||||
}
|
||||
|
||||
|
||||
have_open = false;
|
||||
|
||||
toggleOpen = () => {
|
||||
this.have_open = !this.have_open;
|
||||
@@ -18,55 +12,23 @@ export class Base_Drawer extends enhance.Base {
|
||||
};
|
||||
|
||||
getSwitchStyle(placement: string): React.CSSProperties {
|
||||
const common: React.CSSProperties = {
|
||||
position: "absolute",
|
||||
zIndex: 9999,
|
||||
const common: React.CSSProperties = {position: "absolute", zIndex: 9999};
|
||||
if (placement === "right") return {...common, right: 8, top: 8};
|
||||
if (placement === "top" || placement === "bottom") return {
|
||||
...common,
|
||||
[placement]: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)"
|
||||
};
|
||||
|
||||
|
||||
switch (placement) {
|
||||
case "left":
|
||||
return { ...common, left: 8, top: 8 };
|
||||
|
||||
case "right":
|
||||
return { ...common, right: 8, top: 8 };
|
||||
|
||||
case "top":
|
||||
return {
|
||||
...common,
|
||||
top: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)"
|
||||
};
|
||||
|
||||
case "bottom":
|
||||
return {
|
||||
...common,
|
||||
bottom: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)"
|
||||
};
|
||||
|
||||
default:
|
||||
return { ...common, left: 8, top: 8 };
|
||||
}
|
||||
return {...common, left: 8, top: 8};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
render(props: any): React.JSX.Element {
|
||||
const content = props.children;
|
||||
const placement = props.placement || "right";
|
||||
const header = props.header;
|
||||
const scrollKey = props.scrollKey || "Base_Drawer";
|
||||
|
||||
// 按钮样式自动根据 placement 调整
|
||||
const switchStyle = this.getSwitchStyle(placement);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<Switch
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import enhance from "../core/enhance.tsx";
|
||||
import type {Aircraft} from "../Map/Aircraft.tsx";
|
||||
import type {Base_Station} from "../Map/Base_Station.tsx";
|
||||
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
|
||||
import {Data_Source_Adminive} from "./Data_Source_Adminive.ts";
|
||||
import {Data_Source_Map_Runtime} from "./Data_Source_Map_Runtime.ts";
|
||||
|
||||
@@ -59,15 +58,6 @@ export class Data_Source extends enhance.Base {
|
||||
confirm() { return this.adminive.save(); }
|
||||
refresh_display(mode: Map_Display_Mode) { this.map_runtime.refresh_display(mode); }
|
||||
locate_base_station() { this.map_runtime.locate_base_station(); }
|
||||
center_earth_view() { this.map_runtime.center_earth_view(); }
|
||||
look_straight_down_view() { this.map_runtime.look_straight_down_view(); }
|
||||
north_up_view() { this.map_runtime.north_up_view(); }
|
||||
clear_camera_tracking() { this.map_runtime.clear_camera_tracking(); }
|
||||
restore_previous_camera_view() { this.map_runtime.restore_previous_camera_view(); }
|
||||
fly_to_global_view() { this.map_runtime.fly_to_global_view(); }
|
||||
cesium_camera_control_mode(): Camera_Control_Mode { return this.map_runtime.cesium_camera_control_mode(); }
|
||||
set_cesium_camera_control_mode(mode: Camera_Control_Mode) { this.map_runtime.set_cesium_camera_control_mode(mode); }
|
||||
|
||||
refresh() { return this.map_runtime.refresh(); }
|
||||
manual_track_icao_list(): string[] { return this.map_runtime.manual_track_icao_list(); }
|
||||
is_manual_tracking_aircraft(icao: string): boolean { return this.map_runtime.is_manual_tracking_aircraft(icao); }
|
||||
@@ -85,6 +75,5 @@ export class Data_Source extends enhance.Base {
|
||||
remove_aircraft(icao: string) { this.map_runtime.remove_aircraft(icao); }
|
||||
refresh_aircraft_range() { this.map_runtime.refresh_aircraft_range(); }
|
||||
apply_aircraft_delta(change_list: any[], removed_icaos: string[]) { this.map_runtime.apply_aircraft_delta(change_list, removed_icaos); }
|
||||
apply_aircraft_snapshot(items: any[]) { this.map_runtime.apply_aircraft_snapshot(items); }
|
||||
apply_track_stream_list(list: any[]) { this.map_runtime.apply_track_stream_list(list); }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {app} from "../App.tsx";
|
||||
import {server_url} from "../Global.tsx";
|
||||
import {server_url} from "../Global.ts";
|
||||
import type {Aircraft} from "../Map/Aircraft.tsx";
|
||||
import type {Data_Source} from "./Data_Source.tsx";
|
||||
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import axios from "axios"
|
||||
import {Data_Source} from "./Data_Source.tsx"
|
||||
import type {
|
||||
Backend_Disabled_Rule,
|
||||
Backend_Field_Descriptor,
|
||||
Backend_Form_Section
|
||||
} from "../Adminive/Backend_Fields.tsx"
|
||||
import type {Backend_Field_Descriptor, Backend_Schema_Node} from "../Adminive/Backend_Fields.tsx"
|
||||
|
||||
type Adminive_Data_Source_Row = {
|
||||
id: number
|
||||
@@ -15,26 +11,10 @@ type Adminive_Data_Source_Row = {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type Data_Source_Sidebar_Schema = {
|
||||
title?: string
|
||||
common: Backend_Form_Section
|
||||
position: Backend_Form_Section
|
||||
display: Record<"map2d" | "map3d", Backend_Form_Section>
|
||||
rules?: Backend_Disabled_Rule[]
|
||||
tiles: Backend_Form_Section & {descriptor_fields: Backend_Field_Descriptor[]}
|
||||
view3d: {
|
||||
graphics: Backend_Form_Section & {descriptor_fields: Backend_Field_Descriptor[]}
|
||||
map_view: Backend_Form_Section & {descriptor_fields: Backend_Field_Descriptor[]}
|
||||
}
|
||||
}
|
||||
|
||||
export class Data_Source_Config {
|
||||
private sources: Data_Source[] = []
|
||||
descriptor_fields: Backend_Field_Descriptor[] = []
|
||||
config_descriptor_fields: Backend_Field_Descriptor[] = []
|
||||
sidebar_schema: Data_Source_Sidebar_Schema | null = null
|
||||
loading = true
|
||||
error = ""
|
||||
sidebar_schema: Backend_Schema_Node | null = null
|
||||
|
||||
constructor(private readonly on_change?: () => void) {
|
||||
window.addEventListener("ecap-data-sources-changed", () => void this.refresh())
|
||||
@@ -67,7 +47,6 @@ export class Data_Source_Config {
|
||||
if (!descriptor || !Array.isArray(descriptor.fields) || !descriptor.sidebar) {
|
||||
throw new Error("数据源接口未返回后端字段与右侧栏描述")
|
||||
}
|
||||
this.descriptor_fields = descriptor.fields
|
||||
this.config_descriptor_fields = descriptor.fields.find((field: Backend_Field_Descriptor) => field.name === "config")?.children || []
|
||||
this.sidebar_schema = descriptor.sidebar
|
||||
const editable_fields = descriptor.fields
|
||||
@@ -88,12 +67,9 @@ export class Data_Source_Config {
|
||||
source.normalize_map_display()
|
||||
return source
|
||||
})
|
||||
this.error = ""
|
||||
} catch (error) {
|
||||
this.error = error instanceof Error ? error.message : String(error)
|
||||
console.error("加载后端数据源模型失败", error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.on_change?.()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,9 @@ import axios from "axios";
|
||||
import {message} from "antd";
|
||||
import L from "leaflet";
|
||||
import {app} from "../App.tsx";
|
||||
import {baseURL, darkenColor} from "../Global.tsx";
|
||||
import {baseURL, darkenColor} from "../Global.ts";
|
||||
import {Aircraft} from "../Map/Aircraft.tsx";
|
||||
import {Base_Station} from "../Map/Base_Station.tsx";
|
||||
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
|
||||
import type {Data_Source, Map_Display_Mode} from "./Data_Source.tsx";
|
||||
import {data_source_aircraft_stream} from "./Data_Source_Aircraft_Stream.ts";
|
||||
|
||||
@@ -44,17 +43,6 @@ export class Data_Source_Map_Runtime {
|
||||
app.leaflet_map?.map?.setView([this.source.lat, this.source.lon], 12);
|
||||
}
|
||||
|
||||
center_earth_view() { app.cesium_map?.center_earth(); }
|
||||
look_straight_down_view() { app.cesium_map?.look_straight_down(); }
|
||||
north_up_view() { app.cesium_map?.north_up(); }
|
||||
clear_camera_tracking() { app.cesium_map?.clear_camera_tracking(); }
|
||||
restore_previous_camera_view() { app.cesium_map?.restore_previous_view(); }
|
||||
fly_to_global_view() { app.cesium_map?.fly_to_global_view(); }
|
||||
cesium_camera_control_mode(): Camera_Control_Mode {
|
||||
return app.cesium_map?.camera_control_mode() || Camera_Control_Mode.Surface_Navigation;
|
||||
}
|
||||
set_cesium_camera_control_mode(mode: Camera_Control_Mode) { app.cesium_map?.set_camera_control_mode(mode); }
|
||||
|
||||
async refresh() {
|
||||
if (this.source.key) await this.refresh_base_station_location();
|
||||
data_source_aircraft_stream.ensure_open();
|
||||
@@ -222,14 +210,6 @@ export class Data_Source_Map_Runtime {
|
||||
this.refresh_aircraft_range();
|
||||
}
|
||||
|
||||
apply_aircraft_snapshot(items: any[]) {
|
||||
const alive = new Set(items.map(item => item.icao));
|
||||
for (const icao of Array.from(this.aircraft_map.keys())) {
|
||||
if (!alive.has(icao)) this.remove_aircraft(icao);
|
||||
}
|
||||
this.apply_aircraft_items(items);
|
||||
}
|
||||
|
||||
apply_track_stream_list(list: any[]) {
|
||||
for (const item of list) {
|
||||
const aircraft = this.aircraft_map.get(item.icao);
|
||||
|
||||
@@ -16,9 +16,9 @@ import {Data_Source, type Map_Display_Mode} from "./Data_Source.tsx";
|
||||
import {Base_Drawer} from "../Base_Drawer.tsx";
|
||||
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
|
||||
import {app} from "../App.tsx";
|
||||
import {lightenColor} from "../Global.tsx";
|
||||
import {lightenColor} from "../Global.ts";
|
||||
import {tile_source_options, type Map_Tile_Type} from "../Map/Map_Resources.tsx";
|
||||
import {Backend_Fields} from "../Adminive/Backend_Fields.tsx";
|
||||
import {backend_schema_node, Backend_Fields} from "../Adminive/Backend_Fields.tsx";
|
||||
|
||||
const {Title, Text} = Typography;
|
||||
|
||||
@@ -239,7 +239,7 @@ export class Data_Source_Show extends enhance.Base {
|
||||
] : [{value: "imagery", label: "影像瓦片"}];
|
||||
const source_options = tile_source_options(map?.map_resources || null, tile_type);
|
||||
const source_key = this.selected_tile_source_key(map, tile_type, source_options);
|
||||
const tile_schema = this.data_source_config?.sidebar_schema?.tiles;
|
||||
const tile_schema = backend_schema_node(this.data_source_config?.sidebar_schema, "tiles");
|
||||
return (
|
||||
<div style={{marginTop: 12, paddingTop: 8, borderTop: "1px solid #eee"}}>
|
||||
<Title level={5}>{tile_schema?.title ?? "瓦片显示"}</Title>
|
||||
@@ -258,7 +258,7 @@ export class Data_Source_Show extends enhance.Base {
|
||||
options={source_options}
|
||||
disabled={!source_options.length}
|
||||
onChange={(key: string) => this.change_tile_source(map, tile_type, key)}/>
|
||||
{tile_type === "imagery" && tile_schema &&
|
||||
{tile_type === "imagery" && tile_schema?.descriptor_fields &&
|
||||
<Backend_Fields root={tile_view || {}} descriptor_fields={tile_schema.descriptor_fields}
|
||||
section={{...tile_schema, descriptor_path: ""}}
|
||||
onChange={(_path, value) => {
|
||||
@@ -324,15 +324,16 @@ export class Data_Source_Show extends enhance.Base {
|
||||
const mode = map?.camera_control_mode() || Camera_Control_Mode.Surface_Navigation;
|
||||
const graphics_config = map?.graphics_config || {};
|
||||
const map_view_config = map?.map_view_config || {};
|
||||
const schema = this.data_source_config?.sidebar_schema?.view3d;
|
||||
const schema = backend_schema_node(this.data_source_config?.sidebar_schema, "view3d");
|
||||
const graphics_schema = backend_schema_node(schema, "graphics");
|
||||
const map_view_schema = backend_schema_node(schema, "map_view");
|
||||
const group_style: React.CSSProperties = {
|
||||
marginTop: 8
|
||||
};
|
||||
const grid_style: React.CSSProperties = {
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(86px, 1fr))",
|
||||
gap: 6,
|
||||
maxWidth: 320
|
||||
gap: 6
|
||||
};
|
||||
return (
|
||||
<div style={{marginTop: 12, paddingTop: 8, borderTop: "1px solid #eee"}}>
|
||||
@@ -371,20 +372,20 @@ export class Data_Source_Show extends enhance.Base {
|
||||
</div>
|
||||
</div>
|
||||
<Space direction="vertical" size={8} style={{marginTop: 8, width: "100%"}}>
|
||||
{schema && <>
|
||||
<Backend_Fields root={graphics_config} descriptor_fields={schema.graphics.descriptor_fields}
|
||||
section={{...schema.graphics, descriptor_path: ""}}
|
||||
{graphics_schema?.descriptor_fields ?
|
||||
<Backend_Fields root={graphics_config} descriptor_fields={graphics_schema.descriptor_fields}
|
||||
section={{...graphics_schema, descriptor_path: ""}}
|
||||
onChange={() => {
|
||||
map?.apply_graphics_config();
|
||||
this.flush();
|
||||
}}/>
|
||||
<Backend_Fields root={map_view_config} descriptor_fields={schema.map_view.descriptor_fields}
|
||||
section={{...schema.map_view, descriptor_path: ""}}
|
||||
}}/> : null}
|
||||
{map_view_schema?.descriptor_fields ?
|
||||
<Backend_Fields root={map_view_config} descriptor_fields={map_view_schema.descriptor_fields}
|
||||
section={{...map_view_schema, descriptor_path: ""}}
|
||||
onChange={() => {
|
||||
if (map) map.map_view_config = map_view_config;
|
||||
this.flush();
|
||||
}}/>
|
||||
</>}
|
||||
}}/> : null}
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => map?.save_current_map_view()}>保存视图设置</Button>
|
||||
<Button size="small"
|
||||
@@ -397,7 +398,7 @@ export class Data_Source_Show extends enhance.Base {
|
||||
|
||||
render_data_source(ds: Data_Source, mode: Map_Display_Mode) {
|
||||
const config = this.data_source_config;
|
||||
const schema = config?.sidebar_schema;
|
||||
const schema = backend_schema_node(config?.sidebar_schema, "source");
|
||||
if (!config || !schema) {
|
||||
return <Text type="secondary">正在加载后端字段描述…</Text>;
|
||||
}
|
||||
@@ -408,17 +409,12 @@ export class Data_Source_Show extends enhance.Base {
|
||||
return (
|
||||
<div>
|
||||
<Title level={5}>{schema.title ?? "数据源配置"}:{ds.key}</Title>
|
||||
<Backend_Fields root={ds as any} descriptor_fields={config.config_descriptor_fields}
|
||||
section={schema.common} rules={schema.rules} onChange={refresh}/>
|
||||
<div style={{marginTop: 12}}>
|
||||
<Backend_Fields root={ds as any} descriptor_fields={config.config_descriptor_fields}
|
||||
section={schema.position} rules={schema.rules} onChange={refresh}/>
|
||||
</div>
|
||||
<div style={{marginTop: 18, paddingTop: 12, borderTop: "1px solid #eee"}}>
|
||||
<Title level={5}>{schema.display[mode].title}</Title>
|
||||
<Backend_Fields root={ds as any} descriptor_fields={config.config_descriptor_fields}
|
||||
section={schema.display[mode]} rules={schema.rules} onChange={refresh}/>
|
||||
</div>
|
||||
{(schema.sections || []).filter(section => !section.modes || section.modes.includes(mode)).map((section, index) =>
|
||||
<div key={index} style={index ? {marginTop: 12} : undefined}>
|
||||
{section.title && <Title level={5}>{section.title}</Title>}
|
||||
<Backend_Fields root={ds as any} descriptor_fields={config.config_descriptor_fields}
|
||||
section={section} rules={schema.rules} onChange={refresh}/>
|
||||
</div>)}
|
||||
{this.render_aircraft_monitor_config(ds, mode)}
|
||||
<br/>
|
||||
<Button type="primary" autoInsertSpace onClick={() => {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
function transform_color(hex_color: string, transform: (channel: number) => number): string {
|
||||
const hex = hex_color.replace("#", "");
|
||||
return `#${[0, 2, 4].map(offset => Math.max(0, Math.min(255,
|
||||
Math.floor(transform(parseInt(hex.slice(offset, offset + 2), 16))))).toString(16).padStart(2, "0")).join("")}`;
|
||||
}
|
||||
|
||||
export function lightenColor(color: string, factor = 0.2): string {
|
||||
return transform_color(color, channel => channel + (255 - channel) * factor);
|
||||
}
|
||||
|
||||
export function darkenColor(color: string, factor = 0.2): string {
|
||||
return transform_color(color, channel => channel * (1 - factor));
|
||||
}
|
||||
|
||||
export type Tree_Node = {title: string, key: string, children: Tree_Node[] | null};
|
||||
|
||||
export function transformToTreeData(data: Record<string, any>, parent_key = ""): Tree_Node[] {
|
||||
return Object.entries(data).map(([key, value], index) => {
|
||||
const node_key = parent_key ? `${parent_key}-${index}` : String(index);
|
||||
const suffix = value === null ? ": null" : typeof value === "object" ? "" : `: ${value}`;
|
||||
return {
|
||||
title: `${key}${suffix}`,
|
||||
key: node_key,
|
||||
children: value && typeof value === "object" ? transformToTreeData(value, node_key) : null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export class StateHolder<T> {
|
||||
constructor(public cur: T, private readonly hook: (old_value: T, new_value: T) => void = () => {}) {}
|
||||
set(next: T) {
|
||||
if (next === this.cur) return;
|
||||
const old = this.cur;
|
||||
this.cur = next;
|
||||
this.hook(old, next);
|
||||
}
|
||||
}
|
||||
|
||||
export const server_url = window.location.host;
|
||||
export const baseURL = "/api";
|
||||
-181
@@ -1,181 +0,0 @@
|
||||
import type {CSSProperties} from "react";
|
||||
|
||||
export function lightenColor(hexColor: string, factor: number = 0.2): string {
|
||||
// 去掉 #
|
||||
const hex = hexColor.replace('#', '');
|
||||
|
||||
// 解析 RGB
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
|
||||
// 提高每个分量,最多不超过 255
|
||||
const newR = Math.min(255, Math.floor(r + (255 - r) * factor));
|
||||
const newG = Math.min(255, Math.floor(g + (255 - g) * factor));
|
||||
const newB = Math.min(255, Math.floor(b + (255 - b) * factor));
|
||||
|
||||
// 转回 hex
|
||||
const toHex = (n: number) => n.toString(16).padStart(2, '0');
|
||||
return `#${toHex(newR)}${toHex(newG)}${toHex(newB)}`;
|
||||
}
|
||||
|
||||
export function darkenColor(hexColor: string, factor: number = 0.2): string {
|
||||
const hex = hexColor.replace('#', '');
|
||||
|
||||
const r = parseInt(hex.substring(0, 2), 16);
|
||||
const g = parseInt(hex.substring(2, 4), 16);
|
||||
const b = parseInt(hex.substring(4, 6), 16);
|
||||
|
||||
const newR = Math.max(0, Math.floor(r * (1 - factor)));
|
||||
const newG = Math.max(0, Math.floor(g * (1 - factor)));
|
||||
const newB = Math.max(0, Math.floor(b * (1 - factor)));
|
||||
|
||||
const toHex = (n: number) => n.toString(16).padStart(2, '0');
|
||||
return `#${toHex(newR)}${toHex(newG)}${toHex(newB)}`;
|
||||
}
|
||||
|
||||
export namespace G {
|
||||
|
||||
|
||||
export function to_popup_text(info: any) {
|
||||
let result = "";
|
||||
for (let key in info) {
|
||||
if (info.hasOwnProperty(key)) {
|
||||
result += `${key}: ${JSON.stringify(info[key])}<br>`; // 使用 <br> 来换行
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export const transformToTreeData = (data: Record<string, any>, parentKey = '') => {
|
||||
return Object.entries(data).map(([key, value], index) => {
|
||||
const nodeKey = parentKey ? `${parentKey}-${index}` : `${index}`;
|
||||
let val: string = ""
|
||||
if (value === null) {
|
||||
val = ": null"; // 如果值是 null,显示 ": null"
|
||||
} else if (typeof value !== 'object' && !Array.isArray(value)) {
|
||||
val = `: ${value}`; // 其他情况显示值
|
||||
}
|
||||
|
||||
// 判断值是否为 object 或 array 类型,如果是则不显示值
|
||||
const node = {
|
||||
title: `${key}${val}`,
|
||||
key: nodeKey,
|
||||
children: null
|
||||
};
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
node.children = transformToTreeData(value, nodeKey); // 递归处理子对象
|
||||
}
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export class StateHolder<T> {
|
||||
old: T;
|
||||
cur: T;
|
||||
is_changed: boolean = true;
|
||||
hook: (oldValue: T, newValue: T) => void; // 钩子函数
|
||||
|
||||
// 构造函数:可以接受一个初始值和一个可选的钩子函数
|
||||
constructor(initial: T, hook?: (oldValue: T, newValue: T) => void) {
|
||||
this.old = initial;
|
||||
this.cur = initial;
|
||||
this.hook = hook || (() => {}); // 如果没有提供钩子函数,默认使用空函数
|
||||
}
|
||||
|
||||
// 更新 cur 值,并触发钩子
|
||||
set(next: T) {
|
||||
if (next !== this.cur) {
|
||||
this.old = this.cur;
|
||||
this.cur = next;
|
||||
|
||||
// 调用钩子函数,传入旧值和新值
|
||||
this.hook(this.old, this.cur);
|
||||
this.is_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 判断当前值是否发生变化
|
||||
isChanged(): boolean {
|
||||
if(this.is_changed) return true;
|
||||
return this.old !== this.cur;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const Prefix = (props) => {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: 'auto',
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: 14,
|
||||
color: '#000',
|
||||
fontWeight: 'bold'
|
||||
}}
|
||||
>
|
||||
{props.label + "\u00A0\u00A0\u00A0\u00A0"}
|
||||
|
||||
|
||||
</div>);
|
||||
}
|
||||
|
||||
|
||||
export const setting_style: CSSProperties = {
|
||||
minWidth: "100%",
|
||||
width: "max-content",
|
||||
height: "max-content",
|
||||
overflowX: "auto",
|
||||
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
// background: "red",
|
||||
alignSelf: 'center'
|
||||
};
|
||||
|
||||
export const row_style: CSSProperties = {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
width: "max-content", // row 大于父 -> 撑开,触发外层滚动
|
||||
flexGrow:1,
|
||||
alignSelf: 'center'
|
||||
};
|
||||
|
||||
|
||||
export const col_style: CSSProperties = {
|
||||
display: 'flex', // 关键:尺寸由子元素撑开
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center', // 水平居中(column 时是交叉轴)
|
||||
gap: 8,
|
||||
width: 'max-content', // 可选:更明确
|
||||
height: 'max-content', // 可选
|
||||
// backgroundColor: 'blue',
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
//baseURL: 'http://127.0.0.1:80',
|
||||
//baseURL: ':80',
|
||||
//baseURL: `http://${window.location.hostname}:${window.location.port}/api`,
|
||||
|
||||
export const server_url = `${window.location.hostname}:${window.location.port}`;
|
||||
export const baseURL = `http://${server_url}/api`;
|
||||
export const host = window.location.port ?
|
||||
`${window.location.hostname}:${window.location.port}` :
|
||||
`${window.location.hostname}:80`;
|
||||
|
||||
|
||||
+38
-266
@@ -1,163 +1,69 @@
|
||||
import "leaflet-rotatedmarker"
|
||||
import L, {CircleMarker, LatLngExpression, LeafletMouseEvent, Marker, MarkerOptions} from 'leaflet';
|
||||
import {app} from "../App.tsx"
|
||||
import {baseURL, StateHolder} from "../Global.js";
|
||||
import {lightenColor} from "../Global.tsx";
|
||||
import {baseURL, lightenColor, StateHolder} from "../Global.ts";
|
||||
import axios from "axios";
|
||||
import {Data_Source} from "../Data_Source/Data_Source.tsx";
|
||||
import {Aircraft_Model, Aircraft_Status, get_aircraft_angle, orientation_from_item} from "./Aircraft_Model.tsx";
|
||||
|
||||
// https://leafletjs.cn/reference.html#marker
|
||||
// https://leafletjs.cn/reference.html#circlemarker
|
||||
// https://leafletjs.cn/reference.html#popup
|
||||
// https://leafletjs.cn/reference.html#domutil
|
||||
// https://zhuanlan.zhihu.com/p/35713983
|
||||
// https://github.com/bbecquet/Leaflet.RotatedMarker
|
||||
|
||||
class Aircraft_Marker extends Marker {
|
||||
|
||||
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
|
||||
super(latlng, options);
|
||||
// @ts-ignore
|
||||
this.setRotationOrigin('center center')
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
let pos = this.getLatLng();
|
||||
return {
|
||||
lat: pos.lat,
|
||||
lon: pos.lng,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
export class Aircraft_Track_Point extends CircleMarker {
|
||||
private tooltipVisible: boolean = false; // 用来追踪提示框是否通过右键点击显示
|
||||
private rightClickVisible: boolean = false; // 用来追踪右键点击时提示框是否显示
|
||||
utc : number
|
||||
|
||||
rest
|
||||
class Aircraft_Track_Point extends CircleMarker {
|
||||
private pinned = false;
|
||||
readonly utc: number;
|
||||
readonly rest: Record<string, unknown>;
|
||||
|
||||
constructor(data) {
|
||||
super(L.latLng(data.lat, data.lon, data.alt), {
|
||||
constructor(data: Record<string, any>) {
|
||||
const {lat, lon, alt, utc, ...rest} = data;
|
||||
super(L.latLng(lat, lon, alt), {
|
||||
radius: 8,
|
||||
fillColor: 'red',
|
||||
fillOpacity: 1,
|
||||
color: 'none', // 去掉边框
|
||||
color: 'none',
|
||||
});
|
||||
this.utc = data.utc
|
||||
|
||||
|
||||
const rest = {...data};
|
||||
for (const key of ["lat", "lon", "alt"]) delete rest[key];
|
||||
this.utc = Number(utc);
|
||||
this.rest = rest;
|
||||
|
||||
|
||||
this.on('click', function (e : LeafletMouseEvent) {
|
||||
// 阻止事件冒泡
|
||||
e.originalEvent.preventDefault();
|
||||
e.originalEvent.stopPropagation();
|
||||
const latlng = e.latlng; // 获取点击的坐标
|
||||
const alt = latlng.alt || "未知"; // 获取高度(如果有的话),默认值为 "未知"
|
||||
const extraHtml = Object.entries(rest)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join('<br>');
|
||||
const tooltipContent = `
|
||||
<div>
|
||||
纬度: ${latlng.lat.toFixed(6)}<br>
|
||||
经度: ${latlng.lng.toFixed(6)}<br>
|
||||
高度: ${alt}<br>
|
||||
${extraHtml ? extraHtml + '<br>' : ''}
|
||||
</div>
|
||||
`
|
||||
// 如果提示框已显示,则隐藏提示框
|
||||
if (this.rightClickVisible) {
|
||||
this.on('click', (event: LeafletMouseEvent) => {
|
||||
event.originalEvent.preventDefault();
|
||||
event.originalEvent.stopPropagation();
|
||||
if (this.pinned) {
|
||||
this.closeTooltip();
|
||||
this.rightClickVisible = false;
|
||||
this.pinned = false;
|
||||
} else {
|
||||
// 如果提示框未显示,则显示提示框
|
||||
this.bindTooltip(tooltipContent)
|
||||
.openTooltip();
|
||||
this.rightClickVisible = true;
|
||||
this.show_tooltip(event.latlng, false);
|
||||
this.pinned = true;
|
||||
}
|
||||
});
|
||||
// 鼠标移入显示信息(此功能可根据需要保留)
|
||||
this.on('mouseover', function (e) {
|
||||
if (this.rightClickVisible) return; // 如果是右键点击显示的提示框,则不再显示
|
||||
|
||||
|
||||
const latlng = e.latlng;
|
||||
const alt = latlng.alt || "未知";
|
||||
|
||||
|
||||
let date = new Date(this.utc * 1000);
|
||||
let hours = date.getUTCHours();
|
||||
let minutes = date.getUTCMinutes();
|
||||
let seconds = date.getUTCSeconds();
|
||||
|
||||
let timeString = `${hours}:${minutes}:${seconds}`;
|
||||
// 奇延迟: ${oddLatencyFormatted}<br>
|
||||
// 偶延迟: ${evenLatencyFormatted}<br>
|
||||
|
||||
|
||||
const extraHtml = Object.entries(rest)
|
||||
.map(([k, v]) => `${k}: ${v}`)
|
||||
.join('<br>');
|
||||
|
||||
|
||||
|
||||
|
||||
const tooltipContent = `
|
||||
<div>
|
||||
纬度: ${latlng.lat.toFixed(6)}<br>
|
||||
经度: ${latlng.lng.toFixed(6)}<br>
|
||||
高度: ${alt} <br>
|
||||
utc: ${timeString}<br>
|
||||
|
||||
${extraHtml ? extraHtml + '<br>' : ''}
|
||||
</div>
|
||||
|
||||
`;
|
||||
// 如果提示框没有显示,则显示
|
||||
if (!this.tooltipVisible) {
|
||||
this.bindTooltip(tooltipContent)
|
||||
.openTooltip();
|
||||
this.tooltipVisible = true;
|
||||
}
|
||||
this.on('mouseover', (event: LeafletMouseEvent) => {
|
||||
if (!this.pinned) this.show_tooltip(event.latlng, true);
|
||||
});
|
||||
|
||||
// 鼠标离开时隐藏信息(此功能也可以根据需要保留)
|
||||
this.on('mouseout', function () {
|
||||
if (this.rightClickVisible) return; // 如果是右键点击显示的提示框,则不关闭
|
||||
if (!this.tooltipVisible) return;
|
||||
this.closeTooltip();
|
||||
this.tooltipVisible = false;
|
||||
this.on('mouseout', () => {
|
||||
if (!this.pinned) this.closeTooltip();
|
||||
});
|
||||
}
|
||||
|
||||
setColor(color: string) {
|
||||
this.setStyle({
|
||||
fillColor: color
|
||||
});
|
||||
}
|
||||
setSize(size: number) {
|
||||
this.setRadius(size);
|
||||
private show_tooltip(latlng: L.LatLng, include_time: boolean) {
|
||||
const lines = [
|
||||
`纬度: ${latlng.lat.toFixed(6)}`,
|
||||
`经度: ${latlng.lng.toFixed(6)}`,
|
||||
`高度: ${latlng.alt ?? "未知"}`,
|
||||
...(include_time ? [`UTC: ${Number.isFinite(this.utc) ? new Date(this.utc * 1000).toISOString().slice(11, 19) : "未知"}`] : []),
|
||||
...Object.entries(this.rest).map(([key, value]) => `${key}: ${String(value)}`)
|
||||
];
|
||||
const content = document.createElement("div");
|
||||
content.innerText = lines.join("\n");
|
||||
content.style.whiteSpace = "pre-line";
|
||||
this.bindTooltip(content).openTooltip();
|
||||
}
|
||||
|
||||
|
||||
toJSON() {
|
||||
let pos = this.getLatLng();
|
||||
return {
|
||||
lat: pos.lat,
|
||||
lon: pos.lng,
|
||||
alt: pos.alt,
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(json: any) {
|
||||
let ret = new Aircraft_Track_Point([json.lat, json.lon, json.alt]);
|
||||
return ret;
|
||||
}
|
||||
setColor(color: string) { this.setStyle({fillColor: color}); }
|
||||
setSize(size: number) { this.setRadius(size); }
|
||||
}
|
||||
|
||||
export class Aircraft {
|
||||
@@ -174,12 +80,7 @@ export class Aircraft {
|
||||
angle = new StateHolder<number>(0);
|
||||
|
||||
|
||||
need_refresh(){
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
generateSVG(color: string, size: number, base_angle : number, s:Aircraft_Status) {
|
||||
generateSVG(color: string, size: number, base_angle: number) {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
style="transform: rotate(${base_angle}deg); transform-origin: 50% 50%;
|
||||
@@ -190,66 +91,15 @@ export class Aircraft {
|
||||
<path d="M280-80v-100l120-84v-144L80-280v-120l320-224v-176q0-33 23.5-56.5T480-880q33 0 56.5 23.5T560-800v176l320 224v120L560-408v144l120 84v100l-200-60-200 60Z"/>
|
||||
</svg>
|
||||
`.trim();
|
||||
|
||||
if (s == Aircraft_Status.Level) {
|
||||
return `
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
style="transform: rotate(${base_angle}deg); transform-origin: 50% 50%;
|
||||
height="${size}"
|
||||
viewBox="0 -960 960 960"
|
||||
width="${size}"
|
||||
fill="${color}">
|
||||
<path d="M280-80v-100l120-84v-144L80-280v-120l320-224v-176q0-33 23.5-56.5T480-880q33 0 56.5 23.5T560-800v176l320 224v120L560-408v144l120 84v100l-200-60-200 60Z"/>
|
||||
</svg>
|
||||
`.trim();
|
||||
}
|
||||
else if (s == Aircraft_Status.Descending) {
|
||||
return `
|
||||
<svg viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}"
|
||||
style="transform: rotate(${base_angle-90}deg); transform-origin: 50% 50%;"
|
||||
>
|
||||
<path
|
||||
d="M277.333333 430.933333l209.066667 55.466667L375.466667 298.666667l76.8-42.666667 153.6 264.533333 209.066666 55.466667-21.333333 85.333333L170.666667 494.933333v-4.266666l34.133333-119.466667 85.333333 21.333333-12.8 38.4zM810.666667 806.4H170.666667v-85.333333h640v85.333333z"
|
||||
fill="${color}"></path>
|
||||
</svg>
|
||||
`.trim();
|
||||
|
||||
} else if (s == Aircraft_Status.Ascending) {
|
||||
return `
|
||||
<svg
|
||||
viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}">
|
||||
style="transform: rotate(${base_angle-90}deg); transform-origin: 50% 50%;"
|
||||
<path
|
||||
d="M307.2 490.666667l204.8-55.466667-183.466667-106.666667 42.666667-72.533333 256 149.333333 209.066667-55.466666 17.066666 76.8-238.933333 64-366.933333 98.133333-21.333334-76.8-12.8-42.666667 81.066667-21.333333 12.8 42.666667zM840.533333 768H213.333333v-81.066667h627.2V768z"
|
||||
fill="${color}"></path>
|
||||
</svg>
|
||||
`.trim();
|
||||
} else if (s == Aircraft_Status.Lost) {
|
||||
return `
|
||||
<svg viewBox="0 0 1024 1024"
|
||||
xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}"
|
||||
style="transform: rotate(${base_angle}); transform-origin: 50% 50%;"
|
||||
>
|
||||
<path
|
||||
d="M582.544817 579.225505l-154.606497-154.605281-0.660457 0.440304-182.677737-182.678953-44.784568 44.785784 173.72204 173.72204L148.713471 610.777826v66.037169l297.22625-99.091634v184.978997l-99.067308 79.26455v66.037169L512.001216 841.966908l165.127587 66.037169V841.966908l-99.090418-79.26455v-97.312172l150.737412 150.738629 44.787001-44.784568L580.893067 578.674516zM578.038385 412.618884V181.452912s0-66.037169-66.035952-66.037169c-66.061495 0-66.061495 66.037169-66.061495 66.037169v171.599577L716.903386 624.016154l158.385576 52.798841v-66.037169L578.038385 412.618884z"
|
||||
></path>
|
||||
</svg>
|
||||
`.trim();
|
||||
}
|
||||
}
|
||||
|
||||
set_angle = (angle: number) => {
|
||||
this.angle.set(angle);
|
||||
// @ts-ignore
|
||||
// this.pos_marker.setRotationAngle(this.angle);
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
|
||||
get_label_position(size: number): string {
|
||||
// 角度区间,判断标签应该放在的位置
|
||||
const halfSize = size / 2;
|
||||
|
||||
return `top: ${halfSize + 8}px; left: ${halfSize + 8}px;`;
|
||||
@@ -259,9 +109,8 @@ export class Aircraft {
|
||||
let style = this.data_source.map2d_style();
|
||||
let color = style.color;
|
||||
let size = app.leaflet_map?.aircraft_icon_size(this) || 50;
|
||||
let status = this.status.cur;
|
||||
let rotationDegree = this.angle.cur;
|
||||
const svg = this.generateSVG(color, size, rotationDegree, status);
|
||||
const svg = this.generateSVG(color, size, rotationDegree);
|
||||
const selected = this.data_source.active_aircraft === this;
|
||||
const outline = selected ? "filter: drop-shadow(1px 0 0 #111) drop-shadow(-1px 0 0 #111) drop-shadow(0 1px 0 #111) drop-shadow(0 -1px 0 #111) drop-shadow(0 0 4px #fff);" : "";
|
||||
|
||||
@@ -302,7 +151,6 @@ export class Aircraft {
|
||||
last_size: number = 0;
|
||||
|
||||
is_show_path: boolean = false;
|
||||
mlat_show: boolean = false
|
||||
pos
|
||||
|
||||
|
||||
@@ -314,32 +162,7 @@ export class Aircraft {
|
||||
this.pos_marker.addTo(map);
|
||||
}
|
||||
|
||||
on_un_mount() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
show_track(){
|
||||
//app.
|
||||
if (this.mlat_show) {
|
||||
this.track_point_list.forEach(item => {
|
||||
item.addTo(app.leaflet_map.map)
|
||||
})
|
||||
return
|
||||
}
|
||||
let active_aircraft = this.data_source.active_aircraft;
|
||||
if (active_aircraft != this) {
|
||||
app.leaflet_map.aircraft_click(this);
|
||||
}
|
||||
}
|
||||
|
||||
hide_track(){
|
||||
if (this.mlat_show) {
|
||||
this.track_point_list.forEach(item => {
|
||||
item.remove()
|
||||
})
|
||||
return
|
||||
}
|
||||
let active_aircraft = this.data_source.active_aircraft;
|
||||
if (active_aircraft == this) {
|
||||
active_aircraft.hide_path();
|
||||
@@ -352,14 +175,7 @@ export class Aircraft {
|
||||
constructor(icao: string, ds: Data_Source) {
|
||||
this.icao = icao
|
||||
this.data_model = new Aircraft_Model(ds.key, icao)
|
||||
// this.pos_marker.bindPopup("没有数据也不能删除,否则不弹出!");
|
||||
this.data_source = ds;
|
||||
// this.pos_marker.on('popupopen', () => {
|
||||
// this.show_track()
|
||||
// });
|
||||
// this.pos_marker.on('popupclose', () => {
|
||||
// this.hide_track()
|
||||
// });
|
||||
this.pos_marker.on('click', () => {
|
||||
let active_aircraft = this.data_source.active_aircraft;
|
||||
if(active_aircraft != this){
|
||||
@@ -420,33 +236,6 @@ export class Aircraft {
|
||||
}
|
||||
}
|
||||
|
||||
/*{
|
||||
"icao": "780FBD",
|
||||
"ds": "r205_113",
|
||||
"sta_seq": 19,
|
||||
"end_seq": 21,
|
||||
"size": 2,
|
||||
"list": [
|
||||
{
|
||||
"lat": 34.179382,
|
||||
"lon": 122.403951,
|
||||
"alt": 9174.480000,
|
||||
"odd_latency": -601365776,
|
||||
"even_latency": -3771397588,
|
||||
"utc": 1768876493,
|
||||
"idx": 19
|
||||
},
|
||||
{
|
||||
"lat": 34.178839,
|
||||
"lon": 122.403030,
|
||||
"alt": 9174.480000,
|
||||
"odd_latency": -160474061,
|
||||
"even_latency": -3895432888,
|
||||
"utc": 1768876494,
|
||||
"idx": 20
|
||||
}
|
||||
]
|
||||
}*/
|
||||
apply_track_data(data: any) {
|
||||
if (!data) {
|
||||
return []
|
||||
@@ -470,11 +259,6 @@ export class Aircraft {
|
||||
return list
|
||||
}
|
||||
|
||||
append_path() {
|
||||
this.data_source.request_aircraft_stream_update();
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
append_loaded_path(list: any[]) {
|
||||
let map = app.leaflet_map?.map;
|
||||
list.forEach((data) => {
|
||||
@@ -499,11 +283,6 @@ export class Aircraft {
|
||||
append_path_response(data: any) {
|
||||
this.append_loaded_path(this.apply_track_data(data));
|
||||
}
|
||||
refresh_monitored_path() {
|
||||
this.is_show_path = true;
|
||||
return this.append_path();
|
||||
}
|
||||
|
||||
show_path() {
|
||||
if (this.is_show_path == true) return;
|
||||
this.is_show_path = true;
|
||||
@@ -522,23 +301,16 @@ export class Aircraft {
|
||||
const map = app.leaflet_map?.map;
|
||||
const style = this.data_source.map2d_style();
|
||||
if (style.aircraft_show) {
|
||||
//console.log("aircraft refresh show")
|
||||
if (map){
|
||||
// app.leaflet_map.map.removeLayer(this.pos_marker);
|
||||
this.pos_marker.addTo(map);
|
||||
}
|
||||
} else {
|
||||
//console.log("aircraft refresh hide")
|
||||
if (map) {
|
||||
map.removeLayer(this.pos_marker);
|
||||
}
|
||||
this.hide_path();
|
||||
}
|
||||
if(this.need_refresh()){
|
||||
// @ts-ignore
|
||||
// this.pos_marker.setRotationAngle(this.angle.cur);
|
||||
this.pos_marker.setIcon(this.get_icon());
|
||||
}
|
||||
this.pos_marker.setIcon(this.get_icon());
|
||||
let light_color = lightenColor(style.color);
|
||||
for (let i = 0; i < this.track_point_list.length; i++) {
|
||||
let track_point = this.track_point_list[i];
|
||||
|
||||
+65
-139
@@ -1,161 +1,87 @@
|
||||
import enhance from "../core/enhance.tsx";
|
||||
import {baseURL, G} from "../Global.tsx";
|
||||
import {Button, Flex, Menu, MenuProps, message} from "antd";
|
||||
import enhance from "../core/enhance.tsx";
|
||||
import {baseURL, transformToTreeData, type Tree_Node} from "../Global.ts";
|
||||
import {Button, Flex, Menu, type MenuProps, message} from "antd";
|
||||
import axios from "axios";
|
||||
import {SettingOutlined} from "@ant-design/icons";
|
||||
import React from "react";
|
||||
import {Leaflet_Map} from "./Leaflet_Map.tsx";
|
||||
import {app} from "../App.tsx";
|
||||
import {Aircraft} from "./Aircraft.tsx";
|
||||
import type {Aircraft} from "./Aircraft.tsx";
|
||||
import {Base_Drawer} from "../Base_Drawer.tsx";
|
||||
import {PersistentScroll} from "../PersistentScroll.tsx";
|
||||
import {Refresh} from "../Refresh.tsx";
|
||||
import {Tree_Show} from "./Tree_Show.tsx";
|
||||
|
||||
const information_tabs = {
|
||||
base_information: "基础",
|
||||
general_information: "总体",
|
||||
detailed_information: "详细"
|
||||
} as const;
|
||||
type Information_Key = keyof typeof information_tabs;
|
||||
type Information_View = {tree: Tree_Show, tree_data: Tree_Node[], json: Record<string, any>};
|
||||
|
||||
|
||||
function empty_view(): Information_View {
|
||||
return {tree: new Tree_Show(), tree_data: [], json: {}};
|
||||
}
|
||||
|
||||
export class Aircraft_Info_Show extends enhance.Base {
|
||||
active_aircraft: Aircraft = null
|
||||
par: Leaflet_Map
|
||||
drawer = new Base_Drawer()
|
||||
readonly BASE_INFORMATION : string = 'base_information'
|
||||
readonly GENERAL_INFORMATION : string = 'general_information'
|
||||
readonly DETAILED_INFORMATION : string = 'detailed_information'
|
||||
selected_key: string = null
|
||||
di : Tree_Show = new Tree_Show()
|
||||
gi : Tree_Show = new Tree_Show()
|
||||
bi : Tree_Show = new Tree_Show()
|
||||
detailed_information = G.transformToTreeData({});
|
||||
general_information = G.transformToTreeData({});
|
||||
base_information = G.transformToTreeData({});
|
||||
detailed_information_json: Record<string, any> = {};
|
||||
general_information_json: Record<string, any> = {};
|
||||
base_information_json: Record<string, any> = {};
|
||||
refresh_btn = new Refresh(()=>{
|
||||
this.refresh(this.selected_key);
|
||||
})
|
||||
constructor(p: Leaflet_Map) {
|
||||
super();
|
||||
this.par = p
|
||||
this.selected_key = this.BASE_INFORMATION
|
||||
active_aircraft: Aircraft | null = null;
|
||||
drawer = new Base_Drawer();
|
||||
selected_key: Information_Key = "base_information";
|
||||
views: Record<Information_Key, Information_View> = {
|
||||
base_information: empty_view(),
|
||||
general_information: empty_view(),
|
||||
detailed_information: empty_view()
|
||||
};
|
||||
refresh_btn = new Refresh(() => this.refresh(this.selected_key));
|
||||
|
||||
private set_information(key: Information_Key, data: Record<string, any>) {
|
||||
this.views[key].json = data;
|
||||
this.views[key].tree_data = transformToTreeData(data);
|
||||
}
|
||||
|
||||
refresh_general_information() {
|
||||
let as = app.leaflet_map.aircraft_info_show;
|
||||
axios.post(`${baseURL}/get_general_info`).then((res) => {
|
||||
as.set_general_information(res.data);
|
||||
as.flush();
|
||||
})
|
||||
}
|
||||
set_base_information(data: Record<string, any>) {
|
||||
this.base_information_json = data;
|
||||
this.base_information = G.transformToTreeData(data);
|
||||
}
|
||||
set_general_information(data: Record<string, any>) {
|
||||
this.general_information_json = data;
|
||||
this.general_information = G.transformToTreeData(data);
|
||||
}
|
||||
set_detailed_information(data: Record<string, any>) {
|
||||
this.detailed_information_json = data;
|
||||
this.detailed_information = G.transformToTreeData(data);
|
||||
}
|
||||
selected_json_data(): Record<string, any> {
|
||||
if (this.selected_key === this.BASE_INFORMATION) return this.base_information_json;
|
||||
if (this.selected_key === this.GENERAL_INFORMATION) return this.general_information_json;
|
||||
return this.detailed_information_json;
|
||||
}
|
||||
copy_selected_json() {
|
||||
navigator.clipboard.writeText(JSON.stringify(this.selected_json_data(), null, 2)).then(() => {
|
||||
message.success("JSON已复制");
|
||||
set_base_information(data: Record<string, any>) { this.set_information("base_information", data); }
|
||||
set_general_information(data: Record<string, any>) { this.set_information("general_information", data); }
|
||||
set_detailed_information(data: Record<string, any>) { this.set_information("detailed_information", data); }
|
||||
|
||||
refresh(key: Information_Key) {
|
||||
if (key === "base_information") this.active_aircraft?.refresh_base_information();
|
||||
else if (key === "detailed_information") this.active_aircraft?.refresh_detailed_information();
|
||||
else axios.post(`${baseURL}/get_general_info`).then(response => {
|
||||
this.set_general_information(response.data);
|
||||
this.flush();
|
||||
});
|
||||
}
|
||||
|
||||
refresh(value: string) {
|
||||
if (value === this.BASE_INFORMATION) {
|
||||
if (this.active_aircraft) {
|
||||
this.active_aircraft.refresh_base_information();
|
||||
}
|
||||
}
|
||||
else if (value === this.GENERAL_INFORMATION) {
|
||||
this.refresh_general_information();
|
||||
}
|
||||
else if (value === this.DETAILED_INFORMATION) {
|
||||
if (this.active_aircraft) {
|
||||
this.active_aircraft.refresh_detailed_information();
|
||||
}
|
||||
} else {
|
||||
console.error("Aircraft_Info_Show refresh 收到不合法的值!", value);
|
||||
console.trace();
|
||||
}
|
||||
}
|
||||
|
||||
onClick: MenuProps['onClick'] = (e) => {
|
||||
if (this.selected_key != e.key) {
|
||||
this.selected_key = e.key
|
||||
this.flush();
|
||||
this.refresh(e.key);
|
||||
}
|
||||
onClick: MenuProps["onClick"] = event => {
|
||||
if (event.key === this.selected_key || !(event.key in information_tabs)) return;
|
||||
this.selected_key = event.key as Information_Key;
|
||||
this.flush();
|
||||
this.refresh(this.selected_key);
|
||||
};
|
||||
_open: boolean = true;
|
||||
|
||||
use_hook: () => void = () => {
|
||||
//[this.open, this.setOpen] = useState(true);
|
||||
copy_selected_json() {
|
||||
navigator.clipboard.writeText(JSON.stringify(this.views[this.selected_key].json, null, 2)).then(() =>
|
||||
message.success("JSON已复制"));
|
||||
}
|
||||
|
||||
|
||||
first_open: boolean = true;
|
||||
render() {
|
||||
return (
|
||||
<this.drawer.x placement="left">
|
||||
<Flex vertical style={{ height: '100%' }}>
|
||||
{/* 固定的上半部分 */}
|
||||
<div style={{ position: 'sticky', top: 0, zIndex: 1, backgroundColor: 'white' }}>
|
||||
<Menu
|
||||
onClick={this.onClick}
|
||||
mode="horizontal"
|
||||
selectedKeys={[this.selected_key]}
|
||||
style={{
|
||||
width: '260px',
|
||||
}}
|
||||
items={[
|
||||
{
|
||||
label: '基础',
|
||||
key: this.BASE_INFORMATION,
|
||||
icon: <SettingOutlined />,
|
||||
},
|
||||
{
|
||||
label: '总体',
|
||||
key: this.GENERAL_INFORMATION,
|
||||
icon: <SettingOutlined />,
|
||||
},
|
||||
{
|
||||
label: '详细',
|
||||
key: this.DETAILED_INFORMATION,
|
||||
icon: <SettingOutlined />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Flex gap={8} align="center">
|
||||
<this.refresh_btn.x></this.refresh_btn.x>
|
||||
<Button size="small" onClick={() => this.copy_selected_json()}>复制JSON</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
<PersistentScroll scrollKey={this.selected_key}>
|
||||
{this.selected_key === this.BASE_INFORMATION && (
|
||||
<this.bi.x treeData={this.base_information} jsonData={this.base_information_json}></this.bi.x>
|
||||
)}
|
||||
{this.selected_key === this.GENERAL_INFORMATION && (
|
||||
<this.gi.x treeData={this.general_information} jsonData={this.general_information_json}></this.gi.x>
|
||||
)}
|
||||
{this.selected_key === this.DETAILED_INFORMATION && (
|
||||
<this.di.x treeData={this.detailed_information} jsonData={this.detailed_information_json}></this.di.x>
|
||||
)}
|
||||
<div style={{ height: '50vh' }}></div> {/* 空白区域,保持布局 */}
|
||||
</PersistentScroll>
|
||||
</Flex>
|
||||
</this.drawer.x>
|
||||
);
|
||||
const view = this.views[this.selected_key];
|
||||
const Tree = view.tree.x;
|
||||
return <this.drawer.x placement="left">
|
||||
<Flex vertical style={{height: "100%"}}>
|
||||
<div style={{position: "sticky", top: 0, zIndex: 1, backgroundColor: "white"}}>
|
||||
<Menu onClick={this.onClick} mode="horizontal" selectedKeys={[this.selected_key]}
|
||||
style={{width: 260}}
|
||||
items={Object.entries(information_tabs).map(([key, label]) => ({
|
||||
key, label, icon: <SettingOutlined/>
|
||||
}))}/>
|
||||
<Flex gap={8} align="center">
|
||||
<this.refresh_btn.x/>
|
||||
<Button size="small" onClick={() => this.copy_selected_json()}>复制JSON</Button>
|
||||
</Flex>
|
||||
</div>
|
||||
<PersistentScroll scrollKey={this.selected_key}>
|
||||
<Tree treeData={view.tree_data} jsonData={view.json}/>
|
||||
</PersistentScroll>
|
||||
</Flex>
|
||||
</this.drawer.x>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import enhance from "../core/enhance.tsx";
|
||||
import {Button, Flex, Input, InputNumber, Layout, Select, Space, Table} from "antd";
|
||||
import React, {HTMLProps} from "react";
|
||||
import axios from "axios";
|
||||
import {baseURL, Prefix} from "../Global.tsx";
|
||||
import {baseURL} from "../Global.ts";
|
||||
import {app} from "../App.tsx";
|
||||
import {SearchOutlined} from "@ant-design/icons";
|
||||
import type {TableColumnType} from "antd";
|
||||
@@ -245,7 +245,7 @@ export class Aircraft_List extends enhance.Base {
|
||||
return <Layout style={{height: "100%", minWidth: 0}}>
|
||||
<Flex gap="middle" vertical style={{width: "100%", minWidth: 0}}>
|
||||
<Flex gap="middle">
|
||||
<Select prefix={<Prefix label="数据来源"/>}
|
||||
<Select prefix={<strong style={{whiteSpace: "nowrap"}}>数据来源</strong>}
|
||||
placeholder="请选择数据来源"
|
||||
value={this.data_source_key}
|
||||
options={(app.data_source_config?.enabled() ?? []).map(item => ({
|
||||
|
||||
@@ -85,7 +85,7 @@ export function orientation_from_item(item: any, fallback: Aircraft_Orientation_
|
||||
roll: Number(source.roll ?? fallback.roll ?? 0)
|
||||
}
|
||||
}
|
||||
export function resolve_aircraft_status(item: any, previous_alt?: number): Aircraft_Status {
|
||||
function resolve_aircraft_status(item: any, previous_alt?: number): Aircraft_Status {
|
||||
const vert_speed = item.vert_speed
|
||||
if (typeof vert_speed === "number") {
|
||||
if (vert_speed === 0) return Aircraft_Status.Level
|
||||
@@ -101,7 +101,7 @@ export function resolve_aircraft_status(item: any, previous_alt?: number): Aircr
|
||||
}
|
||||
return Aircraft_Status.Level
|
||||
}
|
||||
export function aircraft_status_text(status: Aircraft_Status): string {
|
||||
function aircraft_status_text(status: Aircraft_Status): string {
|
||||
return {
|
||||
[Aircraft_Status.Level]: "平飞",
|
||||
[Aircraft_Status.Descending]: "下降",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import L from "leaflet";
|
||||
import {darkenColor, G} from "../Global.tsx";
|
||||
import {darkenColor} from "../Global.ts";
|
||||
import {app} from "../App.tsx";
|
||||
import {Data_Source} from "../Data_Source/Data_Source.tsx";
|
||||
|
||||
@@ -175,7 +175,11 @@ export class Base_Station {
|
||||
}
|
||||
|
||||
set_popup(key: string, d: any) {
|
||||
this.that.bindPopup("key:" + key + "<br>" + G.to_popup_text(d));
|
||||
const content = document.createElement("div");
|
||||
content.innerText = [`key: ${key}`, ...Object.entries(d).map(([name, value]) =>
|
||||
`${name}: ${JSON.stringify(value)}`)].join("\n");
|
||||
content.style.whiteSpace = "pre-line";
|
||||
this.that.bindPopup(content);
|
||||
}
|
||||
|
||||
get_distance_from_base_station(lat: number, lng: number) {
|
||||
|
||||
+2
-52
@@ -118,7 +118,6 @@ export class Cesium_Map extends enhance.Base {
|
||||
scene_mode: Scene_Mode_Key = "3d";
|
||||
current_imagery_key: string = "";
|
||||
graphics_config: Cesium_Graphics_Config = {...default_cesium_graphics_config};
|
||||
tile_display_level_draft: number | null | undefined = undefined;
|
||||
displayed_imagery_minimum_level: number | null = null;
|
||||
displayed_imagery_maximum_level: number | null = null;
|
||||
displayed_terrain_minimum_level: number | null = null;
|
||||
@@ -350,41 +349,16 @@ export class Cesium_Map extends enhance.Base {
|
||||
this.flush();
|
||||
}
|
||||
change_tile_display_maximum_level(value: number | null) {
|
||||
this.tile_display_level_draft = value;
|
||||
if (value === null) {
|
||||
this.flush();
|
||||
return;
|
||||
}
|
||||
if (value === null) return;
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.map_view_config = {...config, map3d: {...config.map3d, tile_display_maximum_level: value}};
|
||||
this.refresh_imagery_layer();
|
||||
this.viewer?.scene.requestRender();
|
||||
this.flush();
|
||||
}
|
||||
blur_tile_display_maximum_level() {
|
||||
if (this.tile_display_level_draft !== undefined) {
|
||||
this.tile_display_level_draft = undefined;
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
base_station_fly_to_height_offset_meters(): number {
|
||||
return (this.map_view_config || default_map_view_config).base_station_fly_to_height_offset_meters;
|
||||
}
|
||||
change_base_station_fly_to_height_offset_meters(value: number) {
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.map_view_config = {...config, base_station_fly_to_height_offset_meters: Math.max(100, value)};
|
||||
this.flush();
|
||||
}
|
||||
set_surface_navigation_reference_visible(value: boolean) {
|
||||
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, surface_navigation_reference_visible: value});
|
||||
this.apply_graphics_config();
|
||||
this.flush();
|
||||
}
|
||||
set_view_axes_visible(value: boolean) {
|
||||
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, view_axes_visible: value});
|
||||
this.apply_graphics_config();
|
||||
this.flush();
|
||||
}
|
||||
change_view_axes_panel_size(value: number) {
|
||||
this.graphics_config = normalize_cesium_graphics_config({...this.graphics_config, view_axes_panel_size: value});
|
||||
this.flush();
|
||||
@@ -411,24 +385,6 @@ export class Cesium_Map extends enhance.Base {
|
||||
message.success("地图视图已保存");
|
||||
this.flush();
|
||||
}
|
||||
change_scene_mode(mode: Scene_Mode_Key) {
|
||||
if (!this.viewer) return;
|
||||
this.scene_mode = mode;
|
||||
if (mode !== "3d") {
|
||||
this.destroy_surface_navigation_reference();
|
||||
}
|
||||
this.update_terrain_provider();
|
||||
if (mode === "2d") {
|
||||
this.viewer.scene.morphTo2D(0.5);
|
||||
}
|
||||
else if (mode === "2.5d") {
|
||||
this.viewer.scene.morphToColumbusView(0.5);
|
||||
}
|
||||
else {
|
||||
this.viewer.scene.morphTo3D(0.5);
|
||||
}
|
||||
this.flush();
|
||||
}
|
||||
to_cesium_scene_mode(mode: Scene_Mode_Key): Cesium.SceneMode {
|
||||
if (mode === "2d") return Cesium.SceneMode.SCENE2D;
|
||||
if (mode === "2.5d") return Cesium.SceneMode.COLUMBUS_VIEW;
|
||||
@@ -762,9 +718,6 @@ export class Cesium_Map extends enhance.Base {
|
||||
request_sync_data_sources() {
|
||||
this.data_source_sync.request();
|
||||
}
|
||||
sync_data_sources() {
|
||||
this.data_source_sync.sync();
|
||||
}
|
||||
sync_base_station(ds: Data_Source, alive_base_stations: Set<string>) {
|
||||
if (!this.viewer) return;
|
||||
const station = ds.base_station;
|
||||
@@ -1195,9 +1148,6 @@ export class Cesium_Map extends enhance.Base {
|
||||
sync_aircraft(key: string, aircraft: Aircraft) {
|
||||
this.aircraft_layer.sync(key, aircraft);
|
||||
}
|
||||
create_aircraft_record(key: string, aircraft: Aircraft, position: Cesium.Cartesian3, orientation: Cesium.Quaternion): Aircraft_Entity_Record {
|
||||
return this.aircraft_layer.create_record(key, aircraft, position, orientation);
|
||||
}
|
||||
ensure_aircraft_track_entities(key: string, aircraft: Aircraft, record: Aircraft_Entity_Record) {
|
||||
this.aircraft_layer.ensure_track_entities(key, aircraft, record);
|
||||
}
|
||||
@@ -1697,7 +1647,7 @@ export class Cesium_Map extends enhance.Base {
|
||||
type Cesium_Map_View_Props = {
|
||||
initialSceneMode?: Scene_Mode_Key
|
||||
}
|
||||
export function Cesium_Map_View({initialSceneMode = "3d"}: Cesium_Map_View_Props) {
|
||||
function Cesium_Map_View({initialSceneMode = "3d"}: Cesium_Map_View_Props) {
|
||||
const map_ref = React.useRef<Cesium_Map | null>(null);
|
||||
if (!map_ref.current) {
|
||||
map_ref.current = new Cesium_Map(initialSceneMode);
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from "react";
|
||||
import {Button, Card, message, Select, Space, Upload} from "antd";
|
||||
import {UploadOutlined} from "@ant-design/icons";
|
||||
import enhance from "../core/enhance.tsx";
|
||||
import {col_style, row_style, setting_style} from "../Global.tsx";
|
||||
import {
|
||||
empty_map_model_config,
|
||||
load_map_model_config,
|
||||
@@ -85,7 +84,7 @@ export class Cesium_Model_Settings extends enhance.Base {
|
||||
if (!get_path(this.config, path)) return null;
|
||||
return (
|
||||
<Space key={path} direction="vertical" size={6} style={{width: "100%"}}>
|
||||
<h3 style={row_style}>{title}</h3>
|
||||
<h3>{title}</h3>
|
||||
<Backend_Fields root={this.config} descriptor_fields={fields}
|
||||
section={{object_path: path, descriptor_path: "", fields: fields.map(field => field.name)}}
|
||||
onChange={() => this.flush()}/>
|
||||
@@ -127,9 +126,9 @@ export class Cesium_Model_Settings extends enhance.Base {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={setting_style}>
|
||||
<h2 style={row_style}>{this.schema.title ?? "Cesium模型设置"}</h2>
|
||||
<Space direction="vertical" size={12} style={{...col_style, width: "100%"}}>
|
||||
<div style={{minWidth: "100%", width: "max-content", overflowX: "auto", display: "flex", flexDirection: "column", alignItems: "center"}}>
|
||||
<h2>{this.schema.title ?? "Cesium模型设置"}</h2>
|
||||
<Space direction="vertical" size={12} style={{alignItems: "center", width: "100%"}}>
|
||||
{(this.schema.groups || []).map(group =>
|
||||
this.render_model(group.title, group.path, group.upload))}
|
||||
{(this.schema.collections || []).map(collection => this.render_collection(collection))}
|
||||
|
||||
+11
-134
@@ -2,17 +2,14 @@ import "leaflet/dist/leaflet.css"
|
||||
import L from 'leaflet';
|
||||
import React from 'react';
|
||||
import enhance from "../core/enhance.tsx";
|
||||
import {baseURL, host} from "../Global.js";
|
||||
import {Aircraft} from "./Aircraft.tsx"
|
||||
import {Button, Space, message} from "antd";
|
||||
|
||||
|
||||
import axios from "axios";
|
||||
import {Aircraft_Info_Show} from "./Aircraft_Info_Show.tsx";
|
||||
import {Data_Source_Config} from "../Data_Source/Data_Source_Config.tsx";
|
||||
import {Data_Source_Show} from "../Data_Source/Data_Source_Show.tsx";
|
||||
import {app} from "../App.tsx";
|
||||
//import {Base_Station} from "./Base_Station.tsx";
|
||||
import {Data_Source} from "../Data_Source/Data_Source.tsx";
|
||||
import {
|
||||
imagery_source_for_key,
|
||||
@@ -23,59 +20,27 @@ import {
|
||||
import {default_map_view_config, load_map_view_config, save_map_view_config, type Map_Tile_View_Config, type Map_View_Config} from "./Map_View.tsx";
|
||||
|
||||
|
||||
export class POS {
|
||||
lat: number;
|
||||
lon: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export class Leaflet_Map extends enhance.Base {
|
||||
// @ts-ignore
|
||||
map: L.Map = null;
|
||||
data_source_show: Data_Source_Show = new Data_Source_Show();
|
||||
// @ts-ignore
|
||||
aircraft_info_show: Aircraft_Info_Show = new Aircraft_Info_Show(this);
|
||||
aircraft_info_show: Aircraft_Info_Show = new Aircraft_Info_Show();
|
||||
data_source_config: Data_Source_Config | null = null
|
||||
tile_layer: L.TileLayer | null = null
|
||||
map_resources: Map_Resources_Metadata | null = null
|
||||
map_view_config: Map_View_Config | null = null
|
||||
current_imagery_key: string = ""
|
||||
context_menu: {x: number, y: number, aircraft?: Aircraft, base_station?: Data_Source} | null = null
|
||||
tile_display_level_draft: number | null | undefined = undefined
|
||||
tile_level_listener = () => this.data_source_show.flush()
|
||||
private savedCenter: L.LatLng | null = null;
|
||||
private savedZoom: number | null = null;
|
||||
intervalId: number | null = null;
|
||||
autoRefreshTimer = null; // 用于存储定时器ID
|
||||
refreshInterval : number = 1; // 用于存储定时器ID
|
||||
autoRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
|
||||
private is_click_on_map_entity(e: L.LeafletMouseEvent): boolean {
|
||||
const target = e.originalEvent.target;
|
||||
if (!(target instanceof Element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Boolean(target.closest([
|
||||
".leaflet-marker-icon",
|
||||
".leaflet-interactive",
|
||||
".leaflet-popup",
|
||||
".leaflet-tooltip",
|
||||
".leaflet-control",
|
||||
].join(",")));
|
||||
}
|
||||
|
||||
private clear_active_aircraft_by_map_click = (e: L.LeafletMouseEvent) => {
|
||||
|
||||
console.log(e);
|
||||
private clear_active_aircraft_by_map_click = () => {
|
||||
const had_context_menu = this.context_menu !== null;
|
||||
this.context_menu = null;
|
||||
// if (this.is_click_on_map_entity(e)) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
let changed = false;
|
||||
this.list().forEach((ds: Data_Source) => {
|
||||
if (!ds.active_aircraft) return;
|
||||
@@ -96,24 +61,15 @@ export class Leaflet_Map extends enhance.Base {
|
||||
|
||||
|
||||
on_mount: () => void = () => {
|
||||
console.log("地图加载");
|
||||
this.map = this.load_map();
|
||||
this.load_imagery_layer();
|
||||
this.map.on("click", this.clear_active_aircraft_by_map_click);
|
||||
this.map.on("zoomend", this.tile_level_listener);
|
||||
this.map.whenReady(() => {
|
||||
|
||||
});
|
||||
|
||||
if (this.savedCenter && this.savedZoom) {
|
||||
if (this.savedCenter && this.savedZoom !== null) {
|
||||
this.map.setView(this.savedCenter, this.savedZoom);
|
||||
}
|
||||
|
||||
|
||||
if (this.data_source_config) {
|
||||
const ds_list = this.data_source_config.all();
|
||||
ds_list.forEach((ds: Data_Source) => {
|
||||
if (!ds.enable) return; // ✅ 跳过当前项
|
||||
this.data_source_config.enabled().forEach((ds: Data_Source) => {
|
||||
for (const aircraft of ds.aircraftMap.values()) {
|
||||
aircraft.on_mount();
|
||||
}
|
||||
@@ -124,16 +80,8 @@ export class Leaflet_Map extends enhance.Base {
|
||||
|
||||
|
||||
|
||||
this.autoRefreshTimer = setInterval(() => {
|
||||
if (this.data_source_config){
|
||||
const ds_list = this.data_source_config.all();
|
||||
ds_list.forEach((ds: Data_Source) => {
|
||||
if (!ds.enable) return; // ✅ 跳过当前项
|
||||
ds.refresh()
|
||||
});
|
||||
}
|
||||
|
||||
}, this.refreshInterval * 1000); // 以秒为单位
|
||||
this.autoRefreshTimer = setInterval(() =>
|
||||
this.data_source_config?.enabled().forEach(ds => ds.refresh()), 1000);
|
||||
|
||||
|
||||
this.flush();
|
||||
@@ -143,7 +91,6 @@ export class Leaflet_Map extends enhance.Base {
|
||||
|
||||
|
||||
on_un_mount: () => void = () => {
|
||||
console.log("地图卸载");
|
||||
if (this.map) {
|
||||
this.savedCenter = this.map.getCenter();
|
||||
this.savedZoom = this.map.getZoom();
|
||||
@@ -156,7 +103,7 @@ export class Leaflet_Map extends enhance.Base {
|
||||
map.removeLayer(layer);
|
||||
});
|
||||
if (this.autoRefreshTimer) {
|
||||
clearInterval(this.autoRefreshTimer); // 清除定时器
|
||||
clearInterval(this.autoRefreshTimer);
|
||||
this.autoRefreshTimer = null;
|
||||
}
|
||||
this.map = null
|
||||
@@ -219,62 +166,12 @@ export class Leaflet_Map extends enhance.Base {
|
||||
a.drawer.toggleOpen()
|
||||
}
|
||||
|
||||
if (a.selected_key === a.DETAILED_INFORMATION) {
|
||||
axios.post(`${baseURL}/get_aircraft_detail_info`, {
|
||||
data_source_key: that.data_source.key,
|
||||
icao: that.icao,
|
||||
}).then(res => {
|
||||
a.set_detailed_information(res.data);
|
||||
a.flush();
|
||||
})
|
||||
}
|
||||
if (a.selected_key === a.BASE_INFORMATION) {
|
||||
axios.post(`${baseURL}/get_aircraft_base_info`, {
|
||||
data_source_key: that.data_source.key,
|
||||
icao: that.icao,
|
||||
}).then(res => {
|
||||
a.set_base_information(res.data);
|
||||
a.flush();
|
||||
})
|
||||
}
|
||||
a.refresh(a.selected_key);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
timer
|
||||
mlat_source: Data_Source = new Data_Source();
|
||||
mlat: Map<string, Aircraft> = new Map();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.mlat_source.key = "mlat_source";
|
||||
|
||||
// this.timer = setInterval(() => {
|
||||
// this.mlat_source.aircraft_show = true;
|
||||
// axios.post(`${baseURL}/get_mlat_list`, {})
|
||||
// .then(res => {
|
||||
//
|
||||
// //console.log(res.data);
|
||||
// res.data.forEach(item => {
|
||||
// //console.log(item);
|
||||
// let mlat_air = this.mlat.get(item.icao);
|
||||
// if (!mlat_air) {
|
||||
// mlat_air = new Aircraft(item.icao, this.mlat_source)
|
||||
// mlat_air.mlat_show = true;
|
||||
// this.mlat.set(item.icao, mlat_air);
|
||||
// }
|
||||
//
|
||||
// mlat_air.add_malt_data(item);
|
||||
//
|
||||
// });
|
||||
// })
|
||||
// .catch(err => {
|
||||
// console.error("请求出错:", err);
|
||||
// });
|
||||
// }, 1000);
|
||||
}
|
||||
|
||||
INIT_EXTENT = L.latLngBounds(
|
||||
[36.42, 119.98], // 西南角:
|
||||
[38.40, 122.56] // 东北角:
|
||||
@@ -284,24 +181,14 @@ export class Leaflet_Map extends enhance.Base {
|
||||
const MAP_CENTER: [number, number] = [34.3227, 118.5525];
|
||||
|
||||
|
||||
// Leaflet 默认图标路径(你原来需要的话保留)
|
||||
L.Icon.Default.imagePath = `http://${host}/images/`;
|
||||
|
||||
// 1) 创建地图(等价 MapContainer)
|
||||
L.Icon.Default.imagePath = "/images/";
|
||||
const map = L.map("map", {
|
||||
center: MAP_CENTER,
|
||||
zoom: 3,
|
||||
// minZoom: 3,
|
||||
// maxZoom: 9,
|
||||
zoomControl: false,
|
||||
attributionControl: false,
|
||||
doubleClickZoom: false,
|
||||
// preferCanvas: true, // 可选
|
||||
});
|
||||
|
||||
// 3) 等价 MapContent: fitBounds + on_mount
|
||||
// INIT_EXTENT 必须是 Leaflet 接受的 bounds 格式:
|
||||
// [[southLat, westLng], [northLat, eastLng]]
|
||||
map.fitBounds(this.INIT_EXTENT, {
|
||||
padding: [50, 50],
|
||||
maxZoom: 10,
|
||||
@@ -366,22 +253,12 @@ export class Leaflet_Map extends enhance.Base {
|
||||
this.flush();
|
||||
}
|
||||
change_tile_display_maximum_level(value: number | null) {
|
||||
this.tile_display_level_draft = value;
|
||||
if (value === null) {
|
||||
this.flush();
|
||||
return;
|
||||
}
|
||||
if (value === null) return;
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.map_view_config = {...config, map2d: {...config.map2d, tile_display_maximum_level: value}};
|
||||
this.set_imagery_layer();
|
||||
this.flush();
|
||||
}
|
||||
blur_tile_display_maximum_level() {
|
||||
if (this.tile_display_level_draft !== undefined) {
|
||||
this.tile_display_level_draft = undefined;
|
||||
this.flush();
|
||||
}
|
||||
}
|
||||
async save_current_map_view() {
|
||||
const config = this.map_view_config || default_map_view_config;
|
||||
this.map_view_config = await save_map_view_config({
|
||||
|
||||
@@ -27,6 +27,6 @@ export async function upload_map_model(model_type: string, file: File, aircraft_
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export function emit_map_model_config(config: Map_Model_Config) {
|
||||
function emit_map_model_config(config: Map_Model_Config) {
|
||||
window.dispatchEvent(new CustomEvent<Map_Model_Config>(map_model_config_event, {detail: config}));
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import axios from "axios";
|
||||
|
||||
export type Map_Tile_Y_Axis = "xyz" | "tms"
|
||||
export type Map_Tile_Projection = "web_mercator"
|
||||
export type Map_Tile_Encoding = "terrarium" | string
|
||||
type Map_Tile_Projection = "web_mercator"
|
||||
type Map_Tile_Encoding = "terrarium" | string
|
||||
export type Map_Tile_Type = "imagery" | "terrain"
|
||||
export type Map_Tile_Metadata = {
|
||||
url: string
|
||||
@@ -42,7 +42,7 @@ export function terrain_source_for_key(resources: Map_Resources_Metadata, key: s
|
||||
if (!source) throw new Error("No terrain source configured");
|
||||
return source;
|
||||
}
|
||||
export function tile_sources_for_type(resources: Map_Resources_Metadata, tile_type: Map_Tile_Type): Map_Imagery_Source_Metadata[] {
|
||||
function tile_sources_for_type(resources: Map_Resources_Metadata, tile_type: Map_Tile_Type): Map_Imagery_Source_Metadata[] {
|
||||
return tile_type === "terrain" ? resources.terrain_sources : resources.imagery_sources;
|
||||
}
|
||||
export function tile_source_options(resources: Map_Resources_Metadata | null, tile_type: Map_Tile_Type) {
|
||||
|
||||
@@ -5,7 +5,6 @@ export type Map_Camera_View = Record<string, number>
|
||||
export type Map_Tile_View_Config = Record<string, any>
|
||||
export type Map_View_Config = Record<string, any>
|
||||
|
||||
export const default_map_camera_view: Map_Camera_View = {};
|
||||
export const default_map_view_config: Map_View_Config = {map2d: {}, map3d: {}, camera: {}};
|
||||
|
||||
export async function load_map_view_config(): Promise<Map_View_Config> {
|
||||
|
||||
+74
-357
@@ -1,404 +1,121 @@
|
||||
import React from "react";
|
||||
import React from "react";
|
||||
import {message, Tree, type TreeProps} from "antd";
|
||||
import enhance from "../core/enhance.tsx";
|
||||
import {message, Tree, TreeProps} from "antd";
|
||||
|
||||
type TreeData = NonNullable<TreeProps["treeData"]>;
|
||||
type TreeNode = TreeData[number];
|
||||
|
||||
function stop_event(event: React.SyntheticEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
export class Tree_Show extends enhance.Base {
|
||||
expandedKeys: React.Key[] = [];
|
||||
selectedKeys: React.Key[] = [];
|
||||
checkedKeys: TreeProps["checkedKeys"] = [];
|
||||
|
||||
private treeData: TreeProps["treeData"] = [];
|
||||
private jsonData: unknown = null;
|
||||
|
||||
private uniq(keys: React.Key[]): React.Key[] {
|
||||
return Array.from(new Set(keys));
|
||||
}
|
||||
|
||||
private findNode(
|
||||
nodes: TreeProps["treeData"] | undefined,
|
||||
key: React.Key
|
||||
): TreeNode | undefined {
|
||||
private find_node(nodes: TreeProps["treeData"] | undefined, key: React.Key): TreeNode | undefined {
|
||||
for (const node of nodes ?? []) {
|
||||
if (node.key === key) {
|
||||
return node;
|
||||
}
|
||||
|
||||
const found = this.findNode(
|
||||
node.children as TreeProps["treeData"] | undefined,
|
||||
key
|
||||
);
|
||||
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
if (node.key === key) return node;
|
||||
const found = this.find_node(node.children as TreeProps["treeData"] | undefined, key);
|
||||
if (found) return found;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前节点是否已经展开
|
||||
*/
|
||||
private isExpanded(key: React.Key): boolean {
|
||||
return this.expandedKeys.includes(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集当前节点 + 所有子孙节点中可展开的 key
|
||||
*/
|
||||
private collectSelfAndChildrenKeys(node: TreeNode): React.Key[] {
|
||||
private expandable_keys(node: TreeNode, include_self: boolean): React.Key[] {
|
||||
const result: React.Key[] = [];
|
||||
|
||||
const walk = (current: TreeNode) => {
|
||||
const walk = (current: TreeNode, include: boolean) => {
|
||||
const children = current.children as TreeNode[] | undefined;
|
||||
|
||||
if (children?.length) {
|
||||
result.push(current.key);
|
||||
|
||||
for (const child of children) {
|
||||
walk(child);
|
||||
}
|
||||
}
|
||||
if (!children?.length) return;
|
||||
if (include) result.push(current.key);
|
||||
for (const child of children) walk(child, true);
|
||||
};
|
||||
|
||||
walk(node);
|
||||
|
||||
walk(node, include_self);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集所有子孙节点中可展开的 key,不包含自己
|
||||
*/
|
||||
private collectChildrenKeys(node: TreeNode): React.Key[] {
|
||||
const result: React.Key[] = [];
|
||||
|
||||
const walk = (children?: TreeNode[]) => {
|
||||
for (const child of children ?? []) {
|
||||
const childChildren = child.children as TreeNode[] | undefined;
|
||||
|
||||
if (childChildren?.length) {
|
||||
result.push(child.key);
|
||||
walk(childChildren);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(node.children as TreeNode[] | undefined);
|
||||
|
||||
return result;
|
||||
}
|
||||
private jsonValue(key: React.Key): unknown {
|
||||
private json_value(key: React.Key): unknown {
|
||||
let value = this.jsonData;
|
||||
for (const part of String(key).split("-")) {
|
||||
const index = Number(part);
|
||||
if (Array.isArray(value)) {
|
||||
value = value[index];
|
||||
} else if (value && typeof value === "object") {
|
||||
value = Object.values(value as Record<string, unknown>)[index];
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
if (Array.isArray(value)) value = value[index];
|
||||
else if (value && typeof value === "object") value = Object.values(value as Record<string, unknown>)[index];
|
||||
else break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
private copyJson(key: React.Key, event: React.MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigator.clipboard.writeText(JSON.stringify(this.jsonValue(key), null, 2) ?? "null").then(() => {
|
||||
|
||||
private copy_json(key: React.Key, event: React.MouseEvent) {
|
||||
stop_event(event);
|
||||
void navigator.clipboard.writeText(JSON.stringify(this.json_value(key), null, 2) ?? "null").then(() => {
|
||||
message.success("JSON已复制");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 左侧按钮:
|
||||
* 只展开 / 收起当前节点
|
||||
*
|
||||
* 展开当前节点时,会清掉它下面所有子孙 expanded key。
|
||||
* 这样不会出现“点自己展开,下面之前展开过的子节点也跟着展开”的问题。
|
||||
*/
|
||||
private toggleSelf(key: React.Key, event: React.MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const node = this.findNode(this.treeData, key);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const childrenKeys = new Set(this.collectChildrenKeys(node));
|
||||
|
||||
if (this.isExpanded(key)) {
|
||||
/**
|
||||
* 收起当前节点:
|
||||
* 当前节点和所有子孙节点都从 expandedKeys 移除。
|
||||
*/
|
||||
this.expandedKeys = this.expandedKeys.filter(
|
||||
item => item !== key && !childrenKeys.has(item)
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
* 只展开当前节点:
|
||||
* 加入当前节点 key,同时移除子孙展开 key。
|
||||
*/
|
||||
this.expandedKeys = this.uniq([
|
||||
...this.expandedKeys.filter(item => !childrenKeys.has(item)),
|
||||
key,
|
||||
]);
|
||||
}
|
||||
|
||||
private toggle_self(key: React.Key, event: React.MouseEvent) {
|
||||
stop_event(event);
|
||||
const node = this.find_node(this.treeData, key);
|
||||
if (!node) return;
|
||||
const next = new Set(this.expandedKeys);
|
||||
for (const descendant of this.expandable_keys(node, false)) next.delete(descendant);
|
||||
if (next.has(key)) next.delete(key);
|
||||
else next.add(key);
|
||||
this.expandedKeys = [...next];
|
||||
this.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前节点及其所有子孙是否全部展开
|
||||
*/
|
||||
private isSelfAndChildrenExpanded(key: React.Key): boolean {
|
||||
const node = this.findNode(this.treeData, key);
|
||||
|
||||
if (!node) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const keys = this.collectSelfAndChildrenKeys(node);
|
||||
const expandedKeySet = new Set(this.expandedKeys);
|
||||
|
||||
return keys.length > 0 && keys.every(item => expandedKeySet.has(item));
|
||||
}
|
||||
|
||||
/**
|
||||
* 右侧按钮:
|
||||
* 全部展开 / 全部收起
|
||||
*/
|
||||
private toggleAllChildren(key: React.Key, event: React.MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const node = this.findNode(this.treeData, key);
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = this.collectSelfAndChildrenKeys(node);
|
||||
const keySet = new Set(keys);
|
||||
|
||||
const isAllExpanded = keys.every(item => this.expandedKeys.includes(item));
|
||||
|
||||
if (isAllExpanded) {
|
||||
/**
|
||||
* 全部收起:
|
||||
* 当前节点 + 所有子孙节点全部移除。
|
||||
*/
|
||||
this.expandedKeys = this.expandedKeys.filter(
|
||||
item => !keySet.has(item)
|
||||
);
|
||||
} else {
|
||||
/**
|
||||
* 全部展开:
|
||||
* 当前节点 + 所有子孙节点全部加入。
|
||||
*/
|
||||
this.expandedKeys = this.uniq([
|
||||
...this.expandedKeys,
|
||||
...keys,
|
||||
]);
|
||||
}
|
||||
|
||||
private toggle_tree(key: React.Key, event: React.MouseEvent) {
|
||||
stop_event(event);
|
||||
const node = this.find_node(this.treeData, key);
|
||||
if (!node) return;
|
||||
const keys = this.expandable_keys(node, true);
|
||||
const next = new Set(this.expandedKeys);
|
||||
if (keys.every(item => next.has(item))) keys.forEach(item => next.delete(item));
|
||||
else keys.forEach(item => next.add(item));
|
||||
this.expandedKeys = [...next];
|
||||
this.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义 switcherIcon。
|
||||
*
|
||||
* 注意:
|
||||
* 根 span 也做 stopPropagation。
|
||||
* 这样即使点到两个按钮之间的空隙,也不会触发 antd 自带展开逻辑。
|
||||
*/
|
||||
private renderSwitcherIcon = (nodeProps: any): React.ReactNode => {
|
||||
const key: React.Key = nodeProps.eventKey ?? nodeProps.key;
|
||||
const isLeaf = Boolean(nodeProps.isLeaf);
|
||||
private button(title: string, text: string, onClick: (event: React.MouseEvent) => void) {
|
||||
return <button type="button" className="tree-switcher-btn" title={title}
|
||||
onMouseDown={stop_event} onClick={onClick}>{text}</button>;
|
||||
}
|
||||
|
||||
if (isLeaf) {
|
||||
return (
|
||||
<span className="tree-switcher-custom">
|
||||
<button
|
||||
type="button"
|
||||
className="tree-switcher-btn tree-switcher-copy"
|
||||
title="复制JSON"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event) => this.copyJson(key, event)}
|
||||
>
|
||||
⧉
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
private render_switcher = (node: any): React.ReactNode => {
|
||||
const key: React.Key = node.eventKey ?? node.key;
|
||||
if (node.isLeaf) {
|
||||
return <span className="tree-switcher-custom">
|
||||
{this.button("复制JSON", "⧉", event => this.copy_json(key, event))}
|
||||
</span>;
|
||||
}
|
||||
|
||||
const expanded = this.isExpanded(key);
|
||||
const isAllExpanded = this.isSelfAndChildrenExpanded(key);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="tree-switcher-custom"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="tree-switcher-btn tree-switcher-self"
|
||||
title={expanded ? "收起当前节点" : "展开当前节点"}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event) => this.toggleSelf(key, event)}
|
||||
>
|
||||
{expanded ? "▼" : "▶"}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="tree-switcher-btn tree-switcher-all"
|
||||
title={isAllExpanded ? "全部收起" : "全部展开"}
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event) => this.toggleAllChildren(key, event)}
|
||||
>
|
||||
{isAllExpanded ? "⤴" : "⤵"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="tree-switcher-btn tree-switcher-copy"
|
||||
title="复制JSON"
|
||||
onMouseDown={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onClick={(event) => this.copyJson(key, event)}
|
||||
>
|
||||
⧉
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
onSelect: TreeProps["onSelect"] = (selectedKeys) => {
|
||||
this.selectedKeys = selectedKeys;
|
||||
this.flush();
|
||||
};
|
||||
|
||||
onCheck: TreeProps["onCheck"] = (checkedKeys) => {
|
||||
this.checkedKeys = checkedKeys;
|
||||
this.flush();
|
||||
};
|
||||
|
||||
/**
|
||||
* 理论上,鼠标点击 switcher 已经被我们拦截了。
|
||||
*
|
||||
* 这里保留 onExpand,是为了兼容:
|
||||
* 1. 键盘操作;
|
||||
* 2. antd 内部其他方式触发展开;
|
||||
* 3. 外部 props 控制展开。
|
||||
*/
|
||||
onExpand: TreeProps["onExpand"] = (expandedKeys) => {
|
||||
this.expandedKeys = expandedKeys as React.Key[];
|
||||
this.flush();
|
||||
const expanded = this.expandedKeys.includes(key);
|
||||
const branch_keys = this.expandable_keys(this.find_node(this.treeData, key)!, true);
|
||||
const all_expanded = branch_keys.every(item => this.expandedKeys.includes(item));
|
||||
return <span className="tree-switcher-custom" onMouseDown={stop_event} onClick={stop_event}>
|
||||
{this.button(expanded ? "收起当前节点" : "展开当前节点", expanded ? "▼" : "▶",
|
||||
event => this.toggle_self(key, event))}
|
||||
{this.button(all_expanded ? "全部收起" : "全部展开", all_expanded ? "⤴" : "⤵",
|
||||
event => this.toggle_tree(key, event))}
|
||||
{this.button("复制JSON", "⧉", event => this.copy_json(key, event))}
|
||||
</span>;
|
||||
};
|
||||
|
||||
render(props: TreeProps & {jsonData?: unknown}): React.JSX.Element {
|
||||
const {jsonData, ...treeProps} = props;
|
||||
this.treeData = treeProps.treeData ?? [];
|
||||
this.jsonData = jsonData ?? null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
.ant-tree-switcher {
|
||||
width: 76px !important;
|
||||
flex: 0 0 76px !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tree-switcher-custom {
|
||||
width: 76px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tree-switcher-btn {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.tree-switcher-btn:hover {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.tree-switcher-self {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tree-switcher-all {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tree-switcher-copy {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tree-switcher-empty {
|
||||
display: inline-block;
|
||||
width: 76px;
|
||||
height: 24px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<Tree
|
||||
{...treeProps}
|
||||
autoExpandParent={false}
|
||||
expandedKeys={this.expandedKeys}
|
||||
selectedKeys={this.selectedKeys}
|
||||
checkedKeys={this.checkedKeys}
|
||||
switcherIcon={this.renderSwitcherIcon}
|
||||
onSelect={this.onSelect}
|
||||
onCheck={this.onCheck}
|
||||
onExpand={this.onExpand}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
return <>
|
||||
<style>{`
|
||||
.ant-tree-switcher{width:76px!important;flex:0 0 76px!important;display:inline-flex!important;align-items:center;justify-content:center}
|
||||
.tree-switcher-custom{width:76px;height:24px;display:inline-flex;align-items:center;justify-content:center;gap:4px;user-select:none}
|
||||
.tree-switcher-btn{width:20px;height:20px;padding:0;margin:0;border:0;background:transparent;border-radius:4px;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;font-size:12px;line-height:20px}
|
||||
.tree-switcher-btn:hover{background:rgba(0,0,0,.06)}
|
||||
`}</style>
|
||||
<Tree {...treeProps} autoExpandParent={false} expandedKeys={this.expandedKeys}
|
||||
switcherIcon={this.render_switcher}
|
||||
onExpand={keys => { this.expandedKeys = keys as React.Key[]; this.flush(); }}/>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ function create_webgl_context(canvas: HTMLCanvasElement, name: WebGL_Context_Nam
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export function can_create_webgl_context(): boolean {
|
||||
function can_create_webgl_context(): boolean {
|
||||
if (typeof document === "undefined") return false;
|
||||
const canvas = document.createElement("canvas");
|
||||
const options: WebGL_Context_Options = {alpha: false, antialias: false, depth: true, stencil: false, failIfMajorPerformanceCaveat: true, powerPreference: "high-performance"};
|
||||
|
||||
+12
-52
@@ -1,62 +1,22 @@
|
||||
import React, {useLayoutEffect, useRef} from 'react';
|
||||
|
||||
import type {CSSProperties, ReactNode} from "react";
|
||||
import {useLayoutEffect, useRef} from "react";
|
||||
|
||||
type PersistentScrollProps = {
|
||||
children: React.ReactNode;
|
||||
|
||||
/** 用于区分不同内容的 key(tab / 模块) */
|
||||
children: ReactNode;
|
||||
scrollKey: string;
|
||||
|
||||
/** 可选:监听滚动变化 */
|
||||
onScrollChange?: (scrollTop: number) => void;
|
||||
|
||||
style?: React.CSSProperties;
|
||||
style?: CSSProperties;
|
||||
};
|
||||
|
||||
/** 组件级滚动缓存(不落地、不跨刷新) */
|
||||
const scrollCache: Record<string, number> = {};
|
||||
|
||||
export const PersistentScroll: React.FC<PersistentScrollProps> = ({
|
||||
children,
|
||||
scrollKey,
|
||||
onScrollChange,
|
||||
style,
|
||||
}) => {
|
||||
export function PersistentScroll({children, scrollKey, style}: PersistentScrollProps) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
/** 渲染后恢复滚动位置 */
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
const top = scrollCache[scrollKey] ?? 0;
|
||||
el.scrollTop = top;
|
||||
if (ref.current) ref.current.scrollTop = scrollCache[scrollKey] ?? 0;
|
||||
}, [scrollKey]);
|
||||
|
||||
/** 记录滚动 */
|
||||
const handleScroll = () => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
|
||||
scrollCache[scrollKey] = el.scrollTop;
|
||||
onScrollChange?.(el.scrollTop);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div
|
||||
ref={ref}
|
||||
onScroll={handleScroll}
|
||||
style={{
|
||||
gap: '8px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'auto',
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return <div ref={ref}
|
||||
onScroll={event => scrollCache[scrollKey] = event.currentTarget.scrollTop}
|
||||
style={{gap: 8, width: "100%", height: "100%", overflow: "auto", ...style}}>
|
||||
{children}
|
||||
</div>;
|
||||
}
|
||||
|
||||
+46
-106
@@ -1,140 +1,80 @@
|
||||
import {Button, Form, Input, Modal, Switch} from "antd";
|
||||
import enhance from "./core/enhance.tsx";
|
||||
import {ReloadOutlined, SettingOutlined} from "@ant-design/icons";
|
||||
import {Button, Form, Input, Modal, Switch} from "antd";
|
||||
import type {ChangeEvent} from "react";
|
||||
import enhance from "./core/enhance.tsx";
|
||||
|
||||
export class Refresh extends enhance.Base {
|
||||
isAutoRefresh : boolean = true;
|
||||
isModalVisible : boolean = false;
|
||||
refreshInterval : number = 1;
|
||||
autoRefreshTimer = null; // 用于存储定时器ID
|
||||
refresh : () => void
|
||||
isAutoRefresh = true;
|
||||
isModalVisible = false;
|
||||
refreshInterval = 1;
|
||||
autoRefreshTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
constructor(refresh : () => void) {
|
||||
constructor(readonly refresh: () => void) {
|
||||
super();
|
||||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
on_mount() {
|
||||
this.toggleAutoRefresh(this.isAutoRefresh)
|
||||
this.toggleAutoRefresh(this.isAutoRefresh);
|
||||
}
|
||||
|
||||
on_un_mount() {
|
||||
if (this.isAutoRefresh) {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
|
||||
// 切换自动刷新模式
|
||||
toggleAutoRefresh = (checked) => {
|
||||
toggleAutoRefresh = (checked: boolean) => {
|
||||
this.isAutoRefresh = checked;
|
||||
if (this.isAutoRefresh) {
|
||||
this.startAutoRefresh();
|
||||
} else {
|
||||
this.stopAutoRefresh();
|
||||
}
|
||||
this.flush(); // 更新界面
|
||||
}
|
||||
if (checked) this.startAutoRefresh();
|
||||
else this.stopAutoRefresh();
|
||||
this.flush();
|
||||
};
|
||||
|
||||
// 启动自动刷新定时器
|
||||
startAutoRefresh = () => {
|
||||
if (this.autoRefreshTimer) {
|
||||
clearInterval(this.autoRefreshTimer); // 清除之前的定时器
|
||||
}
|
||||
this.stopAutoRefresh();
|
||||
this.autoRefreshTimer = setInterval(this.refresh, this.refreshInterval * 1000);
|
||||
};
|
||||
|
||||
// 每隔 refreshInterval 秒进行一次刷新
|
||||
this.autoRefreshTimer = setInterval(() => {
|
||||
this.refresh(); // 调用手动刷新的方法
|
||||
}, this.refreshInterval * 1000); // 以秒为单位
|
||||
}
|
||||
|
||||
// 停止自动刷新定时器
|
||||
stopAutoRefresh = () => {
|
||||
if (this.autoRefreshTimer) {
|
||||
clearInterval(this.autoRefreshTimer); // 清除定时器
|
||||
this.autoRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
if (this.autoRefreshTimer) clearInterval(this.autoRefreshTimer);
|
||||
this.autoRefreshTimer = null;
|
||||
};
|
||||
|
||||
// 显示设置对话框
|
||||
showModal = () => {
|
||||
this.isModalVisible = true;
|
||||
this.flush(); // 更新界面
|
||||
this.flush();
|
||||
};
|
||||
|
||||
// 关闭设置对话框
|
||||
handleCancel = () => {
|
||||
closeModal = () => {
|
||||
this.isModalVisible = false;
|
||||
this.flush(); // 更新界面
|
||||
this.flush();
|
||||
};
|
||||
|
||||
// 确认设置并关闭对话框
|
||||
handleOk = () => {
|
||||
this.isModalVisible = false;
|
||||
this.flush(); // 更新界面
|
||||
if (this.isAutoRefresh) {
|
||||
this.startAutoRefresh(); // 如果启用了自动刷新,启动定时器
|
||||
} else {
|
||||
this.stopAutoRefresh(); // 否则停止定时器
|
||||
}
|
||||
this.closeModal();
|
||||
if (this.isAutoRefresh) this.startAutoRefresh();
|
||||
};
|
||||
|
||||
// 输入框值的变化
|
||||
handleIntervalChange = (e) => {
|
||||
this.refreshInterval = Number(e.target.value);
|
||||
this.flush(); // 更新界面
|
||||
if (this.isAutoRefresh) {
|
||||
this.startAutoRefresh(); // 如果启用了自动刷新,重新启动定时器
|
||||
}
|
||||
handleIntervalChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.refreshInterval = Math.max(1, Number(event.target.value) || 1);
|
||||
if (this.isAutoRefresh) this.startAutoRefresh();
|
||||
this.flush();
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
type="default"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => {
|
||||
this.refresh(); // 手动刷新
|
||||
}}
|
||||
>
|
||||
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="default"
|
||||
icon={<SettingOutlined />}
|
||||
onClick={this.showModal} // 打开设置对话框
|
||||
>
|
||||
|
||||
</Button>
|
||||
|
||||
{/* 设置对话框 */}
|
||||
<Modal
|
||||
title="设置刷新属性"
|
||||
open={this.isModalVisible}
|
||||
onOk={this.handleOk}
|
||||
onCancel={this.handleCancel}
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="自动刷新:">
|
||||
<Switch
|
||||
checked={this.isAutoRefresh}
|
||||
onChange={this.toggleAutoRefresh}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="刷新间隔(秒)">
|
||||
<Input
|
||||
type="number"
|
||||
value={this.refreshInterval}
|
||||
onChange={this.handleIntervalChange}
|
||||
min={1}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
return <div>
|
||||
<Button icon={<ReloadOutlined/>} onClick={this.refresh}/>
|
||||
<Button icon={<SettingOutlined/>} onClick={this.showModal}/>
|
||||
<Modal title="设置刷新属性" open={this.isModalVisible} onOk={this.handleOk}
|
||||
onCancel={this.closeModal} okText="确定" cancelText="取消">
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="自动刷新:">
|
||||
<Switch checked={this.isAutoRefresh} onChange={this.toggleAutoRefresh}/>
|
||||
</Form.Item>
|
||||
<Form.Item label="刷新间隔(秒)">
|
||||
<Input type="number" value={this.refreshInterval} min={1}
|
||||
onChange={this.handleIntervalChange}/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,6 +3,6 @@ import {Adminive_Settings} from "./Adminive/Adminive_Settings.tsx"
|
||||
import {PersistentScroll} from "./PersistentScroll.tsx"
|
||||
export class Settings extends enhance.Base {
|
||||
render() {
|
||||
return <PersistentScroll scrollKey="Settings" style={{gap: "16px"}}><Adminive_Settings /><div style={{height: 500}} /></PersistentScroll>
|
||||
return <PersistentScroll scrollKey="Settings"><Adminive_Settings/></PersistentScroll>
|
||||
}
|
||||
}
|
||||
|
||||
+13
-52
@@ -1,72 +1,33 @@
|
||||
import React, {useEffect, useState} from "react";
|
||||
|
||||
let _flush = () => {
|
||||
|
||||
}
|
||||
import {useEffect, useState} from "react";
|
||||
|
||||
const idle_flush = () => {};
|
||||
|
||||
class Base {
|
||||
mounted: boolean = false;
|
||||
flush = _flush
|
||||
mounted = false;
|
||||
flush = idle_flush;
|
||||
|
||||
on_mount() {}
|
||||
on_un_mount() {}
|
||||
use_hook() {}
|
||||
render(_props?: any) { return <h5>==Base==</h5>; }
|
||||
|
||||
|
||||
before_first_render() {
|
||||
};
|
||||
|
||||
on_mount() {
|
||||
};
|
||||
|
||||
on_un_mount() {
|
||||
};
|
||||
|
||||
// 用于使用自定义hook
|
||||
use_hook() {
|
||||
}
|
||||
|
||||
// 这些函数不用箭头函数是为了父类可以通过super调用
|
||||
render(_props?: any) {
|
||||
return <h5>==Base==</h5>
|
||||
}
|
||||
|
||||
|
||||
// x是一个函数组件,x代表jsx或者tsx 这个目前只能写成箭头函数不知道为什么
|
||||
x = (props: any) => {
|
||||
let [flush, setFlush] = useState(0);
|
||||
if (this.flush === _flush) {
|
||||
this.flush = (): void => {
|
||||
if (flush !== Number.MAX_SAFE_INTEGER) {
|
||||
// 使用函数式更新来确保每次更新都基于最新的 `prevFlush` 值。
|
||||
// 这样可以避免由于 React 状态更新是异步的而导致的状态不一致问题。
|
||||
// `prevFlush` 是当前最新的 `flush` 状态,它会被 React 自动传递给更新函数,确保每次都使用最新的状态值。
|
||||
// 通过返回 `prevFlush + 1`,更新后的 `flush` 状态将会是前一个值加 1。
|
||||
setFlush((prevFlush: number): number => prevFlush + 1);
|
||||
} else {
|
||||
setFlush(0);
|
||||
}
|
||||
};
|
||||
this.before_first_render();
|
||||
const [, set_revision] = useState(0);
|
||||
if (this.flush === idle_flush) {
|
||||
this.flush = () => set_revision(value => value === Number.MAX_SAFE_INTEGER ? 0 : value + 1);
|
||||
}
|
||||
this.use_hook();
|
||||
useEffect(() => {
|
||||
this.mounted = true;
|
||||
this.on_mount();
|
||||
|
||||
return () => {
|
||||
this.mounted = false;
|
||||
this.on_un_mount();
|
||||
this.flush = _flush;
|
||||
this.flush = idle_flush;
|
||||
};
|
||||
}, []);
|
||||
return this.render(props);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default {
|
||||
Base
|
||||
};
|
||||
|
||||
|
||||
|
||||
export default {Base};
|
||||
|
||||
Reference in New Issue
Block a user