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 ( - - - - - - - } 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()] }