feat: add task cancell

This commit is contained in:
engigu
2026-02-10 16:47:18 +08:00
parent ea4f7be11e
commit aef124defc
10 changed files with 335 additions and 111 deletions
+20
View File
@@ -33,6 +33,7 @@ const (
WSTypeTaskLog = "task_log" WSTypeTaskLog = "task_log"
WSTypeExecute = "execute" WSTypeExecute = "execute"
WSTypeTaskHeartbeat = "task_heartbeat" WSTypeTaskHeartbeat = "task_heartbeat"
WSTypeStop = "stop"
) )
type WSMessage struct { type WSMessage struct {
@@ -370,6 +371,8 @@ func (a *Agent) handleWSMessage(msg *WSMessage) {
a.fetchTasks() a.fetchTasks()
case WSTypeExecute: case WSTypeExecute:
a.handleExecute(msg.Data) a.handleExecute(msg.Data)
case WSTypeStop:
a.handleStop(msg.Data)
} }
} }
@@ -505,6 +508,23 @@ func (a *Agent) handleExecute(data json.RawMessage) {
a.scheduler.EnqueueOrExecute(execReq) a.scheduler.EnqueueOrExecute(execReq)
} }
func (a *Agent) handleStop(data json.RawMessage) {
var req struct {
LogID uint `json:"log_id"`
}
if err := json.Unmarshal(data, &req); err != nil {
logger.Errorf("解析停止请求失败: %v", err)
return
}
logger.Infof("[Agent] 收到停止指令 LogID: %d", req.LogID)
if a.scheduler.StopLog(req.LogID) {
logger.Infof("[Agent] 任务执行 #%d 已成功停止", req.LogID)
} else {
logger.Warnf("[Agent] 任务执行 #%d 停止失败(可能已完成或不在运行队列中)", req.LogID)
}
}
// RealTimeLogWriter 实时日志写入器,通过 WebSocket 发送日志 // RealTimeLogWriter 实时日志写入器,通过 WebSocket 发送日志
type RealTimeLogWriter struct { type RealTimeLogWriter struct {
agent *Agent agent *Agent
+1
View File
@@ -63,6 +63,7 @@ const (
WSTypeEnabled = "enabled" WSTypeEnabled = "enabled"
WSTypeFetchTasks = "fetch_tasks" WSTypeFetchTasks = "fetch_tasks"
WSTypeTaskHeartbeat = "task_heartbeat" WSTypeTaskHeartbeat = "task_heartbeat"
WSTypeStop = "stop"
) )
// TablePrefix 表前缀,从配置文件读取 // TablePrefix 表前缀,从配置文件读取
+16
View File
@@ -236,3 +236,19 @@ func (tc *TaskController) DeleteTask(c *gin.Context) {
utils.SuccessMsg(c, "删除成功") utils.SuccessMsg(c, "删除成功")
} }
func (tc *TaskController) StopTask(c *gin.Context) {
logID, err := strconv.ParseUint(c.Param("logID"), 10, 32)
if err != nil {
utils.BadRequest(c, "无效的日志ID")
return
}
err = tc.executorService.StopTaskExecution(uint(logID))
if err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.SuccessMsg(c, "停止请求已发送")
}
+24 -2
View File
@@ -170,7 +170,8 @@ type Scheduler struct {
wg sync.WaitGroup wg sync.WaitGroup
mu sync.RWMutex mu sync.RWMutex
logger SchedulerLogger logger SchedulerLogger
runningTasks map[string]context.CancelFunc // 记录运行中的任务,用于停止 runningTasks map[string]context.CancelFunc // 记录运行中的任务,用于停止 (TaskID -> CancelFunc)
runningExecs map[uint]context.CancelFunc // 记录运行中的执行,用于停止 (LogID -> CancelFunc)
} }
// NewScheduler 创建调度器 // NewScheduler 创建调度器
@@ -202,6 +203,7 @@ func NewScheduler(config SchedulerConfig, handler SchedulerEventHandler) *Schedu
stopCh: make(chan struct{}), stopCh: make(chan struct{}),
logger: &DefaultLogger{}, logger: &DefaultLogger{},
runningTasks: make(map[string]context.CancelFunc), runningTasks: make(map[string]context.CancelFunc),
runningExecs: make(map[uint]context.CancelFunc),
} }
return s return s
@@ -377,11 +379,17 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error)
// 注册到运行中任务 // 注册到运行中任务
s.mu.Lock() s.mu.Lock()
s.runningTasks[req.TaskID] = cancel s.runningTasks[req.TaskID] = cancel
if req.LogID > 0 {
s.runningExecs[req.LogID] = cancel
}
s.mu.Unlock() s.mu.Unlock()
defer func() { defer func() {
s.mu.Lock() s.mu.Lock()
delete(s.runningTasks, req.TaskID) delete(s.runningTasks, req.TaskID)
if req.LogID > 0 {
delete(s.runningExecs, req.LogID)
}
s.mu.Unlock() s.mu.Unlock()
}() }()
@@ -440,7 +448,7 @@ func (s *Scheduler) executeTask(req *ExecutionRequest) (*ExecutionResult, error)
return result, execErr return result, execErr
} }
// StopTask 停止正在运行的任务 // StopTask 停止正在运行的任务(通过 TaskID,可能会停止多个并发副本)
func (s *Scheduler) StopTask(taskID string) bool { func (s *Scheduler) StopTask(taskID string) bool {
s.mu.RLock() s.mu.RLock()
cancel, exists := s.runningTasks[taskID] cancel, exists := s.runningTasks[taskID]
@@ -454,6 +462,20 @@ func (s *Scheduler) StopTask(taskID string) bool {
return false return false
} }
// StopLog 停止正在运行的任务(通过 LogID,精确停止单个执行副本)
func (s *Scheduler) StopLog(logID uint) bool {
s.mu.RLock()
cancel, exists := s.runningExecs[logID]
s.mu.RUnlock()
if exists && cancel != nil {
cancel()
s.logger.Infof("[Scheduler] 已尝试停止任务执行 #%d", logID)
return true
}
return false
}
// GetRunningTaskCount 获取正在运行的任务数量 // GetRunningTaskCount 获取正在运行的任务数量
func (s *Scheduler) GetRunningTaskCount() int { func (s *Scheduler) GetRunningTaskCount() int {
s.mu.RLock() s.mu.RLock()
+1
View File
@@ -119,6 +119,7 @@ func Setup(c *Controllers) *gin.Engine {
tasks.GET("/:id", c.Task.GetTask) tasks.GET("/:id", c.Task.GetTask)
tasks.PUT("/:id", c.Task.UpdateTask) tasks.PUT("/:id", c.Task.UpdateTask)
tasks.DELETE("/:id", c.Task.DeleteTask) tasks.DELETE("/:id", c.Task.DeleteTask)
tasks.POST("/stop/:logID", c.Task.StopTask)
} }
// Task execution routes // Task execution routes
@@ -480,6 +480,39 @@ func (es *ExecutorService) ExecuteTask(taskID int) *executor.ExecutionResult {
} }
} }
// StopTaskExecution stops a running task execution by LogID
func (es *ExecutorService) StopTaskExecution(logID uint) error {
var taskLog models.TaskLog
if err := database.DB.First(&taskLog, logID).Error; err != nil {
return fmt.Errorf("日志不存在")
}
if taskLog.Status != "running" {
return fmt.Errorf("任务已结束")
}
task := es.taskService.GetTaskByID(int(taskLog.TaskID))
if task == nil {
return fmt.Errorf("任务不存在")
}
// 远程任务:发送停止指令到 Agent
if task.AgentID != nil && *task.AgentID > 0 {
logger.Infof("[Executor] 请求停止远程任务 #%d (Agent #%d, LogID: %d)", task.ID, *task.AgentID, logID)
return es.agentWSManager.SendToAgent(*task.AgentID, constant.WSTypeStop, map[string]interface{}{
"log_id": logID,
})
}
// 本地任务:直接停止调度器中的执行实例
logger.Infof("[Executor] 请求停止本地任务 #%d (LogID: %d)", task.ID, logID)
if es.scheduler.StopLog(logID) {
return nil
}
return fmt.Errorf("任务当前不在运行队列中或已完成")
}
// GetRunningCount 获取正在运行任务数量 // GetRunningCount 获取正在运行任务数量
func (es *ExecutorService) GetRunningCount() int { func (es *ExecutorService) GetRunningCount() int {
return es.scheduler.GetRunningTaskCount() return es.scheduler.GetRunningTaskCount()
+2 -1
View File
@@ -69,7 +69,8 @@ export const api = {
create: (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) }), create: (data: Partial<Task>) => request<Task>('/tasks', { method: 'POST', body: JSON.stringify(data) }),
update: (id: number, data: Partial<Task>) => request<Task>(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }), update: (id: number, data: Partial<Task>) => request<Task>(`/tasks/${id}`, { method: 'PUT', body: JSON.stringify(data) }),
delete: (id: number) => request(`/tasks/${id}`, { method: 'DELETE' }), delete: (id: number) => request(`/tasks/${id}`, { method: 'DELETE' }),
execute: (id: number) => request<ExecutionResult>(`/execute/task/${id}`, { method: 'POST' }) execute: (id: number) => request<ExecutionResult>(`/execute/task/${id}`, { method: 'POST' }),
stop: (logID: number) => request(`/tasks/stop/${logID}`, { method: 'POST' })
}, },
scripts: { scripts: {
list: () => request<Script[]>('/scripts'), list: () => request<Script[]>('/scripts'),
+111 -59
View File
@@ -6,7 +6,7 @@ import { Label } from '@/components/ui/label'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog' import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { RefreshCw, Trash2, Edit, Copy, Server, Search, Download, RotateCw, Plus, Ticket, Power, PowerOff, ListTodo, Eye } from 'lucide-vue-next' import { RefreshCw, Trash2, Edit, Copy, Server, Search, Download, RotateCw, Plus, Ticket, ListTodo, Eye, WifiOff, Zap, Check, X } from 'lucide-vue-next'
import { api, type Agent, type AgentToken } from '@/api' import { api, type Agent, type AgentToken } from '@/api'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -226,7 +226,8 @@ onUnmounted(() => {
<TabsContent value="agents" class="mt-4"> <TabsContent value="agents" class="mt-4">
<div class="rounded-lg border bg-card overflow-x-auto hide-scrollbar"> <div class="rounded-lg border bg-card overflow-x-auto hide-scrollbar">
<!-- 大屏表头 --> <!-- 大屏表头 -->
<div class="hidden sm:flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium"> <div
class="hidden sm:flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium">
<span class="w-10 sm:w-12 shrink-0">ID</span> <span class="w-10 sm:w-12 shrink-0">ID</span>
<span class="w-6 shrink-0"></span> <span class="w-6 shrink-0"></span>
<span class="w-24 sm:w-32 shrink-0">名称</span> <span class="w-24 sm:w-32 shrink-0">名称</span>
@@ -243,21 +244,35 @@ onUnmounted(() => {
{{ searchQuery ? '无匹配结果' : '暂无 Agent' }} {{ searchQuery ? '无匹配结果' : '暂无 Agent' }}
</div> </div>
<!-- 小屏布局 --> <!-- 小屏布局 -->
<div v-for="agent in filteredAgents" :key="agent.id" class="sm:hidden p-3 hover:bg-muted/50 transition-colors"> <div v-for="agent in filteredAgents" :key="agent.id"
class="sm:hidden p-3 hover:bg-muted/50 transition-colors">
<div class="flex items-start justify-between mb-2"> <div class="flex items-start justify-between mb-2">
<div class="flex items-center gap-2 flex-1 min-w-0"> <div class="flex items-center gap-2 flex-1 min-w-0">
<span class="text-xs text-muted-foreground shrink-0">#{{ agent.id }}</span> <span class="text-xs text-muted-foreground shrink-0">#{{ agent.id }}</span>
<span class="relative flex h-2.5 w-2.5 shrink-0" :title="isOnline(agent) ? '在线' : '离线'"> <span class="flex items-center shrink-0" :title="isOnline(agent) ? '在线' : '离线'">
<span v-if="isOnline(agent)" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span> <div v-if="isOnline(agent)"
<span :class="isOnline(agent) ? 'bg-green-500' : 'bg-gray-400'" class="relative inline-flex rounded-full h-2.5 w-2.5"></span> class="h-5 w-5 rounded-full bg-green-500/10 flex items-center justify-center">
<Zap class="h-3 w-3 text-green-500 fill-green-500" />
</div>
<div v-else class="h-5 w-5 rounded-full bg-muted flex items-center justify-center">
<WifiOff class="h-3 w-3 text-muted-foreground" />
</div>
</span> </span>
<span class="font-medium text-sm truncate cursor-pointer hover:text-primary" @click="viewDetail(agent)" :title="agent.name">{{ agent.name }}</span> <span class="font-medium text-sm truncate cursor-pointer hover:text-primary"
@click="viewDetail(agent)" :title="agent.name">{{ agent.name }}</span>
</div> </div>
<div class="flex items-center gap-0.5 shrink-0 ml-2"> <div class="flex items-center gap-2 shrink-0 ml-2">
<Button variant="ghost" size="icon" class="h-7 w-7" @click="toggleEnabled(agent)" :title="agent.enabled ? '禁用' : '启用'"> <span class="cursor-pointer group" @click="toggleEnabled(agent)"
<Power v-if="agent.enabled" class="h-3.5 w-3.5 text-green-600" /> :title="agent.enabled ? '点击禁用' : '点击启用'">
<PowerOff v-else class="h-3.5 w-3.5 text-gray-400" /> <div v-if="agent.enabled"
</Button> class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
</div>
<div v-else
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
</div>
</span>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="viewDetail(agent)" title="详情"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="viewDetail(agent)" title="详情">
<Eye class="h-3.5 w-3.5" /> <Eye class="h-3.5 w-3.5" />
</Button> </Button>
@@ -293,39 +308,60 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<!-- 大屏布局 --> <!-- 大屏布局 -->
<div v-for="agent in filteredAgents" :key="`desktop-${agent.id}`" class="hidden sm:flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors"> <div v-for="agent in filteredAgents" :key="`desktop-${agent.id}`"
class="hidden sm:flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors">
<span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ agent.id }}</span> <span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ agent.id }}</span>
<span class="w-6 shrink-0 flex justify-center"> <span class="w-6 shrink-0 flex justify-center">
<span class="relative flex h-2.5 w-2.5" :title="isOnline(agent) ? '在线' : '离线'"> <span class="flex justify-center shrink-0" :title="isOnline(agent) ? '在线' : '离线'">
<span v-if="isOnline(agent)" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span> <div v-if="isOnline(agent)"
<span :class="isOnline(agent) ? 'bg-green-500' : 'bg-gray-400'" class="relative inline-flex rounded-full h-2.5 w-2.5"></span> class="h-6 w-6 rounded-full bg-green-500/10 flex items-center justify-center">
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
</div>
<div v-else class="h-6 w-6 rounded-full bg-muted flex items-center justify-center">
<WifiOff class="h-3.5 w-3.5 text-muted-foreground" />
</div>
</span> </span>
</span> </span>
<span class="w-24 sm:w-32 shrink-0 font-medium text-xs sm:text-sm truncate cursor-pointer hover:text-primary" @click="viewDetail(agent)" :title="agent.name">{{ agent.name }}</span> <span
<span class="w-24 sm:w-28 shrink-0 text-xs sm:text-sm text-muted-foreground truncate">{{ agent.ip || '-' }}</span> class="w-24 sm:w-32 shrink-0 font-medium text-xs sm:text-sm truncate cursor-pointer hover:text-primary"
<span class="w-20 sm:w-32 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden md:block">{{ agent.hostname || '-' }}</span> @click="viewDetail(agent)" :title="agent.name">{{ agent.name }}</span>
<span class="w-20 sm:w-36 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden lg:block">{{ agent.version || '-' }}</span> <span class="w-24 sm:w-28 shrink-0 text-xs sm:text-sm text-muted-foreground truncate">{{ agent.ip || '-'
<span class="w-40 shrink-0 text-xs sm:text-sm text-muted-foreground hidden xl:block">{{ agent.last_seen || '-' }}</span> }}</span>
<span class="w-40 shrink-0 text-xs sm:text-sm text-muted-foreground hidden xl:block">{{ agent.created_at || '-' }}</span> <span class="w-20 sm:w-32 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden md:block">{{
<span class="flex-1 flex justify-end gap-0.5 sm:gap-1"> agent.hostname || '-' }}</span>
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="toggleEnabled(agent)" :title="agent.enabled ? '禁用' : '启用'"> <span class="w-20 sm:w-36 shrink-0 text-xs sm:text-sm text-muted-foreground truncate hidden lg:block">{{
<Power v-if="agent.enabled" class="h-3 w-3 sm:h-3.5 sm:w-3.5 text-green-600" /> agent.version || '-' }}</span>
<PowerOff v-else class="h-3 w-3 sm:h-3.5 sm:w-3.5 text-gray-400" /> <span class="w-40 shrink-0 text-xs sm:text-sm text-muted-foreground hidden xl:block">{{ agent.last_seen ||
'-' }}</span>
<span class="w-40 shrink-0 text-xs sm:text-sm text-muted-foreground hidden xl:block">{{ agent.created_at
|| '-' }}</span>
<span class="flex-1 flex justify-end gap-2 items-center">
<span class="cursor-pointer group" @click="toggleEnabled(agent)"
:title="agent.enabled ? '点击禁用' : '点击启用'">
<div v-if="agent.enabled"
class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
<Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
</div>
<div v-else
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
</div>
</span>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="viewDetail(agent)" title="详情">
<Eye class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="viewDetail(agent)" title="详情"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="viewTasks(agent)" title="查看任务">
<Eye class="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <ListTodo class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="viewTasks(agent)" title="查看任务"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="forceUpdate(agent)" title="强制更新">
<ListTodo class="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <RotateCw class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="forceUpdate(agent)" title="强制更新"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="openEditDialog(agent)" title="编辑">
<RotateCw class="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <Edit class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="openEditDialog(agent)" title="编辑"> <Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="confirmDelete(agent)"
<Edit class="h-3 w-3 sm:h-3.5 sm:w-3.5" /> title="删除">
</Button> <Trash2 class="h-3.5 w-3.5" />
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7 text-destructive" @click="confirmDelete(agent)" title="删除">
<Trash2 class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
</Button> </Button>
</span> </span>
</div> </div>
@@ -335,7 +371,8 @@ onUnmounted(() => {
<TabsContent value="regcodes" class="mt-4"> <TabsContent value="regcodes" class="mt-4">
<div class="rounded-lg border bg-card overflow-x-auto hide-scrollbar"> <div class="rounded-lg border bg-card overflow-x-auto hide-scrollbar">
<div class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium min-w-[500px]"> <div
class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium min-w-[500px]">
<span class="w-6 shrink-0"></span> <span class="w-6 shrink-0"></span>
<span class="flex-1 min-w-[200px]">令牌</span> <span class="flex-1 min-w-[200px]">令牌</span>
<span class="w-24 sm:w-32 shrink-0">备注</span> <span class="w-24 sm:w-32 shrink-0">备注</span>
@@ -351,15 +388,21 @@ onUnmounted(() => {
<div v-if="tokens.length === 0" class="text-center py-8 text-muted-foreground"> <div v-if="tokens.length === 0" class="text-center py-8 text-muted-foreground">
<Ticket class="h-8 w-8 mx-auto mb-2 opacity-50" />暂无令牌 <Ticket class="h-8 w-8 mx-auto mb-2 opacity-50" />暂无令牌
</div> </div>
<div v-for="token in tokens" :key="token.id" class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors"> <div v-for="token in tokens" :key="token.id"
class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors">
<span class="w-6 shrink-0 flex justify-center"> <span class="w-6 shrink-0 flex justify-center">
<span class="relative flex h-2.5 w-2.5"> <div v-if="!isTokenExpired(token) && !isTokenExhausted(token)"
<span v-if="!isTokenExpired(token) && !isTokenExhausted(token)" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span> class="h-5 w-5 rounded-full bg-green-500/10 flex items-center justify-center">
<span :class="!isTokenExpired(token) && !isTokenExhausted(token) ? 'bg-green-500' : 'bg-gray-400'" class="relative inline-flex rounded-full h-2.5 w-2.5"></span> <Check class="h-3 w-3 text-green-500 stroke-[3]" />
</span> </div>
<div v-else class="h-5 w-5 rounded-full bg-red-500/10 flex items-center justify-center">
<X class="h-3 w-3 text-red-500 stroke-[3]" />
</div>
</span> </span>
<code class="flex-1 min-w-[200px] font-mono text-xs bg-muted px-2 py-0.5 rounded truncate">{{ token.token }}</code> <code
<span class="w-24 sm:w-32 shrink-0 text-xs sm:text-sm text-muted-foreground truncate">{{ token.remark || '-' }}</span> class="flex-1 min-w-[200px] font-mono text-xs bg-muted px-2 py-0.5 rounded truncate">{{ token.token }}</code>
<span class="w-24 sm:w-32 shrink-0 text-xs sm:text-sm text-muted-foreground truncate">{{ token.remark ||
'-' }}</span>
<span class="w-16 sm:w-20 shrink-0 text-xs sm:text-sm text-muted-foreground text-center"> <span class="w-16 sm:w-20 shrink-0 text-xs sm:text-sm text-muted-foreground text-center">
{{ token.used_count }}/{{ token.max_uses === 0 ? '∞' : token.max_uses }} {{ token.used_count }}/{{ token.max_uses === 0 ? '∞' : token.max_uses }}
</span> </span>
@@ -370,7 +413,8 @@ onUnmounted(() => {
<Button variant="ghost" size="icon" class="h-7 w-7" @click="copyToken(token.token)" title="复制"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="copyToken(token.token)" title="复制">
<Copy class="h-3.5 w-3.5" /> <Copy class="h-3.5 w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="deleteToken(token.id)" title="删除"> <Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="deleteToken(token.id)"
title="删除">
<Trash2 class="h-3.5 w-3.5" /> <Trash2 class="h-3.5 w-3.5" />
</Button> </Button>
</span> </span>
@@ -423,10 +467,8 @@ onUnmounted(() => {
<div class="flex items-center justify-between sm:block"> <div class="flex items-center justify-between sm:block">
<Label class="text-muted-foreground text-xs">在线状态</Label> <Label class="text-muted-foreground text-xs">在线状态</Label>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="relative flex h-2.5 w-2.5"> <Wifi v-if="isOnline(viewingAgent)" class="h-4 w-4 text-green-500" />
<span v-if="isOnline(viewingAgent)" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span> <WifiOff v-else class="h-4 w-4 text-muted-foreground" />
<span :class="isOnline(viewingAgent) ? 'bg-green-500' : 'bg-gray-400'" class="relative inline-flex rounded-full h-2.5 w-2.5"></span>
</span>
<span class="text-sm">{{ isOnline(viewingAgent) ? '在线' : '离线' }}</span> <span class="text-sm">{{ isOnline(viewingAgent) ? '在线' : '离线' }}</span>
</div> </div>
</div> </div>
@@ -485,7 +527,8 @@ onUnmounted(() => {
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel> <AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="deleteAgent">删除</AlertDialogAction> <AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="deleteAgent">删除
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
@@ -499,7 +542,8 @@ onUnmounted(() => {
</DialogHeader> </DialogHeader>
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-2"> <div class="space-y-2">
<div v-for="platform in platforms" :key="`${platform.os}-${platform.arch}`" class="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors"> <div v-for="platform in platforms" :key="`${platform.os}-${platform.arch}`"
class="flex items-center justify-between p-3 border rounded-lg hover:bg-muted/50 transition-colors">
<span class="font-medium">{{ getPlatformLabel(platform.os, platform.arch) }}</span> <span class="font-medium">{{ getPlatformLabel(platform.os, platform.arch) }}</span>
<Button size="sm" @click="downloadAgent(platform.os, platform.arch)"> <Button size="sm" @click="downloadAgent(platform.os, platform.arch)">
<Download class="h-4 w-4 mr-1.5" />下载 <Download class="h-4 w-4 mr-1.5" />下载
@@ -510,18 +554,26 @@ onUnmounted(() => {
<h4 class="font-medium mb-2">使用说明</h4> <h4 class="font-medium mb-2">使用说明</h4>
<ol class="text-sm text-muted-foreground space-y-1.5 list-decimal list-inside"> <ol class="text-sm text-muted-foreground space-y-1.5 list-decimal list-inside">
<li>下载对应平台的 Agent 压缩包并解压</li> <li>下载对应平台的 Agent 压缩包并解压</li>
<li>复制 <code class="bg-muted px-1.5 py-0.5 rounded text-foreground">config.example.ini</code> <code class="bg-muted px-1.5 py-0.5 rounded text-foreground">config.ini</code></li> <li>复制 <code class="bg-muted px-1.5 py-0.5 rounded text-foreground">config.example.ini</code> <code
class="bg-muted px-1.5 py-0.5 rounded text-foreground">config.ini</code></li>
<li>编辑 <code class="bg-muted px-1.5 py-0.5 rounded text-foreground">config.ini</code>填写服务器地址和注册令牌</li> <li>编辑 <code class="bg-muted px-1.5 py-0.5 rounded text-foreground">config.ini</code>填写服务器地址和注册令牌</li>
<li>运行 <code class="bg-muted px-1.5 py-0.5 rounded text-foreground">./baihu-agent start</code> 启动后台运行</li> <li>运行 <code class="bg-muted px-1.5 py-0.5 rounded text-foreground">./baihu-agent start</code> 启动后台运行
</li>
</ol> </ol>
<div class="mt-3 text-sm text-muted-foreground"> <div class="mt-3 text-sm text-muted-foreground">
<p class="font-medium text-foreground mb-1.5">常用命令</p> <p class="font-medium text-foreground mb-1.5">常用命令</p>
<div class="space-y-1"> <div class="space-y-1">
<div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent start</code> <span class="text-xs">- 后台启动</span></div> <div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent start</code> <span
<div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent stop</code> <span class="text-xs">- 停止运行</span></div> class="text-xs">- 后台启动</span></div>
<div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent status</code> <span class="text-xs">- 查看状态</span></div> <div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent stop</code> <span
<div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent logs</code> <span class="text-xs">- 查看日志</span></div> class="text-xs">- 停止运行</span></div>
<div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent run</code> <span class="text-xs">- 前台运行</span></div> <div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent status</code>
<span class="text-xs">- 查看状态</span>
</div>
<div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent logs</code> <span
class="text-xs">- 查看日志</span></div>
<div><code class="bg-muted px-1.5 py-0.5 rounded text-foreground text-xs">baihu-agent run</code> <span
class="text-xs">- 前台运行</span></div>
</div> </div>
</div> </div>
</div> </div>
+85 -20
View File
@@ -5,8 +5,9 @@ import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import Pagination from '@/components/Pagination.vue' import Pagination from '@/components/Pagination.vue'
import LogViewer from './LogViewer.vue' import LogViewer from './LogViewer.vue'
import { RefreshCw, X, Search, Maximize2, GitBranch, Terminal } from 'lucide-vue-next' import { RefreshCw, X, Search, Maximize2, GitBranch, Terminal, CheckCircle2, XCircle, AlertCircle, Ban, Clock, Zap, Check } from 'lucide-vue-next'
import { api, type TaskLog } from '@/api' import { api, type TaskLog } from '@/api'
import { Badge } from '@/components/ui/badge'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings' import { useSiteSettings } from '@/composables/useSiteSettings'
import TextOverflow from '@/components/TextOverflow.vue' import TextOverflow from '@/components/TextOverflow.vue'
@@ -169,6 +170,21 @@ function closeDetail() {
wsContent.value = '' wsContent.value = ''
} }
const isStopping = ref(false)
async function stopTask() {
if (!selectedLog.value || isStopping.value) return
try {
isStopping.value = true
await api.tasks.stop(selectedLog.value.id)
toast.success('停止请求已发送')
} catch (err: any) {
toast.error(err.message || '停止失败')
} finally {
isStopping.value = false
}
}
function formatDuration(ms: number): string { function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}毫秒` if (ms < 1000) return `${ms}毫秒`
if (ms < 60000) return `${(ms / 1000).toFixed(1)}` if (ms < 60000) return `${(ms / 1000).toFixed(1)}`
@@ -256,11 +272,30 @@ watch(() => route.query.task_id, (newTaskId) => {
</span> </span>
<span class="flex-1 min-w-0 font-medium truncate text-xs">{{ log.task_name }}</span> <span class="flex-1 min-w-0 font-medium truncate text-xs">{{ log.task_name }}</span>
<span class="w-8 flex justify-center shrink-0"> <span class="w-8 flex justify-center shrink-0">
<span class="relative flex h-2.5 w-2.5"> <div v-if="log.status === 'success'"
<span class="h-5 w-5 rounded-full bg-green-500/10 flex items-center justify-center">
:class="log.status === 'success' ? 'bg-green-500' : log.status === 'failed' ? 'bg-red-500' : 'bg-yellow-500'" <Check class="h-3 w-3 text-green-500 stroke-[3]" />
class="relative inline-flex rounded-full h-2.5 w-2.5"></span> </div>
</span> <div v-else-if="log.status === 'failed'"
class="h-5 w-5 rounded-full bg-red-500/10 flex items-center justify-center">
<X class="h-3 w-3 text-red-500 stroke-[3]" />
</div>
<div v-else-if="log.status === 'running'"
class="h-5 w-5 rounded-full bg-yellow-500/10 flex items-center justify-center">
<Zap class="h-3 w-3 text-yellow-500 fill-yellow-500 animate-pulse" />
</div>
<div v-else-if="log.status === 'pending'"
class="h-5 w-5 rounded-full bg-yellow-500/10 flex items-center justify-center">
<Clock class="h-3 w-3 text-yellow-500" />
</div>
<div v-else-if="log.status === 'timeout'"
class="h-5 w-5 rounded-full bg-orange-500/10 flex items-center justify-center">
<AlertCircle class="h-3 w-3 text-orange-500" />
</div>
<div v-else-if="log.status === 'cancelled'"
class="h-5 w-5 rounded-full bg-muted flex items-center justify-center">
<Ban class="h-3 w-3 text-muted-foreground" />
</div>
</span> </span>
<span class="w-12 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration) <span class="w-12 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration)
}}</span> }}</span>
@@ -277,11 +312,30 @@ watch(() => route.query.task_id, (newTaskId) => {
<TextOverflow :text="log.command" title="执行命令" /> <TextOverflow :text="log.command" title="执行命令" />
</code> </code>
<span class="w-12 flex justify-center shrink-0"> <span class="w-12 flex justify-center shrink-0">
<span class="relative flex h-2.5 w-2.5"> <div v-if="log.status === 'success'"
<span class="h-6 w-6 rounded-full bg-green-500/10 flex items-center justify-center">
:class="log.status === 'success' ? 'bg-green-500' : log.status === 'failed' ? 'bg-red-500' : 'bg-yellow-500'" <Check class="h-3.5 w-3.5 text-green-500 stroke-[3]" />
class="relative inline-flex rounded-full h-2.5 w-2.5"></span> </div>
</span> <div v-else-if="log.status === 'failed'"
class="h-6 w-6 rounded-full bg-red-500/10 flex items-center justify-center">
<X class="h-3.5 w-3.5 text-red-500 stroke-[3]" />
</div>
<div v-else-if="log.status === 'running'"
class="h-6 w-6 rounded-full bg-yellow-500/10 flex items-center justify-center">
<Zap class="h-3.5 w-3.5 text-yellow-500 fill-yellow-500 animate-pulse" />
</div>
<div v-else-if="log.status === 'pending'"
class="h-6 w-6 rounded-full bg-yellow-500/10 flex items-center justify-center">
<Clock class="h-3.5 w-3.5 text-yellow-500" />
</div>
<div v-else-if="log.status === 'timeout'"
class="h-6 w-6 rounded-full bg-orange-500/10 flex items-center justify-center">
<AlertCircle class="h-3.5 w-3.5 text-orange-500" />
</div>
<div v-else-if="log.status === 'cancelled'"
class="h-6 w-6 rounded-full bg-muted flex items-center justify-center">
<Ban class="h-3.5 w-3.5 text-muted-foreground" />
</div>
</span> </span>
<span class="w-16 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration) <span class="w-16 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration)
}}</span> }}</span>
@@ -299,7 +353,13 @@ watch(() => route.query.task_id, (newTaskId) => {
<div v-if="selectedLog" <div v-if="selectedLog"
class="w-full lg:w-[480px] rounded-lg border bg-card flex flex-col overflow-hidden shrink-0 max-h-[60vh] lg:max-h-[calc(100vh-180px)]"> class="w-full lg:w-[480px] rounded-lg border bg-card flex flex-col overflow-hidden shrink-0 max-h-[60vh] lg:max-h-[calc(100vh-180px)]">
<div class="flex items-center justify-between px-4 py-3 border-b"> <div class="flex items-center justify-between px-4 py-3 border-b">
<span class="text-sm font-medium">日志详情</span> <div class="flex items-center gap-2">
<span class="text-sm font-medium">日志详情</span>
<Button v-if="selectedLog.status === 'running'" variant="destructive" size="sm" class="h-6 px-2 text-[10px]"
:disabled="isStopping" @click="stopTask">
{{ isStopping ? '停止中...' : '停止任务' }}
</Button>
</div>
<Button variant="ghost" size="icon" class="h-7 w-7" @click="closeDetail"> <Button variant="ghost" size="icon" class="h-7 w-7" @click="closeDetail">
<X class="h-3.5 w-3.5" /> <X class="h-3.5 w-3.5" />
</Button> </Button>
@@ -311,14 +371,19 @@ watch(() => route.query.task_id, (newTaskId) => {
</div> </div>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<span class="text-muted-foreground">状态</span> <span class="text-muted-foreground">状态</span>
<span class="flex items-center gap-1.5"> <Badge
<span class="relative flex h-2.5 w-2.5"> :variant="selectedLog.status === 'success' ? 'default' : selectedLog.status === 'failed' ? 'destructive' : 'secondary'"
<span class="capitalize px-4 py-0.5">
:class="selectedLog.status === 'success' ? 'bg-green-500' : selectedLog.status === 'failed' ? 'bg-red-500' : 'bg-yellow-500'" <div class="flex items-center gap-1.5">
class="relative inline-flex rounded-full h-2.5 w-2.5"></span> <CheckCircle2 v-if="selectedLog.status === 'success'" class="h-3 w-3" />
</span> <XCircle v-else-if="selectedLog.status === 'failed'" class="h-3 w-3" />
{{ selectedLog.status }} <Zap v-else-if="selectedLog.status === 'running'" class="h-3 w-3 fill-current animate-pulse" />
</span> <Clock v-else-if="selectedLog.status === 'pending'" class="h-3 w-3" />
<AlertCircle v-else-if="selectedLog.status === 'timeout'" class="h-3 w-3" />
<Ban v-else-if="selectedLog.status === 'cancelled'" class="h-3 w-3" />
{{ selectedLog.status }}
</div>
</Badge>
</div> </div>
<div class="flex justify-between"> <div class="flex justify-between">
<span class="text-muted-foreground">耗时</span> <span class="text-muted-foreground">耗时</span>
+35 -22
View File
@@ -6,7 +6,7 @@ import { Input } from '@/components/ui/input'
import Pagination from '@/components/Pagination.vue' import Pagination from '@/components/Pagination.vue'
import TaskDialog from './TaskDialog.vue' import TaskDialog from './TaskDialog.vue'
import RepoDialog from './RepoDialog.vue' import RepoDialog from './RepoDialog.vue'
import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2 } from 'lucide-vue-next' import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, Wifi, WifiOff, Zap, ZapOff } from 'lucide-vue-next'
import { api, type Task, type Agent } from '@/api' import { api, type Task, type Agent } from '@/api'
import { toast } from 'vue-sonner' import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings' import { useSiteSettings } from '@/composables/useSiteSettings'
@@ -202,9 +202,11 @@ watch(() => route.query.agent_id, (newVal) => {
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<div class="relative flex-1 sm:flex-none"> <div class="relative flex-1 sm:flex-none">
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> <Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full sm:w-56 text-sm" @input="handleSearch" /> <Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full sm:w-56 text-sm"
@input="handleSearch" />
</div> </div>
<div v-if="filterAgentId" class="flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm"> <div v-if="filterAgentId"
class="flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm">
<Server class="h-3.5 w-3.5" /> <Server class="h-3.5 w-3.5" />
<span>{{ filterAgentName }}</span> <span>{{ filterAgentName }}</span>
<X class="h-3.5 w-3.5 cursor-pointer hover:text-destructive" @click="clearAgentFilter" /> <X class="h-3.5 w-3.5 cursor-pointer hover:text-destructive" @click="clearAgentFilter" />
@@ -220,7 +222,8 @@ watch(() => route.query.agent_id, (newVal) => {
<div class="rounded-lg border bg-card overflow-x-auto"> <div class="rounded-lg border bg-card overflow-x-auto">
<!-- 表头 --> <!-- 表头 -->
<div class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium min-w-[360px] sm:min-w-[800px]"> <div
class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 border-b bg-muted/50 text-xs sm:text-sm text-muted-foreground font-medium min-w-[360px] sm:min-w-[800px]">
<span class="w-12 sm:w-14 shrink-0">ID</span> <span class="w-12 sm:w-14 shrink-0">ID</span>
<span class="w-6 sm:w-8 shrink-0 text-center">类型</span> <span class="w-6 sm:w-8 shrink-0 text-center">类型</span>
<span class="flex-1 min-w-0">名称</span> <span class="flex-1 min-w-0">名称</span>
@@ -237,11 +240,8 @@ watch(() => route.query.agent_id, (newVal) => {
<div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8"> <div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8">
暂无任务 暂无任务
</div> </div>
<div <div v-for="task in tasks" :key="task.id"
v-for="task in tasks" class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors">
:key="task.id"
class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors"
>
<span class="w-12 sm:w-14 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ task.id }}</span> <span class="w-12 sm:w-14 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ task.id }}</span>
<span class="w-6 sm:w-8 shrink-0 flex justify-center" :title="getTaskTypeTitle(task.type || 'task')"> <span class="w-6 sm:w-8 shrink-0 flex justify-center" :title="getTaskTypeTitle(task.type || 'task')">
<GitBranch v-if="task.type === 'repo'" class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" /> <GitBranch v-if="task.type === 'repo'" class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
@@ -250,23 +250,34 @@ watch(() => route.query.agent_id, (newVal) => {
<span class="flex-1 min-w-0 font-medium truncate text-xs sm:text-sm">{{ task.name }}</span> <span class="flex-1 min-w-0 font-medium truncate text-xs sm:text-sm">{{ task.name }}</span>
<span class="w-20 shrink-0 hidden md:flex items-center gap-1 text-xs" :title="getExecutorName(task)"> <span class="w-20 shrink-0 hidden md:flex items-center gap-1 text-xs" :title="getExecutorName(task)">
<Monitor v-if="!task.agent_id" class="h-3 w-3 text-muted-foreground" /> <Monitor v-if="!task.agent_id" class="h-3 w-3 text-muted-foreground" />
<Server v-else class="h-3 w-3" :class="getExecutorStatus(task) === 'online' ? 'text-green-500' : 'text-gray-400'" /> <template v-else>
<Wifi v-if="getExecutorStatus(task) === 'online'" class="h-3 w-3 text-green-500" />
<WifiOff v-else class="h-3 w-3 text-muted-foreground" />
</template>
<span class="truncate">{{ getExecutorName(task) }}</span> <span class="truncate">{{ getExecutorName(task) }}</span>
</span> </span>
<code class="w-32 sm:flex-1 shrink-0 sm:shrink text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded hidden sm:block"> <code
<TextOverflow :text="task.command" :title="task.type === 'repo' ? '同步地址' : '执行命令'" /> class="w-32 sm:flex-1 shrink-0 sm:shrink text-muted-foreground truncate text-xs bg-muted px-2 py-1 rounded hidden sm:block">
</code> <TextOverflow :text="task.command" :title="task.type === 'repo' ? '同步地址' : '执行命令'" />
<code class="w-36 shrink-0 text-muted-foreground text-xs bg-muted px-2 py-1 rounded hidden md:block">{{ task.schedule }}</code> </code>
<code class="w-36 shrink-0 text-muted-foreground text-xs bg-muted px-2 py-1 rounded hidden md:block">{{ task.schedule
}}</code>
<span class="w-40 shrink-0 text-muted-foreground text-xs hidden lg:block">{{ task.last_run || '-' }}</span> <span class="w-40 shrink-0 text-muted-foreground text-xs hidden lg:block">{{ task.last_run || '-' }}</span>
<span class="w-40 shrink-0 text-muted-foreground text-xs hidden lg:block">{{ task.next_run || '-' }}</span> <span class="w-40 shrink-0 text-muted-foreground text-xs hidden lg:block">{{ task.next_run || '-' }}</span>
<span class="w-8 sm:w-12 flex justify-center shrink-0 cursor-pointer" @click="toggleTask(task, !task.enabled)" :title="task.enabled ? '点击禁用' : '点击启用'"> <span class="w-8 sm:w-12 flex justify-center shrink-0 cursor-pointer group"
<span class="relative flex h-2.5 w-2.5"> @click="toggleTask(task, !task.enabled)" :title="task.enabled ? '点击禁用' : '点击启用'">
<span v-if="task.enabled" class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span> <div v-if="task.enabled"
<span :class="task.enabled ? 'bg-green-500' : 'bg-gray-400'" class="relative inline-flex rounded-full h-2.5 w-2.5"></span> class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center group-hover:bg-green-500/20 transition-colors">
</span> <Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" />
</div>
<div v-else
class="h-6 w-6 rounded-md bg-muted flex items-center justify-center group-hover:bg-muted/80 transition-colors">
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
</div>
</span> </span>
<span class="w-20 sm:w-36 shrink-0 flex justify-center gap-0.5 sm:gap-1"> <span class="w-20 sm:w-36 shrink-0 flex justify-center gap-0.5 sm:gap-1">
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="runTask(task.id)" title="执行" :disabled="executingTaskId === task.id"> <Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="runTask(task.id)" title="执行"
:disabled="executingTaskId === task.id">
<Loader2 v-if="executingTaskId === task.id" class="h-3 w-3 sm:h-3.5 sm:w-3.5 animate-spin" /> <Loader2 v-if="executingTaskId === task.id" class="h-3 w-3 sm:h-3.5 sm:w-3.5 animate-spin" />
<Play v-else class="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <Play v-else class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
</Button> </Button>
@@ -276,7 +287,8 @@ watch(() => route.query.agent_id, (newVal) => {
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="openEdit(task)" title="编辑"> <Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7" @click="openEdit(task)" title="编辑">
<Pencil class="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <Pencil class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
</Button> </Button>
<Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7 text-destructive" @click="confirmDelete(task.id)" title="删除"> <Button variant="ghost" size="icon" class="h-6 w-6 sm:h-7 sm:w-7 text-destructive"
@click="confirmDelete(task.id)" title="删除">
<Trash2 class="h-3 w-3 sm:h-3.5 sm:w-3.5" /> <Trash2 class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
</Button> </Button>
</span> </span>
@@ -301,7 +313,8 @@ watch(() => route.query.agent_id, (newVal) => {
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel> <AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="deleteTask">删除</AlertDialogAction> <AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="deleteTask">删除
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>