暂存
This commit is contained in:
@@ -101,7 +101,8 @@
|
||||
<ul class="task-list clean-result-list">
|
||||
<li
|
||||
v-for="item in currentSectionItems"
|
||||
:key="`current-${item.resultId || item.taskId || item.sourceFilename}`"
|
||||
:key="`current-${item.taskId || item.resultId || item.sourceFilename}`"
|
||||
|
||||
class="task-item split-result-item"
|
||||
>
|
||||
<div class="left split-result-main">
|
||||
@@ -109,6 +110,7 @@
|
||||
<div class="files">店铺名:{{ item.shopName || '-' }}</div>
|
||||
<div class="files">匹配结果:{{ item.matched ? `已匹配 ${item.shopId || ''}` : '未匹配到紫鸟店铺' }}</div>
|
||||
<div v-if="item.platform" class="files">平台:{{ item.platform }}</div>
|
||||
<div v-if="getQueueStatus(item)" class="files">队列状态:{{ getQueueStatus(item) }}</div>
|
||||
<div v-if="item.countryCount !== undefined && item.matched" class="files">国家数:{{ item.countryCount }}</div>
|
||||
<div v-if="item.totalRows !== undefined && item.matched" class="time">去重后 {{ item.totalRows }} 条</div>
|
||||
<div v-if="shouldShowProgress(item)" class="delete-brand-progress-block">
|
||||
@@ -135,7 +137,7 @@
|
||||
v-if="item.openStoreUrl"
|
||||
type="button"
|
||||
class="download"
|
||||
@click="openShop(item.openStoreUrl)"
|
||||
@click="triggerItemRun(item)"
|
||||
>
|
||||
打开紫鸟
|
||||
</button>
|
||||
@@ -165,7 +167,8 @@
|
||||
<ul class="task-list clean-result-list">
|
||||
<li
|
||||
v-for="item in historySectionItems"
|
||||
:key="`history-${item.resultId || item.taskId || item.sourceFilename}`"
|
||||
:key="`history-${item.taskId || item.resultId || item.sourceFilename}`"
|
||||
|
||||
class="task-item split-result-item"
|
||||
>
|
||||
<div class="left split-result-main">
|
||||
@@ -195,14 +198,7 @@
|
||||
<span class="status" :class="getTaskStatusInfo(item).className">
|
||||
{{ getTaskStatusInfo(item).text }}
|
||||
</span>
|
||||
<button
|
||||
v-if="item.openStoreUrl"
|
||||
type="button"
|
||||
class="download"
|
||||
@click="openShop(item.openStoreUrl)"
|
||||
>
|
||||
打开紫鸟
|
||||
</button>
|
||||
<!-- 历史记录区不再显示打开紫鸟按钮 -->
|
||||
<button
|
||||
v-if="canDownloadTaskResult(item)"
|
||||
type="button"
|
||||
@@ -249,11 +245,24 @@ import {
|
||||
} from '@/shared/api/java-modules'
|
||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||
|
||||
interface StoredCurrentTask {
|
||||
taskId: number
|
||||
items: DeleteBrandResultItem[]
|
||||
createdAt: number
|
||||
queuePushResult?: string
|
||||
queuePayloadText?: string
|
||||
}
|
||||
|
||||
function getStorageKey() {
|
||||
const uid = typeof window !== 'undefined' ? window.localStorage.getItem('uid') || '0' : '0'
|
||||
return `delete-brand:current-tasks:${uid}`
|
||||
}
|
||||
|
||||
const selectedPaths = ref<string[]>([])
|
||||
const uploadedFiles = ref<UploadedJavaFile[]>([])
|
||||
const running = ref(false)
|
||||
const resultItems = ref<DeleteBrandResultItem[]>([])
|
||||
const currentRunItems = ref<DeleteBrandResultItem[]>([])
|
||||
const sessionTasks = ref<StoredCurrentTask[]>([])
|
||||
const historyItems = ref<DeleteBrandResultItem[]>([])
|
||||
const summary = ref<DeleteBrandRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
|
||||
const queuePushResult = ref('')
|
||||
@@ -262,36 +271,65 @@ const taskDetails = ref<Record<number, DeleteBrandTaskDetailVo>>({})
|
||||
const pollTimer = ref<number | null>(null)
|
||||
const pollingInFlight = ref(false)
|
||||
const displayPaths = computed(() => selectedPaths.value.slice(0, 10))
|
||||
const runningTaskIdsInHistory = computed(() => {
|
||||
return historyItems.value
|
||||
.filter(item => {
|
||||
const status = item.taskId ? taskDetails.value[item.taskId]?.task?.status : null;
|
||||
// 优先认后端的实时状态
|
||||
if (status) return status === 'RUNNING';
|
||||
// 如果还没拿到底层详情,看 history 接口带回来的全局任务状态
|
||||
if (item.taskStatus) return item.taskStatus === 'RUNNING';
|
||||
// 最后降级到根据 success/error 猜测(向下兼容)
|
||||
return (item.taskId || 0) > 0 && item.success === false && !item.error;
|
||||
})
|
||||
.map(item => item.taskId);
|
||||
});
|
||||
|
||||
const currentSectionItems = computed(() => {
|
||||
// 当前运行的任务 = 用户手动触发的 + 历史记录里还在跑的
|
||||
const allCurrent = [...currentRunItems.value];
|
||||
|
||||
historyItems.value.forEach(hItem => {
|
||||
if (hItem.taskId && runningTaskIdsInHistory.value.includes(hItem.taskId)) {
|
||||
// 避免重复
|
||||
if (!allCurrent.some(c => c.taskId === hItem.taskId)) {
|
||||
allCurrent.push(hItem);
|
||||
}
|
||||
}
|
||||
const allCurrent: DeleteBrandResultItem[] = [];
|
||||
sessionTasks.value.forEach(task => {
|
||||
allCurrent.push(...task.items);
|
||||
});
|
||||
|
||||
return allCurrent;
|
||||
});
|
||||
|
||||
function loadSessionTasksFromStorage() {
|
||||
try {
|
||||
const raw = typeof window !== 'undefined' ? window.localStorage.getItem(getStorageKey()) : null
|
||||
if (raw) {
|
||||
sessionTasks.value = JSON.parse(raw)
|
||||
if (sessionTasks.value.length > 0) {
|
||||
const lastTask = sessionTasks.value[sessionTasks.value.length - 1]
|
||||
queuePushResult.value = lastTask.queuePushResult || ''
|
||||
queuePayloadText.value = lastTask.queuePayloadText || ''
|
||||
}
|
||||
} else {
|
||||
sessionTasks.value = []
|
||||
}
|
||||
} catch {
|
||||
sessionTasks.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function saveSessionTask(taskId: number, items: DeleteBrandResultItem[], pushResult?: string, payloadText?: string) {
|
||||
// 只将解析成功且匹配成功的任务放入当前队列中,失败的将留在历史记录里
|
||||
const validItems = items.filter(i => i.matched !== false && i.success !== false)
|
||||
if (validItems.length === 0) return
|
||||
|
||||
const newTask: StoredCurrentTask = {
|
||||
taskId,
|
||||
items: validItems,
|
||||
createdAt: Date.now(),
|
||||
queuePushResult: pushResult,
|
||||
queuePayloadText: payloadText
|
||||
}
|
||||
const existingIndex = sessionTasks.value.findIndex(t => t.taskId === taskId)
|
||||
if (existingIndex >= 0) {
|
||||
sessionTasks.value[existingIndex] = newTask
|
||||
} else {
|
||||
sessionTasks.value.push(newTask)
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
|
||||
}
|
||||
}
|
||||
|
||||
function removeSessionTaskFromStorage(taskId: number) {
|
||||
const oldLen = sessionTasks.value.length
|
||||
sessionTasks.value = sessionTasks.value.filter(t => t.taskId !== taskId)
|
||||
if (sessionTasks.value.length !== oldLen && typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const historySectionItems = computed(() =>
|
||||
historyItems.value.filter(
|
||||
(item) => !currentSectionItems.value.some(c => c.taskId === item.taskId || (c.resultId && c.resultId === item.resultId))
|
||||
@@ -312,19 +350,17 @@ function getDisplayError(item: DeleteBrandResultItem) {
|
||||
}
|
||||
|
||||
function shouldShowProgress(item: DeleteBrandResultItem) {
|
||||
const taskId = item.taskId
|
||||
if (!taskId) return false
|
||||
const status = getTaskStatus(taskId)
|
||||
// 只要状态是运行中就显示进度条,不管 matched 了(matched 不对的状态应该在后端就标记为 FAILED)
|
||||
return status === 'RUNNING'
|
||||
const qs = getQueueStatus(item)
|
||||
return qs === '已入队' || qs === '处理中'
|
||||
}
|
||||
|
||||
|
||||
function normalizeDeleteBrandItems(items: DeleteBrandResultItem[]) {
|
||||
return items.map((item) => {
|
||||
if (item.matched === false) {
|
||||
// 只有在既没成功,又显式标记为未匹配时,才做错误兜底
|
||||
if (!item.success && item.matched === false) {
|
||||
return {
|
||||
...item,
|
||||
success: false,
|
||||
error: item.error || '未匹配到紫鸟店铺',
|
||||
openStoreUrl: undefined,
|
||||
}
|
||||
@@ -368,7 +404,9 @@ function formatProgress(taskId: number) {
|
||||
if (info.phase) parts.push(`阶段:${info.phase}`)
|
||||
if (info.file_index && info.file_total) parts.push(`文件:${info.file_index}/${info.file_total}`)
|
||||
if (info.file_name) parts.push(`当前文件:${info.file_name}`)
|
||||
if (info.current_line != null && info.total_lines != null) parts.push(`进度:${info.current_line}/${info.total_lines}`)
|
||||
if (info.current_line != null && info.total_lines != null) {
|
||||
parts.push(`进度:${info.current_line}/${info.total_lines}`)
|
||||
}
|
||||
if (info.current_country) parts.push(`国家:${info.current_country}`)
|
||||
if (info.current_asin) parts.push(`ASIN:${info.current_asin}`)
|
||||
if (info.finished_files != null) parts.push(`已完成文件:${info.finished_files}`)
|
||||
@@ -402,38 +440,58 @@ function isTaskTerminal(taskId: number) {
|
||||
|
||||
function getPollingTaskIds() {
|
||||
const ids = new Set<number>()
|
||||
|
||||
// 1. 检查本会话触发的任务
|
||||
currentRunItems.value.forEach((item) => {
|
||||
if (item.taskId && !isTaskTerminal(item.taskId)) {
|
||||
ids.add(item.taskId)
|
||||
sessionTasks.value.forEach(task => {
|
||||
// 只轮询有文件被推给 Python 的任务
|
||||
if (task.items.some(i => (i as any)._pushed)) {
|
||||
ids.add(task.taskId)
|
||||
}
|
||||
})
|
||||
|
||||
// 2. 检查历史加载的任务
|
||||
historyItems.value.forEach((item) => {
|
||||
if (item.taskId) {
|
||||
if (taskDetails.value[item.taskId]) {
|
||||
// 如果已经拿到了详情,以后端详情为准
|
||||
if (!isTaskTerminal(item.taskId)) {
|
||||
ids.add(item.taskId)
|
||||
}
|
||||
} else {
|
||||
// 还没拿到详情,看 history 接口中的 taskStatus 或 success 标记
|
||||
const isRunning = item.taskStatus === 'RUNNING' || (!item.taskStatus && !item.success && !item.error);
|
||||
if (isRunning) {
|
||||
ids.add(item.taskId)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(ids)
|
||||
}
|
||||
|
||||
function canDownloadTaskResult(item: DeleteBrandResultItem) {
|
||||
function getQueueStatus(item: DeleteBrandResultItem) {
|
||||
const taskId = item.taskId
|
||||
if (!taskId || !item.matched) return false
|
||||
if (!taskId) return ''
|
||||
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
||||
if (!sessionTask) return ''
|
||||
|
||||
const status = getTaskStatus(taskId)
|
||||
if (status === 'SUCCESS' || status === 'FAILED' || item.taskStatus === 'SUCCESS' || item.taskStatus === 'FAILED') {
|
||||
return '已被全局判定为终态'
|
||||
}
|
||||
|
||||
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
||||
|
||||
const info = taskDetails.value[taskId]?.line_progress
|
||||
|
||||
// 此时这个特定文件是否正好被后端报了进度名
|
||||
if (status === 'RUNNING' && info?.has_progress && info?.info?.file_name === item.sourceFilename) {
|
||||
return '处理中'
|
||||
}
|
||||
|
||||
// 查查是否已经完结了这个特定文件
|
||||
const detail = taskDetails.value[taskId]
|
||||
if (detail?.items) {
|
||||
// 后端返回的 items 是 `resultJson` 解析来的,里面只含有已经做完的!
|
||||
const finished = detail.items.find((i: DeleteBrandResultItem) => i.sourceFilename === item.sourceFilename)
|
||||
if (finished) {
|
||||
return '本文件解析完毕'
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionItem && (sessionItem as any)._pushed) {
|
||||
return '已入队'
|
||||
}
|
||||
|
||||
return '未入队'
|
||||
}
|
||||
|
||||
function canDownloadTaskResult(item: DeleteBrandResultItem) {
|
||||
// 核心修复:只要单个项目已经成功(后端返回了下载地址或标记为成功),就允许下载,哪怕整包任务还在“执行中”
|
||||
if (item.downloadUrl || item.success) return true
|
||||
|
||||
const taskId = item.taskId
|
||||
if (!taskId) return false
|
||||
const status = taskDetails.value[taskId]?.task?.status
|
||||
return status === 'SUCCESS'
|
||||
}
|
||||
@@ -477,14 +535,25 @@ function scheduleNextPoll(immediate = false) {
|
||||
if (!taskIds.length) return
|
||||
|
||||
pollingInFlight.value = true
|
||||
let stateChanged = false
|
||||
try {
|
||||
await refreshTaskDetails(taskIds)
|
||||
for (const taskId of taskIds) {
|
||||
if (isTaskTerminal(taskId)) {
|
||||
removeSessionTaskFromStorage(taskId)
|
||||
stateChanged = true
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
pollingInFlight.value = false
|
||||
}
|
||||
|
||||
const stillRunning = taskIds.some((id) => isTaskRunning(id))
|
||||
if (stillRunning) {
|
||||
if (stateChanged) {
|
||||
loadHistory()
|
||||
syncResultState()
|
||||
}
|
||||
|
||||
if (getPollingTaskIds().length > 0) {
|
||||
scheduleNextPoll()
|
||||
}
|
||||
}
|
||||
@@ -598,39 +667,24 @@ async function submitRun() {
|
||||
})),
|
||||
})
|
||||
const normalizedItems = normalizeDeleteBrandItems(result.items || [])
|
||||
currentRunItems.value = normalizedItems
|
||||
const hasUnmatched = normalizedItems.some((item) => !item.matched)
|
||||
|
||||
queuePushResult.value = '解析完成,等待手动点击打开紫鸟进行单任务处理'
|
||||
queuePayloadText.value = ''
|
||||
|
||||
if (normalizedItems.length && normalizedItems[0].taskId) {
|
||||
saveSessionTask(normalizedItems[0].taskId, normalizedItems, queuePushResult.value, queuePayloadText.value)
|
||||
}
|
||||
syncResultState()
|
||||
|
||||
await refreshTaskDetails(getPollingTaskIds())
|
||||
ensurePolling()
|
||||
|
||||
const api = getPywebviewApi()
|
||||
const hasUnmatched = normalizedItems.some((item) => !item.matched)
|
||||
if (!hasUnmatched && api?.enqueue_json) {
|
||||
const payload = {
|
||||
type: 'delete-brand-run',
|
||||
ts: Date.now(),
|
||||
data: { ...result, items: normalizedItems },
|
||||
}
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
||||
const pushResult = await api.enqueue_json(payload)
|
||||
if (pushResult?.success) {
|
||||
queuePushResult.value = `已推送到 Python 队列,当前队列长度:${pushResult.queue_size ?? '-'}`
|
||||
} else {
|
||||
queuePushResult.value = `推送到 Python 队列失败:${pushResult?.error || '未知错误'}`
|
||||
}
|
||||
} else {
|
||||
queuePushResult.value = hasUnmatched
|
||||
? '存在未匹配到紫鸟店铺的文件,已跳过推送到 Python 队列'
|
||||
: '当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)'
|
||||
queuePayloadText.value = ''
|
||||
}
|
||||
|
||||
if (hasUnmatched) {
|
||||
ElMessage.error('存在未匹配到紫鸟店铺的文件,已标记为失败')
|
||||
ElMessage.error('存在未匹配到紫鸟店铺的文件,请检查报错内容。这些文件已自动归档至历史记录。')
|
||||
} else {
|
||||
ElMessage.success('删除品牌解析完成')
|
||||
ElMessage.success('删除品牌解析完成,请手动推送')
|
||||
}
|
||||
|
||||
// 更新历史记录,以展示那些未进入队列的失败项
|
||||
await loadHistory()
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '执行失败')
|
||||
} finally {
|
||||
@@ -679,16 +733,17 @@ async function loadHistory() {
|
||||
}
|
||||
|
||||
function removeRecordFromView(resultId: number) {
|
||||
const removedFromCurrent = currentRunItems.value.find((item) => item.resultId === resultId)
|
||||
const removedTaskId = removedFromCurrent?.taskId
|
||||
|
||||
currentRunItems.value = currentRunItems.value.filter((item) => item.resultId !== resultId)
|
||||
historyItems.value = historyItems.value.filter((item) => item.resultId !== resultId)
|
||||
|
||||
if (removedTaskId && !currentRunItems.value.some((item) => item.taskId === removedTaskId)) {
|
||||
delete taskDetails.value[removedTaskId]
|
||||
sessionTasks.value.forEach(st => {
|
||||
st.items = st.items.filter(item => item.resultId !== resultId)
|
||||
})
|
||||
const oldLen = sessionTasks.value.length
|
||||
sessionTasks.value = sessionTasks.value.filter(st => st.items.length > 0)
|
||||
if (sessionTasks.value.length !== oldLen && typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(getStorageKey(), JSON.stringify(sessionTasks.value))
|
||||
}
|
||||
|
||||
historyItems.value = historyItems.value.filter((item) => item.resultId !== resultId)
|
||||
|
||||
syncResultState()
|
||||
|
||||
if (getPollingTaskIds().length === 0) {
|
||||
@@ -720,11 +775,93 @@ function formatPreview(rows: DeleteBrandPreviewRow[]) {
|
||||
return hiddenCount > 0 ? `${preview.join('、')} 等 ${rows.length} 条` : preview.join('、')
|
||||
}
|
||||
|
||||
function isPythonQueueBusy() {
|
||||
for (const task of sessionTasks.value) {
|
||||
for (const item of task.items) {
|
||||
const qs = getQueueStatus(item)
|
||||
if (qs === '已入队' || qs === '处理中') {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async function triggerItemRun(item: DeleteBrandResultItem) {
|
||||
if (isPythonQueueBusy()) {
|
||||
ElMessage.warning('当前存在正在处理的任务,请等待其完成后再推送下一条!')
|
||||
return
|
||||
}
|
||||
|
||||
if (item.openStoreUrl) {
|
||||
window.open(item.openStoreUrl, '_blank')
|
||||
}
|
||||
|
||||
const taskId = item.taskId
|
||||
if (!taskId) return
|
||||
|
||||
const sessionTask = sessionTasks.value.find(t => t.taskId === taskId)
|
||||
if (!sessionTask) return
|
||||
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.enqueue_json) {
|
||||
ElMessage.error('当前环境未启用 pywebview enqueue_json(浏览器环境不会推送)')
|
||||
return
|
||||
}
|
||||
|
||||
const payload = {
|
||||
type: 'delete-brand-run',
|
||||
ts: Date.now(),
|
||||
data: {
|
||||
taskId: taskId,
|
||||
items: [item]
|
||||
},
|
||||
}
|
||||
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
||||
const pushResult = await api.enqueue_json(payload)
|
||||
let qpr = ''
|
||||
if (pushResult?.success) {
|
||||
qpr = `已单独推送文件 ${item.sourceFilename},当前队列长度:${pushResult.queue_size ?? '-'}`
|
||||
|
||||
// Mark the item as pushed
|
||||
const sessionItem = sessionTask.items.find(i => i.resultId === item.resultId || i.sourceFilename === item.sourceFilename)
|
||||
if (sessionItem) {
|
||||
(sessionItem as any)._pushed = true
|
||||
}
|
||||
saveSessionTask(taskId, sessionTask.items, qpr, queuePayloadText.value)
|
||||
|
||||
ensurePolling(true)
|
||||
} else {
|
||||
qpr = `推送到 Python 队列失败:${pushResult?.error || '未知错误'}`
|
||||
}
|
||||
queuePushResult.value = qpr
|
||||
}
|
||||
|
||||
function openShop(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadSessionTasksFromStorage()
|
||||
const taskIds = getPollingTaskIds()
|
||||
if (taskIds.length > 0) {
|
||||
refreshTaskDetails(taskIds).then(() => {
|
||||
let stateChanged = false
|
||||
for (const taskId of taskIds) {
|
||||
if (isTaskTerminal(taskId)) {
|
||||
removeSessionTaskFromStorage(taskId)
|
||||
stateChanged = true
|
||||
}
|
||||
}
|
||||
if (stateChanged) {
|
||||
syncResultState()
|
||||
}
|
||||
ensurePolling(true)
|
||||
})
|
||||
} else {
|
||||
syncResultState()
|
||||
}
|
||||
loadHistory().catch(() => undefined)
|
||||
})
|
||||
|
||||
|
||||
@@ -203,6 +203,7 @@ export interface DeleteBrandTaskItem {
|
||||
export interface DeleteBrandTaskDetailVo {
|
||||
task: DeleteBrandTaskItem
|
||||
line_progress: DeleteBrandLineProgress
|
||||
items?: DeleteBrandResultItem[]
|
||||
}
|
||||
|
||||
export interface DeleteBrandTaskBatchVo {
|
||||
@@ -384,7 +385,11 @@ export function submitDeleteBrandResult(taskId: number, request: DeleteBrandSubm
|
||||
}
|
||||
|
||||
export function getJavaDownloadUrl(path: string) {
|
||||
const raw = path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}`
|
||||
let raw = path.startsWith('http://') || path.startsWith('https://') ? path : `${JAVA_API_PREFIX}${path}`
|
||||
// 核心修复:pywebview 的 save_file_from_url 需要完整带 schema 的 URL
|
||||
if (!raw.startsWith('http://') && !raw.startsWith('https://') && typeof window !== 'undefined') {
|
||||
raw = `${window.location.origin}${raw}`
|
||||
}
|
||||
const separator = raw.includes('?') ? '&' : '?'
|
||||
return `${raw}${separator}user_id=${encodeURIComponent(String(getCurrentUserId()))}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user