Files
zsglpt/admin-frontend/src/pages/StatsPage.vue

468 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { fetchDockerStats, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks'
const loading = ref(false)
const server = ref({
cpu_percent: '-',
memory_used: '-',
memory_total: '-',
disk_used: '-',
disk_total: '-',
uptime: '-',
})
const docker = ref({
status: 'Unknown',
memory_usage: 'N/A',
memory_limit: 'N/A',
memory_percent: 'N/A',
uptime: 'N/A',
})
const taskStats = ref({
today: { success_tasks: 0, failed_tasks: 0, total_items: 0, total_attachments: 0 },
total: { success_tasks: 0, failed_tasks: 0, total_items: 0, total_attachments: 0 },
})
const monitor = ref({
running_count: 0,
queuing_count: 0,
max_concurrent: 0,
running: [],
queuing: [],
})
const sourceMap = {
manual: { label: '手动', type: 'success' },
scheduled: { label: '定时', type: 'primary' },
immediate: { label: '即时', type: 'warning' },
resumed: { label: '恢复', type: 'info' },
}
const statusColorMap = {
初始化: '#6b7280',
正在登录: '#f59e0b',
正在浏览: '#10b981',
浏览完成: '#3b82f6',
正在截图: '#06b6d4',
}
function statusColor(text) {
return statusColorMap[text] || '#6b7280'
}
const serverMemoryDisplay = computed(() => `${server.value.memory_used} / ${server.value.memory_total}`)
const serverDiskDisplay = computed(() => `${server.value.disk_used} / ${server.value.disk_total}`)
let stop = false
let timer = null
async function loadOnce() {
loading.value = true
try {
const [serverInfo, dockerInfo, taskStat, running] = await Promise.all([
fetchServerInfo(),
fetchDockerStats(),
fetchTaskStats(),
fetchRunningTasks(),
])
server.value = serverInfo || server.value
docker.value = dockerInfo || docker.value
taskStats.value = taskStat || taskStats.value
monitor.value = running || monitor.value
} catch {
// handled by interceptor
} finally {
loading.value = false
}
}
async function loop() {
if (stop) return
const start = Date.now()
await loadOnce()
if (stop) return
const elapsed = Date.now() - start
// server/info 正常会阻塞约 1s如果异常很快失败避免疯狂重试
timer = window.setTimeout(loop, elapsed < 900 ? 1000 : 0)
}
onMounted(() => {
stop = false
loop()
})
onBeforeUnmount(() => {
stop = true
if (timer) window.clearTimeout(timer)
})
</script>
<template>
<div class="page-stack" v-loading="loading">
<div class="app-page-title">
<h2>统计</h2>
<span class="app-muted">实时更新</span>
</div>
<el-row :gutter="12">
<el-col :xs="12" :sm="8" :md="6">
<el-card shadow="never" class="metric-card" :body-style="{ padding: '14px' }">
<div class="metric-label">CPU</div>
<div class="metric-value">{{ server.cpu_percent }}%</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="6">
<el-card shadow="never" class="metric-card" :body-style="{ padding: '14px' }">
<div class="metric-label">内存</div>
<div class="metric-value">{{ serverMemoryDisplay }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="6">
<el-card shadow="never" class="metric-card" :body-style="{ padding: '14px' }">
<div class="metric-label">磁盘</div>
<div class="metric-value">{{ serverDiskDisplay }}</div>
</el-card>
</el-col>
<el-col :xs="12" :sm="8" :md="6">
<el-card shadow="never" class="metric-card" :body-style="{ padding: '14px' }">
<div class="metric-label">容器内存</div>
<div class="metric-value">{{ docker.memory_limit !== 'N/A' ? `${docker.memory_usage} / ${docker.memory_limit}` : docker.memory_usage }}</div>
<div v-if="docker.memory_percent !== 'N/A'" class="metric-sub app-muted">{{ docker.memory_percent }}</div>
</el-card>
</el-col>
</el-row>
<el-row :gutter="12">
<el-col :xs="24" :md="14">
<el-card shadow="never" class="card" :body-style="{ padding: '16px' }">
<div class="section-head">
<h3 class="section-title">实时监控</h3>
<span class="app-muted">最大并发{{ monitor.max_concurrent }}</span>
</div>
<el-row :gutter="12" class="count-row">
<el-col :span="8">
<el-card shadow="never" class="count-card ok" :body-style="{ padding: '12px' }">
<div class="count-value">{{ monitor.running_count }}</div>
<div class="count-label">运行中</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="count-card warn" :body-style="{ padding: '12px' }">
<div class="count-value">{{ monitor.queuing_count }}</div>
<div class="count-label">排队中</div>
</el-card>
</el-col>
<el-col :span="8">
<el-card shadow="never" class="count-card" :body-style="{ padding: '12px' }">
<div class="count-value">{{ monitor.max_concurrent }}</div>
<div class="count-label">并发上限</div>
</el-card>
</el-col>
</el-row>
<div class="sub-title">运行中任务</div>
<div v-if="monitor.running.length === 0" class="empty app-muted">暂无运行中的任务</div>
<div v-else class="task-list">
<div v-for="t in monitor.running" :key="`r-${t.account_id}`" class="task-item">
<div class="task-left">
<div class="task-line">
<el-tag :type="(sourceMap[t.source] || sourceMap.manual).type" effect="light" size="small">
{{ (sourceMap[t.source] || sourceMap.manual).label }}
</el-tag>
<span class="task-user">{{ t.user_username }}</span>
<span class="app-muted"></span>
<span class="task-account">{{ t.username }}</span>
<el-tag effect="plain" size="small">{{ t.browse_type }}</el-tag>
</div>
<div class="task-line2">
<span class="dot" :style="{ background: statusColor(t.detail_status) }"></span>
<span class="task-status" :style="{ color: statusColor(t.detail_status) }">{{ t.detail_status }}</span>
<span v-if="t.progress_items || t.progress_attachments" class="app-muted"
>内容/附件{{ t.progress_items }} / {{ t.progress_attachments }}</span
>
</div>
</div>
<div class="task-right">{{ t.elapsed_display }}</div>
</div>
</div>
<div class="sub-title">排队中任务</div>
<div v-if="monitor.queuing.length === 0" class="empty app-muted">暂无排队中的任务</div>
<div v-else class="task-list">
<div v-for="t in monitor.queuing" :key="`q-${t.account_id}`" class="task-item queue">
<div class="task-left">
<div class="task-line">
<el-tag :type="(sourceMap[t.source] || sourceMap.manual).type" effect="light" size="small">
{{ (sourceMap[t.source] || sourceMap.manual).label }}
</el-tag>
<span class="task-user">{{ t.user_username }}</span>
<span class="app-muted"></span>
<span class="task-account">{{ t.username }}</span>
<el-tag effect="plain" size="small">{{ t.browse_type }}</el-tag>
</div>
<div class="task-line2">
<span class="dot" style="background: #f59e0b"></span>
<span class="task-status" style="color: #f59e0b">{{ t.detail_status || '等待资源' }}</span>
</div>
</div>
<div class="task-right warn">{{ t.elapsed_display }}</div>
</div>
</div>
</el-card>
</el-col>
<el-col :xs="24" :md="10">
<el-card shadow="never" class="card" :body-style="{ padding: '16px' }">
<div class="section-head">
<h3 class="section-title">任务统计</h3>
<span class="app-muted">运行{{ server.uptime }}</span>
</div>
<div class="stat-grid">
<div class="stat-box ok">
<div class="stat-name">成功任务</div>
<div class="stat-row">
<span class="stat-big">{{ taskStats.today.success_tasks }}</span>
<span class="app-muted">今日</span>
</div>
<div class="stat-row2 app-muted">累计{{ taskStats.total.success_tasks }}</div>
</div>
<div class="stat-box err">
<div class="stat-name">失败任务</div>
<div class="stat-row">
<span class="stat-big">{{ taskStats.today.failed_tasks }}</span>
<span class="app-muted">今日</span>
</div>
<div class="stat-row2 app-muted">累计{{ taskStats.total.failed_tasks }}</div>
</div>
<div class="stat-box info">
<div class="stat-name">浏览内容</div>
<div class="stat-row">
<span class="stat-big">{{ taskStats.today.total_items }}</span>
<span class="app-muted">今日</span>
</div>
<div class="stat-row2 app-muted">累计{{ taskStats.total.total_items }}</div>
</div>
<div class="stat-box info2">
<div class="stat-name">查看附件</div>
<div class="stat-row">
<span class="stat-big">{{ taskStats.today.total_attachments }}</span>
<span class="app-muted">今日</span>
</div>
<div class="stat-row2 app-muted">累计{{ taskStats.total.total_attachments }}</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<style scoped>
.page-stack {
display: flex;
flex-direction: column;
gap: 12px;
}
.metric-card,
.card {
border-radius: var(--app-radius);
border: 1px solid var(--app-border);
}
.metric-label {
font-size: 12px;
color: var(--app-muted);
}
.metric-value {
margin-top: 6px;
font-size: 18px;
font-weight: 800;
}
.metric-sub {
margin-top: 4px;
font-size: 12px;
}
.section-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.section-title {
margin: 0;
font-size: 14px;
font-weight: 800;
}
.count-row {
margin-bottom: 10px;
}
.count-card {
border-radius: 10px;
border: 1px solid var(--app-border);
}
.count-card.ok {
background: rgba(16, 185, 129, 0.08);
}
.count-card.warn {
background: rgba(245, 158, 11, 0.08);
}
.count-value {
font-size: 22px;
font-weight: 900;
line-height: 1.1;
}
.count-label {
margin-top: 4px;
font-size: 12px;
color: var(--app-muted);
}
.sub-title {
margin-top: 14px;
margin-bottom: 8px;
font-size: 13px;
font-weight: 800;
}
.empty {
padding: 10px 0;
}
.task-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.task-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--app-border);
background: #fff;
}
.task-item.queue {
background: rgba(245, 158, 11, 0.06);
}
.task-line {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.task-line2 {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-top: 6px;
font-size: 12px;
}
.task-user {
font-weight: 600;
}
.task-account {
font-weight: 700;
color: #2563eb;
}
.dot {
width: 8px;
height: 8px;
border-radius: 999px;
display: inline-block;
}
.task-status {
font-weight: 700;
}
.task-right {
font-size: 12px;
font-weight: 700;
color: #10b981;
white-space: nowrap;
}
.task-right.warn {
color: #f59e0b;
}
.stat-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.stat-box {
border-radius: 12px;
border: 1px solid var(--app-border);
padding: 12px;
}
.stat-box.ok {
background: rgba(16, 185, 129, 0.08);
}
.stat-box.err {
background: rgba(239, 68, 68, 0.08);
}
.stat-box.info {
background: rgba(59, 130, 246, 0.08);
}
.stat-box.info2 {
background: rgba(6, 182, 212, 0.08);
}
.stat-name {
font-size: 12px;
font-weight: 800;
margin-bottom: 6px;
}
.stat-row {
display: flex;
align-items: baseline;
gap: 8px;
}
.stat-big {
font-size: 20px;
font-weight: 900;
}
.stat-row2 {
margin-top: 6px;
font-size: 12px;
}
</style>