@@ -12,8 +12,10 @@ 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 CollectionTaskProgress from '../components/CollectionTaskProgress.vue'
import CollectionAccountMonitor from '../components/CollectionAccountMonitor.vue'
import TaskExecutionFacts from '../components/TaskExecutionFacts.vue'
import { taskStateText , taskProgressText , taskCompletionText } from '../collectionPresentation'
import { taskStateText , taskProgressText } from '../collectionPresentation'
import type { Overview , Run , Task , TaskDetail , Validation , WarehouseOverview } from '../types'
import { RequestFence , RetryIntent , RuntimePoller , hasActiveTask , label , beijingTime , reasonsText , progressText , quantityText , taskRetryAllowed } from '../state'
@@ -23,7 +25,13 @@ 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 filters = ref < QueryModel > ( { warehouseCode : typeof route . query . warehouseCode === 'string' ? route . query . warehouseCode : '' , dataFamily : '' , taskNo : typeof route . query . taskNo === 'string' ? route . query . taskNo : '' } )
const dataFamily = computed ( ( ) => ( filters . value . dataFamily || undefined ) as 'OUTBOUND' | 'INVENTORY' | undefined )
const focusNo = ref ( typeof route . query . taskNo === 'string' ? route . query . taskNo : '' )
const manualFocus = ref ( Boolean ( focusNo . value ) )
const historyLoaded = ref ( false )
const historyOpen = ref ( false )
const visible = ref ( ! document . hidden )
const warehouseCode = computed ( ( ) => String ( filters . value . warehouseCode ? ? '' ) )
const overview = ref < Overview | null > ( null )
const configOpen = ref ( false )
@@ -31,7 +39,7 @@ 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 )
const size = ref ( 10 )
const total = ref ( 0 )
const upperBound = ref < { listUpperCreatedAt : string | null ; listUpperId : string | null } | null > ( null )
const observedAt = ref < string | null > ( null )
@@ -48,55 +56,56 @@ const attemptsTotal = ref(0)
const attemptsUpperBound = ref < { listUpperCreatedAt : string | null ; listUpperId : string | null } | null > ( null )
const detailLoading = ref ( false )
const detailError = ref ( '' )
const detailErrorDetails = ref < ApiErrorDetails | null > ( null )
const reason = ref ( '' )
const confirming = ref ( false )
const submitting = ref ( false )
const actionFeedback = ref ( '' )
const fence = new RequestFence ( )
const detailFence = new RequestFence ( )
const historyFence = new RequestFence ( )
const actionFence = new RequestFence ( )
const retryIntent = new RetryIntent ( )
let active = false
const warehouses = computed ( ( ) => overview . value ? . warehouses . filter ( row => ! warehouseCode . value || row . warehouseCode === warehouseCode . value ) ? ? [ ] )
const preparationTasks = computed ( ( ) => {
const visible = new Set ( warehouses . value . map ( item => item . sharedUpstream ? . taskNo ) . filter ( Boolean ) )
const tasks = new Map < string , { task : Task ; warehouseCodes : string [ ] } > ( )
for ( const item of overview . value ? . warehouses ? ? [ ] ) {
const task = item . sharedUpstream
if ( ! task ? . taskNo || ! visible . has ( task . taskNo ) ) continue
let entry = tasks . get ( task . taskNo )
if ( ! entry ) { entry = { task , warehouseCodes : [ ] } ; tasks . set ( task . taskNo , entry ) }
if ( ! entry . warehouseCodes . includes ( item . warehouseCode ) ) entry . warehouseCodes . push ( item . warehouseCode )
const activityTasks = computed ( ( ) => warehouses . value . flatMap ( item => item . activeTasks ? ? [ ] ) . filter ( task => ! dataFamily . value || task . dataFamily === dataFamily . value ) )
const focusWarehouse = computed ( ( ) => overview . value ? . warehouses . find ( item => item . warehouseCode === selected . value ? . warehouseCode && item . dataFamily === selected . value ? . dataFamily ) ? ? null )
const sharedSources = computed ( ( ) => {
const sources = new Map < string , { task : Task ; warehouses : string [ ] } > ( )
for ( const row of warehouses . value ) {
if ( dataFamily . value && row . dataFamily !== dataFamily . value ) continue
const source = row . sharedUpstream
if ( ! source || ! [ 'WAITING_CONFIRMED' , 'SATISFIED' , 'FAILED' ] . includes ( row . currentTask ? . dependencyState ? ? '' ) ) continue
const item = sources . get ( source . taskNo ) ? ? { task : source , warehouses : [ ] }
if ( ! item . warehouses . includes ( row . warehouseCode ) ) item . warehouses . push ( row . warehouseCode )
sources . set ( source . taskNo , item )
}
return [ ... tasks . values ( ) ]
return [ ... sources . 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 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 ( ) ] } ,
{ key : 'dataFamily' , label : '采集内容' , type : 'select' , width : 160 , options : [ { value : '' , label : '全部采集内容' } , { value : 'OUTBOUND' , label : '出库销量' } , { value : 'INVENTORY' , label : '产品库存' } ] } ,
{ key : 'taskNo' , label : '任务号定位' , type : 'input' , width : 320 , placeholder : '粘贴完整任务号;先选择仓库' } ,
] )
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 : '当前状态 ' , minWidth : 150 , slotName : 'execution' } ,
{ key : 'currentProgress' , label : '本仓处理进度' , minWidth : 300 , slotName : 'currentProgress' } ,
{ key : 'completion' , label : '完成情况' , minWidth : 170 , slotName : 'completion' } ,
{ key : 'publication' , label : '最近发布' , minWidth : 180 , slotName : 'publication' } ,
{ key : 'coverage' , label : '数据覆盖截至' , minWidth : 180 , slotName : 'coverage' } ,
{ key : 'freshness' , label : '数据时效' , width : 100 , slotName : 'freshness' } ,
{ key : 'configuration' , label : '操作' , width : 100 , fixed : 'left' , slotName : 'configuration' } ,
{ key : 'warehouse' , label : '仓库/采集内容 ' , minWidth : 160 , slotName : 'warehouse' } ,
{ key : 'execution ' , label : '当前活动 ' , minWidth : 220 , slotName : 'execution' , showOverflowTooltip : false } ,
{ key : 'publication ' , label : '最近可用数据 ' , minWidth : 220 , slotName : 'publication' , showOverflowTooltip : false } ,
{ key : 'coverage ' , label : '覆盖/新鲜度 ' , minWidth : 240 , slotName : 'coverage' , showOverflowTooltip : false } ,
{ key : 'dailySamples' , label : '库存日样本' , minWidth : 150 , slotName : 'dailySamples' } ,
]
const scope = computed ( ( ) => ( { sourceAccount , warehouseCode : warehouseCode . value } ) )
const scope = computed ( ( ) => ( { sourceAccount , warehouseCode : selected . value ? . warehouseCode || warehouseCode . value } ) )
const retryEligible = computed ( ( ) => Boolean ( canSync . value && selected . value ? . allowedActions . includes ( 'RETRY' ) && ! selected . value . actionBlockReason ) )
const retryAllowed = computed ( ( ) => taskRetryAllowed ( selected . value , canSync . value , scope . value , reason . value ) )
const columns : DataTableColumn [ ] = [
{ key : 'actions' , label : '操作' , width : 110 , fixed : 'left' , slotName : 'actions' } ,
{ prop : 'taskNo ' , label : '任务号 ' , minWidth : 230 } ,
{ key : 'family ' , label : '采集内容 ' , minWidth : 160 , slotName : 'family' } ,
{ prop : 'warehouseCode ' , label : '仓库 ' , minWidth : 120 } ,
{ key : 'r ang e' , label : '本任务数据时间段(北京时间) ' , minWidth : 280 , slotName : 'r ang e' , showOverflowTooltip : false } ,
{ key : 'status ' , label : '任务状态 ' , minWidth : 150 , slotName : 'status' } ,
{ key : 'publication' , label : '发布结果' , minWidth : 150 , slotName : 'publication' } ,
{ key : 'progress' , label : '当前阶段与进度' , minWidth : 310 , slotName : 'progress' } ,
{ key : 'completion' , label : '完成情况' , minWidth : 170 , slotName : 'completion' } ,
{ key : 'actions' , label : '操作' , width : 100 , fixed : 'left' , slotName : 'actions' } ,
{ key : 'identity ' , label : '任务/范围 ' , minWidth : 265 , slotName : 'identity' , showOverflowTooltip : false } ,
{ key : 'range ' , label : '数据时间段 ' , minWidth : 170 , slotName : 'range' , showOverflowTooltip : false } ,
{ key : 'progress ' , label : '当前阶段与进度 ' , minWidth : 220 , slotName : 'progress' , showOverflowTooltip : false } ,
{ key : 'adv anc e' , label : '最近进展 ' , minWidth : 180 , slotName : 'adv anc e' , showOverflowTooltip : false } ,
{ key : 'result ' , label : '执行/发布结果 ' , minWidth : 170 , slotName : 'result' , showOverflowTooltip : false } ,
]
const runColumns : DataTableColumn [ ] = [
{ prop : 'attemptNo' , label : '尝试序号' , width : 100 } ,
@@ -109,50 +118,72 @@ 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 shortDate ( value : string | null ) { return value ? beijingTime ( value ) . split ( ' ' ) [ 0 ] : '—' }
function advanceAge ( task : Task ) { const at = task . progressUpdatedAt || task . progress ? . progressUpdatedAt ; if ( ! at || ! observedAt . value ) return '' ; const minutes = Math . floor ( ( Date . parse ( observedAt . value ) - Date . parse ( at ) ) / 60000 ) ; return Number . isFinite ( minutes ) && minutes >= 0 ? ( minutes < 1 ? '本次读取前1分钟内推进' : ` 本次读取前 ${ minutes } 分钟推进 ` ) : '' }
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 ) : '—' }
function blockReason ( value : string | null | undefined ) { return value ? reasonsText ( [ value ] ) : '—' }
function budgetName ( key : string ) { return ( { pages : '分页HTTP' , detailHttp : '详情HTTP' , http : '全部HTTP保留额度' , confirmedHttp : '已确认真实HTTP' , distinctDetails : '不同详情对象' , maxPages : '分页HTTP上限' , maxPageSize : '每页数量上限' , maxDistinctDetails : '不同详情对象上限' , maxDetailHttp : '详情HTTP上限' , maxInventoryPagesPerWarehouse : '每仓库存页上限' , maxHttp : '全部HTTP上限' } as Record < string , string > ) [ key ] ? ? '其他保护额度' }
function limitFor ( used : string , value : Validation | null ) { const key = ( { pages : 'maxPages' , detailHttp : 'maxDetailHttp' , http : 'maxHttp' , confirmedHttp : 'maxHttp' , distinctDetails : 'maxDistinctDetails' } as Record < string , string > ) [ used ] ; return key ? value ? . budgetLimits ? . [ key ] : null }
async function read ( ) {
if ( ! active || ! canList . value ) return false
async function read ( refreshHistory = false ) {
if ( ! active || ! canList . value || ! visible . value ) return false
const request = fence . begin ( )
loading . value = ! overview . value
try {
const nextOverview = await analyticsApi . overview ( sourceAccount , undefined , request . signal )
if ( ! request . current ( ) ) return false
const tasks = warehouseCode . value ? await analyticsApi . tasks ( { ... scope . value , current : current . value , size : size . value , ... upperBound . value } , request . signal ) : null
if ( ! request . current ( ) ) return false
overview . value = nextOverview
observedAt . value = nextOverview . observedAt
if ( tasks ) { rows . value = tasks . records ; total . value = tasks . total ; upperBound . value = { listUpperCreatedAt : tasks . listUpperCreatedAt , listUpperId : tasks . listUpperId } }
error . value = ''
errorDetails . value = null
return hasActiveTask ( [ ... rows . value , ... nextOverview . warehouses . flatMap ( row => [ row . currentTask , row . sharedUpstream ] . filter ( ( task ) : task is Task => Boolean ( task ) ) ) ] )
} catch ( cause ) { if ( request . current ( ) ) { errorDetails . value = getApiErrorDetails ( cause , '状态更新失败,保留最后读取数据' ) ; error . value = errorDetails . value . message } ; return hasActiveTask ( rows . value ) }
overview . value = nextOverview ; observedAt . value = nextOverview . observedAt
if ( warehouseCode . value && ( refreshHistory || ! historyLoaded . value ) ) {
const tasks = await analyticsApi . tasks ( { sourceAccount , warehouseCode : warehouseCode . value , dataFamily : dataFamily . value , current : current . value , size : size . value , ... upperBound . value } , request . signal )
if ( ! request . current ( ) ) return false
rows . value = tasks . records ; total . value = tasks . total ; historyLoaded . value = true
upperBound . value = { listUpperCreatedAt : tasks . listUpperCreatedAt , listUpperId : tasks . listUpperId }
}
error . value = '' ; errorDetails . value = null
if ( ! manualFocus . value && activityTasks . value [ 0 ] && focusNo . value !== activityTasks . value [ 0 ] . taskNo ) { resetFocusState ( ) ; selected . value = activityTasks . value [ 0 ] ; selectedDetail . value = null ; focusNo . value = selected . value . taskNo }
if ( focusNo . value && ! submitting . value ) await readFocus ( )
return isBusy ( )
} catch ( cause ) { if ( request . current ( ) ) { errorDetails . value = getApiErrorDetails ( cause , '状态更新失败,保留最后读取数据' ) ; error . value = errorDetails . value . message } ; return isBusy ( ) }
finally { if ( request . current ( ) ) loading . value = false }
}
async function selectTask ( task : Task , reset = true ) {
if ( ! active || ! task . taskNo || task . warehouseCode !== warehouseCode . value ) return
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 = ''
function isBusy ( ) { const tasks = [ ... activityTasks . value , ... ( overview . value ? . accountActivity ? [ overview . value . accountActivity ] : [ ] ) , ... ( selected . value ? [ selected . value ] : [ ] ) , ... ( selectedDetail . value ? . sharedUpstream ? [ selectedDetail . value . sharedUpstream ] : [ ] ) ] ; return hasActiveTask ( tasks ) || tasks . some ( t => [ 'READY' , 'APPLYING' ] . includes ( t . taskStatus ) ) }
async function readFocus ( ) {
const taskNo = focusNo . value , warehouse = selected . value ? . warehouseCode || warehouseCode . value
if ( ! active || ! visible . value || ! taskNo || ! warehouse ) return
const request = detailFence . begin ( ) ; detailLoading . value = true
try {
const detail = await analyticsApi . task ( scope . value , task . taskNo , request . signal )
if ( ! request . current ( ) ) return
const history = await analyticsApi . runs ( { ... scope . value , taskNo : task . taskNo , current : attemptsPage . value , size : attemptsSize . value , ... attemptsUpperBound . value } , request . signal )
if ( ! request . current ( ) ) return
if ( detail . task . taskNo !== task . taskNo || detail . task . warehouseCode !== warehouseCode . value ) throw new Error ( '任务归属不匹配' )
selected . value = detail . task
selectedDetail . value = detail
attempts . value = history . records
attemptsTotal . value = history . total
attemptsUpperBound . value = { listUpperCreatedAt : history . listUpperCreatedAt , listUpperId : history . listUpperId }
} catch ( cause ) { if ( request . current ( ) ) detailError . value = getApiErrorMessage ( cause , '任务详情更新失败,保留上次详情' ) }
const detail = await analyticsApi . task ( { sourceAccount , warehouseCode : warehouse } , taskNo , request . signal )
if ( ! request . current ( ) || taskNo !== focusNo . value ) return
if ( detail . task . taskNo !== taskNo || detail . task . warehouseCode !== warehouse ) throw new Error ( '任务归属不匹配,请核对所选仓库' )
selected . value = detail . task ; selectedDetail . value = detail ; detailError . value = '' ; detailErrorDetails . value = null
} catch ( cause ) { if ( request . current ( ) ) { detailErrorDetails . value = getApiErrorDetails ( cause , selectedDetail . value ? '关注任务状态更新失败,保留最后有效观测' : '任务首次读取失败,请核对仓库和完整任务号后重试' ) ; detailError . value = detailErrorDetails . value . message } }
finally { if ( request . current ( ) ) detailLoading . value = false }
}
async function readAttempts ( ) {
if ( ! active || ! visible . value || ! selected . value || ! historyOpen . value || ! Number ( selected . value . attemptCount ) ) return
const taskNo = selected . value . taskNo , request = historyFence . begin ( )
try {
const history = await analyticsApi . runs ( { ... scope . value , taskNo , current : attemptsPage . value , size : attemptsSize . value , ... attemptsUpperBound . value } , request . signal )
if ( ! request . current ( ) || taskNo !== selected . value ? . taskNo ) return
attempts . value = history . records ; attemptsTotal . value = history . total
attemptsUpperBound . value = { listUpperCreatedAt : history . listUpperCreatedAt , listUpperId : history . listUpperId }
} catch ( cause ) { if ( request . current ( ) ) detailError . value = getApiErrorMessage ( cause , '执行记录读取失败' ) }
}
async function selectTask ( task : Task , reset = true ) {
if ( ! active || ! task . taskNo || ( warehouseCode . value && task . warehouseCode !== warehouseCode . value ) ) return
if ( reset ) { manualFocus . value = true ; selected . value = task ; focusNo . value = task . taskNo ; selectedDetail . value = null ; resetFocusState ( ) ; taskOpen . value = true ; syncFocusRoute ( task . warehouseCode , task . taskNo ) }
await readFocus ( )
}
function resetFocusState ( ) { detailFence . cancel ( ) ; historyFence . cancel ( ) ; actionFence . cancel ( ) ; detailError . value = '' ; detailErrorDetails . value = null ; submitting . value = false ; attempts . value = [ ] ; attemptsTotal . value = 0 ; attemptsPage . value = 1 ; attemptsUpperBound . value = null ; historyOpen . value = false ; reason . value = '' ; confirming . value = false ; actionFeedback . value = '' }
function syncFocusRoute ( warehouse : string , taskNo : string ) { if ( route . query . warehouseCode !== warehouse || route . query . taskNo !== taskNo ) void router . push ( { path : '/self-operated/collection' , query : { warehouseCode : warehouse , ... ( taskNo ? { taskNo } : { } ) } } ) }
async function locateTask ( ) {
const taskNo = String ( filters . value . taskNo ? ? '' ) . trim ( )
if ( ! taskNo ) return
if ( ! warehouseCode . value ) { detailError . value = '请先选择仓库,再定位完整任务号' ; return }
manualFocus . value = true ; focusNo . value = taskNo ; selected . value = null ; selectedDetail . value = null ; resetFocusState ( ) ; syncFocusRoute ( warehouseCode . value , taskNo ) ; await readFocus ( )
}
async function returnActivity ( ) { manualFocus . value = false ; focusNo . value = '' ; selected . value = null ; selectedDetail . value = null ; filters . value . taskNo = '' ; resetFocusState ( ) ; syncFocusRoute ( warehouseCode . value , '' ) ; await read ( ) }
async function submitRetry ( ) {
if ( ! confirming . value || ! retryAllowed . value || submitting . value || ! selected . value ) return
const target = selected . value
@@ -160,7 +191,7 @@ async function submitRetry() {
submitting . value = true
actionFeedback . value = ''
try {
const accepted = await analyticsApi . retry ( retryIntent . create ( target . taskNo , warehouseCode . value , reason . value ) , request . signal )
const accepted = await analyticsApi . retry ( retryIntent . create ( target . taskNo , target . warehouseCode , reason . value ) , request . signal )
if ( ! request . current ( ) ) return
if ( accepted . taskNo !== target . taskNo ) throw new Error ( '受理任务号不匹配' )
retryIntent . accepted ( )
@@ -171,21 +202,26 @@ async function submitRetry() {
} catch ( cause ) { if ( request . current ( ) ) actionFeedback . value = getApiErrorMessage ( cause , '恢复请求失败;再次提交相同内容将沿用原幂等键' ) }
finally { if ( request . current ( ) ) submitting . value = false }
}
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 ( ) }
function openAnalysis ( path : string ) { void router . push ( { path , query : { warehouseCode : warehouseCode . value } } ) }
const poller = new RuntimePoller ( ( ) => read ( ) , ( ) => { fence . cancel ( ) ; detailFence . cancel ( ) ; historyFence . cancel ( ) } )
function visibility ( ) { visible . value = ! document . hidden ; poller . setVisible ( visible . value ) }
function refresh ( ) { upperBound . value = null ; current . value = 1 ; historyLoaded . value = false ; if ( String ( filters . value . taskNo ? ? '' ) . trim ( ) ) void locateTask ( ) ; void read ( true ) }
function openAnalysis ( path : string ) { void router . push ( { path , query : { warehouseCode : warehouseCode . value || selected . value ? . warehouseCode } } ) }
watch ( [ ( ) => route . path , ( ) => route . query ] , ( ) => {
if ( route . path && route . path !== '/self-operated/collection' ) return
filters . value . warehouseCode = typeof route . query . warehouseCode === 'string' ? route . query . warehouseCode : ''
const warehouse = typeof route . query . warehouseCode === 'string' ? route . query . warehouseCode : ''
const taskNo = typeof route . query . taskNo === 'string' ? route . query . taskNo : ''
if ( warehouse === warehouseCode . value && taskNo === focusNo . value ) return
filters . value . warehouseCode = warehouse ; filters . value . taskNo = taskNo
selected . value = null ; selectedDetail . value = null ; focusNo . value = taskNo ; manualFocus . value = Boolean ( taskNo ) ; resetFocusState ( ) ; void read ( )
} )
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 ( [ warehouseCode , dataFamily ] , ( ) => { configOpen . value = false ; taskOpen . value = false ; fence . cancel ( ) ; resetFocusState ( ) ; selected . value = null ; selectedDetail . value = null ; focusNo . value = String ( filters . value . taskNo ? ? '' ) . trim ( ) ; manualFocus . value = Boolean ( focusNo . value ) ; rows . value = [ ] ; total . value = 0 ; upperBound . value = null ; historyLoaded . value = false ; current . value = 1 ; void read ( ) } )
watch ( taskOpen , open => { if ( ! open ) { historyFence . cancel ( ) ; historyOpen . value = false ; confirming . value = false ; reason . value = '' ; actionFeedback . value = '' } } )
watch ( [ current , size ] , ( ) => { historyLoaded . value = false ; void read ( true ) } )
watch ( [ attemptsPage , attemptsSize ] , ( ) => { void readAttempts ( ) } )
watch ( historyOpen , open => { if ( open ) void readAttempts ( ) ; else historyFence . cancel ( ) } )
watch ( reason , ( ) => { confirming . value = false } )
function resume ( ) { if ( active ) return ; active = true ; visibility ( ) ; poller . start ( ) }
function suspend ( ) { active = false ; poller . stop ( ) ; actionFence . cancel ( ) ; configOpen . value = false ; taskOpen . value = false ; loading . value = false ; detailLoading . value = false ; submitting . value = false }
function suspend ( ) { active = false ; visible . value = false ; poller . stop ( ) ; actionFence . cancel ( ) ; configOpen . value = false ; taskOpen . value = false ; loading . value = false ; detailLoading . value = false ; submitting . value = false }
onMounted ( ( ) => { resume ( ) ; document . addEventListener ( 'visibilitychange' , visibility ) } )
onActivated ( resume )
onDeactivated ( suspend )
@@ -195,75 +231,67 @@ onUnmounted(() => { suspend(); document.removeEventListener('visibilitychange',
< AdminPage class = "admin-density" >
< p v-if = "!canList" role="alert" > 没有采集状态查看权限 。 < / p >
< ListPageLayout v-else >
< CollectionAccountMonitor :overview = "overview" :loading = "loading" :stale = "Boolean(error)" :visible = "visible" @refresh ="read()" / >
< details class = "collection-secondary-tools" > < summary > 历史记录与排查 < / summary >
< div class = "collection-workspace" >
< DataTable appearance = "integrated" :data = "warehouses" :columns = "overviewColumns" :loading = "loading" empty -text = " 暂无仓库采集状态 " >
< template # toolbar -start > < h2 > 当前采集状态 < / h2 > < / template >
< 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 >
< div v-for = "entry in preparationTasks" :key="entry.task.taskNo" class="collection-preparation" role="status" aria-label="来源数据准备任务" >
< strong > 来源数据准备 < / strong > < span > { { entry . warehouseCodes . join ( '、' ) } } 使用这批数据 < / span > < span > { { taskStateText ( entry . task ) } } < / span > < span > { { taskProgressText ( entry . task ) } } < / span >
< small > 任务 { { entry . task . taskNo } } ; 这里的单数是来源账号的数据准备进度 , 不是各仓出库量 , 也不是各仓完成率 。 < / small >
< / 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 }" > { { taskStateText ( asWarehouse ( row ) . currentTask ) } } < / template >
< template # currentProgress = "{ row }" > { { taskProgressText ( asWarehouse ( row ) . currentTask ) } } < / template >
< template # completion = "{ row }" > { { taskCompletionText ( asWarehouse ( row ) . currentTask ) } } < / 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 # toolbar -start > < h2 > 采集任务记录 < / h2 > < / template >
< template # notices > < p class = "collection-history-note" > 这里包含当前任务和历史任务 。 每行对应一个任务及数据时间段 ; 同一仓库出现多条出库销量记录 , 不代表这些任务同时执行 。 当前运行情况请看上方 “ 当前采集状态 ” 。 < / p > < / template >
< template # actions = "{ row }" > < el-button link :disabled = "!asTask(row).taskNo" @click ="selectTask(asTask(row))" > 详情 / 恢复 < / el -button > < / template >
< template # family = "{ row }" > { { family ( asTask ( row ) . dataFamily ) } } < small class = "collection-secondary" > { { asTask ( row ) . taskType === 'BACKFILL' ? '补采历史数据' : asTask ( row ) . taskType === 'SCHEDULED' ? '定时更新' : label ( asTask ( row ) . executionPurpose ) } } < / small > < / template >
< template # range = "{ row }" > { { beijingTime ( asTask ( row ) . queryFrom ) } } < br / > 至 { { beijingTime ( asTask ( row ) . queryUntil ) } } < / template >
< template # status = "{ row }" > { { taskStateText ( asTask ( row ) ) } } < / template >
< template # publication = "{ row }" > { { label ( asTask ( row ) . resultPublicationState ) } } < / template >
< template # progress = "{ row }" > { { taskProgressText ( asTask ( row ) ) } } < / template >
< template # completion = "{ row }" > { { taskCompletionText ( asTask ( row ) ) } } < / template >
< / DataTable >
< 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 > 状态 : { { taskStateText ( selected ) } } ; 结果 : { { label ( selected . resultPublicationState ) } } ; 已执行 { { selected . attemptCount } } 次 。 < / p > < p > 进度 : { { taskProgressText ( selected ) } } ; { { taskCompletionText ( selected ) } } 。 < / p >
< p > 目标 { { beijingTime ( selected . queryFrom ) } } 至 { { beijingTime ( selected . queryUntil ) } } ; 下次重试 { { beijingTime ( selected . nextRetryAt ) } } ; { { selected . actionBlockReason ? blockReason ( selected . actionBlockReason ) : ( selected . latestAttempt ? . failureSummary ? ? '' ) } } < / p >
< TaskExecutionFacts :task = "selected" scope -label = " 本仓任务 " / >
< details v-if = "selected.progress?.stages?.length"><summary>分阶段记录</summary><p v-for="(stage, index) in selected.progress.stages" :key="index" > {{ progressText ( stage ) }} < / p > < / details >
< section v-if = "selectedDetail?.sharedUpstream" class="collection-upstream" aria-label="数据准备任务详情" >
< h3 > 数据准备任务 : { { selectedDetail . sharedUpstream . taskNo } } < / h3 >
< p > { { taskStateText ( selectedDetail . sharedUpstream ) } } ; { { taskProgressText ( selectedDetail . sharedUpstream ) } } ; { { taskCompletionText ( selectedDetail . sharedUpstream ) } } 。 < / p >
< TaskExecutionFacts :task = "selectedDetail.sharedUpstream" scope -label = " 数据准备任务 " / >
< p > 系统先读取来源账号的数据 , 再按仓库分别校验 、 计算和发布 。 以上是前面的数据准备进度 , 可能包含其他仓库 ; 本仓是否成功以结果发布为准 , 不把该进度计入本仓尝试次数 。 < / p >
< / section >
< DataTable appearance = "integrated" v -model :page = "attemptsPage" v -model :page-size = "attemptsSize" :data = "attempts" :columns = "runColumns" :total = "attemptsTotal" :loading = "detailLoading" empty -text = " 尚无执行尝试 " >
< template # status = "{ row }" > { { label ( asRun ( row ) . status ) } } < / template > < template # progress = "{ row }" > { { progressText ( asRun ( row ) . progress ) } } < / template > < template # startedAt = "{ row }" > { { beijingTime ( asRun ( row ) . startedAt ) } } < / template > < template # finishedAt = "{ row }" > { { beijingTime ( asRun ( row ) . finishedAt ) } } < / template >
< DataTable appearance = "integrated" : data = "warehouses.filter(row => !dataFamily || row.dataFamily === dataFamily) " :columns = "overviewColumns" :loading = "loading" empty -text = " 暂无仓库采集状态 " >
< template # toolbar -start > < h2 > 各仓数据可用情况 < / h2 > < / template >
< template # toolbar -actions > < el-button : disabled = "!warehouseCode && !selected " @click ="openAnalysis('/self-operated/sales')" > 查看销量 < / el -button > < el-button : disabled = "!warehouseCode && !selected " @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 > { { overview ? . collectionMode === 'OFF' ? '自动采集已关闭,当前仅查看已留存结果。' : ` 采集模式: ${ label ( overview ? . collectionMode ) } ` } } < / span > < span v-if = "hasSyntheticSamples">包含合成测试样本,不代表真实采集覆盖。</span><span class="collection-observed" > 状态读取 {{ beijingTime ( observedAt ) }} ( 北京时间 ) < / span > < / div >
< section class = "collection-focus" aria -label = " 当前关注任务 " >
< div class = "density-actions" > < h2 > { { manualFocus ? '当前关注任务 · 手动选择/历史记录' : '当前关注任务' } } < / h2 > < el-button v-if = "selected" @click="taskOpen = true">查看关注任务详情</el-button><el-button v-if="manualFocus" @click="returnActivity" > 返回当前活动 < / el -button > < / div >
< p v-if = "focusNo" class="collection-task-no" > {{ focusNo }} < / p >
< div v-if = "activityTasks.length > 1" class="density-actions"><span>本范围 {{ activityTasks.length }} 个活动/等待任务</span><el-button v-for="task in activityTasks" :key="task.taskNo" :aria-label="`关注 ${task.taskNo}`" @click="selectTask(task) " > {{ task.taskNo }} < / el -button > < / div >
< p v-if = "detailLoading && !selectedDetail">正在读取关注任务…</p><p v-if="detailError" role="alert">{{ detailError }}<ApiErrorMeta v-if="detailErrorDetails" :details="detailErrorDetails" / > < / p >
< CollectionTaskProgress v-if = "selected" :task="selected" :detail="selectedDetail" :warehouse="focusWarehouse" :stale="Boolean(error || detailError)" :visible="visible" :observed-at="observedAt" / >
< p v-else-if = "!detailLoading && !detailError" > {{ overview ? ' 暂无已确认的活动任务 ; 可选择仓库后定位完整任务号或查看历史记录 。 ' : ' 尚未读取采集任务 。 ' }} < / p >
< / section >
< section v-for = "source in sharedSources" :key="source.task.taskNo" aria-label="来源数据准备任务" class="collection-preparation" >
< template v-if = "source.task.taskNo === selectedDetail?.sharedUpstream?.taskNo" > 关注任务的关联来源 < / template > < template v-else > 关联来源 {{ source.task.taskNo }} · 账号共享 · {{ taskStateText ( source.task ) }} · {{ taskProgressText ( source.task ) }} < / template > ; 关联仓库 {{ source.warehouses.join ( ' 、 ' ) }} 。 共享数量只记一次 , 不计入各仓完成量 。
< / section >
< p v-if = "overview?.accountActivity && overview.accountActivity.taskNo !== selectedDetail?.sharedUpstream?.taskNo && !sharedSources.some(source => source.task.taskNo === overview?.accountActivity?.taskNo)" class="collection-preparation "> 账号正在执行其他采集 : {{ overview.accountActivity.taskNo }} · {{ taskStateText ( overview.accountActivity ) }} 。 与当前任务的依赖尚未确认 , 不作为本任务进度 。 < / p >
< / 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 } } < small class = "collection-secondary" > { { family ( asWarehouse ( row ) . dataFamily ) } } · { { asWarehouse ( row ) . executionEnabled ? '采集已启用' : '采集已关闭' } } < / small > < / template >
< template # execution = "{ row }" > { { asWarehouse ( row ) . activeTasks ? . length ? ` ${ asWarehouse ( row ) . activeTasks ! . length } 个活动/等待任务 ` : '暂无已确认活动' } } < small v-if = "asWarehouse(row).currentTask" class="collection-secondary" > {{ taskStateText ( asWarehouse ( row ) .currentTask ) }} · {{ taskProgressText ( asWarehouse ( row ) .currentTask ) }} < / small > < / template >
< template # publication = "{ row }" > { { asWarehouse ( row ) . lastPublishedAt ? beijingTime ( asWarehouse ( row ) . lastPublishedAt ) : '尚无可用结果' } } < small class = "collection-secondary" > 本次任务 : { { label ( asWarehouse ( row ) . currentTask ? . resultPublicationState ) } } < / small > < / template >
< template # coverage = "{ row }" > { { asWarehouse ( row ) . dataFamily === 'INVENTORY' ? '库存观测截至' : '销量覆盖截至(不含)' } } { { beijingTime ( asWarehouse ( row ) . dataFamily === 'INVENTORY' ? asWarehouse ( row ) . inventoryObservedUntil : asWarehouse ( row ) . salesCoveredUntil ) } } < small class = "collection-secondary" > { { label ( asWarehouse ( row ) . dataFreshness ) } } < / small > < / 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 >
< form v-if = "canSync" class="density-form" @submit.prevent="submitRetry" >
< label for = "retry-reason" > 恢复原因 < / label > < el-input id = "retry-reason" v-model = "reason" type="textarea" placeholder="请说明已完成的修复及恢复原任务的原因" :maxlength="500" :disabled="submitting" / >
< p v-if = "!selected.allowedActions.includes('RETRY') || selected.actionBlockReason " > {{ selected.actionBlockReason ? blockReason ( selected.actionBlockReason ) : ' 当前任务不允许恢复 ' }} < / p >
< el -button v-if = "!confirming" :disabled="!retryAllowed || submitting" @click="confirming = true" > 核对恢复请求 < / el -button >
< template v-else > < p > 确认恢复任务 { { selected . taskNo } } ( 仓库 { { selected . warehouseCode } } ) , 范围保持不变 , 原因 : { { reason } } 。 受理后仍须等待执行与发布 。 < / p > < el-button : disabled = "!retryAllowed || submitting" :loading = "submitting" native -type = " submit " type = "primary" > 确认提交恢复请求 < / el-button > < el-button :disabled = "submitting" @click ="confirming = false" > 返回修改 < / el -button > < / template >
< / form >
< p v-else > 没有任务恢复权限 。 < / p > < p v-if = "actionFeedback" role="status " > {{ actionFeedback }} < / p >
< / section >
< / AppDialog >
< 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 # toolbar -start > < h2 > 采集任务记录 < / h2 > < / template >
< template # notices > < p class = "collection-history-note " > 历史记录不代表同时执行 ; 轮询保留当前分页和关注任务 。 < / p > < / template >
< template # actions = "{ row }" > < el-button link :disabled = "!asTask(row).taskNo" @click ="selectTask(asTask(row))" > 查看详情 < / el -button > < / template >
< template # identity = "{ row }" > < span class = "collection-task-no" > { { asTask ( row ) . taskNo } } < / span > < small class = "collection-secondary" > { { asTask ( row ) . warehouseCode } } · { { family ( asTask ( row ) . dataFamily ) } } · { { label ( asTask ( row ) . executionPurpose ) } } < / small > < / template >
< template # range = "{ row }" > { { shortDate ( asTask ( row ) . queryFrom ) } } 至 { { shortDate ( asTask ( row ) . queryUntil ) } } < small class = "collection-secondary" > 结束时刻不含 ; 精确范围见详情 < / small > < / template >
< template # progress = "{ row }" > { { taskStateText ( asTask ( row ) ) } } < small class = "collection-secondary " > { { taskProgressText ( asTask ( row ) ) } } < / small > < / template >
< template # advance = "{ row }" > { { asTask ( row ) . attemptCount ? beijingTime ( asTask ( row ) . progressUpdatedAt || asTask ( row ) . progress ? . progressUpdatedAt ) : '本仓尚未开始执行' } } < small class = "collection-secondary" > { { advanceAge ( asTask ( row ) ) } } < / small > < / template >
< template # result = "{ row }" > { { taskStateText ( asTask ( row ) ) } } · { { label ( asTask ( row ) . resultPublicationState ) } } < / template >
< / DataTable >
< AppDialog v-model = "taskOpen" class="admin-density-dialog" :title="`采集任务详情 · ${selected?.taskNo || focusNo || '—'}`" width="1200px" :loading="detailLoading" :confirm-loading="submitting" :show-confirm="false" cancel-text="关闭" fullscreenable >
< section v-if = "selected" class="collection-details admin-density" aria-label="选中任务详情" >
< p v-if = "detailError" role="alert">{{ detailError }}<ApiErrorMeta v-if="detailErrorDetails" :details="detailErrorDetails" / > < / p >
< CollectionTaskProgress :task = "selected" :detail = "selectedDetail" :warehouse = "focusWarehouse" : stale = "Boolean(error || detailError)" : visible = "visible && taskOpen" :observed-at = "observedAt" / >
< p v-if = "!Number(selected.attemptCount)" > 本仓尚未开始执行 ; 来源准备进度见上方 。 < / p >
< details v-else :open = "historyOpen" @toggle ="historyOpen = ($event.target as HTMLDetailsElement).open" > < summary > 执行记录 ( { { selected . attemptCount } } 次 ) < / summary >
< DataTable v-if = "historyOpen" appearance="integrated" v-model:page="attemptsPage" v-model:page-size="attemptsSize" :data="attempts" :columns="runColumns" :total="attemptsTotal" empty-text="尚无已读取的执行记录" >
< template # status = "{ row }" > { { label ( asRun ( row ) . status ) } } < / template > < template # progress = "{ row }" > { { progressText ( asRun ( row ) . progress ) } } < / template > < template # startedAt = "{ row }" > { { beijingTime ( asRun ( row ) . startedAt ) } } < / template > < template # finishedAt = "{ row }" > { { beijingTime ( asRun ( row ) . finishedAt ) } } < / template >
< / DataTable >
< / details >
< details > < summary > 校验与诊断 < / summary > < TaskExecutionFacts :task = "selected" scope -label = " 本仓任务 " / > < p > 操作资格 : { { selected . actionBlockReason ? blockReason ( selected . actionBlockReason ) : '以服务端允许操作为准' } } ; 下次重试 { { beijingTime ( selected . nextRetryAt ) } } 。 < / p > < p > 账号冷却 { { beijingTime ( selectedDetail ? . protection . accountCooldownUntil ) } } ; { { selectedDetail ? . protection . accountPauseReason ? ? '未提供暂停原因' } } < / p > < p v-for = "(stage, index) in selected.progress?.stages ?? []" :key="index">{{ progressText(stage) }}</p><TaskExecutionFacts v-if="selectedDetail?.sharedUpstream && selected.progressSourceTaskNo === selectedDetail.sharedUpstream.taskNo" :task="selectedDetail.sharedUpstream" scope-label="账号共享来源" / > < / details >
< form v-if = "retryEligible" class="density-form" @submit.prevent="submitRetry" >
< label for = "retry-reason" > 恢复原因 < / label > < el-input id = "retry-reason" v-model = "reason" type="textarea" placeholder="请说明已完成的修复及恢复原任务的原因" :maxlength="500" :disabled="submitting" / >
< el-button v-if = "!confirming" :disabled="!retryAllowed || submitting" @click="confirming = true" > 核对恢复请求 < / el -button >
< template v-else > < p > 确认恢复任务 { { selected . taskNo } } ( 仓库 { { selected . warehouseCode } } ) , 范围保持不变 , 原因 : { { reason } } 。 受理后仍须等待执行与发布 。 < / p > < el-button : disabled = "!retryAllowed || submitting" :loading = "submitting" native -type = " submit " type = "primary" > 确认提交恢复请求 < / el-button > < el-button :disabled = "submitting" @click ="confirming = false" > 返回修改 < / el -button > < / template >
< / form >
< p v-else-if = "!canSync">没有任务恢复权限。</p><p v-if="actionFeedback" role="status" > {{ actionFeedback }} < / p >
< / section >
< / AppDialog >
< / div >
< / details >
< / 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" >
@@ -278,7 +306,7 @@ onUnmounted(() => { suspend(); document.removeEventListener('visibilitychange',
< 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 = "首采目标范围 " > { { 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 >
@@ -306,6 +334,9 @@ onUnmounted(() => { suspend(); document.removeEventListener('visibilitychange',
< / AdminPage >
< / template >
< style scoped >
. collection - secondary - tools > summary { cursor : pointer ; color : var ( -- el - text - color - secondary ) ; padding : var ( -- admin - space - 3 ) 0 ; font - size : var ( -- admin - font - caption ) ; }
. collection - task - no { overflow - wrap : anywhere ; font - variant - numeric : tabular - nums ; }
. collection - focus { border - block : 1 px solid var ( -- el - border - color ) ; padding - block : var ( -- admin - space - 4 ) ; margin - block : var ( -- admin - space - 4 ) ; }
. 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 ) ; }