feat: add delete log

This commit is contained in:
engigu
2026-02-27 09:02:00 +08:00
parent 789a538557
commit d4f1defdeb
7 changed files with 187 additions and 10 deletions
+116 -6
View File
@@ -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>
+20
View File
@@ -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" />