Files
admin-web/src/modules/self-operated/components/CollectionProgressGauge.vue
T

68 lines
6.2 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 lang="ts">
import InlineHelp from '@/shared/components/InlineHelp.vue'
import { computed, ref, watch } from 'vue'
import AnalyticsChart from './AnalyticsChart.vue'
import { progressGaugeOption } from '../dashboardChartOptions'
import type { Progress } from '../types'
import { beijingTime, phaseText, quantityText, unitText } from '../state'
import '@/styles/adminWorkbench.css'
const props = defineProps<{ progress: Partial<Progress> | null | undefined; taskNo: string; stale?: boolean; compact?: boolean; running?: boolean }>()
const identity = computed(() => JSON.stringify([props.taskNo, props.progress?.progressScope, props.progress?.countBasis, props.progress?.attemptNo, props.progress?.phase, props.progress?.scanRound, props.progress?.sliceFrom, props.progress?.sliceUntil, props.progress?.unit]))
const changedTotal = ref(false)
watch([identity, () => props.progress?.total], ([id, total], [oldId, oldTotal]) => { changedTotal.value = id === oldId ? changedTotal.value || (oldTotal != null && total != null && total !== oldTotal) : false })
const counts = computed(() => {
const p = props.progress
if (!p || p.completed == null || p.total == null || !p.totalKnown || !p.unit || p.unit === 'NONE' || !['ACCOUNT_SHARED', 'WAREHOUSE'].includes(p.progressScope ?? '') || !['TASK_CUMULATIVE', 'ATTEMPT_ONLY'].includes(p.countBasis ?? '') || changedTotal.value) return null
const done = Number(p.completed), total = Number(p.total)
if (!Number.isSafeInteger(done) || !Number.isSafeInteger(total) || done < 0 || total < done || total <= 0) return null
return { remaining: total - done, percent: Number(BigInt(done) * 1000n / BigInt(total)) / 10 }
})
const scope = computed(() => props.progress?.progressScope === 'ACCOUNT_SHARED' ? '账号共享' : props.progress?.progressScope === 'WAREHOUSE' ? '本仓' : '范围待确认')
const indeterminate = computed(() => Boolean(props.running && !props.stale && !counts.value))
const option = computed(() => progressGaugeOption(counts.value?.percent ?? null, indeterminate.value))
const readingDirectory = computed(() => !props.progress?.totalKnown && ['DIRECTORY', 'DISCOVERY'].includes(props.progress?.phase ?? ''))
</script>
<template>
<section class="collection-stage-counts admin-workbench" aria-label="当前阶段数量">
<div class="collection-ring" :role="counts ? 'progressbar' : 'img'" :aria-label="`${scope} · ${phaseText(progress?.phase)} · ${counts ? '当前阶段' : '总量待确认'}`" :aria-valuenow="counts?.percent" :aria-valuemin="counts ? 0 : undefined" :aria-valuemax="counts ? 100 : undefined">
<AnalyticsChart :option="option" merge-updates class="ring-chart" :class="{ 'is-indeterminate': indeterminate }" aria-hidden="true" />
<div class="ring-label"><strong>{{ counts ? `${counts.percent.toFixed(1)}%` : indeterminate ? '执行中' : '—' }}</strong><span>{{ stale ? '上次阶段记录' : '当前阶段' }}</span></div>
</div>
<div :class="{ 'compact-counts': compact }">
<p v-if="!compact" class="workbench-note">{{ phaseText(progress?.phase) }} · {{ scope }}<InlineHelp v-if="scope === '账号共享'" title="阶段数量说明"><p>来源账号的阶段数量,不作为本仓出库量或整个同步完成率。</p></InlineHelp><span v-if="progress?.scanRound != null"> · 第 {{ progress.scanRound }} 轮</span></p>
<dl class="collection-count-grid">
<div class="workbench-metric is-compact">
<dt class="workbench-metric-label">总量</dt>
<dd class="workbench-metric-value"><strong>{{ progress?.totalKnown ? quantityText(progress.total) : compact ? '—' : '待确认' }}</strong><span>{{ compact && (!progress?.unit || progress.unit === 'NONE') ? '' : unitText(progress?.unit) }}</span></dd>
</div>
<div class="workbench-metric is-compact">
<dt class="workbench-metric-label">已完成</dt>
<dd class="workbench-metric-value"><strong>{{ quantityText(progress?.completed) }}</strong><span>{{ compact && (!progress?.unit || progress.unit === 'NONE') ? '' : unitText(progress?.unit) }}</span></dd>
</div>
<div class="workbench-metric is-compact">
<dt class="workbench-metric-label">剩余</dt>
<dd class="workbench-metric-value"><strong>{{ counts ? counts.remaining : '—' }}</strong><span>{{ compact && (!progress?.unit || progress.unit === 'NONE') ? '' : unitText(progress?.unit) }}</span></dd>
</div>
</dl>
<p v-if="changedTotal" class="workbench-note">总量发生变化,正在复核;暂不展示比例。</p>
<p v-else-if="!counts" class="workbench-note">{{ readingDirectory ? '目录总页数尚未返回,已读取数量见“已完成”。' : progress?.totalKnown && progress.total != null && Number(progress.total) === 0 ? '本阶段数量为零;是否完成以阶段及发布证据为准。' : '总量或计数依据待确认,不估算完成比例。' }}</p>
<p v-if="!compact && (progress?.sliceFrom || progress?.sliceUntil)" class="workbench-note">当前切片:{{ beijingTime(progress?.sliceFrom) }} 至 {{ beijingTime(progress?.sliceUntil) }}(结束边界不含)</p>
</div>
</section>
</template>
<style scoped>
.collection-stage-counts { display: grid; grid-template-columns: 156px minmax(0, 1fr); align-items: center; gap: var(--admin-space-6); }
.collection-ring { width: 156px; height: 156px; position: relative; }
.compact-counts { min-width: 0; }
.ring-chart { width: 100%; height: 100%; }
.ring-chart.is-indeterminate { animation: collection-spin 2.4s linear infinite; }
@keyframes collection-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ring-chart.is-indeterminate { animation: none; } }
.ring-label { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.ring-label strong { color: var(--workbench-primary); font-size: var(--workbench-compact-number-size); font-weight: 700; font-variant-numeric: tabular-nums; }
.ring-label span { margin-top: 4px; color: var(--workbench-muted); font-size: var(--workbench-note-size); }
.collection-count-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--admin-space-4); margin: var(--admin-space-4) 0; }
.compact-counts .collection-count-grid { margin-block: 0; }
</style>