@@ -129,8 +129,11 @@ const pollTimer = ref<number | null>(null)
const pollingInFlight = ref ( false )
const dispatchTimers = new Map < number , number > ( )
const scheduledDispatchInFlight = ref ( false )
const currentSectionItems = computed ( ( ) => uniqueTaskRows ( historyItems . value . filter ( ( item ) => { const taskId = normalizeTaskId ( item . taskId ) ; return ! ! taskId && ! isTaskTerminalById ( taskId ) } ) , taskSnapshots . value , 'current' ) )
const historySectionItems = computed ( ( ) => uniqueTaskRows ( historyItems . value . filter ( ( item ) => { const taskId = normalizeTaskId ( item . taskId ) ; return ! taskId || isTaskTerminalById ( taskId ) } ) , taskSnapshots . value , 'history' ) )
let scheduleHeartbeatTimer : number | null = null
let lastScheduledErrorAt = 0
let lastScheduledErrorKey = ''
const currentSectionItems = computed ( ( ) => uniqueTaskRows ( historyItems . value . filter ( ( item ) => { const taskId = normalizeTaskId ( item . taskId ) ; return ! ! taskId && ! isTaskTerminalItem ( item ) } ) , taskSnapshots . value , 'current' ) )
const historySectionItems = computed ( ( ) => uniqueTaskRows ( historyItems . value . filter ( ( item ) => { const taskId = normalizeTaskId ( item . taskId ) ; return ! taskId || isTaskTerminalItem ( item ) } ) , taskSnapshots . value , 'history' ) )
const hasVisibleList = computed ( ( ) => currentSectionItems . value . length > 0 || historySectionItems . value . length > 0 )
const countryCheckboxRows = computed ( ( ) => { const selectedSet = new Set ( orderedCountryCodes . value ) ; return [ ... orderedCountryCodes . value . map ( ( code ) => ( { code , label : countryLabel ( code ) } ) ) , ... COUNTRY _OPTIONS . filter ( ( item ) => ! selectedSet . has ( item . code ) ) . map ( ( item ) => ( { code : item . code , label : item . label } ) ) ] } )
@@ -173,7 +176,14 @@ function addPollingTask(taskId: number) { if (!pollingTaskIds.value.includes(tas
function clearDispatchTimer ( taskId : number ) { const timer = dispatchTimers . get ( taskId ) ; if ( timer ) { window . clearTimeout ( timer ) ; dispatchTimers . delete ( taskId ) } }
function removePollingTask ( taskId : number ) { clearDispatchTimer ( taskId ) ; pollingTaskIds . value = pollingTaskIds . value . filter ( ( id ) => id !== taskId ) ; delete taskDetails . value [ taskId ] ; savePollingIds ( ) ; saveTaskDetailsToStorage ( ) }
function stopPollingTask ( taskId : number , keepStatus = true ) { pollingTaskIds . value = pollingTaskIds . value . filter ( ( id ) => id !== taskId ) ; if ( ! keepStatus ) delete taskDetails . value [ taskId ] ; savePollingIds ( ) ; saveTaskDetailsToStorage ( ) }
function isTaskTerminalById ( taskId ? : number ) { if ( ! taskId ) return false ; const status = taskDetails . value [ taskId ] || taskSnapshots . value [ taskId ] ? . task ? . status ; return status === 'SUCCESS ' || status === 'FAIL ED' }
function isTaskTerminalStatus ( s tatus ? : string ) { return status === 'SUCCESS' || status === 'FAILED ' || status === 'COMPLET ED' }
function isTaskTerminalById ( taskId ? : number ) { if ( ! taskId ) return false ; const status = taskDetails . value [ taskId ] || taskSnapshots . value [ taskId ] ? . task ? . status ; return isTaskTerminalStatus ( status ) }
function isTaskTerminalItem ( item : ShopMatchHistoryItem ) {
const taskId = normalizeTaskId ( item . taskId )
if ( taskId && isTaskTerminalById ( taskId ) ) return true
if ( isTaskTerminalStatus ( item . taskStatus ) ) return true
return ! ! item . success
}
async function loadCandidates ( ) { candidates . value = await listShopMatchCandidates ( ) }
async function loadDashboard ( ) { dashboard . value = await getShopMatchDashboard ( ) }
async function loadHistory ( ) { const data = await getShopMatchHistory ( ) ; historyItems . value = data . items || [ ] }
@@ -201,33 +211,56 @@ function removeMatchedRowsLocally(rows: ShopMatchHistoryItem[] | ShopMatchShopQu
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 = resolveScheduleDateTime ( 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 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 ] , 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' } ; 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 ) }
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 = snapshot ? . task ? . s tatus || taskDetails . value [ taskId ] ; 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 }
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 } 调度失败 ` ; queuePushResult . value = message ; ElMessage . error ( message ) } finally { scheduledDispatchInFlight . value = false } }
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 ? . s tatus ; 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 }
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 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 ( ) }
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 || [ ] ) { removePollingTask ( missingId ) ; delete nextSnapshots [ 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 ( status === 'SUCCESS' || status === 'FAILED' ) { 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 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 || [ ] ) { removePollingTask ( missingId ) ; delete nextSnapshots [ 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' ? 4000 : 10000 }
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 ] ) ; 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 ( status === 'SUCCESS' || status === 'FAILED' ) { removePollingTask ( taskId ) ; await loadHistory ( ) ; await loadDashboard ( ) ; restoreScheduledDispatches ( ) ; return status } await new Promise < void > ( ( resolve ) => { window . setTimeout ( ( ) => resolve ( ) , getPollIntervalMs ( ) ) } ) } }
async function waitForTaskTerminal ( taskId : number ) { while ( true ) { const batch = await getShopMatchTasksBatch ( [ taskId ] ) ; 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 loadHistory ( ) ; await loadDashboard ( ) ; 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 statusText ( item : ShopMatchHistoryItem ) { const status = resolvedTaskStatus ( item ) ; if ( status === 'SCHEDULED' ) return '待执行' ; if ( status === 'RUNNING' ) return '执行中' ; if ( status === 'SUCCESS' ) return '已完成' ; if ( status === 'FAILED' ) return '失败' ; return item . success ? '已完成' : '未知' }
function statusClass ( item : ShopMatchHistoryItem ) { const status = resolvedTaskStatus ( item ) ; return status === 'SUCCESS' ? 'success' : status === 'FAILED' ? 'failed' : 'running' }
function canDownload ( item : ShopMatchHistoryItem ) { return ! ! item . resultId && ! ! item . downloadUrl && resolvedTaskStatus ( item ) === 'SUCCESS' }
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' ) }
async function downloadResult ( item : ShopMatchHistoryItem ) { if ( ! item . resultId ) return ; const api = getPywebviewApi ( ) ; if ( ! api ? . save _file _from _url _new ) { ElMessage . error ( '当前客户端未提供下载能力' ) ; return } const url = getShopMatchResultDownloadUrl ( item . resultId ) ; const filename = item . outputFilename || ` ${ item . shopName || 'result' } .xlsx ` ; const result = await api . save _file _from _url _new ( url , filename ) ; if ( result . success ) ElMessage . success ( ` 已保存: ${ result . path || filename } ` ) ; else if ( result . error && result . error !== '用户取消' ) ElMessage . error ( result . error ) }
async function deleteTaskRecord ( item : ShopMatchHistoryItem ) { try { if ( item . resultId ) await deleteShopMatchHistory ( item . resultId ) ; else if ( item . taskId ) { await deleteShopMatchTask ( item . taskId ) ; removePollingTask ( item . taskId ) } await loadHistory ( ) ; await loadCandidates ( ) ; await loadDashboard ( ) } catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '删除失败' ) } }
async function deleteTaskRecord ( item : ShopMatchHistoryItem ) {
const taskId = normalizeTaskId ( item . taskId )
const resultId = Number ( item . resultId || 0 )
try {
if ( taskId > 0 ) {
await deleteShopMatchTask ( taskId )
removePollingTask ( taskId )
historyItems . value = historyItems . value . filter ( ( row ) => normalizeTaskId ( row . taskId ) !== taskId )
delete taskSnapshots . value [ taskId ]
saveTaskSnapshotsToStorage ( )
} else if ( resultId > 0 ) {
await deleteShopMatchHistory ( resultId )
historyItems . value = historyItems . value . filter ( ( row ) => Number ( row . resultId || 0 ) !== resultId )
} else {
throw new Error ( '缺少可删除的任务标识' )
}
await Promise . allSettled ( [ loadHistory ( ) , loadCandidates ( ) , loadDashboard ( ) ] )
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : '删除失败' )
}
}
function formatMatchStatus ( status ? : string ) { const value = ( status || '' ) . trim ( ) ; return { MATCHED : '已匹配' , PENDING : '待匹配' , CONFLICT : '需人工确认' , INDEX _STALE : '索引过期' } [ value ] || value || '—' }
function formatMatchRemark ( row : ShopMatchShopQueueItem ) { const message = ( row . matchMessage || '' ) . trim ( ) ; if ( message ) return message ; if ( row . matched && row . matchStatus === 'MATCHED' ) return '紫鸟索引已命中,可推送队列' ; if ( row . matched ) return '已关联索引,请结合状态列查看是否可推送' ; return '未命中或未就绪,请检查店铺名与索引刷新' }
async function pushToPythonQueue ( ) { const api = getPywebviewApi ( ) ; if ( ! api ? . enqueue _json ) { ElMessage . error ( '当前客户端未提供 enqueue_json' ) ; return } const matched = matchedItems . value . filter ( ( item ) => item . matched ) ; if ( ! matched . length ) { ElMessage . warning ( '请先匹配可用店铺' ) ; return } if ( ! orderedCountryCodes . value . length ) { ElMessage . warning ( '请至少勾选一个国家' ) ; return } let scheduleValues : string [ ] | undefined ; try { scheduleValues = parseScheduleValues ( ) } catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '定时配置无效' ) ; return } pushing . value = true ; queuePayloadText . value = '' ; try { for ( let index = 0 ; index < matched . length ; index += 1 ) { const item = matched [ index ] ; const created = await createShopMatchTask ( [ item ] , orderedCountryCodes . value , scheduleValues ) ; const taskId = created . taskId ; const initialStatus = scheduleValues ? . length ? 'SCHEDULED' : 'RUNNING' ; const snapshot : ShopMatchTaskDetailVo = { task : { id : taskId , status : initialStatus , scheduledAt : scheduleValues ? . [ 0 ] , countryCodes : [ ... orderedCountryCodes . value ] , currentStageIndex : scheduleValues ? . length ? 0 : undefined , activeStageIndex : undefined , scheduleStages : ( scheduleValues || [ ] ) . map ( ( value , stageIndex ) => ( { stageIndex , scheduledAt : value , status : stageIndex === 0 ? 'SCHEDULED' : 'PENDING' } ) ) } , items : created . items } ; taskSnapshots . value = { ... taskSnapshots . value , [ taskId ] : snapshot } ; taskDetails . value = { ... taskDetails . value , [ taskId ] : initialStatus } ; saveTaskSnapshotsToStorage ( ) ; saveTaskDetailsToStorage ( ) ; const createdItem = created . items ? . [ 0 ] ; if ( ! createdItem ) continue ; if ( scheduleValues ? . length ) { s cheduleDispatch(taskId , scheduleValues [ 0 ] ) ; queuePushResult . value = ` 任务 ${ taskId } 已创建,共 ${ scheduleValues . length } 个执行时间点 ` ; removeMatchedRowsLocally ( [ item ] ) ; continue } const payload = buildQueuePayload ( taskId , item , orderedCountryCodes . value ) ; 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 ) ; ensurePolling ( true ) ; removeMatchedRowsLocally ( [ item ] ) ; queuePushResult . value = matched . length > 1 ? ` 任务 ${ taskId } 已入队,等待完成后继续推送下一条( ${ index + 1 } / ${ matched . length } ) ` : ` 任务 ${ taskId } 已入队,等待执行完成 ` ; const finalStatus = await waitForTaskTerminal ( taskId ) ; if ( finalStatus !== 'SUCCESS' ) { throw new Error ( ` 任务 ${ taskId } 执行失败,已停止后续店铺推送 ` ) } queuePushResult . value = index + 1 < matched . length ? ` 任务 ${ taskId } 已完成,继续推送下一条( ${ index + 1 } / ${ matched . length } ) ` : ` 任务 ${ taskId } 已完成 ` } restoreScheduledDispatches ( ) ; if ( ! scheduleValues ? . length ) ensurePolling ( true ) ; ElMessage . success ( scheduleValues ? . length ? '定时任务已创建,后续会按时间串行推入 Python 队列' : ` 已按顺序完成 ${ matched . length } 条店铺推送 ` ) } catch ( error ) { const message = error instanceof Error ? error . message : '推送失败' ; queuePushResult . value = message ; ElMessage . error ( message ) } finally { pushing . value = false } }
async function pushToPythonQueue ( ) { const api = getPywebviewApi ( ) ; if ( ! api ? . enqueue _json ) { ElMessage . error ( '当前客户端未提供 enqueue_json' ) ; return } const matched = matchedItems . value . filter ( ( item ) => item . matched ) ; if ( ! matched . length ) { ElMessage . warning ( '请先匹配可用店铺' ) ; return } if ( ! orderedCountryCodes . value . length ) { ElMessage . warning ( '请至少勾选一个国家' ) ; return } let scheduleValues : string [ ] | undefined ; try { scheduleValues = parseScheduleValues ( ) } catch ( error ) { ElMessage . error ( error instanceof Error ? error . message : '定时配置无效' ) ; return } pushing . value = true ; queuePayloadText . value = '' ; try { for ( let index = 0 ; index < matched . length ; index += 1 ) { const item = matched [ index ] ; const created = await createShopMatchTask ( [ item ] , orderedCountryCodes . value , scheduleValues ) ; const taskId = created . taskId ; const initialStatus = scheduleValues ? . length ? 'SCHEDULED' : 'RUNNING' ; const snapshot : ShopMatchTaskDetailVo = { task : { id : taskId , status : initialStatus , scheduledAt : scheduleValues ? . [ 0 ] , countryCodes : [ ... orderedCountryCodes . value ] , currentStageIndex : scheduleValues ? . length ? 0 : undefined , activeStageIndex : undefined , scheduleStages : ( scheduleValues || [ ] ) . map ( ( value , stageIndex ) => ( { stageIndex , scheduledAt : value , status : stageIndex === 0 ? 'SCHEDULED' : 'PENDING' } ) ) } , items : created . items } ; taskSnapshots . value = { ... taskSnapshots . value , [ taskId ] : snapshot } ; taskDetails . value = { ... taskDetails . value , [ taskId ] : initialStatus } ; saveTaskSnapshotsToStorage ( ) ; saveTaskDetailsToStorage ( ) ; const createdItem = created . items ? . [ 0 ] ; if ( ! createdItem ) continue ; if ( scheduleValues ? . length ) { restoreS cheduled Dispatches ( ) ; queuePushResult . value = ` 任务 ${ taskId } 已创建,共 ${ scheduleValues . length } 个执行时间点 ` ; removeMatchedRowsLocally ( [ item ] ) ; continue } const payload = buildQueuePayload ( taskId , item , orderedCountryCodes . value ) ; 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 ) ; ensurePolling ( true ) ; removeMatchedRowsLocally ( [ item ] ) ; queuePushResult . value = matched . length > 1 ? ` 任务 ${ taskId } 已入队,等待完成后继续推送下一条( ${ index + 1 } / ${ matched . length } ) ` : ` 任务 ${ taskId } 已入队,等待执行完成 ` ; const finalStatus = await waitForTaskTerminal ( taskId ) ; if ( finalStatus !== 'SUCCESS' ) { throw new Error ( ` 任务 ${ taskId } 执行失败,已停止后续店铺推送 ` ) } queuePushResult . value = index + 1 < matched . length ? ` 任务 ${ taskId } 已完成,继续推送下一条( ${ index + 1 } / ${ matched . length } ) ` : ` 任务 ${ taskId } 已完成 ` } restoreScheduledDispatches ( ) ; if ( ! scheduleValues ? . length ) ensurePolling ( true ) ; ElMessage . success ( scheduleValues ? . length ? '定时任务已创建,后续会按时间串行推入 Python 队列' : ` 已按顺序完成 ${ matched . length } 条店铺推送 ` ) } catch ( error ) { const message = error instanceof Error ? error . message : '推送失败' ; queuePushResult . value = message ; ElMessage . error ( message ) } finally { pushing . value = false } }
onMounted ( async ( ) => { loadPollingIdsFromStorage ( ) ; loadTaskDetailsFromStorage ( ) ; loadTaskSnapshotsFromStorage ( ) ; loadMatchedItemsFromStorage ( ) ; syncPollingIdsWithTaskState ( ) ; await Promise . allSettled ( [ loadCandidates ( ) , loadCountryPreference ( ) , loadHistory ( ) , loadDashboard ( ) ] ) ; restoreScheduledDispatches ( ) ; if ( pollingTaskIds . value . length ) ensurePolling ( true ) } )
onUnmounted ( ( ) => { stopPolling ( ) ; if ( countryPrefSaveTimer ) clearTimeout ( countryPrefSaveTimer ) ; for ( const timer of dispatchTimers . values ( ) ) window . clearTimeout ( timer ) ; dispatchTimers . clear ( ) } )
onMounted ( async ( ) => { loadPollingIdsFromStorage ( ) ; loadTaskDetailsFromStorage ( ) ; loadTaskSnapshotsFromStorage ( ) ; loadMatchedItemsFromStorage ( ) ; syncPollingIdsWithTaskState ( ) ; await Promise . allSettled ( [ loadCandidates ( ) , loadCountryPreference ( ) , loadHistory ( ) , loadDashboard ( ) ] ) ; startScheduleHeartbeat ( ) ; window . addEventListener ( 'focus' , handleScheduleRecovery ) ; document . addEventListener ( 'visibilitychange' , handleScheduleRecovery ) ; restoreScheduledDispatches ( ) ; if ( pollingTaskIds . value . length ) ensurePolling ( true ) } )
onUnmounted ( ( ) => { stopPolling ( ) ; stopScheduleHeartbeat ( ) ; window . removeEventListener ( 'focus' , handleScheduleRecovery ) ; document . removeEventListener ( 'visibilitychange' , handleScheduleRecovery ) ; if ( countryPrefSaveTimer ) clearTimeout ( countryPrefSaveTimer ) ; for ( const timer of dispatchTimers . values ( ) ) window . clearTimeout ( timer ) ; dispatchTimers . clear ( ) } )
< / script >
< style scoped >