@@ -110,6 +110,7 @@ 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 , 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'
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
const COUNTRY _OPTIONS = [ { code : 'DE' , label : '德国' } , { code : 'UK' , label : '英国' } , { code : 'FR' , label : '法国' } , { code : 'IT' , label : '意大利' } , { code : 'ES' , label : '西班牙' } ] as const
const shopInput = ref ( '' )
@@ -159,7 +160,7 @@ function taskRowKey(item: ShopMatchHistoryItem) { return `${normalizeTaskId(item
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 formatPickerDateTime ( 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' ) ; return ` ${ year } - ${ month } - ${ day } ${ hours } : ${ minutes } :00 ` }
function getDefaultScheduleValue ( ) { const date = new Date ( ) ; date . setSeconds ( 0 , 0 ) ; date . setMinutes ( date . getMinutes ( ) + 1 ) ; return formatPickerDateTime ( date ) }
function getDefaultScheduleValue ( ) { const date = new Date ( ) ; date . setSeconds ( 0 , 0 ) ; date . setMinutes ( date . getMinutes ( ) + 5 ) ; return formatPickerDateTime ( date ) }
function parseScheduleDateTime ( value ? : string | null ) {
if ( ! value ) return null
const normalized = value . trim ( ) . replace ( 'T' , ' ' )
@@ -235,15 +236,56 @@ 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 = [ getDefaultScheduleValue ( ) ] ; if ( ! scheduleEnabled . value ) schedulePickerValues . value = [ ] }
function onScheduleToggle ( ) { if ( scheduleEnabled . value ) schedulePickerValues . value = sanitizeSchedulePickerValues ( schedulePickerValues . value , true ) ; if ( ! scheduleEnabled . value ) schedulePickerValues . value = [ ] }
function addScheduleValue ( ) { const last = schedulePickerValues . value [ schedulePickerValues . value . length - 1 ] ; const nextValue = last ? nextScheduleValueAfter ( last , 60 ) : getDefaultScheduleValue ( ) ; 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 || '' ; schedulePickerValues . value = list }
function updateScheduleValue ( index : number , value : string | null ) { const list = [ ... schedulePickerValues . value ] ; list [ index ] = value || '' ; schedulePickerValues . value = sanitizeSchedulePickerValues ( list , false ) }
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 date = parseScheduleDateTime ( 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 parseScheduleValues ( ) {
if ( ! scheduleEnabled . value ) return undefined
const now = Date . now ( )
const values = schedulePickerValues . value . map ( ( item ) => item . trim ( ) ) . filter ( Boolean )
if ( ! values . length ) throw new Error ( '请至少选择一个执行时间' )
const withTime = values . map ( ( value ) => {
const date = parseScheduleDateTime ( value )
if ( ! date ) throw new Error ( ` 时间格式无效: ${ value } ` )
return { raw : formatScheduleDateTime ( date ) , picker : formatPickerDateTime ( 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 ( '执行时间不能重复' )
const futureOnly = withTime . filter ( ( item ) => item . time > now )
if ( ! futureOnly . length ) throw new Error ( '执行时间必须晚于当前时间,请重新选择' )
if ( futureOnly . length !== withTime . length ) {
schedulePickerValues . value = futureOnly . map ( ( item ) => item . picker )
ElMessage . warning ( '已自动移除早于当前时间的执行时间' )
}
return futureOnly . map ( ( item ) => item . raw )
}
function sanitizeSchedulePickerValues ( values : string [ ] , ensureOne : boolean ) {
const now = Date . now ( )
const normalized = values
. map ( ( item ) => item . trim ( ) )
. filter ( Boolean )
. map ( ( value ) => {
const date = parseScheduleDateTime ( value )
return date ? { value : formatPickerDateTime ( date ) , time : date . getTime ( ) } : null
} )
. filter ( ( item ) : item is { value : string ; time : number } => ! ! item )
. filter ( ( item ) => item . time > now )
. sort ( ( a , b ) => a . time - b . time )
const deduped : string [ ] = [ ]
const seen = new Set < string > ( )
for ( const item of normalized ) {
if ( seen . has ( item . value ) ) continue
seen . add ( item . value )
deduped . push ( item . value )
}
if ( ! deduped . length && ensureOne ) return [ getDefaultScheduleValue ( ) ]
return deduped
}
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 withTransientRetry ( ( ) => activateShopMatchTask ( taskId , stage . stageIndex ) , ( attempt , maxAttempts ) => { queuePushResult . value = ` 任务 ${ taskId } 等待后端恢复后再激活执行( ${ attempt } / ${ maxAttempts } ) ...` } ) ; 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 ) }
@@ -261,11 +303,11 @@ function sleep(ms: number) { return new Promise<void>((resolve) => { window.setT
function isTransientBackendError ( error : unknown ) { const message = ( error instanceof Error ? error . message : String ( error || '' ) ) . toLowerCase ( ) ; return TRANSIENT _BACKEND _ERROR _PATTERNS . some ( ( pattern ) => message . includes ( pattern . toLowerCase ( ) ) ) }
async function withTransientRetry < T > ( action : ( ) => Promise < T > , onRetry : ( attempt : number , maxAttempts : number ) => void , maxAttempts = 10 ) { let attempt = 0 ; while ( true ) { try { return await action ( ) } catch ( error ) { attempt += 1 ; if ( ! isTransientBackendError ( error ) || attempt >= maxAttempts ) throw error ; onRetry ( attempt , maxAttempts ) ; await sleep ( getPollIntervalMs ( ) ) } } }
async function refreshTaskBatch ( ) { if ( ! pollingTaskIds . value . length ) return { settledTaskIds : [ ] as number [ ] } ; try { const batch = await getShopMatchTaskProgressBatch ( 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' ? 6000 : 20000 }
function getPollIntervalMs ( ) { return getTaskPollIntervalMs ( ) }
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 ) { let transientErrorCount = 0 ; const maxTransientErrors = 30 ; while ( true ) { try { const batch = await getShopMatchTaskProgressBatch ( [ taskId ] ) ; transientErrorCount = 0 ; 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 } } catch ( error ) { if ( ! isTransientBackendError ( error ) ) throw error ; transientErrorCount += 1 ; if ( transientErrorCount >= maxTransientErrors ) throw new Error ( ` 任务 ${ taskId } 等待后端恢复超时,请稍后手动刷新查看状态 ` ) ; queuePushResult . value = ` 任务 ${ taskId } 运行中,正在等待后端服务恢复( ${ transientErrorCount } / ${ maxTransientErrors } ) ...` ; if ( transientErrorCount === 1 || transientErrorCount % 5 === 0 ) ElMessage . warning ( ` 任务 ${ taskId } 运行中,后端服务暂时不可用,正在自动重试 ` ) ; await sleep ( getPollIntervalMs ( ) ) ; continue } await sleep ( getPollIntervalMs ( ) ) } }
async function waitForTaskTerminal ( taskId : number ) { let transientErrorCount = 0 ; const maxTransientErrors = 30 ; while ( true ) { try { const batch = await getShopMatchTaskProgressBatch ( [ taskId ] ) ; if ( transientErrorCount > 0 ) queuePushResult . value = ` 任务 ${ taskId } 后端已恢复,继续等待执行结果... ` ; transientErrorCount = 0 ; 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 } } catch ( error ) { if ( ! isTransientBackendError ( error ) ) throw error ; transientErrorCount += 1 ; if ( transientErrorCount >= maxTransientErrors ) throw new Error ( ` 任务 ${ taskId } 等待后端恢复超时,请稍后手动刷新查看状态 ` ) ; queuePushResult . value = ` 任务 ${ taskId } 运行中,正在等待后端服务恢复( ${ transientErrorCount } / ${ maxTransientErrors } ) ...` ; if ( transientErrorCount === 1 || transientErrorCount % 5 === 0 ) ElMessage . warning ( ` 任务 ${ taskId } 运行中,后端服务暂时不可用,正在自动重试 ` ) ; await sleep ( getPollIntervalMs ( ) ) ; continue } await sleep ( 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 } 次 ` }
@@ -302,9 +344,8 @@ async function deleteTaskRecord(item: ShopMatchHistoryItem) {
}
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 withTransientRetry ( ( ) => createShopMatchTask ( [ item ] , orderedCountryCodes . value , scheduleValues ) , ( attempt , maxAttempts ) => { queuePushResult . value = ` 后端服务暂时不可用,正在重试创建任务( ${ attempt } / ${ maxAttempts } ) ...` } ) ; 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 ) { restoreScheduledDispatches ( ) ; 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 ( ) ] ) ; startScheduleHeartbeat ( ) ; window . addEventListener ( 'focus' , handleScheduleRecovery ) ; document . addEventListener ( 'visibilitychange' , handleScheduleRecovery ) ; restoreScheduledDispatches ( ) ; if ( pollingTaskIds . value . length ) ensurePolling ( true ) } )
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 = '' ; let successCount = 0 ; let failedCount = 0 ; try { for ( let index = 0 ; index < matched . length ; index += 1 ) { const item = matched [ index ] ; try { const created = await withTransientRetry ( ( ) => createShopMatchTask ( [ item ] , orderedCountryCodes . value , scheduleValues ) , ( attempt , maxAttempts ) => { queuePushResult . value = ` 后端服务暂时不可用,正在重试创建任务( ${ attempt } / ${ maxAttempts } ) ...` } ) ; 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 ) { successCount += 1 ; restoreScheduledDispatches ( ) ; 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 ) { failedCount += 1 ; queuePushResult . value = ` 任务 ${ taskId } 推送失败,已自动继续下一条: ${ result ? . error || '未知错误' } ` ; ElMessage . error ( queuePushResult . value ) ; continue } 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' ) { failedCount += 1 ; queuePushResult . value = ` 任务 ${ taskId } 执行失败,已自动继续下一条( ${ index + 1 } / ${ matched . length } ) ` ; ElMessage . error ( queuePushResult . value ) ; continue } successCount += 1 ; queuePushResult . value = index + 1 < matched . length ? ` 任务 ${ taskId } 已完成,继续推送下一条( ${ index + 1 } / ${ matched . length } ) ` : ` 任务 ${ taskId } 已完成 ` } catch ( error ) { failedCount += 1 ; queuePushResult . value = ` 第 ${ index + 1 } / ${ matched . length } 条任务处理失败,已自动继续下一条: ${ error instanceof Error ? error . message : '未知错误' } ` ; ElMessage . error ( queuePushResult . value ) ; continue } } restoreScheduledDispatches ( ) ; if ( ! scheduleValues ? . length ) ensurePolling ( true ) ; ElMessage . success ( scheduleValues ? . length ? ` 定时任务已创建:成功 ${ successCount } 条,失败 ${ failedCount } 条 ` : ` 店铺推送已完成:成功 ${ successCount } 条,失败 ${ failedCount } 条 ` ) } 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 ( ) ; schedulePickerValues . value = sanitizeSchedulePickerValues ( schedulePickerValues . value , scheduleEnabled . value ) ; 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 >
@@ -394,3 +435,4 @@ onUnmounted(() => { stopPolling(); stopScheduleHeartbeat(); window.removeEventLi
. btn - delete : hover { background : rgba ( 231 , 76 , 60 , 0.22 ) ; }
@ media ( max - width : 1100 px ) { . main - content { flex - direction : column ; height : auto ; } . left - panel { width : 100 % ; border - right : none ; border - bottom : 1 px solid # 2 a2a2a ; } . clean - result - summary { grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ; } . schedule - row { flex - direction : column ; align - items : stretch ; } }
< / style >