diff --git a/api/app/api/v1/elevation.py b/api/app/api/v1/elevation.py index a41a2a4..5a5d8a3 100644 --- a/api/app/api/v1/elevation.py +++ b/api/app/api/v1/elevation.py @@ -21,6 +21,7 @@ from ...services.elevation_service import ( analyze_dataset, create_apply_job, create_dataset, + delete_dataset, get_job_by_id, list_datasets, list_jobs, @@ -71,6 +72,18 @@ def update_elevation_dataset( return updated +@router.delete("/datasets/{dataset_id}") +def delete_elevation_dataset( + dataset_id: str, + _: CurrentUser = Depends(require_permission("elevation.manage")), + db: Session = Depends(get_db), +) -> dict[str, bool]: + deleted = delete_dataset(db, dataset_id) + if not deleted: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="高程数据集不存在") + return {"success": True} + + @router.post("/datasets/{dataset_id}/analyze", response_model=ElevationDatasetAnalyzeResponse) def analyze_elevation_dataset( dataset_id: str, diff --git a/api/app/services/elevation_service.py b/api/app/services/elevation_service.py index 8f3758a..43c509b 100644 --- a/api/app/services/elevation_service.py +++ b/api/app/services/elevation_service.py @@ -9,7 +9,7 @@ from tempfile import NamedTemporaryFile from typing import Any from fastapi import HTTPException, status -from sqlalchemy import func, select +from sqlalchemy import delete, func, select from sqlalchemy.orm import Session from ..core.database import SessionLocal @@ -261,6 +261,41 @@ def update_dataset( return serialize_dataset(saved) +def delete_dataset( + db: Session, + dataset_id: str, +) -> bool: + item = get_dataset_by_id(db, dataset_id) + if not item: + return False + + running_job_count = int( + db.scalar( + select(func.count()) + .select_from(ElevationApplyJob) + .where( + ElevationApplyJob.dataset_id == dataset_id, + ElevationApplyJob.status == "running", + ) + ) + or 0 + ) + if running_job_count > 0: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail=f"该数据集存在 {running_job_count} 个运行中的回填任务,暂不能删除", + ) + + db.execute(delete(ElevationApplyJob).where(ElevationApplyJob.dataset_id == dataset_id)) + db.delete(item) + db.commit() + _publish_elevation_change( + "elevation.dataset.deleted", + {"action": "dataset_deleted", "dataset_id": dataset_id}, + ) + return True + + def analyze_dataset( db: Session, *, diff --git a/memory/2026-05-03.md b/memory/2026-05-03.md index 3a37137..a46d861 100644 --- a/memory/2026-05-03.md +++ b/memory/2026-05-03.md @@ -89,3 +89,31 @@ - 提交:`556da5c` - 信息:`改造高程预览为地形网格渲染` - 已推送到 `origin/dev`。 + +## Work Log - 高程数据集支持删除(2026-05-03) + +- 背景: + - Issue `FL-180` 需要“高程数据集支持删除”。 + - 现有高程管理仅支持创建/更新/分析/预览,缺少删除闭环。 + +- 本次改动: + - 后端新增数据集删除能力: + - 文件:`api/app/services/elevation_service.py` + - 新增 `delete_dataset(db, dataset_id)`: + - 数据集不存在返回 `False`; + - 存在运行中回填任务时返回 `409`,避免删除过程中任务写入异常; + - 删除前先清理关联 `elevation_apply_job` 记录,再删除数据集; + - 发布 `elevation.dataset.deleted` 主题事件,触发前端数据刷新。 + - 后端新增删除接口: + - 文件:`api/app/api/v1/elevation.py` + - 新增 `DELETE /api/v1/elevation/datasets/{dataset_id}`(权限:`elevation.manage`)。 + - 前端高程管理页新增删除入口: + - 文件:`web/src/app/admin/elevation/page.tsx` + - 数据集操作列新增“删除”; + - 使用 `App.useApp().modal.confirm` 二次确认; + - 删除成功后提示并刷新数据集/任务列表,同时清理预览弹窗状态。 + +- 风险与影响: + - 删除数据集会同时删除其关联的回填任务记录(仅记录,不会回滚已写入杆塔的高程值)。 + - 若数据集存在运行中任务,接口会拒绝删除并提示先等待任务结束。 + diff --git a/web/src/app/admin/elevation/page.tsx b/web/src/app/admin/elevation/page.tsx index d9d01e5..75275ad 100644 --- a/web/src/app/admin/elevation/page.tsx +++ b/web/src/app/admin/elevation/page.tsx @@ -4,6 +4,7 @@ import Link from "next/link"; import { useCallback, useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { + App, Alert, Empty, Form, @@ -89,6 +90,7 @@ function formatDate(value: string | null): string { } export default function AdminElevationPage() { + const { modal } = App.useApp(); const queryClient = useQueryClient(); const { user, initializing, hasPermission, fetchWithAuth } = useAuth(); const [messageApi, messageContextHolder] = message.useMessage(); @@ -283,6 +285,33 @@ export default function AdminElevationPage() { }, }); + const datasetDeleteMutation = useMutation({ + mutationFn: async (datasetId: string) => { + const response = await fetchWithAuth(`/api/v1/elevation/datasets/${datasetId}`, { + method: "DELETE", + }); + if (!response.ok) { + throw new Error(await readApiError(response)); + } + }, + onSuccess: async () => { + setSuccess("高程数据集已删除"); + setError(""); + messageApi.success("高程数据集已删除"); + setPreviewModalOpen(false); + setPreviewDataset(null); + setPreviewData(null); + setPreviewLoading(false); + await refreshElevationData(); + }, + onError: (candidate) => { + const nextError = candidate instanceof Error ? candidate.message : "删除高程数据集失败"; + setError(nextError); + setSuccess(""); + messageApi.error(nextError); + }, + }); + const datasets = datasetsQuery.data?.items ?? []; const jobs = jobsQuery.data?.items ?? []; const lines = linesQuery.data?.items ?? []; @@ -383,11 +412,29 @@ export default function AdminElevationPage() { > {analyzingDatasetId === row.id ? "分析中..." : "分析"} + { + if (!canManage || datasetDeleteMutation.isPending) return; + modal.confirm({ + title: "删除高程数据集", + content: `确认删除数据集「${row.code} - ${row.name}」?该操作会同时删除关联的回填任务记录,且不可恢复。`, + okText: "确认删除", + okButtonProps: { danger: true, loading: datasetDeleteMutation.isPending }, + cancelText: "取消", + onOk: async () => { + await datasetDeleteMutation.mutateAsync(row.id); + }, + }); + }} + > + 删除 + ), }, ], - [analyzeMutation, analyzingDatasetId, canManage, fetchWithAuth, messageApi], + [analyzeMutation, analyzingDatasetId, canManage, datasetDeleteMutation, fetchWithAuth, messageApi, modal], ); const jobColumns = useMemo>(