Files
zsglpt/admin-frontend/src/pages/LogsPage.vue
T
19174616018 49897081b6 更新日志页面和统计页面
- 更新 LogsPage 和 StatsPage 组件
- 添加 taskSource 工具模块
- 更新 db/tasks.py
- 重新构建前端静态资源
2025-12-15 16:25:37 +08:00

292 lines
8.7 KiB
Vue

<script setup>
import { computed, onMounted, ref } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { fetchAllUsers } from '../api/users'
import { clearOldTaskLogs, fetchTaskLogs } from '../api/tasks'
import { getTaskSourceMeta } from '../utils/taskSource'
const pageSize = 20
const loading = ref(false)
const logs = ref([])
const total = ref(0)
const currentPage = ref(1)
const usersLoading = ref(false)
const userOptions = ref([])
const dateFilter = ref('')
const statusFilter = ref('')
const sourceFilter = ref('')
const userIdFilter = ref('')
const accountFilter = ref('')
const totalPages = computed(() => Math.max(1, Math.ceil((total.value || 0) / pageSize)))
function formatDuration(seconds) {
if (seconds === null || seconds === undefined) return '-'
const n = Number(seconds)
if (!Number.isFinite(n)) return '-'
if (n < 60) return `${n}秒`
return `${Math.floor(n / 60)}${n % 60}秒`
}
function sourceMeta(source) {
const meta = getTaskSourceMeta(source)
return { key: meta.group, label: meta.label, type: meta.type, tooltip: meta.tooltip }
}
function statusMeta(status) {
if (status === 'success') return { label: '成功', type: 'success' }
if (status === 'failed') return { label: '失败', type: 'danger' }
return { label: status || '-', type: 'info' }
}
async function loadUsers() {
usersLoading.value = true
try {
const users = await fetchAllUsers()
userOptions.value = (users || []).map((u) => ({ id: u.id, username: u.username }))
} catch {
userOptions.value = []
} finally {
usersLoading.value = false
}
}
async function load() {
loading.value = true
try {
const offset = (currentPage.value - 1) * pageSize
const params = {
limit: pageSize,
offset,
}
if (dateFilter.value) params.date = dateFilter.value
if (statusFilter.value) params.status = statusFilter.value
if (sourceFilter.value) params.source = sourceFilter.value
if (userIdFilter.value) params.user_id = userIdFilter.value
if (accountFilter.value) params.account = accountFilter.value
const data = await fetchTaskLogs(params)
logs.value = data?.logs || []
total.value = data?.total || 0
} catch {
logs.value = []
total.value = 0
} finally {
loading.value = false
}
}
function onFilter() {
currentPage.value = 1
load()
}
function onReset() {
dateFilter.value = ''
statusFilter.value = ''
sourceFilter.value = ''
userIdFilter.value = ''
accountFilter.value = ''
currentPage.value = 1
load()
}
async function onClearOld() {
let days
try {
const res = await ElMessageBox.prompt('请输入要清理多少天前的日志(默认30天)', '清理旧日志', {
inputValue: '30',
confirmButtonText: '下一步',
cancelButtonText: '取消',
inputValidator: (v) => {
const n = parseInt(String(v), 10)
return Number.isFinite(n) && n >= 1
},
inputErrorMessage: '请输入有效的天数(大于0的整数)',
})
days = parseInt(String(res.value), 10)
} catch {
return
}
try {
await ElMessageBox.confirm(`确定要删除 ${days} 天前的所有日志吗?此操作不可恢复!`, '二次确认', {
confirmButtonText: '删除',
cancelButtonText: '取消',
type: 'warning',
})
} catch {
return
}
try {
const res = await clearOldTaskLogs(days)
ElMessage.success(res?.message || '清理成功')
currentPage.value = 1
await load()
} catch {
// handled by interceptor
}
}
onMounted(async () => {
await loadUsers()
await load()
})
</script>
<template>
<div class="page-stack">
<div class="app-page-title">
<h2>任务日志</h2>
<div class="toolbar">
<el-button @click="load">刷新</el-button>
</div>
</div>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<div class="filters">
<el-date-picker
v-model="dateFilter"
type="date"
value-format="YYYY-MM-DD"
placeholder="日期"
style="width: 150px"
/>
<el-select v-model="statusFilter" placeholder="状态" style="width: 120px">
<el-option label="全部" value="" />
<el-option label="成功" value="success" />
<el-option label="失败" value="failed" />
</el-select>
<el-select v-model="sourceFilter" placeholder="来源" style="width: 120px">
<el-option label="全部" value="" />
<el-option label="手动" value="manual" />
<el-option label="定时任务(系统)" value="scheduled" />
<el-option label="定时任务(用户)" value="user_scheduled" />
<el-option label="手动(批量)" value="batch" />
<el-option label="手动(截图)" value="manual_screenshot" />
<el-option label="手动(立即)" value="immediate" />
<el-option label="手动(恢复)" value="resumed" />
</el-select>
<el-select
v-model="userIdFilter"
placeholder="用户"
style="width: 140px"
:loading="usersLoading"
filterable
clearable
>
<el-option label="全部" value="" />
<el-option v-for="u in userOptions" :key="u.id" :label="u.username" :value="String(u.id)" />
</el-select>
<el-input v-model="accountFilter" placeholder="账号关键字" style="width: 170px" clearable />
<el-button type="primary" @click="onFilter">筛选</el-button>
<el-button @click="onReset">重置</el-button>
<el-button type="danger" plain @click="onClearOld">清理旧日志</el-button>
</div>
</el-card>
<el-card shadow="never" :body-style="{ padding: '16px' }" class="card">
<div class="table-wrap">
<el-table :data="logs" v-loading="loading" style="width: 100%">
<el-table-column prop="created_at" label="时间" width="180" />
<el-table-column label="来源" width="110">
<template #default="{ row }">
<el-tooltip
v-if="sourceMeta(row.source).tooltip"
:content="sourceMeta(row.source).tooltip"
placement="top"
:show-after="300"
>
<el-tag :type="sourceMeta(row.source).type" effect="light">{{ sourceMeta(row.source).label }}</el-tag>
</el-tooltip>
<el-tag v-else :type="sourceMeta(row.source).type" effect="light">{{ sourceMeta(row.source).label }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="user_username" label="用户" width="140" />
<el-table-column prop="username" label="账号" width="160" />
<el-table-column prop="browse_type" label="浏览类型" width="120" />
<el-table-column label="状态" width="90">
<template #default="{ row }">
<el-tag :type="statusMeta(row.status).type" effect="light">{{ statusMeta(row.status).label }}</el-tag>
</template>
</el-table-column>
<el-table-column label="内容/附件" width="110">
<template #default="{ row }">{{ row.total_items }} / {{ row.total_attachments }}</template>
</el-table-column>
<el-table-column label="用时" width="90">
<template #default="{ row }">{{ formatDuration(row.duration) }}</template>
</el-table-column>
<el-table-column label="失败原因" min-width="220">
<template #default="{ row }">
<el-tooltip :content="row.error_message || ''" placement="top" :show-after="300">
<span class="ellipsis">{{ row.error_message || '-' }}</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
<div class="pagination">
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
layout="prev, pager, next, jumper, ->, total"
@current-change="load"
/>
<div class="page-hint app-muted"> {{ currentPage }} / {{ totalPages }} </div>
</div>
</el-card>
</div>
</template>
<style scoped>
.page-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.card {
border-radius: var(--app-radius);
border: 1px solid var(--app-border);
}
.filters {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.table-wrap {
overflow-x: auto;
}
.ellipsis {
display: inline-block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-top: 14px;
flex-wrap: wrap;
}
.page-hint {
font-size: 12px;
}
</style>