@@ -49,11 +49,11 @@
< label class = "schedule-switch" > < input v-model = "scheduleEnabled" type="checkbox" :disabled="pushing" @change="onScheduleToggle" / > < span > 启用多时间点调度 < / span > < / label >
< div v-if = "scheduleEnabled" class="schedule-config" >
< div v-for = "(value, index) in schedulePickerValues" :key="`schedule-${index}`" class="schedule-row" >
< el -tim e -picker :model-value = "value" value -format = " HH : mm : ss " format = "HH:mm" placeholder = "选择执行时间" :disabled = "pushing" :disabled-hours = "disablePastScheduleHours" :disabled-minutes = "disablePastScheduleMinutes" :disabled-seconds = "disableScheduleSeconds" class = "schedule-picker" @ update : model -value = " updateScheduleValue ( index , $ event ) " / >
< el -dat e -picker :model-value = "value" type = "datetime" value -format = " YYYY -MM -DD HH: mm : ss " format = "MM-DD HH:mm" placeholder = "选择执行时间" :disabled = "pushing" class = "schedule-picker" @ update : model -value = " updateScheduleValue ( index , $ event ) " / >
< button type = "button" class = "schedule-remove" : disabled = "pushing || schedulePickerValues.length <= 1" @click ="removeScheduleValue(index)" > 删除 < / button >
< / div >
< button type = "button" class = "schedule-add" :disabled = "pushing" @click ="addScheduleValue" > 添加时间点 < / button >
< p class = "hint schedule-hint" > 默认开启定时 ; 只需选择 24 小时制的时分 , 系统会自动按最近可执行时间顺序处理 。 < / p >
< p class = "hint schedule-hint" > 默认开启定时 ; 支持按 ` MM-DD HH:mm ` 配置多个时间点 , 前端不再限制选择更早时间 , 实际是否可执行以后端校验为准 。 < / p >
< / div >
< div class = "run-row" >
< button type = "button" class = "btn-run" :disabled = "matching" @click ="runMatch" > {{ matching ? ' 匹配中... ' : ' 匹配店铺 ' }} < / button >
@@ -108,7 +108,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
import { LISTING _FILTER _OPTIONS , type ListingFilterValue } from '@/pages/brand/components/listingFilters'
import { activateShopMatchTask , addShopMatchCandidate , createShopMatchTask , deleteShopMatchCandidate , deleteShopMatchHistory , deleteShopMatchTask , getShopMatchCountryPreference , getShopMatchDashboard , getShopMatchHistory , getShopMatchResultDownloadUrl , getShopMatchTasksBatch , listShopMatchCandidates , matchShopMatchShops , putShopMatchCountryPreference , type ShopMatchCandidateVo , type ShopMatchDashboardVo , type ShopMatchHistoryItem , type ShopMatchShopQueueItem , type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
import { activateShopMatchTask , addShopMatchCandidate , createShopMatchTask , deleteShopMatchCandidate , deleteShopMatchHistory , deleteShopMatchTask , getShopMatchCountryPreference , getShopMatchDashboard , getShopMatchHistory , getShopMatchResultDownloadUrl , getShopMatchTaskProgressBatch , getShopMatchTasksBatch , listShopMatchCandidates , matchShopMatchShops , putShopMatchCountryPreference , type ShopMatchCandidateVo , type ShopMatchDashboardVo , type ShopMatchHistoryItem , type ShopMatchShopQueueItem , type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
const COUNTRY _OPTIONS = [ { code : 'DE' , label : '德国' } , { code : 'UK' , label : '英国' } , { code : 'FR' , label : '法国' } , { code : 'IT' , label : '意大利' } , { code : 'ES' , label : '西班牙' } ] as const
@@ -128,7 +128,7 @@ const countryPrefSaving = ref(false)
const countryPrefUserTouched = ref ( false )
let countryPrefSaveTimer : ReturnType < typeof setTimeout > | null = null
const scheduleEnabled = ref ( true )
const schedulePickerValues = ref < string [ ] > ( [ getMinimum ScheduleTim e ( ) ] )
const schedulePickerValues = ref < string [ ] > ( [ getDefault ScheduleValu e ( ) ] )
const dashboard = ref < ShopMatchDashboardVo > ( { candidateCount : 0 , processedTaskCount : 0 , successTaskCount : 0 , failedTaskCount : 0 } )
const historyItems = ref < ShopMatchHistoryItem [ ] > ( [ ] )
const pollingTaskIds = ref < number [ ] > ( [ ] )
@@ -158,20 +158,27 @@ function rowKeyForMatch(row: ShopMatchShopQueueItem) { return `${(row.shopName |
function taskRowKey ( item : ShopMatchHistoryItem ) { return ` ${ normalizeTaskId ( item . taskId ) } - ${ item . resultId || 0 } - ${ item . shopName || '' } ` }
function countryLabel ( code : string ) { return COUNTRY _OPTIONS . find ( ( item ) => item . code === code ) ? . label || code }
function formatScheduleDateTime ( date : Date ) { const year = date . getFullYear ( ) ; const month = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ; const day = String ( date . getDate ( ) ) . padStart ( 2 , '0' ) ; const hours = String ( date . getHours ( ) ) . padStart ( 2 , '0' ) ; const minutes = String ( date . getMinutes ( ) ) . padStart ( 2 , '0' ) ; const seconds = String ( date . getSeconds ( ) ) . padStart ( 2 , '0' ) ; return ` ${ year } - ${ month } - ${ day } T ${ hours } : ${ minutes } : ${ seconds } ` }
function formatSchedul eTime ( date : Date ) { const hours = String ( date . getHours ( ) ) . padStart ( 2 , '0' ) ; const minute s = String ( date . getMinute s ( ) ) . padStart ( 2 , '0' ) ; const second s = String ( date . getSecond s ( ) ) . padStart ( 2 , '0' ) ; return ` ${ hours } : ${ minutes } : ${ seconds } ` }
function getMinimum ScheduleDat e ( ) { const date = new Date ( ) ; date . setSeconds ( 0 , 0 ) ; date . setMinutes ( date . getMinutes ( ) + 1 ) ; return date }
function getMinimum ScheduleTime( ) { return formatScheduleTime ( getMinimumScheduleDate ( ) ) }
function parseTimeParts ( value : string ) { const matched = value . match ( /^(\d{2}):(\d{2})(?::(\d{2}))?$/ ) ; if ( ! matched ) return null ; const hours = Number ( matched [ 1 ] ) ; const minutes = Number ( matched [ 2 ] ) ; const seconds = Number ( matched [ 3 ] || '0' ) ; if ( hours > 23 || minutes > 59 || seconds > 59 ) return null ; return { hours , minutes , seconds } }
function buildTodayTime ( parts : { hours : number ; minutes : number ; seconds : number } ) { const candidate = new Date ( ) ; candidate . setHours ( parts . hours , parts . minutes , parts . seconds , 0 ) ; return candidate }
function resolveScheduleDateTime ( value : string ) { const parts = parseTimeParts ( value ) ; if ( ! parts ) return null ; const minimum = getMinimumScheduleDate ( ) ; const candidate = new Date ( ) ; candidate . setHours ( parts . hours , parts . minutes , parts . seconds , 0 ) ; if ( candidate . getTime ( ) < minimum . getTime ( ) ) candidate . setDate ( candidate . getDate ( ) + 1 ) ; return candidate }
function nextMinuteString ( offsetMinutes : number ) { const date = getMinimumScheduleDate ( ) ; date . setMinutes ( date . getMinutes ( ) + Math . max ( 0 , offsetMinutes - 1 ) ) ; return formatScheduleTime ( date ) }
function nextMinuteAfter ( value : string , offsetMinutes : number ) { const date = resolveScheduleDateTime ( value ) ; if ( ! date ) return nextMinuteString ( offsetMinutes ) ; date . setMinutes ( date . getMinutes ( ) + offsetMinutes ) ; return for matS cheduleTime ( date ) }
function normalizeScheduleValue ( value : string | null | undefined , showWarning = false ) { if ( ! value ) return '' ; const parts = parseTimeParts ( value ) ; if ( ! parts ) return value ; const minimum = getMinimumScheduleDate ( ) ; const candidate = buildTodayTime ( parts ) ; if ( candidate . getTime ( ) < minimum . getTime ( ) ) { if ( showWarning ) ElMessage . warning ( '执行时间不能早于当前时间,已自动调整到当前时间后 1 分钟' ) ; return for matS cheduleTime ( minimum ) } return formatScheduleTime ( candidate ) }
function disablePastScheduleHours ( ) { const minimum = getMinimumScheduleDate ( ) ; return Array . from ( { length : minimum . getHours ( ) } , ( _ , index ) => index ) }
function disablePastScheduleMinutes ( selectedHour : number ) { const minimum = getMinimumScheduleDate ( ) ; if ( selectedH our != = minimum . getHours ( ) ) return [ ] ; return Array . from ( { length : minimum . getMinutes ( ) } , ( _ , index ) => index ) }
function disableScheduleSeconds ( ) { return Array . from ( { length : 59 } , ( _ , index ) => index + 1 ) }
function formatPickerDat eTime ( date : Date ) { const year = date . getFullYear ( ) ; const month = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ; const day = String ( date . getDate ( ) ) . padStart ( 2 , '0' ) ; const hour s = String ( date . getHour s ( ) ) . padStart ( 2 , '0' ) ; const minute s = String ( date . getMinute s ( ) ) . padStart ( 2 , '0' ) ; return ` ${ year } - ${ month } - ${ day } ${ hours } : ${ minutes } :00 ` }
function getDefault ScheduleValu e ( ) { const date = new Date ( ) ; date . setSeconds ( 0 , 0 ) ; date . setMinutes ( date . getMinutes ( ) + 1 ) ; return formatPickerDateTime ( date) }
function parse ScheduleDate Time( value ? : string | null ) {
if ( ! value ) return null
const normalized = value . trim ( ) . replace ( 'T' , ' ' )
const matched = normalized . match ( /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(?::(\d{2}))?$/ )
if ( ! matched ) return null
const year = Number ( matched[ 1 ] )
const month = Number ( matched[ 2 ] )
const day = Number ( matched [ 3 ] )
const h ours = Number ( matched [ 4 ] )
const minutes = Number ( matched [ 5 ] )
const seconds = Number ( matched [ 6 ] || '0' )
const date = new Date ( year , month - 1 , day , hours , minutes , seconds , 0 )
if ( Number . isNaN ( date . getTime ( ) ) ) return null
if ( date . getFullYear ( ) !== year || date . getMonth ( ) !== month - 1 || date . getDate ( ) !== day ) return null
return date
}
function nextScheduleValueAfter ( value : string , offsetMinutes : number ) { const date = parseScheduleDateTime ( value ) || new Date ( ) ; date . setSeconds ( 0 , 0 ) ; date . setMinutes ( date . getMinutes ( ) + offsetMinutes ) ; return formatPickerDateTime ( date ) }
function formatDateTime ( value ? : string ) { return value ? value . replace ( 'T' , ' ' ) . slice ( 0 , 19 ) : '-' }
function formatTimeOnly ( value ? : string ) { return value ? value . slice ( 11 , 16 ) : '-' }
function formatMonthDay Time ( value ? : string ) { const date = parseScheduleDateTime ( value ) ; if ( ! date ) return '-' ; const month = String ( date . getMonth ( ) + 1 ) . padStart ( 2 , '0' ) ; const day = String ( date . getDate ( ) ) . padStart ( 2 , '0' ) ; const hours = String ( date . getHours ( ) ) . padStart ( 2 , '0' ) ; const minutes = String ( date . getMinutes ( ) ) . padStart ( 2 , '0' ) ; return ` ${ month } - ${ day } ${ hours } : ${ minutes } ` }
function uniqueTaskRows ( rows : ShopMatchHistoryItem [ ] , snapshots : Record < number , ShopMatchTaskDetailVo > , section : 'current' | 'history' ) { const map = new Map < string , ShopMatchHistoryItem > ( ) ; for ( const row of rows ) map . set ( taskRowKey ( row ) , row ) ; for ( const [ taskIdText , snapshot ] of Object . entries ( snapshots ) ) { const taskId = normalizeTaskId ( taskIdText ) ; const first = snapshot . items ? . [ 0 ] ; if ( ! first || ! taskId ) continue ; const terminal = isTaskTerminalById ( taskId ) ; if ( ( section === 'current' && terminal ) || ( section === 'history' && ! terminal ) ) continue ; const key = taskRowKey ( { ... first , taskId } ) ; if ( ! map . has ( key ) ) map . set ( key , { ... first , taskId } ) } return Array . from ( map . values ( ) ) }
function loadPollingIdsFromStorage ( ) { try { const raw = window . localStorage . getItem ( sessionKey ( ) ) ; const parsed = raw ? JSON . parse ( raw ) : [ ] ; pollingTaskIds . value = Array . isArray ( parsed ) ? parsed . filter ( ( item ) : item is number => typeof item === 'number' && item > 0 ) : [ ] } catch { pollingTaskIds . value = [ ] } }
function savePollingIds ( ) { setStorageJson ( sessionKey ( ) , pollingTaskIds . value , pollingTaskIds . value . length === 0 ) }
@@ -228,36 +235,37 @@ function onCountryToggle(code: string, checked: boolean) { countryPrefUserTouche
function onCountryDragStart ( index : number ) { dragCountryIndex . value = index }
function onCountryDragEnd ( ) { dragCountryIndex . value = null }
function onCountryDrop ( toIndex : number ) { const from = dragCountryIndex . value ; dragCountryIndex . value = null ; if ( from == null || from === toIndex ) return ; countryPrefUserTouched . value = true ; const list = [ ... orderedCountryCodes . value ] ; const [ item ] = list . splice ( from , 1 ) ; list . splice ( toIndex , 0 , item ) ; orderedCountryCodes . value = list ; scheduleSaveCountryPreference ( ) }
function onScheduleToggle ( ) { if ( scheduleEnabled . value && schedulePickerValues . value . length === 0 ) schedulePickerValues . value = [ getMinimum ScheduleTim e ( ) ] ; if ( ! scheduleEnabled . value ) schedulePickerValues . value = [ ] }
function addScheduleValue ( ) { const last = schedulePickerValues . value [ schedulePickerValues . value . length - 1 ] ; const nextValue = last ? nextMinut eAfter ( last , 60 ) : getMinimum ScheduleTim e ( ) ; schedulePickerValues . value = [ ... schedulePickerValues . value , normalizeScheduleValue ( nextValue) ] }
function onScheduleToggle ( ) { if ( scheduleEnabled . value && schedulePickerValues . value . length === 0 ) schedulePickerValues . value = [ getDefault ScheduleValu e ( ) ] ; if ( ! scheduleEnabled . value ) schedulePickerValues . value = [ ] }
function addScheduleValue ( ) { const last = schedulePickerValues . value [ schedulePickerValues . value . length - 1 ] ; const nextValue = last ? nextScheduleValu eAfter ( last , 60 ) : getDefault ScheduleValu e ( ) ; schedulePickerValues . value = [ ... schedulePickerValues . value , nextValue ] }
function removeScheduleValue ( index : number ) { if ( schedulePickerValues . value . length <= 1 ) return ; schedulePickerValues . value = schedulePickerValues . value . filter ( ( _ , idx ) => idx !== index ) }
function updateScheduleValue ( index : number , value : string | null ) { const list = [ ... schedulePickerValues . value ] ; list [ index ] = value ? normalizeScheduleValue ( value , true ) : '' ; schedulePickerValues . value = list }
function updateScheduleValue ( index : number , value : string | null ) { const list = [ ... schedulePickerValues . value ] ; list [ index ] = value || '' ; schedulePickerValues . value = list }
function mergeMatchedItems ( base : ShopMatchShopQueueItem [ ] , incoming : ShopMatchShopQueueItem [ ] ) { const map = new Map < string , ShopMatchShopQueueItem > ( ) ; for ( const item of base ) map . set ( rowKeyForMatch ( item ) , item ) ; for ( const item of incoming ) map . set ( rowKeyForMatch ( item ) , item ) ; return Array . from ( map . values ( ) ) }
async function runMatch ( ) { const names = selectedCandidates . value . map ( ( item ) => item . shop _name ) . filter ( Boolean ) ; if ( ! names . length ) { ElMessage . warning ( '请先选择候选店铺' ) ; return } matching . value = true ; try { const data = await matchShopMatchShops ( names ) ; matchedItems . value = mergeMatchedItems ( matchedItems . value , data . items || [ ] ) ; saveMatchedItemsToStorage ( ) ; ElMessage . success ( ` 本次返回 ${ data . items ? . length || 0 } 条匹配结果 ` ) } catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '匹配失败' ) } finally { matching . value = false } }
function removeMatchedRow ( row : ShopMatchShopQueueItem ) { matchedItems . value = matchedItems . value . filter ( ( item ) => rowKeyForMatch ( item ) !== rowKeyForMatch ( row ) ) ; saveMatchedItemsToStorage ( ) }
function removeMatchedRowsLocally ( rows : ShopMatchHistoryItem [ ] | ShopMatchShopQueueItem [ ] ) { const keys = new Set ( rows . map ( ( row ) => rowKeyForMatch ( row as ShopMatchShopQueueItem ) ) ) ; matchedItems . value = matchedItems . value . filter ( ( item ) => ! keys . has ( rowKeyForMatch ( item ) ) ) ; saveMatchedItemsToStorage ( ) }
function parseScheduleValues ( ) { if ( ! scheduleEnabled . value ) return undefined ; const values = schedulePickerValues . value . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ; if ( ! values . length ) throw new Error ( '请至少选择一个执行时间' ) ; const withTime = values . map ( ( value ) => { const normalized = normalizeScheduleValue ( value ) ; const date = resolv eScheduleDateTime( normalized ) ; if ( ! date ) throw new Error ( ` 时间格式无效: ${ value } ` ) ; return { raw : formatScheduleDateTime ( date ) , time : date . getTime ( ) } } ) . sort ( ( a , b ) => a . time - b . time ) ; for ( let i = 1 ; i < withTime . length ; i += 1 ) if ( withTime [ i ] . time === withTime [ i - 1 ] . time ) throw new Error ( '执行时间不能重复' ) ; return withTime . map ( ( item ) => item . raw ) }
function parseScheduleValues ( ) { if ( ! scheduleEnabled . value ) return undefined ; const values = schedulePickerValues . value . map ( ( item ) => item . trim ( ) ) . filter ( Boolean ) ; if ( ! values . length ) throw new Error ( '请至少选择一个执行时间' ) ; const withTime = values . map ( ( value ) => { const date = pars eScheduleDateTime( value ) ; if ( ! date ) throw new Error ( ` 时间格式无效: ${ value } ` ) ; return { raw : formatScheduleDateTime ( date ) , time : date . getTime ( ) } } ) . sort ( ( a , b ) => a . time - b . time ) ; for ( let i = 1 ; i < withTime . length ; i += 1 ) if ( withTime [ i ] . time === withTime [ i - 1 ] . time ) throw new Error ( '执行时间不能重复' ) ; return withTime . map ( ( item ) => item . raw ) }
function buildQueuePayload ( taskId : number , item : Pick < ShopMatchHistoryItem , ' shopName ' | ' shopId ' | ' platform ' | ' companyName ' | ' matchStatus ' | ' matchMessage ' > , countryCodes : string [ ] , stageIndex ? : number , finalStage = true ) { return { type : 'shop-match-run' , ts : Date . now ( ) , data : { taskId , user _id : currentUserId ( ) , items : [ { shopName : item . shopName , shopId : item . shopId , platform : item . platform , companyName : item . companyName , matched : true , matchStatus : item . matchStatus , matchMessage : item . matchMessage } ] , country _codes : [ ... countryCodes ] , risk _listing _filter : shopMatchListingFilter . value , stage _index : stageIndex , final _stage : finalStage } } }
function nextScheduleStage ( taskId : number ) { const task = taskSnapshots . value [ taskId ] ? . task ; const stages = task ? . scheduleStages || [ ] ; const currentStageIndex = task ? . currentStageIndex ; if ( typeof currentStageIndex !== 'number' ) return null ; const stage = stages . find ( ( item ) => item . stageIndex === currentStageIndex ) ; if ( ! stage ? . scheduledAt ) return null ; return { stageIndex : currentStageIndex , scheduledAt : stage . scheduledAt , finalStage : currentStageIndex === stages . length - 1 } }
async function dispatchScheduledTask ( taskId : number , item : ShopMatchHistoryItem ) { const api = getPywebviewApi ( ) ; if ( ! api ? . enqueue _json ) throw new Error ( '当前客户端未提供 enqueue_json' ) ; const stage = nextScheduleStage ( taskId ) ; const snapshot = taskSnapshots . value [ taskId ] ; const countryCodes = snapshot ? . task ? . countryCodes || orderedCountryCodes . value ; if ( ! stage ) throw new Error ( ` 任务 ${ taskId } 缺少待执行阶段 ` ) ; clearDispatchTimer ( taskId ) ; await activateShopMatchTask ( taskId , stage . stageIndex ) ; taskDetails . value = { ... taskDetails . value , [ taskId ] : 'RUNNING' } ; if ( snapshot ? . task ) taskSnapshots . value = { ... taskSnapshots . value , [ taskId ] : { ... snapshot , task : { ... snapshot . task , status : 'RUNNING' , activeStageIndex : stage . stageIndex } } } ; saveTaskSnapshotsToStorage ( ) ; saveTaskDetailsToStorage ( ) ; const payload = buildQueuePayload ( taskId , item , countryCodes , stage . stageIndex , stage . finalStage ) ; queuePayloadText . value = JSON . stringify ( payload , null , 2 ) ; const result = await api . enqueue _json ( payload ) ; if ( ! result ? . success ) throw new Error ( result ? . error || ` 任务 ${ taskId } 推送失败 ` ) ; addPollingTask ( taskId ) ; queuePushResult . value = ` 任务 ${ taskId } 已推送第 ${ stage . stageIndex + 1 } 轮 ` ; ensurePolling ( true ) }
function hasRunningTask ( ) { return Object . values ( taskDetails . value ) . some ( ( status ) => status === 'RUNNING' ) || Object . values ( taskSnapshots . value ) . some ( ( detail ) => detail . task ? . status === 'RUNNING' ) }
function findNextReadyScheduledTask ( ) { const taskIds = new Set < number > ( pollingTaskIds . value ) ; for ( const key of Object . keys ( taskSnapshots . value ) ) { const id = normalizeTaskId ( key ) ; if ( id > 0 ) taskIds . add ( id ) } for ( const item of historyItems . value ) { const id = normalizeTaskId ( item . taskId ) ; if ( id > 0 ) taskIds . add ( id ) } const ready = Array . from ( taskIds ) . map ( ( taskId ) => { const snapshot = taskSnapshots . value [ taskId ] ; const taskStatus = taskDetails . value [ taskId ] || snapshot ? . task ? . status ; const item = snapshot ? . items ? . [ 0 ] || historyItems . value . find ( ( row ) => normalizeTaskId ( row . taskId ) === taskId ) ; const stage = nextScheduleStage ( taskId ) ; if ( taskStatus !== 'SCHEDULED' || ! item || ! stage ? . scheduledAt ) return null ; const scheduledTime = new Date ( stage . scheduledAt ) . getTime ( ) ; if ( ! Number . isFinite ( scheduledTime ) || scheduledTime > Date . now ( ) ) return null ; return { taskId , item , scheduledTime } } ) . filter ( ( item ) : item is { taskId : number ; item : ShopMatchHistoryItem ; scheduledTime : number } => ! ! item ) ; ready . sort ( ( a , b ) => a . scheduledTime - b . scheduledTime || a . taskId - b . taskId ) ; return ready [ 0 ] || null }
function scheduledTimestamp ( value ? : string ) { const date = parseScheduleDateTime ( value ) ; return date ? date . getTime ( ) : Number . NaN }
function findNextReadyScheduledTask ( ) { const taskIds = new Set < number > ( pollingTaskIds . value ) ; for ( const key of Object . keys ( taskSnapshots . value ) ) { const id = normalizeTaskId ( key ) ; if ( id > 0 ) taskIds . add ( id ) } for ( const item of historyItems . value ) { const id = normalizeTaskId ( item . taskId ) ; if ( id > 0 ) taskIds . add ( id ) } const ready = Array . from ( taskIds ) . map ( ( taskId ) => { const snapshot = taskSnapshots . value [ taskId ] ; const taskStatus = taskDetails . value [ taskId ] || snapshot ? . task ? . status ; const item = snapshot ? . items ? . [ 0 ] || historyItems . value . find ( ( row ) => normalizeTaskId ( row . taskId ) === taskId ) ; const stage = nextScheduleStage ( taskId ) ; if ( taskStatus !== 'SCHEDULED' || ! item || ! stage ? . scheduledAt ) return null ; const scheduledTime = scheduledTimestamp ( stage . scheduledAt ) ; if ( ! Number . isFinite ( scheduledTime ) || scheduledTime > Date . now ( ) ) return null ; return { taskId , item , scheduledTime } } ) . filter ( ( item ) : item is { taskId : number ; item : ShopMatchHistoryItem ; scheduledTime : number } => ! ! item ) ; ready . sort ( ( a , b ) => a . scheduledTime - b . scheduledTime || a . taskId - b . taskId ) ; return ready [ 0 ] || null }
async function pumpScheduledQueue ( ) { if ( scheduledDispatchInFlight . value || hasRunningTask ( ) ) return ; const readyTask = findNextReadyScheduledTask ( ) ; if ( ! readyTask ) return ; scheduledDispatchInFlight . value = true ; try { await dispatchScheduledTask ( readyTask . taskId , readyTask . item ) } catch ( error ) { const message = error instanceof Error ? error . message : ` 任务 ${ readyTask . taskId } 调度失败 ` ; const key = ` ${ readyTask . taskId } : ${ message } ` ; const now = Date . now ( ) ; queuePushResult . value = message ; if ( lastScheduledErrorKey !== key || now - lastScheduledErrorAt > 10000 ) { lastScheduledErrorKey = key ; lastScheduledErrorAt = now ; ElMessage . error ( message ) } await Promise . allSettled ( [ loadHistory ( ) , refreshTaskBatch ( ) ] ) } finally { scheduledDispatchInFlight . value = false } }
function scheduleDispatch ( taskId : number , scheduledAt ? : string ) { if ( ! scheduledAt ) return ; clearDispatchTimer ( taskId ) ; const time = new Date ( scheduledAt ) . getTime ( ) ; if ( ! Number . isFinite ( time ) ) return ; const delay = Math . max ( 0 , time - Date . now ( ) ) ; const timer = window . setTimeout ( ( ) => { dispatchTimers . delete ( taskId ) ; void pumpScheduledQueue ( ) } , delay ) ; dispatchTimers . set ( taskId , timer ) }
function scheduleDispatch ( taskId : number , scheduledAt ? : string ) { if ( ! scheduledAt ) return ; clearDispatchTimer ( taskId ) ; const time = scheduledTimestamp ( scheduledAt ) ; if ( ! Number . isFinite ( time ) ) return ; const delay = Math . max ( 0 , time - Date . now ( ) ) ; const timer = window . setTimeout ( ( ) => { dispatchTimers . delete ( taskId ) ; void pumpScheduledQueue ( ) } , delay ) ; dispatchTimers . set ( taskId , timer ) }
function restoreScheduledDispatches ( ) { const taskIds = new Set < number > ( pollingTaskIds . value ) ; for ( const key of Object . keys ( taskSnapshots . value ) ) { const id = normalizeTaskId ( key ) ; if ( id > 0 ) taskIds . add ( id ) } for ( const item of historyItems . value ) { const id = normalizeTaskId ( item . taskId ) ; if ( id > 0 ) taskIds . add ( id ) } for ( const taskId of taskIds ) { const snapshot = taskSnapshots . value [ taskId ] ; const taskStatus = snapshot ? . task ? . status || taskDetails . value [ taskId ] ; const stage = nextScheduleStage ( taskId ) ; if ( taskStatus === 'SCHEDULED' && stage ? . scheduledAt ) scheduleDispatch ( taskId , stage . scheduledAt ) ; else clearDispatchTimer ( taskId ) } void pumpScheduledQueue ( ) }
function startScheduleHeartbeat ( ) { if ( scheduleHeartbeatTimer ) return ; scheduleHeartbeatTimer = window . setInterval ( ( ) => { void pumpScheduledQueue ( ) } , 1000 ) }
function stopScheduleHeartbeat ( ) { if ( ! scheduleHeartbeatTimer ) return ; window . clearInterval ( scheduleHeartbeatTimer ) ; scheduleHeartbeatTimer = null }
function handleScheduleRecovery ( ) { restoreScheduledDispatches ( ) ; if ( pollingTaskIds . value . length ) ensurePolling ( true ) }
async function refreshTaskBatch ( ) { if ( ! pollingTaskIds . value . length ) return { settledTaskIds : [ ] as number [ ] } ; try { const batch = await getShopMatchTasksBatch ( pollingTaskIds . value ) ; const nextSnapshots = { ... taskSnapshots . value } ; const settledTaskIds = new Set < number > ( ) ; for ( const missingId of batch . missingTaskIds || [ ] ) { removeTaskLocally ( missingId ) ; delete nextSnapshots [ missingId ] ; settledTaskIds . add ( missingId ) } for ( const detail of batch . items || [ ] ) { const taskId = detail . task ? . id ; if ( ! taskId ) continue ; nextSnapshots [ taskId ] = detail ; const status = detail . task ? . status || '' ; if ( ! status ) continue ; taskDetails . value [ taskId ] = status ; if ( isTaskTerminalStatus ( status ) ) { removePollingTask ( taskId ) ; settledTaskIds . add ( taskId ) ; continue } if ( status === 'SCHEDULED' ) { stopPollingTask ( taskId ) ; settledTaskIds . add ( taskId ) } } taskSnapshots . value = nextSnapshots ; saveTaskSnapshotsToStorage ( ) ; saveTaskDetailsToStorage ( ) ; restoreScheduledDispatches ( ) ; return { settledTaskIds : Array . from ( settledTaskIds ) } } catch { return { settledTaskIds : [ ] as number [ ] } } }
function getPollIntervalMs ( ) { return document . visibilityState === 'visible' ? 4 000 : 1 0000 }
async function refreshTaskBatch ( ) { if ( ! pollingTaskIds . value . length ) return { settledTaskIds : [ ] as number [ ] } ; try { const batch = await getShopMatchTaskProgres sBatch ( pollingTaskIds . value ) ; const nextSnapshots = { ... taskSnapshots . value } ; const settledTaskIds = new Set < number > ( ) ; for ( const missingId of batch . missingTaskIds || [ ] ) { removeTaskLocally ( missingId ) ; delete nextSnapshots [ missingId ] ; settledTaskIds . add ( missingId ) } for ( const detail of batch . items || [ ] ) { const taskId = detail . task ? . id ; if ( ! taskId ) continue ; const prev = nextSnapshots [ taskId ] ; nextSnapshots [ taskId ] = prev ? { ... prev , task : { ... ( prev . task || { } ) , ... ( detail . task || { } ) } } : detail ; const status = detail . task ? . status || '' ; if ( ! status ) continue ; taskDetails . value [ taskId ] = status ; if ( isTaskTerminalStatus ( status ) ) { removePollingTask ( taskId ) ; settledTaskIds . add ( taskId ) ; continue } if ( status === 'SCHEDULED' ) { stopPollingTask ( taskId ) ; settledTaskIds . add ( taskId ) } } taskSnapshots . value = nextSnapshots ; saveTaskSnapshotsToStorage ( ) ; saveTaskDetailsToStorage ( ) ; restoreScheduledDispatches ( ) ; return { settledTaskIds : Array . from ( settledTaskIds ) } } catch { return { settledTaskIds : [ ] as number [ ] } } }
function getPollIntervalMs ( ) { return document . visibilityState === 'visible' ? 6 000 : 2 0000 }
function scheduleNextPoll ( immediate = false ) { if ( pollTimer . value ) { if ( ! immediate ) return ; window . clearTimeout ( pollTimer . value ) ; pollTimer . value = null } const run = async ( ) => { pollTimer . value = null ; if ( pollingInFlight . value || ! pollingTaskIds . value . length ) { if ( pollingTaskIds . value . length ) scheduleNextPoll ( ) ; return } pollingInFlight . value = true ; try { const { settledTaskIds } = await refreshTaskBatch ( ) ; if ( settledTaskIds . length ) await Promise . allSettled ( [ loadHistory ( ) , loadDashboard ( ) ] ) } finally { pollingInFlight . value = false } if ( pollingTaskIds . value . length ) pollTimer . value = window . setTimeout ( run , getPollIntervalMs ( ) ) } ; if ( immediate ) void run ( ) ; else pollTimer . value = window . setTimeout ( run , getPollIntervalMs ( ) ) }
function ensurePolling ( immediate = false ) { scheduleNextPoll ( immediate ) }
function stopPolling ( ) { if ( pollTimer . value ) { window . clearTimeout ( pollTimer . value ) ; pollTimer . value = null } }
async function waitForTaskTerminal ( taskId : number ) { while ( true ) { const batch = await getShopMatchTasksBatch ( [ taskId ] ) ; if ( ( batch . missingTaskIds || [ ] ) . includes ( taskId ) ) { removeTaskLocally ( taskId ) ; await refreshTaskViewsBestEffort ( ) ; restoreScheduledDispatches ( ) ; return 'FAILED' } const detail = ( batch . items || [ ] ) . find ( ( item ) => item . task ? . id === taskId ) ; const status = detail ? . task ? . status || '' ; if ( detail ) { taskSnapshots . value = { ... taskSnapshots . value , [ taskId ] : detail } ; saveTaskSnapshotsToStorage ( ) } if ( status ) { taskDetails . value [ taskId ] = status ; saveTaskDetailsToStorage ( ) } if ( isTaskTerminalStatus ( status ) ) { removePollingTask ( taskId ) ; await refreshTaskViewsBestEffort ( ) ; restoreScheduledDispatches ( ) ; return status } await new Promise < void > ( ( resolve ) => { window . setTimeout ( ( ) => resolve ( ) , getPollIntervalMs ( ) ) } ) } }
async function waitForTaskTerminal ( taskId : number ) { while ( true ) { const batch = await getShopMatchTaskProgres sBatch ( [ taskId ] ) ; if ( ( batch . missingTaskIds || [ ] ) . includes ( taskId ) ) { removeTaskLocally ( taskId ) ; await refreshTaskViewsBestEffort ( ) ; restoreScheduledDispatches ( ) ; return 'FAILED' } const detail = ( batch . items || [ ] ) . find ( ( item ) => item . task ? . id === taskId ) ; const status = detail ? . task ? . status || '' ; if ( detail ) { const prev = taskSnapshots . value [ taskId ] ; taskSnapshots . value = { ... taskSnapshots . value , [ taskId ] : prev ? { ... prev , task : { ... ( prev . task || { } ) , ... ( detail . task || { } ) } } : detail } ; saveTaskSnapshotsToStorage ( ) } if ( status ) { taskDetails . value [ taskId ] = status ; saveTaskDetailsToStorage ( ) } if ( isTaskTerminalStatus ( status ) ) { removePollingTask ( taskId ) ; await refreshTaskViewsBestEffort ( ) ; restoreScheduledDispatches ( ) ; return status } await new Promise < void > ( ( resolve ) => { window . setTimeout ( ( ) => resolve ( ) , getPollIntervalMs ( ) ) } ) } }
function resolvedTaskStatus ( item : ShopMatchHistoryItem ) { const taskId = normalizeTaskId ( item . taskId ) ; if ( ! taskId ) return item . taskStatus || '' ; return taskDetails . value [ taskId ] || taskSnapshots . value [ taskId ] ? . task ? . status || item . taskStatus || '' }
function taskSnapshotOf ( item : ShopMatchHistoryItem ) { const taskId = normalizeTaskId ( item . taskId ) ; return taskId ? taskSnapshots . value [ taskId ] : undefined }
function currentTaskStageText ( item : ShopMatchHistoryItem ) { const snapshot = taskSnapshotOf ( item ) ; const task = snapshot ? . task ; const stages = task ? . scheduleStages || [ ] ; if ( ! stages . length ) return '' ; const total = stages . length ; const activeIndex = typeof task ? . activeStageIndex === 'number' ? task . activeStageIndex : undefined ; const currentIndex = typeof task ? . currentStageIndex === 'number' ? task . currentStageIndex : undefined ; if ( typeof activeIndex === 'number' ) return ` 执行进度: 第 ${ activeIndex + 1 } / ${ total } 次执行中 ` ; if ( typeof currentIndex === 'number' ) return ` 执行进度: 等待第 ${ currentIndex + 1 } / ${ total } 次 ` ; return ` 执行进度: 共 ${ total } 次 ` }
function nextScheduledDisplay ( item : ShopMatchHistoryItem ) { const snapshot = taskSnapshotOf ( item ) ; const task = snapshot ? . task ; const stages = task ? . scheduleStages || [ ] ; if ( typeof task ? . currentStageIndex === 'number' ) { const stage = stages . find ( ( entry ) => entry . stageIndex === task . currentStageIndex ) ; if ( stage ? . scheduledAt ) return formatTimeOnly ( stage . scheduledAt ) } if ( item . scheduledAt ) return formatTimeOnly ( item . scheduledAt ) ; return '' }
function nextScheduledDisplay ( item : ShopMatchHistoryItem ) { const snapshot = taskSnapshotOf ( item ) ; const task = snapshot ? . task ; const stages = task ? . scheduleStages || [ ] ; if ( typeof task ? . currentStageIndex === 'number' ) { const stage = stages . find ( ( entry ) => entry . stageIndex === task . currentStageIndex ) ; if ( stage ? . scheduledAt ) return formatMonthDay Time ( stage . scheduledAt ) } if ( item . scheduledAt ) return formatMonthDay Time ( item . scheduledAt ) ; return '' }
function statusText ( item : ShopMatchHistoryItem ) { const status = resolvedTaskStatus ( item ) ; if ( status === 'SCHEDULED' ) return '待执行' ; if ( status === 'RUNNING' ) return '执行中' ; if ( status === 'SUCCESS' || status === 'COMPLETED' ) return '已完成' ; if ( status === 'FAILED' ) return '失败' ; return item . success ? '已完成' : '未知' }
function statusClass ( item : ShopMatchHistoryItem ) { const status = resolvedTaskStatus ( item ) ; return status === 'SUCCESS' || status === 'COMPLETED' ? 'success' : status === 'FAILED' ? 'failed' : 'running' }
function canDownload ( item : ShopMatchHistoryItem ) { const status = resolvedTaskStatus ( item ) ; return ! ! item . resultId && ! ! item . downloadUrl && ( status === 'SUCCESS' || status === 'COMPLETED' ) }