diff --git a/internal/controllers/log_controller.go b/internal/controllers/log_controller.go index 763be80..6a92c6b 100644 --- a/internal/controllers/log_controller.go +++ b/internal/controllers/log_controller.go @@ -97,3 +97,41 @@ func (lc *LogController) GetLogDetail(c *gin.Context) { utils.Success(c, vo.ToTaskLogVO(&log)) } + +func (lc *LogController) ClearLogs(c *gin.Context) { + var req struct { + TaskID *int `json:"task_id"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + query := database.DB.Model(&models.TaskLog{}) + if req.TaskID != nil && *req.TaskID > 0 { + query = query.Where("task_id = ?", *req.TaskID) + } + + if err := query.Delete(&models.TaskLog{}).Error; err != nil { + utils.ServerError(c, "清空日志失败") + return + } + + utils.SuccessMsg(c, "日志清空成功") +} + +func (lc *LogController) DeleteLog(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + utils.BadRequest(c, "无效的日志ID") + return + } + + if err := database.DB.Delete(&models.TaskLog{}, id).Error; err != nil { + utils.ServerError(c, "删除日志失败") + return + } + + utils.SuccessMsg(c, "日志已删除") +} diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index 38a5c8d..c26bb6b 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -109,6 +109,7 @@ func (tc *TaskController) GetTasks(c *gin.Context) { agentIDStr := c.DefaultQuery("agent_id", "") tags := c.DefaultQuery("tags", "") + taskType := c.DefaultQuery("type", "") var agentID *uint if agentIDStr != "" { @@ -118,7 +119,7 @@ func (tc *TaskController) GetTasks(c *gin.Context) { } } - tasks, total := tc.taskService.GetTasksWithPagination(p.Page, p.PageSize, name, agentID, tags) + tasks, total := tc.taskService.GetTasksWithPagination(p.Page, p.PageSize, name, agentID, tags, taskType) utils.PaginatedResponse(c, vo.ToTaskVOListFromModels(tasks), total, p) } diff --git a/internal/router/router.go b/internal/router/router.go index c2e1e6f..be3fbcf 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -172,8 +172,10 @@ func Setup(c *Controllers) *gin.Engine { logs := authorized.Group("/logs") { logs.GET("", c.Log.GetLogs) + logs.POST("/clear", c.Log.ClearLogs) logs.GET("/ws", c.LogWS.StreamLog) logs.GET("/:id", c.Log.GetLogDetail) + logs.DELETE("/:id", c.Log.DeleteLog) } // 终端模块 diff --git a/internal/services/tasks/task_service.go b/internal/services/tasks/task_service.go index 3d878ab..4c2791f 100644 --- a/internal/services/tasks/task_service.go +++ b/internal/services/tasks/task_service.go @@ -49,7 +49,7 @@ func (ts *TaskService) GetTasks() []models.Task { } // GetTasksWithPagination 分页获取任务列表 -func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, agentID *uint, tags string) ([]models.Task, int64) { +func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, agentID *uint, tags string, taskType string) ([]models.Task, int64) { var tasks []models.Task var total int64 @@ -60,6 +60,9 @@ func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, a if tags != "" { query = query.Where("tags LIKE ?", "%"+tags+"%") } + if taskType != "" { + query = query.Where("type = ?", taskType) + } if agentID != nil { query = query.Where("agent_id = ?", *agentID) } diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 5a21ef2..6108dae 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -58,13 +58,14 @@ export const api = { request('/auth/register', { method: 'POST', body: JSON.stringify(data) }) }, tasks: { - list: (params?: { page?: number; page_size?: number; name?: string; agent_id?: number; tags?: string }) => { + list: (params?: { page?: number; page_size?: number; name?: string; agent_id?: number; tags?: string; type?: string }) => { const query = new URLSearchParams() if (params?.page) query.set('page', String(params.page)) if (params?.page_size) query.set('page_size', String(params.page_size)) if (params?.name) query.set('name', params.name) if (params?.tags) query.set('tags', params.tags) if (params?.agent_id) query.set('agent_id', String(params.agent_id)) + if (params?.type) query.set('type', params.type) return request(`/tasks?${query}`) }, create: (data: Partial) => request('/tasks', { method: 'POST', body: JSON.stringify(data) }), @@ -106,7 +107,9 @@ export const api = { return request(`/logs?${query}`) }, get: (id: number) => request(`/logs/${id}`), - detail: (id: number) => request(`/logs/${id}`) + detail: (id: number) => request(`/logs/${id}`), + delete: (id: number) => request(`/logs/${id}`, { method: 'DELETE' }), + clear: (taskId?: number) => request('/logs/clear', { method: 'POST', body: JSON.stringify({ task_id: taskId }) }) }, dashboard: { stats: () => request('/stats'), diff --git a/web/src/views/history/History.vue b/web/src/views/history/History.vue index 75db6ea..e47b8a4 100644 --- a/web/src/views/history/History.vue +++ b/web/src/views/history/History.vue @@ -8,10 +8,20 @@ import Pagination from '@/components/Pagination.vue' import LogViewer from './LogViewer.vue' import { RefreshCw, X, Search, Maximize2, GitBranch, Terminal, - CheckCircle2, XCircle, AlertCircle, Ban, Clock, Zap as ZapIcon, Check + CheckCircle2, XCircle, AlertCircle, Ban, Clock, Zap as ZapIcon, Check, Trash2 } from 'lucide-vue-next' import { api, type TaskLog } from '@/api' import { Badge } from '@/components/ui/badge' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' import { toast } from 'vue-sonner' import { useSiteSettings } from '@/composables/useSiteSettings' import TextOverflow from '@/components/TextOverflow.vue' @@ -32,6 +42,13 @@ let durationTimer: ReturnType | null = null // 全屏查看 const showFullscreen = ref(false) +// 清除所有日志弹窗 +const showClearDialog = ref(false) + +// 删除单条日志弹窗 +const showDeleteDialog = ref(false) +const deleteLogId = ref(null) + const wsContent = ref('') const isWsLoading = ref(false) let logSocket: WebSocket | null = null @@ -203,6 +220,40 @@ function formatDuration(ms: number): string { return `${(ms / 60000).toFixed(1)}分钟` } +async function handleClearLogs() { + try { + await api.logs.clear(filterTaskId.value) + toast.success('相关日志已清空') + showClearDialog.value = false + loadLogs() + } catch (error: any) { + toast.error(error.message || '清空失败') + } +} + +function confirmDeleteLog(id: number) { + deleteLogId.value = id + showDeleteDialog.value = true +} + +async function handleDeleteLog() { + if (!deleteLogId.value) return + try { + await api.logs.delete(deleteLogId.value) + toast.success('该日志已删除') + + // 如果当前选中的是这条日志,关闭详情页 + if (selectedLog.value?.id === deleteLogId.value) { + closeDetail() + } + + showDeleteDialog.value = false + loadLogs() + } catch (err: any) { + toast.error(err.message || '删除失败') + } +} + function getStatusBadgeClass(status: string) { switch (status) { case TASK_STATUS.SUCCESS: @@ -256,9 +307,12 @@ watch(() => route.query.task_id, (newTaskId) => { - + @@ -273,6 +327,7 @@ watch(() => route.query.task_id, (newTaskId) => { 任务名称 状态 耗时 +
route.query.task_id, (newTaskId) => { 状态 耗时 +
@@ -291,7 +347,7 @@ watch(() => route.query.task_id, (newTaskId) => { 暂无日志
@@ -330,6 +386,11 @@ watch(() => route.query.task_id, (newTaskId) => { {{ formatDuration(log.duration) }} + + +
@@ -390,10 +456,18 @@ watch(() => route.query.task_id, (newTaskId) => { class="h-6 px-2 text-[10px]" :disabled="isStopping" @click="stopTask"> {{ isStopping ? '停止中...' : '停止任务' }} + + +
+ +
-
@@ -466,5 +540,41 @@ watch(() => route.query.task_id, (newTaskId) => { + + + + + + 确认清空日志? + + 此操作将永久删除{{ filterTaskId ? '当前任务的' : '所有' }}任务历史记录,包括控制台输出,并且无法撤销。 + + + + 取消 + + 清空 + + + + + + + + + + 确认删除这条日志? + + 此操作将永久删除该次运行记录和日志文件,且不可恢复。 + + + + 取消 + + 删除 + + + +
diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue index 3b085e0..4291d87 100644 --- a/web/src/views/tasks/Tasks.vue +++ b/web/src/views/tasks/Tasks.vue @@ -7,6 +7,7 @@ import Pagination from '@/components/Pagination.vue' import TaskDialog from './TaskDialog.vue' import RepoDialog from './RepoDialog.vue' import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, Wifi, WifiOff, Zap, ZapOff, Copy, Tag } from 'lucide-vue-next' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { api, type Agent, type Task } from '@/api' import { toast } from 'vue-sonner' import { useSiteSettings } from '@/composables/useSiteSettings' @@ -29,6 +30,7 @@ const deleteTaskId = ref(null) const filterName = ref('') const filterTags = ref('') +const filterType = ref('all') const filterAgentId = ref(null) const currentPage = ref(1) const total = ref(0) @@ -69,6 +71,7 @@ async function loadTasks() { page_size: pageSize.value, name: filterName.value || undefined, tags: filterTags.value || undefined, + type: filterType.value === 'all' ? undefined : filterType.value, agent_id: filterAgentId.value || undefined }) tasks.value = res.data @@ -90,6 +93,11 @@ function handleSearch() { }, 300) } +function handleTypeChange() { + currentPage.value = 1 + loadTasks() +} + function handlePageChange(page: number) { currentPage.value = page loadTasks() @@ -228,6 +236,18 @@ watch(() => route.query.agent_id, (newVal) => {
+
+ +