diff --git a/src/components/pages/BatchJobManager.vue b/src/components/pages/BatchJobManager.vue index deb2810..dd64868 100644 --- a/src/components/pages/BatchJobManager.vue +++ b/src/components/pages/BatchJobManager.vue @@ -18,28 +18,28 @@
-
0
+
{{ jobs.filter(j => j.status === 'running').length }}
运行中任务
-
0
+
{{ jobs.filter(j => ['completed', 'completed_with_errors'].includes(j.status)).length }}
已完成
-
0
+
{{ jobs.filter(j => j.status === 'pending').length }}
排队中
-
0
+
{{ jobs.filter(j => j.status === 'failed').length }}
失败/异常
@@ -52,9 +52,9 @@
+ - - +
@@ -114,8 +114,8 @@ @@ -202,15 +202,16 @@ @@ -279,7 +280,7 @@ const cadSettings = ref({ }, revit: { strategy: 'shell_execute', - params: { lod: '粗糙', removeInternal: true } + params: { mode: 'EnvelopeOnly', backupOriginal: false } }, pdms: { strategy: 'pdms_simplify', @@ -306,7 +307,7 @@ const detectedCadTypes = computed(() => { const newModelPath = ref('') -// 模拟列表数据 - 配合后端的 queued -> dispatching -> waiting_callback -> succeeded/failed 状态 +// 队列数据及过滤 const jobs = ref([]) const filteredJobs = computed(() => { @@ -315,15 +316,129 @@ const filteredJobs = computed(() => { }) // === WebSocket 模拟与生命周期 === -// 这里为后续真实接入 WebSocket 通道(submit_batch_tasks / batch_item_update)留出框架 +const activeBatchIds = new Set() +let pollTimer = null + +const handleWsMessage = (msg) => { + if (msg.type === 'batch_created' || msg.type === 'batch_completed') { + if (msg.data && msg.data.batch) { + updateBatchInJobs(msg.data.batch, msg.data.items || []) + } + } else if (msg.type === 'batch_item_update') { + if (msg.data && msg.data.batch && msg.data.item) { + mergeBatchItemUpdate(msg.data.batch, msg.data.item) + } + } else if (msg.type === 'info' && msg.data && msg.data.batch) { + // Check if backend uses generic info or batch_status_response for updates + updateBatchInJobs(msg.data.batch, msg.data.items || []) + } +} + +const isTerminalBatchStatus = (status) => { + return status === 'completed' || status === 'completed_with_errors' || status === 'failed' +} + +const mapItemData = (it) => { + return { + id: it.id, + modelPath: it.model_path, + taskType: it.task_type, + status: it.status, + retry: `${it.retry_count || 0}/${it.max_retries || 1}`, + errorMessage: it.error_message || '', + finishedAt: it.finished_at || it.started_at || it.created_at || '-' + } +} + +const updateBatchInJobs = (batchData, itemsData) => { + const existingJob = jobs.value.find(j => j.batchId === batchData.id) + + const mappedItems = itemsData.map(mapItemData).sort((a,b) => { + const aItem = itemsData.find(i => i.id === a.id) + const bItem = itemsData.find(i => i.id === b.id) + return (aItem?.sequence || 0) - (bItem?.sequence || 0) + }) + + const progressCalc = batchData.total_count ? Math.round(((batchData.succeeded_count + batchData.failed_count) / batchData.total_count) * 100) : 0 + + if (existingJob) { + existingJob.status = batchData.status + existingJob.completedCount = batchData.succeeded_count + batchData.failed_count + existingJob.totalCount = batchData.total_count + existingJob.progress = progressCalc + existingJob.items = mappedItems + } else { + jobs.value.unshift({ + batchId: batchData.id, + strategy: batchData.metadata?.strategy || batchData.name || '批量处理', + priority: batchData.metadata?.priority || '普通', + progress: progressCalc, + status: batchData.status, + totalCount: batchData.total_count || 0, + completedCount: (batchData.succeeded_count || 0) + (batchData.failed_count || 0), + createdAt: new Date(batchData.created_at).toLocaleString(), + items: mappedItems, + rawBatch: batchData + }) + } + + managePolling(batchData) +} + +const mergeBatchItemUpdate = (batchData, itemData) => { + const existingJob = jobs.value.find(j => j.batchId === batchData.id) + if (!existingJob) { + // 如果还没记录,可能错过了 batch_created,进行一次获取全量状态 + websocketService.send({ type: 'get_batch_status', batch_id: batchData.id }) + return + } + + existingJob.status = batchData.status + existingJob.completedCount = batchData.succeeded_count + batchData.failed_count + existingJob.totalCount = batchData.total_count + existingJob.progress = batchData.total_count ? Math.round((existingJob.completedCount / batchData.total_count) * 100) : 0 + + const mappedItem = mapItemData(itemData) + const itemIndex = existingJob.items.findIndex(it => it.id === itemData.id) + + if (itemIndex > -1) { + existingJob.items.splice(itemIndex, 1, mappedItem) + } else { + existingJob.items.push(mappedItem) + } + + managePolling(batchData) +} + +const managePolling = (batchData) => { + if (!isTerminalBatchStatus(batchData.status)) { + activeBatchIds.add(batchData.id) + } else { + activeBatchIds.delete(batchData.id) + } + + if (activeBatchIds.size > 0 && !pollTimer) { + pollTimer = window.setInterval(() => { + activeBatchIds.forEach(id => { + websocketService.send({ type: 'get_batch_status', batch_id: id }) + }) + }, 4000) + } else if (activeBatchIds.size === 0 && pollTimer) { + window.clearInterval(pollTimer) + pollTimer = null + } +} + onMounted(() => { - console.log('BatchJobManager mounted') - // 如果需要监听事件,可以在这里 websocketService.on(...) + websocketService.on('onMessage', handleWsMessage) }) onUnmounted(() => { - console.log('BatchJobManager unmounted') - // websocketService.off(...) + websocketService.off('onMessage', handleWsMessage) + if (pollTimer) { + window.clearInterval(pollTimer) + pollTimer = null + } }) // 向导控制 @@ -340,7 +455,7 @@ const openWizard = () => { }, revit: { strategy: 'shell_execute', - params: { lod: '粗糙', removeInternal: true } + params: { mode: 'EnvelopeOnly', backupOriginal: false } }, pdms: { strategy: 'pdms_simplify', @@ -354,7 +469,7 @@ const onStrategyChange = (cadType, val) => { if (val === 'creo_shrinkwrap') { cadSettings.value[cadType].params = { quality: 5, exportFormat: 'step', ignoreSkeletons: true, ignoreQuilts: false, fillHoles: false } } else if (val === 'shell_execute') { - cadSettings.value[cadType].params = { lod: '粗糙', removeInternal: true } + cadSettings.value[cadType].params = { mode: 'EnvelopeOnly', backupOriginal: false } } else if (val.includes('pdms')) { cadSettings.value[cadType].params = { levelFilter: '', removeAttachments: true } } else { @@ -369,7 +484,8 @@ const submitTask = () => { return } - const batchId = `B-${Date.now().toString().slice(-8)}` + const batchName = `B-${Date.now().toString().slice(-8)}` + const strategiesUsed = detectedCadTypes.value.map(t => cadSettings.value[t].strategy).join(', ') || '混合组合策略' // 组装并发送 submit_batch_tasks WebSocket 消息给后端 const items = taskForm.value.modelPaths.map(p => { @@ -380,43 +496,31 @@ const submitTask = () => { strategy = cadSettings.value[type].strategy; params = cadSettings.value[type].params; } + + // 如果是Revit,并且没有特定参数,按接入文档建议 + if (type === 'revit' && (!strategy || strategy === 'revit_shell')) { + strategy = 'shell_execute'; + } + return { model_path: p, task_type: strategy, - params: params + task_params: params // 注意:文档要求 task_params 而不是 params } }) websocketService.send({ type: 'submit_batch_tasks', - batch_name: batchId, + batch_name: batchName, + metadata: { + strategy: strategiesUsed, + priority: taskForm.value.priority + }, items: items }) - // Display logic config - const strategiesUsed = detectedCadTypes.value.map(t => cadSettings.value[t].strategy).join(', ') || '混合组合策略' - - ElMessage.success('批量任务已成功提交至全局串行队列!') + ElMessage.success('批量任务已成功提交至全局串行队列,等待状态同步...') wizardVisible.value = false - - // 将新任务加入前端列表 (未来应该通过后端 batch_created 事件推送进行更新,目前暂留以显示即时反馈) - jobs.value.unshift({ - batchId: batchId, - strategy: strategiesUsed, - priority: taskForm.value.priority, - params: { }, // mix params - progress: 0, - status: 'queued', - totalCount: taskForm.value.modelPaths.length, - completedCount: 0, - createdAt: new Date().toLocaleString(), - items: taskForm.value.modelPaths.map(p => ({ - modelPath: p, - status: 'queued', - errorMessage: '', - finishedAt: '-' - })) - }) } const addModelPath = () => { @@ -425,7 +529,7 @@ const addModelPath = () => { // 按照路由规范,增加简单的扩展名限制校验 const lowerPath = path.toLowerCase(); - const validExtensions = ['.prt', '.asm', '.rvt', '.rfa', '.ifc', 'sam', 'mdb'] + const validExtensions = ['.prt', '.asm', '.rvt', '.rfa', '.ifc', '.sam', '.mdb'] const isValid = validExtensions.some(ext => lowerPath.endsWith(ext) || lowerPath.includes(ext)) if (!isValid) { @@ -447,11 +551,11 @@ const removeModelPath = (index) => { // 格式辅助 const getStatusLabel = (status) => { - const map = { running: '执行中', succeeded: '已完成', queued: '排队中', failed: '失败/异常' } + const map = { pending: '待执行', running: '执行中', completed: '已完成', completed_with_errors: '部分失败', queued: '排队中', failed: '失败/异常' } return map[status] || status } const getStatusType = (status) => { - const map = { running: 'primary', succeeded: 'success', queued: 'info', failed: 'danger' } + const map = { pending: 'info', running: 'primary', completed: 'success', completed_with_errors: 'warning', queued: 'info', failed: 'danger' } return map[status] || 'info' } const getPriorityType = (priority) => { @@ -459,7 +563,8 @@ const getPriorityType = (priority) => { return map[priority] || 'info' } const getProgressStatus = (status) => { - if (status === 'succeeded') return 'success' + if (status === 'completed') return 'success' + if (status === 'completed_with_errors') return 'warning' if (status === 'failed') return 'exception' return '' /* running/queued 无特殊状态效果 */ } @@ -468,8 +573,8 @@ const getProgressStatus = (status) => { const getItemStatusLabel = (status) => { const map = { queued: '排队中', - dispatching: '正在下发', - waiting_callback: '执行中(等待回调)', + dispatching: '处理中', + waiting_callback: '等待插件回调', succeeded: '成功', failed: '失败' }