feat(channel-monitor-v2): 渠道监控展示首次聚合进度

在用户端监控页显示 bootstrap 进度横幅与百分比,完成后自动消失;
bootstrap 期间加快轮询,空态文案与进度联动。
This commit is contained in:
IanShaw027
2026-08-07 11:53:25 +08:00
parent d2d259cb7c
commit 59b5ac5458
4 changed files with 93 additions and 5 deletions
@@ -29,11 +29,18 @@
</span>
<span v-else class="text-gray-400">{{ t('common.loading') }}</span>
<span
v-if="snapshot && !snapshot.coverage.coverage_complete"
v-if="snapshot && !snapshot.coverage.coverage_complete && !bootstrapActive"
class="badge badge-warning"
>
{{ t('channelMonitorV2.partialCoverage') }}
</span>
<span
v-if="bootstrapActive"
class="badge badge-primary inline-flex items-center gap-1"
>
<LoadingSpinner size="sm" />
{{ t('channelMonitorV2.bootstrap.progress', { percent: bootstrapPercent }) }}
</span>
</div>
</div>
<button
@@ -47,6 +54,41 @@
</button>
</header>
<!-- First-upgrade silent backfill: show until 30d product window is covered -->
<div
v-if="bootstrapActive"
class="border-b border-blue-100 bg-blue-50/90 px-5 py-3 dark:border-blue-900/40 dark:bg-blue-950/40 sm:px-6"
role="status"
aria-live="polite"
>
<div class="flex flex-wrap items-start justify-between gap-3">
<div class="min-w-0 flex-1">
<p class="text-sm font-semibold text-blue-900 dark:text-blue-100">
{{ t('channelMonitorV2.bootstrap.title') }}
</p>
<p class="mt-0.5 text-xs text-blue-800/80 dark:text-blue-200/80">
{{ t('channelMonitorV2.bootstrap.description') }}
</p>
</div>
<span class="shrink-0 text-xs font-medium tabular-nums text-blue-700 dark:text-blue-300">
{{ t('channelMonitorV2.bootstrap.progress', { percent: bootstrapPercent }) }}
</span>
</div>
<div
class="mt-2.5 h-1.5 overflow-hidden rounded-full bg-blue-200/80 dark:bg-blue-900/60"
role="progressbar"
:aria-valuenow="bootstrapPercent"
aria-valuemin="0"
aria-valuemax="100"
:aria-label="t('channelMonitorV2.bootstrap.working')"
>
<div
class="h-full rounded-full bg-blue-500 transition-[width] duration-500 ease-out dark:bg-blue-400"
:style="{ width: `${bootstrapPercent}%` }"
/>
</div>
</div>
<!-- Single compact toolbar row: range · filters · view controls -->
<div class="monitor-toolbar flex flex-nowrap items-center gap-1.5 overflow-x-auto px-4 py-3 sm:gap-2 sm:px-5">
<div
@@ -394,8 +436,20 @@
<div v-if="tabLoading" class="empty-state py-10 text-sm text-gray-400">{{ t('common.loading') }}</div>
<div v-else-if="activeRowsEmpty" class="empty-state py-10">
<p class="empty-state-title text-base">{{ t('channelMonitorV2.empty.title') }}</p>
<p class="empty-state-description">{{ t('channelMonitorV2.empty.description') }}</p>
<p class="empty-state-title text-base">
{{
bootstrapActive
? t('channelMonitorV2.bootstrap.title')
: t('channelMonitorV2.empty.title')
}}
</p>
<p class="empty-state-description">
{{
bootstrapActive
? t('channelMonitorV2.bootstrap.description')
: t('channelMonitorV2.empty.description')
}}
</p>
</div>
</div>
</section>
@@ -585,6 +639,13 @@ const activeRowsEmpty = computed(() =>
? errorRows.value.length === 0
: userRows.value.length === 0
)
/** First-upgrade backfill toward 90m/24h/7d/30d; banner hides when backend omits bootstrap. */
const bootstrapActive = computed(() => Boolean(snapshot.value?.coverage?.bootstrap?.active))
const bootstrapPercent = computed(() => {
const raw = snapshot.value?.coverage?.bootstrap?.progress_percent
if (typeof raw !== 'number' || Number.isNaN(raw)) return 0
return Math.min(100, Math.max(0, Math.round(raw)))
})
const matrixRows = computed(() => {
const items = matrix.value?.items || []
// platform_group views should only show real groups, never bare platform placeholders.
@@ -740,12 +801,15 @@ function scheduleAutoRefresh() {
window.clearInterval(autoRefreshTimer)
autoRefreshTimer = null
}
const seconds = snapshot.value?.config?.refresh_interval_seconds || 300
// Poll faster while first-upgrade bootstrap is filling 90m→30d so the progress bar moves.
const seconds = bootstrapActive.value
? 10
: snapshot.value?.config?.refresh_interval_seconds || 300
autoRefreshTimer = window.setInterval(() => {
if (!loading.value && !refreshing.value) {
void reload(true)
}
}, Math.max(60, seconds) * 1000)
}, Math.max(bootstrapActive.value ? 10 : 60, seconds) * 1000)
}
function drillModel(row: MonitorModelRow) {
filter.value.platforms = [row.platform]