From 934f29bf702b7fce79b4f5f2ada7fa7932283cf2 Mon Sep 17 00:00:00 2001
From: wyc <1104749580@qq.com>
Date: Sun, 9 Aug 2026 16:05:16 +0800
Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/App.tsx | 23 +-
src/Data_Source/Data_Source.tsx | 4 +-
src/Data_Source/Data_Source_Config.tsx | 86 ++-
src/Data_Source/Data_Source_Show.tsx | 23 +-
src/Map/Aircraft_List.tsx | 743 +++++++------------------
src/Map/Cesium_Map.tsx | 3 +-
src/Map/Leaflet_Map.tsx | 8 +-
7 files changed, 301 insertions(+), 589 deletions(-)
diff --git a/src/App.tsx b/src/App.tsx
index d61c64f..cd07cf9 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -21,6 +21,7 @@ import {Navigate, Router} from "react-router";
import axios from "axios";
import {baseURL} from "./Global.tsx";
import {is_cesium_webgl_available, webgl_unavailable_message} from "./Map/WebGL_Support.ts";
+import {Data_Source_Config} from "./Data_Source/Data_Source_Config.tsx";
import L from 'leaflet';
@@ -41,6 +42,7 @@ class App extends enhance.Base {
setting: Settings | null = null;
aircraft_list: Aircraft_List | null = null;
dsp: DSP | null = null;
+ data_source_config: Data_Source_Config | null = null;
// @ts-ignore
navigate: NavigateFunction
@@ -106,6 +108,7 @@ class App extends enhance.Base {
this.leaflet_map = new Leaflet_Map();
this.aircraft_list = new Aircraft_List();
this.dsp = new DSP();
+ this.data_source_config = new Data_Source_Config(this.on_data_sources_changed);
// 延迟初始化Settings,避免“初始化前访问”错误
setTimeout(() => {
@@ -114,6 +117,20 @@ class App extends enhance.Base {
}, 0);
};
+ on_data_sources_changed = () => {
+ if (!this.data_source_config) return;
+
+ if (this.leaflet_map) {
+ this.leaflet_map.data_source_config = this.data_source_config;
+ this.leaflet_map.data_source_show.data_source_config = this.data_source_config;
+ this.leaflet_map.data_source_show.flush();
+ this.leaflet_map.flush();
+ }
+ this.aircraft_list?.data_sources_changed();
+ this.cesium_map?.request_sync_data_sources?.();
+ this.flush();
+ };
+
use_hook() {
this.navigate = useNavigate();
this.location = useLocation();
@@ -138,9 +155,7 @@ class App extends enhance.Base {
};
get_Data_Source(key: string) {
- // ************************ 优化:添加空值保护,避免报错 ************************
- if (!this.setting) return null;
- return this.setting.data_source_config.map(key)
+ return this.data_source_config?.map(key) ?? null;
}
onClick: MenuProps['onClick'] = (e) => {
@@ -174,7 +189,7 @@ class App extends enhance.Base {
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
组件初始化中...
;
}
diff --git a/src/Data_Source/Data_Source.tsx b/src/Data_Source/Data_Source.tsx
index c2ad329..747f00a 100644
--- a/src/Data_Source/Data_Source.tsx
+++ b/src/Data_Source/Data_Source.tsx
@@ -555,8 +555,8 @@ class Aircraft_Stream_Client {
}));
}
source_payloads() {
- const list = app.setting?.data_source_config.list.list || [];
- return list.filter((ds: Data_Source) => ds.enable).map((ds: Data_Source) => this.source_payload(ds));
+ const list = app.data_source_config?.enabled() ?? [];
+ return list.map((ds: Data_Source) => this.source_payload(ds));
}
source_payload(ds: Data_Source) {
return {
diff --git a/src/Data_Source/Data_Source_Config.tsx b/src/Data_Source/Data_Source_Config.tsx
index b8f09e4..08cad4b 100644
--- a/src/Data_Source/Data_Source_Config.tsx
+++ b/src/Data_Source/Data_Source_Config.tsx
@@ -1,7 +1,13 @@
-import axios from "axios"
-import {app} from "../App.tsx"
-import {List_Data} from "../Global.tsx"
-import {Data_Source, Dll_Data_Source, File_Data_Source, Serial_Data_Source, Shared_Memory_Data_Source, TCP_Client_Data_Source} from "./Data_Source.tsx"
+import axios from "axios"
+import {
+ Data_Source,
+ 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 = {
id: number
key: string
@@ -10,6 +16,7 @@ type Adminive_Data_Source_Row = {
config: Record
[key: string]: unknown
}
+
function create_data_source(type: string): Data_Source {
if (type === "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()
if (type === "Shared_Memory_Data_Source")
return new Shared_Memory_Data_Source()
- throw new Error(`未知数据源类型: ${type}`)
+
+ // 未知类型仍按公共数据源模型展示。新增后端类型不应让整个前端崩溃;
+ // 只有确实需要类型专属行为时,前端才需要增加对应实现。
+ return new Data_Source()
}
+
export class Data_Source_Config {
- list = new List_Data()
- map(key: string): Data_Source | undefined {
- return this.list.list.find((item: Data_Source) => item.key === key)
- }
- constructor() {
+ private sources: Data_Source[] = []
+ loading = true
+ error = ""
+
+ constructor(private readonly on_change?: () => void) {
window.addEventListener("ecap-data-sources-changed", () => 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() {
- const response = await axios.get("/api/adminive/config/data_sources", {params: {page: 1, perPage: 1000}})
- const rows = response.data?.data?.items as Adminive_Data_Source_Row[] ?? []
- this.list.list = rows.map(row => {
- const source = create_data_source(row.type)
- const {id, config, state, ...data} = row
- Object.assign(source, config, data)
- source.adminive_id = id
- source.normalize_map_display()
- return source
- })
- if (app.leaflet_map) {
- app.leaflet_map.data_source_show.data_source_config = this
- app.leaflet_map.data_source_config = this
- app.leaflet_map.data_source_show.flush()
+ try {
+ const response = await axios.get("/api/adminive/config/data_sources", {
+ params: {page: 1, perPage: 1000}
+ })
+ const rows = response.data?.data?.items
+ if (!Array.isArray(rows)) {
+ throw new Error("数据源接口未返回 data.items 数组")
+ }
+
+ const previous_sources = new Map(this.sources.map(source => [source.key, source]))
+ this.sources = (rows as Adminive_Data_Source_Row[]).map(row => {
+ const previous = previous_sources.get(row.key)
+ const source = previous?.type === row.type ? previous : create_data_source(row.type)
+ 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()
}
}
diff --git a/src/Data_Source/Data_Source_Show.tsx b/src/Data_Source/Data_Source_Show.tsx
index 99a0ee7..f6fd217 100644
--- a/src/Data_Source/Data_Source_Show.tsx
+++ b/src/Data_Source/Data_Source_Show.tsx
@@ -6,7 +6,6 @@ import {
Checkbox,
CheckboxChangeEvent,
ColorPicker,
- ColorPickerProps,
Dropdown,
InputNumber,
type MenuProps,
@@ -14,12 +13,10 @@ import {
Space,
Tabs,
Tag,
- theme,
Tooltip,
Typography
} from "antd";
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 {Base_Drawer} from "../Base_Drawer.tsx";
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;
-type Presets = Required['presets'][number];
type Settings_Tab = "tiles" | "view3d" | "source"
-function genPresets(presets = presetPalettes) {
- return Object.entries(presets).map(([label, colors]) => ({label, colors, key: label}));
-}
-
-const Demo: React.FC = () => {
- const {token} = theme.useToken();
- const presets = genPresets({primary: generate(token.colorPrimary), red, green});
- return ;
-};
-
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()
active_data_source_key = ""
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 }[] = [{
key: "source",
label: "子数据源",
@@ -464,7 +450,7 @@ export class Data_Source_Show extends enhance.Base {
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") {
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") {
this.active_tab_key = "tiles";
}
- const source_list = this.data_source_config.list.list as Data_Source[];
- const enabled_list = source_list.filter((ds: Data_Source) => ds.enable);
+ const enabled_list = this.data_source_config.enabled();
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;
}
diff --git a/src/Map/Aircraft_List.tsx b/src/Map/Aircraft_List.tsx
index ff2a526..0e4d47f 100644
--- a/src/Map/Aircraft_List.tsx
+++ b/src/Map/Aircraft_List.tsx
@@ -1,574 +1,251 @@
-import enhance from "../core/enhance.tsx";
-import {Button, Select, SelectProps, Table} from 'antd';
-import React, {HTMLProps, useRef, useState} from 'react';
+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 {app} from "../App.tsx";
-import {Data_Source} from "../Data_Source/Data_Source.tsx";
-import {CaretDownOutlined, CaretUpOutlined} from "@ant-design/icons";
-import { Input, Space } from 'antd';
-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 {SearchOutlined} from "@ant-design/icons";
+import type {TableColumnType} from "antd";
+import type {FilterDropdownProps} from "antd/es/table/interface";
import {Refresh} from "../Refresh.tsx";
-export interface Aircraft {
- key?: string;
+type Aircraft_Row = Record & {key: string};
- // 统计 / 时间
- times: number;
- uti: number; // 时间戳(秒)
- day_second: number; // 时间戳(秒)
- ns: number; // 时间戳(纳秒)
-
- // 识别信息
- 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 => ({
- 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 (
-
-
-
- setSelectedKeys([`${v ?? ''},${max ?? ''}`])
- }
- style={{ width: '100%' }}
- />
-
- setSelectedKeys([`${min ?? ''},${v ?? ''}`])
- }
- style={{ width: '100%' }}
- />
-
-
-
-
-
-
- );
- },
-
- filterIcon: (filtered: boolean) => (
-
- ),
-
- 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 (
-
-
- setSelectedKeys(e.target.value ? [e.target.value] : [])
- }
- onPressEnter={() => confirm()}
- style={{ marginBottom: 8, display: 'block' }}
- />
-
-
-
-
-
- );
- },
-
- filterIcon: (filtered: boolean) => (
-
- ),
-
- 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]; // 数字排序
- };
+type Aircraft_Column_Model = {
+ key: string;
+ label: string;
+ type: "index" | "timestamp" | "number" | "text" | "boolean" | string;
+ defaultSortOrder?: "ascend" | "descend";
+ secondsKey?: string;
+ nanosecondsKey?: string;
};
const onHeaderCell = (): HTMLProps => ({
style: {
- writingMode: 'vertical-lr',
- textOrientation: 'upright',
- paddingLeft: '0px', // 调整左右内边距
- paddingRight: '0px',
- // paddingTop: '0px',
- // paddingBottom: '0px',
- // textAlign: 'center',
+ writingMode: "vertical-lr",
+ textOrientation: "upright",
+ paddingLeft: 0,
+ paddingRight: 0,
},
});
-// const onHeaderCell = (): HTMLProps => ({
-// style: {
-// writingMode: 'vertical-rl' as React.CSSProperties['writingMode'],
-// whiteSpace: 'nowrap' as React.CSSProperties['whiteSpace'],
-// textAlign: 'center' as 'center', // 强制设置为 'center' 类型
-// },
-// });
-
-const get_handle = (line2 : string)=>{
- return
- {
- line2.split(/(<[^>]+>)/g).map((part, index) =>
- part.startsWith('<') && part.endsWith('>') ? (
- // 如果是< >之间的内容,进行处理
- {part.slice(1, -1)} // 去掉< >并包裹在span中
- ) : (
- // 其他内容保持不变
- {part}
- )
- )
- }
-
+function render_column_title(label: string) {
+ return {label.split(/(<[^>]+>)/g).filter(Boolean).map((part, index) =>
+ part.startsWith("<") && part.endsWith(">")
+ ? {part.slice(1, -1)}
+ : {part}
+ )}
;
}
+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 = (
- title: string | React.ReactNode, // 修改为支持字符串和 React 节点
- field: string,
- extra: Partial = {}
-) => {
- // 判断 title 是否是字符串类型
- const isTitleString = typeof title === 'string';
-
- return {
- title: isTitleString ? get_handle(title) : title, // 如果是字符串,处理,否则直接使用
- dataIndex: field,
- key: field,
- onHeaderCell: onHeaderCell,
- sorter: int_sorter(field),
- render: (text) => {
- return (
- {text}
- )
- },
- ...getColumnRangeSearchProps(field),
- ...extra,
- };
-};
-
-
-const create_str = (
- title: string | React.ReactNode,
- field: string,
- extra: Partial = {}
-) => {
- const isTitleString = typeof title === 'string';
-
- return {
- title: isTitleString ? get_handle(title) : title,
- dataIndex: field,
- key: field,
- sorter: str_sorter(field),
- render: (text) => {
- return (
- {text}
- )
- },
- onHeaderCell: onHeaderCell,
- ...withTextSearch(field), // 👈 搜索能力
- ...extra,
- };
-};
-
-const cr2 = (line1: string, line2: string) => {
- const renderLine1 = get_handle(line1);
- const renderLine2 = get_handle(line2);
- return (
-
- {renderLine1}
- {renderLine2}
-
- );
-};
-
-
-const columns: TableColumnType[] = [
- {
- title: '序号',
- key: 'index',
- width: 60,
- align: 'center',
- render: (_value, _record, index) => index + 1,
+const range_filter = (field: string): TableColumnType => ({
+ filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}: FilterDropdownProps) => {
+ const raw = selectedKeys[0] as string | undefined;
+ const [minimum, maximum] = raw
+ ? raw.split(",").map(value => value === "" ? undefined : Number(value))
+ : [undefined, undefined];
+ return
+
+ setSelectedKeys([`${value ?? ""},${maximum ?? ""}`])}
+ style={{width: "100%"}}/>
+ setSelectedKeys([`${minimum ?? ""},${value ?? ""}`])}
+ style={{width: "100%"}}/>
+
+
+
+
+
+
;
},
- {
- title: '时间',
- dataIndex: 'formattedDate',
- key: 'formattedDate',
- onHeaderCell: () => ({
- style: {
- // 设置文本垂直显示
- writingMode: 'vertical-rl',
- whiteSpace: 'nowrap', // 防止文字换行
- 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 (
-
- )
- },
+ filterIcon: filtered => ,
+ onFilter: (value, record) => {
+ if (typeof value !== "string") return true;
+ const [minimum_text, maximum_text] = value.split(",");
+ const minimum = minimum_text === "" ? undefined : Number(minimum_text);
+ const maximum = maximum_text === "" ? undefined : Number(maximum_text);
+ const current = Number(record[field]);
+ if (!Number.isFinite(current)) return false;
+ return (minimum === undefined || current >= minimum) && (maximum === undefined || current <= maximum);
},
- create_int('消息数', 'times'),
- // create_int(cr2('时间戳', '(utc)'), 'uti'),
- // create_int(cr2('时间戳', '(秒内纳秒数)'), 'ns'),
- // create_int(cr2('时间戳', '(当天的秒数)'), 'day_second'),
- create_str(cr2('识别码', ''), 'hex'),
- create_str('航班编号', 'fli'),
- create_str('数据来源标识', 'ava'),
- create_str('首选数据源', 'src'),
- create_int('纬度', 'lat'),
- create_int('经度', 'lon', {defaultSortOrder: 'descend'}),
- create_int(cr2('高度', '(米)'), 'alt'),
- create_int(cr2('地速','(节)'), 'spd'),
- create_int(cr2('航向', '(真)'), 'trk'),
- create_int(cr2('垂直速度', '()'), 'vrt'),
- create_str(cr2('空中/地面', '状态'), 'gda'),
- create_str('机型分类', 'cat'),
- create_str('起飞机场', 'org'),
- create_str('目的机场', 'dst'),
- create_str('航空公司', 'opr'),
- create_str('机型', 'typ'),
- create_str('注册号', 'reg'),
- create_str('应答机编码', 'squ'),
- create_str('国家', 'cou'),
- create_str('接收器距离', 'dis'),
- create_int('信任值', 'tru'),
- create_int(cr2('信号强度', '()'), 'dbm'),
- create_int(cr2('位置数据延迟', '(秒)'), 'lla'),
- create_int(cr2('温度', '(℃)'), 'tmp'),
- create_int(cr2('风速', '(节)'), 'wsp'),
- create_int(cr2('风向', '(°)'), 'wdi'),
- create_int(cr2('ADS-B', ' 协议版本'), 'mop'),
- create_int(cr2('识别应答', '()'), 'spi'),
- create_int('告警状态', 'alr'),
- create_int(cr2('指示空速', '()'), 'ias'),
- create_int(cr2('真空速', '()'), 'tas'),
- create_int('磁航向', 'hdgm'),
- create_int('真航向', 'hdgt'),
- create_int(cr2('气压设定', '()'), 'qnhs'),
- create_int('选择高度', 'alts'),
- create_int('', 'pic'),
- create_int('防撞系统状态', 'tcm'),
- create_int('自动驾驶启用', 'ape'),
- create_int(cr2('系统完整性等级', '()'), 'sil'),
- create_int(cr2('系统设计保证', '()'), 'sda'),
- create_int(cr2('位置精度', '()'), 'nacp'),
- create_int(cr2('位置估算', '次数每秒'), 'pest'),
- create_int(cr2('提供数据', '的接收机数'), 'nocl'),
- create_int('时间质量', 'tq'),
-];
+});
+
+const text_filter = (field: string): TableColumnType => ({
+ filterDropdown: ({setSelectedKeys, selectedKeys, confirm, clearFilters}: FilterDropdownProps) =>
+
+ setSelectedKeys(event.target.value ? [event.target.value] : [])}
+ onPressEnter={() => confirm()} style={{marginBottom: 8, display: "block"}}/>
+
+
+
+
+
,
+ filterIcon: filtered => ,
+ onFilter: (value, record) => String(record[field] ?? "").toLowerCase().includes(String(value).toLowerCase()),
+});
+
+function format_timestamp(row: Record, column: Aircraft_Column_Model): string {
+ const seconds = Number(row[column.secondsKey ?? "uti"]);
+ if (!Number.isFinite(seconds) || seconds <= 0) return "";
+ const nanoseconds = Number(row[column.nanosecondsKey ?? "ns"]);
+ const date = new Date(seconds * 1000);
+ const pad = (value: number, length = 2) => String(value).padStart(length, "0");
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}\n` +
+ `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(Number.isFinite(nanoseconds) ? nanoseconds : 0, 9)}`;
+}
+
+function normalized_sort_order(value: unknown): "ascend" | "descend" | undefined {
+ return value === "ascend" || value === "descend" ? value : undefined;
+}
+
+function create_columns(model: readonly Aircraft_Column_Model[]): TableColumnType[] {
+ return model.map(column => {
+ if (column.type === "index") {
+ return {
+ title: render_column_title(column.label),
+ key: column.key,
+ width: 60,
+ align: "center",
+ render: (_value, _record, index) => index + 1,
+ };
+ }
+
+ const common: TableColumnType = {
+ 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 =>
+ {value == null ? "" : typeof value === "boolean" ? (value ? "是" : "否") : String(value)}
+
,
+ };
+ return {
+ ...common,
+ ...(column.type === "number" ? range_filter(column.key) : text_filter(column.key)),
+ };
+ });
+}
+
+function fallback_model(rows: readonly Record[]): 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 {
- dataSource: Aircraft[]
- data_source_key: string = null
- limit_msg_num: number = 0
+ dataSource: Aircraft_Row[] = [];
+ columns: TableColumnType[] = [];
+ data_source_key: string | null = null;
+ limit_msg_num = 0;
-
- refresh_btn = new Refresh(()=>{
- this.refresh();
- })
-
- constructor() {
- super();
- }
+ refresh_btn = new Refresh(() => this.refresh());
refresh() {
+ if (!this.data_source_key) {
+ this.dataSource = [];
+ this.columns = [];
+ this.flush();
+ return;
+ }
+
axios.post(`${baseURL}/aircraft_list`, {
data_source_key: this.data_source_key,
- limit_msg_num: this.limit_msg_num
- }).then((res) => {
- let ret = []
- res.data.forEach((item : Aircraft, index : number) => {
-
- const date = new Date(item.uti * 1000); // 将秒转换为毫秒
- const nanoSeconds = item.ns;
- let daySecond = item.day_second;
- let hours = Math.floor(daySecond / 3600); // 获取小时
- let minutes = Math.floor((daySecond % 3600) / 60); // 获取分钟
- 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 = `${dateString}
${timeString}`;
-
- item.formattedDate = formattedDate;
-
- ret.push({
- ...item,
- key: String(index)
- })
-
+ limit_msg_num: this.limit_msg_num,
+ include_model: true,
+ }).then(response => {
+ const payload = response.data;
+ const raw_rows: Record[] = Array.isArray(payload?.data?.items)
+ ? payload.data.items
+ : Array.isArray(payload) ? payload : [];
+ const model: Aircraft_Column_Model[] = Array.isArray(payload?.model?.columns)
+ ? payload.model.columns
+ : fallback_model(raw_rows);
+ 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();
- })
+ }).catch(error => {
+ console.error("加载飞机列表失败", error);
+ this.dataSource = [];
+ this.columns = [];
+ this.flush();
+ });
}
load_default_data_source() {
- if (this.data_source_key === null) {
- let ds_list: Data_Source[] = app.setting.data_source_config.list.list;
- if (ds_list.length > 0) {
- this.data_source_key = ds_list[0].key;
- this.flush();
- }
+ const sources = app.data_source_config?.enabled() ?? [];
+ if (!sources.some(source => source.key === this.data_source_key)) {
+ this.data_source_key = sources[0]?.key ?? null;
+ this.flush();
}
}
- on_mount() {
- this.load_default_data_source()
- this.refresh()
+
+ data_sources_changed() {
+ this.load_default_data_source();
+ if (this.mounted) this.refresh();
}
+ on_mount() {
+ this.load_default_data_source();
+ this.refresh();
+ }
- render(props: any) {
- return (
-
-
-
-
-
-
- }
+ render() {
+ return
+
+
+ }
placeholder="请选择数据来源"
value={this.data_source_key}
- options={(() => {
- let options: SelectProps['options'] = [];
- let t = app.setting.data_source_config;
- t.list.list.forEach((item: Data_Source) => {
- if (item.enable) {
- options.push({
- value: item.key,
- label: item.key
- });
- }
- })
- return options;
- })()}
- onChange={(value) => {
+ options={(app.data_source_config?.enabled() ?? []).map(item => ({
+ value: item.key,
+ label: item.key,
+ }))}
+ onChange={value => {
this.data_source_key = value;
- this.flush();
- }}
- />
-
-
-
-
-
-
- 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' }}
- />
+ this.refresh();
+ }}/>
+
-
-
-
- );
+ dataSource={this.dataSource}
+ columns={this.columns}
+ showSorterTooltip={false}
+ size="small"
+ scroll={{x: "max-content", y: "calc(100vh - 300px)"}}
+ pagination={false}
+ style={{overflowX: "auto"}}/>
+
+ ;
}
-
-}
\ No newline at end of file
+}
diff --git a/src/Map/Cesium_Map.tsx b/src/Map/Cesium_Map.tsx
index 3b9c2b4..a852ceb 100644
--- a/src/Map/Cesium_Map.tsx
+++ b/src/Map/Cesium_Map.tsx
@@ -285,8 +285,7 @@ export class Cesium_Map extends enhance.Base {
}
}
list(): Data_Source[] {
- if (!app.setting) return [];
- return app.setting.data_source_config.list.list;
+ return [...(app.data_source_config?.all() ?? [])];
}
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()]);
diff --git a/src/Map/Leaflet_Map.tsx b/src/Map/Leaflet_Map.tsx
index bdc69de..890cb0d 100644
--- a/src/Map/Leaflet_Map.tsx
+++ b/src/Map/Leaflet_Map.tsx
@@ -44,7 +44,7 @@ export class Leaflet_Map extends enhance.Base {
data_source_show: Data_Source_Show = new Data_Source_Show();
// @ts-ignore
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
map_resources: Map_Resources_Metadata | 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) {
- let ds_list = this.data_source_config.list.list;
+ const ds_list = this.data_source_config.all();
ds_list.forEach((ds: Data_Source) => {
if (!ds.enable) return; // ✅ 跳过当前项
for (const [key, aircraft] of ds.aircraftMap) {
@@ -133,7 +133,7 @@ export class Leaflet_Map extends enhance.Base {
this.autoRefreshTimer = setInterval(() => {
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) => {
if (!ds.enable) return; // ✅ 跳过当前项
ds.refresh()
@@ -172,7 +172,7 @@ export class Leaflet_Map extends enhance.Base {
list(): Data_Source[] {
if (!this.data_source_config) return []
- return this.data_source_config.list.list
+ return [...this.data_source_config.all()]
}