feat: add delete log
This commit is contained in:
@@ -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<typeof setInterval> | null = null
|
||||
// 全屏查看
|
||||
const showFullscreen = ref(false)
|
||||
|
||||
// 清除所有日志弹窗
|
||||
const showClearDialog = ref(false)
|
||||
|
||||
// 删除单条日志弹窗
|
||||
const showDeleteDialog = ref(false)
|
||||
const deleteLogId = ref<number | null>(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) => {
|
||||
<Input v-model="filterKeyword" placeholder="搜索任务..." class="h-9 pl-9 w-full sm:w-56 text-sm"
|
||||
@input="handleSearch" />
|
||||
</div>
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLogs">
|
||||
<Button variant="outline" size="icon" class="h-9 w-9 shrink-0" @click="loadLogs" title="刷新">
|
||||
<RefreshCw class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" class="h-9 px-4 shrink-0 text-sm text-destructive hover:bg-destructive/10 hover:text-destructive border-destructive/20" @click="showClearDialog = true">
|
||||
<Trash2 class="h-4 w-4 sm:mr-2" /> <span class="hidden sm:inline" style="padding-left: 2px;">清空日志</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -273,6 +327,7 @@ watch(() => route.query.task_id, (newTaskId) => {
|
||||
<span class="flex-1 min-w-0">任务名称</span>
|
||||
<span class="w-8 shrink-0 text-center">状态</span>
|
||||
<span class="w-12 text-right shrink-0">耗时</span>
|
||||
<span class="w-8 text-center shrink-0"></span>
|
||||
</div>
|
||||
<!-- 大屏表头 -->
|
||||
<div
|
||||
@@ -284,6 +339,7 @@ watch(() => route.query.task_id, (newTaskId) => {
|
||||
<span class="w-12 shrink-0 text-center">状态</span>
|
||||
<span class="w-16 text-right shrink-0">耗时</span>
|
||||
<span v-if="!selectedLog" class="w-40 text-right shrink-0 hidden md:block">执行时间</span>
|
||||
<span class="w-10 shrink-0 text-center"></span>
|
||||
</div>
|
||||
<!-- 列表 -->
|
||||
<div class="divide-y flex-1">
|
||||
@@ -291,7 +347,7 @@ watch(() => route.query.task_id, (newTaskId) => {
|
||||
暂无日志
|
||||
</div>
|
||||
<div v-for="log in logs" :key="log.id" :class="[
|
||||
'cursor-pointer hover:bg-muted/30 transition-colors',
|
||||
'cursor-pointer hover:bg-muted/30 transition-colors group',
|
||||
selectedLog?.id === log.id && 'bg-accent/50'
|
||||
]" @click="selectLog(log)">
|
||||
<!-- 小屏行 -->
|
||||
@@ -330,6 +386,11 @@ watch(() => route.query.task_id, (newTaskId) => {
|
||||
</span>
|
||||
<span class="w-12 text-right shrink-0 text-muted-foreground text-xs">{{ formatDuration(log.duration)
|
||||
}}</span>
|
||||
<span class="w-8 shrink-0 flex justify-center opacity-100">
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground hover:text-destructive shrink-0" @click.stop="confirmDeleteLog(log.id)" title="删除该日志">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 大屏行 -->
|
||||
<div class="hidden sm:flex items-center gap-4 px-4 py-2">
|
||||
@@ -373,6 +434,11 @@ watch(() => route.query.task_id, (newTaskId) => {
|
||||
<span v-if="!selectedLog"
|
||||
class="w-40 text-right shrink-0 text-muted-foreground text-xs hidden md:block">{{ log.start_time ||
|
||||
log.created_at }}</span>
|
||||
<span class="w-10 shrink-0 flex justify-center opacity-100">
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-muted-foreground hover:text-destructive shrink-0" @click.stop="confirmDeleteLog(log.id)" title="删除该日志">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -390,10 +456,18 @@ watch(() => route.query.task_id, (newTaskId) => {
|
||||
class="h-6 px-2 text-[10px]" :disabled="isStopping" @click="stopTask">
|
||||
{{ isStopping ? '停止中...' : '停止任务' }}
|
||||
</Button>
|
||||
<Button v-else variant="outline" size="sm" class="h-7 px-3 text-xs text-destructive hover:text-destructive hover:bg-destructive/10 border-destructive/30" @click="confirmDeleteLog(selectedLog.id)">
|
||||
<Trash2 class="h-3.5 w-3.5 mr-1" /> 删除日志
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 text-muted-foreground hover:text-destructive" title="删除该日志" @click="confirmDeleteLog(selectedLog.id)">
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="closeDetail" title="关闭">
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7" @click="closeDetail">
|
||||
<X class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-b space-y-2 text-sm">
|
||||
<div class="flex justify-between items-center h-6">
|
||||
@@ -466,5 +540,41 @@ watch(() => route.query.task_id, (newTaskId) => {
|
||||
<!-- 全屏查看日志 -->
|
||||
<LogViewer v-model:open="showFullscreen" :title="`日志输出 - ${selectedLog?.task_name || ''}`"
|
||||
:content="decompressedOutput" :status="selectedLog?.status" />
|
||||
|
||||
<!-- 清空日志确认弹窗 -->
|
||||
<AlertDialog :open="showClearDialog" @update:open="showClearDialog = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认清空日志?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
此操作将永久删除{{ filterTaskId ? '当前任务的' : '所有' }}任务历史记录,包括控制台输出,并且无法撤销。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleClearLogs" class="bg-red-500 text-white hover:bg-red-600 dark:bg-red-600 dark:text-white dark:hover:bg-red-700">
|
||||
清空
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<!-- 单条删除确认弹窗 -->
|
||||
<AlertDialog :open="showDeleteDialog" @update:open="showDeleteDialog = $event">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>确认删除这条日志?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
此操作将永久删除该次运行记录和日志文件,且不可恢复。
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction @click="handleDeleteLog" class="bg-red-500 text-white hover:bg-red-600 dark:bg-red-600 dark:text-white dark:hover:bg-red-700">
|
||||
删除
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<number | null>(null)
|
||||
|
||||
const filterName = ref('')
|
||||
const filterTags = ref('')
|
||||
const filterType = ref('all')
|
||||
const filterAgentId = ref<number | null>(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) => {
|
||||
<Input v-model="filterTags" placeholder="搜索标签..." class="h-9 pl-9 w-full sm:w-32 text-sm"
|
||||
@input="handleSearch" />
|
||||
</div>
|
||||
<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>
|
||||
<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" />
|
||||
|
||||
Reference in New Issue
Block a user