feat: add qlrepo sync crontab

This commit is contained in:
engigu
2026-03-19 11:06:45 +08:00
parent a03800f196
commit cd818f7b0c
23 changed files with 4126 additions and 529 deletions
+111 -27
View File
@@ -6,8 +6,10 @@ import { Input } from '@/components/ui/input'
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, Wifi, WifiOff, Zap, ZapOff, Copy, Tag } from 'lucide-vue-next'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { api, type Agent, type Task } from '@/api'
import { toast } from 'vue-sonner'
import { useSiteSettings } from '@/composables/useSiteSettings'
@@ -30,7 +32,7 @@ const deleteTaskId = ref<string | null>(null)
const filterName = ref('')
const filterTags = ref('')
const filterType = ref('all')
const filterType = ref<string>(TASK_TYPE.NORMAL)
const filterAgentId = ref<string | null>(null)
const currentPage = ref(1)
const total = ref(0)
@@ -147,18 +149,41 @@ function duplicateTask(task: Task) {
}
}
const showBatchDeleteDialog = ref(false)
function confirmDelete(id: string) {
deleteTaskId.value = id
showDeleteDialog.value = true
}
function confirmBatchDelete() {
if (total.value === 0) return
showBatchDeleteDialog.value = true
}
async function batchDeleteTasks() {
try {
const res = await api.tasks.batchDeleteByQuery({
name: filterName.value || undefined,
tags: filterTags.value || undefined,
type: filterType.value === 'all' ? undefined : filterType.value,
agent_id: filterAgentId.value || undefined
})
toast.success(`成功删除 ${res.count} 个任务`)
loadTasks()
} catch {
toast.error('批量删除失败')
}
showBatchDeleteDialog.value = false
}
async function deleteTask() {
if (!deleteTaskId.value) return
try {
await api.tasks.delete(deleteTaskId.value)
toast.success('任务已删除')
loadTasks()
} catch { toast.error('删除失败') }
} catch { toast.error('删除失败') }
showDeleteDialog.value = false
deleteTaskId.value = null
}
@@ -189,8 +214,27 @@ async function toggleTask(task: Task, enabled: boolean) {
} catch { toast.error('操作失败') }
}
function viewLogs(taskId: string) {
router.push({ path: '/history', query: { task_id: taskId } })
const showLogViewer = ref(false)
const selectedLogId = ref<string | undefined>()
const latestLogStatus = ref('')
const latestLogTitle = ref('')
async function viewLogs(taskId: string) {
try {
const res = await api.logs.list({ task_id: taskId, page: 1, page_size: 1 })
if (res.data && res.data.length > 0) {
const latestLog = res.data[0]
if (!latestLog) return
latestLogTitle.value = latestLog.task_name || ''
latestLogStatus.value = latestLog.status || ''
selectedLogId.value = latestLog.id
showLogViewer.value = true
} else {
toast.info('该任务暂无执行日志')
}
} catch {
toast.error('获取日志失败')
}
}
function getTaskTypeTitle(type: string) {
@@ -221,12 +265,12 @@ watch(() => route.query.agent_id, (newVal) => {
<template>
<div class="space-y-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<div class="flex items-center gap-3">
<h2 class="text-xl sm:text-2xl font-bold tracking-tight">定时任务</h2>
<p class="text-muted-foreground text-sm">管理和调度自动化任务</p>
</div>
<div class="flex flex-col sm:flex-row gap-2.5 w-full md:w-auto">
<!-- 第1行: 搜索框 -->
<!-- 搜索与标签 -->
<div class="flex items-center gap-2 w-full sm:w-auto">
<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" />
@@ -239,34 +283,48 @@ watch(() => route.query.agent_id, (newVal) => {
@input="handleSearch" />
</div>
</div>
<!-- 第2行: 下拉框与按钮 -->
<div class="flex items-center gap-2 w-full sm:w-auto mt-1 sm:mt-0">
<div class="relative flex-1 sm:flex-none">
<Select v-model="filterType" @update:model-value="handleTypeChange">
<SelectTrigger class="h-9 w-full sm:w-28 text-sm">
<SelectValue placeholder="所有类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">所有类型</SelectItem>
<SelectItem :value="TASK_TYPE.NORMAL">定时任务</SelectItem>
<SelectItem :value="TASK_TYPE.REPO">仓库同步</SelectItem>
</SelectContent>
</Select>
<div class="flex items-center gap-3 w-full sm:w-auto">
<!-- 移动端类型切换 -->
<div class="md:hidden flex-1 shrink-0">
<Select v-model="filterType" @update:model-value="handleTypeChange">
<SelectTrigger class="h-9 w-full text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem :value="TASK_TYPE.NORMAL">定时任务</SelectItem>
<SelectItem :value="TASK_TYPE.REPO">仓库同步</SelectItem>
</SelectContent>
</Select>
</div>
<div v-if="filterAgentId"
class="hidden sm:flex items-center gap-1 px-2 py-1 bg-primary/10 text-primary rounded-md text-sm shrink-0">
<Server class="h-3.5 w-3.5" />
<span>{{ filterAgentName }}</span>
<X class="h-3.5 w-3.5 cursor-pointer hover:text-destructive" @click="clearAgentFilter" />
</div>
<div class="flex items-center gap-2 shrink-0 justify-end">
<Button variant="outline" @click="openCreateRepo" class="shrink-0 px-3 h-9">
<GitBranch class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">仓库同步</span>
<!-- 动态新增按钮 -->
<Button variant="outline" class="shrink-0 px-3 h-9 shadow-sm text-destructive border-destructive/20 hover:bg-destructive/10" @click="confirmBatchDelete">
<Trash2 class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">批量删除</span>
</Button>
<Button @click="openCreate" class="shrink-0 px-3 h-9">
<Button v-if="filterType === TASK_TYPE.NORMAL" @click="openCreate" class="shrink-0 px-3 h-9 shadow-sm">
<Plus class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">新建任务</span>
</Button>
<Button v-else-if="filterType === TASK_TYPE.REPO" @click="openCreateRepo" class="shrink-0 px-3 h-9 shadow-sm">
<GitBranch class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline">同步仓库</span>
</Button>
</div>
<!-- 桌面端类型切换移到后面 -->
<Tabs :model-value="filterType" @update:model-value="v => { filterType = String(v); handleTypeChange() }" class="shrink-0 hidden md:block">
<TabsList class="h-9 p-1 bg-muted/30 border">
<TabsTrigger :value="TASK_TYPE.NORMAL" class="px-4 h-7 text-[13px]">定时任务</TabsTrigger>
<TabsTrigger :value="TASK_TYPE.REPO" class="px-4 h-7 text-[13px]">仓库同步</TabsTrigger>
</TabsList>
</Tabs>
</div>
<!-- 移动端 agent 过滤标签 -->
<div v-if="filterAgentId"
@@ -282,7 +340,9 @@ watch(() => route.query.agent_id, (newVal) => {
<!-- 表头 -->
<div
class="flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-2 sm:gap-4 px-3 sm:px-4 py-2 sm:py-1.5 border-b bg-muted/20 text-xs sm:text-sm text-muted-foreground font-medium min-w-0 sm:min-w-[1000px]">
<span class="w-10 sm:w-12 shrink-0 max-sm:order-1">序号</span>
<div class="w-10 sm:w-12 shrink-0 flex items-center gap-2 max-sm:order-1 pl-1">
<span class="text-xs sm:text-sm">序号</span>
</div>
<span class="w-8 shrink-0 text-center max-sm:order-2">类型</span>
<span class="flex-1 min-w-0 sm:flex-none sm:w-40 md:w-48 lg:w-56 shrink-0 max-sm:order-3">名称</span>
<span class="w-24 sm:w-32 shrink-0 hidden md:block">执行位置</span>
@@ -304,8 +364,10 @@ watch(() => route.query.agent_id, (newVal) => {
</div>
<div v-for="(task, index) in tasks" :key="task.id"
class="flex flex-wrap sm:flex-nowrap items-center gap-x-2 gap-y-2 sm:gap-4 px-3 sm:px-4 py-2.5 sm:py-1.5 hover:bg-muted/30 transition-colors">
<span class="w-10 sm:w-12 shrink-0 text-muted-foreground text-xs sm:text-sm max-sm:order-1">#{{ total -
(currentPage - 1) * pageSize - index }}</span>
<div class="w-10 sm:w-12 shrink-0 flex items-center gap-2 max-sm:order-1 pl-1">
<span class="text-muted-foreground text-xs sm:text-sm">#{{ total -
(currentPage - 1) * pageSize - index }}</span>
</div>
<span class="w-8 shrink-0 flex justify-center max-sm:order-2" :title="getTaskTypeTitle(task.type || 'task')">
<GitBranch v-if="task.type === TASK_TYPE.REPO" class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
<Terminal v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4 text-primary" />
@@ -397,7 +459,29 @@ watch(() => route.query.agent_id, (newVal) => {
<!-- 仓库同步弹窗 -->
<RepoDialog v-model:open="showRepoDialog" :task="editingTask" :is-edit="isEdit" @saved="loadTasks" />
<!-- 删除确认 -->
<!-- 最新日志全屏查看 -->
<LogViewer v-model:open="showLogViewer" :task-name="latestLogTitle"
:log-id="selectedLogId" :initial-status="latestLogStatus" />
<!-- 删除确认 (批量) -->
<AlertDialog v-model:open="showBatchDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>确认批量删除</AlertDialogTitle>
<AlertDialogDescription>
将会删除当前所有过滤条件下匹配的 <b>{{ total }}</b> 个任务。操作不可撤销。
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>取消</AlertDialogCancel>
<AlertDialogAction class="bg-destructive text-white hover:bg-destructive/90" @click="batchDeleteTasks">
确认删除
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<!-- 删除确认 (单个) -->
<AlertDialog v-model:open="showDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>