修复bug
This commit is contained in:
+19
-4
@@ -21,6 +21,7 @@ import {Navigate, Router} from "react-router";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {baseURL} from "./Global.tsx";
|
import {baseURL} from "./Global.tsx";
|
||||||
import {is_cesium_webgl_available, webgl_unavailable_message} from "./Map/WebGL_Support.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";
|
||||||
|
|
||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ class App extends enhance.Base {
|
|||||||
setting: Settings | null = null;
|
setting: Settings | null = null;
|
||||||
aircraft_list: Aircraft_List | null = null;
|
aircraft_list: Aircraft_List | null = null;
|
||||||
dsp: DSP | null = null;
|
dsp: DSP | null = null;
|
||||||
|
data_source_config: Data_Source_Config | null = null;
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
navigate: NavigateFunction
|
navigate: NavigateFunction
|
||||||
@@ -106,6 +108,7 @@ class App extends enhance.Base {
|
|||||||
this.leaflet_map = new Leaflet_Map();
|
this.leaflet_map = new Leaflet_Map();
|
||||||
this.aircraft_list = new Aircraft_List();
|
this.aircraft_list = new Aircraft_List();
|
||||||
this.dsp = new DSP();
|
this.dsp = new DSP();
|
||||||
|
this.data_source_config = new Data_Source_Config(this.on_data_sources_changed);
|
||||||
|
|
||||||
// 延迟初始化Settings,避免“初始化前访问”错误
|
// 延迟初始化Settings,避免“初始化前访问”错误
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -114,6 +117,20 @@ class App extends enhance.Base {
|
|||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
on_data_sources_changed = () => {
|
||||||
|
if (!this.data_source_config) return;
|
||||||
|
|
||||||
|
if (this.leaflet_map) {
|
||||||
|
this.leaflet_map.data_source_config = this.data_source_config;
|
||||||
|
this.leaflet_map.data_source_show.data_source_config = this.data_source_config;
|
||||||
|
this.leaflet_map.data_source_show.flush();
|
||||||
|
this.leaflet_map.flush();
|
||||||
|
}
|
||||||
|
this.aircraft_list?.data_sources_changed();
|
||||||
|
this.cesium_map?.request_sync_data_sources?.();
|
||||||
|
this.flush();
|
||||||
|
};
|
||||||
|
|
||||||
use_hook() {
|
use_hook() {
|
||||||
this.navigate = useNavigate();
|
this.navigate = useNavigate();
|
||||||
this.location = useLocation();
|
this.location = useLocation();
|
||||||
@@ -138,9 +155,7 @@ class App extends enhance.Base {
|
|||||||
};
|
};
|
||||||
|
|
||||||
get_Data_Source(key: string) {
|
get_Data_Source(key: string) {
|
||||||
// ************************ 优化:添加空值保护,避免报错 ************************
|
return this.data_source_config?.map(key) ?? null;
|
||||||
if (!this.setting) return null;
|
|
||||||
return this.setting.data_source_config.map(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick: MenuProps['onClick'] = (e) => {
|
onClick: MenuProps['onClick'] = (e) => {
|
||||||
@@ -174,7 +189,7 @@ class App extends enhance.Base {
|
|||||||
|
|
||||||
render(props: any) {
|
render(props: any) {
|
||||||
// ************************ 优化:渲染前校验子组件是否初始化完成,避免报错 ************************
|
// ************************ 优化:渲染前校验子组件是否初始化完成,避免报错 ************************
|
||||||
if (!this.leaflet_map || !this.setting || !this.aircraft_list || !this.dsp) {
|
if (!this.leaflet_map || !this.setting || !this.aircraft_list || !this.dsp || !this.data_source_config) {
|
||||||
return <div style={{textAlign: 'center', padding: '20px'}}>组件初始化中...</div>;
|
return <div style={{textAlign: 'center', padding: '20px'}}>组件初始化中...</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -555,8 +555,8 @@ class Aircraft_Stream_Client {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
source_payloads() {
|
source_payloads() {
|
||||||
const list = app.setting?.data_source_config.list.list || [];
|
const list = app.data_source_config?.enabled() ?? [];
|
||||||
return list.filter((ds: Data_Source) => ds.enable).map((ds: Data_Source) => this.source_payload(ds));
|
return list.map((ds: Data_Source) => this.source_payload(ds));
|
||||||
}
|
}
|
||||||
source_payload(ds: Data_Source) {
|
source_payload(ds: Data_Source) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
import axios from "axios"
|
import axios from "axios"
|
||||||
import {app} from "../App.tsx"
|
import {
|
||||||
import {List_Data} from "../Global.tsx"
|
Data_Source,
|
||||||
import {Data_Source, Dll_Data_Source, File_Data_Source, Serial_Data_Source, Shared_Memory_Data_Source, TCP_Client_Data_Source} from "./Data_Source.tsx"
|
Dll_Data_Source,
|
||||||
|
File_Data_Source,
|
||||||
|
Serial_Data_Source,
|
||||||
|
Shared_Memory_Data_Source,
|
||||||
|
TCP_Client_Data_Source
|
||||||
|
} from "./Data_Source.tsx"
|
||||||
|
|
||||||
type Adminive_Data_Source_Row = {
|
type Adminive_Data_Source_Row = {
|
||||||
id: number
|
id: number
|
||||||
key: string
|
key: string
|
||||||
@@ -10,6 +16,7 @@ type Adminive_Data_Source_Row = {
|
|||||||
config: Record<string, unknown>
|
config: Record<string, unknown>
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
function create_data_source(type: string): Data_Source {
|
function create_data_source(type: string): Data_Source {
|
||||||
if (type === "Serial_Data_Source")
|
if (type === "Serial_Data_Source")
|
||||||
return new Serial_Data_Source()
|
return new Serial_Data_Source()
|
||||||
@@ -21,33 +28,62 @@ function create_data_source(type: string): Data_Source {
|
|||||||
return new Dll_Data_Source()
|
return new Dll_Data_Source()
|
||||||
if (type === "Shared_Memory_Data_Source")
|
if (type === "Shared_Memory_Data_Source")
|
||||||
return new Shared_Memory_Data_Source()
|
return new Shared_Memory_Data_Source()
|
||||||
throw new Error(`未知数据源类型: ${type}`)
|
|
||||||
|
// 未知类型仍按公共数据源模型展示。新增后端类型不应让整个前端崩溃;
|
||||||
|
// 只有确实需要类型专属行为时,前端才需要增加对应实现。
|
||||||
|
return new Data_Source()
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Data_Source_Config {
|
export class Data_Source_Config {
|
||||||
list = new List_Data()
|
private sources: Data_Source[] = []
|
||||||
map(key: string): Data_Source | undefined {
|
loading = true
|
||||||
return this.list.list.find((item: Data_Source) => item.key === key)
|
error = ""
|
||||||
}
|
|
||||||
constructor() {
|
constructor(private readonly on_change?: () => void) {
|
||||||
window.addEventListener("ecap-data-sources-changed", () => void this.refresh())
|
window.addEventListener("ecap-data-sources-changed", () => void this.refresh())
|
||||||
void this.refresh()
|
void this.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
all(): readonly Data_Source[] {
|
||||||
|
return this.sources
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled(): readonly Data_Source[] {
|
||||||
|
return this.sources.filter(source => source.enable)
|
||||||
|
}
|
||||||
|
|
||||||
|
map(key: string): Data_Source | undefined {
|
||||||
|
return this.sources.find(item => item.key === key)
|
||||||
|
}
|
||||||
|
|
||||||
async refresh() {
|
async refresh() {
|
||||||
const response = await axios.get("/api/adminive/config/data_sources", {params: {page: 1, perPage: 1000}})
|
try {
|
||||||
const rows = response.data?.data?.items as Adminive_Data_Source_Row[] ?? []
|
const response = await axios.get("/api/adminive/config/data_sources", {
|
||||||
this.list.list = rows.map(row => {
|
params: {page: 1, perPage: 1000}
|
||||||
const source = create_data_source(row.type)
|
})
|
||||||
const {id, config, state, ...data} = row
|
const rows = response.data?.data?.items
|
||||||
Object.assign(source, config, data)
|
if (!Array.isArray(rows)) {
|
||||||
source.adminive_id = id
|
throw new Error("数据源接口未返回 data.items 数组")
|
||||||
source.normalize_map_display()
|
}
|
||||||
return source
|
|
||||||
})
|
const previous_sources = new Map(this.sources.map(source => [source.key, source]))
|
||||||
if (app.leaflet_map) {
|
this.sources = (rows as Adminive_Data_Source_Row[]).map(row => {
|
||||||
app.leaflet_map.data_source_show.data_source_config = this
|
const previous = previous_sources.get(row.key)
|
||||||
app.leaflet_map.data_source_config = this
|
const source = previous?.type === row.type ? previous : create_data_source(row.type)
|
||||||
app.leaflet_map.data_source_show.flush()
|
const {id, config, state, ...data} = row
|
||||||
|
void state
|
||||||
|
Object.assign(source, config ?? {}, data)
|
||||||
|
source.adminive_id = id
|
||||||
|
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?.()
|
||||||
}
|
}
|
||||||
app.aircraft_list?.load_default_data_source()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
CheckboxChangeEvent,
|
CheckboxChangeEvent,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
ColorPickerProps,
|
|
||||||
Dropdown,
|
Dropdown,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
type MenuProps,
|
type MenuProps,
|
||||||
@@ -14,12 +13,10 @@ import {
|
|||||||
Space,
|
Space,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tag,
|
Tag,
|
||||||
theme,
|
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography
|
||||||
} from "antd";
|
} from "antd";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {generate, green, presetPalettes, red} from '@ant-design/colors';
|
|
||||||
import {Data_Source, type Data_Source_Map_Display_Data, type Map_Display_Mode} from "./Data_Source.tsx";
|
import {Data_Source, type Data_Source_Map_Display_Data, type Map_Display_Mode} from "./Data_Source.tsx";
|
||||||
import {Base_Drawer} from "../Base_Drawer.tsx";
|
import {Base_Drawer} from "../Base_Drawer.tsx";
|
||||||
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
|
import {Camera_Control_Mode} from "../Map/Camera_Control_Mode.ts";
|
||||||
@@ -31,21 +28,10 @@ import {default_map_view_config} from "../Map/Map_View.tsx";
|
|||||||
|
|
||||||
const {Title, Text} = Typography;
|
const {Title, Text} = Typography;
|
||||||
|
|
||||||
type Presets = Required<ColorPickerProps>['presets'][number];
|
|
||||||
type Settings_Tab = "tiles" | "view3d" | "source"
|
type Settings_Tab = "tiles" | "view3d" | "source"
|
||||||
|
|
||||||
function genPresets(presets = presetPalettes) {
|
|
||||||
return Object.entries(presets).map<Presets>(([label, colors]) => ({label, colors, key: label}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const Demo: React.FC = () => {
|
|
||||||
const {token} = theme.useToken();
|
|
||||||
const presets = genPresets({primary: generate(token.colorPrimary), red, green});
|
|
||||||
return <ColorPicker presets={presets} defaultValue="#1677ff"/>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Data_Source_Show extends enhance.Base {
|
export class Data_Source_Show extends enhance.Base {
|
||||||
data_source_config: Data_Source_Config = null
|
data_source_config: Data_Source_Config | null = null
|
||||||
drawer = new Base_Drawer()
|
drawer = new Base_Drawer()
|
||||||
active_data_source_key = ""
|
active_data_source_key = ""
|
||||||
active_tab_key: Settings_Tab = "source"
|
active_tab_key: Settings_Tab = "source"
|
||||||
@@ -446,7 +432,7 @@ export class Data_Source_Show extends enhance.Base {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
settings_tab_items(mode: Map_Display_Mode, enabled_list: Data_Source[]) {
|
settings_tab_items(mode: Map_Display_Mode, enabled_list: readonly Data_Source[]) {
|
||||||
const items: { key: Settings_Tab, label: string, disabled?: boolean }[] = [{
|
const items: { key: Settings_Tab, label: string, disabled?: boolean }[] = [{
|
||||||
key: "source",
|
key: "source",
|
||||||
label: "子数据源",
|
label: "子数据源",
|
||||||
@@ -464,7 +450,7 @@ export class Data_Source_Show extends enhance.Base {
|
|||||||
return this.render_tile_controls(mode);
|
return this.render_tile_controls(mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
render_header_tabs(mode: Map_Display_Mode, enabled_list: Data_Source[]) {
|
render_header_tabs(mode: Map_Display_Mode, enabled_list: readonly Data_Source[]) {
|
||||||
if (mode !== "map3d" && this.active_tab_key === "view3d") {
|
if (mode !== "map3d" && this.active_tab_key === "view3d") {
|
||||||
this.active_tab_key = "tiles";
|
this.active_tab_key = "tiles";
|
||||||
}
|
}
|
||||||
@@ -654,8 +640,7 @@ export class Data_Source_Show extends enhance.Base {
|
|||||||
if (mode !== "map3d" && this.active_tab_key === "view3d") {
|
if (mode !== "map3d" && this.active_tab_key === "view3d") {
|
||||||
this.active_tab_key = "tiles";
|
this.active_tab_key = "tiles";
|
||||||
}
|
}
|
||||||
const source_list = this.data_source_config.list.list as Data_Source[];
|
const enabled_list = this.data_source_config.enabled();
|
||||||
const enabled_list = source_list.filter((ds: Data_Source) => ds.enable);
|
|
||||||
if (enabled_list.length > 0 && !enabled_list.some((ds: Data_Source) => ds.key === this.active_data_source_key)) {
|
if (enabled_list.length > 0 && !enabled_list.some((ds: Data_Source) => ds.key === this.active_data_source_key)) {
|
||||||
this.active_data_source_key = enabled_list[0].key;
|
this.active_data_source_key = enabled_list[0].key;
|
||||||
}
|
}
|
||||||
|
|||||||
+210
-533
@@ -1,574 +1,251 @@
|
|||||||
import enhance from "../core/enhance.tsx";
|
import enhance from "../core/enhance.tsx";
|
||||||
import {Button, Select, SelectProps, Table} from 'antd';
|
import {Button, Flex, Input, InputNumber, Layout, Select, Space, Table} from "antd";
|
||||||
import React, {HTMLProps, useRef, useState} from 'react';
|
import React, {HTMLProps} from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {baseURL, Prefix} from "../Global.tsx";
|
import {baseURL, Prefix} from "../Global.tsx";
|
||||||
import {app} from "../App.tsx";
|
import {app} from "../App.tsx";
|
||||||
import {Data_Source} from "../Data_Source/Data_Source.tsx";
|
import {SearchOutlined} from "@ant-design/icons";
|
||||||
import {CaretDownOutlined, CaretUpOutlined} from "@ant-design/icons";
|
import type {TableColumnType} from "antd";
|
||||||
import { Input, Space } from 'antd';
|
import type {FilterDropdownProps} from "antd/es/table/interface";
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
|
||||||
import { Layout, Flex} from 'antd';
|
|
||||||
import { InputNumber } from 'antd';
|
|
||||||
import type { InputRef, TableColumnsType, TableColumnType } from 'antd';
|
|
||||||
import type { FilterDropdownProps } from 'antd/es/table/interface';
|
|
||||||
import Highlighter from 'react-highlight-words';
|
|
||||||
import {Refresh} from "../Refresh.tsx";
|
import {Refresh} from "../Refresh.tsx";
|
||||||
|
|
||||||
export interface Aircraft {
|
type Aircraft_Row = Record<string, unknown> & {key: string};
|
||||||
key?: string;
|
|
||||||
|
|
||||||
// 统计 / 时间
|
type Aircraft_Column_Model = {
|
||||||
times: number;
|
key: string;
|
||||||
uti: number; // 时间戳(秒)
|
label: string;
|
||||||
day_second: number; // 时间戳(秒)
|
type: "index" | "timestamp" | "number" | "text" | "boolean" | string;
|
||||||
ns: number; // 时间戳(纳秒)
|
defaultSortOrder?: "ascend" | "descend";
|
||||||
|
secondsKey?: string;
|
||||||
// 识别信息
|
nanosecondsKey?: string;
|
||||||
hex: string; // ICAO 识别码
|
|
||||||
fli: string; // 航班编号
|
|
||||||
ava: string; // 数据来源标识
|
|
||||||
src: string; // 首选数据源
|
|
||||||
|
|
||||||
// 位置
|
|
||||||
lat: number; // 纬度
|
|
||||||
lon: number; // 经度
|
|
||||||
alt: number; // 高度(米)
|
|
||||||
dis: string; // 距离接收器距离(你现在是字符串)
|
|
||||||
|
|
||||||
// 运动
|
|
||||||
spd: number; // 地速(节)
|
|
||||||
trk: number; // 航向(真)
|
|
||||||
vrt: number; // 垂直速度(ft/min)
|
|
||||||
|
|
||||||
// 飞机状态
|
|
||||||
gda: string; // 空中/地面状态
|
|
||||||
cat: string; // 机型分类
|
|
||||||
org: string; // 起飞机场
|
|
||||||
dst: string; // 目的机场
|
|
||||||
opr: string; // 航空公司
|
|
||||||
typ: string; // 机型
|
|
||||||
reg: string; // 注册号
|
|
||||||
squ: string; // 应答机编码
|
|
||||||
cou: string; // 国家
|
|
||||||
|
|
||||||
// 信号 / 环境
|
|
||||||
tru: number; // 信任值
|
|
||||||
dbm: number; // 信号强度(dBm)
|
|
||||||
lla: number; // 位置数据延迟(秒)
|
|
||||||
tmp: number; // 温度(℃)
|
|
||||||
wsp: number; // 风速(节)
|
|
||||||
wdi: number; // 风向(°)
|
|
||||||
|
|
||||||
// ADS-B / 系统
|
|
||||||
mop: number; // ADS-B 协议版本
|
|
||||||
spi: number; // 识别应答(SPI)
|
|
||||||
alr: number; // 告警状态
|
|
||||||
ias: number; // 指示空速
|
|
||||||
tas: number; // 真空速
|
|
||||||
hdgm: number; // 磁航向
|
|
||||||
hdgt: number; // 真航向
|
|
||||||
qnhs: number; // QNH
|
|
||||||
alts: number; // 选择高度
|
|
||||||
pic: number; // Asterix PIC
|
|
||||||
tcm: number; // 防撞系统状态
|
|
||||||
ape: number; // 自动驾驶启用
|
|
||||||
sil: number; // 系统完整性等级
|
|
||||||
sda: number; // 系统设计保证
|
|
||||||
nacp: number; // 位置精度
|
|
||||||
pest: number; // 位置估算次数
|
|
||||||
nocl: number; // 接收机数
|
|
||||||
tq: number; // 时间质量
|
|
||||||
|
|
||||||
// 额外字段聚合成时间
|
|
||||||
formattedDate
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const getColumnRangeSearchProps = (field: string): TableColumnType<any> => ({
|
|
||||||
filterDropdown: (props: FilterDropdownProps) => {
|
|
||||||
const { setSelectedKeys, selectedKeys, confirm, clearFilters } = props;
|
|
||||||
|
|
||||||
// selectedKeys 中用字符串保存区间:"min,max"
|
|
||||||
const raw = selectedKeys[0] as string | undefined;
|
|
||||||
|
|
||||||
// 关键点:'' → undefined(而不是 Number('') === 0)
|
|
||||||
const [min, max] = raw
|
|
||||||
? raw.split(',').map(v => (v === '' ? undefined : Number(v)))
|
|
||||||
: [undefined, undefined];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
|
||||||
<InputNumber
|
|
||||||
placeholder="最小值"
|
|
||||||
value={min}
|
|
||||||
onChange={(v) =>
|
|
||||||
setSelectedKeys([`${v ?? ''},${max ?? ''}`])
|
|
||||||
}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
<InputNumber
|
|
||||||
placeholder="最大值"
|
|
||||||
value={max}
|
|
||||||
onChange={(v) =>
|
|
||||||
setSelectedKeys([`${min ?? ''},${v ?? ''}`])
|
|
||||||
}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="small"
|
|
||||||
onClick={() => confirm()}
|
|
||||||
>
|
|
||||||
确定
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
onClick={() => {
|
|
||||||
clearFilters?.();
|
|
||||||
confirm();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
filterIcon: (filtered: boolean) => (
|
|
||||||
<SearchOutlined style={{ color: filtered ? '#1677ff' : undefined }} />
|
|
||||||
),
|
|
||||||
|
|
||||||
filterSearch: true,
|
|
||||||
filterMultiple: true,
|
|
||||||
|
|
||||||
onFilter: (value, record) => {
|
|
||||||
if (typeof value !== 'string') return true;
|
|
||||||
|
|
||||||
const [minStr, maxStr] = value.split(',');
|
|
||||||
const min = minStr !== '' ? Number(minStr) : undefined;
|
|
||||||
const max = maxStr !== '' ? Number(maxStr) : undefined;
|
|
||||||
|
|
||||||
// 🔑 核心:都没设 → 不过滤
|
|
||||||
if (min == null && max == null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const v = record[field];
|
|
||||||
if (v == null) return false;
|
|
||||||
|
|
||||||
if (min != null && v < min) return false;
|
|
||||||
if (max != null && v > max) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const withTextSearch = (field: string) => ({
|
|
||||||
filterDropdown: (props: FilterDropdownProps) => {
|
|
||||||
const {
|
|
||||||
setSelectedKeys,
|
|
||||||
selectedKeys,
|
|
||||||
confirm,
|
|
||||||
clearFilters,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 8 }}>
|
|
||||||
<Input
|
|
||||||
placeholder={`搜索 ${field}`}
|
|
||||||
value={selectedKeys[0]}
|
|
||||||
onChange={e =>
|
|
||||||
setSelectedKeys(e.target.value ? [e.target.value] : [])
|
|
||||||
}
|
|
||||||
onPressEnter={() => confirm()}
|
|
||||||
style={{ marginBottom: 8, display: 'block' }}
|
|
||||||
/>
|
|
||||||
<Space>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
onClick={() => confirm()}
|
|
||||||
icon={<SearchOutlined />}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
搜索
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => clearFilters?.()}
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
重置
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
|
|
||||||
filterIcon: (filtered: boolean) => (
|
|
||||||
<SearchOutlined style={{ color: filtered ? '#1677ff' : undefined }} />
|
|
||||||
),
|
|
||||||
|
|
||||||
onFilter: (value, record: any) =>
|
|
||||||
String(record[field] ?? '')
|
|
||||||
.toLowerCase()
|
|
||||||
.includes(String(value).toLowerCase()),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const str_sorter = (field: string) => {
|
|
||||||
return (a: any, b: any) => {
|
|
||||||
const va = a[field];
|
|
||||||
const vb = b[field];
|
|
||||||
|
|
||||||
// null / undefined 处理
|
|
||||||
if (va == null && vb == null) return 0;
|
|
||||||
if (va == null) return -1;
|
|
||||||
if (vb == null) return 1;
|
|
||||||
|
|
||||||
// 只对 string 做字符串排序
|
|
||||||
if (typeof va === 'string' && typeof vb === 'string') {
|
|
||||||
return va.localeCompare(vb);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兜底:强转为字符串(防脏数据)
|
|
||||||
return String(va).localeCompare(String(vb));
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const int_sorter = (field: string) => {
|
|
||||||
return (a: any, b: any) => {
|
|
||||||
// 处理空值(假设空值是 `null` 或 `undefined`,可以根据需要修改处理方式)
|
|
||||||
if (a[field] == null) return -1; // 如果 a[field] 为 null 或 undefined,认为它在 b 之前
|
|
||||||
if (b[field] == null) return 1; // 如果 b[field] 为 null 或 undefined,认为它在 a 之前
|
|
||||||
return a[field] - b[field]; // 数字排序
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onHeaderCell = (): HTMLProps<HTMLTableCellElement> => ({
|
const onHeaderCell = (): HTMLProps<HTMLTableCellElement> => ({
|
||||||
style: {
|
style: {
|
||||||
writingMode: 'vertical-lr',
|
writingMode: "vertical-lr",
|
||||||
textOrientation: 'upright',
|
textOrientation: "upright",
|
||||||
paddingLeft: '0px', // 调整左右内边距
|
paddingLeft: 0,
|
||||||
paddingRight: '0px',
|
paddingRight: 0,
|
||||||
// paddingTop: '0px',
|
|
||||||
// paddingBottom: '0px',
|
|
||||||
// textAlign: 'center',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// const onHeaderCell = (): HTMLProps<HTMLTableCellElement> => ({
|
|
||||||
// style: {
|
|
||||||
// writingMode: 'vertical-rl' as React.CSSProperties['writingMode'],
|
|
||||||
// whiteSpace: 'nowrap' as React.CSSProperties['whiteSpace'],
|
|
||||||
// textAlign: 'center' as 'center', // 强制设置为 'center' 类型
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
|
|
||||||
|
function render_column_title(label: string) {
|
||||||
const get_handle = (line2 : string)=>{
|
return <div>{label.split(/(<[^>]+>)/g).filter(Boolean).map((part, index) =>
|
||||||
return <div>
|
part.startsWith("<") && part.endsWith(">")
|
||||||
{
|
? <span style={{writingMode: "horizontal-tb"}} key={index}>{part.slice(1, -1)}</span>
|
||||||
line2.split(/(<[^>]+>)/g).map((part, index) =>
|
: <span key={index}>{part}</span>
|
||||||
part.startsWith('<') && part.endsWith('>') ? (
|
)}</div>;
|
||||||
// 如果是< >之间的内容,进行处理
|
|
||||||
<span style={{
|
|
||||||
writingMode: 'horizontal-tb',
|
|
||||||
// textOrientation: 'upright',
|
|
||||||
}} key={index}> {part.slice(1, -1)}</span> // 去掉< >并包裹在span中
|
|
||||||
) : (
|
|
||||||
// 其他内容保持不变
|
|
||||||
<span key={index}>{part}</span>
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function compare_values(a: unknown, b: unknown): number {
|
||||||
|
if (a == null && b == null) return 0;
|
||||||
|
if (a == null) return -1;
|
||||||
|
if (b == null) return 1;
|
||||||
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
||||||
|
return String(a).localeCompare(String(b));
|
||||||
|
}
|
||||||
|
|
||||||
const create_int = (
|
const range_filter = (field: string): TableColumnType<Aircraft_Row> => ({
|
||||||
title: string | React.ReactNode, // 修改为支持字符串和 React 节点
|
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}: FilterDropdownProps) => {
|
||||||
field: string,
|
const raw = selectedKeys[0] as string | undefined;
|
||||||
extra: Partial<any> = {}
|
const [minimum, maximum] = raw
|
||||||
) => {
|
? raw.split(",").map(value => value === "" ? undefined : Number(value))
|
||||||
// 判断 title 是否是字符串类型
|
: [undefined, undefined];
|
||||||
const isTitleString = typeof title === 'string';
|
return <div style={{padding: 8}}>
|
||||||
|
<Space direction="vertical" style={{width: "100%"}}>
|
||||||
return {
|
<InputNumber placeholder="最小值" value={minimum}
|
||||||
title: isTitleString ? get_handle(title) : title, // 如果是字符串,处理,否则直接使用
|
onChange={value => setSelectedKeys([`${value ?? ""},${maximum ?? ""}`])}
|
||||||
dataIndex: field,
|
style={{width: "100%"}}/>
|
||||||
key: field,
|
<InputNumber placeholder="最大值" value={maximum}
|
||||||
onHeaderCell: onHeaderCell,
|
onChange={value => setSelectedKeys([`${minimum ?? ""},${value ?? ""}`])}
|
||||||
sorter: int_sorter(field),
|
style={{width: "100%"}}/>
|
||||||
render: (text) => {
|
<Space>
|
||||||
return (
|
<Button type="primary" size="small" onClick={() => confirm()}>确定</Button>
|
||||||
<div style={{ textAlign: 'center'}}>{text}</div>
|
<Button size="small" onClick={() => {
|
||||||
)
|
clearFilters?.();
|
||||||
},
|
confirm();
|
||||||
...getColumnRangeSearchProps(field),
|
}}>重置</Button>
|
||||||
...extra,
|
</Space>
|
||||||
};
|
</Space>
|
||||||
};
|
</div>;
|
||||||
|
|
||||||
|
|
||||||
const create_str = (
|
|
||||||
title: string | React.ReactNode,
|
|
||||||
field: string,
|
|
||||||
extra: Partial<any> = {}
|
|
||||||
) => {
|
|
||||||
const isTitleString = typeof title === 'string';
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: isTitleString ? get_handle(title) : title,
|
|
||||||
dataIndex: field,
|
|
||||||
key: field,
|
|
||||||
sorter: str_sorter(field),
|
|
||||||
render: (text) => {
|
|
||||||
return (
|
|
||||||
<div style={{ textAlign: 'center' }}>{text}</div>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onHeaderCell: onHeaderCell,
|
|
||||||
...withTextSearch(field), // 👈 搜索能力
|
|
||||||
...extra,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const cr2 = (line1: string, line2: string) => {
|
|
||||||
const renderLine1 = get_handle(line1);
|
|
||||||
const renderLine2 = get_handle(line2);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{renderLine1}
|
|
||||||
{renderLine2}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const columns: TableColumnType<Aircraft>[] = [
|
|
||||||
{
|
|
||||||
title: '序号',
|
|
||||||
key: 'index',
|
|
||||||
width: 60,
|
|
||||||
align: 'center',
|
|
||||||
render: (_value, _record, index) => index + 1,
|
|
||||||
},
|
},
|
||||||
{
|
filterIcon: filtered => <SearchOutlined style={{color: filtered ? "#1677ff" : undefined}}/>,
|
||||||
title: '时间',
|
onFilter: (value, record) => {
|
||||||
dataIndex: 'formattedDate',
|
if (typeof value !== "string") return true;
|
||||||
key: 'formattedDate',
|
const [minimum_text, maximum_text] = value.split(",");
|
||||||
onHeaderCell: () => ({
|
const minimum = minimum_text === "" ? undefined : Number(minimum_text);
|
||||||
style: {
|
const maximum = maximum_text === "" ? undefined : Number(maximum_text);
|
||||||
// 设置文本垂直显示
|
const current = Number(record[field]);
|
||||||
writingMode: 'vertical-rl',
|
if (!Number.isFinite(current)) return false;
|
||||||
whiteSpace: 'nowrap', // 防止文字换行
|
return (minimum === undefined || current >= minimum) && (maximum === undefined || current <= maximum);
|
||||||
textAlign: 'center', // 居中对齐
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
sorter: (a1: Aircraft, a2: Aircraft) => {
|
|
||||||
// 首先比较 uti 字段
|
|
||||||
if (a1.uti < a2.uti) {
|
|
||||||
return -1; // a1 排在前面
|
|
||||||
}
|
|
||||||
if (a1.uti > a2.uti) {
|
|
||||||
return 1; // a2 排在前面
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果 uti 相同,再比较 ns 字段
|
|
||||||
if (a1.ns < a2.ns) {
|
|
||||||
return -1; // a1 排在前面
|
|
||||||
}
|
|
||||||
if (a1.ns > a2.ns) {
|
|
||||||
return 1; // a2 排在前面
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0; // 如果 uti 和 ns 都相同,返回 0
|
|
||||||
},
|
|
||||||
... withTextSearch('formattedDate'),
|
|
||||||
// ...extra,
|
|
||||||
render: (text) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
dangerouslySetInnerHTML={{ __html: text }}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
create_int('<mode_s>消息数', 'times'),
|
});
|
||||||
// create_int(cr2('时间戳', '(utc)'), 'uti'),
|
|
||||||
// create_int(cr2('时间戳', '(秒内纳秒数)'), 'ns'),
|
const text_filter = (field: string): TableColumnType<Aircraft_Row> => ({
|
||||||
// create_int(cr2('时间戳', '(当天的秒数)'), 'day_second'),
|
filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}: FilterDropdownProps) =>
|
||||||
create_str(cr2('识别码', '<ICAO>'), 'hex'),
|
<div style={{padding: 8}}>
|
||||||
create_str('航班编号', 'fli'),
|
<Input placeholder={`搜索 ${field}`} value={selectedKeys[0]}
|
||||||
create_str('数据来源标识', 'ava'),
|
onChange={event => setSelectedKeys(event.target.value ? [event.target.value] : [])}
|
||||||
create_str('首选数据源', 'src'),
|
onPressEnter={() => confirm()} style={{marginBottom: 8, display: "block"}}/>
|
||||||
create_int('纬度', 'lat'),
|
<Space>
|
||||||
create_int('经度', 'lon', {defaultSortOrder: 'descend'}),
|
<Button type="primary" icon={<SearchOutlined/>} size="small"
|
||||||
create_int(cr2('高度', '(米)'), 'alt'),
|
onClick={() => confirm()}>搜索</Button>
|
||||||
create_int(cr2('地速','(节)'), 'spd'),
|
<Button size="small" onClick={() => {
|
||||||
create_int(cr2('航向', '(真)'), 'trk'),
|
clearFilters?.();
|
||||||
create_int(cr2('垂直速度', '(<ft/min>)'), 'vrt'),
|
confirm();
|
||||||
create_str(cr2('空中/地面', '状态'), 'gda'),
|
}}>重置</Button>
|
||||||
create_str('机型分类', 'cat'),
|
</Space>
|
||||||
create_str('起飞机场', 'org'),
|
</div>,
|
||||||
create_str('目的机场', 'dst'),
|
filterIcon: filtered => <SearchOutlined style={{color: filtered ? "#1677ff" : undefined}}/>,
|
||||||
create_str('航空公司', 'opr'),
|
onFilter: (value, record) => String(record[field] ?? "").toLowerCase().includes(String(value).toLowerCase()),
|
||||||
create_str('机型', 'typ'),
|
});
|
||||||
create_str('注册号', 'reg'),
|
|
||||||
create_str('应答机编码', 'squ'),
|
function format_timestamp(row: Record<string, unknown>, column: Aircraft_Column_Model): string {
|
||||||
create_str('国家', 'cou'),
|
const seconds = Number(row[column.secondsKey ?? "uti"]);
|
||||||
create_str('接收器距离', 'dis'),
|
if (!Number.isFinite(seconds) || seconds <= 0) return "";
|
||||||
create_int('信任值', 'tru'),
|
const nanoseconds = Number(row[column.nanosecondsKey ?? "ns"]);
|
||||||
create_int(cr2('信号强度', '(<dBm>)'), 'dbm'),
|
const date = new Date(seconds * 1000);
|
||||||
create_int(cr2('位置数据延迟', '(秒)'), 'lla'),
|
const pad = (value: number, length = 2) => String(value).padStart(length, "0");
|
||||||
create_int(cr2('温度', '(℃)'), 'tmp'),
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}\n` +
|
||||||
create_int(cr2('风速', '(节)'), 'wsp'),
|
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(Number.isFinite(nanoseconds) ? nanoseconds : 0, 9)}`;
|
||||||
create_int(cr2('风向', '(°)'), 'wdi'),
|
}
|
||||||
create_int(cr2('ADS-B', ' 协议版本'), 'mop'),
|
|
||||||
create_int(cr2('识别应答', '(<SPI>)'), 'spi'),
|
function normalized_sort_order(value: unknown): "ascend" | "descend" | undefined {
|
||||||
create_int('告警状态', 'alr'),
|
return value === "ascend" || value === "descend" ? value : undefined;
|
||||||
create_int(cr2('指示空速', '(<IAS>)'), 'ias'),
|
}
|
||||||
create_int(cr2('真空速', '(<TAS>)'), 'tas'),
|
|
||||||
create_int('磁航向', 'hdgm'),
|
function create_columns(model: readonly Aircraft_Column_Model[]): TableColumnType<Aircraft_Row>[] {
|
||||||
create_int('真航向', 'hdgt'),
|
return model.map(column => {
|
||||||
create_int(cr2('气压设定', '(<QNH>)'), 'qnhs'),
|
if (column.type === "index") {
|
||||||
create_int('选择高度', 'alts'),
|
return {
|
||||||
create_int('<Asterix PIC>', 'pic'),
|
title: render_column_title(column.label),
|
||||||
create_int('防撞系统状态', 'tcm'),
|
key: column.key,
|
||||||
create_int('自动驾驶启用', 'ape'),
|
width: 60,
|
||||||
create_int(cr2('系统完整性等级', '(<SIL>)'), 'sil'),
|
align: "center",
|
||||||
create_int(cr2('系统设计保证', '(<SDA>)'), 'sda'),
|
render: (_value, _record, index) => index + 1,
|
||||||
create_int(cr2('位置精度', '(<NACp>)'), 'nacp'),
|
};
|
||||||
create_int(cr2('位置估算', '次数每秒'), 'pest'),
|
}
|
||||||
create_int(cr2('提供数据', '的接收机数'), 'nocl'),
|
|
||||||
create_int('时间质量', 'tq'),
|
const common: TableColumnType<Aircraft_Row> = {
|
||||||
];
|
title: render_column_title(column.label),
|
||||||
|
dataIndex: column.key,
|
||||||
|
key: column.key,
|
||||||
|
onHeaderCell,
|
||||||
|
defaultSortOrder: normalized_sort_order(column.defaultSortOrder),
|
||||||
|
sorter: (a, b) => compare_values(a[column.key], b[column.key]),
|
||||||
|
render: value => <div style={{textAlign: "center", whiteSpace: column.type === "timestamp" ? "pre-line" : undefined}}>
|
||||||
|
{value == null ? "" : typeof value === "boolean" ? (value ? "是" : "否") : String(value)}
|
||||||
|
</div>,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
...common,
|
||||||
|
...(column.type === "number" ? range_filter(column.key) : text_filter(column.key)),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallback_model(rows: readonly Record<string, unknown>[]): Aircraft_Column_Model[] {
|
||||||
|
const first = rows[0];
|
||||||
|
if (!first) return [];
|
||||||
|
return [
|
||||||
|
{key: "index", label: "序号", type: "index"},
|
||||||
|
...Object.keys(first).map(key => ({
|
||||||
|
key,
|
||||||
|
label: key,
|
||||||
|
type: typeof first[key] === "number" ? "number" : "text",
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
// https://juejin.cn/post/7166529533330817031 表格优化
|
|
||||||
export class Aircraft_List extends enhance.Base {
|
export class Aircraft_List extends enhance.Base {
|
||||||
dataSource: Aircraft[]
|
dataSource: Aircraft_Row[] = [];
|
||||||
data_source_key: string = null
|
columns: TableColumnType<Aircraft_Row>[] = [];
|
||||||
limit_msg_num: number = 0
|
data_source_key: string | null = null;
|
||||||
|
limit_msg_num = 0;
|
||||||
|
|
||||||
|
refresh_btn = new Refresh(() => this.refresh());
|
||||||
refresh_btn = new Refresh(()=>{
|
|
||||||
this.refresh();
|
|
||||||
})
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
|
if (!this.data_source_key) {
|
||||||
|
this.dataSource = [];
|
||||||
|
this.columns = [];
|
||||||
|
this.flush();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
axios.post(`${baseURL}/aircraft_list`, {
|
axios.post(`${baseURL}/aircraft_list`, {
|
||||||
data_source_key: this.data_source_key,
|
data_source_key: this.data_source_key,
|
||||||
limit_msg_num: this.limit_msg_num
|
limit_msg_num: this.limit_msg_num,
|
||||||
}).then((res) => {
|
include_model: true,
|
||||||
let ret = []
|
}).then(response => {
|
||||||
res.data.forEach((item : Aircraft, index : number) => {
|
const payload = response.data;
|
||||||
|
const raw_rows: Record<string, unknown>[] = Array.isArray(payload?.data?.items)
|
||||||
const date = new Date(item.uti * 1000); // 将秒转换为毫秒
|
? payload.data.items
|
||||||
const nanoSeconds = item.ns;
|
: Array.isArray(payload) ? payload : [];
|
||||||
let daySecond = item.day_second;
|
const model: Aircraft_Column_Model[] = Array.isArray(payload?.model?.columns)
|
||||||
let hours = Math.floor(daySecond / 3600); // 获取小时
|
? payload.model.columns
|
||||||
let minutes = Math.floor((daySecond % 3600) / 60); // 获取分钟
|
: fallback_model(raw_rows);
|
||||||
let seconds = daySecond % 60; // 获取秒数
|
|
||||||
|
|
||||||
const dateString = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`;
|
|
||||||
const timeString = `
|
|
||||||
${hours.toString().padStart(2, '0')}:
|
|
||||||
${minutes.toString().padStart(2, '0')}:
|
|
||||||
${seconds.toString().padStart(2, '0')}.
|
|
||||||
${nanoSeconds.toString().padStart(9, '0')}`
|
|
||||||
const formattedDate = `<span>${dateString}</span><br/><span>${timeString}</span>`;
|
|
||||||
|
|
||||||
item.formattedDate = formattedDate;
|
|
||||||
|
|
||||||
ret.push({
|
|
||||||
...item,
|
|
||||||
key: String(index)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
this.dataSource = raw_rows.map((item, index) => {
|
||||||
|
const row: Aircraft_Row = {...item, key: `${String(item.hex ?? "aircraft")}:${index}`};
|
||||||
|
for (const column of model) {
|
||||||
|
if (column.type === "timestamp") row[column.key] = format_timestamp(row, column);
|
||||||
|
}
|
||||||
|
return row;
|
||||||
});
|
});
|
||||||
this.dataSource = ret;
|
this.columns = create_columns(model);
|
||||||
this.flush();
|
this.flush();
|
||||||
})
|
}).catch(error => {
|
||||||
|
console.error("加载飞机列表失败", error);
|
||||||
|
this.dataSource = [];
|
||||||
|
this.columns = [];
|
||||||
|
this.flush();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
load_default_data_source() {
|
load_default_data_source() {
|
||||||
if (this.data_source_key === null) {
|
const sources = app.data_source_config?.enabled() ?? [];
|
||||||
let ds_list: Data_Source[] = app.setting.data_source_config.list.list;
|
if (!sources.some(source => source.key === this.data_source_key)) {
|
||||||
if (ds_list.length > 0) {
|
this.data_source_key = sources[0]?.key ?? null;
|
||||||
this.data_source_key = ds_list[0].key;
|
this.flush();
|
||||||
this.flush();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
on_mount() {
|
|
||||||
this.load_default_data_source()
|
data_sources_changed() {
|
||||||
this.refresh()
|
this.load_default_data_source();
|
||||||
|
if (this.mounted) this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
on_mount() {
|
||||||
|
this.load_default_data_source();
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
render(props: any) {
|
render() {
|
||||||
return (
|
return <Layout style={{height: "100%"}}>
|
||||||
<Layout style={{ height: '100%' }}>
|
<Flex gap="middle" vertical>
|
||||||
<Flex gap="middle" vertical>
|
<Flex gap="middle">
|
||||||
|
<Select prefix={<Prefix label="数据来源"/>}
|
||||||
<Flex gap="middle">
|
|
||||||
|
|
||||||
|
|
||||||
<Select
|
|
||||||
prefix={<Prefix label="数据来源"/>}
|
|
||||||
placeholder="请选择数据来源"
|
placeholder="请选择数据来源"
|
||||||
value={this.data_source_key}
|
value={this.data_source_key}
|
||||||
options={(() => {
|
options={(app.data_source_config?.enabled() ?? []).map(item => ({
|
||||||
let options: SelectProps['options'] = [];
|
value: item.key,
|
||||||
let t = app.setting.data_source_config;
|
label: item.key,
|
||||||
t.list.list.forEach((item: Data_Source) => {
|
}))}
|
||||||
if (item.enable) {
|
onChange={value => {
|
||||||
options.push({
|
|
||||||
value: item.key,
|
|
||||||
label: item.key
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return options;
|
|
||||||
})()}
|
|
||||||
onChange={(value) => {
|
|
||||||
this.data_source_key = value;
|
this.data_source_key = value;
|
||||||
this.flush();
|
this.refresh();
|
||||||
}}
|
}}/>
|
||||||
/>
|
<this.refresh_btn.x/>
|
||||||
|
|
||||||
<this.refresh_btn.x></this.refresh_btn.x>
|
|
||||||
|
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Table<Aircraft>
|
|
||||||
dataSource={this.dataSource}
|
|
||||||
columns={columns}
|
|
||||||
showSorterTooltip={false}
|
|
||||||
size="small"
|
|
||||||
scroll={{ x: 'max-content', y: 'calc(100vh - 300px)' }}
|
|
||||||
|
|
||||||
pagination={false}
|
|
||||||
// pagination={{
|
|
||||||
// position: ['topRight'],
|
|
||||||
// pageSize: Infinity, // 单页设置
|
|
||||||
// }}
|
|
||||||
style={{ overflowX: 'auto' }}
|
|
||||||
/>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Layout>
|
<Table<Aircraft_Row> dataSource={this.dataSource}
|
||||||
|
columns={this.columns}
|
||||||
|
showSorterTooltip={false}
|
||||||
);
|
size="small"
|
||||||
|
scroll={{x: "max-content", y: "calc(100vh - 300px)"}}
|
||||||
|
pagination={false}
|
||||||
|
style={{overflowX: "auto"}}/>
|
||||||
|
</Flex>
|
||||||
|
</Layout>;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -285,8 +285,7 @@ export class Cesium_Map extends enhance.Base {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
list(): Data_Source[] {
|
list(): Data_Source[] {
|
||||||
if (!app.setting) return [];
|
return [...(app.data_source_config?.all() ?? [])];
|
||||||
return app.setting.data_source_config.list.list;
|
|
||||||
}
|
}
|
||||||
async load_map() {
|
async load_map() {
|
||||||
const [resources, graphics_config, view_config, model_config] = await Promise.all([load_map_resources(), load_cesium_graphics_config(), load_map_view_config(), load_map_model_config()]);
|
const [resources, graphics_config, view_config, model_config] = await Promise.all([load_map_resources(), load_cesium_graphics_config(), load_map_view_config(), load_map_model_config()]);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export class Leaflet_Map extends enhance.Base {
|
|||||||
data_source_show: Data_Source_Show = new Data_Source_Show();
|
data_source_show: Data_Source_Show = new Data_Source_Show();
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
aircraft_info_show: Aircraft_Info_Show = new Aircraft_Info_Show(this);
|
aircraft_info_show: Aircraft_Info_Show = new Aircraft_Info_Show(this);
|
||||||
data_source_config: Data_Source_Config = null
|
data_source_config: Data_Source_Config | null = null
|
||||||
tile_layer: L.TileLayer | null = null
|
tile_layer: L.TileLayer | null = null
|
||||||
map_resources: Map_Resources_Metadata | null = null
|
map_resources: Map_Resources_Metadata | null = null
|
||||||
map_view_config: Map_View_Config | null = null
|
map_view_config: Map_View_Config | null = null
|
||||||
@@ -118,7 +118,7 @@ export class Leaflet_Map extends enhance.Base {
|
|||||||
|
|
||||||
|
|
||||||
if (this.data_source_config) {
|
if (this.data_source_config) {
|
||||||
let ds_list = this.data_source_config.list.list;
|
const ds_list = this.data_source_config.all();
|
||||||
ds_list.forEach((ds: Data_Source) => {
|
ds_list.forEach((ds: Data_Source) => {
|
||||||
if (!ds.enable) return; // ✅ 跳过当前项
|
if (!ds.enable) return; // ✅ 跳过当前项
|
||||||
for (const [key, aircraft] of ds.aircraftMap) {
|
for (const [key, aircraft] of ds.aircraftMap) {
|
||||||
@@ -133,7 +133,7 @@ export class Leaflet_Map extends enhance.Base {
|
|||||||
|
|
||||||
this.autoRefreshTimer = setInterval(() => {
|
this.autoRefreshTimer = setInterval(() => {
|
||||||
if (this.data_source_config){
|
if (this.data_source_config){
|
||||||
let ds_list = this.data_source_config.list.list;
|
const ds_list = this.data_source_config.all();
|
||||||
ds_list.forEach((ds: Data_Source) => {
|
ds_list.forEach((ds: Data_Source) => {
|
||||||
if (!ds.enable) return; // ✅ 跳过当前项
|
if (!ds.enable) return; // ✅ 跳过当前项
|
||||||
ds.refresh()
|
ds.refresh()
|
||||||
@@ -172,7 +172,7 @@ export class Leaflet_Map extends enhance.Base {
|
|||||||
|
|
||||||
list(): Data_Source[] {
|
list(): Data_Source[] {
|
||||||
if (!this.data_source_config) return []
|
if (!this.data_source_config) return []
|
||||||
return this.data_source_config.list.list
|
return [...this.data_source_config.all()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user