拓扑图
This commit is contained in:
Generated
+885
-4
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons": "^6.1.0",
|
"@ant-design/icons": "^6.1.0",
|
||||||
|
"@antv/g6": "^5.1.1",
|
||||||
"antd": "^5.29.1",
|
"antd": "^5.29.1",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"cesium": "^1.143.0",
|
"cesium": "^1.143.0",
|
||||||
|
|||||||
@@ -0,0 +1,362 @@
|
|||||||
|
import {CanvasEvent, Graph, GraphEvent, NodeEvent} from "@antv/g6"
|
||||||
|
import {Alert, Button, Card, Collapse, Descriptions, Drawer, Empty, Space, Spin, Tag, Typography} from "antd"
|
||||||
|
import axios from "axios"
|
||||||
|
import {useCallback, useEffect, useMemo, useRef, useState} from "react"
|
||||||
|
import {AmisPanel} from "./AmisPanel.tsx"
|
||||||
|
|
||||||
|
type Topology_Node = {
|
||||||
|
id: string
|
||||||
|
kind: string
|
||||||
|
entity_id: number
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
type: string
|
||||||
|
enabled: boolean
|
||||||
|
status_api?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Topology_Edge = {
|
||||||
|
id: string
|
||||||
|
source: string
|
||||||
|
target: string
|
||||||
|
active: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type Topology_View = {
|
||||||
|
protocol: "ecap.data-topology-view"
|
||||||
|
protocol_version: 1
|
||||||
|
initialized: boolean
|
||||||
|
zoom: number
|
||||||
|
pan_x: number
|
||||||
|
pan_y: number
|
||||||
|
node_positions: Record<string, {x: number, y: number}>
|
||||||
|
}
|
||||||
|
|
||||||
|
type Topology_Data = {
|
||||||
|
protocol: "ecap.data-topology"
|
||||||
|
protocol_version: 1
|
||||||
|
nodes: Topology_Node[]
|
||||||
|
edges: Topology_Edge[]
|
||||||
|
view: Topology_View
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node_Kind = {
|
||||||
|
kind: string
|
||||||
|
title: string
|
||||||
|
fill: string
|
||||||
|
stroke: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Topology_Editor = {
|
||||||
|
kind: string
|
||||||
|
title: string
|
||||||
|
button_label: string
|
||||||
|
schema: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Data_Topology_Schema = {
|
||||||
|
title: string
|
||||||
|
data_api: string
|
||||||
|
view_api: string
|
||||||
|
graph: {
|
||||||
|
height: number
|
||||||
|
layout: Record<string, unknown>
|
||||||
|
behaviors: string[]
|
||||||
|
active_edge_color: string
|
||||||
|
inactive_edge_color: string
|
||||||
|
}
|
||||||
|
node_kinds: Node_Kind[]
|
||||||
|
status: {
|
||||||
|
title: string
|
||||||
|
poll_interval_ms: number
|
||||||
|
collapsed_hint: string
|
||||||
|
}
|
||||||
|
editors: Topology_Editor[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Adminive_Response<T> = {
|
||||||
|
status: number
|
||||||
|
msg: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
const topology_changed_event = "ecap-data-topology-changed"
|
||||||
|
|
||||||
|
function status_text(value: unknown): string {
|
||||||
|
if (value === null) return "null"
|
||||||
|
if (typeof value === "object") return JSON.stringify(value, null, 2)
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Node_Status({node, schema}: {node: Topology_Node, schema: Data_Topology_Schema["status"]}) {
|
||||||
|
const [expanded, set_expanded] = useState(false)
|
||||||
|
const [status, set_status] = useState<Record<string, unknown> | null>(null)
|
||||||
|
const [error, set_error] = useState("")
|
||||||
|
const [updated_at, set_updated_at] = useState<Date | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!expanded || !node.status_api) return
|
||||||
|
let active = true
|
||||||
|
let timer: number | undefined
|
||||||
|
let controller: AbortController | undefined
|
||||||
|
const poll = async () => {
|
||||||
|
controller = new AbortController()
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Adminive_Response<Record<string, unknown>>>(node.status_api, {signal: controller.signal})
|
||||||
|
if (!active) return
|
||||||
|
set_status(response.data.data)
|
||||||
|
set_error("")
|
||||||
|
set_updated_at(new Date())
|
||||||
|
} catch (reason) {
|
||||||
|
if (!active || axios.isCancel(reason)) return
|
||||||
|
set_error(reason instanceof Error ? reason.message : String(reason))
|
||||||
|
} finally {
|
||||||
|
if (active) timer = window.setTimeout(poll, Math.max(250, schema.poll_interval_ms))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void poll()
|
||||||
|
return () => {
|
||||||
|
active = false
|
||||||
|
controller?.abort()
|
||||||
|
if (timer !== undefined) window.clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [expanded, node.status_api, schema.poll_interval_ms])
|
||||||
|
|
||||||
|
if (!node.status_api) return <Typography.Text type="secondary">该节点没有后端运行状态接口</Typography.Text>
|
||||||
|
return <Collapse size="small" activeKey={expanded ? ["status"] : []}
|
||||||
|
onChange={keys => {
|
||||||
|
const open = keys.includes("status")
|
||||||
|
set_expanded(open)
|
||||||
|
if (!open) {
|
||||||
|
set_status(null)
|
||||||
|
set_error("")
|
||||||
|
set_updated_at(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
items={[{
|
||||||
|
key: "status",
|
||||||
|
label: <Space><span>{schema.title}</span><Typography.Text type="secondary">{expanded ? "轮询中" : schema.collapsed_hint}</Typography.Text></Space>,
|
||||||
|
children: error
|
||||||
|
? <Alert type="error" showIcon message="状态读取失败" description={error}/>
|
||||||
|
: status
|
||||||
|
? <Space direction="vertical" size={8} style={{width: "100%"}}>
|
||||||
|
<Descriptions size="small" bordered column={1}
|
||||||
|
items={Object.entries(status).map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
label: key,
|
||||||
|
children: <Typography.Text style={{whiteSpace: "pre-wrap", wordBreak: "break-word"}}>{status_text(value)}</Typography.Text>
|
||||||
|
}))}/>
|
||||||
|
{updated_at && <Typography.Text type="secondary">更新于 {updated_at.toLocaleTimeString()}</Typography.Text>}
|
||||||
|
</Space>
|
||||||
|
: <Spin size="small"/>
|
||||||
|
}]}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTopology({schema}: {schema: Data_Topology_Schema}) {
|
||||||
|
const container = useRef<HTMLDivElement>(null)
|
||||||
|
const graph_ref = useRef<Graph | null>(null)
|
||||||
|
const [data, set_data] = useState<Topology_Data | null>(null)
|
||||||
|
const [selected_id, set_selected_id] = useState("")
|
||||||
|
const [editor_kind, set_editor_kind] = useState("")
|
||||||
|
const [error, set_error] = useState("")
|
||||||
|
const [view_error, set_view_error] = useState("")
|
||||||
|
|
||||||
|
const load = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Adminive_Response<Topology_Data>>(schema.data_api)
|
||||||
|
if (response.data.data.protocol !== "ecap.data-topology" || response.data.data.protocol_version !== 1)
|
||||||
|
throw new Error("数据拓扑协议或版本不受支持")
|
||||||
|
if (response.data.data.view.protocol !== "ecap.data-topology-view" || response.data.data.view.protocol_version !== 1)
|
||||||
|
throw new Error("数据拓扑视图协议或版本不受支持")
|
||||||
|
set_data(response.data.data)
|
||||||
|
set_error("")
|
||||||
|
} catch (reason) {
|
||||||
|
set_error(reason instanceof Error ? reason.message : String(reason))
|
||||||
|
}
|
||||||
|
}, [schema.data_api])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load()
|
||||||
|
window.addEventListener(topology_changed_event, load)
|
||||||
|
return () => window.removeEventListener(topology_changed_event, load)
|
||||||
|
}, [load])
|
||||||
|
|
||||||
|
const kind_by_name = useMemo(() => new Map(schema.node_kinds.map(kind => [kind.kind, kind])), [schema.node_kinds])
|
||||||
|
const selected = data?.nodes.find(node => node.id === selected_id)
|
||||||
|
const selected_kind = selected && kind_by_name.get(selected.kind)
|
||||||
|
const editor = schema.editors.find(item => item.kind === editor_kind)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!container.current || !data) return
|
||||||
|
let disposed = false
|
||||||
|
let restoring = true
|
||||||
|
let save_timer: number | undefined
|
||||||
|
const graph = new Graph({
|
||||||
|
container: container.current,
|
||||||
|
autoFit: data.view.initialized ? undefined : "view",
|
||||||
|
animation: false,
|
||||||
|
data: {
|
||||||
|
nodes: data.nodes.map(node => ({id: node.id, data: node})),
|
||||||
|
edges: data.edges.map(edge => ({id: edge.id, source: edge.source, target: edge.target, data: edge}))
|
||||||
|
},
|
||||||
|
layout: schema.graph.layout as never,
|
||||||
|
behaviors: schema.graph.behaviors,
|
||||||
|
node: {
|
||||||
|
type: "rect",
|
||||||
|
style: datum => {
|
||||||
|
const node = datum.data as unknown as Topology_Node
|
||||||
|
const kind = kind_by_name.get(node.kind)
|
||||||
|
return {
|
||||||
|
size: [190, 62],
|
||||||
|
radius: 10,
|
||||||
|
fill: kind?.fill ?? "#fafafa",
|
||||||
|
fillOpacity: node.enabled ? 1 : 0.48,
|
||||||
|
stroke: kind?.stroke ?? "#8c8c8c",
|
||||||
|
lineWidth: node.enabled ? 2 : 1,
|
||||||
|
lineDash: node.enabled ? undefined : [5, 4],
|
||||||
|
labelText: `${node.label}\n${node.type}`,
|
||||||
|
labelFill: "#1f1f1f",
|
||||||
|
labelFontSize: 13,
|
||||||
|
labelLineHeight: 18,
|
||||||
|
cursor: "pointer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edge: {
|
||||||
|
type: "polyline",
|
||||||
|
style: datum => {
|
||||||
|
const edge = datum.data as unknown as Topology_Edge
|
||||||
|
return {
|
||||||
|
stroke: edge.active ? schema.graph.active_edge_color : schema.graph.inactive_edge_color,
|
||||||
|
lineWidth: edge.active ? 2.5 : 1.5,
|
||||||
|
lineDash: edge.active ? undefined : [6, 5],
|
||||||
|
endArrow: true,
|
||||||
|
opacity: edge.active ? 1 : 0.62
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
graph.on(NodeEvent.CLICK, event => {
|
||||||
|
const target = (event as unknown as {target: {id: string}}).target
|
||||||
|
set_selected_id(target.id)
|
||||||
|
})
|
||||||
|
graph.on(CanvasEvent.CLICK, () => set_selected_id(""))
|
||||||
|
const persist_view = async () => {
|
||||||
|
if (restoring || disposed) return
|
||||||
|
const node_positions: Topology_View["node_positions"] = {}
|
||||||
|
for (const node of data.nodes) {
|
||||||
|
const [x, y] = graph.getElementPosition(node.id)
|
||||||
|
node_positions[node.id] = {x, y}
|
||||||
|
}
|
||||||
|
const [pan_x, pan_y] = graph.getPosition()
|
||||||
|
const payload: Topology_View = {
|
||||||
|
protocol: "ecap.data-topology-view",
|
||||||
|
protocol_version: 1,
|
||||||
|
initialized: true,
|
||||||
|
zoom: graph.getZoom(),
|
||||||
|
pan_x,
|
||||||
|
pan_y,
|
||||||
|
node_positions
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await axios.put(schema.view_api, payload)
|
||||||
|
if (!disposed) set_view_error("")
|
||||||
|
} catch (reason) {
|
||||||
|
if (!disposed) set_view_error(reason instanceof Error ? reason.message : String(reason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const schedule_view_save = () => {
|
||||||
|
if (restoring || disposed) return
|
||||||
|
if (save_timer !== undefined) window.clearTimeout(save_timer)
|
||||||
|
save_timer = window.setTimeout(() => void persist_view(), 400)
|
||||||
|
}
|
||||||
|
graph.on(NodeEvent.DRAG_END, schedule_view_save)
|
||||||
|
graph.on(GraphEvent.AFTER_TRANSFORM, schedule_view_save)
|
||||||
|
graph_ref.current = graph
|
||||||
|
const initialize = async () => {
|
||||||
|
await graph.render()
|
||||||
|
if (disposed) return
|
||||||
|
if (data.view.initialized) {
|
||||||
|
const positions: Record<string, [number, number]> = {}
|
||||||
|
for (const [id, position] of Object.entries(data.view.node_positions)) {
|
||||||
|
if (data.nodes.some(node => node.id === id))
|
||||||
|
positions[id] = [position.x, position.y]
|
||||||
|
}
|
||||||
|
if (Object.keys(positions).length > 0)
|
||||||
|
await graph.translateElementTo(positions, false)
|
||||||
|
await graph.zoomTo(data.view.zoom, false)
|
||||||
|
await graph.translateTo([data.view.pan_x, data.view.pan_y], false)
|
||||||
|
}
|
||||||
|
restoring = false
|
||||||
|
}
|
||||||
|
void initialize()
|
||||||
|
const observer = new ResizeObserver(() => graph.resize())
|
||||||
|
observer.observe(container.current)
|
||||||
|
return () => {
|
||||||
|
if (save_timer !== undefined) window.clearTimeout(save_timer)
|
||||||
|
if (!restoring) void persist_view()
|
||||||
|
disposed = true
|
||||||
|
observer.disconnect()
|
||||||
|
graph.destroy()
|
||||||
|
graph_ref.current = null
|
||||||
|
}
|
||||||
|
}, [data, kind_by_name, schema.graph, schema.view_api])
|
||||||
|
|
||||||
|
return <Card title={schema.title} extra={<Space wrap>
|
||||||
|
<Button onClick={() => void load()}>刷新拓扑</Button>
|
||||||
|
<Button onClick={() => void graph_ref.current?.fitView()}>适应画布</Button>
|
||||||
|
{schema.editors.map(item => <Button key={item.kind} onClick={() => set_editor_kind(item.kind)}>{item.button_label}</Button>)}
|
||||||
|
</Space>}>
|
||||||
|
{error && <Alert type="error" showIcon message="数据拓扑加载失败" description={error} style={{marginBottom: 12}}/>}
|
||||||
|
{view_error && <Alert type="warning" showIcon message="拓扑视图保存失败" description={view_error} closable onClose={() => set_view_error("")} style={{marginBottom: 12}}/>}
|
||||||
|
{!data && !error ? <div style={{padding: 36, textAlign: "center"}}><Spin/></div> : data && <div style={{display: "flex", flexWrap: "wrap", gap: 12, width: "100%"}}>
|
||||||
|
<div style={{flex: "1 1 680px", minWidth: 0, height: schema.graph.height, border: "1px solid #f0f0f0", borderRadius: 8, overflow: "hidden"}} ref={container}/>
|
||||||
|
<Card size="small" title="节点详情" style={{flex: "1 1 320px", minWidth: 0}}
|
||||||
|
extra={selected && <Button type="link" onClick={() => set_editor_kind(selected.kind)}>打开编辑器</Button>}>
|
||||||
|
{selected ? <Space direction="vertical" size={12} style={{width: "100%"}}>
|
||||||
|
<Space wrap><Tag color={selected_kind?.stroke}>{selected_kind?.title ?? selected.kind}</Tag><Tag color={selected.enabled ? "success" : "default"}>{selected.enabled ? "已启用" : "未启用"}</Tag></Space>
|
||||||
|
<Descriptions size="small" bordered column={1} items={[
|
||||||
|
{key: "key", label: "名称", children: selected.key},
|
||||||
|
{key: "type", label: "类型", children: selected.type},
|
||||||
|
{key: "id", label: "后端序号", children: selected.entity_id}
|
||||||
|
]}/>
|
||||||
|
<Node_Status key={`${selected.id}:${selected.status_api ?? ""}`} node={selected} schema={schema.status}/>
|
||||||
|
</Space> : <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="点击图中的节点查看详情;节点可直接拖拽"/>}
|
||||||
|
</Card>
|
||||||
|
</div>}
|
||||||
|
<Drawer open={Boolean(editor)} title={editor?.title} width="88vw" destroyOnClose
|
||||||
|
onClose={() => {
|
||||||
|
set_editor_kind("")
|
||||||
|
void load()
|
||||||
|
}}>
|
||||||
|
{editor && <AmisPanel schema={editor.schema}/>}
|
||||||
|
</Drawer>
|
||||||
|
</Card>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTopologyPage() {
|
||||||
|
const [schema, set_schema] = useState<Data_Topology_Schema | null>(null)
|
||||||
|
const [error, set_error] = useState("")
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController()
|
||||||
|
const load_schema = async () => {
|
||||||
|
try {
|
||||||
|
const response = await axios.get<Adminive_Response<Data_Topology_Schema>>("/api/adminive/config/data_topology/schema", {signal: controller.signal})
|
||||||
|
set_schema(response.data.data)
|
||||||
|
set_error("")
|
||||||
|
} catch (reason) {
|
||||||
|
if (!axios.isCancel(reason)) set_error(reason instanceof Error ? reason.message : String(reason))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const handle_topology_changed = () => void load_schema()
|
||||||
|
void load_schema()
|
||||||
|
window.addEventListener(topology_changed_event, handle_topology_changed)
|
||||||
|
return () => {
|
||||||
|
controller.abort()
|
||||||
|
window.removeEventListener(topology_changed_event, handle_topology_changed)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
if (error) return <Alert type="error" showIcon message="数据拓扑页面加载失败" description={error} style={{margin: 16}}/>
|
||||||
|
if (!schema) return <div style={{padding: 36, textAlign: "center"}}><Spin/></div>
|
||||||
|
return <div style={{flex: 1, minWidth: 0, minHeight: 0, overflow: "auto", padding: 12}}><DataTopology schema={schema}/></div>
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import {emit_cesium_graphics_config, load_cesium_graphics_config} from "../Map/C
|
|||||||
import type {Amis_Request} from "./amis_runtime.ts"
|
import type {Amis_Request} from "./amis_runtime.ts"
|
||||||
const cesium_graphics_data_api = "/api/adminive/config/cesium_graphics/data"
|
const cesium_graphics_data_api = "/api/adminive/config/cesium_graphics/data"
|
||||||
const data_sources_api = "/api/adminive/config/data_sources"
|
const data_sources_api = "/api/adminive/config/data_sources"
|
||||||
|
const topology_collection_apis = [data_sources_api, "/api/adminive/config/data_feeds"]
|
||||||
const raw_action_apis = new Set(["/api/refresh_external_databases", "/api/refresh_external_database", "/api/upload_external_database", "/api/clear_external_database_table", "/api/restart_device"])
|
const raw_action_apis = new Set(["/api/refresh_external_databases", "/api/refresh_external_database", "/api/upload_external_database", "/api/clear_external_database_table", "/api/restart_device"])
|
||||||
export async function amis_fetcher(request: Amis_Request): Promise<AxiosResponse> {
|
export async function amis_fetcher(request: Amis_Request): Promise<AxiosResponse> {
|
||||||
const method = (request.method ?? "get").toLowerCase()
|
const method = (request.method ?? "get").toLowerCase()
|
||||||
@@ -29,5 +30,8 @@ export async function amis_fetcher(request: Amis_Request): Promise<AxiosResponse
|
|||||||
if (method !== "get" && method !== "head" && request.url.startsWith(data_sources_api) && success) {
|
if (method !== "get" && method !== "head" && request.url.startsWith(data_sources_api) && success) {
|
||||||
window.dispatchEvent(new Event("ecap-data-sources-changed"))
|
window.dispatchEvent(new Event("ecap-data-sources-changed"))
|
||||||
}
|
}
|
||||||
|
if (method !== "get" && method !== "head" && topology_collection_apis.some(api => pathname.startsWith(api)) && success) {
|
||||||
|
window.dispatchEvent(new Event("ecap-data-topology-changed"))
|
||||||
|
}
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {Data_Source_Config} from "./Data_Source/Data_Source_Config.tsx";
|
|||||||
import L from 'leaflet';
|
import L from 'leaflet';
|
||||||
|
|
||||||
const Cesium_Map_View = React.lazy(() => import("./Map/Cesium_Map.tsx"));
|
const Cesium_Map_View = React.lazy(() => import("./Map/Cesium_Map.tsx"));
|
||||||
|
const Data_Topology_View = React.lazy(() => import("./Adminive/DataTopology.tsx").then(module => ({default: module.DataTopologyPage})));
|
||||||
|
|
||||||
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
delete (L.Icon.Default.prototype as any)._getIconUrl;
|
||||||
|
|
||||||
@@ -83,6 +84,11 @@ class App extends enhance.Base {
|
|||||||
icon: <SettingOutlined/>,
|
icon: <SettingOutlined/>,
|
||||||
key: '/aircraftlist',
|
key: '/aircraftlist',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '数据拓扑',
|
||||||
|
icon: <SettingOutlined/>,
|
||||||
|
key: '/topology',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
axios.post(`${baseURL}/version`, {}).then(res => {
|
axios.post(`${baseURL}/version`, {}).then(res => {
|
||||||
@@ -166,6 +172,7 @@ class App extends enhance.Base {
|
|||||||
const map2d = <div style={{width: '100%', height: '100%', position: 'relative'}}><this.leaflet_map.x/></div>;
|
const map2d = <div style={{width: '100%', height: '100%', position: 'relative'}}><this.leaflet_map.x/></div>;
|
||||||
const settings = <this.setting.x/>;
|
const settings = <this.setting.x/>;
|
||||||
const aircraftList = <div style={{flex: 1}}><this.aircraft_list.x/></div>;
|
const aircraftList = <div style={{flex: 1}}><this.aircraft_list.x/></div>;
|
||||||
|
const dataTopology = <React.Suspense fallback={<div style={{padding: 20}}>数据拓扑加载中...</div>}><Data_Topology_View/></React.Suspense>;
|
||||||
const map3d = (fallback: string) => this.webgl_supported
|
const map3d = (fallback: string) => this.webgl_supported
|
||||||
? <div style={{width: '100%', height: '100%', position: 'relative'}}>
|
? <div style={{width: '100%', height: '100%', position: 'relative'}}>
|
||||||
<React.Suspense fallback={<div style={{padding: 20}}>3D地图加载中...</div>}>
|
<React.Suspense fallback={<div style={{padding: 20}}>3D地图加载中...</div>}>
|
||||||
@@ -215,6 +222,7 @@ class App extends enhance.Base {
|
|||||||
<Route path="/map3d" element={map3d("/map")}/>
|
<Route path="/map3d" element={map3d("/map")}/>
|
||||||
<Route path="/ui/map3d" element={map3d("/ui/map")}/>
|
<Route path="/ui/map3d" element={map3d("/ui/map")}/>
|
||||||
{["/aircraftlist", "/ui/aircraftlist"].map(path => <Route key={path} path={path} element={aircraftList}/>)}
|
{["/aircraftlist", "/ui/aircraftlist"].map(path => <Route key={path} path={path} element={aircraftList}/>)}
|
||||||
|
{["/topology", "/ui/topology"].map(path => <Route key={path} path={path} element={dataTopology}/>)}
|
||||||
<Route path="*" element={<Navigate to="/map"/>}/>
|
<Route path="*" element={<Navigate to="/map"/>}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user