feat: add delete env ref tasks chekc

This commit is contained in:
engigu
2026-03-05 12:02:56 +08:00
parent bc09625708
commit 06dc3798fc
5 changed files with 158 additions and 18 deletions
+23 -2
View File
@@ -117,11 +117,32 @@ func (ec *EnvController) DeleteEnvVar(c *gin.Context) {
return
}
success := ec.envService.DeleteEnvVar(id)
force := c.Query("force") == "true"
success, associatedTasks := ec.envService.DeleteEnvVar(id, force)
if len(associatedTasks) > 0 {
c.JSON(200, utils.Response{
Code: 409,
Msg: "该环境变量已被任务引用,请先在任务中删除引用或选择强制删除",
Data: vo.ToTaskVOListFromModels(associatedTasks),
})
return
}
if !success {
utils.NotFound(c, "环境变量不存在")
utils.NotFound(c, "环境变量不存在或删除失败")
return
}
utils.SuccessMsg(c, "删除成功")
}
func (ec *EnvController) GetAssociatedTasks(c *gin.Context) {
id := c.Param("id")
if id == "" {
utils.BadRequest(c, "无效的环境变量ID")
return
}
tasks := ec.envService.GetAssociatedTasks(id)
utils.Success(c, vo.ToTaskVOListFromModels(tasks))
}
+1
View File
@@ -139,6 +139,7 @@ func Setup(c *Controllers) *gin.Engine {
env.GET("", c.Env.GetEnvVars)
env.GET("/all", c.Env.GetAllEnvVars)
env.GET("/:id", c.Env.GetEnvVar)
env.GET("/:id/tasks", c.Env.GetAssociatedTasks)
env.PUT("/:id", c.Env.UpdateEnvVar)
env.DELETE("/:id", c.Env.DeleteEnvVar)
}
+42 -2
View File
@@ -6,6 +6,8 @@ import (
"github.com/engigu/baihu-panel/internal/database"
"github.com/engigu/baihu-panel/internal/models"
"github.com/engigu/baihu-panel/internal/utils"
"gorm.io/gorm"
)
type EnvService struct{}
@@ -72,9 +74,47 @@ func (es *EnvService) UpdateEnvVar(id string, name, value, remark string, hidden
return &env
}
func (es *EnvService) DeleteEnvVar(id string) bool {
func (es *EnvService) GetAssociatedTasks(id string) []models.Task {
var associatedTasks []models.Task
query := "envs = ? OR envs LIKE ? OR envs LIKE ? OR envs LIKE ?"
database.DB.Where(query, id, id+",%", "%,"+id, "%,"+id+",%").Find(&associatedTasks)
return associatedTasks
}
func (es *EnvService) DeleteEnvVar(id string, force bool) (bool, []models.Task) {
associatedTasks := es.GetAssociatedTasks(id)
if len(associatedTasks) > 0 && !force {
return false, associatedTasks
}
if force {
err := database.DB.Transaction(func(tx *gorm.DB) error {
// Update tasks to remove this env ID
for _, task := range associatedTasks {
ids := splitEnvIDs(task.Envs)
var newIDs []string
for _, eid := range ids {
if eid != id {
newIDs = append(newIDs, eid)
}
}
newEnvs := strings.Join(newIDs, ",")
if err := tx.Model(&task).Update("envs", newEnvs).Error; err != nil {
return err
}
}
// Delete the env var
if err := tx.Where("id = ?", id).Delete(&models.EnvironmentVariable{}).Error; err != nil {
return err
}
return nil
})
return err == nil, nil
}
result := database.DB.Where("id = ?", id).Delete(&models.EnvironmentVariable{})
return result.RowsAffected > 0
return result.RowsAffected > 0, nil
}
// GetEnvVarsByIDs 根据逗号分隔的ID字符串获取环境变量列表,返回 NAME=VALUE 格式
+8 -1
View File
@@ -89,9 +89,16 @@ export const api = {
return request<EnvListResponse>(`/env?${query}`)
},
all: () => request<EnvVar[]>('/env/all'),
tasks: (id: string) => request<Task[]>(`/env/${id}/tasks`),
create: (data: Partial<EnvVar>) => request<EnvVar>('/env', { method: 'POST', body: JSON.stringify(data) }),
update: (id: string, data: Partial<EnvVar>) => request<EnvVar>(`/env/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (id: string) => request(`/env/${id}`, { method: 'DELETE' })
delete: (id: string, force?: boolean) => {
const query = force ? '?force=true' : ''
return fetch(`${API_BASE_URL}/env/${id}${query}`, {
method: 'DELETE',
credentials: 'include'
}).then(res => res.json() as Promise<ApiResponse<any>>)
}
},
execute: {
command: (command: string) => request('/execute/command', { method: 'POST', body: JSON.stringify({ command }) }),
+84 -13
View File
@@ -2,12 +2,12 @@
import { ref, onMounted, watch } from 'vue'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Label } from '@/components/ui/label'
import Pagination from '@/components/Pagination.vue'
import { Plus, Pencil, Trash2, Eye, EyeOff, Search } from 'lucide-vue-next'
import { Plus, Pencil, Trash2, Eye, EyeOff, Search, AlertTriangle, Terminal } from 'lucide-vue-next'
import TextOverflow from '@/components/TextOverflow.vue'
import { api, type EnvVar } from '@/api'
import { toast } from 'vue-sonner'
@@ -23,6 +23,8 @@ const isEdit = ref(false)
const showValues = ref<Record<string, boolean>>({})
const showDeleteDialog = ref(false)
const deleteEnvId = ref<string | null>(null)
const associatedTasks = ref<any[]>([])
const isDeleting = ref(false)
const filterName = ref('')
const currentPage = ref(1)
@@ -84,22 +86,49 @@ async function saveEnv() {
} catch { toast.error('保存失败') }
}
function confirmDelete(id: string) {
async function confirmDelete(id: string) {
deleteEnvId.value = id
showDeleteDialog.value = true
try {
const res = await api.env.tasks(id)
associatedTasks.value = res || []
showDeleteDialog.value = true
} catch {
toast.error('检查变量引用失败')
}
}
async function deleteEnv() {
async function deleteEnv(force = false) {
if (!deleteEnvId.value) return
isDeleting.value = true
try {
await api.env.delete(deleteEnvId.value)
const res = await api.env.delete(deleteEnvId.value, force)
if (res.code === 409) {
associatedTasks.value = res.data || []
isDeleting.value = false
return
}
if (res.code !== 200) {
toast.error(res.msg || '删除失败')
isDeleting.value = false
return
}
toast.success('变量已删除')
loadEnvVars()
} catch { toast.error('删除失败') }
showDeleteDialog.value = false
deleteEnvId.value = null
showDeleteDialog.value = false
} catch {
toast.error('网络错误,删除失败')
} finally {
isDeleting.value = false
}
}
watch(showDeleteDialog, (val) => {
if (!val) {
associatedTasks.value = []
deleteEnvId.value = null
}
})
function toggleShow(id: string) {
showValues.value[id] = !showValues.value[id]
}
@@ -208,12 +237,54 @@ onMounted(loadEnvVars)
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认删除</AlertDialogTitle>
<AlertDialogDescription>确定要删除此环境变量吗此操作无法撤销</AlertDialogDescription>
<AlertDialogDescription>
<div v-if="associatedTasks.length > 0" class="space-y-4 pt-1">
<div class="flex items-start gap-3 p-3 rounded-lg bg-destructive/10 border border-destructive/20">
<AlertTriangle class="h-5 w-5 text-destructive shrink-0 mt-0.5" />
<div class="space-y-1">
<p class="text-sm font-bold text-destructive">环境变量正在使用中</p>
<p class="text-xs text-muted-foreground leading-relaxed">
该变量已被以下任务引用直接删除可能导致任务运行失败建议先移除引用或选择强制删除
</p>
</div>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between px-1">
<p class="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">关联任务 ({{ associatedTasks.length }})</p>
</div>
<div class="bg-muted/30 rounded-lg p-1.5 max-h-40 overflow-y-auto space-y-1 border border-border/40">
<div v-for="task in associatedTasks" :key="task.id"
class="text-xs flex items-center justify-between bg-background/50 p-2 rounded-md border border-border/50 hover:bg-background transition-colors">
<div class="flex items-center gap-2 min-w-0">
<Terminal class="h-3 w-3 text-primary/70" />
<span class="font-medium truncate">{{ task.name }}</span>
</div>
<code class="text-[10px] text-muted-foreground/70 font-mono bg-muted/50 px-1.5 py-0.5 rounded">{{ task.id }}</code>
</div>
</div>
</div>
<div class="p-3 rounded-lg bg-secondary/30 border border-border/20">
<p class="text-xs text-muted-foreground leading-relaxed">
<span class="font-bold text-foreground/80">提示</span>选择强制删除将自动解除以上任务对该变量的绑定并执行物理删除
</p>
</div>
</div>
<p v-else class="py-2">确定要删除此环境变量吗此操作无法撤销请谨慎操作</p>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="deleteEnv">删除
</AlertDialogAction>
<AlertDialogCancel :disabled="isDeleting">取消</AlertDialogCancel>
<Button v-if="associatedTasks.length > 0" variant="destructive" @click="deleteEnv(true)"
:disabled="isDeleting">
<template v-if="isDeleting">删除中...</template>
<template v-else>强制删除</template>
</Button>
<Button v-else variant="destructive" @click="deleteEnv(false)" :disabled="isDeleting">
<template v-if="isDeleting">删除中...</template>
<template v-else>确认删除</template>
</Button>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>