feat(admin): compact mobile cards for report center

This commit is contained in:
2026-02-07 09:54:11 +08:00
parent 12e07962c7
commit 69e3e4c45c
26 changed files with 324 additions and 53 deletions

View File

@@ -63,6 +63,10 @@ function parsePercent(value) {
return n
}
function percentText(value) {
return `${Math.round(parsePercent(value))}%`
}
function sourceLabel(source) {
const raw = String(source ?? '').trim()
if (!raw) return '手动'
@@ -227,6 +231,119 @@ const runningCountsLabel = computed(() => {
return `运行中 ${runningCount} / 排队 ${queuingCount} / 并发上限 ${maxGlobal || maxConcurrentGlobal.value || '-'}`
})
const taskModuleItems = computed(() => [
{ label: '今日总任务', value: normalizeCount(taskToday.value.total_tasks) },
{ label: '今日成功', value: normalizeCount(taskToday.value.success_tasks) },
{ label: '今日失败', value: normalizeCount(taskToday.value.failed_tasks) },
{ label: '今日成功率', value: `${taskTodaySuccessRate.value}%` },
{ label: '累计任务', value: normalizeCount(taskTotal.value.total_tasks) },
{ label: '累计成功', value: normalizeCount(taskTotal.value.success_tasks) },
])
const queueModuleItems = computed(() => [
{ label: '运行中', value: runningCount.value },
{ label: '排队中', value: queuingCount.value },
{ label: '并发上限', value: normalizeCount(runningTasks.value?.max_concurrent) || maxConcurrentGlobal.value || '-' },
{ label: '排队首条来源', value: sourceLabel(queuingTaskList.value[0]?.source) },
{ label: '排队首条状态', value: queuingTaskList.value[0]?.detail_status || queuingTaskList.value[0]?.status || '-' },
{ label: '最长等待', value: queuingTaskList.value[0]?.elapsed_display || '-' },
])
const emailModuleItems = computed(() => [
{ label: '总发送', value: normalizeCount(emailStats.value?.total_sent) },
{ label: '成功', value: normalizeCount(emailStats.value?.total_success) },
{ label: '失败', value: normalizeCount(emailStats.value?.total_failed) },
{ label: '成功率', value: `${emailSuccessRate.value}%` },
{ label: '注册验证', value: normalizeCount(emailStats.value?.register_sent) },
{ label: '重置密码', value: normalizeCount(emailStats.value?.reset_sent) },
])
const feedbackModuleItems = computed(() => [
{ label: '总反馈', value: normalizeCount(feedbackStats.value?.total) },
{ label: '待处理', value: normalizeCount(feedbackStats.value?.pending) },
{ label: '已回复', value: normalizeCount(feedbackStats.value?.replied) },
])
const resourceModuleItems = computed(() => [
{ label: 'CPU', value: percentText(serverInfo.value?.cpu_percent) },
{ label: '内存', value: percentText(serverInfo.value?.memory_percent) },
{ label: '磁盘', value: percentText(serverInfo.value?.disk_percent) },
{ label: '容器状态', value: dockerStats.value?.status || '-' },
{ label: '容器名', value: dockerStats.value?.container_name || '-' },
{ label: '容器运行', value: dockerStats.value?.uptime || '-' },
])
const workerModuleItems = computed(() => [
{ label: '总 Worker', value: browserPoolTotalWorkers.value },
{ label: '活跃 Worker', value: browserPoolActiveWorkers.value },
{ label: '忙碌 Worker', value: browserPoolBusyWorkers.value },
{ label: '空闲 Worker', value: browserPoolIdleWorkers.value },
{ label: '任务队列', value: browserPoolQueueSize.value },
])
const configModuleItems = computed(() => [
{ label: '定时任务', value: scheduleEnabled.value ? '启用' : '关闭' },
{ label: '执行时间', value: scheduleTime.value || '-' },
{ label: '浏览类型', value: scheduleBrowseType.value || '-' },
{ label: '代理', value: proxyEnabled.value ? '启用' : '关闭' },
{ label: '代理有效期', value: proxyExpireMinutes.value ? `${proxyExpireMinutes.value} 分钟` : '-' },
{ label: '全局并发', value: maxConcurrentGlobal.value || '-' },
{ label: '单账号并发', value: maxConcurrentPerAccount.value || '-' },
{ label: '截图并发', value: maxScreenshotConcurrent.value || '-' },
])
const mobileModules = computed(() => [
{
key: 'task',
title: '任务概览',
desc: normalizeCount(taskToday.value.total_tasks) > 0 ? `今日成功率 ${taskTodaySuccessRate.value}%` : '今日暂无任务',
tone: 'purple',
items: taskModuleItems.value,
},
{
key: 'queue',
title: '队列监控',
desc: runningCountsLabel.value,
tone: 'blue',
items: queueModuleItems.value,
},
{
key: 'email',
title: '邮件报表',
desc: `成功率 ${emailSuccessRate.value}%`,
tone: 'cyan',
items: emailModuleItems.value,
},
{
key: 'feedback',
title: '反馈概览',
desc: `待处理 ${normalizeCount(feedbackStats.value?.pending)}`,
tone: 'orange',
items: feedbackModuleItems.value,
},
{
key: 'resource',
title: '系统资源',
desc: serverInfo.value?.uptime ? `运行 ${serverInfo.value.uptime}` : '运行状态获取中',
tone: 'green',
items: resourceModuleItems.value,
},
{
key: 'worker',
title: '截图线程池',
desc: `活跃 ${browserPoolActiveWorkers.value} · 忙碌 ${browserPoolBusyWorkers.value}`,
tone: 'cyan',
items: workerModuleItems.value,
},
{
key: 'config',
title: '配置概览',
desc: '并发 / 代理 / 定时任务',
tone: 'red',
items: configModuleItems.value,
},
])
async function refreshAll(options = {}) {
const showLoading = options.showLoading ?? true
if (refreshing.value) return
@@ -307,6 +424,29 @@ onUnmounted(() => {
<MetricGrid :items="overviewCards" :loading="loading" :min-width="165" />
</section>
<section class="mobile-report">
<el-card
v-for="module in mobileModules"
:key="module.key"
shadow="never"
class="mobile-module-card"
:class="`mobile-tone-${module.tone}`"
:body-style="{ padding: '12px' }"
>
<div class="mobile-module-head">
<div class="mobile-module-title">{{ module.title }}</div>
<div class="mobile-module-desc app-muted">{{ module.desc }}</div>
</div>
<div class="mobile-metrics">
<div v-for="item in module.items" :key="`${module.key}-${item.label}`" class="mobile-metric-item">
<div class="mobile-metric-label app-muted">{{ item.label }}</div>
<div class="mobile-metric-value">{{ item.value }}</div>
</div>
</div>
</el-card>
</section>
<div class="desktop-report">
<el-row :gutter="12">
<el-col :xs="24" :lg="12">
<el-card shadow="never" class="panel" :body-style="{ padding: '16px' }">
@@ -635,6 +775,7 @@ onUnmounted(() => {
</el-card>
</el-col>
</el-row>
</div>
</div>
</template>
@@ -695,6 +836,102 @@ onUnmounted(() => {
flex-wrap: wrap;
}
.mobile-report {
display: none;
}
.mobile-module-card {
position: relative;
overflow: hidden;
border-radius: 14px;
border: 1px solid rgba(17, 24, 39, 0.12);
background: rgba(255, 255, 255, 0.9);
box-shadow: var(--app-shadow-soft);
}
.mobile-module-card::before {
content: '';
position: absolute;
left: 0;
top: 0;
right: 0;
height: 3px;
background: var(--mobile-accent, #3b82f6);
}
.mobile-tone-blue {
--mobile-accent: linear-gradient(90deg, #3b82f6, #06b6d4);
}
.mobile-tone-cyan {
--mobile-accent: linear-gradient(90deg, #06b6d4, #3b82f6);
}
.mobile-tone-purple {
--mobile-accent: linear-gradient(90deg, #8b5cf6, #ec4899);
}
.mobile-tone-orange {
--mobile-accent: linear-gradient(90deg, #f59e0b, #f97316);
}
.mobile-tone-green {
--mobile-accent: linear-gradient(90deg, #10b981, #22c55e);
}
.mobile-tone-red {
--mobile-accent: linear-gradient(90deg, #ef4444, #f43f5e);
}
.mobile-module-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
}
.mobile-module-title {
font-size: 13px;
font-weight: 900;
color: #0f172a;
}
.mobile-module-desc {
min-width: 0;
max-width: 68%;
font-size: 11px;
line-height: 1.4;
text-align: right;
}
.mobile-metrics {
margin-top: 10px;
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.mobile-metric-item {
padding: 8px 10px;
border-radius: 10px;
border: 1px solid rgba(17, 24, 39, 0.08);
background: rgba(248, 250, 252, 0.9);
}
.mobile-metric-label {
font-size: 11px;
line-height: 1.35;
}
.mobile-metric-value {
margin-top: 4px;
font-size: 14px;
font-weight: 800;
color: #0f172a;
line-height: 1.3;
word-break: break-word;
}
.panel {
border-radius: 18px;
border: 1px solid rgba(17, 24, 39, 0.1);
@@ -895,8 +1132,42 @@ onUnmounted(() => {
}
@media (max-width: 768px) {
.desktop-report {
display: none;
}
.mobile-report {
display: grid;
gap: 10px;
}
.report-hero {
border-radius: 14px;
padding: 12px;
}
.hero-main h2 {
font-size: 17px;
}
.hero-meta {
margin-top: 4px;
gap: 6px;
font-size: 11px;
}
.resource-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 420px) {
.mobile-metrics {
grid-template-columns: 1fr;
}
.mobile-module-desc {
max-width: 62%;
}
}
</style>