fix(selfop): unify collection layout and show details in dialogs
This commit is contained in:
@@ -1,27 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import '@/styles/adminDensity.css'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||||
import ListPageLayout from '@/shared/components/ListPageLayout.vue'
|
||||
import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'
|
||||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import { getApiErrorMessage, getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { analyticsApi } from '../api/analytics'
|
||||
import type { Overview, Run, Task, TaskDetail, Validation } from '../types'
|
||||
import type { Overview, Run, Task, TaskDetail, Validation, WarehouseOverview } from '../types'
|
||||
import { RequestFence, RetryIntent, RuntimePoller, hasActiveTask, label, beijingTime, reasonsText, unitText, progressText, quantityText, taskRetryAllowed } from '../state'
|
||||
|
||||
const access = useAccessPreviewStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const canList = computed(() => access.can('platform:self-operated-analytics:list'))
|
||||
const canSync = computed(() => access.can('platform:self-operated-analytics:sync'))
|
||||
const sourceAccount = 'lingxing-primary'
|
||||
const filters = ref<QueryModel>({ warehouseCode: typeof route.query.warehouseCode === 'string' ? route.query.warehouseCode : '' })
|
||||
const warehouseCode = computed(() => String(filters.value.warehouseCode ?? ''))
|
||||
const overview = ref<Overview | null>(null)
|
||||
const configOpen = ref(false)
|
||||
const configKey = ref('')
|
||||
const configItem = computed(() => overview.value?.warehouses.find(item => `${item.warehouseCode}:${item.dataFamily}` === configKey.value) ?? null)
|
||||
const rows = ref<Task[]>([])
|
||||
const current = ref(1)
|
||||
const size = ref(20)
|
||||
@@ -32,6 +37,7 @@ const error = ref('')
|
||||
const errorDetails = ref<ApiErrorDetails | null>(null)
|
||||
const loading = ref(false)
|
||||
const selected = ref<Task | null>(null)
|
||||
const taskOpen = ref(false)
|
||||
const selectedDetail = ref<TaskDetail | null>(null)
|
||||
const attempts = ref<Run[]>([])
|
||||
const attemptsPage = ref(1)
|
||||
@@ -49,7 +55,19 @@ const detailFence = new RequestFence()
|
||||
const actionFence = new RequestFence()
|
||||
const retryIntent = new RetryIntent()
|
||||
const warehouses = computed(() => overview.value?.warehouses.filter(row => !warehouseCode.value || row.warehouseCode === warehouseCode.value) ?? [])
|
||||
const fields = computed<QueryField[]>(() => [{ key: 'warehouseCode', label: '仓库', type: 'select', options: [...new Map((overview.value?.warehouses ?? []).map(row => [row.warehouseCode, { label: row.warehouseName || row.warehouseCode, value: row.warehouseCode }])).values()] }])
|
||||
const fields = computed<QueryField[]>(() => [{ key: 'warehouseCode', label: '仓库', type: 'select', width: 230, placeholder: '全部仓库采集状态', options: [...new Map((overview.value?.warehouses ?? []).map(row => [row.warehouseCode, { label: row.warehouseName || row.warehouseCode, value: row.warehouseCode }])).values()] }])
|
||||
const hasSyntheticSamples = computed(() => warehouses.value.some(item => item.warehouseCode.startsWith('SELFOP-TEST-')))
|
||||
const overviewColumns: DataTableColumn[] = [
|
||||
{ key: 'configuration', label: '操作', width: 110, fixed: 'left', slotName: 'configuration' },
|
||||
{ prop: 'warehouseName', label: '仓库', minWidth: 160, slotName: 'warehouse' },
|
||||
{ key: 'family', label: '数据类型', width: 110, slotName: 'family' },
|
||||
{ key: 'enabled', label: '自动采集', width: 100, slotName: 'enabled' },
|
||||
{ key: 'execution', label: '最近执行', width: 110, slotName: 'execution' },
|
||||
{ key: 'publication', label: '最近发布', minWidth: 180, slotName: 'publication' },
|
||||
{ key: 'coverage', label: '数据覆盖截至', minWidth: 180, slotName: 'coverage' },
|
||||
{ key: 'freshness', label: '数据时效', width: 100, slotName: 'freshness' },
|
||||
{ key: 'dailySamples', label: '库存日样本', minWidth: 150, slotName: 'dailySamples' },
|
||||
]
|
||||
const scope = computed(() => ({ sourceAccount, warehouseCode: warehouseCode.value }))
|
||||
const retryAllowed = computed(() => taskRetryAllowed(selected.value, canSync.value, scope.value, reason.value))
|
||||
const columns: DataTableColumn[] = [
|
||||
@@ -73,6 +91,8 @@ const runColumns: DataTableColumn[] = [
|
||||
]
|
||||
function asTask(row: unknown) { return row as Task }
|
||||
function asRun(row: unknown) { return row as Run }
|
||||
function asWarehouse(row: unknown) { return row as WarehouseOverview }
|
||||
function openConfiguration(item: WarehouseOverview) { configKey.value = `${item.warehouseCode}:${item.dataFamily}`; configOpen.value = true }
|
||||
function family(value: string) { return value === 'INVENTORY' ? '产品库存' : value === 'OUTBOUND' || value === 'SALES' ? '出库销量' : '任务类型待核对' }
|
||||
function validation(task: Task | null | undefined): Validation | null { return task?.validation ?? null }
|
||||
function budgetValue(value: unknown) { return typeof value === 'number' || typeof value === 'string' ? String(value) : '—' }
|
||||
@@ -98,7 +118,7 @@ async function read() {
|
||||
}
|
||||
async function selectTask(task: Task, reset = true) {
|
||||
if (!task.taskNo || task.warehouseCode !== warehouseCode.value) return
|
||||
if (reset) { actionFence.cancel(); submitting.value = false; selected.value = task; selectedDetail.value = null; attempts.value = []; attemptsPage.value = 1; attemptsUpperBound.value = null; reason.value = ''; confirming.value = false; actionFeedback.value = '' }
|
||||
if (reset) { taskOpen.value = true; actionFence.cancel(); submitting.value = false; selected.value = task; selectedDetail.value = null; attempts.value = []; attemptsPage.value = 1; attemptsUpperBound.value = null; reason.value = ''; confirming.value = false; actionFeedback.value = '' }
|
||||
const request = detailFence.begin()
|
||||
detailLoading.value = true
|
||||
detailError.value = ''
|
||||
@@ -136,7 +156,9 @@ async function submitRetry() {
|
||||
const poller = new RuntimePoller(async () => { const busy = await read(); if (selected.value && !submitting.value) await selectTask(selected.value, false); return busy }, () => { fence.cancel(); detailFence.cancel() })
|
||||
function visibility() { poller.setVisible(!document.hidden) }
|
||||
function refresh() { upperBound.value = null; current.value = 1; void read() }
|
||||
watch(warehouseCode, () => { fence.cancel(); detailFence.cancel(); actionFence.cancel(); selected.value = null; rows.value = []; total.value = 0; attempts.value = []; upperBound.value = null; confirming.value = false; submitting.value = false; reason.value = ''; actionFeedback.value = ''; current.value = 1; void read() })
|
||||
function openAnalysis(path: string) { void router.push({ path, query: { warehouseCode: warehouseCode.value } }) }
|
||||
watch(warehouseCode, () => { configOpen.value = false; taskOpen.value = false; fence.cancel(); detailFence.cancel(); actionFence.cancel(); selected.value = null; rows.value = []; total.value = 0; attempts.value = []; upperBound.value = null; confirming.value = false; submitting.value = false; reason.value = ''; actionFeedback.value = ''; current.value = 1; void read() })
|
||||
watch(taskOpen, open => { if (!open) { detailFence.cancel(); detailLoading.value = false; selected.value = null; selectedDetail.value = null; attempts.value = []; confirming.value = false; reason.value = ''; actionFeedback.value = '' } })
|
||||
watch([current, size], () => { void read() })
|
||||
watch([attemptsPage, attemptsSize], () => { if (selected.value) void selectTask(selected.value, false) })
|
||||
watch(reason, () => { confirming.value = false })
|
||||
@@ -147,43 +169,31 @@ onUnmounted(() => { poller.stop(); actionFence.cancel(); document.removeEventLis
|
||||
<AdminPage class="admin-density">
|
||||
<p v-if="!canList" role="alert">没有采集状态查看权限。</p>
|
||||
<ListPageLayout v-else>
|
||||
<p>来源账号:{{ sourceAccount }} · 全部业务时间为北京时间 · 最近观测 {{ beijingTime(observedAt) }}</p>
|
||||
<QueryToolbar v-model="filters" :fields="fields" ariaLabel="采集仓库筛选" :loading="loading" @search="refresh" @reset="refresh" />
|
||||
<LoadErrorState v-if="error" title="采集状态更新失败" :message="error" :loading="loading" compact @retry="read"><template #details><p>状态观测时间 {{ beijingTime(observedAt) }}。任务运行失败与本页读取失败分别判断。</p><ApiErrorMeta v-if="errorDetails" :details="errorDetails" /></template></LoadErrorState>
|
||||
<section v-for="item in warehouses" :key="`${item.warehouseCode}:${item.dataFamily}`" :aria-label="`${item.warehouseName || item.warehouseCode}采集状态`">
|
||||
<h3>{{ item.warehouseName || item.warehouseCode }} · {{ family(item.dataFamily) }}</h3>
|
||||
<el-descriptions class="density-descriptions" :column="4" border>
|
||||
<el-descriptions-item label="环境 / 模式">{{ overview?.environmentLabel ?? '—' }} / {{ label(overview?.collectionMode) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="采集 / 发布">{{ item.executionEnabled ? '已启用' : '未启用' }} / {{ item.publicationEnabled ? '已启用' : '未启用' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用途">{{ label(item.currentTask?.executionPurpose) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="停用原因">{{ blockReason(item.disabledReason) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最近执行">{{ label(item.currentTask?.taskStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发布结果">{{ label(item.currentTask?.resultPublicationState) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最近成功">{{ item.lastSucceededAt ? beijingTime(item.lastSucceededAt) : '尚无成功记录' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结果发布时间">{{ item.lastPublishedAt ? beijingTime(item.lastPublishedAt) : '尚无可用数据' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="两个月目标">{{ beijingTime(item.initialFrom) }} 至 {{ beijingTime(item.initialUntil) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="首采完整覆盖">{{ label(item.initialCoverageStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="连续覆盖截至">{{ beijingTime(item.salesCoveredUntil) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据新鲜度">{{ label(item.dataFreshness) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库存采集区间" :span="2">{{ beijingTime(item.inventoryObservedFrom) }} 至 {{ beijingTime(item.inventoryObservedUntil) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="下次计划">{{ item.nextScheduledAt ? beijingTime(item.nextScheduledAt) : (item.executionEnabled ? '待确认' : '未启用') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="修订状态">{{ item.rebuildRequired ? '待重建' : label(item.revisionReviewStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号冷却">{{ beijingTime(item.protection.accountCooldownUntil) }} {{ item.protection.accountPauseReason ?? '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="验证会话">{{ validation(item.currentTask)?.validationSessionNo ?? '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会话截止">{{ beijingTime(validation(item.currentTask)?.deadlineAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="停止原因">{{ validation(item.currentTask)?.stopReason ?? '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<p v-if="item.dataFamily === 'INVENTORY' && item.inventoryDailySamples">每日库存观测:{{ item.inventoryDailySamples.windowFrom }} 至 {{ item.inventoryDailySamples.windowUntil }}(结束边界不含,近 {{ item.inventoryDailySamples.expectedDays }} 个完整日),已留存 {{ item.inventoryDailySamples.sampleCount }} / {{ item.inventoryDailySamples.expectedDays }} 个日样本;数据积累起点 {{ item.inventoryDailySamples.accumulationStartedOn ?? '尚无日样本' }}。缺口日期:{{ item.inventoryDailySamples.missingDates.length ? item.inventoryDailySamples.missingDates.join('、') : '无' }}。日样本只表示实际观测留存,不代表真实库龄。</p>
|
||||
<p v-else-if="item.dataFamily === 'INVENTORY'">每日库存观测积累状态尚无可用资料。</p>
|
||||
<p v-if="item.currentTask?.taskStatus === 'FAILED' || item.currentTask?.taskStatus === 'DEAD'">本次失败,{{ item.lastPublishedAt ? '仍展示上次成功数据;请核对数据时效。' : '尚无可用数据。' }}</p>
|
||||
<p v-if="item.sharedUpstream">账号共享阶段:{{ item.sharedUpstream.taskNo || '—' }} · {{ label(item.sharedUpstream.taskStatus) }} · {{ progressText(item.sharedUpstream.progress) }}。共享采集完成不代表本仓发布成功。</p>
|
||||
<p>账号日预算 {{ quantityText(item.protection.dailyRequestBudget) }};已保留请求 {{ quantityText(item.protection.reservedHttpRequests) }};已确认 HTTP {{ quantityText(item.protection.confirmedHttpRequests) }};额度日期 {{ item.protection.budgetDate ?? '—' }}。</p>
|
||||
<p>验证会话预算(含保守预留):<span v-for="(used, key) in validation(item.currentTask)?.budgetConsumed" :key="key">{{ budgetName(String(key)) }} {{ budgetValue(used) }} / {{ budgetValue(limitFor(String(key), validation(item.currentTask))) }}; </span><span v-if="!validation(item.currentTask)?.budgetConsumed">尚无预算观测</span></p>
|
||||
<p v-if="validation(item.currentTask)">会话限制:<span v-for="(limit, key) in validation(item.currentTask)?.budgetLimits" :key="key">{{ budgetName(String(key)) }} {{ budgetValue(limit) }}; </span>最多 1 个在途,请求起始间隔至少 5 秒。验证样本不会发布正式结果。</p>
|
||||
</section>
|
||||
<p v-if="!warehouseCode">选择仓库后查看逻辑任务与尝试历史。首采完整覆盖不以单切片成功或账号目录完成推定。</p>
|
||||
<DataTable appearance="integrated" v-model:page="current" v-model:page-size="size" :data="rows" :columns="columns" :total="total" :loading="loading" empty-text="暂无逻辑任务">
|
||||
<template #toolbar-actions><el-button @click="refresh">更新任务列表</el-button></template>
|
||||
<div class="collection-workspace">
|
||||
<DataTable appearance="integrated" :data="warehouses" :columns="overviewColumns" :loading="loading" empty-text="暂无仓库采集状态">
|
||||
<template #toolbar-actions><el-button :disabled="!warehouseCode" @click="openAnalysis('/self-operated/sales')">查看销量</el-button><el-button :disabled="!warehouseCode" @click="openAnalysis('/self-operated/inventory')">查看库存</el-button></template>
|
||||
<template #filters><QueryToolbar v-model="filters" :fields="fields" ariaLabel="采集仓库筛选" preference-key="self-operated-collection-query" :loading="loading" @search="refresh" @reset="refresh" /></template>
|
||||
<template #notices>
|
||||
<LoadErrorState v-if="error" title="采集状态更新失败" :message="error" :loading="loading" compact @retry="read"><template #details><p>状态观测时间 {{ beijingTime(observedAt) }}。任务运行失败与本页读取失败分别判断。</p><ApiErrorMeta v-if="errorDetails" :details="errorDetails" /></template></LoadErrorState>
|
||||
<div class="collection-summary" role="status">
|
||||
<span v-if="overview?.collectionMode === 'OFF'">自动采集已关闭,当前仅查看已留存结果。</span>
|
||||
<span v-else>采集模式:{{ label(overview?.collectionMode) }}</span>
|
||||
<span v-if="hasSyntheticSamples">包含合成测试样本,不代表真实领星采集覆盖。</span>
|
||||
<span class="collection-observed">最近更新:{{ beijingTime(observedAt) }}(北京时间)</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #configuration="{ row }"><el-button class="density-action" @click="openConfiguration(asWarehouse(row))">查看配置</el-button></template>
|
||||
<template #warehouse="{ row }">{{ asWarehouse(row).warehouseName || asWarehouse(row).warehouseCode }}</template>
|
||||
<template #family="{ row }">{{ family(asWarehouse(row).dataFamily) }}</template>
|
||||
<template #enabled="{ row }">{{ asWarehouse(row).executionEnabled ? '已启用' : '已关闭' }}</template>
|
||||
<template #execution="{ row }">{{ label(asWarehouse(row).currentTask?.taskStatus) }}</template>
|
||||
<template #publication="{ row }">{{ label(asWarehouse(row).currentTask?.resultPublicationState) }}<small class="collection-secondary">{{ asWarehouse(row).lastPublishedAt ? beijingTime(asWarehouse(row).lastPublishedAt) : '尚无可用结果' }}</small></template>
|
||||
<template #coverage="{ row }">{{ beijingTime(asWarehouse(row).dataFamily === 'INVENTORY' ? asWarehouse(row).inventoryObservedUntil : asWarehouse(row).salesCoveredUntil) }}</template>
|
||||
<template #freshness="{ row }">{{ label(asWarehouse(row).dataFreshness) }}</template>
|
||||
<template #dailySamples="{ row }"><template v-if="asWarehouse(row).dataFamily === 'INVENTORY' && asWarehouse(row).inventoryDailySamples">已留存 {{ asWarehouse(row).inventoryDailySamples?.sampleCount }} / {{ asWarehouse(row).inventoryDailySamples?.expectedDays }} 个日样本</template><template v-else>—</template></template>
|
||||
</DataTable>
|
||||
|
||||
<DataTable appearance="integrated" v-model:page="current" v-model:page-size="size" :data="rows" :columns="columns" :total="total" :loading="loading" :empty-text="!warehouseCode ? '选择仓库后查看采集任务与尝试历史' : error && !rows.length ? '任务读取失败,请重试' : '暂无采集任务'">
|
||||
<template #actions="{ row }"><el-button link :disabled="!asTask(row).taskNo" @click="selectTask(asTask(row))">详情 / 恢复</el-button></template>
|
||||
<template #family="{ row }">{{ family(asTask(row).dataFamily) }} / {{ label(asTask(row).executionPurpose) }}</template>
|
||||
<template #range="{ row }">{{ beijingTime(asTask(row).queryFrom) }} 至 {{ beijingTime(asTask(row).queryUntil) }}</template>
|
||||
@@ -193,7 +203,8 @@ onUnmounted(() => { poller.stop(); actionFence.cancel(); document.removeEventLis
|
||||
<template #health="{ row }">{{ beijingTime(asTask(row).latestAttempt?.progressUpdatedAt) }} / {{ label(asTask(row).latestAttempt?.executionHealth) }}</template>
|
||||
<template #block="{ row }">{{ blockReason(asTask(row).actionBlockReason) }}</template>
|
||||
</DataTable>
|
||||
<section v-if="selected" aria-label="选中任务详情">
|
||||
<AppDialog v-model="taskOpen" class="admin-density-dialog" :title="`采集任务详情 · ${selected?.taskNo || '—'}`" width="1200px" :loading="detailLoading" :confirm-loading="submitting" :show-confirm="false" cancel-text="关闭" fullscreenable>
|
||||
<section v-if="selected" class="collection-details admin-density" aria-label="选中任务详情">
|
||||
<h3>任务 {{ selected.taskNo || '—' }}</h3>
|
||||
<p v-if="detailLoading">读取任务详情…</p><p v-if="detailError" role="alert">{{ detailError }}</p>
|
||||
<p>执行:{{ label(selected.taskStatus) }};发布:{{ label(selected.resultPublicationState) }};尝试 {{ selected.attemptCount }} 次;{{ progressText(selected.progress) }}</p>
|
||||
@@ -213,6 +224,56 @@ onUnmounted(() => { poller.stop(); actionFence.cancel(); document.removeEventLis
|
||||
</form>
|
||||
<p v-else>没有任务恢复权限。</p><p v-if="actionFeedback" role="status">{{ actionFeedback }}</p>
|
||||
</section>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</ListPageLayout>
|
||||
<AppDialog v-model="configOpen" class="admin-density-dialog" :title="`${configItem?.warehouseName || configItem?.warehouseCode || '仓库'} · ${family(configItem?.dataFamily || '')}配置`" width="1120px" :show-confirm="false" cancel-text="关闭" fullscreenable>
|
||||
<div class="admin-density">
|
||||
<section v-if="configItem" class="collection-details">
|
||||
<h3>{{ configItem.warehouseName || configItem.warehouseCode }} · {{ family(configItem.dataFamily) }}</h3>
|
||||
<el-descriptions class="density-descriptions" :column="3" border>
|
||||
<el-descriptions-item label="环境 / 模式">{{ overview?.environmentLabel ?? '—' }} / {{ label(overview?.collectionMode) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="采集 / 发布">{{ configItem.executionEnabled ? '已启用' : '未启用' }} / {{ configItem.publicationEnabled ? '已启用' : '未启用' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用途">{{ label(configItem.currentTask?.executionPurpose) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="停用原因">{{ blockReason(configItem.disabledReason) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最近执行">{{ label(configItem.currentTask?.taskStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="发布结果">{{ label(configItem.currentTask?.resultPublicationState) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最近成功">{{ configItem.lastSucceededAt ? beijingTime(configItem.lastSucceededAt) : '尚无成功记录' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结果发布时间">{{ configItem.lastPublishedAt ? beijingTime(configItem.lastPublishedAt) : '尚无可用数据' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="两个月目标">{{ beijingTime(configItem.initialFrom) }} 至 {{ beijingTime(configItem.initialUntil) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="首采完整覆盖">{{ label(configItem.initialCoverageStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="连续覆盖截至">{{ beijingTime(configItem.salesCoveredUntil) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="数据新鲜度">{{ label(configItem.dataFreshness) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库存采集区间" :span="2">{{ beijingTime(configItem.inventoryObservedFrom) }} 至 {{ beijingTime(configItem.inventoryObservedUntil) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="下次计划">{{ configItem.nextScheduledAt ? beijingTime(configItem.nextScheduledAt) : (configItem.executionEnabled ? '待确认' : '未启用') }}</el-descriptions-item>
|
||||
<el-descriptions-item label="修订状态">{{ configItem.rebuildRequired ? '待重建' : label(configItem.revisionReviewStatus) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账号冷却">{{ beijingTime(configItem.protection.accountCooldownUntil) }} {{ configItem.protection.accountPauseReason ?? '' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="验证会话">{{ validation(configItem.currentTask)?.validationSessionNo ?? '—' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="会话截止">{{ beijingTime(validation(configItem.currentTask)?.deadlineAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="停止原因">{{ validation(configItem.currentTask)?.stopReason ?? '—' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<template v-if="configItem.dataFamily === 'INVENTORY' && configItem.inventoryDailySamples">
|
||||
<p>每日库存观测:{{ configItem.inventoryDailySamples.windowFrom }} 至 {{ configItem.inventoryDailySamples.windowUntil }}(结束边界不含);数据积累起点 {{ configItem.inventoryDailySamples.accumulationStartedOn ?? '尚无日样本' }}。日样本只表示实际观测留存,不代表真实库龄。</p>
|
||||
<details class="collection-gaps"><summary>库存日样本缺口({{ configItem.inventoryDailySamples.missingDates.length }} 天)</summary><p>缺口日期:{{ configItem.inventoryDailySamples.missingDates.length ? configItem.inventoryDailySamples.missingDates.join('、') : '无' }}</p></details>
|
||||
</template>
|
||||
<p v-else-if="configItem.dataFamily === 'INVENTORY'">每日库存观测积累状态尚无可用资料。</p>
|
||||
<p v-if="configItem.currentTask?.taskStatus === 'FAILED' || configItem.currentTask?.taskStatus === 'DEAD'">本次失败,{{ configItem.lastPublishedAt ? '仍展示上次成功数据;请核对数据时效。' : '尚无可用数据。' }}</p>
|
||||
<p v-if="configItem.sharedUpstream">账号共享阶段:{{ configItem.sharedUpstream.taskNo || '—' }} · {{ label(configItem.sharedUpstream.taskStatus) }} · {{ progressText(configItem.sharedUpstream.progress) }}。共享采集完成不代表本仓发布成功。</p>
|
||||
<p>账号日预算 {{ quantityText(configItem.protection.dailyRequestBudget) }};已保留请求 {{ quantityText(configItem.protection.reservedHttpRequests) }};已确认 HTTP {{ quantityText(configItem.protection.confirmedHttpRequests) }};额度日期 {{ configItem.protection.budgetDate ?? '—' }}。</p>
|
||||
<p>验证会话预算(含保守预留):<span v-for="(used, key) in validation(configItem.currentTask)?.budgetConsumed" :key="key">{{ budgetName(String(key)) }} {{ budgetValue(used) }} / {{ budgetValue(limitFor(String(key), validation(configItem.currentTask))) }}; </span><span v-if="!validation(configItem.currentTask)?.budgetConsumed">尚无预算观测</span></p>
|
||||
<p v-if="validation(configItem.currentTask)">会话限制:<span v-for="(limit, key) in validation(configItem.currentTask)?.budgetLimits" :key="key">{{ budgetName(String(key)) }} {{ budgetValue(limit) }}; </span>最多 1 个在途,请求起始间隔至少 5 秒。验证样本不会发布正式结果。</p>
|
||||
</section>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</AdminPage>
|
||||
</template>
|
||||
<style scoped>
|
||||
.collection-workspace { display: grid; min-width: 0; gap: var(--admin-space-6); }
|
||||
.collection-summary { display: flex; flex-wrap: wrap; align-items: center; gap: var(--admin-space-5); font-size: var(--admin-font-caption); }
|
||||
.collection-observed { margin-left: auto; color: var(--el-text-color-secondary); }
|
||||
.collection-secondary { display: block; font-size: var(--admin-font-caption); color: var(--el-text-color-secondary); }
|
||||
.collection-details summary, .collection-gaps summary { cursor: pointer; font-size: var(--admin-font-caption); }
|
||||
.collection-details h3 { font-size: var(--admin-font-section); margin: var(--admin-space-5) 0; }
|
||||
.collection-details p { margin: var(--admin-space-3) 0; overflow-wrap: anywhere; }
|
||||
.collection-details section + section { margin-top: var(--admin-space-5); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user