feat: 新增批量处理任务管理页面,包含任务概览、列表及创建向导。
This commit is contained in:
parent
0c17093e59
commit
e1f745cf21
@ -18,28 +18,28 @@
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon"><i class="fas fa-spinner fa-spin"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-value">{{ jobs.filter(j => j.status === 'running').length }}</div>
|
||||
<div class="stat-label">运行中任务</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon success"><i class="fas fa-check-circle"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-value">{{ jobs.filter(j => ['completed', 'completed_with_errors'].includes(j.status)).length }}</div>
|
||||
<div class="stat-label">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon warning"><i class="fas fa-clock"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-value">{{ jobs.filter(j => j.status === 'pending').length }}</div>
|
||||
<div class="stat-label">排队中</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon danger"><i class="fas fa-times-circle"></i></div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">0</div>
|
||||
<div class="stat-value">{{ jobs.filter(j => j.status === 'failed').length }}</div>
|
||||
<div class="stat-label">失败/异常</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -52,9 +52,9 @@
|
||||
<div class="filter-group">
|
||||
<el-select v-model="statusFilter" placeholder="状态过滤" size="small">
|
||||
<el-option label="全部" value="all" />
|
||||
<el-option label="待执行" value="pending" />
|
||||
<el-option label="运行中" value="running" />
|
||||
<el-option label="排队中" value="queued" />
|
||||
<el-option label="已完成" value="succeeded" />
|
||||
<el-option label="已完成" value="completed" />
|
||||
<el-option label="失败/异常" value="failed" />
|
||||
</el-select>
|
||||
</div>
|
||||
@ -114,8 +114,8 @@
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="scope">
|
||||
<!-- 取消已支持:如果不是终态则允许取消 -->
|
||||
<el-button v-if="['running', 'queued'].includes(scope.row.status)" type="danger" size="small" link @click="cancelJob(scope.row)">取消批次</el-button>
|
||||
<el-button v-if="['succeeded', 'failed'].includes(scope.row.status)" type="primary" size="small" link @click="viewDetails(scope.row)">详情与报告</el-button>
|
||||
<el-button v-if="['running', 'pending'].includes(scope.row.status)" type="danger" size="small" link @click="cancelJob(scope.row)">取消批次</el-button>
|
||||
<el-button v-if="['completed', 'completed_with_errors', 'failed'].includes(scope.row.status)" type="primary" size="small" link @click="viewDetails(scope.row)">详情与报告</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@ -202,15 +202,16 @@
|
||||
</template>
|
||||
|
||||
<template v-else-if="cadSettings[cadType].strategy === 'shell_execute'">
|
||||
<el-form-item label="细节级别 (LOD)">
|
||||
<el-radio-group v-model="cadSettings[cadType].params.lod">
|
||||
<el-radio label="粗糙">粗糙 (Coarse)</el-radio>
|
||||
<el-radio label="中等">中等 (Medium)</el-radio>
|
||||
<el-radio label="精细">精细 (Fine)</el-radio>
|
||||
</el-radio-group>
|
||||
<el-form-item label="处理模式">
|
||||
<el-select v-model="cadSettings[cadType].params.mode" placeholder="请选择模式" style="width: 250px">
|
||||
<el-option label="保守保留 (Conservative)" value="Conservative" />
|
||||
<el-option label="标准平衡 (Standard)" value="Standard" />
|
||||
<el-option label="激进删除 (Aggressive)" value="Aggressive" />
|
||||
<el-option label="仅保留外壳 (EnvelopeOnly)" value="EnvelopeOnly" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="移除内部构件">
|
||||
<el-switch v-model="cadSettings[cadType].params.removeInternal" active-text="是" inactive-text="否" />
|
||||
<el-form-item label="备份原文件">
|
||||
<el-switch v-model="cadSettings[cadType].params.backupOriginal" active-text="是" inactive-text="否" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
@ -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: '失败'
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user