修复问题

This commit is contained in:
2026-08-09 15:30:11 +08:00
parent cb86fbeca5
commit a005abee62
5 changed files with 60 additions and 531 deletions
+26 -33
View File
@@ -1,6 +1,8 @@
import axios from "axios"
import {useEffect, useState, type CSSProperties} from "react"
import {useEffect, useMemo, useState} from "react"
import {Cesium_Model_Settings, type Cesium_Model_Settings_Schema} from "../Map/Cesium_Model_Settings.tsx"
import {AmisPanel} from "./AmisPanel.tsx"
import {ArchiveOperations, type Archive_Operations_Schema} from "./ArchiveOperations.tsx"
type Composition_Node = {
kind: string
slot?: string
@@ -8,6 +10,10 @@ type Composition_Node = {
}
type Slot_Contract = {
component_kind: string
descriptor_api?: string
view_api?: string
data_api?: string
amis_api?: string
schema: Record<string, unknown>
}
type Composition_Manifest = {
@@ -21,22 +27,20 @@ type Adminive_Response<T> = {
msg: string
data: T
}
const frontend_layout: Record<string, CSSProperties> = {
mode_acs: {gridColumn: "1 / -1"},
data_feed_settings: {gridColumn: "1 / -1"},
data_sources: {gridColumn: "1 / -1"},
data_feeds: {gridColumn: "1 / -1"},
source_feed_relations: {gridColumn: "1 / -1"},
mlat: {gridColumn: "1 / -1"},
cesium_graphics: {gridColumn: "1 / -1"}
}
function collect_slots(node: Composition_Node, result: string[]) {
if (node.kind === "slot" && node.slot) {
result.push(node.slot)
}
for (const child of node.children ?? []) {
collect_slots(child, result)
}
if (node.kind === "slot" && node.slot) result.push(node.slot)
for (const child of node.children ?? []) collect_slots(child, result)
}
function CesiumModelsSlot({slot}: {slot: Slot_Contract}) {
const manager = useMemo(() => new Cesium_Model_Settings(slot.schema as unknown as Cesium_Model_Settings_Schema), [slot])
const Component = manager.x
return <Component />
}
function render_slot(name: string, slot: Slot_Contract) {
if (slot.component_kind === "amis_schema") return <AmisPanel schema={slot.schema} />
if (slot.component_kind === "ecap_archive_operations") return <ArchiveOperations schema={slot.schema as unknown as Archive_Operations_Schema} />
if (slot.component_kind === "ecap_cesium_models") return <CesiumModelsSlot slot={slot} />
return <div style={{padding: 16, color: "#ff4d4f"}}> Adminive {name} ({slot.component_kind})</div>
}
export function Adminive_Settings() {
const [manifest, set_manifest] = useState<Composition_Manifest | null>(null)
@@ -48,23 +52,12 @@ export function Adminive_Settings() {
set_error(reason instanceof Error ? reason.message : String(reason))
})
}, [])
if (error) {
return <div style={{color: "#ff4d4f", padding: 16}}>Adminive {error}</div>
}
if (!manifest) {
return <div style={{padding: 16}}>Adminive ...</div>
}
if (error) return <div style={{color: "#ff4d4f", padding: 16}}>Adminive {error}</div>
if (!manifest) return <div style={{padding: 16}}>Adminive ...</div>
const slot_names: string[] = []
collect_slots(manifest.view.body, slot_names)
return (
<div style={{display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 16, width: "100%"}}>
{slot_names.map(name => {
const slot = manifest.slots[name]
if (!slot || slot.component_kind !== "amis_schema") {
return null
}
return <div key={name} style={{minWidth: 0, ...frontend_layout[name]}}><AmisPanel schema={slot.schema} /></div>
})}
</div>
)
return <div style={{display: "grid", gridTemplateColumns: "minmax(0, 1fr)", gap: 16, width: "100%"}}>{slot_names.map(name => {
const slot = manifest.slots[name]
return slot ? <div key={name} style={{minWidth: 0}}>{render_slot(name, slot)}</div> : null
})}</div>
}
+5
View File
@@ -3,6 +3,7 @@ import {emit_cesium_graphics_config, load_cesium_graphics_config} from "../Map/C
import type {Amis_Request} from "./amis_runtime.ts"
const cesium_graphics_data_api = "/api/adminive/config/cesium_graphics/data"
const data_sources_api = "/api/adminive/config/data_sources"
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> {
const method = (request.method ?? "get").toLowerCase()
const query_method = method === "get" || method === "head"
@@ -17,6 +18,10 @@ export async function amis_fetcher(request: Amis_Request): Promise<AxiosResponse
...(request.config ?? {})
}
const response = await axios(config)
const pathname = new URL(request.url, window.location.href).pathname
if (response.status >= 200 && response.status < 300 && raw_action_apis.has(pathname) && response.data?.status === undefined) {
response.data = {status: 0, msg: "", data: response.data}
}
const success = response.status >= 200 && response.status < 300 && response.data?.status === 0
if (method === "post" && request.url === cesium_graphics_data_api && success) {
emit_cesium_graphics_config(await load_cesium_graphics_config())
+18 -7
View File
@@ -15,11 +15,22 @@ import {
type Model_Number_Key = keyof Pick<Map_Model_Item_Config, "builtInSize" | "headingOffsetDegrees" | "pitchOffsetDegrees" | "rollOffsetDegrees">
type Model_Upload_Type = "aircraft" | "base_station" | "device" | "aircraft_type"
export type Cesium_Model_Settings_Schema = {
title?: string
data_api: string
upload_accept: string
aircraft_types: string[]
}
export class Cesium_Model_Settings extends enhance.Base {
config: Map_Model_Config = {...empty_map_model_config};
number_input_drafts: Record<string, number | null> = {};
schema: Cesium_Model_Settings_Schema;
constructor(schema: Cesium_Model_Settings_Schema) {
super();
this.schema = schema;
}
async on_mount() {
this.config = await load_map_model_config();
this.config = await load_map_model_config(this.schema.data_api);
this.number_input_drafts = {};
this.flush();
}
@@ -28,7 +39,7 @@ export class Cesium_Model_Settings extends enhance.Base {
message.error("只支持上传 .glb 模型");
return false;
}
this.config = await upload_map_model(model_type, file, aircraft_model_key);
this.config = await upload_map_model(model_type, file, aircraft_model_key, this.schema.data_api);
message.success(this.model_upload_message(model_type));
this.flush();
return false;
@@ -62,7 +73,7 @@ export class Cesium_Model_Settings extends enhance.Base {
this.flush();
}
async save() {
this.config = await save_map_model_config(this.config);
this.config = await save_map_model_config(this.config, this.schema.data_api);
this.number_input_drafts = {};
message.success("模型配置已保存");
this.flush();
@@ -84,7 +95,7 @@ export class Cesium_Model_Settings extends enhance.Base {
<Space wrap>
<span style={{width: 210}}>{option.label}</span>
<Input value={model.url} readOnly style={{width: 360}} />
<Upload accept=".glb" showUploadList={false} beforeUpload={(file) => this.upload("aircraft_type", file, option.key)}>
<Upload accept={this.schema.upload_accept} showUploadList={false} beforeUpload={(file) => this.upload("aircraft_type", file, option.key)}>
<Button icon={<UploadOutlined />}>GLB</Button>
</Upload>
</Space>
@@ -97,7 +108,7 @@ export class Cesium_Model_Settings extends enhance.Base {
<Space direction="vertical" size={4}>
<Space wrap>
<Input addonBefore={label} value={model.url} readOnly style={{width: 360}} />
<Upload accept=".glb" showUploadList={false} beforeUpload={(file) => this.upload(model_type, file)}>
<Upload accept={this.schema.upload_accept} showUploadList={false} beforeUpload={(file) => this.upload(model_type, file)}>
<Button icon={<UploadOutlined />}>GLB</Button>
</Upload>
</Space>
@@ -108,7 +119,7 @@ export class Cesium_Model_Settings extends enhance.Base {
render(props: any) {
return (
<div style={setting_style}>
<h2 style={row_style}>Cesium模型设置</h2>
<h2 style={row_style}>{this.schema.title ?? "Cesium模型设置"}</h2>
<Space direction="vertical" size={8} style={col_style}>
<h3 style={row_style}></h3>
{this.render_model_upload("飞机模型", this.config.aircraftModel, "aircraft")}
@@ -117,7 +128,7 @@ export class Cesium_Model_Settings extends enhance.Base {
<h3 style={row_style}></h3>
{this.render_model_upload("设备模型", this.config.deviceModel, "device")}
<h3 style={row_style}>/</h3>
{aircraft_model_type_options.map(option => this.render_aircraft_type_model(option))}
{this.schema.aircraft_types.map(key => this.render_aircraft_type_model(aircraft_model_type_options.find(option => option.key === key) ?? {key, label: key}))}
<Button type="primary" onClick={() => this.save()}></Button>
</Space>
</div>
+6 -6
View File
@@ -114,22 +114,22 @@ function to_server_config(config: Map_Model_Config): Map_Model_Config_Json {
device_model: to_server_model_item(config.deviceModel)
};
}
export async function load_map_model_config(): Promise<Map_Model_Config> {
const response = await axios.get<Map_Model_Config_Json>("/map/models");
export async function load_map_model_config(api = "/map/models"): Promise<Map_Model_Config> {
const response = await axios.get<Map_Model_Config_Json>(api);
return from_server_config(response.data);
}
export async function save_map_model_config(config: Map_Model_Config): Promise<Map_Model_Config> {
const response = await axios.post<Map_Model_Config_Json>("/map/models", to_server_config(config));
export async function save_map_model_config(config: Map_Model_Config, api = "/map/models"): Promise<Map_Model_Config> {
const response = await axios.post<Map_Model_Config_Json>(api, to_server_config(config));
const saved_config = from_server_config(response.data);
emit_map_model_config(saved_config);
return saved_config;
}
export async function upload_map_model(model_type: "aircraft" | "base_station" | "device" | "aircraft_type", file: File, aircraft_model_key?: string): Promise<Map_Model_Config> {
export async function upload_map_model(model_type: "aircraft" | "base_station" | "device" | "aircraft_type", file: File, aircraft_model_key?: string, api = "/map/models"): Promise<Map_Model_Config> {
const form = new FormData();
form.append("model_type", model_type);
if (aircraft_model_key) form.append("aircraft_model_key", aircraft_model_key);
form.append("file", file);
const response = await axios.post<Map_Model_Config_Json>("/map/models", form);
const response = await axios.post<Map_Model_Config_Json>(api, form);
const config = from_server_config(response.data);
emit_map_model_config(config);
return config;
+5 -485
View File
@@ -1,488 +1,8 @@
import enhance from "./core/enhance.tsx";
import {Button, Flex, InputNumber, Select} from "antd";
import { Input } from 'antd';
import {baseURL, setting_style, row_style, col_style} from "./Global.js";
import { Data_Source_Config } from "./Data_Source/Data_Source_Config.tsx";
import { Device_Config } from "./Device_Config.tsx";
import axios from "axios";
import { ReloadOutlined } from "@ant-design/icons";
import { PersistentScroll } from "./PersistentScroll.tsx";
import { External_Resources_Manager } from "./External_Resources_Manager/External_Resources_Manager.tsx";
import {Adminive_Settings} from "./Adminive/Adminive_Settings.tsx";
import { Cesium_Model_Settings } from "./Map/Cesium_Model_Settings.tsx";
const { TextArea } = Input;
export class Console extends enhance.Base {
serial_number: number = 0;
log: string = "";
style = {
"width": "60vw",
}
render(props: any) {
return (
<>
<Flex gap="middle" vertical justify='center' align='center'>
<h2 style={row_style}></h2>
<Flex gap="middle" justify='center' align='center'>
<InputNumber addonBefore={"当前序列号"} value={this.serial_number}
onChange={(value: number | null) => {
this.serial_number = value;
this.flush();
}}></InputNumber>
<Button type="primary" autoInsertSpace onClick={() => {
this.log = "";
this.flush();
}}>
</Button>
<Button type="primary" autoInsertSpace
icon={<ReloadOutlined></ReloadOutlined>}
onClick={() => {
axios.post(`${baseURL}/get_debug_log`, { serial_number: this.serial_number })
.then(res => {
this.serial_number = res.data.serial_number;
const decoded = decodeURIComponent(escape(atob(res.data.log)));
this.log += decoded;
this.flush();
})
.catch(error => {
console.error("Error fetching log:", error);
});
this.flush();
}}
>
log
</Button>
</Flex>
<TextArea value={this.log} rows={20} style={this.style} />
</Flex>
</>
)
}
}
import { Upload, Space, Modal, Progress, message } from 'antd';
import { UploadOutlined, DownloadOutlined, CloudUploadOutlined } from '@ant-design/icons';
type ArchiveFormatOption = {
label: string;
value: string;
format: string;
filter: string;
extension: string;
};
const archiveFormatOptions: ArchiveFormatOption[] = [
{ label: 'ZIP (.zip)', value: 'zip', format: 'zip', filter: '', extension: 'zip' },
{ label: 'TAR (.tar)', value: 'tar', format: 'pax', filter: '', extension: 'tar' },
{ label: 'TAR.GZ (.tar.gz)', value: 'tar.gz', format: 'pax', filter: 'gzip', extension: 'tar.gz' },
{ label: 'TAR.XZ (.tar.xz)', value: 'tar.xz', format: 'pax', filter: 'xz', extension: 'tar.xz' },
{ label: 'TAR.ZST (.tar.zst)', value: 'tar.zst', format: 'pax', filter: 'zstd', extension: 'tar.zst' },
{ label: 'TAR.BZ2 (.tar.bz2)', value: 'tar.bz2', format: 'pax', filter: 'bzip2', extension: 'tar.bz2' },
{ label: 'TAR.LZ4 (.tar.lz4)', value: 'tar.lz4', format: 'pax', filter: 'lz4', extension: 'tar.lz4' },
{ label: '7Z (.7z)', value: '7z', format: '7zip', filter: '', extension: '7z' },
];
const archiveImportSuffixes = [
'.tar.zstd', '.tar.zst', '.tar.gz', '.tgz', '.tar.xz', '.txz', '.tar.bz2', '.tbz2', '.tar.lz4',
'.zip', '.7z', '.rar', '.tar', '.cpio', '.iso', '.xar'
];
const archiveImportAccept = archiveImportSuffixes.join(',');
function getArchiveImportExtension(fileName: string) {
const lowerName = fileName.toLowerCase();
const suffix = archiveImportSuffixes.find(item => lowerName.endsWith(item));
if (suffix) return suffix.slice(1);
const dot = fileName.lastIndexOf('.');
return dot >= 0 ? fileName.slice(dot + 1) : 'zip';
}
function stripArchiveImportExtension(fileName: string) {
const lowerName = fileName.toLowerCase();
const suffix = archiveImportSuffixes.find(item => lowerName.endsWith(item));
if (suffix) return fileName.slice(0, fileName.length - suffix.length);
const dot = fileName.lastIndexOf('.');
return dot > 0 ? fileName.slice(0, dot) : fileName;
}
export class Export extends enhance.Base {
exporting = false; // 导出中
exportElapsed = 0; // 已用时间(秒)
exportTimer?: number; // 定时器句柄
name : string;
url : string;
archiveFormat = archiveFormatOptions[0].value;
versionName = "";
allowVersionSelection = false;
constructor(name: string, url: string, options: { allowVersionSelection?: boolean } = {}) {
super();
this.name = name;
this.url = url;
this.allowVersionSelection = options.allowVersionSelection ?? false;
}
selectedArchiveFormat() {
return archiveFormatOptions.find(item => item.value === this.archiveFormat) ?? archiveFormatOptions[0];
}
export() {
if (this.exporting) return;
this.exporting = true;
this.exportElapsed = 0;
const start = Date.now();
// ⏱ 每秒更新时间
this.exportTimer = window.setInterval(() => {
this.exportElapsed = Math.floor((Date.now() - start) / 1000);
this.flush();
}, 1000);
this.flush();
const selectedArchive = this.selectedArchiveFormat();
const payload: {
version?: string;
format: string;
filter: string;
extension: string;
} = {
format: selectedArchive.format,
filter: selectedArchive.filter,
extension: selectedArchive.extension,
};
const version = this.versionName.trim();
if (this.allowVersionSelection && version.length > 0) {
payload.version = version;
}
axios.post(`${baseURL}/${this.url}`, payload, {
responseType: 'blob'
}).then(res => {
const ok = res.headers['ok'];
if(ok === "false"){
res.data.text().then(text => {
console.log(text)
const jsonData = JSON.parse(text);
message.error(jsonData.message);
})
return;
}
const disposition = res.headers['content-disposition'];
if (!disposition) {
message.error('后端未返回 Content-Disposition,无法确定文件名');
return;
}
const match = disposition.match(/filename\*=UTF-8''([^;]+)|filename="([^"]+)"/);
if (!match) {
message.error('Content-Disposition 中未包含 filename');
return;
}
const filename = decodeURIComponent(match[1] || match[2]);
const blob = new Blob([res.data], {
type: res.headers['content-type'] || 'application/octet-stream'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
message.success(`当前${this.name}已导出(耗时 ${this.exportElapsed}s`);
}).catch(() => {
message.error(`${this.name}导出失败`);
}).finally(() => {
this.exporting = false;
if (this.exportTimer) {
clearInterval(this.exportTimer);
this.exportTimer = undefined;
}
this.flush();
});
}
render() {
return (
<Space>
<Select
value={this.archiveFormat}
options={archiveFormatOptions.map(item => ({ label: item.label, value: item.value }))}
style={{ width: 170 }}
disabled={this.exporting}
onChange={(value: string) => {
this.archiveFormat = value;
this.flush();
}}
/>
<Button
icon={<DownloadOutlined />}
loading={this.exporting}
disabled={this.exporting}
onClick={() => this.export()}
>
{this.exporting
? `正在导出… ${this.exportElapsed}s`
: `导出当前${this.name}`}
</Button>
</Space>
)
}
}
export class Online_Upgrade extends enhance.Base {
upgrading = false; // 导入中
progress = 0;
export_version2 : Export;
export_logs : Export;
constructor() {
super();
this.export_version2 = new Export("版本", "export_version", { allowVersionSelection: true });
this.export_logs = new Export("日志", "export_logs");
}
/* ================= 导入版本 ================= */
import_version(file: File) {
if (this.upgrading) return false;
this.upgrading = true;
this.progress = 0;
this.flush();
const dir = stripArchiveImportExtension(file.name);
const archiveExtension = getArchiveImportExtension(file.name);
// axios.post(
// `${baseURL}/import_version`,
// file,
// {
// headers: {
// 'Content-Type': 'application/zip',
// 'dir': dir,
// },
// onUploadProgress: (e) => {
// if (e.total) {
// this.progress = Math.round((e.loaded / e.total) * 100);
// this.flush();
// }
// },
// }
// ).then(() => {
// message.success('版本导入完成');
// }).catch(() => {
// message.error('版本导入失败');
// }).finally(() => {
// this.upgrading = false;
// this.flush();
// });
axios.post(
`${baseURL}/import_version`,
file,
{
headers: {
'Content-Type': 'application/octet-stream',
'dir': dir,
'archive-extension': archiveExtension,
},
validateStatus: () => true,
transformRequest: [
data => data,
],
onUploadProgress: (e) => {
if (e.total) {
this.progress = Math.round((e.loaded / e.total) * 100);
this.flush();
}
},
}
).then((resp) => {
const data = resp.data || {};
if (resp.status >= 200 && resp.status < 300 && data.ok === true) {
message.success('版本导入完成');
return;
}
const reason =
data.error ||
data.message ||
resp.statusText ||
'未知错误';
if (resp.status === 400) {
message.error(`版本导入失败:参数或压缩包无效:${reason}`);
} else if (resp.status === 500) {
message.error(`版本导入失败:服务器内部错误:${reason}`);
} else {
message.error(`版本导入失败:HTTP ${resp.status}${reason}`);
}
console.error('版本导入失败详情:', {
status: resp.status,
data,
});
}).catch((err) => {
let reason = '未知错误';
if (err.response) {
reason =
err.response.data?.error ||
err.response.data?.message ||
err.response.statusText ||
err.message;
} else if (err.request) {
reason = '服务器无响应,请检查地址、服务状态或跨域配置';
} else if (err.message) {
reason = err.message;
}
message.error(`版本导入失败:${reason}`);
console.error('版本导入异常:', err);
}).finally(() => {
this.upgrading = false;
this.flush();
});
return false;
}
render() {
return (
<div style={setting_style}>
<h2 style={row_style}></h2>
<div style={col_style}>
<this.export_version2.x></this.export_version2.x> <br/>
<this.export_logs.x></this.export_logs.x> <br/>
<Upload
accept={archiveImportAccept}
showUploadList={false}
beforeUpload={(file) => this.import_version(file)}
disabled={this.upgrading}
>
<Button
danger
icon={<UploadOutlined />}
loading={this.upgrading}
>
</Button>
</Upload>
</div>
<Modal
open={this.upgrading}
footer={null}
closable={false}
title="正在导入版本"
>
<Progress percent={this.progress} />
<div style={{ marginTop: 8 }}>
</div>
</Modal>
</div>
);
}
}
import enhance from "./core/enhance.tsx"
import {Adminive_Settings} from "./Adminive/Adminive_Settings.tsx"
import {PersistentScroll} from "./PersistentScroll.tsx"
export class Settings extends enhance.Base {
// 第一步:仅声明子组件属性,不即时实例化
console: Console | null = null;
data_source_config: Data_Source_Config | null = null;
device_config: Device_Config | null = null;
config_Import_Export_Upgrade: Online_Upgrade | null = null;
external_resources_manager: External_Resources_Manager | null = null;
cesium_model_settings: Cesium_Model_Settings | null = null;
isRebooting: boolean = false;
// 第二步:在构造函数中延迟初始化子组件
constructor() {
super();
this.initSubComponents();
}
// 新增:子组件初始化方法
initSubComponents = () => {
this.console = new Console();
this.data_source_config = new Data_Source_Config();
this.device_config = new Device_Config();
this.config_Import_Export_Upgrade = new Online_Upgrade();
this.external_resources_manager = new External_Resources_Manager();
this.cesium_model_settings = new Cesium_Model_Settings();
};
render(props: any) {
// 第三步:渲染前校验子组件是否初始化完成
if (!this.console || !this.data_source_config || !this.device_config || !this.config_Import_Export_Upgrade || !this.external_resources_manager || !this.cesium_model_settings) {
return <div style={{ textAlign: 'center', padding: '20px' }}>...</div>;
}
return (
<>
<PersistentScroll scrollKey="Settings" style={{
gap: '16px',
// display: 'flex',
// flexDirection: 'column',
// alignItems: 'center',
}}>
<Adminive_Settings />
<this.external_resources_manager.x></this.external_resources_manager.x>
<this.config_Import_Export_Upgrade.x></this.config_Import_Export_Upgrade.x>
<this.cesium_model_settings.x></this.cesium_model_settings.x>
<br></br>
<div style={setting_style}>
<Button
type="primary"
onClick={()=>{
this.isRebooting = true;
axios.post(`${baseURL}/restart_device`).then(res => {
});
}}
loading={this.isRebooting}
disabled={this.isRebooting} // 禁用按钮,防止重复点击
>
fpga设备
</Button>
</div>
<div style={{
height: "500px",
}}></div>
</PersistentScroll>
</>
)
render() {
return <PersistentScroll scrollKey="Settings" style={{gap: "16px"}}><Adminive_Settings /><div style={{height: 500}} /></PersistentScroll>
}
}