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>(