diff --git a/backend/internal/api/handlers/alert.go b/backend/internal/api/handlers/alert.go index f4d1f4b..5c220e2 100644 --- a/backend/internal/api/handlers/alert.go +++ b/backend/internal/api/handlers/alert.go @@ -251,6 +251,48 @@ func (h *AlertHandler) DeleteRule(c *gin.Context) { }) } +// BatchDeleteRules 批量删除告警规则 // Logan.Li +// @Summary 批量删除告警规则 +// @Description 批量删除指定告警规则 +// @Tags 告警 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body models.IDsRequest true "删除请求" +// @Success 200 {object} models.Response +// @Router /alerts/rules/batch-delete [delete] +func (h *AlertHandler) BatchDeleteRules(c *gin.Context) { + var req models.IDsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "请求参数错误", + }) + return + } + + if len(req.IDs) == 0 { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "请选择要删除的规则", + }) + return + } + + if err := h.db.Delete(&models.AlertRule{}, req.IDs).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{ + Code: 500, + Message: "删除失败", + }) + return + } + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "删除成功", + }) +} + // ToggleRule 切换告警规则状态 // @Summary 切换告警规则状态 // @Description 启用或禁用告警规则 diff --git a/backend/internal/api/handlers/ansible.go b/backend/internal/api/handlers/ansible.go index d59dec0..25ad4f4 100644 --- a/backend/internal/api/handlers/ansible.go +++ b/backend/internal/api/handlers/ansible.go @@ -115,6 +115,24 @@ func (h *AnsibleHandler) DeletePlaybook(c *gin.Context) { c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"}) } +// BatchDeletePlaybooks 批量删除 Playbook // Logan.Li +func (h *AnsibleHandler) BatchDeletePlaybooks(c *gin.Context) { + var req models.IDsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"}) + return + } + if len(req.IDs) == 0 { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "ids 不能为空"}) + return + } + if err := h.db.Delete(&models.AnsiblePlaybook{}, req.IDs).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "已删除"}) +} + // ==================== Job ==================== func (h *AnsibleHandler) ListJobs(c *gin.Context) { diff --git a/backend/internal/api/handlers/automation_cron.go b/backend/internal/api/handlers/automation_cron.go index f22936f..1ffec75 100644 --- a/backend/internal/api/handlers/automation_cron.go +++ b/backend/internal/api/handlers/automation_cron.go @@ -210,6 +210,40 @@ func (h *AutomationHandler) DeleteCronJob(c *gin.Context) { }) } +// BatchDeleteCronJobs 批量删除定时任务 +// @Summary 批量删除定时任务 +// @Description 批量删除定时任务 +// @Tags 自动化运维 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body models.IDsRequest true "删除请求" +// @Success 200 {object} models.Response +// @Router /cron-jobs/batch-delete [delete] +func (h *AutomationHandler) BatchDeleteCronJobs(c *gin.Context) { + var req models.IDsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "请求参数错误", + }) + return + } + + if err := h.db.Delete(&models.CronJob{}, req.IDs).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{ + Code: 500, + Message: "删除失败", + }) + return + } + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "删除成功", + }) +} + // ToggleCronJob 切换定时任务状态 // @Summary 切换定时任务状态 // @Description 启用或禁用定时任务 diff --git a/backend/internal/api/handlers/automation_script.go b/backend/internal/api/handlers/automation_script.go index fe810c9..ffa6029 100644 --- a/backend/internal/api/handlers/automation_script.go +++ b/backend/internal/api/handlers/automation_script.go @@ -205,6 +205,40 @@ func (h *AutomationHandler) DeleteScript(c *gin.Context) { }) } +// BatchDeleteScripts 批量删除脚本 +// @Summary 批量删除脚本 +// @Description 批量删除脚本 +// @Tags 自动化运维 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body models.IDsRequest true "删除请求" +// @Success 200 {object} models.Response +// @Router /scripts/batch-delete [post] +func (h *AutomationHandler) BatchDeleteScripts(c *gin.Context) { + var req models.IDsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "请求参数错误", + }) + return + } + + if err := h.db.Delete(&models.Script{}, req.IDs).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{ + Code: 500, + Message: "删除失败", + }) + return + } + + c.JSON(http.StatusOK, models.Response{ + Code: 200, + Message: "删除成功", + }) +} + // ExecuteScript 执行脚本 // @Summary 执行脚本 // @Description 在目标服务器上执行脚本 diff --git a/backend/internal/api/handlers/cloud_region.go b/backend/internal/api/handlers/cloud_region.go index cf4954e..7d3ba78 100644 --- a/backend/internal/api/handlers/cloud_region.go +++ b/backend/internal/api/handlers/cloud_region.go @@ -125,3 +125,27 @@ func (h *RegionHandler) DeleteRegion(c *gin.Context) { } c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"}) } + +// BatchDeleteRegions 批量删除区域 // Logan.Li +// @Summary 批量删除区域 +// @Description 批量删除区域 +// @Tags 云区域 +// @Accept json +// @Produce json +// @Security Bearer +// @Param request body models.IDsRequest true "删除请求" +// @Success 200 {object} models.Response +// @Router /regions/batch-delete [delete] +func (h *RegionHandler) BatchDeleteRegions(c *gin.Context) { + var req models.IDsRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, models.Response{Code: 400, Message: "请求参数错误"}) + return + } + + if err := h.db.Delete(&models.Region{}, req.IDs).Error; err != nil { + c.JSON(http.StatusInternalServerError, models.Response{Code: 500, Message: "删除失败"}) + return + } + c.JSON(http.StatusOK, models.Response{Code: 200, Message: "删除成功"}) +} diff --git a/backend/internal/api/routes/routes.go b/backend/internal/api/routes/routes.go index 655464b..e437aea 100644 --- a/backend/internal/api/routes/routes.go +++ b/backend/internal/api/routes/routes.go @@ -150,6 +150,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { regions.GET("", regionHandler.ListRegions) regions.POST("", regionHandler.CreateRegion) regions.PUT("/:id", regionHandler.UpdateRegion) + regions.DELETE("/batch-delete", regionHandler.BatchDeleteRegions) // Logan.Li regions.DELETE("/:id", regionHandler.DeleteRegion) } @@ -207,6 +208,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { alerts.GET("/rules", alertHandler.ListRules) alerts.POST("/rules", alertHandler.CreateRule) alerts.PUT("/rules/:id", alertHandler.UpdateRule) + alerts.DELETE("/rules/batch-delete", alertHandler.BatchDeleteRules) alerts.DELETE("/rules/:id", alertHandler.DeleteRule) alerts.POST("/rules/:id/toggle", alertHandler.ToggleRule) alerts.POST("/:id/ack", alertHandler.AcknowledgeAlert) @@ -249,6 +251,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { cronJobs.POST("", automationHandler.CreateCronJob) cronJobs.GET("/:id", automationHandler.GetCronJob) cronJobs.PUT("/:id", automationHandler.UpdateCronJob) + cronJobs.DELETE("/batch-delete", automationHandler.BatchDeleteCronJobs) cronJobs.DELETE("/:id", automationHandler.DeleteCronJob) cronJobs.POST("/:id/toggle", automationHandler.ToggleCronJob) cronJobs.POST("/:id/execute", automationHandler.ExecuteCronJob) @@ -260,6 +263,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { scripts.POST("", automationHandler.CreateScript) scripts.GET("/:id", automationHandler.GetScript) scripts.PUT("/:id", automationHandler.UpdateScript) + scripts.POST("/batch-delete", automationHandler.BatchDeleteScripts) scripts.DELETE("/:id", automationHandler.DeleteScript) scripts.POST("/:id/execute", automationHandler.ExecuteScript) } @@ -286,6 +290,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) { ansible.GET("/playbooks/:id", ansibleHandler.GetPlaybook) ansible.PUT("/playbooks/:id", ansibleHandler.UpdatePlaybook) ansible.DELETE("/playbooks/:id", ansibleHandler.DeletePlaybook) + ansible.DELETE("/playbooks/batch-delete", ansibleHandler.BatchDeletePlaybooks) ansible.POST("/playbooks/:id/execute", ansibleHandler.ExecutePlaybook) ansible.GET("/jobs", ansibleHandler.ListJobs) diff --git a/frontend/src/components/automation/AnsibleManager.tsx b/frontend/src/components/automation/AnsibleManager.tsx index a9bfbdf..c9d16ce 100644 --- a/frontend/src/components/automation/AnsibleManager.tsx +++ b/frontend/src/components/automation/AnsibleManager.tsx @@ -17,6 +17,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { Checkbox } from '@/components/ui/checkbox'; import { toast } from 'sonner'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { ansibleApi, taskExecutionApi } from '@/services/api'; @@ -116,6 +117,7 @@ const AnsibleManager: React.FC = () => { const [searchQuery, setSearchQuery] = useState(''); const [categoryFilter, setCategoryFilter] = useState('all'); const [activeTab, setActiveTab] = useState('playbooks'); + const [selectedIds, setSelectedIds] = useState([]); // 弹窗状态 const [editorOpen, setEditorOpen] = useState(false); // 新建/编辑 @@ -251,6 +253,41 @@ const AnsibleManager: React.FC = () => { } }; + // ---- 批量选择 / 删除 ---- // Logan.Li + + const toggleSelect = (id: number) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ); + }; + + const toggleSelectAll = () => { + if (selectedIds.length === filteredPlaybooks.length && filteredPlaybooks.length > 0) { + setSelectedIds([]); + } else { + setSelectedIds(filteredPlaybooks.map((pb) => pb.id)); + } + }; + + const handleBatchDelete = async () => { + if (selectedIds.length === 0) return; + const ok = await _confirm({ + title: '批量删除 Playbook', + description: `确定删除选中的 ${selectedIds.length} 个 Playbook 吗?此操作不可恢复。`, + confirmText: '批量删除', + variant: 'destructive', + }); + if (!ok) return; + try { + await ansibleApi.batchDeletePlaybooks(selectedIds); + toast.success(`已删除 ${selectedIds.length} 个 Playbook`); + setSelectedIds([]); + fetchPlaybooks(); + } catch (error) { + toast.error('批量删除失败'); + } + }; + // ---- 执行 ---- const handleExecute = async (pb: Playbook) => { @@ -419,6 +456,36 @@ const AnsibleManager: React.FC = () => { + {/* 批量操作栏 */} + {filteredPlaybooks.length > 0 && ( +
+
+ 0 + } + onCheckedChange={toggleSelectAll} + /> + + {selectedIds.length > 0 + ? `已选择 ${selectedIds.length} / ${filteredPlaybooks.length} 项` + : `共 ${filteredPlaybooks.length} 项`} + +
+ {selectedIds.length > 0 && ( +
+ + +
+ )} +
+ )} + {/* Playbook 卡片列表 */}
{loading ? ( @@ -434,6 +501,11 @@ const AnsibleManager: React.FC = () => {
+ toggleSelect(playbook.id)} + className="shrink-0" + /> {playbook.name}
diff --git a/frontend/src/components/automation/CronJobManager.tsx b/frontend/src/components/automation/CronJobManager.tsx index 5ee5cc4..76c23cb 100644 --- a/frontend/src/components/automation/CronJobManager.tsx +++ b/frontend/src/components/automation/CronJobManager.tsx @@ -3,6 +3,7 @@ import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; +import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, @@ -59,6 +60,8 @@ export function CronJobManager() { const [executionsLoading, setExecutionsLoading] = useState(false); const [expandedExec, setExpandedExec] = useState(null); const [executingId, setExecutingId] = useState(null); + // 批量选择(批量删除) // Logan.Li + const [selectedIds, setSelectedIds] = useState([]); // editingId === null 表示新建模式;非 null 表示编辑已有任务 // Logan.Li const [editingId, setEditingId] = useState(null); const [formJob, setFormJob] = useState({ @@ -250,6 +253,42 @@ export function CronJobManager() { } }; + // 批量选择相关 // Logan.Li + const toggleSelect = (id: number) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((i) => i !== id) : [...prev, id] + ); + }; + + const toggleSelectAll = () => { + if (selectedIds.length === filteredJobs.length && filteredJobs.length > 0) { + setSelectedIds([]); + } else { + setSelectedIds(filteredJobs.map((j) => j.id)); + } + }; + + // 批量删除任务(带确认框) // Logan.Li + const handleBatchDelete = async () => { + if (selectedIds.length === 0) return; + const ok = await _confirm({ + title: '批量删除定时任务', + description: `确定删除选中的 ${selectedIds.length} 个任务吗?此操作不可恢复。`, + confirmText: '批量删除', + variant: 'destructive', + }); + if (!ok) return; + try { + await cronJobApi.batchDelete(selectedIds); + toast.success(`已删除 ${selectedIds.length} 个任务`); + setSelectedIds([]); + refreshData(); + } catch (error) { + console.error('Failed to batch delete jobs:', error); + toast.error('批量删除失败'); + } + }; + return (
{/* Header */} @@ -318,16 +357,44 @@ export function CronJobManager() {
{/* Filters */} -
-
- - setSearchQuery(e.target.value)} - className="pl-10 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" - /> +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10 bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" + /> +
+ {filteredJobs.length > 0 && ( + + )}
+ {selectedIds.length > 0 && ( +
+ 已选择 {selectedIds.length} 项 + +
+ )}
{initialLoading ? ( @@ -346,7 +413,14 @@ export function CronJobManager() { {filteredJobs.map((job) => ( -
+
+
+ toggleSelect(job.id)} + className="border-[var(--dg-border)]" + /> +

{job.name}

diff --git a/frontend/src/components/automation/ScriptManager.tsx b/frontend/src/components/automation/ScriptManager.tsx index 0099f92..a8271a0 100644 --- a/frontend/src/components/automation/ScriptManager.tsx +++ b/frontend/src/components/automation/ScriptManager.tsx @@ -14,6 +14,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Checkbox } from '@/components/ui/checkbox'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { ScrollArea } from '@/components/ui/scroll-area'; @@ -100,6 +101,7 @@ export function ScriptManager() { const [searchQuery, setSearchQuery] = useState(''); const [typeFilter, setTypeFilter] = useState('all'); const [activeTab, setActiveTab] = useState('scripts'); + const [selectedIds, setSelectedIds] = useState([]); // 弹窗状态 const [editorOpen, setEditorOpen] = useState(false); // 新建/编辑 @@ -306,6 +308,41 @@ export function ScriptManager() { return matchSearch && matchType; }); + // ---- 批量选择 / 批量删除 ---- + + const toggleSelect = (id: number) => { + setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]); + }; + + const toggleSelectAll = () => { + setSelectedIds(prev => + prev.length === filteredScripts.length && filteredScripts.length > 0 + ? [] + : filteredScripts.map(sc => sc.id) + ); + }; + + const handleBatchDelete = async () => { + if (selectedIds.length === 0) return; + const ok = await _confirm({ + title: '批量删除脚本', + description: `确定删除选中的 ${selectedIds.length} 个脚本吗?此操作不可恢复。`, + confirmText: '批量删除', + variant: 'destructive', + }); + if (!ok) return; + try { + await scriptApi.batchDelete(selectedIds); + toast.success(`已删除 ${selectedIds.length} 个脚本`); + setSelectedIds([]); + fetchScripts(); + } catch (error) { + toast.error('批量删除失败'); + } + }; + + const allSelected = filteredScripts.length > 0 && selectedIds.length === filteredScripts.length; + const runningCount = executions.filter(e => e.status === 'running').length; const todaySuccess = executions.filter(e => e.status === 'success' && new Date(e.created_at).toDateString() === new Date().toDateString() @@ -415,6 +452,36 @@ export function ScriptManager() {
+ {/* 批量操作栏 */} + {selectedIds.length > 0 && ( +
+
+ + + 已选择 {selectedIds.length} 项 / 共 {filteredScripts.length} 项 + +
+
+ + +
+
+ )} + {/* 脚本卡片列表 */}
{loading ? ( @@ -432,6 +499,11 @@ export function ScriptManager() {
+ toggleSelect(sc.id)} + className="border-[var(--dg-border)]" + /> {sc.name}
diff --git a/frontend/src/components/cloud/RegionManager.tsx b/frontend/src/components/cloud/RegionManager.tsx index 82f5bbb..b592139 100644 --- a/frontend/src/components/cloud/RegionManager.tsx +++ b/frontend/src/components/cloud/RegionManager.tsx @@ -3,6 +3,7 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; +import { Checkbox } from '@/components/ui/checkbox'; import { Table, TableBody, @@ -36,6 +37,8 @@ import { Loader2, } from 'lucide-react'; import { regionApi } from '@/services/api'; +import { useConfirm } from '@/components/ui/confirm-dialog'; +import { toast } from 'sonner'; const providerNames: Record = { aws: 'AWS', @@ -54,10 +57,12 @@ const providerColors: Record = { }; export function RegionManager() { + const confirm = useConfirm(); const [regions, setRegions] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); const [selectedProvider, setSelectedProvider] = useState('all'); + const [selectedIds, setSelectedIds] = useState([]); const [dialogOpen, setDialogOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); const [submitting, setSubmitting] = useState(false); @@ -88,6 +93,19 @@ export function RegionManager() { return matchSearch && matchProvider; }); + // 全选 / 单选 // Logan.Li + const allSelected = filtered.length > 0 && filtered.every(r => selectedIds.includes(r.id)); + const toggleSelectAll = () => { + if (allSelected) { + setSelectedIds([]); + } else { + setSelectedIds(filtered.map(r => r.id)); + } + }; + const toggleSelect = (id: number) => { + setSelectedIds(prev => prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]); + }; + const openCreate = () => { setEditingItem(null); setFormData({ name: '', code: '', provider: 'aliyun', location: '', status: 'active', endpoint: '' }); @@ -120,13 +138,46 @@ export function RegionManager() { } }; - const [deleteId, setDeleteId] = useState(null); - const handleDelete = (id: number) => setDeleteId(id); - const confirmDelete = async () => { - if (deleteId === null) return; - await regionApi.delete(deleteId); - setDeleteId(null); - loadData(); + // 单条删除 // Logan.Li + const handleDelete = async (id: number) => { + const region = regions.find(r => r.id === id); + const ok = await confirm({ + title: '删除区域', + description: `确定删除「${region?.name || id}」吗?此操作不可撤销。`, + confirmText: '删除', + variant: 'destructive', + }); + if (!ok) return; + try { + await regionApi.delete(id); + toast.success('区域已删除'); + setSelectedIds(prev => prev.filter(i => i !== id)); + loadData(); + } catch (e) { + console.error('删除区域失败:', e); + toast.error('删除失败'); + } + }; + + // 批量删除 // Logan.Li + const handleBatchDelete = async () => { + if (selectedIds.length === 0) return; + const ok = await confirm({ + title: '批量删除区域', + description: `确定删除选中的 ${selectedIds.length} 个区域吗?此操作不可撤销。`, + confirmText: '批量删除', + variant: 'destructive', + }); + if (!ok) return; + try { + await regionApi.batchDelete(selectedIds); + toast.success(`已删除 ${selectedIds.length} 个区域`); + setSelectedIds([]); + loadData(); + } catch (e) { + console.error('批量删除失败:', e); + toast.error('批量删除失败'); + } }; return ( @@ -161,6 +212,29 @@ export function RegionManager() {
+ {/* 批量操作栏 // Logan.Li */} + {selectedIds.length > 0 && ( +
+ + 已选择 {selectedIds.length} 个区域 + +
+ + +
+
+ )} + {/* Table */} {loading ? (
@@ -176,6 +250,13 @@ export function RegionManager() { + + + 区域名称 编码 云商 @@ -188,6 +269,13 @@ export function RegionManager() { {filtered.map((r) => ( + + toggleSelect(r.id)} + className="border-slate-600" + /> + {r.name} {r.code} @@ -308,20 +396,6 @@ export function RegionManager() { - - {/* 删除确认 */} - !v && setDeleteId(null)}> - - - 确认删除 - -

确定删除此区域?此操作不可撤销。

- - - - -
-
); } diff --git a/frontend/src/components/monitor/AlertRulesTab.tsx b/frontend/src/components/monitor/AlertRulesTab.tsx index f9ea717..ea09087 100644 --- a/frontend/src/components/monitor/AlertRulesTab.tsx +++ b/frontend/src/components/monitor/AlertRulesTab.tsx @@ -1,8 +1,10 @@ +import { useState } from 'react'; import { useConfirm } from '@/components/ui/confirm-dialog'; import { Card, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; +import { Checkbox } from '@/components/ui/checkbox'; import { Table, TableBody, @@ -12,9 +14,12 @@ import { TableRow, } from '@/components/ui/table'; import { Settings, Trash2 } from 'lucide-react'; +import { toast } from 'sonner'; import { alertApi } from '@/services/api'; import { renderRuleChannels } from './alertShared'; +const COL_SPAN = 9; // 复选框 + 7 数据列 + 操作列 // Logan.Li + export function AlertRulesTab({ rules, notifyChannels, @@ -26,15 +31,61 @@ export function AlertRulesTab({ onReload: () => void; onEditRule: (rule: any) => void; }) { - const _confirm = useConfirm(); + const confirm = useConfirm(); + const [selectedIds, setSelectedIds] = useState([]); + + // 全选/取消全选 // Logan.Li + const toggleSelectAll = () => { + if (rules.length > 0 && selectedIds.length === rules.length) { + setSelectedIds([]); + } else { + setSelectedIds(rules.map((r) => r.id)); + } + }; + + // 单行选择切换 // Logan.Li + const toggleSelect = (id: number) => { + setSelectedIds((prev) => + prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id] + ); + }; const handleDeleteRule = async (id: any) => { - { const ok = await _confirm({ title: '删除告警规则', description: '确定删除此告警规则?', confirmText: '删除', variant: 'destructive' }); if (!ok) return; } + const ok = await confirm({ + title: '删除告警规则', + description: '确定删除此告警规则?', + confirmText: '删除', + variant: 'destructive', + }); + if (!ok) return; try { await alertApi.deleteRule(id); + toast.success('告警规则已删除'); onReload(); } catch (error) { console.error('Failed to delete rule:', error); + toast.error('删除失败'); + } + }; + + // 批量删除 // Logan.Li + const handleBatchDelete = async () => { + if (selectedIds.length === 0) return; + const ok = await confirm({ + title: '批量删除告警规则', + description: `确定删除选中的 ${selectedIds.length} 条告警规则吗?此操作不可撤销。`, + confirmText: '批量删除', + variant: 'destructive', + }); + if (!ok) return; + try { + await alertApi.batchDeleteRules(selectedIds); + toast.success(`已删除 ${selectedIds.length} 条告警规则`); + setSelectedIds([]); + onReload(); + } catch (error) { + console.error('Failed to batch delete rules:', error); + toast.error('批量删除失败'); } }; @@ -47,14 +98,39 @@ export function AlertRulesTab({ } }; + const allSelected = rules.length > 0 && selectedIds.length === rules.length; + return (
+ {/* 批量操作栏 // Logan.Li */} + {selectedIds.length > 0 && ( +
+ 已选择 {selectedIds.length} 项 + +
+ )} +
+ + + 规则名称 数据源 触发条件 @@ -66,76 +142,91 @@ export function AlertRulesTab({ - {rules.map((rule) => ( - - -
- {rule.name} - {rule.description && ( -

{rule.description}

- )} -
-
- - - {rule.source || '--'} - - - - - {rule.expression || '--'} - - - - {rule.severity === 'critical' ? ( - - 严重 - - ) : rule.severity === 'warning' ? ( - - 警告 - - ) : ( - - 提示 - - )} - - - {(rule.duration || 0) / 60}分钟 - - - {renderRuleChannels(rule, notifyChannels)} - - - handleToggleRule(rule.id)} - /> - - -
- - -
+ {rules.length === 0 ? ( + + + 暂无告警规则 - ))} + ) : ( + rules.map((rule) => ( + + + toggleSelect(rule.id)} + className="border-slate-600" + /> + + +
+ {rule.name} + {rule.description && ( +

{rule.description}

+ )} +
+
+ + + {rule.source || '--'} + + + + + {rule.expression || '--'} + + + + {rule.severity === 'critical' ? ( + + 严重 + + ) : rule.severity === 'warning' ? ( + + 警告 + + ) : ( + + 提示 + + )} + + + {(rule.duration || 0) / 60}分钟 + + + {renderRuleChannels(rule, notifyChannels)} + + + handleToggleRule(rule.id)} + /> + + +
+ + +
+
+
+ )) + )}
diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 7c21f2b..3a72d57 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -191,6 +191,9 @@ export const alertApi = { deleteRule: async (id: number | string) => { return http.delete(`/alerts/rules/${id}`); }, + batchDeleteRules: async (ids: number[]) => { + return http.delete('/alerts/rules/batch-delete', { ids }); + }, toggleRule: async (id: number | string) => { return http.post(`/alerts/rules/${id}/toggle`); }, @@ -285,6 +288,9 @@ export const cronJobApi = { executeJob: async (id: number | string) => { return http.post(`/cron-jobs/${id}/execute`); }, + batchDelete: async (ids: number[]) => { + return http.delete('/cron-jobs/batch-delete', { ids }); + }, }; // ==================== 脚本 API ==================== @@ -304,6 +310,9 @@ export const scriptApi = { deleteScript: async (id: number | string) => { return http.delete(`/scripts/${id}`); }, + batchDelete: async (ids: number[]) => { + return http.post('/scripts/batch-delete', { ids }); + }, executeScript: async (id: number | string) => { return http.post(`/scripts/${id}/execute`); }, @@ -488,6 +497,10 @@ export const regionApi = { delete: async (id: number | string) => { return http.delete(`/regions/${id}`); }, + // 批量删除 // Logan.Li + batchDelete: async (ids: number[]) => { + return http.delete('/regions/batch-delete', { ids }); + }, }; // ==================== 资源模型 API ==================== @@ -580,6 +593,10 @@ export const ansibleApi = { deletePlaybook: async (id: number | string) => { return http.delete(`/ansible/playbooks/${id}`); }, + // 批量删除 Playbook // Logan.Li + batchDeletePlaybooks: async (ids: number[]) => { + return http.delete('/ansible/playbooks/batch-delete', { ids }); + }, executePlaybook: async (id: number | string) => { return http.post(`/ansible/playbooks/${id}/execute`); }, diff --git a/frontend/src/services/http.ts b/frontend/src/services/http.ts index e1413bb..f1553a8 100644 --- a/frontend/src/services/http.ts +++ b/frontend/src/services/http.ts @@ -75,8 +75,8 @@ export const http = { request('POST', url, body), put: (url: string, body?: any) => request('PUT', url, body), - delete: (url: string) => - request('DELETE', url), + delete: (url: string, body?: any) => + request('DELETE', url, body), }; export default http;