diff --git a/netx_api/managed_ne_router.py b/netx_api/managed_ne_router.py
index ef11f3e..0f9d217 100644
--- a/netx_api/managed_ne_router.py
+++ b/netx_api/managed_ne_router.py
@@ -13,6 +13,7 @@ from .ne_service import (
batch_apply_hop_proxy,
build_managed_ne_import_template,
create_managed_ne,
+ batch_delete_managed_ne,
delete_managed_ne,
get_managed_ne,
import_managed_ne,
@@ -96,6 +97,11 @@ def api_batch_apply_hop(body: BatchHopApplyRequest, db: Session = Depends(get_db
return batch_apply_hop_proxy(db, body.ids, body.hop)
+@router.post("/batch-delete")
+def api_batch_delete_managed_ne(body: ConnectTestRequest, db: Session = Depends(get_db)):
+ return batch_delete_managed_ne(db, body.ids)
+
+
@router.post("/connect-test")
def api_connect_test(body: ConnectTestRequest, db: Session = Depends(get_db)):
ids = [str(x).strip() for x in body.ids if str(x).strip()]
diff --git a/netx_api/ne_service.py b/netx_api/ne_service.py
index 80eccce..e541228 100644
--- a/netx_api/ne_service.py
+++ b/netx_api/ne_service.py
@@ -328,6 +328,21 @@ def delete_managed_ne(db: Session, ne_id: str) -> dict[str, bool]:
return {"ok": True}
+def batch_delete_managed_ne(db: Session, ids: list[str]) -> dict[str, Any]:
+ ne_ids = [str(x).strip() for x in ids if str(x).strip()]
+ if not ne_ids:
+ raise HTTPException(status_code=400, detail="ids_required")
+ rows = db.query(ManagedNE).filter(ManagedNE.id.in_(ne_ids)).all()
+ found_ids = {str(r.id) for r in rows}
+ missing = [x for x in ne_ids if x not in found_ids]
+ if missing:
+ raise HTTPException(status_code=404, detail=f"managed_ne_not_found: {','.join(missing[:5])}")
+ for row in rows:
+ db.delete(row)
+ db.commit()
+ return {"ok": True, "deleted": len(rows)}
+
+
def build_managed_ne_import_template(fmt: str = "xlsx") -> tuple[str, bytes, str]:
"""Return (filename, content, media_type) for bulk-import template."""
rows = [
diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index e2aa6c6..5c290f0 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -136,6 +136,11 @@ const en = {
edit: "Edit",
delete: "Delete",
confirmDelete: "Delete this network element?",
+ batchDelete: "Batch delete",
+ batchDeleting: "Deleting…",
+ batchDeleteConfirm: "Delete {{n}} selected network element(s)? This cannot be undone.",
+ batchDeleteDone: "Deleted {{n}} network element(s)",
+ batchDeleteSelectRequired: "Select network elements to delete",
keywordPh: "name / IP / user / tags",
allVendors: "All vendors",
allConnectStatus: "All connect status",
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index b718acf..50adcf8 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -135,6 +135,11 @@ const zh = {
edit: "编辑",
delete: "删除",
confirmDelete: "确定删除该网元?",
+ batchDelete: "批量删除",
+ batchDeleting: "删除中…",
+ batchDeleteConfirm: "确定删除选中的 {{n}} 台网元?此操作不可恢复。",
+ batchDeleteDone: "已删除 {{n}} 台网元",
+ batchDeleteSelectRequired: "请先勾选要删除的网元",
keywordPh: "名称 / IP / 用户名 / 标签",
allVendors: "全部厂商",
allConnectStatus: "全部连通状态",
diff --git a/web/src/index.css b/web/src/index.css
index 8df66d1..04c72d6 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -793,6 +793,21 @@ pre {
gap: 8px;
}
+.btn--danger {
+ color: #b71c1c;
+ border-color: #ef9a9a;
+ background: #ffebee;
+}
+
+.btn--danger:hover:not(:disabled) {
+ background: #ffcdd2;
+}
+
+.btn--danger:disabled {
+ opacity: 0.55;
+ cursor: not-allowed;
+}
+
.table-actions {
display: flex;
gap: 8px;
diff --git a/web/src/pages/NePage.tsx b/web/src/pages/NePage.tsx
index 0e379c8..391a617 100644
--- a/web/src/pages/NePage.tsx
+++ b/web/src/pages/NePage.tsx
@@ -2,6 +2,7 @@ import { useMemo, useRef, useState, type ReactNode } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
batchApplyHopManagedNe,
+ batchDeleteManagedNe,
connectTestManagedNe,
createManagedNe,
deleteManagedNe,
@@ -203,6 +204,16 @@ export function NePage() {
onError: (err) => showError(String(err)),
});
+ const batchDeleteMutation = useMutation({
+ mutationFn: batchDeleteManagedNe,
+ onSuccess: async (res) => {
+ setSelected([]);
+ showOk(t("managedNe.batchDeleteDone", { n: res.deleted }));
+ await invalidateList();
+ },
+ onError: (err) => showError(String(err)),
+ });
+
const batchHopMutation = useMutation({
mutationFn: () =>
batchApplyHopManagedNe(selected, {
@@ -361,6 +372,21 @@ export function NePage() {
>
{batchHopMutation.isPending ? t("managedNe.hop.applying") : t("managedNe.hop.batchAdd")}
+
diff --git a/web/src/services/api.ts b/web/src/services/api.ts
index 3f278c2..418e144 100644
--- a/web/src/services/api.ts
+++ b/web/src/services/api.ts
@@ -126,6 +126,9 @@ export const updateManagedNe = (id: string, body: Record) =>
export const deleteManagedNe = (id: string) => apiDelete<{ ok: boolean }>(`/v1/managed-ne/${id}`);
+export const batchDeleteManagedNe = (ids: string[]) =>
+ apiPost<{ ok: boolean; deleted: number }>("/v1/managed-ne/batch-delete", { ids });
+
export const connectTestManagedNe = (ids: string[]) =>
apiPost<{ ok: boolean; submitted: number }>("/v1/managed-ne/connect-test", { ids });