48 lines
4.8 KiB
Vue
48 lines
4.8 KiB
Vue
<script setup lang="ts">
|
||||
|
|
import { computed, ref, watch } from 'vue'
|
|||
|
|
import type { Progress } from '../types'
|
|||
|
|
import { beijingTime, phaseText, quantityText, unitText } from '../state'
|
|||
|
|
const props = defineProps<{ progress: Partial<Progress> | null | undefined; taskNo: string; stale?: boolean; compact?: 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 || 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' ? '本仓' : '范围待确认')
|
|||
|
|
</script>
|
|||
|
|
<template>
|
|||
|
|
<section class="collection-stage-counts" 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">
|
|||
|
|
<svg viewBox="0 0 160 160" aria-hidden="true"><circle cx="80" cy="80" r="64" class="ring-track" /><circle v-if="counts" cx="80" cy="80" r="64" class="ring-value" :stroke-dasharray="402.124" :stroke-dashoffset="402.124 * (1 - counts.percent / 100)" /></svg>
|
|||
|
|
<div class="ring-label"><strong>{{ counts ? `${counts.percent.toFixed(1)}%` : '—' }}</strong><span>{{ stale ? '上次阶段记录' : '当前阶段' }}</span></div>
|
|||
|
|
</div>
|
|||
|
|
<div>
|
|||
|
|
<p v-if="!compact">{{ phaseText(progress?.phase) }} · {{ scope }}<span v-if="progress?.scanRound != null"> · 第 {{ progress.scanRound }} 轮</span></p>
|
|||
|
|
<dl class="collection-count-grid"><div><dt>总量</dt><dd>{{ progress?.totalKnown ? quantityText(progress.total) : '待确认' }} <small>{{ unitText(progress?.unit) }}</small></dd></div><div><dt>已完成</dt><dd>{{ quantityText(progress?.completed) }} <small>{{ unitText(progress?.unit) }}</small></dd></div><div><dt>剩余</dt><dd>{{ counts ? counts.remaining : '—' }} <small>{{ unitText(progress?.unit) }}</small></dd></div></dl>
|
|||
|
|
<p v-if="changedTotal">总量发生变化,正在复核;暂不展示比例。</p>
|
|||
|
|
<p v-else-if="!counts">{{ progress?.totalKnown && Number(progress.total) === 0 ? '本阶段数量为零;是否完成以阶段及发布证据为准。' : '总量或计数依据待确认,不估算完成比例。' }}</p>
|
|||
|
|
<p v-if="!compact && scope === '账号共享'">来源账号的阶段数量,不能当作本仓出库量或整个同步完成率。</p>
|
|||
|
|
<p v-if="!compact && (progress?.sliceFrom || progress?.sliceUntil)">当前切片:{{ 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; }
|
|||
|
|
.collection-ring svg { width: 100%; height: 100%; }
|
|||
|
|
.ring-track, .ring-value { fill: none; stroke-width: 10; }
|
|||
|
|
.ring-track { stroke: var(--el-border-color-light); }
|
|||
|
|
.ring-value { stroke: var(--el-color-primary); stroke-linecap: round; transform: rotate(-90deg); transform-origin: 80px 80px; }
|
|||
|
|
.ring-label { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|||
|
|
.ring-label strong { font-size: 28px; font-weight: 500; font-variant-numeric: tabular-nums; }
|
|||
|
|
.collection-count-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--admin-space-4); margin: var(--admin-space-4) 0; }
|
|||
|
|
dt, small, .ring-label span { color: var(--el-text-color-secondary); font-size: var(--admin-font-caption); }
|
|||
|
|
dd { margin: var(--admin-space-2) 0 0; font-size: 30px; font-variant-numeric: tabular-nums; }
|
|||
|
|
p { margin: var(--admin-space-2) 0; font-size: var(--admin-font-caption); overflow-wrap: anywhere; }
|
|||
|
|
</style>
|