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

74 lines
6.7 KiB
Vue
Raw Normal View History

<script setup lang="ts">
import { computed } from 'vue'
import type { Overview, Task } from '../types'
import CollectionTaskProgress from './CollectionTaskProgress.vue'
import { beijingTime, phaseText } from '../state'
import { taskStateText } from '../collectionPresentation'
const props = defineProps<{ overview: Overview | null; loading?: boolean; stale?: boolean; visible?: boolean }>()
defineEmits<{ refresh: [] }>()
const activeStates = new Set(['RUNNING', 'APPLYING', 'READY', 'QUEUED', 'WAITING', 'WAITING_SOURCE', 'WAITING_UPSTREAM', 'RETRY_WAIT', 'YIELDED'])
const tasks = computed(() => {
const all = [props.overview?.accountActivity, ...(props.overview?.warehouses.flatMap(w => [...(w.activeTasks ?? []), w.sharedUpstream]) ?? [])]
return [...new Map(all.filter((t): t is Task => Boolean(t && activeStates.has(t.taskStatus))).map(t => [t.taskNo, t])).values()]
})
const running = computed(() => tasks.value.filter(t => ['RUNNING', 'APPLYING'].includes(t.taskStatus)))
const current = computed(() => running.value[0] ?? tasks.value[0] ?? null)
const waiting = computed(() => tasks.value.filter(t => t.taskNo !== current.value?.taskNo && !['RUNNING', 'APPLYING'].includes(t.taskStatus)))
const upcoming = computed(() => {
const groups = new Map<string, number>()
for (const task of waiting.value) {
const name = `${task.warehouseCode || '账号'} · ${task.dataFamily === 'INVENTORY' ? '产品库存' : '出库销量'}`
groups.set(name, (groups.get(name) ?? 0) + 1)
}
return [...groups].map(([name, count]) => ({ name, count }))
})
const nextStep = computed(() => {
const phase = current.value?.progress?.phase
if (['DIRECTORY', 'DISCOVERY'].includes(phase ?? '')) return '获取出库详情 → 各仓校验与归并 → 统计计算 → 发布结果'
if (['DETAIL', 'DETAILS'].includes(phase ?? '')) return '各仓校验与归并 → 统计计算 → 发布结果'
if (['INVENTORY', 'FETCH'].includes(phase ?? '')) return '库存完整性校验 → 统计计算 → 发布结果'
if (['VALIDATE', 'VALIDATION', 'MERGE'].includes(phase ?? '')) return '统计计算 → 发布结果'
if (['BUILD', 'BUILDING', 'BUILT'].includes(phase ?? '')) return '发布结果'
if (['PUBLISH', 'PUBLISHED'].includes(phase ?? '')) return '本次发布完成后,由调度继续安排后续任务'
return '等待调度确认下一步'
})
const scopeText = computed(() => current.value?.taskNo === props.overview?.accountActivity?.taskNo || current.value?.progress?.progressScope === 'ACCOUNT_SHARED' ? '领星账号 · 出库数据' : `${current.value?.warehouseCode || '领星账号'} · ${current.value?.dataFamily === 'INVENTORY' ? '产品库存' : '出库销量'}`)
</script>
<template>
<section class="account-monitor" aria-label="领星账号运行进度">
<header class="account-header"><div><h2>数据采集</h2><span>领星账号</span></div><el-button :loading="loading" @click="$emit('refresh')">刷新状态</el-button></header>
<div class="account-body">
<p v-if="stale" role="status">状态更新失败,以下保留最后一次读取的进度。</p>
<template v-if="current">
<div class="account-now"><div><span class="account-eyebrow">{{ running.length ? '正在进行' : '当前等待' }}</span><h3>{{ current.progress?.phase ? phaseText(current.progress.phase) : taskStateText(current) }}</h3><p>{{ scopeText }}<template v-if="current.queryFrom"> · {{ beijingTime(current.queryFrom) }} 至 {{ beijingTime(current.queryUntil) }}(结束时刻不含)</template></p></div><span class="account-status">{{ taskStateText(current) }}</span></div>
<CollectionTaskProgress :key="current.taskNo" :task="current" compact :stale="stale" :visible="visible" :observed-at="overview?.observedAt" />
<div class="account-next"><strong>后续处理</strong><span>{{ nextStep }}<small>流程参考,实际切换由调度确认。</small></span></div>
<p v-if="running.length > 1">同时运行:<span v-for="task in running.slice(1)" :key="task.taskNo">{{ task.warehouseCode || '账号' }} · {{ phaseText(task.progress?.phase) }};</span></p>
</template>
<div v-else class="account-empty"><h3>{{ loading ? '正在读取采集状态…' : overview ? '当前没有正在运行的采集' : '尚未读取到采集状态' }}</h3><p>{{ overview?.collectionMode === 'OFF' ? '采集已关闭,已有报表仍可查看。' : '运行任务会在这里自动更新。' }}</p></div>
<div v-if="overview" class="account-pending"><strong>已知待处理</strong><span v-for="group in upcoming" :key="group.name">{{ group.name }} <b>{{ group.count }}</b> 项</span><span v-if="!upcoming.length">当前摘要中暂无其他待处理项</span><small>仅当前活动摘要;完整队列数量及下一任务顺序尚未提供。</small></div>
<footer><span>最近推进 {{ beijingTime(current?.progressUpdatedAt) }}</span><span>状态更新 {{ beijingTime(overview?.observedAt) }} · 北京时间</span></footer>
</div>
</section>
</template>
<style scoped>
.account-monitor { background: var(--admin-surface, #fff); border: 1px solid var(--el-border-color-light); border-radius: var(--admin-radius); overflow: hidden; }
.account-header, .account-header > div, .account-now, .account-next, .account-pending, footer { display: flex; align-items: center; gap: var(--admin-space-4); }
.account-header { justify-content: space-between; padding: var(--admin-space-5) var(--admin-space-6); border-bottom: 1px solid var(--el-border-color-light); }
h2, h3, p { margin: 0; }
h2 { font-size: var(--admin-font-section); }
h3 { font-size: 24px; margin: var(--admin-space-2) 0; }
.account-body { max-width: 1180px; margin-inline: auto; padding: var(--admin-space-6); }
.account-header span, p, footer, small { color: var(--el-text-color-secondary); font-size: var(--admin-font-caption); }
.account-now { justify-content: space-between; align-items: flex-start; }
.account-eyebrow { color: var(--el-color-primary); font-size: var(--admin-font-caption); }
.account-status { color: var(--el-color-primary); background: var(--el-color-primary-light-9); padding: var(--admin-space-2) var(--admin-space-3); border-radius: var(--admin-radius); white-space: nowrap; }
.account-next { padding: var(--admin-space-4); background: var(--el-fill-color-light); border-left: 3px solid var(--el-color-primary); }
.account-next small { display: block; margin-top: var(--admin-space-2); }
.account-pending { flex-wrap: wrap; margin-top: var(--admin-space-5); }
.account-pending small { flex-basis: 100%; }
footer { justify-content: space-between; margin-top: var(--admin-space-5); padding-top: var(--admin-space-4); border-top: 1px solid var(--el-border-color-light); }
.account-empty { padding-block: var(--admin-space-6); }
</style>