@@ -7,7 +7,7 @@
< div class = "section-title" > 店铺输入 < / div >
< div class = "input-zone" >
< div class = "hint" >
左侧负责录入店铺并加入备选区 , 确认命中后 推送到 Python 队列 。 巡店删除按店铺串行执行 : 上一个任务完成后会自动开始下一个 , 失败也会继续下一个 。
左侧负责录入店铺并加入备选区 , 点击 “ 匹配店铺 ” 只会生成匹配结果 ; 确认后需手动点击 “ 推送到 Python 队列 ” 才会创建并执行任务 。
< / div >
< div class = "input-row" >
< el-input
@@ -60,6 +60,54 @@
< / el-table >
< / div >
< div class = "section-title" > 删除条件 < / div >
< div class = "condition-panel" >
< div class = "condition-input-row" >
< el-input
v-model = "conditionInput"
clearable
placeholder = "请输入删除条件"
@keyup.enter ="confirmAddCondition"
/ >
< button
type = "button"
class = "opt-btn"
:disabled = "addingCondition"
@click ="confirmAddCondition"
>
{ { addingCondition ? "保存中..." : "保存" } }
< / button >
< / div >
< div v-if = "!deleteConditions.length" class="empty-conditions" >
暂无删除条件 , 保存后可在推送任务时多选携带 。
< / div >
< ul v-else class = "condition-list" >
< li
v-for = "condition in deleteConditions"
:key = "condition.id"
class = "condition-item"
>
< label class = "condition-check" >
< input
v-model = "selectedConditionIds"
type = "checkbox"
:value = "condition.id"
/ >
< span :title = "condition.conditionText" >
{ { condition . conditionText } }
< / span >
< / label >
< button
type = "button"
class = "link-danger"
@click ="removeCondition(condition.id)"
>
删除
< / button >
< / li >
< / ul >
< / div >
< div class = "run-row" >
< button
type = "button"
@@ -75,12 +123,12 @@
: disabled = "pushing || !matchedRunnableItems.length"
@click ="pushToPythonQueue"
>
{ { pushing ? "串行 执行中..." : "推送到 Python 队列" } }
{ { pushing ? "任务 执行中..." : "推送到 Python 队列" } }
< / button >
< / div >
< p class = "loading-msg" >
当前页面会按店铺串行推送任务 , 并在任务成功或失败后自动接续下一个任务 。 服务端临时重启时会自动重试查询状态 , 恢复后继续后续店铺 。
当前页面会把匹配结果合并为一个巡店删除任务推送 ; 多个店铺会在同一个任务和同一个结果文件中汇总 。
< / p >
< div v-if = "queuePushResult" class="queue-debug-card" >
@@ -192,19 +240,19 @@
< ul class = "task-list" >
< li
v-for = "item in currentSectionItems"
:key = "`${item.resultId}-${item.taskId}` "
:key = "taskGroupKey(item) "
class = "task-item"
>
< div class = "left split-result-main" >
< span class = "id" : title= "item.shopName || '' " >
{ { item . shopName || "-" } }
< span class = "id" : title= "formatTaskGroupShopNames(item) " >
{ { formatTaskGroupShopNames ( item ) } }
< / span >
< div class = "files" > 任务 ID : { { item . taskId ? ? "-" } } < / div >
< div v-if = "item.resultId " class="files" >
结果 ID : {{ item.resultId }}
< div v-if = "formatTaskGroupResultIds(item) " class="files" >
结果 ID : {{ formatTaskGroupResultIds ( item ) }}
< / div >
< div v-if = "item.shopId " class="files" >
店铺 ID : {{ item.shopId }}
< div v-if = "formatTaskGroupShopIds(item) " class="files" >
店铺 ID : {{ formatTaskGroupShopIds ( item ) }}
< / div >
< div v-if = "item.platform" class="files" >
平台 : {{ item.platform }}
@@ -213,10 +261,10 @@
创建时间 : { { formatDateTime ( item . createdAt ) } }
< / div >
< div class = "files" >
模板结构 : { { formatTemplateSummary ( item ) } }
模板 : { { formatTemplateSummary ( item ) } }
< / div >
< div v-if = "item.error " class="files" >
错误 : {{ item.error }}
< div v-if = "formatTaskGroupErrors(item) " class="files" >
错误 : {{ formatTaskGroupErrors ( item ) }}
< / div >
< / div >
< div class = "task-right" >
@@ -243,19 +291,19 @@
< ul class = "task-list" >
< li
v-for = "item in historySectionItems"
:key = "`${item.resultId}-${item.taskId}` "
:key = "taskGroupKey(item) "
class = "task-item"
>
< div class = "left split-result-main" >
< span class = "id" : title= "item.shopName || '' " >
{ { item . shopName || "-" } }
< span class = "id" : title= "formatTaskGroupShopNames(item) " >
{ { formatTaskGroupShopNames ( item ) } }
< / span >
< div class = "files" > 任务 ID : { { item . taskId ? ? "-" } } < / div >
< div v-if = "item.resultId " class="files" >
结果 ID : {{ item.resultId }}
< div v-if = "formatTaskGroupResultIds(item) " class="files" >
结果 ID : {{ formatTaskGroupResultIds ( item ) }}
< / div >
< div v-if = "item.shopId " class="files" >
店铺 ID : {{ item.shopId }}
< div v-if = "formatTaskGroupShopIds(item) " class="files" >
店铺 ID : {{ formatTaskGroupShopIds ( item ) }}
< / div >
< div v-if = "item.platform" class="files" >
平台 : {{ item.platform }}
@@ -267,10 +315,10 @@
完成时间 : {{ formatDateTime ( item.finishedAt ) }}
< / div >
< div class = "files" >
模板结构 : { { formatTemplateSummary ( item ) } }
模板 : { { formatTemplateSummary ( item ) } }
< / div >
< div v-if = "item.error " class="files" >
错误 : {{ item.error }}
< div v-if = "formatTaskGroupErrors(item) " class="files" >
错误 : {{ formatTaskGroupErrors ( item ) }}
< / div >
< / div >
< div class = "task-right" >
@@ -313,18 +361,22 @@ import { ElMessage } from "element-plus";
import BrandTopBar from "@/pages/brand/components/BrandTopBar.vue" ;
import {
addPatrolDeleteCandidate ,
addPatrolDeleteCondition ,
createPatrolDeleteTask ,
deletePatrolDeleteCandidate ,
deletePatrolDeleteCondition ,
deletePatrolDeleteHistory ,
deletePatrolDeleteTask ,
getPatrolDeleteDashboard ,
getPatrolDeleteHistory ,
getPatrolDeleteTaskProgressBatch ,
getPatrolDeleteResultDownloadUrl ,
listPatrolDeleteConditions ,
listPatrolDeleteCandidates ,
matchPatrolDeleteShops ,
submitPatrolDeleteTaskResult ,
type PatrolDeleteCandidateVo ,
type PatrolDeleteConditionVo ,
type PatrolDeleteCartRatio ,
type PatrolDeleteCountrySection ,
type PatrolDeleteDashboardVo ,
@@ -340,8 +392,11 @@ const COUNTRY_TEMPLATE = ["德国", "英国", "法国", "意大利", "西班牙"
const MAX _TRANSIENT _ERRORS = 30 ;
const shopInput = ref ( "" ) ;
const conditionInput = ref ( "" ) ;
const candidates = ref < PatrolDeleteCandidateVo [ ] > ( [ ] ) ;
const selectedCandidates = ref < PatrolDeleteCandidateVo [ ] > ( [ ] ) ;
const deleteConditions = ref < PatrolDeleteConditionVo [ ] > ( [ ] ) ;
const selectedConditionIds = ref < number [ ] > ( [ ] ) ;
const matchedItems = ref < PatrolDeleteShopQueueItem [ ] > ( [ ] ) ;
const historyItems = ref < PatrolDeleteHistoryItem [ ] > ( [ ] ) ;
const dashboard = ref < PatrolDeleteDashboardVo > ( {
@@ -351,32 +406,30 @@ const dashboard = ref<PatrolDeleteDashboardVo>({
failedTaskCount : 0 ,
} ) ;
const adding = ref ( false ) ;
const addingCondition = ref ( false ) ;
const matching = ref ( false ) ;
const pushing = ref ( false ) ;
const queuePushResult = ref ( "" ) ;
const queuePayloadText = ref ( "" ) ;
const pendingQueue = ref < PatrolDeleteShopQueueItem [ ] > ( [ ] ) ;
const activeTaskId = ref < number | null > ( null ) ;
const activeQueueItem = ref < PatrolDeleteShopQueueItem | null > ( null ) ;
const queueWorkerRunning = ref ( false ) ;
const autoQueueEnabled = ref ( false ) ;
let historyPollTimer : number | null = null ;
const timers = createCategorizedTimers ( "patrol-delete" ) ;
const matchedRunnableItems = computed ( ( ) =>
matchedItems . value . filter ( ( item ) => item . matched ) ,
) ;
const taskRecordItems = computed ( ( ) => groupHistoryItemsByTask ( historyItems . value ) ) ;
const currentSectionItems = computed ( ( ) =>
hist ory Items. value . filter ( ( item ) => ! isTaskTerminal ( item . taskStatus ) ) ,
taskRec ord Items. value . filter ( ( item ) => ! isTaskTerminal ( item . taskStatus ) ) ,
) ;
const historySectionItems = computed ( ( ) =>
hist ory Items. value . filter ( ( item ) => isTaskTerminal ( item . taskStatus ) ) ,
taskRec ord Items. value . filter ( ( item ) => isTaskTerminal ( item . taskStatus ) ) ,
) ;
const hasQueueWork = computed (
( ) =>
queueWorkerRunning . value ||
! ! activeTaskId . value ||
pendingQueue . value . length > 0 ,
! ! activeTaskId . value ,
) ;
function uidForStorage ( ) {
@@ -401,6 +454,10 @@ function historyItemKey(item: PatrolDeleteHistoryItem) {
return ` ${ item . taskId ? ? 0 } : ${ item . resultId ? ? 0 } ` ;
}
function taskGroupKey ( item : PatrolDeleteHistoryItem ) {
return ` task: ${ item . taskId ? ? item . resultId ? ? 0 } ` ;
}
let disposed = false ;
function sleep ( ms : number ) {
@@ -546,8 +603,65 @@ function formatTemplateSummary(
"shopName" | "countrySections" | "cartRatios"
> ,
) {
const rows = flattenTemplateRows ( record ) ;
return ` ${ rows . length } 行状态数据,首行含 ${ record . cartRatios ? . length || 0 } 个购物车比例列 ` ;
const countries = new Set ( [
... ( record . countrySections || [ ] ) . map ( ( item ) => item . country ) . filter ( Boolean ) ,
... ( record . cartRatios || [ ] ) . map ( ( item ) => item . country ) . filter ( Boolean ) ,
] ) ;
return ` ${ countries . size || 0 } 个国家,含状态数据和购物车比例 ` ;
}
function uniqueJoined ( values : Array < string | number | undefined | null > ) {
return Array . from (
new Set (
values
. map ( ( value ) => ( value == null ? "" : String ( value ) . trim ( ) ) )
. filter ( Boolean ) ,
) ,
) . join ( "、" ) ;
}
function groupHistoryItemsByTask ( items : PatrolDeleteHistoryItem [ ] ) {
const groups = new Map < string , PatrolDeleteHistoryItem [ ] > ( ) ;
for ( const item of items || [ ] ) {
const key = String ( item . taskId ? ? ` result: ${ item . resultId ? ? Math . random ( ) } ` ) ;
const group = groups . get ( key ) || [ ] ;
group . push ( item ) ;
groups . set ( key , group ) ;
}
return Array . from ( groups . values ( ) ) . map ( ( group ) => {
const base =
group . find ( ( item ) => canDownload ( item ) ) ||
group . find ( ( item ) => item . taskStatus ) ||
group [ 0 ] ;
const errors = uniqueJoined ( group . map ( ( item ) => item . error ) ) ;
return {
... base ,
shopName : uniqueJoined ( group . map ( ( item ) => item . shopName ) ) || base . shopName ,
shopId : uniqueJoined ( group . map ( ( item ) => item . shopId ) ) || base . shopId ,
error : errors || base . error ,
_ _groupItems : group ,
} as PatrolDeleteHistoryItem & { _ _groupItems ? : PatrolDeleteHistoryItem [ ] } ;
} ) ;
}
function taskGroupItems ( item : PatrolDeleteHistoryItem ) {
return ( ( item as PatrolDeleteHistoryItem & { _ _groupItems ? : PatrolDeleteHistoryItem [ ] } ) . _ _groupItems || [ item ] ) ;
}
function formatTaskGroupShopNames ( item : PatrolDeleteHistoryItem ) {
return uniqueJoined ( taskGroupItems ( item ) . map ( ( row ) => row . shopName ) ) || "-" ;
}
function formatTaskGroupResultIds ( item : PatrolDeleteHistoryItem ) {
return uniqueJoined ( taskGroupItems ( item ) . map ( ( row ) => row . resultId ) ) ;
}
function formatTaskGroupShopIds ( item : PatrolDeleteHistoryItem ) {
return uniqueJoined ( taskGroupItems ( item ) . map ( ( row ) => row . shopId ) ) ;
}
function formatTaskGroupErrors ( item : PatrolDeleteHistoryItem ) {
return uniqueJoined ( taskGroupItems ( item ) . map ( ( row ) => row . error ) ) ;
}
function isTaskTerminal ( status ? : string ) {
@@ -569,7 +683,9 @@ function statusClass(status?: string) {
}
function canDownload ( item : PatrolDeleteHistoryItem ) {
return Boolean ( item . resultId && ( item . fileReady || item . d ownloadUrl ) ) ;
return taskGroupItems ( item ) .some ( ( r ow) =>
Boolean ( row . resultId && ( row . fileReady || row . downloadUrl ) ) ,
) ;
}
function saveMatchedItems ( ) {
@@ -595,9 +711,7 @@ function loadMatchedItems() {
function saveQueueState ( ) {
if ( typeof window === "undefined" ) return ;
const payload = {
pendingQueue : pendingQueue . value ,
activeTaskId : activeTaskId . value ,
activeQueueItem : activeQueueItem . value ,
} ;
window . localStorage . setItem ( queueStateStorageKey ( ) , JSON . stringify ( payload ) ) ;
}
@@ -610,23 +724,16 @@ function loadQueueState() {
: null ;
if ( ! raw ) return ;
const parsed = JSON . parse ( raw ) as {
pendingQueue ? : PatrolDeleteShopQueueItem [ ] ;
activeTaskId ? : number | null ;
activeQueueItem ? : PatrolDeleteShopQueueItem | null ;
} ;
pendingQueue . value = parsed . pendingQueue || [ ] ;
activeTaskId . value = parsed . activeTaskId ? ? null ;
activeQueueItem . value = parsed . activeQueueItem ? ? null ;
} catch {
pendingQueue . value = [ ] ;
activeTaskId . value = null ;
activeQueueItem . value = null ;
}
}
function clearActiveQueueTask ( ) {
activeTaskId . value = null ;
activeQueueItem . value = null ;
saveQueueState ( ) ;
}
@@ -636,9 +743,8 @@ function isRecordMissingError(error: unknown) {
}
function removeMatchedRowLocally ( row : PatrolDeleteShopQueueItem ) {
matchedItems . value = matchedItems . value . filter (
( item ) => rowKeyForMatch ( item ) !== rowKeyForMatch ( row ) ,
) ;
const key = rowKeyForMatch ( row ) ;
matchedItems . value = matchedItems . value . filter ( ( item ) => rowKeyForMatch ( item ) !== key ) ;
saveMatchedItems ( ) ;
}
@@ -650,20 +756,18 @@ function removeHistoryItemLocally(item: PatrolDeleteHistoryItem) {
} ) ;
}
function mergeQueueItems (
base : PatrolDeleteShopQueueItem [ ] ,
incoming : PatrolDeleteShopQueueItem [ ] ,
) {
const map = new Map < string , PatrolDeleteShopQueueItem > ( ) ;
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 loadCandidates ( ) {
candidates . value = await listPatrolDeleteCandidates ( ) ;
}
async function loadConditions ( ) {
deleteConditions . value = await listPatrolDeleteConditions ( ) ;
const existingIds = new Set ( deleteConditions . value . map ( ( item ) => item . id ) ) ;
selectedConditionIds . value = selectedConditionIds . value . filter ( ( id ) =>
existingIds . has ( id ) ,
) ;
}
async function loadDashboard ( ) {
dashboard . value = await getPatrolDeleteDashboard ( ) ;
}
@@ -705,6 +809,10 @@ function mergeHistoryProgressItems(incoming: PatrolDeleteHistoryItem[]) {
historyItems . value = merged ;
}
function upsertHistoryItems ( incoming : PatrolDeleteHistoryItem [ ] ) {
mergeHistoryProgressItems ( incoming ) ;
}
async function refreshActiveTaskProgress ( taskIds ? : number [ ] ) {
const ids = Array . from (
new Set (
@@ -768,6 +876,41 @@ async function confirmAdd() {
}
}
async function confirmAddCondition ( ) {
const text = conditionInput . value . trim ( ) ;
if ( ! text ) {
ElMessage . warning ( "请输入删除条件" ) ;
return ;
}
addingCondition . value = true ;
try {
const saved = await addPatrolDeleteCondition ( text ) ;
conditionInput . value = "" ;
await loadConditions ( ) ;
if ( saved ? . id && ! selectedConditionIds . value . includes ( saved . id ) ) {
selectedConditionIds . value = [ ... selectedConditionIds . value , saved . id ] ;
}
ElMessage . success ( "删除条件已保存" ) ;
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : "保存失败" ) ;
} finally {
addingCondition . value = false ;
}
}
async function removeCondition ( id : number ) {
try {
await deletePatrolDeleteCondition ( id ) ;
selectedConditionIds . value = selectedConditionIds . value . filter (
( item ) => item !== id ,
) ;
await loadConditions ( ) ;
ElMessage . success ( "删除条件已删除" ) ;
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : "删除失败" ) ;
}
}
async function removeCandidate ( id : number ) {
try {
await deletePatrolDeleteCandidate ( id ) ;
@@ -804,11 +947,6 @@ async function runMatch() {
const data = await matchPatrolDeleteShops ( names ) ;
matchedItems . value = mergeMatchedItems ( matchedItems . value , data . items || [ ] ) ;
saveMatchedItems ( ) ;
if ( autoQueueEnabled . value ) {
pendingQueue . value = mergeQueueItems ( pendingQueue . value , matchedRunnableItems . value ) ;
saveQueueState ( ) ;
void processQueue ( ) ;
}
ElMessage . success ( ` 本次返回 ${ data . items ? . length || 0 } 条匹配结果 ` ) ;
} catch ( error ) {
ElMessage . error ( error instanceof Error ? error . message : "匹配失败" ) ;
@@ -818,14 +956,8 @@ async function runMatch() {
}
function removeMatchedRow ( row : PatrolDeleteShopQueueItem ) {
if ( hasQueueWork . value ) {
ElMessage . warning ( "队列执行中,请等待当前串行任务结束后再调整 ") ;
return ;
}
matchedItems . value = matchedItems . value . filter (
( item ) => rowKeyForMatch ( item ) !== rowKeyForMatch ( row ) ,
) ;
saveMatchedItems ( ) ;
removeMatchedRowLocally ( row ) ;
ElMessage . success ( "已从匹配结果移除 ") ;
}
function buildTaskItem ( item : PatrolDeleteShopQueueItem ) : PatrolDeleteTaskItem {
@@ -842,7 +974,19 @@ function buildTaskItem(item: PatrolDeleteShopQueueItem): PatrolDeleteTaskItem {
} ;
}
function buildQueuePayload ( taskId : number , item : PatrolDeleteHistoryItem ) {
function selectedDeleteConditions ( ) {
const selected = new Set ( selectedConditionIds . value ) ;
return deleteConditions . value
. filter ( ( condition ) => selected . has ( condition . id ) )
. map ( ( condition ) => ( {
id : condition . id ,
conditionText : condition . conditionText ,
} ) ) ;
}
function buildQueuePayload ( taskId : number , items : PatrolDeleteHistoryItem [ ] ) {
const firstItem = items [ 0 ] ;
const deleteConditionsForTask = selectedDeleteConditions ( ) ;
return {
type : "patrol-delete-run" ,
ts : Date . now ( ) ,
@@ -850,8 +994,9 @@ function buildQueuePayload(taskId: number, item: PatrolDeleteHistoryItem) {
taskId ,
user _id : Number ( uidForStorage ( ) ) || 0 ,
source : "frontend-vue-patrol-delete" ,
items : [
{
delete _conditions : deleteConditionsForTask ,
deleteConditions : deleteConditionsForTask ,
items : items . map ( ( item ) => ( {
shopName : item . shopName ,
shopId : item . shopId ,
platform : item . platform ,
@@ -859,11 +1004,17 @@ function buildQueuePayload(taskId: number, item: PatrolDeleteHistoryItem) {
matched : item . matched ,
matchStatus : item . matchStatus ,
matchMessage : item . matchMessage ,
} ,
] ,
template _rows : flattenTemplateRows ( item ) ,
country _sections : item . countrySections || [ ] ,
cart _ratio s: item . cartRatios || [ ] ,
countrySections : item . countrySections || [ ] ,
cartRatios : item . cartRatios || [ ] ,
} ) ) ,
template _rows : firstItem ? flattenTemplateRows ( firstItem ) : [ ] ,
shop _template _row s: items . map ( ( item ) => ( {
shopName : item . shopName ,
shopId : item . shopId ,
templateRows : flattenTemplateRows ( item ) ,
} ) ) ,
country _sections : firstItem ? . countrySections || [ ] ,
cart _ratios : firstItem ? . cartRatios || [ ] ,
} ,
} ;
}
@@ -876,8 +1027,10 @@ async function waitForTaskTerminal(taskId: number) {
let transientErrorCount = 0 ;
while ( true ) {
if ( disposed ) return "STOPPED" ;
if ( activeTaskId . value !== taskId ) return "DELETED" ;
try {
await refreshActiveTaskProgress ( [ taskId ] ) ;
if ( activeTaskId . value !== taskId ) return "DELETED" ;
if ( transientErrorCount > 0 ) {
queuePushResult . value = ` 任务 ${ taskId } 后端已恢复,继续等待执行结果... ` ;
}
@@ -915,76 +1068,72 @@ async function processQueue() {
throw new Error ( "当前客户端未提供 enqueue_json" ) ;
}
while ( ! disposed && ( activeTaskId . value || pendingQueue . value . length ) ) {
if ( activeTaskId . value ) {
const finalStatus = await waitForTaskTerminal ( activeTaskId . value ) ;
queuePushResult . value =
pendingQueue . value . length > 0
? ` 任务 ${ activeTaskId . value } ${ finalStatus === "SUCCESS" ? "已完成" : "执行失败" } ,自动继续下一个(剩余 ${ pendingQueue . value . length } 条) `
: ` 任务 ${ activeTaskId . value } ${ finalStatus === "SUCCESS" ? "已完成" : "执行失败" } ` ;
clearActiveQueueTask ( ) ;
continue ;
if ( activeTaskId . value ) {
const finalStatus = await waitForTaskTerminal ( activeTaskId . value ) ;
queuePushResult . value =
finalStatus === "DELETED"
? "任务已删除"
: ` 任务 ${ activeTaskId . value } ${ finalStatus === "SUCCESS" ? "已完成" : "执行失败" } ` ;
clearActiveQueueTask ( ) ;
return ;
}
const runnable = matchedRunnableItems . value ;
if ( ! runnable . length ) {
ElMessage . warning ( "请先匹配可用店铺" ) ;
return ;
}
const nextItem = pendingQueue . value . shift ( ) ;
saveQueueState ( ) ;
if ( ! nextItem ) break ;
const created = await withTransientRetry (
( ) => createPatrolDeleteTask ( [ buildTaskItem ( nextItem ) ] ) ,
( ) => createPatrolDeleteTask ( runnable . map ( buildTaskItem ) ) ,
( attempt , maxAttempts ) => {
queuePushResult . value = ` 后端服务暂时不可用,正在重试创建任务( ${ attempt } / ${ maxAttempts } ) ...` ;
} ,
) ;
const createdItem = created . items ? . [ 0 ] ;
if ( ! createdItem ? . taskId || ! createdItem . resultId ) {
const createdItems = created . items || [ ] ;
if ( ! created . taskId || ! createdItems . length || createdItems . some ( ( item ) => ! item . taskId || ! item . resultId ) ) {
throw new Error ( "后端未返回有效任务标识" ) ;
}
activeTaskId . value = created . taskId ;
activeQueueItem . value = nextItem ;
saveQueueState ( ) ;
await refreshTaskViews ( ) ;
upsertHistoryItems ( createdItems ) ;
void loadDashboard ( ) ;
const payload = buildQueuePayload ( created . taskId , createdItem ) ;
const payload = buildQueuePayload ( created . taskId , createdItems ) ;
queuePayloadText . value = JSON . stringify ( payload , null , 2 ) ;
const pushResult = await api . enqueue _json ( payload ) ;
if ( ! pushResult ? . success ) {
await submitPatrolDeleteTaskResult ( created . taskId , {
shops : [
{
shopName : createdItem . shopName || nextItem . shopName || "" ,
shops : createdItems . map ( ( createdItem ) => ( {
shopName : createdItem . shopName || "" ,
error : pushResult ? . error || ` 任务 ${ created . taskId } 推送失败 ` ,
countrySections : createdItem . countrySections || [ ] ,
cartRatios : createdItem . cartRatios || [ ] ,
shopDone : true ,
submissionId : createSubmissionId (
created . taskId ,
createdItem . shopName || nextItem . shopName ,
createdItem . shopName ,
) ,
chunkIndex : 1 ,
chunkTotal : 1 ,
} ,
] ,
}) ) ,
} ) ;
removeMatchedRowLocally ( nextI tem) ;
for ( const item of runnable ) removeMatchedRowLocally ( i tem) ;
await refreshTaskViews ( ) ;
queuePushResult . value = ` 任务 ${ created . taskId } 推送失败,已自动继续下一个任务 ` ;
queuePushResult . value = ` 任务 ${ created . taskId } 推送失败 ` ;
clearActiveQueueTask ( ) ;
continue ;
return ;
}
removeMatchedRowLocally ( nextI tem) ;
queuePushResult . value =
pendingQueue . value . length > 0
? ` 任务 ${ created . taskId } 已入队,等待完成后自动继续下一个(剩余 ${ pendingQueue . value . length } 条) `
: ` 任务 ${ created . taskId } 已入队,等待执行完成 ` ;
}
for ( const item of runnable ) removeMatchedRowLocally ( i tem) ;
queuePushResult . value = ` 任务 ${ created . taskId } 已入队,共 ${ createdItems . length } 个店铺,等待执行完成 ` ;
queuePushResult . value = "串行队列已执行完成" ;
const finalStatus = await waitForTaskTerminal ( created . taskId ) ;
queuePushResult . value = ` 任务 ${ created . taskId } ${ finalStatus === "SUCCESS" ? "已完成" : finalStatus === "DELETED" ? "已删除" : "执行失败" } ` ;
clearActiveQueueTask ( ) ;
await refreshTaskViews ( ) ;
ElMessage . success ( "巡店删除队列已按顺序执行 完成" ) ;
ElMessage . success ( "巡店删除任务已 完成" ) ;
} catch ( error ) {
if ( disposed ) return ;
const message = error instanceof Error ? error . message : "队列执行失败" ;
@@ -1000,27 +1149,28 @@ async function processQueue() {
}
async function pushToPythonQueue ( ) {
autoQueueEnabled . value = true ;
const runnable = matchedRunnableItems . value ;
if ( ! runnable . length ) {
ElMessage . warning ( "请先匹配可用店铺" ) ;
return ;
}
pendingQueue . value = mergeQueueItems ( pendingQueue . value , runnable ) ;
saveQueueState ( ) ;
queuePushResult . value = ` 已加入 ${ runnable . length } 条店铺,开始串行执行 ` ;
queuePushResult . value = ` 开始创建任务,共 ${ runnable . length } 个店铺 ` ;
await processQueue ( ) ;
}
async function downloadResult ( item : PatrolDeleteHistoryItem ) {
if ( ! item . resultId ) return ;
const downloadable = taskGroupItems ( item ) . find ( ( row ) =>
Boolean ( row . resultId && ( row . fileReady || row . downloadUrl ) ) ,
) ;
if ( ! downloadable ? . resultId ) return ;
const api = getPywebviewApi ( ) ;
if ( ! api ? . save _file _from _url _new ) {
ElMessage . error ( "当前客户端未提供下载能力" ) ;
return ;
}
const url = getPatrolDeleteResultDownloadUrl ( item . resultId ) ;
const filename = item . outputFilename || ` ${ item . shopName || "result" } .xlsx ` ;
const url = getPatrolDeleteResultDownloadUrl ( downloadable . resultId ) ;
const filename =
downloadable . outputFilename || ` ${ item . shopName || "patrol-delete-result" } .xlsx ` ;
const result = await api . save _file _from _url _new ( url , filename ) ;
if ( result . success ) {
ElMessage . success ( ` 已保存: ${ result . path || filename } ` ) ;
@@ -1041,7 +1191,8 @@ async function deleteTaskRecord(item: PatrolDeleteHistoryItem) {
} else {
throw new Error ( "缺少可删除的任务标识" ) ;
}
await Promise . all( [ loadCandidates ( ) , refreshTaskViews ( ) ] ) ;
removeHistoryItemLoc ally ( item ) ;
await Promise . allSettled ( [ loadCandidates ( ) , refreshTaskViews ( ) ] ) ;
ElMessage . success ( "已删除" ) ;
} catch ( error ) {
if ( isRecordMissingError ( error ) ) {
@@ -1060,14 +1211,13 @@ async function deleteTaskRecord(item: PatrolDeleteHistoryItem) {
onMounted ( async ( ) => {
loadMatchedItems ( ) ;
loadQueueState ( ) ;
await Promise . all ( [ loadCandidates ( ) , loadDashboard ( ) , loadHistory ( ) ] ) ;
await Promise . all ( [ loadCandidates ( ) , loadConditions ( ) , loadDashboard ( ) , loadHistory ( ) ] ) ;
if ( activeTaskId . value || pendingQueue . value . length ) {
autoQueueEnabled . value = true ;
if ( activeTaskId . value ) {
queuePushResult . value =
activeTaskId . value != null
? ` 检测到未完成队列 ,继续等待任务 ${ activeTaskId . value } 完成并自动接续后续店铺 `
: ` 检测到未完成队列,继续执行剩余 ${ pendingQueue . value . length } 条店铺 ` ;
? ` 检测到未完成任务 ,继续等待任务 ${ activeTaskId . value } 完成 `
: "" ;
void processQueue ( ) ;
}
} ) ;
@@ -1096,6 +1246,16 @@ onUnmounted(() => {
. empty - candidates { color : # 666 ; font - size : 13 px ; padding : 16 px ; border : 1 px dashed # 333 ; border - radius : 8 px ; margin - bottom : 16 px ; }
. candidate - table - scroll { max - height : 320 px ; overflow : auto ; margin - bottom : 18 px ; border - radius : 8 px ; border : 1 px solid # 2 a2a2a ; }
. candidate - table { -- el - table - bg - color : # 252525 ; -- el - table - tr - bg - color : # 252525 ; -- el - table - header - bg - color : # 2 a2a2a ; -- el - table - text - color : # ccc ; -- el - table - border - color : # 333 ; }
. condition - panel { margin - bottom : 18 px ; border : 1 px solid # 2 a2a2a ; border - radius : 8 px ; background : # 222 ; padding : 12 px ; }
. condition - input - row { display : flex ; gap : 10 px ; align - items : center ; margin - bottom : 10 px ; }
. condition - input - row : deep ( . el - input ) { flex : 1 ; }
. empty - conditions { color : # 666 ; font - size : 12 px ; padding : 10 px 4 px ; }
. condition - list { list - style : none ; margin : 0 ; padding : 0 ; max - height : 150 px ; overflow : auto ; display : flex ; flex - direction : column ; gap : 8 px ; }
. condition - item { display : flex ; align - items : center ; justify - content : space - between ; gap : 10 px ; padding : 8 px 6 px ; border - bottom : 1 px solid # 303030 ; }
. condition - item : last - child { border - bottom : none ; }
. condition - check { display : flex ; align - items : center ; gap : 8 px ; min - width : 0 ; color : # cfd6df ; font - size : 12 px ; cursor : pointer ; }
. condition - check input { width : 16 px ; height : 16 px ; margin : 0 ; flex : 0 0 auto ; }
. condition - check span { overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. link - danger { background : none ; border : none ; color : # e74c3c ; cursor : pointer ; font - size : 12 px ; padding : 0 ; }
. link - danger : hover { text - decoration : underline ; }
. run - row { display : flex ; flex - wrap : wrap ; gap : 10 px ; margin - top : 8 px ; }