feat: add delete env ref tasks chekc
This commit is contained in:
@@ -117,11 +117,32 @@ func (ec *EnvController) DeleteEnvVar(c *gin.Context) {
|
|||||||
return
|
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 {
|
if !success {
|
||||||
utils.NotFound(c, "环境变量不存在")
|
utils.NotFound(c, "环境变量不存在或删除失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
utils.SuccessMsg(c, "删除成功")
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ func Setup(c *Controllers) *gin.Engine {
|
|||||||
env.GET("", c.Env.GetEnvVars)
|
env.GET("", c.Env.GetEnvVars)
|
||||||
env.GET("/all", c.Env.GetAllEnvVars)
|
env.GET("/all", c.Env.GetAllEnvVars)
|
||||||
env.GET("/:id", c.Env.GetEnvVar)
|
env.GET("/:id", c.Env.GetEnvVar)
|
||||||
|
env.GET("/:id/tasks", c.Env.GetAssociatedTasks)
|
||||||
env.PUT("/:id", c.Env.UpdateEnvVar)
|
env.PUT("/:id", c.Env.UpdateEnvVar)
|
||||||
env.DELETE("/:id", c.Env.DeleteEnvVar)
|
env.DELETE("/:id", c.Env.DeleteEnvVar)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"github.com/engigu/baihu-panel/internal/database"
|
"github.com/engigu/baihu-panel/internal/database"
|
||||||
"github.com/engigu/baihu-panel/internal/models"
|
"github.com/engigu/baihu-panel/internal/models"
|
||||||
"github.com/engigu/baihu-panel/internal/utils"
|
"github.com/engigu/baihu-panel/internal/utils"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type EnvService struct{}
|
type EnvService struct{}
|
||||||
@@ -72,9 +74,47 @@ func (es *EnvService) UpdateEnvVar(id string, name, value, remark string, hidden
|
|||||||
return &env
|
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{})
|
result := database.DB.Where("id = ?", id).Delete(&models.EnvironmentVariable{})
|
||||||
return result.RowsAffected > 0
|
return result.RowsAffected > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEnvVarsByIDs 根据逗号分隔的ID字符串获取环境变量列表,返回 NAME=VALUE 格式
|
// GetEnvVarsByIDs 根据逗号分隔的ID字符串获取环境变量列表,返回 NAME=VALUE 格式
|
||||||
|
|||||||
@@ -89,9 +89,16 @@ export const api = {
|
|||||||
return request<EnvListResponse>(`/env?${query}`)
|
return request<EnvListResponse>(`/env?${query}`)
|
||||||
},
|
},
|
||||||
all: () => request<EnvVar[]>('/env/all'),
|
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) }),
|
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) }),
|
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: {
|
execute: {
|
||||||
command: (command: string) => request('/execute/command', { method: 'POST', body: JSON.stringify({ command }) }),
|
command: (command: string) => request('/execute/command', { method: 'POST', body: JSON.stringify({ command }) }),
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
import { ref, onMounted, watch } from 'vue'
|
import { ref, onMounted, watch } from 'vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
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 { Input } from '@/components/ui/input'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import Pagination from '@/components/Pagination.vue'
|
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 TextOverflow from '@/components/TextOverflow.vue'
|
||||||
import { api, type EnvVar } from '@/api'
|
import { api, type EnvVar } from '@/api'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
@@ -23,6 +23,8 @@ const isEdit = ref(false)
|
|||||||
const showValues = ref<Record<string, boolean>>({})
|
const showValues = ref<Record<string, boolean>>({})
|
||||||
const showDeleteDialog = ref(false)
|
const showDeleteDialog = ref(false)
|
||||||
const deleteEnvId = ref<string | null>(null)
|
const deleteEnvId = ref<string | null>(null)
|
||||||
|
const associatedTasks = ref<any[]>([])
|
||||||
|
const isDeleting = ref(false)
|
||||||
|
|
||||||
const filterName = ref('')
|
const filterName = ref('')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
@@ -84,22 +86,49 @@ async function saveEnv() {
|
|||||||
} catch { toast.error('保存失败') }
|
} catch { toast.error('保存失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
function confirmDelete(id: string) {
|
async function confirmDelete(id: string) {
|
||||||
deleteEnvId.value = id
|
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
|
if (!deleteEnvId.value) return
|
||||||
|
isDeleting.value = true
|
||||||
try {
|
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('变量已删除')
|
toast.success('变量已删除')
|
||||||
loadEnvVars()
|
loadEnvVars()
|
||||||
} catch { toast.error('删除失败') }
|
showDeleteDialog.value = false
|
||||||
showDeleteDialog.value = false
|
} catch {
|
||||||
deleteEnvId.value = null
|
toast.error('网络错误,删除失败')
|
||||||
|
} finally {
|
||||||
|
isDeleting.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(showDeleteDialog, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
associatedTasks.value = []
|
||||||
|
deleteEnvId.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function toggleShow(id: string) {
|
function toggleShow(id: string) {
|
||||||
showValues.value[id] = !showValues.value[id]
|
showValues.value[id] = !showValues.value[id]
|
||||||
}
|
}
|
||||||
@@ -208,12 +237,54 @@ onMounted(loadEnvVars)
|
|||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>确认删除</AlertDialogTitle>
|
<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>
|
</AlertDialogHeader>
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
<AlertDialogCancel :disabled="isDeleting">取消</AlertDialogCancel>
|
||||||
<AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="deleteEnv">删除
|
<Button v-if="associatedTasks.length > 0" variant="destructive" @click="deleteEnv(true)"
|
||||||
</AlertDialogAction>
|
: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>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
|
|||||||
Reference in New Issue
Block a user