feat: add task pin #89
This commit is contained in:
@@ -133,6 +133,10 @@ const (
|
|||||||
TaskTypeNormal = "task"
|
TaskTypeNormal = "task"
|
||||||
TaskTypeRepo = "repo"
|
TaskTypeRepo = "repo"
|
||||||
|
|
||||||
|
// 任务置顶类型
|
||||||
|
PinTypeNone = "none"
|
||||||
|
PinTypeTop = "top"
|
||||||
|
|
||||||
// 触发类型
|
// 触发类型
|
||||||
TriggerTypeCron = "cron"
|
TriggerTypeCron = "cron"
|
||||||
TriggerTypeBaihuStartup = "baihu_startup"
|
TriggerTypeBaihuStartup = "baihu_startup"
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
|||||||
RetryCount int `json:"retry_count"`
|
RetryCount int `json:"retry_count"`
|
||||||
RetryInterval int `json:"retry_interval"`
|
RetryInterval int `json:"retry_interval"`
|
||||||
RandomRange int `json:"random_range"`
|
RandomRange int `json:"random_range"`
|
||||||
|
PinType string `json:"pin_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -116,12 +117,12 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
|
|||||||
if sourceID != "" {
|
if sourceID != "" {
|
||||||
task = tc.taskService.GetTaskBySourceID(sourceID)
|
task = tc.taskService.GetTaskBySourceID(sourceID)
|
||||||
if task != nil {
|
if task != nil {
|
||||||
task = tc.taskService.UpdateTask(task.ID, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, true, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID)
|
task = tc.taskService.UpdateTask(task.ID, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, true, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if task == nil {
|
if task == nil {
|
||||||
task = tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID)
|
task = tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果是 Agent 任务,通知 Agent;否则添加到本地 cron
|
// 如果是 Agent 任务,通知 Agent;否则添加到本地 cron
|
||||||
@@ -237,6 +238,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
|||||||
RetryCount int `json:"retry_count"`
|
RetryCount int `json:"retry_count"`
|
||||||
RetryInterval int `json:"retry_interval"`
|
RetryInterval int `json:"retry_interval"`
|
||||||
RandomRange int `json:"random_range"`
|
RandomRange int `json:"random_range"`
|
||||||
|
PinType string `json:"pin_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
@@ -270,7 +272,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
|
|||||||
sourceID = oldTask.SourceID
|
sourceID = oldTask.SourceID
|
||||||
}
|
}
|
||||||
|
|
||||||
task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID)
|
task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, workDir, req.CleanConfig, req.Envs, req.Enabled, req.Type, req.Config, req.AgentID, req.Languages, req.TriggerType, req.Tags, req.RetryCount, req.RetryInterval, req.RandomRange, sourceID, req.PinType)
|
||||||
if task == nil {
|
if task == nil {
|
||||||
utils.NotFound(c, "任务不存在")
|
utils.NotFound(c, "任务不存在")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ type Task struct {
|
|||||||
ID string `json:"id" gorm:"primaryKey;size:20"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Name string `json:"name" gorm:"size:255;not null"`
|
Name string `json:"name" gorm:"size:255;not null"`
|
||||||
Remark string `json:"remark" gorm:"size:255;default:''"`
|
Remark string `json:"remark" gorm:"size:255;default:''"`
|
||||||
|
PinType string `json:"pin_type" gorm:"size:20;default:none;index"` // 置顶类型: constant.PinTypeNone, constant.PinTypeTop
|
||||||
Command BigText `json:"command"` // 普通任务的命令
|
Command BigText `json:"command"` // 普通任务的命令
|
||||||
Tags string `json:"tags" gorm:"size:255;default:''"` // 标签,逗号分隔
|
Tags string `json:"tags" gorm:"size:255;default:''"` // 标签,逗号分隔
|
||||||
Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: constant.TaskTypeNormal, constant.TaskTypeRepo
|
Type string `json:"type" gorm:"size:20;default:'task'"` // 任务类型: constant.TaskTypeNormal, constant.TaskTypeRepo
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type TaskVO struct {
|
|||||||
RetryCount int `json:"retry_count"`
|
RetryCount int `json:"retry_count"`
|
||||||
RetryInterval int `json:"retry_interval"`
|
RetryInterval int `json:"retry_interval"`
|
||||||
RandomRange int `json:"random_range"`
|
RandomRange int `json:"random_range"`
|
||||||
|
PinType string `json:"pin_type"`
|
||||||
LastRun *models.LocalTime `json:"last_run"`
|
LastRun *models.LocalTime `json:"last_run"`
|
||||||
NextRun *models.LocalTime `json:"next_run"`
|
NextRun *models.LocalTime `json:"next_run"`
|
||||||
CreatedAt models.LocalTime `json:"created_at"`
|
CreatedAt models.LocalTime `json:"created_at"`
|
||||||
@@ -58,6 +59,7 @@ func ToTaskVO(task *models.Task) *TaskVO {
|
|||||||
RetryCount: task.RetryCount,
|
RetryCount: task.RetryCount,
|
||||||
RetryInterval: task.RetryInterval,
|
RetryInterval: task.RetryInterval,
|
||||||
RandomRange: task.RandomRange,
|
RandomRange: task.RandomRange,
|
||||||
|
PinType: task.PinType,
|
||||||
LastRun: task.LastRun,
|
LastRun: task.LastRun,
|
||||||
NextRun: task.NextRun,
|
NextRun: task.NextRun,
|
||||||
CreatedAt: task.CreatedAt,
|
CreatedAt: task.CreatedAt,
|
||||||
|
|||||||
@@ -24,17 +24,21 @@ func (ts *TaskService) GetTaskBySourceID(sourceID string) *models.Task {
|
|||||||
return &task
|
return &task
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string) *models.Task {
|
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string, pinType string) *models.Task {
|
||||||
if taskType == "" {
|
if taskType == "" {
|
||||||
taskType = "task"
|
taskType = "task"
|
||||||
}
|
}
|
||||||
if triggerType == "" {
|
if triggerType == "" {
|
||||||
triggerType = constant.TriggerTypeCron
|
triggerType = constant.TriggerTypeCron
|
||||||
}
|
}
|
||||||
|
if pinType == "" {
|
||||||
|
pinType = constant.PinTypeNone
|
||||||
|
}
|
||||||
task := &models.Task{
|
task := &models.Task{
|
||||||
ID: utils.GenerateID(),
|
ID: utils.GenerateID(),
|
||||||
Name: name,
|
Name: name,
|
||||||
Command: models.BigText(command),
|
Command: models.BigText(command),
|
||||||
|
PinType: pinType,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
Type: taskType,
|
Type: taskType,
|
||||||
TriggerType: triggerType,
|
TriggerType: triggerType,
|
||||||
@@ -104,7 +108,7 @@ func (ts *TaskService) GetTasksWithPagination(page, pageSize int, name string, a
|
|||||||
}
|
}
|
||||||
|
|
||||||
query.Count(&total)
|
query.Count(&total)
|
||||||
query.Order("id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&tasks)
|
query.Order("pin_type DESC, updated_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&tasks)
|
||||||
|
|
||||||
return tasks, total
|
return tasks, total
|
||||||
}
|
}
|
||||||
@@ -118,7 +122,7 @@ func (ts *TaskService) GetTaskByID(id string) *models.Task {
|
|||||||
return &task
|
return &task
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ts *TaskService) UpdateTask(id string, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string) *models.Task {
|
func (ts *TaskService) UpdateTask(id string, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *string, languages models.TaskLanguages, triggerType string, tags string, retryCount int, retryInterval int, randomRange int, sourceID string, pinType string) *models.Task {
|
||||||
var task models.Task
|
var task models.Task
|
||||||
res := database.DB.Where("id = ?", id).Limit(1).Find(&task)
|
res := database.DB.Where("id = ?", id).Limit(1).Find(&task)
|
||||||
if res.Error != nil || res.RowsAffected == 0 {
|
if res.Error != nil || res.RowsAffected == 0 {
|
||||||
@@ -126,6 +130,7 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim
|
|||||||
}
|
}
|
||||||
task.Name = name
|
task.Name = name
|
||||||
task.Command = models.BigText(command)
|
task.Command = models.BigText(command)
|
||||||
|
task.PinType = pinType
|
||||||
task.Tags = tags
|
task.Tags = tags
|
||||||
task.Schedule = schedule
|
task.Schedule = schedule
|
||||||
task.Timeout = timeout
|
task.Timeout = timeout
|
||||||
@@ -153,7 +158,7 @@ func (ts *TaskService) UpdateTask(id string, name, command, schedule string, tim
|
|||||||
"Name", "Command", "Tags", "Schedule", "Timeout", "WorkDir",
|
"Name", "Command", "Tags", "Schedule", "Timeout", "WorkDir",
|
||||||
"CleanConfig", "Envs", "Enabled", "AgentID", "Languages",
|
"CleanConfig", "Envs", "Enabled", "AgentID", "Languages",
|
||||||
"RetryCount", "RetryInterval", "RandomRange", "Type",
|
"RetryCount", "RetryInterval", "RandomRange", "Type",
|
||||||
"TriggerType", "Config", "SourceID",
|
"TriggerType", "Config", "SourceID", "PinType",
|
||||||
).Updates(&task)
|
).Updates(&task)
|
||||||
|
|
||||||
return &task
|
return &task
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ func CheckWSOrigin(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 兜底策略:如果是开发环境常见的 localhost/127.0.0.1,且端口不一致的情况,
|
// 3. 允许来自 localhost 和 127.0.0.1 的请求 (方便本地开发和同机部署)
|
||||||
// 如果用户没有配置允许列表,我们在非 Release 模式下可以考虑放行,
|
if strings.HasPrefix(u.Host, "localhost") || strings.HasPrefix(u.Host, "127.0.0.1") {
|
||||||
// 但为了安全,默认应严格限制。建议开发时通过 BH_ALLOWED_ORIGINS=localhost:5173 显式开启。
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ export interface Task {
|
|||||||
retry_count: number
|
retry_count: number
|
||||||
retry_interval: number
|
retry_interval: number
|
||||||
random_range: number
|
random_range: number
|
||||||
|
pin_type: 'none' | 'top'
|
||||||
languages: { name: string; version: string }[]
|
languages: { name: string; version: string }[]
|
||||||
agent_id: string | null
|
agent_id: string | null
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ watch(() => props.open, async (val: boolean) => {
|
|||||||
retry_interval: props.task?.retry_interval ?? 0,
|
retry_interval: props.task?.retry_interval ?? 0,
|
||||||
random_range: props.task?.random_range ?? 0,
|
random_range: props.task?.random_range ?? 0,
|
||||||
timeout: props.task?.timeout ?? 30,
|
timeout: props.task?.timeout ?? 30,
|
||||||
|
pin_type: props.task?.pin_type ?? 'none',
|
||||||
...props.task
|
...props.task
|
||||||
}
|
}
|
||||||
// 解析清理配置
|
// 解析清理配置
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ watch(() => props.open, async (val: boolean) => {
|
|||||||
retry_interval: props.task?.retry_interval ?? 0,
|
retry_interval: props.task?.retry_interval ?? 0,
|
||||||
random_range: props.task?.random_range ?? 0,
|
random_range: props.task?.random_range ?? 0,
|
||||||
timeout: props.task?.timeout ?? 30,
|
timeout: props.task?.timeout ?? 30,
|
||||||
|
pin_type: props.task?.pin_type ?? 'none',
|
||||||
...props.task
|
...props.task
|
||||||
}
|
}
|
||||||
// 解析清理配置
|
// 解析清理配置
|
||||||
|
|||||||
+158
-55
@@ -7,11 +7,18 @@ 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 LogViewer from '@/views/history/LogViewer.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, ChevronDown } 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, Pin, PinOff, MoreHorizontal } from 'lucide-vue-next'
|
||||||
import TagInput from '@/components/TagInput.vue'
|
import TagInput from '@/components/TagInput.vue'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { api, type Agent, type Task, type TaskLog } from '@/api'
|
import { api, type Agent, type Task, type TaskLog } from '@/api'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useSiteSettings } from '@/composables/useSiteSettings'
|
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||||
@@ -223,6 +230,15 @@ async function toggleTask(task: Task, enabled: boolean) {
|
|||||||
} catch { toast.error('操作失败') }
|
} catch { toast.error('操作失败') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function togglePin(task: Task) {
|
||||||
|
const newType = task.pin_type === 'top' ? 'none' : 'top'
|
||||||
|
try {
|
||||||
|
await api.tasks.update(task.id, { ...task, pin_type: newType })
|
||||||
|
toast.success(newType === 'top' ? '任务已置顶' : '已取消置顶')
|
||||||
|
loadTasks()
|
||||||
|
} catch { toast.error('置顶操作失败') }
|
||||||
|
}
|
||||||
|
|
||||||
const showLogViewer = ref(false)
|
const showLogViewer = ref(false)
|
||||||
const selectedLog = ref<TaskLog | null>(null)
|
const selectedLog = ref<TaskLog | null>(null)
|
||||||
const logContent = ref('')
|
const logContent = ref('')
|
||||||
@@ -414,7 +430,7 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="space-y-6">
|
||||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
|
<div class="flex flex-col xl:flex-row xl:items-center justify-between gap-4">
|
||||||
<div class="flex flex-col shrink-0">
|
<div class="flex flex-col shrink-0">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
@@ -464,22 +480,22 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<p class="text-muted-foreground text-xs mt-0.5 ml-0.5">管理和调度自动化执行任务</p>
|
<p class="text-muted-foreground text-xs mt-0.5 ml-0.5">管理和调度自动化执行任务</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-row items-center flex-wrap gap-2 w-full lg:w-auto lg:ml-auto lg:justify-end">
|
<div class="flex flex-row items-center flex-wrap gap-2 w-full xl:w-auto xl:ml-auto xl:justify-end">
|
||||||
<!-- 搜索与标签 -->
|
<!-- 搜索与标签 -->
|
||||||
<div class="flex flex-row items-center gap-2 w-full sm:flex-1 lg:flex-none lg:w-auto text-sm">
|
<div class="flex flex-row items-center gap-2 w-full sm:flex-1 xl:flex-none xl:w-auto text-sm">
|
||||||
<div class="relative flex-1 lg:flex-none lg:w-[240px] group">
|
<div class="relative flex-1 xl:flex-none xl:w-[240px] group">
|
||||||
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
<Search class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||||
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full bg-muted/20 border-muted-foreground/10 focus:bg-background text-sm"
|
<Input v-model="filterName" placeholder="搜索任务..." class="h-9 pl-9 w-full bg-muted/20 border-muted-foreground/10 focus:bg-background text-sm"
|
||||||
@input="handleSearch" />
|
@input="handleSearch" />
|
||||||
</div>
|
</div>
|
||||||
<TagInput v-model="filterTags" placeholder="搜索标签..." :icon="Tag" multiple
|
<TagInput v-model="filterTags" placeholder="搜索标签..." :icon="Tag" multiple
|
||||||
class="h-9 flex-1 lg:flex-none lg:w-[180px] bg-muted/20 border-muted-foreground/10 focus:bg-background text-sm"
|
class="h-9 flex-1 xl:flex-none xl:w-[180px] bg-muted/20 border-muted-foreground/10 focus:bg-background text-sm"
|
||||||
@enter="handleSearch" @update:modelValue="handleSearch" />
|
@enter="handleSearch" @update:modelValue="handleSearch" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 w-full sm:w-auto sm:justify-end">
|
<div class="flex items-center gap-2 w-full sm:w-auto sm:justify-end">
|
||||||
<!-- 移动端类型切换 -->
|
<!-- 移动端类型切换 -->
|
||||||
<div class="lg:hidden flex-1 shrink-0">
|
<div class="xl:hidden flex-1 shrink-0">
|
||||||
<Select v-model="filterType" @update:model-value="(_v: any) => handleTypeChange()">
|
<Select v-model="filterType" @update:model-value="(_v: any) => handleTypeChange()">
|
||||||
<SelectTrigger class="h-9 w-full text-sm bg-muted/20 border-muted-foreground/10">
|
<SelectTrigger class="h-9 w-full text-sm bg-muted/20 border-muted-foreground/10">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
@@ -503,18 +519,18 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div class="flex items-center gap-2 shrink-0 ml-auto sm:ml-0">
|
<div class="flex items-center gap-2 shrink-0 ml-auto sm:ml-0">
|
||||||
<Button variant="outline" class="shrink-0 px-2 lg:px-3 h-9 shadow-sm text-destructive border-destructive/20 hover:bg-destructive/10" @click="confirmBatchDelete" title="批量删除">
|
<Button variant="outline" class="shrink-0 px-2 xl:px-3 h-9 shadow-sm text-destructive border-destructive/20 hover:bg-destructive/10" @click="confirmBatchDelete" title="批量删除">
|
||||||
<Trash2 class="h-4 w-4 lg:mr-2" /> <span class="hidden lg:inline">批量删除</span>
|
<Trash2 class="h-4 w-4 xl:mr-2" /> <span class="hidden xl:inline">批量删除</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-if="filterType === TASK_TYPE.NORMAL" @click="openCreate" class="shrink-0 px-2 lg:px-3 h-9 shadow-sm font-medium" title="新建任务">
|
<Button v-if="filterType === TASK_TYPE.NORMAL" @click="openCreate" class="shrink-0 px-2 xl:px-3 h-9 shadow-sm font-medium" title="新建任务">
|
||||||
<Plus class="h-4 w-4 lg:mr-2" /> <span class="hidden lg:inline">新建任务</span>
|
<Plus class="h-4 w-4 xl:mr-2" /> <span class="hidden xl:inline">新建任务</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else-if="filterType === TASK_TYPE.REPO" @click="openCreateRepo" class="shrink-0 px-2 lg:px-3 h-9 shadow-sm font-medium" title="同步仓库">
|
<Button v-else-if="filterType === TASK_TYPE.REPO" @click="openCreateRepo" class="shrink-0 px-2 xl:px-3 h-9 shadow-sm font-medium" title="同步仓库">
|
||||||
<GitBranch class="h-4 w-4 lg:mr-2" /> <span class="hidden lg:inline">同步仓库</span>
|
<GitBranch class="h-4 w-4 xl:mr-2" /> <span class="hidden xl:inline">同步仓库</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<!-- 桌面端类型切换 -->
|
<!-- 桌面端类型切换 -->
|
||||||
<Tabs :model-value="filterType" @update:model-value="(v: string | number) => { filterType = String(v); handleTypeChange() }" class="shrink-0 hidden lg:block">
|
<Tabs :model-value="filterType" @update:model-value="(v: string | number) => { filterType = String(v); handleTypeChange() }" class="shrink-0 hidden xl:block">
|
||||||
<TabsList class="h-9 p-1 bg-muted/30 border">
|
<TabsList class="h-9 p-1 bg-muted/30 border">
|
||||||
<TabsTrigger :value="TASK_TYPE.NORMAL" class="px-4 h-7 text-sm">定时任务</TabsTrigger>
|
<TabsTrigger :value="TASK_TYPE.NORMAL" class="px-4 h-7 text-sm">定时任务</TabsTrigger>
|
||||||
<TabsTrigger :value="TASK_TYPE.REPO" class="px-4 h-7 text-sm">仓库同步</TabsTrigger>
|
<TabsTrigger :value="TASK_TYPE.REPO" class="px-4 h-7 text-sm">仓库同步</TabsTrigger>
|
||||||
@@ -524,7 +540,7 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
</div>
|
</div>
|
||||||
<!-- 移动端/平板 agent 过滤标签 -->
|
<!-- 移动端/平板 agent 过滤标签 -->
|
||||||
<div v-if="filterAgentId"
|
<div v-if="filterAgentId"
|
||||||
class="flex lg:hidden items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm w-fit mt-1">
|
class="flex xl:hidden items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm w-fit mt-1">
|
||||||
<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" />
|
||||||
@@ -533,8 +549,8 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-lg border bg-card overflow-hidden">
|
<div class="rounded-lg border bg-card overflow-hidden">
|
||||||
<!-- ========== 1. 大屏布局 (Large >= 1024px) - 用户调好 ========== -->
|
<!-- ========== 1. 大屏布局 (Large >= 1280px) ========== -->
|
||||||
<div class="hidden lg:block">
|
<div class="hidden xl:block">
|
||||||
<!-- 表头 -->
|
<!-- 表头 -->
|
||||||
<div class="flex items-center gap-4 px-4 py-1.5 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
<div class="flex items-center gap-4 px-4 py-1.5 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
||||||
<span class="w-12 shrink-0 pl-1">序号</span>
|
<span class="w-12 shrink-0 pl-1">序号</span>
|
||||||
@@ -542,12 +558,14 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<span class="w-56 shrink-0">名称</span>
|
<span class="w-56 shrink-0">名称</span>
|
||||||
<span class="w-32 shrink-0">执行位置</span>
|
<span class="w-32 shrink-0">执行位置</span>
|
||||||
<span class="flex-1 min-w-0 flex items-center gap-1.5 line-clamp-1">
|
<span class="flex-1 min-w-0 flex items-center gap-1.5 line-clamp-1">
|
||||||
<Terminal class="h-3.5 w-3.5 opacity-50" />命令/地址
|
<GitBranch v-if="filterType === TASK_TYPE.REPO" class="h-3.5 w-3.5 opacity-50" />
|
||||||
|
<Terminal v-else class="h-3.5 w-3.5 opacity-50" />
|
||||||
|
{{ filterType === TASK_TYPE.REPO ? '仓库地址' : '命令内容' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="w-28 shrink-0">定时规则</span>
|
<span class="w-28 shrink-0">{{ filterType === TASK_TYPE.REPO ? '同步周期' : '定时规则' }}</span>
|
||||||
<span class="w-40 shrink-0">执行时间</span>
|
<span class="w-40 shrink-0">执行时间</span>
|
||||||
<span class="w-8 shrink-0 text-center">状态</span>
|
<span class="w-8 shrink-0 text-center">状态</span>
|
||||||
<span class="w-36 shrink-0 text-center">操作</span>
|
<span class="w-24 shrink-0 text-center">操作</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<div class="divide-y text-sm">
|
<div class="divide-y text-sm">
|
||||||
@@ -555,11 +573,17 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
class="flex items-center gap-4 px-4 py-1.5 hover:bg-muted/30 transition-colors">
|
class="flex items-center gap-4 px-4 py-1.5 hover:bg-muted/30 transition-colors">
|
||||||
<div class="w-12 shrink-0 pl-1 text-muted-foreground tabular-nums">#{{ total - (currentPage - 1) * pageSize - index }}</div>
|
<div class="w-12 shrink-0 pl-1 text-muted-foreground tabular-nums">#{{ total - (currentPage - 1) * pageSize - index }}</div>
|
||||||
<span class="w-8 shrink-0 flex justify-center" :title="getTaskTypeTitle(task.type || 'task')">
|
<span class="w-8 shrink-0 flex justify-center" :title="getTaskTypeTitle(task.type || 'task')">
|
||||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" />
|
<div class="relative">
|
||||||
<Terminal v-else class="h-4 w-4 text-primary" />
|
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-4 w-4 text-primary" />
|
||||||
|
<Terminal v-else class="h-4 w-4 text-primary" />
|
||||||
|
<div v-if="task.pin_type === 'top'" class="absolute -top-1 -right-1 h-2 w-2 bg-primary rounded-full border-2 border-background" />
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<div class="w-56 shrink-0 flex flex-col justify-center gap-0.5 overflow-hidden">
|
<div class="w-56 shrink-0 flex flex-col justify-center gap-0.5 overflow-hidden">
|
||||||
<span class="font-medium truncate cursor-help" :title="task.name">{{ task.name }}</span>
|
<div class="flex items-center gap-1.5 overflow-hidden">
|
||||||
|
<span class="font-medium truncate cursor-help" :title="task.name">{{ task.name }}</span>
|
||||||
|
<Pin v-if="task.pin_type === 'top'" class="h-3 w-3 text-primary fill-primary shrink-0 rotate-45" />
|
||||||
|
</div>
|
||||||
<div v-if="task.tags" class="flex items-center gap-1 overflow-hidden">
|
<div v-if="task.tags" class="flex items-center gap-1 overflow-hidden">
|
||||||
<span v-for="tag in task.tags.split(',').filter(Boolean).slice(0, 3)" :key="tag"
|
<span v-for="tag in task.tags.split(',').filter(Boolean).slice(0, 3)" :key="tag"
|
||||||
class="truncate text-[10px] leading-none px-1 py-0.5 bg-secondary text-secondary-foreground rounded border">{{ tag }}</span>
|
class="truncate text-[10px] leading-none px-1 py-0.5 bg-secondary text-secondary-foreground rounded border">{{ tag }}</span>
|
||||||
@@ -574,7 +598,7 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<span class="truncate">{{ getExecutorName(task) }}</span>
|
<span class="truncate">{{ getExecutorName(task) }}</span>
|
||||||
</span>
|
</span>
|
||||||
<code class="flex-1 min-w-0 text-muted-foreground truncate text-xs bg-muted/40 px-2 py-1 rounded">
|
<code class="flex-1 min-w-0 text-muted-foreground truncate text-xs bg-muted/40 px-2 py-1 rounded">
|
||||||
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '同步地址' : '执行命令'" class="truncate" />
|
<TextOverflow :text="task.command" :title="task.type === TASK_TYPE.REPO ? '仓库地址' : '执行命令'" class="truncate" />
|
||||||
</code>
|
</code>
|
||||||
<div class="w-28 shrink-0 flex flex-col items-start justify-center gap-1 overflow-hidden">
|
<div class="w-28 shrink-0 flex flex-col items-start justify-center gap-1 overflow-hidden">
|
||||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP" class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-2 py-1 rounded-md">服务启动时</span>
|
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP" class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-2 py-1 rounded-md">服务启动时</span>
|
||||||
@@ -592,40 +616,70 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span class="w-36 shrink-0 flex justify-center gap-1">
|
<span class="w-24 shrink-0 flex justify-center">
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
||||||
<Loader2 v-if="executingTaskId === task.id" class="h-3.5 w-3.5 animate-spin" />
|
<Loader2 v-if="executingTaskId === task.id" class="h-3 w-3 animate-spin" />
|
||||||
<Play v-else class="h-3.5 w-3.5" />
|
<Play v-else class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="viewLogs(task.id)"><ScrollText class="h-3.5 w-3.5" /></Button>
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="viewLogs(task.id)"><ScrollText class="h-3 w-3" /></Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="openEdit(task)"><Pencil class="h-3.5 w-3.5" /></Button>
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openEdit(task)"><Pencil class="h-3 w-3" /></Button>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="duplicateTask(task)"><Copy class="h-3.5 w-3.5" /></Button>
|
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-destructive" @click="confirmDelete(task.id)"><Trash2 class="h-3.5 w-3.5" /></Button>
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="h-6 w-6"><MoreHorizontal class="h-3 w-3" /></Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" class="w-32">
|
||||||
|
<DropdownMenuItem @click="togglePin(task)">
|
||||||
|
<Pin v-if="task.pin_type !== 'top'" class="h-3.5 w-3.5 mr-2" />
|
||||||
|
<PinOff v-else class="h-3.5 w-3.5 mr-2 text-primary" />
|
||||||
|
<span>{{ task.pin_type === 'top' ? '取消置顶' : '置顶任务' }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="duplicateTask(task)">
|
||||||
|
<Copy class="h-3.5 w-3.5 mr-2" />
|
||||||
|
<span>复制任务</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
||||||
|
<Trash2 class="h-3.5 w-3.5 mr-2" />
|
||||||
|
<span>删除任务</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== 2. 中屏布局 (Medium 640px - 1024px) - 新抽取优化 ========== -->
|
<!-- ========== 2. 中屏布局 (Medium 640px - 1280px) ========== -->
|
||||||
<div class="hidden sm:block lg:hidden">
|
<div class="hidden sm:block xl:hidden">
|
||||||
<!-- 表头 -->
|
<!-- 表头 -->
|
||||||
<div class="flex items-center gap-4 px-4 py-1.5 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
<div class="flex items-center gap-4 px-4 py-1.5 border-b bg-muted/20 text-xs text-muted-foreground font-medium">
|
||||||
|
<span class="w-12 shrink-0 pl-1">序号</span>
|
||||||
<span class="w-48 shrink-0">任务信息</span>
|
<span class="w-48 shrink-0">任务信息</span>
|
||||||
<span class="flex-1 min-w-0">命令内容</span>
|
<span class="flex-1 min-w-0">
|
||||||
|
{{ filterType === TASK_TYPE.REPO ? '仓库地址' : '命令内容' }}
|
||||||
|
</span>
|
||||||
<span class="w-8 shrink-0 text-center">状态</span>
|
<span class="w-8 shrink-0 text-center">状态</span>
|
||||||
<span class="w-24 shrink-0 text-center">操作</span>
|
<span class="w-24 shrink-0 text-center">操作</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- 列表 -->
|
<!-- 列表 -->
|
||||||
<div class="divide-y text-sm">
|
<div class="divide-y text-sm">
|
||||||
<div v-for="task in tasks" :key="`medium-${task.id}`"
|
<div v-for="(task, index) in tasks" :key="`medium-${task.id}`"
|
||||||
class="flex items-center gap-4 px-4 py-2.5 hover:bg-muted/30 transition-colors">
|
class="flex items-center gap-4 px-4 py-2.5 hover:bg-muted/30 transition-colors">
|
||||||
|
<div class="w-12 shrink-0 pl-1 text-muted-foreground tabular-nums text-xs">#{{ total - (currentPage - 1) * pageSize - index }}</div>
|
||||||
<div class="w-48 shrink-0 flex items-center gap-2 overflow-hidden">
|
<div class="w-48 shrink-0 flex items-center gap-2 overflow-hidden">
|
||||||
<span class="shrink-0" :title="getTaskTypeTitle(task.type || 'task')">
|
<span class="shrink-0" :title="getTaskTypeTitle(task.type || 'task')">
|
||||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
|
<div class="relative">
|
||||||
<Terminal v-else class="h-3.5 w-3.5 text-primary" />
|
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
|
||||||
|
<Terminal v-else class="h-3.5 w-3.5 text-primary" />
|
||||||
|
<div v-if="task.pin_type === 'top'" class="absolute -top-1 -right-1 h-1.5 w-1.5 bg-primary rounded-full border border-background" />
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="flex flex-col min-w-0">
|
||||||
<span class="font-medium truncate">{{ task.name }}</span>
|
<div class="flex items-center gap-1.5 overflow-hidden">
|
||||||
|
<span class="font-medium truncate">{{ task.name }}</span>
|
||||||
|
<Pin v-if="task.pin_type === 'top'" class="h-3 w-3 text-primary fill-primary shrink-0 rotate-45" />
|
||||||
|
</div>
|
||||||
<span v-if="task.schedule" class="text-[10px] text-muted-foreground font-mono truncate">{{ task.schedule }}</span>
|
<span v-if="task.schedule" class="text-[10px] text-muted-foreground font-mono truncate">{{ task.schedule }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -640,19 +694,41 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
<ZapOff class="h-3.5 w-3.5 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<div class="w-24 shrink-0 flex justify-center gap-0.5">
|
<div class="w-24 shrink-0 flex justify-center">
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
||||||
<Loader2 v-if="executingTaskId === task.id" class="h-3.5 w-3.5 animate-spin" />
|
<Loader2 v-if="executingTaskId === task.id" class="h-3 w-3 animate-spin" />
|
||||||
<Play v-else class="h-3.5 w-3.5" />
|
<Play v-else class="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="viewLogs(task.id)"><ScrollText class="h-3.5 w-3.5" /></Button>
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="viewLogs(task.id)"><ScrollText class="h-3 w-3" /></Button>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="openEdit(task)"><Pencil class="h-3.5 w-3.5" /></Button>
|
<Button variant="ghost" size="icon" class="h-6 w-6" @click="openEdit(task)"><Pencil class="h-3 w-3" /></Button>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button variant="ghost" size="icon" class="h-6 w-6"><MoreHorizontal class="h-3 w-3" /></Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" class="w-32">
|
||||||
|
<DropdownMenuItem @click="togglePin(task)">
|
||||||
|
<Pin v-if="task.pin_type !== 'top'" class="h-3.5 w-3.5 mr-2" />
|
||||||
|
<PinOff v-else class="h-3.5 w-3.5 mr-2 text-primary" />
|
||||||
|
<span>{{ task.pin_type === 'top' ? '取消置顶' : '置顶任务' }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="duplicateTask(task)">
|
||||||
|
<Copy class="h-3.5 w-3.5 mr-2" />
|
||||||
|
<span>复制任务</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
||||||
|
<Trash2 class="h-3.5 w-3.5 mr-2" />
|
||||||
|
<span>删除任务</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========== 3. 小屏布局 (Small < 640px) - 用户调好 ========== -->
|
<!-- ========== 3. 小屏布局 (Small < 640px) ========== -->
|
||||||
<div class="divide-y sm:hidden">
|
<div class="divide-y sm:hidden">
|
||||||
<div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8">暂无任务</div>
|
<div v-if="tasks.length === 0" class="text-sm text-muted-foreground text-center py-8">暂无任务</div>
|
||||||
<div v-for="(task, index) in tasks" :key="`small-${task.id}`" class="p-3 hover:bg-muted/50 transition-colors">
|
<div v-for="(task, index) in tasks" :key="`small-${task.id}`" class="p-3 hover:bg-muted/50 transition-colors">
|
||||||
@@ -660,10 +736,16 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<div class="flex items-center gap-2 flex-1 min-w-0 pr-2">
|
<div class="flex items-center gap-2 flex-1 min-w-0 pr-2">
|
||||||
<span class="text-xs text-muted-foreground tabular-nums flex-shrink-0">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
<span class="text-xs text-muted-foreground tabular-nums flex-shrink-0">#{{ total - (currentPage - 1) * pageSize - index }}</span>
|
||||||
<span class="shrink-0">
|
<span class="shrink-0">
|
||||||
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
|
<div class="relative">
|
||||||
<Terminal v-else class="h-3.5 w-3.5 text-primary" />
|
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 text-primary" />
|
||||||
|
<Terminal v-else class="h-3.5 w-3.5 text-primary" />
|
||||||
|
<div v-if="task.pin_type === 'top'" class="absolute -top-1 -right-1 h-1.5 w-1.5 bg-primary rounded-full border border-background" />
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
<span class="font-bold text-sm truncate">{{ task.name }}</span>
|
<div class="flex items-center gap-1.5 min-w-0 flex-1">
|
||||||
|
<span class="font-bold text-sm truncate">{{ task.name }}</span>
|
||||||
|
<Pin v-if="task.pin_type === 'top'" class="h-3 w-3 text-primary fill-primary shrink-0 rotate-45" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span @click="toggleTask(task, !task.enabled)" class="cursor-pointer">
|
<span @click="toggleTask(task, !task.enabled)" class="cursor-pointer">
|
||||||
<div v-if="task.enabled" class="h-6 w-6 rounded-md 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-if="task.enabled" class="h-6 w-6 rounded-md bg-green-500/10 flex items-center justify-center"><Zap class="h-3.5 w-3.5 text-green-500 fill-green-500" /></div>
|
||||||
@@ -672,7 +754,7 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-1.5 text-xs text-muted-foreground mb-3 px-1">
|
<div class="space-y-1.5 text-xs text-muted-foreground mb-3 px-1">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span class="w-10 shrink-0 font-medium opacity-70">定时:</span>
|
<span class="w-10 shrink-0 font-medium opacity-70">{{ task.type === TASK_TYPE.REPO ? '周期:' : '定时:' }}</span>
|
||||||
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP" class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded font-medium">服务启动时</span>
|
<span v-if="task.trigger_type === TRIGGER_TYPE.BAIHU_STARTUP" class="text-[10px] leading-tight bg-blue-500/10 text-blue-500 px-1.5 py-0.5 rounded font-medium">服务启动时</span>
|
||||||
<div v-else-if="task.schedule" class="flex items-center gap-1.5 flex-1 min-w-0">
|
<div v-else-if="task.schedule" class="flex items-center gap-1.5 flex-1 min-w-0">
|
||||||
<span class="text-xs text-foreground bg-muted/40 px-1.5 py-0.5 rounded shrink-0">{{ task.schedule }}</span>
|
<span class="text-xs text-foreground bg-muted/40 px-1.5 py-0.5 rounded shrink-0">{{ task.schedule }}</span>
|
||||||
@@ -683,7 +765,7 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<span class="w-10 shrink-0 font-medium mt-0.5 opacity-70">命令:</span>
|
<span class="w-10 shrink-0 font-medium mt-0.5 opacity-70">{{ task.type === TASK_TYPE.REPO ? '地址:' : '命令:' }}</span>
|
||||||
<div class="flex-1 min-w-0 overflow-hidden text-foreground"><TextOverflow :text="task.command" class="truncate opacity-80" /></div>
|
<div class="flex-1 min-w-0 overflow-hidden text-foreground"><TextOverflow :text="task.command" class="truncate opacity-80" /></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="task.remark" class="flex items-start gap-3">
|
<div v-if="task.remark" class="flex items-start gap-3">
|
||||||
@@ -694,7 +776,7 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<div class="grid grid-cols-4 items-center pt-2 mt-2 border-t border-border/40 -mx-1">
|
<div class="grid grid-cols-4 items-center pt-2 mt-2 border-t border-border/40 -mx-1">
|
||||||
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none" @click="runTask(task.id)" :disabled="executingTaskId === task.id">
|
||||||
<Loader2 v-if="executingTaskId === task.id" class="h-3.5 w-3.5 animate-spin" />
|
<Loader2 v-if="executingTaskId === task.id" class="h-3.5 w-3.5 animate-spin" />
|
||||||
<Play v-else class="h-3.5 w-3.5" />执行
|
<Play v-else class="h-3.5 w-3.5" />{{ task.type === TASK_TYPE.REPO ? '同步' : '执行' }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none border-l border-border/10" @click="viewLogs(task.id)">
|
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none border-l border-border/10" @click="viewLogs(task.id)">
|
||||||
<ScrollText class="h-3.5 w-3.5" />日志
|
<ScrollText class="h-3.5 w-3.5" />日志
|
||||||
@@ -702,9 +784,30 @@ watch(() => route.query.agent_id, (newVal: any) => {
|
|||||||
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none border-l border-border/10" @click="openEdit(task)">
|
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none border-l border-border/10" @click="openEdit(task)">
|
||||||
<Pencil class="h-3.5 w-3.5" />编辑
|
<Pencil class="h-3.5 w-3.5" />编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-destructive/5 text-destructive rounded-none border-l border-border/10" @click="confirmDelete(task.id)">
|
|
||||||
<Trash2 class="h-3.5 w-3.5" />删除
|
<DropdownMenu>
|
||||||
</Button>
|
<DropdownMenuTrigger as-child>
|
||||||
|
<Button variant="ghost" class="h-9 px-0 text-xs gap-1.5 hover:bg-primary/5 rounded-none border-l border-border/10 w-full">
|
||||||
|
<MoreHorizontal class="h-3.5 w-3.5" />更多
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" class="w-40">
|
||||||
|
<DropdownMenuItem @click="togglePin(task)">
|
||||||
|
<Pin v-if="task.pin_type !== 'top'" class="h-4 w-4 mr-2" />
|
||||||
|
<PinOff v-else class="h-4 w-4 mr-2 text-primary" />
|
||||||
|
<span>{{ task.pin_type === 'top' ? '取消置顶' : '置顶任务' }}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="duplicateTask(task)">
|
||||||
|
<Copy class="h-4 w-4 mr-2" />
|
||||||
|
<span>复制任务</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem class="text-destructive focus:text-destructive" @click="confirmDelete(task.id)">
|
||||||
|
<Trash2 class="h-4 w-4 mr-2" />
|
||||||
|
<span>删除任务</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user