feat(admin): compact mobile cards for report center
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user