完成菜单权限添加和软件菜单改造

This commit is contained in:
super
2026-04-12 17:15:15 +08:00
parent 7589b4e418
commit e53eaf738e
17 changed files with 814 additions and 297 deletions

View File

@@ -54,6 +54,10 @@
<button type="button" class="btn-run" :disabled="running" @click="submitRun">
{{ running ? '解析中...' : '开始解析' }}
</button>
<button type="button" class="btn-run btn-queue" :disabled="pushing || !queueRunnableItems.length"
@click="pushToPythonQueue">
{{ pushing ? '推送中...' : '推送到 Python 队列' }}
</button>
<span class="loading-msg">
{{ running ? '正在读取删除品牌数据,请稍候…' : '解析后会在右侧展示按国家分组的去重结果' }}
</span>
@@ -133,9 +137,6 @@
<span class="status" :class="getTaskStatusInfo(item).className">
{{ getTaskStatusInfo(item).text }}
</span>
<button v-if="item.openStoreUrl" type="button" class="download" @click="triggerItemRun(item)">
打开紫鸟
</button>
<button v-if="canDownloadTaskResult(item)" type="button" class="download"
@click="downloadTaskResult(item)">
下载
@@ -254,9 +255,11 @@ const queuePayloadText = ref('')
const taskDetails = ref<Record<number, DeleteBrandTaskDetailVo>>({})
const pollTimer = ref<number | null>(null)
const pollingInFlight = ref(false)
const pushing = ref(false)
const chainStarted = ref(false)
const activeItemKey = ref('')
const autoAdvancing = ref(false)
const autoRetryTimer = ref<number | null>(null)
const displayPaths = computed(() => selectedPaths.value.slice(0, 10))
const currentSectionItems = computed(() => {
@@ -375,10 +378,14 @@ const historySectionItems = computed(() =>
),
);
const hasVisibleItems = computed(() => currentSectionItems.value.length > 0 || historySectionItems.value.length > 0)
const queueRunnableItems = computed(() =>
currentSectionItems.value.filter((item) => canPushToQueue(item) && getQueueStatus(item) === '未入队'),
)
function persistSessionTasks() {
if (typeof window !== 'undefined') {
window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
if (sessionTasks.value.length === 0) window.localStorage.removeItem(getStorageKey())
else window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
}
}
@@ -490,7 +497,7 @@ function shouldShowProgress(item: DeleteBrandResultItem) {
return true
}
return false
return { success: false, retryable: true }
}
// 单文件任务:完成/终态后隐藏。
@@ -773,7 +780,7 @@ function canDownloadTaskResult(item: DeleteBrandResultItem) {
if (item.downloadUrl) return true
const taskId = item.taskId
if (!taskId) return false
if (!taskId) return { success: false, retryable: false }
const status = taskDetails.value[taskId]?.task?.status
return status === 'SUCCESS'
}
@@ -782,7 +789,7 @@ function mergeTaskDetailItemsIntoSession(detail: DeleteBrandTaskDetailVo) {
const taskId = detail?.task?.id
if (!taskId) return false
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
if (!sessionTask) return false
if (!sessionTask) return { success: false, retryable: false }
const detailItems = detail?.items || []
let changed = false
@@ -953,6 +960,7 @@ function stopPolling() {
window.clearTimeout(pollTimer.value)
pollTimer.value = null
}
clearAutoRetryTimer()
}
async function uploadPathsToJava(paths: Array<string | BrandExpandFolderItem>) {
@@ -1031,7 +1039,9 @@ async function submitRun() {
const hasMatchedItems = normalizedItems.some((item) => item.matchStatus === 'MATCHED')
const hasPendingItems = normalizedItems.some((item) => item.matchStatus && item.matchStatus !== 'MATCHED')
queuePushResult.value = '解析完成,等待手动点击打开紫鸟进行单任务处理'
queuePushResult.value = hasMatchedItems
? '解析完成,可在左侧点击“推送到 Python 队列”开始串行处理'
: '解析完成,当前没有可推送的已匹配文件'
queuePayloadText.value = ''
if (normalizedItems.length && normalizedItems[0].taskId) {
@@ -1042,7 +1052,7 @@ async function submitRun() {
if (hasPendingItems) {
ElMessage.warning('部分文件尚未命中可用店铺索引,已保留状态信息,请等待后台刷新后重试。')
} else if (hasMatchedItems) {
ElMessage.success('删除品牌解析完成,请手动推送')
ElMessage.success('删除品牌解析完成,请在左侧推送到 Python 队列')
} else {
ElMessage.warning('当前没有可推送的已匹配文件,请先等待店铺索引刷新。')
}
@@ -1143,6 +1153,26 @@ function debugAutoAdvance(step: string, payload?: Record<string, unknown>) {
console.log(`[DeleteBrandAutoAdvance] ${step}`, payload || {})
}
function clearAutoRetryTimer() {
if (autoRetryTimer.value) {
window.clearTimeout(autoRetryTimer.value)
autoRetryTimer.value = null
}
}
function scheduleAutoAdvanceRetry(delayMs = 3000) {
clearAutoRetryTimer()
autoRetryTimer.value = window.setTimeout(() => {
autoRetryTimer.value = null
maybeAutoAdvance().catch(() => undefined)
}, delayMs)
}
function isQueueBusyError(message?: string) {
if (!message) return false
return message.includes('当前店铺正在执行中')
}
function isPythonQueueBusy() {
for (const task of sessionTasks.value) {
for (const item of task.items) {
@@ -1160,9 +1190,13 @@ function getItemKey(item: DeleteBrandResultItem) {
return `task:${item.taskId || 0}:${item.sourceFilename || ''}`
}
function canPushToQueue(item: DeleteBrandResultItem) {
return Boolean(item.taskId && (item.matchStatus === 'MATCHED' || item.matched))
}
function findNextAutoRunnableItem() {
return currentSectionItems.value.find((item) => {
if (!item.openStoreUrl) return false
if (!canPushToQueue(item)) return false
if (getItemKey(item) === activeItemKey.value) return false
const sessionItem = findSessionItem(item.taskId, item)
if (sessionItem?._completed) return false
@@ -1177,20 +1211,20 @@ async function runItem(item: DeleteBrandResultItem, options?: { auto?: boolean }
taskId: item.taskId,
sourceFilename: item.sourceFilename,
})
return false
return { success: false, retryable: true }
}
const api = getPywebviewApi()
const taskId = item.taskId
if (!taskId) return false
if (!taskId) return { success: false, retryable: false }
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
if (!sessionTask) return false
if (!sessionTask) return { success: false, retryable: false }
if (!api?.enqueue_json) {
ElMessage.error('当前环境未启用 pywebview enqueue_json浏览器环境不会推送')
return false
return { success: false, retryable: false }
}
const payload = {
@@ -1216,11 +1250,39 @@ async function runItem(item: DeleteBrandResultItem, options?: { auto?: boolean }
activeItemKey.value = getItemKey(item)
saveSessionTask(taskId, sessionTask.items, qpr, queuePayloadText.value)
ensurePolling(true)
queuePushResult.value = qpr
return { success: true, retryable: false }
} else {
qpr = `推送到 Python 队列失败:${pushResult?.error || '未知错误'}`
const message = pushResult?.error || '未知错误'
qpr = `推送到 Python 队列失败:${message}`
queuePushResult.value = qpr
saveSessionTask(taskId, sessionTask.items, qpr, queuePayloadText.value)
if (options?.auto && isQueueBusyError(message)) {
return { success: false, retryable: true }
}
}
queuePushResult.value = qpr
return Boolean(pushResult?.success)
return { success: false, retryable: false }
}
async function pushToPythonQueue() {
const nextItem = findNextAutoRunnableItem()
if (!nextItem) {
ElMessage.warning('当前没有可推送的已匹配文件')
return
}
pushing.value = true
chainStarted.value = true
clearAutoRetryTimer()
const started = await runItem(nextItem)
pushing.value = false
if (!started.success) {
chainStarted.value = false
activeItemKey.value = ''
return
}
ElMessage.success('已开始按顺序推送到 Python 队列')
}
async function maybeAutoAdvance() {
@@ -1294,7 +1356,15 @@ async function maybeAutoAdvance() {
nextKey: getItemKey(nextItem),
})
const started = await runItem(nextItem, { auto: true })
if (!started) {
if (!started.success) {
if (started.retryable) {
debugAutoAdvance('delay retry next item', {
taskId: nextItem.taskId,
sourceFilename: nextItem.sourceFilename,
})
scheduleAutoAdvanceRetry()
return
}
debugAutoAdvance('stop chain: runItem returned false', {
taskId: nextItem.taskId,
sourceFilename: nextItem.sourceFilename,
@@ -1306,15 +1376,6 @@ async function maybeAutoAdvance() {
}
}
async function triggerItemRun(item: DeleteBrandResultItem) {
chainStarted.value = true
const started = await runItem(item)
if (!started) {
chainStarted.value = false
activeItemKey.value = ''
}
}
onMounted(() => {
loadSessionTasksFromStorage()
const taskIds = getPollingTaskIds()