优化高程数据管理交互并补充分析进度与文件明细能力
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
@@ -10,10 +10,12 @@ from ...schemas.elevation import (
|
||||
ElevationApplyJobCreateResponse,
|
||||
ElevationApplyJobListResponse,
|
||||
ElevationApplyJobSummary,
|
||||
ElevationDatasetAnalysisTaskStatusResponse,
|
||||
ElevationDatasetAnalyzeResponse,
|
||||
ElevationDatasetBatchImportResponse,
|
||||
ElevationDatasetDataImportResponse,
|
||||
ElevationDatasetCreateRequest,
|
||||
ElevationDatasetFileListResponse,
|
||||
ElevationDatasetListResponse,
|
||||
ElevationDatasetPreviewResponse,
|
||||
ElevationDatasetSummary,
|
||||
@@ -24,9 +26,11 @@ from ...services.elevation_service import (
|
||||
create_apply_job,
|
||||
create_dataset,
|
||||
delete_dataset,
|
||||
get_dataset_analysis_task_status,
|
||||
get_job_by_id,
|
||||
import_dataset_data_files,
|
||||
import_datasets_from_csv,
|
||||
list_dataset_files,
|
||||
list_datasets,
|
||||
list_jobs,
|
||||
preview_dataset,
|
||||
@@ -139,6 +143,24 @@ def preview_elevation_dataset(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/datasets/{dataset_id}/files", response_model=ElevationDatasetFileListResponse)
|
||||
def get_elevation_dataset_files(
|
||||
dataset_id: str,
|
||||
_: CurrentUser = Depends(require_any_permission("elevation.read", "elevation.manage")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> ElevationDatasetFileListResponse:
|
||||
return list_dataset_files(db, dataset_id=dataset_id)
|
||||
|
||||
|
||||
@router.get("/datasets/{dataset_id}/analysis-task", response_model=ElevationDatasetAnalysisTaskStatusResponse)
|
||||
def get_elevation_dataset_analysis_task_status(
|
||||
dataset_id: str,
|
||||
_: CurrentUser = Depends(require_any_permission("elevation.read", "elevation.manage")),
|
||||
db: Session = Depends(get_db),
|
||||
) -> ElevationDatasetAnalysisTaskStatusResponse:
|
||||
return get_dataset_analysis_task_status(db, dataset_id=dataset_id)
|
||||
|
||||
|
||||
@router.get("/jobs", response_model=ElevationApplyJobListResponse)
|
||||
def get_elevation_jobs(
|
||||
line_id: str | None = Query(default=None),
|
||||
|
||||
@@ -230,6 +230,52 @@ def _ensure_elevation_dataset_column_compatibility() -> None:
|
||||
"Detected missing elevation_dataset.usage_status; added with default 'idle'.",
|
||||
)
|
||||
|
||||
if "analysis_task_id" not in column_names:
|
||||
connection.execute(
|
||||
text("ALTER TABLE elevation_dataset ADD COLUMN IF NOT EXISTS analysis_task_id VARCHAR(128)"),
|
||||
)
|
||||
logger.warning(
|
||||
"Detected missing elevation_dataset.analysis_task_id; added nullable analysis task id column.",
|
||||
)
|
||||
|
||||
if "analysis_status" not in column_names:
|
||||
connection.execute(
|
||||
text("ALTER TABLE elevation_dataset ADD COLUMN IF NOT EXISTS analysis_status VARCHAR(32)"),
|
||||
)
|
||||
connection.execute(
|
||||
text("UPDATE elevation_dataset SET analysis_status = 'not_started' WHERE analysis_status IS NULL"),
|
||||
)
|
||||
connection.execute(
|
||||
text("ALTER TABLE elevation_dataset ALTER COLUMN analysis_status SET NOT NULL"),
|
||||
)
|
||||
logger.warning(
|
||||
"Detected missing elevation_dataset.analysis_status; added with default 'not_started'.",
|
||||
)
|
||||
|
||||
if "analysis_error_message" not in column_names:
|
||||
connection.execute(
|
||||
text("ALTER TABLE elevation_dataset ADD COLUMN IF NOT EXISTS analysis_error_message TEXT"),
|
||||
)
|
||||
logger.warning(
|
||||
"Detected missing elevation_dataset.analysis_error_message; added nullable analysis error column.",
|
||||
)
|
||||
|
||||
if "analysis_started_at" not in column_names:
|
||||
connection.execute(
|
||||
text("ALTER TABLE elevation_dataset ADD COLUMN IF NOT EXISTS analysis_started_at TIMESTAMPTZ"),
|
||||
)
|
||||
logger.warning(
|
||||
"Detected missing elevation_dataset.analysis_started_at; added nullable analysis start time column.",
|
||||
)
|
||||
|
||||
if "analysis_finished_at" not in column_names:
|
||||
connection.execute(
|
||||
text("ALTER TABLE elevation_dataset ADD COLUMN IF NOT EXISTS analysis_finished_at TIMESTAMPTZ"),
|
||||
)
|
||||
logger.warning(
|
||||
"Detected missing elevation_dataset.analysis_finished_at; added nullable analysis finish time column.",
|
||||
)
|
||||
|
||||
|
||||
def get_db() -> Generator[Session, None, None]:
|
||||
db = SessionLocal()
|
||||
|
||||
@@ -37,6 +37,11 @@ class ElevationDataset(Base):
|
||||
resolution_m: Mapped[float | None] = mapped_column(Float)
|
||||
status: Mapped[str] = mapped_column(String(32), default="active", index=True)
|
||||
usage_status: Mapped[str] = mapped_column(String(32), default="idle", index=True)
|
||||
analysis_task_id: Mapped[str | None] = mapped_column(String(128), index=True)
|
||||
analysis_status: Mapped[str] = mapped_column(String(32), default="not_started", index=True)
|
||||
analysis_error_message: Mapped[str | None] = mapped_column(Text)
|
||||
analysis_started_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
analysis_finished_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
sample_count: Mapped[int] = mapped_column(Integer, default=0)
|
||||
bbox_min_lon: Mapped[float | None] = mapped_column(Float)
|
||||
bbox_max_lon: Mapped[float | None] = mapped_column(Float)
|
||||
|
||||
@@ -29,6 +29,11 @@ class ElevationDatasetSummary(BaseModel):
|
||||
bbox_max_lon: float | None = None
|
||||
bbox_min_lat: float | None = None
|
||||
bbox_max_lat: float | None = None
|
||||
analysis_task_id: str | None = None
|
||||
analysis_status: str = "not_started"
|
||||
analysis_error_message: str | None = None
|
||||
analysis_started_at: datetime | None = None
|
||||
analysis_finished_at: datetime | None = None
|
||||
notes: str | None = None
|
||||
create_date: datetime
|
||||
create_user: str | None = None
|
||||
@@ -137,6 +142,34 @@ class ElevationDatasetDataImportResponse(BaseModel):
|
||||
imported_files: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ElevationDatasetFileItem(BaseModel):
|
||||
path: str
|
||||
name: str
|
||||
size: int
|
||||
modified_at: datetime | None = None
|
||||
mime_type: str | None = None
|
||||
|
||||
|
||||
class ElevationDatasetFileListResponse(BaseModel):
|
||||
dataset_id: str
|
||||
dataset_code: str
|
||||
dataset_dir: str
|
||||
mount_code: str
|
||||
items: list[ElevationDatasetFileItem] = Field(default_factory=list)
|
||||
total: int = 0
|
||||
|
||||
|
||||
class ElevationDatasetAnalysisTaskStatusResponse(BaseModel):
|
||||
dataset_id: str
|
||||
dataset_code: str
|
||||
task_id: str | None = None
|
||||
status: Literal["queued", "running", "success", "failed", "unknown", "not_found"] = "not_found"
|
||||
detail: str | None = None
|
||||
started_at: datetime | None = None
|
||||
finished_at: datetime | None = None
|
||||
update_date: datetime | None = None
|
||||
|
||||
|
||||
class ElevationApplyJobSummary(BaseModel):
|
||||
id: str
|
||||
line_id: str
|
||||
|
||||
@@ -25,12 +25,15 @@ from ..schemas.elevation import (
|
||||
ElevationApplyJobCreateResponse,
|
||||
ElevationApplyJobListResponse,
|
||||
ElevationApplyJobSummary,
|
||||
ElevationDatasetAnalysisTaskStatusResponse,
|
||||
ElevationDatasetAnalyzeResponse,
|
||||
ElevationDatasetBatchImportResponse,
|
||||
ElevationDatasetDataImportResponse,
|
||||
ElevationDatasetPreviewCell,
|
||||
ElevationDatasetPreviewDiagnostics,
|
||||
ElevationDatasetCreateRequest,
|
||||
ElevationDatasetFileItem,
|
||||
ElevationDatasetFileListResponse,
|
||||
ElevationDatasetListResponse,
|
||||
ElevationDatasetPreviewPoint,
|
||||
ElevationDatasetPreviewResponse,
|
||||
@@ -119,6 +122,11 @@ def serialize_dataset(item: ElevationDataset) -> ElevationDatasetSummary:
|
||||
bbox_max_lon=item.bbox_max_lon,
|
||||
bbox_min_lat=item.bbox_min_lat,
|
||||
bbox_max_lat=item.bbox_max_lat,
|
||||
analysis_task_id=item.analysis_task_id,
|
||||
analysis_status=item.analysis_status,
|
||||
analysis_error_message=item.analysis_error_message,
|
||||
analysis_started_at=item.analysis_started_at,
|
||||
analysis_finished_at=item.analysis_finished_at,
|
||||
notes=item.notes,
|
||||
create_date=item.create_date,
|
||||
create_user=item.create_user,
|
||||
@@ -207,6 +215,50 @@ def get_dataset_by_code(db: Session, code: str) -> ElevationDataset | None:
|
||||
).scalar_one_or_none()
|
||||
|
||||
|
||||
def list_dataset_files(
|
||||
db: Session,
|
||||
*,
|
||||
dataset_id: str,
|
||||
) -> ElevationDatasetFileListResponse:
|
||||
dataset = get_dataset_by_id(db, dataset_id)
|
||||
if not dataset:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="高程数据集不存在")
|
||||
|
||||
mount = _require_mount(db, dataset.mount_code)
|
||||
driver = _build_driver_or_400(mount)
|
||||
dataset_dir = _resolve_dataset_dir(dataset.code)
|
||||
try:
|
||||
entries = driver.list_dir(dataset_dir)
|
||||
except StoragePathNotFoundError:
|
||||
entries = []
|
||||
except StorageInvalidPathError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
|
||||
except StorageDriverError as exc:
|
||||
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
|
||||
|
||||
files = [
|
||||
ElevationDatasetFileItem(
|
||||
path=item.path,
|
||||
name=item.name,
|
||||
size=max(0, int(item.size)),
|
||||
modified_at=item.modified_at,
|
||||
mime_type=item.mime_type,
|
||||
)
|
||||
for item in entries
|
||||
if not item.is_dir
|
||||
]
|
||||
files.sort(key=lambda item: item.name.lower())
|
||||
|
||||
return ElevationDatasetFileListResponse(
|
||||
dataset_id=dataset.id,
|
||||
dataset_code=dataset.code,
|
||||
dataset_dir=dataset_dir,
|
||||
mount_code=dataset.mount_code,
|
||||
items=files,
|
||||
total=len(files),
|
||||
)
|
||||
|
||||
|
||||
def create_dataset(
|
||||
db: Session,
|
||||
payload: ElevationDatasetCreateRequest,
|
||||
@@ -445,6 +497,14 @@ def import_dataset_data_files(
|
||||
task = _dispatch_elevation_dataset_analysis_task(dataset_id=dataset.id, actor_user_id=actor.id)
|
||||
analysis_task_queued = True
|
||||
analysis_task_id = str(task.id)
|
||||
dataset.analysis_task_id = analysis_task_id
|
||||
dataset.analysis_status = "queued"
|
||||
dataset.analysis_error_message = None
|
||||
dataset.analysis_started_at = None
|
||||
dataset.analysis_finished_at = None
|
||||
dataset.update_date = utcnow()
|
||||
dataset.update_user = actor.id
|
||||
db.commit()
|
||||
except Exception as exc: # pragma: no cover
|
||||
warnings.append(f"自动分析任务派发失败:{exc}")
|
||||
|
||||
@@ -469,6 +529,46 @@ def import_dataset_data_files(
|
||||
)
|
||||
|
||||
|
||||
def get_dataset_analysis_task_status(
|
||||
db: Session,
|
||||
*,
|
||||
dataset_id: str,
|
||||
) -> ElevationDatasetAnalysisTaskStatusResponse:
|
||||
dataset = get_dataset_by_id(db, dataset_id)
|
||||
if not dataset:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="高程数据集不存在")
|
||||
|
||||
status_value = dataset.analysis_status or "not_started"
|
||||
status_map = {
|
||||
"queued": "queued",
|
||||
"running": "running",
|
||||
"success": "success",
|
||||
"failed": "failed",
|
||||
"unknown": "unknown",
|
||||
"not_started": "not_found",
|
||||
}
|
||||
mapped_status = status_map.get(status_value, "unknown")
|
||||
detail = dataset.analysis_error_message
|
||||
if detail is None:
|
||||
if mapped_status == "queued":
|
||||
detail = "分析任务已提交,等待执行。"
|
||||
elif mapped_status == "running":
|
||||
detail = "分析任务执行中。"
|
||||
elif mapped_status == "success":
|
||||
detail = "最近一次分析已完成。"
|
||||
|
||||
return ElevationDatasetAnalysisTaskStatusResponse(
|
||||
dataset_id=dataset.id,
|
||||
dataset_code=dataset.code,
|
||||
task_id=dataset.analysis_task_id,
|
||||
status=mapped_status, # type: ignore[arg-type]
|
||||
detail=detail,
|
||||
started_at=dataset.analysis_started_at,
|
||||
finished_at=dataset.analysis_finished_at,
|
||||
update_date=dataset.update_date,
|
||||
)
|
||||
|
||||
|
||||
def update_dataset(
|
||||
db: Session,
|
||||
dataset_id: str,
|
||||
@@ -552,13 +652,32 @@ def analyze_dataset(
|
||||
if item.status != "active":
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="高程数据集未启用")
|
||||
|
||||
stats, warnings = _analyze_dataset_content(db, item)
|
||||
item.analysis_status = "running"
|
||||
item.analysis_error_message = None
|
||||
if item.analysis_started_at is None:
|
||||
item.analysis_started_at = utcnow()
|
||||
item.analysis_finished_at = None
|
||||
item.update_date = utcnow()
|
||||
db.commit()
|
||||
|
||||
try:
|
||||
stats, warnings = _analyze_dataset_content(db, item)
|
||||
except Exception as exc:
|
||||
item.analysis_status = "failed"
|
||||
item.analysis_error_message = str(exc)
|
||||
item.analysis_finished_at = utcnow()
|
||||
item.update_date = utcnow()
|
||||
db.commit()
|
||||
raise
|
||||
|
||||
item.sample_count = stats["sample_count"]
|
||||
item.bbox_min_lon = stats["bbox_min_lon"]
|
||||
item.bbox_max_lon = stats["bbox_max_lon"]
|
||||
item.bbox_min_lat = stats["bbox_min_lat"]
|
||||
item.bbox_max_lat = stats["bbox_max_lat"]
|
||||
item.analysis_status = "success"
|
||||
item.analysis_error_message = None
|
||||
item.analysis_finished_at = utcnow()
|
||||
item.update_user = actor.id
|
||||
item.update_date = utcnow()
|
||||
db.commit()
|
||||
@@ -858,13 +977,59 @@ def execute_dataset_analysis_job(*, dataset_id: str, actor_user_id: str | None)
|
||||
if not item:
|
||||
return
|
||||
|
||||
item.analysis_status = "running"
|
||||
item.analysis_error_message = None
|
||||
item.analysis_started_at = utcnow()
|
||||
item.analysis_finished_at = None
|
||||
item.update_date = utcnow()
|
||||
db.commit()
|
||||
_publish_elevation_change(
|
||||
"elevation.dataset.analysis.running",
|
||||
{"action": "dataset_analysis_running", "dataset_id": item.id},
|
||||
)
|
||||
|
||||
actor = db.execute(select(User).where(User.id == actor_user_id)).scalar_one_or_none() if actor_user_id else None
|
||||
if actor is None:
|
||||
actor = db.execute(select(User).where(User.status == "active").order_by(User.id.asc())).scalars().first()
|
||||
if actor is None:
|
||||
item.analysis_status = "failed"
|
||||
item.analysis_error_message = "未找到可用用户执行分析"
|
||||
item.analysis_finished_at = utcnow()
|
||||
item.update_date = utcnow()
|
||||
db.commit()
|
||||
_publish_elevation_change(
|
||||
"elevation.dataset.analysis.failed",
|
||||
{"action": "dataset_analysis_failed", "dataset_id": item.id},
|
||||
)
|
||||
return
|
||||
|
||||
analyze_dataset(db, dataset_id=dataset_id, actor=actor)
|
||||
saved = get_dataset_by_id(db, dataset_id)
|
||||
if saved is None:
|
||||
return
|
||||
saved.analysis_status = "success"
|
||||
saved.analysis_error_message = None
|
||||
saved.analysis_finished_at = utcnow()
|
||||
saved.update_date = utcnow()
|
||||
saved.update_user = actor.id
|
||||
db.commit()
|
||||
_publish_elevation_change(
|
||||
"elevation.dataset.analysis.success",
|
||||
{"action": "dataset_analysis_success", "dataset_id": saved.id},
|
||||
)
|
||||
except Exception as exc:
|
||||
failed = get_dataset_by_id(db, dataset_id)
|
||||
if failed is not None:
|
||||
failed.analysis_status = "failed"
|
||||
failed.analysis_error_message = str(exc)
|
||||
failed.analysis_finished_at = utcnow()
|
||||
failed.update_date = utcnow()
|
||||
db.commit()
|
||||
_publish_elevation_change(
|
||||
"elevation.dataset.analysis.failed",
|
||||
{"action": "dataset_analysis_failed", "dataset_id": failed.id},
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ChangeEvent, useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
App,
|
||||
Alert,
|
||||
Descriptions,
|
||||
Empty,
|
||||
@@ -12,15 +11,17 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Progress,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
Progress,
|
||||
Upload,
|
||||
message,
|
||||
} from "antd";
|
||||
import type { UploadFile } from "antd/es/upload/interface";
|
||||
import type { ColumnsType } from "antd/es/table";
|
||||
|
||||
import { useAuth } from "@/components/auth-provider";
|
||||
@@ -32,9 +33,11 @@ import type {
|
||||
ElevationApplyJobCreateResponse,
|
||||
ElevationApplyJobListResponse,
|
||||
ElevationApplyJobSummary,
|
||||
ElevationDatasetAnalyzeResponse,
|
||||
ElevationDatasetAnalysisTaskStatusResponse,
|
||||
ElevationDatasetBatchImportResponse,
|
||||
ElevationDatasetDataImportResponse,
|
||||
ElevationDatasetFileItem,
|
||||
ElevationDatasetFileListResponse,
|
||||
ElevationDatasetListResponse,
|
||||
ElevationDatasetPreviewResponse,
|
||||
ElevationDatasetSummary,
|
||||
@@ -84,7 +87,7 @@ function applyModeLabel(mode: string): string {
|
||||
return mode;
|
||||
}
|
||||
|
||||
function formatDate(value: string | null): string {
|
||||
function formatDate(value: string | null | undefined): string {
|
||||
if (!value) return "-";
|
||||
return new Date(value).toLocaleString();
|
||||
}
|
||||
@@ -94,6 +97,14 @@ function formatNumber(value: number | null | undefined, digits = 6): string {
|
||||
return Number(value).toFixed(digits);
|
||||
}
|
||||
|
||||
function formatFileSize(size: number): string {
|
||||
if (!Number.isFinite(size) || size < 0) return "-";
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
|
||||
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function readXhrError(xhr: XMLHttpRequest): string {
|
||||
const fallback = `HTTP ${xhr.status}`;
|
||||
const raw = xhr.responseText?.trim();
|
||||
@@ -109,7 +120,6 @@ function readXhrError(xhr: XMLHttpRequest): string {
|
||||
}
|
||||
|
||||
export default function AdminElevationPage() {
|
||||
const { modal } = App.useApp();
|
||||
const queryClient = useQueryClient();
|
||||
const {
|
||||
user,
|
||||
@@ -120,20 +130,33 @@ export default function AdminElevationPage() {
|
||||
refreshAccessToken,
|
||||
} = useAuth();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
|
||||
const [error, setError] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
const [datasetModalOpen, setDatasetModalOpen] = useState(false);
|
||||
const [applyModalOpen, setApplyModalOpen] = useState(false);
|
||||
const [previewModalOpen, setPreviewModalOpen] = useState(false);
|
||||
const [previewDataset, setPreviewDataset] = useState<ElevationDatasetSummary | null>(null);
|
||||
const [previewData, setPreviewData] = useState<ElevationDatasetPreviewResponse | null>(null);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [analyzingDatasetId, setAnalyzingDatasetId] = useState<string | null>(null);
|
||||
|
||||
const [importModalOpen, setImportModalOpen] = useState(false);
|
||||
const [importDataset, setImportDataset] = useState<ElevationDatasetSummary | null>(null);
|
||||
const [importFileList, setImportFileList] = useState<UploadFile[]>([]);
|
||||
const [datasetDataUploadProgress, setDatasetDataUploadProgress] = useState(0);
|
||||
const [datasetDataUploadFileName, setDatasetDataUploadFileName] = useState("");
|
||||
const [datasetDataUploadingDatasetId, setDatasetDataUploadingDatasetId] = useState<string | null>(null);
|
||||
const [lastImportedFiles, setLastImportedFiles] = useState<string[]>([]);
|
||||
const [lastAnalysisTaskId, setLastAnalysisTaskId] = useState<string | null>(null);
|
||||
|
||||
const [datasetFilesModalOpen, setDatasetFilesModalOpen] = useState(false);
|
||||
const [datasetFilesDataset, setDatasetFilesDataset] = useState<ElevationDatasetSummary | null>(null);
|
||||
const [datasetFiles, setDatasetFiles] = useState<ElevationDatasetFileItem[]>([]);
|
||||
const [datasetFilesLoading, setDatasetFilesLoading] = useState(false);
|
||||
|
||||
const [analysisModalOpen, setAnalysisModalOpen] = useState(false);
|
||||
const [analysisDataset, setAnalysisDataset] = useState<ElevationDatasetSummary | null>(null);
|
||||
|
||||
const importInputRef = useRef<HTMLInputElement | null>(null);
|
||||
const datasetDataImportRefs = useRef<Record<string, HTMLInputElement | null>>({});
|
||||
|
||||
const [datasetForm] = Form.useForm<DatasetFormValues>();
|
||||
const [applyForm] = Form.useForm<ApplyFormValues>();
|
||||
|
||||
@@ -214,38 +237,6 @@ export default function AdminElevationPage() {
|
||||
}, [refreshPowerLines]),
|
||||
);
|
||||
|
||||
const analyzeMutation = useMutation({
|
||||
mutationFn: async (datasetId: string) => {
|
||||
const response = await fetchWithAuth(`/api/v1/elevation/datasets/${datasetId}/analyze`, {
|
||||
method: "POST",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await readApiError(response));
|
||||
}
|
||||
return (await response.json()) as ElevationDatasetAnalyzeResponse;
|
||||
},
|
||||
onMutate: (datasetId) => {
|
||||
setAnalyzingDatasetId(datasetId);
|
||||
},
|
||||
onSuccess: async (payload) => {
|
||||
const warnings = payload.warnings.length;
|
||||
const msg = warnings > 0 ? `分析完成(${warnings} 条告警)` : "分析完成";
|
||||
setSuccess(msg);
|
||||
setError("");
|
||||
messageApi.success(msg);
|
||||
await refreshElevationData();
|
||||
},
|
||||
onError: (candidate) => {
|
||||
const nextError = candidate instanceof Error ? candidate.message : "分析失败";
|
||||
setError(nextError);
|
||||
setSuccess("");
|
||||
messageApi.error(nextError);
|
||||
},
|
||||
onSettled: () => {
|
||||
setAnalyzingDatasetId(null);
|
||||
},
|
||||
});
|
||||
|
||||
const datasetCreateMutation = useMutation({
|
||||
mutationFn: async (values: DatasetFormValues) => {
|
||||
const payload = {
|
||||
@@ -268,7 +259,6 @@ export default function AdminElevationPage() {
|
||||
return (await response.json()) as ElevationDatasetSummary;
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setSuccess("高程数据集已创建");
|
||||
setError("");
|
||||
messageApi.success("高程数据集已创建");
|
||||
setDatasetModalOpen(false);
|
||||
@@ -278,7 +268,6 @@ export default function AdminElevationPage() {
|
||||
onError: (candidate) => {
|
||||
const nextError = candidate instanceof Error ? candidate.message : "创建高程数据集失败";
|
||||
setError(nextError);
|
||||
setSuccess("");
|
||||
messageApi.error(nextError);
|
||||
},
|
||||
});
|
||||
@@ -289,7 +278,6 @@ export default function AdminElevationPage() {
|
||||
setDatasetDataUploadFileName(
|
||||
payload.files.length === 1 ? payload.files[0].name : `共 ${payload.files.length} 个文件`,
|
||||
);
|
||||
setDatasetDataUploadingDatasetId(payload.datasetId);
|
||||
|
||||
const uploadWithXhr = (token: string | null) =>
|
||||
new Promise<ElevationDatasetDataImportResponse>((resolve, reject) => {
|
||||
@@ -349,28 +337,21 @@ export default function AdminElevationPage() {
|
||||
return result;
|
||||
},
|
||||
onSuccess: async (payload) => {
|
||||
const monitorHint = payload.analysis_task_id
|
||||
? `,分析任务已入队(Task ID: ${payload.analysis_task_id},可在“任务监控”查看进度)`
|
||||
: "";
|
||||
const monitorHint = payload.analysis_task_id ? `,分析任务ID:${payload.analysis_task_id}` : "";
|
||||
const msg = payload.warning_count > 0
|
||||
? `数据导入完成:上传 ${payload.uploaded_file_count} 个、解压 ${payload.extracted_file_count} 个、可用 ${payload.imported_file_count} 个,告警 ${payload.warning_count} 条${monitorHint}`
|
||||
: `数据导入完成:上传 ${payload.uploaded_file_count} 个、解压 ${payload.extracted_file_count} 个、可用 ${payload.imported_file_count} 个${monitorHint}`;
|
||||
setSuccess(msg);
|
||||
setError("");
|
||||
messageApi.success(msg);
|
||||
setLastImportedFiles(payload.imported_files);
|
||||
setLastAnalysisTaskId(payload.analysis_task_id);
|
||||
await refreshElevationData();
|
||||
setDatasetDataUploadProgress(0);
|
||||
setDatasetDataUploadFileName("");
|
||||
setDatasetDataUploadingDatasetId(null);
|
||||
},
|
||||
onError: (candidate) => {
|
||||
const nextError = candidate instanceof Error ? candidate.message : "导入高程数据失败";
|
||||
setError(nextError);
|
||||
setSuccess("");
|
||||
messageApi.error(nextError);
|
||||
setDatasetDataUploadProgress(0);
|
||||
setDatasetDataUploadFileName("");
|
||||
setDatasetDataUploadingDatasetId(null);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -391,7 +372,6 @@ export default function AdminElevationPage() {
|
||||
const msg = payload.warning_count > 0
|
||||
? `批量导入完成:新增 ${payload.imported_count} 条,分析 ${payload.analyzed_count} 条,跳过 ${payload.skipped_count} 条,告警 ${payload.warning_count} 条`
|
||||
: `批量导入完成:新增 ${payload.imported_count} 条,分析 ${payload.analyzed_count} 条,跳过 ${payload.skipped_count} 条`;
|
||||
setSuccess(msg);
|
||||
setError("");
|
||||
messageApi.success(msg);
|
||||
await refreshElevationData();
|
||||
@@ -399,7 +379,6 @@ export default function AdminElevationPage() {
|
||||
onError: (candidate) => {
|
||||
const nextError = candidate instanceof Error ? candidate.message : "批量导入高程数据集失败";
|
||||
setError(nextError);
|
||||
setSuccess("");
|
||||
messageApi.error(nextError);
|
||||
},
|
||||
});
|
||||
@@ -421,7 +400,6 @@ export default function AdminElevationPage() {
|
||||
return (await response.json()) as ElevationApplyJobCreateResponse;
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setSuccess("高程回填任务已提交");
|
||||
setError("");
|
||||
messageApi.success("高程回填任务已提交");
|
||||
setApplyModalOpen(false);
|
||||
@@ -431,7 +409,6 @@ export default function AdminElevationPage() {
|
||||
onError: (candidate) => {
|
||||
const nextError = candidate instanceof Error ? candidate.message : "提交回填任务失败";
|
||||
setError(nextError);
|
||||
setSuccess("");
|
||||
messageApi.error(nextError);
|
||||
},
|
||||
});
|
||||
@@ -446,7 +423,6 @@ export default function AdminElevationPage() {
|
||||
}
|
||||
},
|
||||
onSuccess: async () => {
|
||||
setSuccess("高程数据集已删除");
|
||||
setError("");
|
||||
messageApi.success("高程数据集已删除");
|
||||
setPreviewModalOpen(false);
|
||||
@@ -458,11 +434,46 @@ export default function AdminElevationPage() {
|
||||
onError: (candidate) => {
|
||||
const nextError = candidate instanceof Error ? candidate.message : "删除高程数据集失败";
|
||||
setError(nextError);
|
||||
setSuccess("");
|
||||
messageApi.error(nextError);
|
||||
},
|
||||
});
|
||||
|
||||
const datasetFilesMutation = useMutation({
|
||||
mutationFn: async (datasetId: string) => {
|
||||
const response = await fetchWithAuth(`/api/v1/elevation/datasets/${datasetId}/files`);
|
||||
if (!response.ok) {
|
||||
throw new Error(await readApiError(response));
|
||||
}
|
||||
return (await response.json()) as ElevationDatasetFileListResponse;
|
||||
},
|
||||
onSuccess: (payload) => {
|
||||
setDatasetFiles(payload.items);
|
||||
setDatasetFilesLoading(false);
|
||||
setError("");
|
||||
},
|
||||
onError: (candidate) => {
|
||||
const nextError = candidate instanceof Error ? candidate.message : "加载文件明细失败";
|
||||
setError(nextError);
|
||||
messageApi.error(nextError);
|
||||
setDatasetFiles([]);
|
||||
setDatasetFilesLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const analysisStatusQuery = useQuery({
|
||||
queryKey: ["/api/v1/elevation/datasets/analysis-task", analysisDataset?.id],
|
||||
enabled: !!analysisDataset,
|
||||
queryFn: async () => {
|
||||
const response = await fetchWithAuth(`/api/v1/elevation/datasets/${analysisDataset?.id}/analysis-task`);
|
||||
if (!response.ok) {
|
||||
throw new Error(await readApiError(response));
|
||||
}
|
||||
return (await response.json()) as ElevationDatasetAnalysisTaskStatusResponse;
|
||||
},
|
||||
refetchInterval: analysisModalOpen ? 3000 : false,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const datasets = datasetsQuery.data?.items ?? [];
|
||||
const jobs = jobsQuery.data?.items ?? [];
|
||||
const lines = linesQuery.data?.items ?? [];
|
||||
@@ -486,6 +497,32 @@ export default function AdminElevationPage() {
|
||||
[datasets],
|
||||
);
|
||||
|
||||
const fileColumns = useMemo<ColumnsType<ElevationDatasetFileItem>>(
|
||||
() => [
|
||||
{ title: "文件名", dataIndex: "name", width: 260 },
|
||||
{ title: "路径", dataIndex: "path", width: 420 },
|
||||
{
|
||||
title: "大小",
|
||||
dataIndex: "size",
|
||||
width: 120,
|
||||
render: (value: number) => formatFileSize(value),
|
||||
},
|
||||
{
|
||||
title: "修改时间",
|
||||
dataIndex: "modified_at",
|
||||
width: 180,
|
||||
render: (value: string | null) => formatDate(value),
|
||||
},
|
||||
{
|
||||
title: "类型",
|
||||
dataIndex: "mime_type",
|
||||
width: 160,
|
||||
render: (value: string | null) => value || "-",
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
const datasetColumns = useMemo<ColumnsType<ElevationDatasetSummary>>(
|
||||
() => [
|
||||
{ title: "编码", dataIndex: "code", width: 140 },
|
||||
@@ -502,6 +539,21 @@ export default function AdminElevationPage() {
|
||||
width: 90,
|
||||
render: (value: string) => <Tag color={statusTagColor(value)}>{value}</Tag>,
|
||||
},
|
||||
{
|
||||
title: "分析状态",
|
||||
dataIndex: "analysis_status",
|
||||
width: 120,
|
||||
render: (value: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
queued: "orange",
|
||||
running: "processing",
|
||||
success: "green",
|
||||
failed: "red",
|
||||
not_started: "default",
|
||||
};
|
||||
return <Tag color={colorMap[value] || "default"}>{value}</Tag>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "使用状态",
|
||||
dataIndex: "usage_status",
|
||||
@@ -528,9 +580,9 @@ export default function AdminElevationPage() {
|
||||
title: "操作",
|
||||
key: "actions",
|
||||
fixed: "right",
|
||||
width: 190,
|
||||
width: 280,
|
||||
render: (_, row) => (
|
||||
<Space size="small">
|
||||
<Space size="small" wrap>
|
||||
<Typography.Link
|
||||
onClick={() => {
|
||||
setPreviewDataset(row);
|
||||
@@ -551,7 +603,6 @@ export default function AdminElevationPage() {
|
||||
.catch((candidate) => {
|
||||
const nextError = candidate instanceof Error ? candidate.message : "加载预览失败";
|
||||
setError(nextError);
|
||||
setSuccess("");
|
||||
messageApi.error(nextError);
|
||||
})
|
||||
.finally(() => {
|
||||
@@ -562,44 +613,44 @@ export default function AdminElevationPage() {
|
||||
预览
|
||||
</Typography.Link>
|
||||
<Typography.Link
|
||||
disabled={!canManage}
|
||||
onClick={() => {
|
||||
if (!canManage) return;
|
||||
analyzeMutation.mutate(row.id);
|
||||
setDatasetFilesDataset(row);
|
||||
setDatasetFiles([]);
|
||||
setDatasetFilesModalOpen(true);
|
||||
setDatasetFilesLoading(true);
|
||||
datasetFilesMutation.mutate(row.id);
|
||||
}}
|
||||
>
|
||||
{analyzingDatasetId === row.id ? "分析中..." : "分析"}
|
||||
文件明细
|
||||
</Typography.Link>
|
||||
<Typography.Link
|
||||
onClick={() => {
|
||||
setAnalysisDataset(row);
|
||||
setAnalysisModalOpen(true);
|
||||
}}
|
||||
>
|
||||
分析进度
|
||||
</Typography.Link>
|
||||
<Typography.Link
|
||||
disabled={!canManage || datasetDataImportMutation.isPending}
|
||||
onClick={() => {
|
||||
if (!canManage || datasetDataImportMutation.isPending) return;
|
||||
datasetDataImportRefs.current[row.id]?.click();
|
||||
setImportDataset(row);
|
||||
setImportFileList([]);
|
||||
setDatasetDataUploadProgress(0);
|
||||
setDatasetDataUploadFileName("");
|
||||
setLastImportedFiles([]);
|
||||
setLastAnalysisTaskId(null);
|
||||
setImportModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{datasetDataImportMutation.isPending && datasetDataUploadingDatasetId === row.id ? "导入中..." : "导入数据"}
|
||||
导入数据
|
||||
</Typography.Link>
|
||||
<input
|
||||
ref={(element) => {
|
||||
datasetDataImportRefs.current[row.id] = element;
|
||||
}}
|
||||
type="file"
|
||||
multiple
|
||||
accept=".csv,.img,.tif,.tiff,.zip"
|
||||
className="hidden"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const selected = event.target.files ? Array.from(event.target.files) : [];
|
||||
if (selected.length > 0) {
|
||||
datasetDataImportMutation.mutate({ datasetId: row.id, files: selected });
|
||||
}
|
||||
event.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<Typography.Link
|
||||
disabled={!canManage || datasetDeleteMutation.isPending}
|
||||
onClick={() => {
|
||||
if (!canManage || datasetDeleteMutation.isPending) return;
|
||||
modal.confirm({
|
||||
Modal.confirm({
|
||||
title: "删除高程数据集",
|
||||
content: `确认删除数据集「${row.code} - ${row.name}」?该操作会同时删除关联的回填任务记录,且不可恢复。`,
|
||||
okText: "确认删除",
|
||||
@@ -618,15 +669,12 @@ export default function AdminElevationPage() {
|
||||
},
|
||||
],
|
||||
[
|
||||
analyzeMutation,
|
||||
analyzingDatasetId,
|
||||
canManage,
|
||||
datasetDataImportMutation,
|
||||
datasetDataUploadingDatasetId,
|
||||
datasetDeleteMutation,
|
||||
datasetFilesMutation,
|
||||
fetchWithAuth,
|
||||
messageApi,
|
||||
modal,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -695,33 +743,23 @@ export default function AdminElevationPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const datasetTableScrollX = 1950;
|
||||
const datasetTableScrollX = 2200;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{messageContextHolder}
|
||||
|
||||
{(error || success || datasetsQuery.error || jobsQuery.error || linesQuery.error) && (
|
||||
{(error || datasetsQuery.error || jobsQuery.error || linesQuery.error) && (
|
||||
<Alert
|
||||
type={error || datasetsQuery.error || jobsQuery.error || linesQuery.error ? "error" : "success"}
|
||||
type="error"
|
||||
showIcon
|
||||
message={error || (datasetsQuery.error instanceof Error ? datasetsQuery.error.message : jobsQuery.error instanceof Error ? jobsQuery.error.message : linesQuery.error instanceof Error ? linesQuery.error.message : success)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{datasetDataImportMutation.isPending && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={datasetDataUploadingDatasetId
|
||||
? `正在导入数据(数据集 ${datasetDataUploadingDatasetId})`
|
||||
: "正在导入数据"}
|
||||
description={(
|
||||
<div className="space-y-2">
|
||||
<Typography.Text type="secondary">{datasetDataUploadFileName || "正在上传文件..."}</Typography.Text>
|
||||
<Progress percent={datasetDataUploadProgress} status={datasetDataUploadProgress >= 100 ? "active" : "normal"} />
|
||||
</div>
|
||||
)}
|
||||
message={error || (datasetsQuery.error instanceof Error
|
||||
? datasetsQuery.error.message
|
||||
: jobsQuery.error instanceof Error
|
||||
? jobsQuery.error.message
|
||||
: linesQuery.error instanceof Error
|
||||
? linesQuery.error.message
|
||||
: "加载失败")}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -747,7 +785,7 @@ export default function AdminElevationPage() {
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
className="hidden"
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) => {
|
||||
onChange={(event) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
datasetImportMutation.mutate(file);
|
||||
@@ -769,13 +807,6 @@ export default function AdminElevationPage() {
|
||||
</Space>
|
||||
)}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="支持文件格式:CSV(点集)/ IMG / TIF / TIFF(栅格)/ ZIP(解压后 csv/img/tif)"
|
||||
description="先新建数据集,再使用“导入数据”上传多个高程文件。数据集目录自动固定为 /elevation/datasets/{数据集编码},导入完成后自动触发分析。"
|
||||
className="mb-4"
|
||||
/>
|
||||
{datasets.length === 0 ? (
|
||||
<Empty description="暂无高程数据集,请先上传 CSV/IMG/TIF 并创建数据集。" />
|
||||
) : (
|
||||
@@ -917,6 +948,178 @@ export default function AdminElevationPage() {
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="导入高程数据"
|
||||
open={importModalOpen}
|
||||
onCancel={() => {
|
||||
if (datasetDataImportMutation.isPending) return;
|
||||
setImportModalOpen(false);
|
||||
setImportDataset(null);
|
||||
setImportFileList([]);
|
||||
setDatasetDataUploadProgress(0);
|
||||
setDatasetDataUploadFileName("");
|
||||
setLastImportedFiles([]);
|
||||
setLastAnalysisTaskId(null);
|
||||
}}
|
||||
onOk={() => {
|
||||
if (!importDataset || datasetDataImportMutation.isPending) return;
|
||||
const files = importFileList
|
||||
.map((item) => item.originFileObj)
|
||||
.filter((item): item is File => !!item);
|
||||
if (files.length === 0) {
|
||||
messageApi.warning("请先选择至少一个文件");
|
||||
return;
|
||||
}
|
||||
datasetDataImportMutation.mutate({ datasetId: importDataset.id, files });
|
||||
}}
|
||||
confirmLoading={datasetDataImportMutation.isPending}
|
||||
okText={datasetDataImportMutation.isPending ? "导入中" : "开始导入"}
|
||||
cancelText="取消"
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="支持文件格式:CSV(点集)/ IMG / TIF / TIFF(栅格)/ ZIP(解压后 csv/img/tif)"
|
||||
description="先新建数据集,再使用“导入数据”上传多个高程文件。数据集目录自动固定为 /elevation/datasets/{数据集编码},导入完成后自动触发分析。"
|
||||
/>
|
||||
{importDataset && (
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="数据集">{`${importDataset.code} - ${importDataset.name}`}</Descriptions.Item>
|
||||
<Descriptions.Item label="目录">{importDataset.dataset_dir}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
)}
|
||||
<Upload
|
||||
multiple
|
||||
fileList={importFileList}
|
||||
beforeUpload={() => false}
|
||||
onChange={({ fileList: nextFileList }) => {
|
||||
setImportFileList(nextFileList);
|
||||
}}
|
||||
accept=".csv,.img,.tif,.tiff,.zip"
|
||||
disabled={datasetDataImportMutation.isPending}
|
||||
>
|
||||
<Typography.Link>选择文件(支持多选)</Typography.Link>
|
||||
</Upload>
|
||||
|
||||
{(datasetDataImportMutation.isPending || datasetDataUploadProgress > 0) && (
|
||||
<div className="space-y-2">
|
||||
<Typography.Text type="secondary">
|
||||
{datasetDataUploadFileName || "正在上传文件..."}
|
||||
</Typography.Text>
|
||||
<Progress
|
||||
percent={datasetDataUploadProgress}
|
||||
status={datasetDataUploadProgress >= 100 ? "active" : "normal"}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lastImportedFiles.length > 0 && (
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
message={`已导入文件(${lastImportedFiles.length})`}
|
||||
description={lastImportedFiles.slice(0, 5).join(";")}
|
||||
/>
|
||||
)}
|
||||
|
||||
{lastAnalysisTaskId && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="分析任务已触发"
|
||||
description={`Task ID: ${lastAnalysisTaskId},可点击数据集行「分析进度」查看实时状态。`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={datasetFilesDataset ? `文件明细:${datasetFilesDataset.code}` : "文件明细"}
|
||||
open={datasetFilesModalOpen}
|
||||
footer={null}
|
||||
width={1040}
|
||||
onCancel={() => {
|
||||
setDatasetFilesModalOpen(false);
|
||||
setDatasetFilesDataset(null);
|
||||
setDatasetFiles([]);
|
||||
setDatasetFilesLoading(false);
|
||||
}}
|
||||
>
|
||||
{datasetFilesDataset && (
|
||||
<div className="space-y-3">
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message={`目录:${datasetFilesDataset.dataset_dir}`}
|
||||
description={`挂载:${datasetFilesDataset.mount_code}`}
|
||||
/>
|
||||
{datasetFilesLoading ? (
|
||||
<div className="flex min-h-[180px] items-center justify-center">
|
||||
<Spin tip="文件明细加载中..." />
|
||||
</div>
|
||||
) : datasetFiles.length === 0 ? (
|
||||
<Empty description="当前目录暂无文件。" />
|
||||
) : (
|
||||
<Table<ElevationDatasetFileItem>
|
||||
rowKey={(row) => row.path}
|
||||
columns={fileColumns}
|
||||
dataSource={datasetFiles}
|
||||
pagination={false}
|
||||
scroll={{ x: 1000 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={analysisDataset ? `分析进度:${analysisDataset.code}` : "分析进度"}
|
||||
open={analysisModalOpen}
|
||||
footer={null}
|
||||
onCancel={() => {
|
||||
setAnalysisModalOpen(false);
|
||||
setAnalysisDataset(null);
|
||||
}}
|
||||
>
|
||||
{analysisDataset && (
|
||||
<div className="space-y-3">
|
||||
{analysisStatusQuery.isLoading ? (
|
||||
<div className="flex min-h-[180px] items-center justify-center">
|
||||
<Spin tip="分析状态加载中..." />
|
||||
</div>
|
||||
) : analysisStatusQuery.error ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
message={analysisStatusQuery.error instanceof Error ? analysisStatusQuery.error.message : "分析状态加载失败"}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Descriptions bordered size="small" column={1}>
|
||||
<Descriptions.Item label="任务状态">
|
||||
<Tag color={statusTagColor(analysisStatusQuery.data?.status || "default")}>
|
||||
{analysisStatusQuery.data?.status || "-"}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="Task ID">{analysisStatusQuery.data?.task_id || "-"}</Descriptions.Item>
|
||||
<Descriptions.Item label="开始时间">{formatDate(analysisStatusQuery.data?.started_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="结束时间">{formatDate(analysisStatusQuery.data?.finished_at)}</Descriptions.Item>
|
||||
<Descriptions.Item label="更新时间">{formatDate(analysisStatusQuery.data?.update_date)}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
{analysisStatusQuery.data?.detail && (
|
||||
<Alert
|
||||
type={analysisStatusQuery.data.status === "failed" ? "error" : "info"}
|
||||
showIcon
|
||||
message={analysisStatusQuery.data.detail}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title="新建高程数据集"
|
||||
open={datasetModalOpen}
|
||||
@@ -932,10 +1135,10 @@ export default function AdminElevationPage() {
|
||||
confirmLoading={datasetCreateMutation.isPending}
|
||||
>
|
||||
<Form<DatasetFormValues> form={datasetForm} layout="vertical" initialValues={DEFAULT_DATASET_FORM}>
|
||||
<Form.Item name="code" label="编码" rules={[{ required: true, message: "请输入编码" }]}>
|
||||
<Form.Item name="code" label="编码" rules={[{ required: true, message: "请输入编码" }]}>
|
||||
<Input placeholder="dem_china_90m_v1" />
|
||||
</Form.Item>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: "请输入名称" }]}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: "请输入名称" }]}>
|
||||
<Input placeholder="中国90米DEM(IMG)" />
|
||||
</Form.Item>
|
||||
<Form.Item name="source" label="来源">
|
||||
@@ -972,13 +1175,13 @@ export default function AdminElevationPage() {
|
||||
confirmLoading={applyMutation.isPending}
|
||||
>
|
||||
<Form<ApplyFormValues> form={applyForm} layout="vertical" initialValues={DEFAULT_APPLY_FORM}>
|
||||
<Form.Item name="line_id" label="线路" rules={[{ required: true, message: "请选择线路" }]}>
|
||||
<Form.Item name="line_id" label="线路" rules={[{ required: true, message: "请选择线路" }]}>
|
||||
<Select showSearch options={lineOptions} optionFilterProp="label" placeholder="选择线路" />
|
||||
</Form.Item>
|
||||
<Form.Item name="dataset_id" label="高程数据集" rules={[{ required: true, message: "请选择高程数据集" }]}>
|
||||
<Form.Item name="dataset_id" label="高程数据集" rules={[{ required: true, message: "请选择高程数据集" }]}>
|
||||
<Select showSearch options={datasetOptions} optionFilterProp="label" placeholder="选择高程数据集" />
|
||||
</Form.Item>
|
||||
<Form.Item name="mode" label="回填模式" rules={[{ required: true, message: "请选择回填模式" }]}>
|
||||
<Form.Item name="mode" label="回填模式" rules={[{ required: true, message: "请选择回填模式" }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: "fill_null_only", label: "仅填空(推荐)" },
|
||||
|
||||
@@ -300,6 +300,11 @@ export type ElevationDatasetSummary = {
|
||||
bbox_max_lon: number | null;
|
||||
bbox_min_lat: number | null;
|
||||
bbox_max_lat: number | null;
|
||||
analysis_task_id: string | null;
|
||||
analysis_status: string;
|
||||
analysis_error_message: string | null;
|
||||
analysis_started_at: string | null;
|
||||
analysis_finished_at: string | null;
|
||||
notes: string | null;
|
||||
create_date: string;
|
||||
create_user: string | null;
|
||||
@@ -338,6 +343,42 @@ export type ElevationDatasetDataImportResponse = {
|
||||
imported_files: string[];
|
||||
};
|
||||
|
||||
export type ElevationDatasetFileItem = {
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
modified_at: string | null;
|
||||
mime_type: string | null;
|
||||
};
|
||||
|
||||
export type ElevationDatasetFileListResponse = {
|
||||
dataset_id: string;
|
||||
dataset_code: string;
|
||||
dataset_dir: string;
|
||||
mount_code: string;
|
||||
items: ElevationDatasetFileItem[];
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type ElevationDatasetAnalysisTaskStatus =
|
||||
| "queued"
|
||||
| "running"
|
||||
| "success"
|
||||
| "failed"
|
||||
| "unknown"
|
||||
| "not_found";
|
||||
|
||||
export type ElevationDatasetAnalysisTaskStatusResponse = {
|
||||
dataset_id: string;
|
||||
dataset_code: string;
|
||||
task_id: string | null;
|
||||
status: ElevationDatasetAnalysisTaskStatus;
|
||||
detail: string | null;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
update_date: string | null;
|
||||
};
|
||||
|
||||
export type ElevationDatasetPreviewPoint = {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
|
||||
Reference in New Issue
Block a user