优化报表页面,移除统计页面
This commit is contained in:
@@ -5,7 +5,6 @@ import { ElMessageBox } from 'element-plus'
|
||||
import {
|
||||
Bell,
|
||||
ChatLineSquare,
|
||||
DataAnalysis,
|
||||
Document,
|
||||
List,
|
||||
Message,
|
||||
@@ -106,7 +105,6 @@ const menuItems = [
|
||||
{ path: '/reports', label: '报表', icon: Document },
|
||||
{ path: '/users', label: '用户', icon: User, badgeKey: 'resets' },
|
||||
{ path: '/feedbacks', label: '反馈', icon: ChatLineSquare, badgeKey: 'feedbacks' },
|
||||
{ path: '/stats', label: '统计', icon: DataAnalysis },
|
||||
{ path: '/logs', label: '任务日志', icon: List },
|
||||
{ path: '/announcements', label: '公告', icon: Bell },
|
||||
{ path: '/email', label: '邮件', icon: Message },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,480 +0,0 @@
|
||||
<script setup>
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import { fetchDockerStats, fetchRunningTasks, fetchServerInfo, fetchTaskStats } from '../api/tasks'
|
||||
import { getTaskSourceMeta } from '../utils/taskSource'
|
||||
|
||||
const initialLoading = ref(true)
|
||||
const lastUpdatedAt = ref('')
|
||||
|
||||
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 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
|
||||
|
||||
function recordUpdatedAt() {
|
||||
try {
|
||||
lastUpdatedAt.value = new Date().toLocaleTimeString('zh-CN', { hour12: false, timeZone: 'Asia/Shanghai' })
|
||||
} catch {
|
||||
lastUpdatedAt.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOnce() {
|
||||
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
|
||||
recordUpdatedAt()
|
||||
} catch {
|
||||
// handled by interceptor
|
||||
} finally {
|
||||
initialLoading.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="initialLoading">
|
||||
<div class="app-page-title">
|
||||
<h2>统计</h2>
|
||||
<span class="app-muted">{{ lastUpdatedAt ? `最后更新:${lastUpdatedAt}` : '实时更新' }}</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="getTaskSourceMeta(t.source).type" effect="light" size="small">
|
||||
{{ getTaskSourceMeta(t.source).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="getTaskSourceMeta(t.source).type" effect="light" size="small">
|
||||
{{ getTaskSourceMeta(t.source).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;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.task-item {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-right {
|
||||
align-self: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
@@ -5,7 +5,6 @@ import AdminLayout from '../layouts/AdminLayout.vue'
|
||||
const ReportPage = () => import('../pages/ReportPage.vue')
|
||||
const UsersPage = () => import('../pages/UsersPage.vue')
|
||||
const FeedbacksPage = () => import('../pages/FeedbacksPage.vue')
|
||||
const StatsPage = () => import('../pages/StatsPage.vue')
|
||||
const LogsPage = () => import('../pages/LogsPage.vue')
|
||||
const AnnouncementsPage = () => import('../pages/AnnouncementsPage.vue')
|
||||
const EmailPage = () => import('../pages/EmailPage.vue')
|
||||
@@ -19,10 +18,10 @@ const routes = [
|
||||
children: [
|
||||
{ path: '', redirect: '/reports' },
|
||||
{ path: '/pending', redirect: '/reports' },
|
||||
{ path: '/stats', redirect: '/reports' },
|
||||
{ path: '/reports', name: 'reports', component: ReportPage },
|
||||
{ path: '/users', name: 'users', component: UsersPage },
|
||||
{ path: '/feedbacks', name: 'feedbacks', component: FeedbacksPage },
|
||||
{ path: '/stats', name: 'stats', component: StatsPage },
|
||||
{ path: '/logs', name: 'logs', component: LogsPage },
|
||||
{ path: '/announcements', name: 'announcements', component: AnnouncementsPage },
|
||||
{ path: '/email', name: 'email', component: EmailPage },
|
||||
|
||||
Reference in New Issue
Block a user