diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go index e5ba995..008909a 100644 --- a/internal/controllers/task_controller.go +++ b/internal/controllers/task_controller.go @@ -456,3 +456,20 @@ func (tc *TaskController) StopTask(c *gin.Context) { utils.SuccessMsg(c, "停止请求已发送") } + +// GetTags 获取所有任务标签 +// @Summary 获取所有任务标签 +// @Description 获取系统中所有任务已使用的唯一标签列表 +// @Tags 任务管理 +// @Produce json +// @Security BearerAuth +// @Success 200 {object} utils.Response{data=[]string} +// @Router /tasks/tags [get] +func (tc *TaskController) GetTags(c *gin.Context) { + tags, err := tc.taskService.GetAllTags() + if err != nil { + utils.ServerError(c, err.Error()) + return + } + utils.Success(c, tags) +} diff --git a/internal/router/api_routes.go b/internal/router/api_routes.go index 58aebef..23f7f10 100644 --- a/internal/router/api_routes.go +++ b/internal/router/api_routes.go @@ -76,6 +76,7 @@ func registerTaskRoutes(g *gin.RouterGroup, c *Controllers) { tasks.POST("/batch-delete", c.Task.BatchDeleteTasks) tasks.DELETE("/batch-by-query", c.Task.BatchDeleteByQuery) tasks.POST("/stop/:logID", c.Task.StopTask) + tasks.GET("/tags", c.Task.GetTags) } execution := g.Group("/execute") diff --git a/internal/services/tasks/task_service.go b/internal/services/tasks/task_service.go index e0a9421..1fe32c5 100644 --- a/internal/services/tasks/task_service.go +++ b/internal/services/tasks/task_service.go @@ -5,9 +5,11 @@ import ( "github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/models" "github.com/engigu/baihu-panel/internal/utils" + "strings" ) -type TaskService struct{} +type TaskService struct { +} func NewTaskService() *TaskService { return &TaskService{} @@ -56,6 +58,8 @@ func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, w task.NextRun = nil } database.DB.Select("*").Create(task) + + return task } @@ -74,10 +78,25 @@ func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, a if name != "" { query = query.Where("name LIKE ? OR remark LIKE ?", "%"+name+"%", "%"+name+"%") } + + // 标签筛选 (并集) if tags != "" { - query = query.Where("tags LIKE ?", "%"+tags+"%") + tagList := strings.Split(tags, ",") + var orConditions []string + var orValues []interface{} + for _, tag := range tagList { + tag = strings.TrimSpace(tag) + if tag != "" { + orConditions = append(orConditions, "tags LIKE ?") + orValues = append(orValues, "%"+tag+"%") + } + } + if len(orConditions) > 0 { + query = query.Where(strings.Join(orConditions, " OR "), orValues...) + } } - if taskType != "" { + + if taskType != "" && taskType != "all" { query = query.Where("type = ?", taskType) } if agentID != nil { @@ -136,6 +155,7 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim "RetryCount", "RetryInterval", "RandomRange", "Type", "TriggerType", "Config", "SourceID", ).Updates(&task) + return &task } @@ -154,3 +174,26 @@ func (ts *TaskService) BatchDeleteTasks(ids []string) int64 { result := database.DB.Where("id IN ?", ids).Delete(&models.Task{}) return result.RowsAffected } + +// GetAllTags 获取所有任务标签 +func (ts *TaskService) GetAllTags() ([]string, error) { + var tasks []models.Task + database.DB.Select("tags").Where("tags != ?", "").Find(&tasks) + + tagMap := make(map[string]bool) + for _, task := range tasks { + tags := strings.Split(task.Tags, ",") + for _, tag := range tags { + tag = strings.TrimSpace(tag) + if tag != "" { + tagMap[tag] = true + } + } + } + + result := make([]string, 0, len(tagMap)) + for tag := range tagMap { + result = append(result, tag) + } + return result, nil +} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 2d1a04b..7bea2f9 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -81,7 +81,8 @@ export const api = { return request<{ count: number }>(`/tasks/batch-by-query?${query.toString()}`, { method: 'DELETE' }) }, execute: (id: string) => request(`/execute/task/${id}`, { method: 'POST' }), - stop: (logID: string) => request(`/tasks/stop/${logID}`, { method: 'POST' }) + stop: (logID: string) => request(`/tasks/stop/${logID}`, { method: 'POST' }), + tags: () => request('/tasks/tags') }, scripts: { list: () => request('/scripts'), diff --git a/web/src/components/TagInput.vue b/web/src/components/TagInput.vue new file mode 100644 index 0000000..bc1ea43 --- /dev/null +++ b/web/src/components/TagInput.vue @@ -0,0 +1,157 @@ + + + diff --git a/web/src/views/tasks/TaskDialog.vue b/web/src/views/tasks/TaskDialog.vue index e39492f..e72effd 100644 --- a/web/src/views/tasks/TaskDialog.vue +++ b/web/src/views/tasks/TaskDialog.vue @@ -11,6 +11,7 @@ import { ScrollArea } from '@/components/ui/scroll-area' import DirTreeSelect from '@/components/DirTreeSelect.vue' import { Plus, ChevronDown, X, Search, Check, ChevronsUpDown, AlertCircle, Terminal, Zap, Loader2, Lock, Variable } from 'lucide-vue-next' import { Badge } from '@/components/ui/badge' +import TagInput from '@/components/TagInput.vue' import { cn } from '@/lib/utils' import { api, type Task, type EnvVar, type Agent, type MiseLanguage } from '@/api' import { PATHS, TRIGGER_TYPE } from '@/constants' @@ -77,8 +78,8 @@ function onAllEnvsChange(val: boolean) { allEnvsEnabled.value = val } -function addTag() { - const val = tagInput.value.trim() +function addTag(passedTag?: string) { + const val = (passedTag || tagInput.value).trim() if (!val) return const currentTags = form.value.tags ? form.value.tags.split(',').filter(Boolean) : [] if (!currentTags.includes(val)) { @@ -475,17 +476,18 @@ async function save() {
-
-
- - -
-
-
- +
+ {{ tag }} +
+ +
diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue index 17ee594..9656790 100644 --- a/web/src/views/tasks/Tasks.vue +++ b/web/src/views/tasks/Tasks.vue @@ -7,8 +7,10 @@ import Pagination from '@/components/Pagination.vue' import TaskDialog from './TaskDialog.vue' import RepoDialog from './RepoDialog.vue' import LogViewer from '@/views/history/LogViewer.vue' -import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, RefreshCw, Wifi, WifiOff, Zap, ZapOff, Copy, Tag } from 'lucide-vue-next' +import { Plus, Play, Pencil, Trash2, Search, ScrollText, GitBranch, Terminal, Server, Monitor, X, Loader2, RefreshCw, Wifi, WifiOff, Zap, ZapOff, Copy, Tag, ChevronDown } from 'lucide-vue-next' +import TagInput from '@/components/TagInput.vue' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs' import { api, type Agent, type Task, type TaskLog } from '@/api' import { toast } from 'vue-sonner' @@ -314,6 +316,76 @@ async function viewLogs(taskId: string) { } } +// 视图管理 +const taskViews = ref([]) +const newViewName = ref('') +const isSavingView = ref(false) + +async function loadViewsFromSettings() { + try { + const res = await api.settings.getSection('task_qviews') + const val = res['task_views'] + if (val) { + taskViews.value = JSON.parse(val) + } + } catch (e) { + console.error('Failed to load views', e) + } +} + +async function saveView() { + if (!newViewName.value.trim()) { + toast.error('请输入视图名称') + return + } + + const newView = { + name: newViewName.value.trim(), + query: { + name: filterName.value, + tags: filterTags.value, + agent_id: filterAgentId.value, + type: filterType.value + } + } + + const updatedViews = [...taskViews.value, newView] + isSavingView.value = true + try { + await api.settings.setSection('task_qviews', { + 'task_views': JSON.stringify(updatedViews) + }) + taskViews.value = updatedViews + newViewName.value = '' + toast.success('视图已保存') + } catch (e) { + toast.error('保存失败') + } finally { + isSavingView.value = false + } +} + +function applyView(view: any) { + filterName.value = view.query.name || '' + filterTags.value = view.query.tags || '' + filterAgentId.value = view.query.agent_id || null + filterType.value = view.query.type || TASK_TYPE.NORMAL + handleSearch() +} + +async function deleteView(index: number) { + const updatedViews = taskViews.value.filter((_, i) => i !== index) + try { + await api.settings.setSection('task_qviews', { + 'task_views': JSON.stringify(updatedViews) + }) + taskViews.value = updatedViews + toast.success('视图已删除') + } catch (e) { + toast.error('删除失败') + } +} + function getTaskTypeTitle(type: string) { return type === TASK_TYPE.REPO ? '仓库同步' : '普通任务' } @@ -329,6 +401,7 @@ onMounted(async () => { } loadTasks() + loadViewsFromSettings() }) // 监听路由参数变化 @@ -343,23 +416,65 @@ watch(() => route.query.agent_id, (newVal: any) => {
-

定时任务

-

管理和调度自动化执行任务

+ + +
+

{{ filterType === TASK_TYPE.REPO ? '仓库同步' : '定时任务' }}

+
+ 视图 + +
+
+
+ +
+
+
+

我的视图

+
+
+ 暂无保存的视图 +
+
+
+ {{ view.name }} + +
+
+
+ +
+

保存当前过滤为新视图

+
+ + +
+
+
+
+
+

管理和调度自动化执行任务

-
-
+
+
-
- - -
+