feat(flow): 添加动作队列步骤独立执行功能和媒体分析增强
- 在动作队列参数组件中添加单独执行按钮,允许独立运行指定步骤 - 实现步骤验证和构建逻辑,支持单步执行的参数处理 - 更新媒体分析参数界面,添加高级设置和参数微调功能 - 增加视频分析的置信度、通过标准等配置选项 - 优化任务执行日志显示,添加分析结果的结构化展示 - 集成分页请求工具,改进日志加载性能和用户体验 - 添加执行步骤索引跟踪,确保执行状态正确管理 - 实现防重复ID机制,保证分页数据完整性 - 新增分析结果解析和可视化展示功能 - 扩展媒体分析类型的处理和结果显示
This commit is contained in:
parent
cad4184a2f
commit
6be8bc9bfd
45
src/utils/pagedRequest.js
Normal file
45
src/utils/pagedRequest.js
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
const DEFAULT_PAGE_SIZE = 200
|
||||||
|
const DEFAULT_MAX_PAGES = 1000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads every page from a standard TableDataInfo endpoint. Endpoints that ignore
|
||||||
|
* page parameters are also supported because their first response already reaches total.
|
||||||
|
*/
|
||||||
|
export async function fetchAllPagedRows(fetchPage, params = {}, options = {}) {
|
||||||
|
const pageSize = Math.max(1, Number(options.pageSize || DEFAULT_PAGE_SIZE))
|
||||||
|
const maxPages = Math.max(1, Number(options.maxPages || DEFAULT_MAX_PAGES))
|
||||||
|
const rows = []
|
||||||
|
const seenIds = new Set()
|
||||||
|
let lastResponse = { code: 200, rows: [], total: 0 }
|
||||||
|
|
||||||
|
for (let pageNum = 1; pageNum <= maxPages; pageNum += 1) {
|
||||||
|
const response = await fetchPage({ ...params, pageNum, pageSize })
|
||||||
|
lastResponse = response || lastResponse
|
||||||
|
const batch = Array.isArray(response?.rows) ? response.rows : []
|
||||||
|
let addedCount = 0
|
||||||
|
batch.forEach((row) => {
|
||||||
|
const id = row?.id
|
||||||
|
if (id !== undefined && id !== null && id !== '') {
|
||||||
|
const key = String(id)
|
||||||
|
if (seenIds.has(key)) return
|
||||||
|
seenIds.add(key)
|
||||||
|
}
|
||||||
|
rows.push(row)
|
||||||
|
addedCount += 1
|
||||||
|
})
|
||||||
|
|
||||||
|
const total = Number(response?.total)
|
||||||
|
const hasTotal = Number.isFinite(total) && total >= 0
|
||||||
|
if (addedCount === 0 && hasTotal && rows.length < total) {
|
||||||
|
throw new Error('日志分页没有返回新记录,无法保证数据完整')
|
||||||
|
}
|
||||||
|
if (batch.length === 0
|
||||||
|
|| addedCount === 0
|
||||||
|
|| (hasTotal && rows.length >= total)
|
||||||
|
|| (!hasTotal && batch.length < pageSize)) {
|
||||||
|
return { ...lastResponse, rows, total: hasTotal ? total : rows.length }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`日志记录超过 ${pageSize * maxPages} 条,未能完整加载`)
|
||||||
|
}
|
||||||
@ -15,6 +15,19 @@
|
|||||||
<div class="single-queue-step__header">
|
<div class="single-queue-step__header">
|
||||||
<div class="single-queue-step__title">{{ getActionQueueStepName(step, index) }} · {{ actionName(step.type) }}</div>
|
<div class="single-queue-step__title">{{ getActionQueueStepName(step, index) }} · {{ actionName(step.type) }}</div>
|
||||||
<div class="single-queue-step__actions">
|
<div class="single-queue-step__actions">
|
||||||
|
<el-tooltip content="单独执行此步骤" placement="top">
|
||||||
|
<el-button
|
||||||
|
:icon="VideoPlay"
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
aria-label="单独执行此步骤"
|
||||||
|
:loading="executingStepIndex === index"
|
||||||
|
:disabled="executingStepIndex >= 0 && executingStepIndex !== index"
|
||||||
|
@click="emit('execute-step', index)"
|
||||||
|
/>
|
||||||
|
</el-tooltip>
|
||||||
<el-tooltip content="上移一步" placement="top">
|
<el-tooltip content="上移一步" placement="top">
|
||||||
<el-button :icon="ArrowUp" circle size="small" :disabled="index === 0" aria-label="上移一步" @click="moveStep(index, -1)" />
|
<el-button :icon="ArrowUp" circle size="small" :disabled="index === 0" aria-label="上移一步" @click="moveStep(index, -1)" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@ -44,7 +57,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { ArrowDown, ArrowUp, CopyDocument } from '@element-plus/icons-vue'
|
import { ArrowDown, ArrowUp, CopyDocument, VideoPlay } from '@element-plus/icons-vue'
|
||||||
import { getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
import { getRobotDevicesByRobotId } from '@/api/inspection/robot'
|
||||||
import { DEVICE_KIND } from '@/utils/robotDevice'
|
import { DEVICE_KIND } from '@/utils/robotDevice'
|
||||||
import { copyActionQueueStep, getActionQueueStepName, moveActionQueueStep, toActionQueueExecutionSteps } from '@/utils/actionQueue'
|
import { copyActionQueueStep, getActionQueueStepName, moveActionQueueStep, toActionQueueExecutionSteps } from '@/utils/actionQueue'
|
||||||
@ -57,7 +70,9 @@ const props = defineProps({
|
|||||||
nodeParams: { type: Array, default: () => [] },
|
nodeParams: { type: Array, default: () => [] },
|
||||||
robotOptions: { type: Array, default: () => [] },
|
robotOptions: { type: Array, default: () => [] },
|
||||||
initialRobotId: { type: String, default: '' },
|
initialRobotId: { type: String, default: '' },
|
||||||
|
executingStepIndex: { type: Number, default: -1 },
|
||||||
})
|
})
|
||||||
|
const emit = defineEmits(['execute-step'])
|
||||||
|
|
||||||
/** 动作类型到中文名称的映射,作用范围为当前组件的步骤标题展示。 */
|
/** 动作类型到中文名称的映射,作用范围为当前组件的步骤标题展示。 */
|
||||||
const labels = {
|
const labels = {
|
||||||
@ -178,6 +193,17 @@ const validate = () => {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const validateStep = (index) => {
|
||||||
|
if (!robotId.value) return ElMessage.warning('请选择机器人'), false
|
||||||
|
const step = steps.value[index]
|
||||||
|
if (!step) return ElMessage.warning('未找到要执行的步骤'), false
|
||||||
|
if (deviceKind(step.type) && !step.deviceId) {
|
||||||
|
ElMessage.warning(`步骤 ${index + 1} 没有可用的执行设备`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/** 汇总当前表单数据为运行接口载荷;返回范围包含机器人、队列配置和全部步骤。 */
|
/** 汇总当前表单数据为运行接口载荷;返回范围包含机器人、队列配置和全部步骤。 */
|
||||||
const getPayload = () => ({
|
const getPayload = () => ({
|
||||||
robotId: robotId.value,
|
robotId: robotId.value,
|
||||||
@ -186,6 +212,17 @@ const getPayload = () => ({
|
|||||||
steps: toActionQueueExecutionSteps(steps.value),
|
steps: toActionQueueExecutionSteps(steps.value),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const getStepPayload = (index) => {
|
||||||
|
const step = steps.value[index]
|
||||||
|
const stepTimeoutMs = Math.max(0, Number(step?.timeoutMs || 0))
|
||||||
|
return {
|
||||||
|
robotId: robotId.value,
|
||||||
|
actionId: '',
|
||||||
|
totalTimeoutMs: Math.max(1000, stepTimeoutMs + 5000),
|
||||||
|
steps: toActionQueueExecutionSteps(step ? [step] : []),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 返回当前选中的机器人 ID,供父组件处理兼容字段或运行上下文。 */
|
/** 返回当前选中的机器人 ID,供父组件处理兼容字段或运行上下文。 */
|
||||||
const getRobotId = () => robotId.value
|
const getRobotId = () => robotId.value
|
||||||
|
|
||||||
@ -193,7 +230,7 @@ const getRobotId = () => robotId.value
|
|||||||
watch(() => [props.nodeParams, props.initialRobotId], initData, { immediate: true, deep: true })
|
watch(() => [props.nodeParams, props.initialRobotId], initData, { immediate: true, deep: true })
|
||||||
|
|
||||||
/** 向父组件开放提交前校验、载荷读取和机器人 ID 读取能力。 */
|
/** 向父组件开放提交前校验、载荷读取和机器人 ID 读取能力。 */
|
||||||
defineExpose({ validate, getPayload, getRobotId })
|
defineExpose({ validate, validateStep, getPayload, getStepPayload, getRobotId })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -4,6 +4,47 @@
|
|||||||
<div class="task-log__time">{{ formatDate(log.logTime) }}</div>
|
<div class="task-log__time">{{ formatDate(log.logTime) }}</div>
|
||||||
<div class="task-log__body">
|
<div class="task-log__body">
|
||||||
<div class="task-log__content">{{ log.logContent }}</div>
|
<div class="task-log__content">{{ log.logContent }}</div>
|
||||||
|
<div v-if="analysisOf(log)" class="task-log__analysis">
|
||||||
|
<div class="task-log__analysis-heading">
|
||||||
|
<el-tag :type="analysisOf(log).tagType" size="small">
|
||||||
|
{{ analysisOf(log).title }} · {{ analysisOf(log).statusLabel }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-if="analysisOf(log).modelName" class="task-log__analysis-meta">
|
||||||
|
模型:{{ analysisOf(log).modelName }}
|
||||||
|
</span>
|
||||||
|
<span v-if="analysisOf(log).timingMs !== null" class="task-log__analysis-meta">
|
||||||
|
耗时:{{ analysisOf(log).timingMs }}ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="task-log__analysis-grid">
|
||||||
|
<div v-if="analysisOf(log).label" class="task-log__analysis-field">
|
||||||
|
<span>识别结果</span>
|
||||||
|
<strong>{{ analysisOf(log).label }}</strong>
|
||||||
|
</div>
|
||||||
|
<div v-if="analysisOf(log).matchLabel" class="task-log__analysis-field">
|
||||||
|
<span>匹配状态</span>
|
||||||
|
<strong>{{ analysisOf(log).matchLabel }}</strong>
|
||||||
|
</div>
|
||||||
|
<div v-if="analysisOf(log).confidence !== null" class="task-log__analysis-field">
|
||||||
|
<span>置信度</span>
|
||||||
|
<strong>{{ analysisOf(log).confidence }}%</strong>
|
||||||
|
</div>
|
||||||
|
<div v-if="analysisOf(log).passedLabel" class="task-log__analysis-field">
|
||||||
|
<span>判断结果</span>
|
||||||
|
<strong>{{ analysisOf(log).passedLabel }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="analysisOf(log).summary" class="task-log__analysis-summary">
|
||||||
|
{{ analysisOf(log).summary }}
|
||||||
|
</div>
|
||||||
|
<div v-if="analysisOf(log).events.length" class="task-log__analysis-events">
|
||||||
|
<div class="task-log__analysis-label">关键事件</div>
|
||||||
|
<div v-for="(event, eventIndex) in analysisOf(log).events" :key="eventIndex" class="task-log__analysis-event">
|
||||||
|
{{ event }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre v-if="analysisOf(log).details" class="task-log__analysis-details">{{ analysisOf(log).details }}</pre>
|
||||||
|
</div>
|
||||||
<div v-if="mediaOf(log).length" class="task-log__media">
|
<div v-if="mediaOf(log).length" class="task-log__media">
|
||||||
<template v-for="media in mediaOf(log)" :key="`${media.type}:${media.url}`">
|
<template v-for="media in mediaOf(log)" :key="`${media.type}:${media.url}`">
|
||||||
<el-image
|
<el-image
|
||||||
@ -58,6 +99,77 @@ const addMedia = (result, seen, type, value) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const analysisCache = new WeakMap()
|
||||||
|
const analysisOf = (log) => {
|
||||||
|
if (!log || typeof log !== 'object') return null
|
||||||
|
if (analysisCache.has(log)) return analysisCache.get(log)
|
||||||
|
const output = {
|
||||||
|
...parseObject(log.actualResult),
|
||||||
|
...parseObject(log.extraInfo),
|
||||||
|
}
|
||||||
|
const analysisType = String(output.analysisType || '').toUpperCase()
|
||||||
|
const profileCode = String(output.profileCode || '').toLowerCase()
|
||||||
|
const isAudio = analysisType.includes('AUDIO') || profileCode.includes('audio') || Number(log.logType) === 4
|
||||||
|
const isVideo = analysisType.includes('VIDEO') || profileCode.includes('video') || Number(log.logType) === 3
|
||||||
|
const hasResult = Boolean(
|
||||||
|
output.analysisStatus
|
||||||
|
|| output.analysisType
|
||||||
|
|| output.profileCode
|
||||||
|
|| output.label
|
||||||
|
|| output.labelName
|
||||||
|
|| output.summary
|
||||||
|
|| output.conclusion
|
||||||
|
|| output.passed !== undefined
|
||||||
|
)
|
||||||
|
if (!hasResult || (!isAudio && !isVideo)) {
|
||||||
|
analysisCache.set(log, null)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const status = String(output.analysisStatus || '').toUpperCase()
|
||||||
|
const matched = output.matched
|
||||||
|
const passed = output.passed
|
||||||
|
const confidenceValue = Number(output.confidence)
|
||||||
|
const confidence = Number.isFinite(confidenceValue)
|
||||||
|
? Math.max(0, Math.min(100, confidenceValue <= 1 ? confidenceValue * 100 : confidenceValue)).toFixed(1)
|
||||||
|
: null
|
||||||
|
const statusLabel = status === 'SUCCEEDED' || status === 'SUCCESS'
|
||||||
|
|| status === 'COMPLETED'
|
||||||
|
? '分析完成'
|
||||||
|
: status === 'FAILED' || status === 'ERROR'
|
||||||
|
? '分析失败'
|
||||||
|
: status || '已返回结果'
|
||||||
|
const detailSource = { ...output }
|
||||||
|
;[
|
||||||
|
'analysisStatus', 'analysisType', 'profileCode', 'analysisModel', 'analysisTimingMs',
|
||||||
|
'mediaUrl', 'audioUrl', 'videoUrl', 'imageUrl', 'label', 'labelName', 'matched',
|
||||||
|
'confidence', 'passed', 'summary', 'conclusion', 'events',
|
||||||
|
].forEach((key) => delete detailSource[key])
|
||||||
|
const hasDetails = Object.keys(detailSource).length > 0
|
||||||
|
const result = {
|
||||||
|
title: isAudio ? '声音分析' : '视频分析',
|
||||||
|
statusLabel,
|
||||||
|
tagType: status === 'FAILED' || status === 'ERROR' || passed === false
|
||||||
|
? 'danger'
|
||||||
|
: matched === false ? 'warning' : 'success',
|
||||||
|
modelName: typeof output.analysisModel === 'string'
|
||||||
|
? output.analysisModel
|
||||||
|
: output.analysisModel?.name || '',
|
||||||
|
timingMs: Number.isFinite(Number(output.analysisTimingMs)) ? Number(output.analysisTimingMs) : null,
|
||||||
|
label: output.labelName || output.label || '',
|
||||||
|
matchLabel: matched === true ? '匹配' : matched === false ? '未匹配' : '',
|
||||||
|
confidence,
|
||||||
|
passedLabel: passed === true ? '通过' : passed === false ? '未通过' : '',
|
||||||
|
summary: output.summary || output.conclusion || '',
|
||||||
|
events: Array.isArray(output.events)
|
||||||
|
? output.events.slice(0, 20).map((event) => typeof event === 'string' ? event : JSON.stringify(event))
|
||||||
|
: [],
|
||||||
|
details: hasDetails ? JSON.stringify(detailSource, null, 2) : '',
|
||||||
|
}
|
||||||
|
analysisCache.set(log, result)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
const mediaOf = (log) => {
|
const mediaOf = (log) => {
|
||||||
if (!log || typeof log !== 'object') return []
|
if (!log || typeof log !== 'object') return []
|
||||||
if (mediaCache.has(log)) return mediaCache.get(log)
|
if (mediaCache.has(log)) return mediaCache.get(log)
|
||||||
@ -70,6 +182,10 @@ const mediaOf = (log) => {
|
|||||||
addMedia(result, seen, 'image', output.imageUrl)
|
addMedia(result, seen, 'image', output.imageUrl)
|
||||||
addMedia(result, seen, 'video', output.videoUrl)
|
addMedia(result, seen, 'video', output.videoUrl)
|
||||||
addMedia(result, seen, 'audio', output.audioUrl)
|
addMedia(result, seen, 'audio', output.audioUrl)
|
||||||
|
const analysisType = String(output.analysisType || '').toUpperCase()
|
||||||
|
const profileCode = String(output.profileCode || '').toLowerCase()
|
||||||
|
if (analysisType.includes('VIDEO') || profileCode.includes('video')) addMedia(result, seen, 'video', output.mediaUrl)
|
||||||
|
if (analysisType.includes('AUDIO') || profileCode.includes('audio')) addMedia(result, seen, 'audio', output.mediaUrl)
|
||||||
addMedia(result, seen, 'image', log.screenshotUrl)
|
addMedia(result, seen, 'image', log.screenshotUrl)
|
||||||
addMedia(result, seen, 'video', log.videoUrl)
|
addMedia(result, seen, 'video', log.videoUrl)
|
||||||
const legacyType = Number(log.logType) === 2 ? 'image'
|
const legacyType = Number(log.logType) === 2 ? 'image'
|
||||||
@ -128,6 +244,80 @@ onMounted(scrollToBottom)
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-log__analysis {
|
||||||
|
margin-top: 9px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-left: 3px solid #5b8ff9;
|
||||||
|
background: #f6f8fc;
|
||||||
|
color: #344054;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-heading {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-meta,
|
||||||
|
.task-log__analysis-field span,
|
||||||
|
.task-log__analysis-label {
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-field {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-field strong {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 13px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-summary {
|
||||||
|
margin-top: 9px;
|
||||||
|
color: #344054;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-events {
|
||||||
|
margin-top: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-event {
|
||||||
|
margin-top: 3px;
|
||||||
|
color: #475467;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-details {
|
||||||
|
max-height: 180px;
|
||||||
|
margin: 9px 0 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 8px;
|
||||||
|
background: #fff;
|
||||||
|
color: #475467;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.task-log__media {
|
.task-log__media {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@ -158,5 +348,9 @@ onMounted(scrollToBottom)
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.task-log__analysis-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -33,6 +33,9 @@
|
|||||||
<span class="queue-step__action">{{ actionLabel(step.type) }}</span>
|
<span class="queue-step__action">{{ actionLabel(step.type) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="queue-step__actions" @click.stop>
|
<div class="queue-step__actions" @click.stop>
|
||||||
|
<el-tooltip content="单独执行此步骤" placement="top">
|
||||||
|
<el-button :icon="VideoPlay" circle size="small" type="primary" plain aria-label="单独执行此步骤" @click="runStep(index)" />
|
||||||
|
</el-tooltip>
|
||||||
<el-tooltip content="上移一步" placement="top">
|
<el-tooltip content="上移一步" placement="top">
|
||||||
<el-button :icon="ArrowUp" circle size="small" :disabled="index === 0" aria-label="上移一步" @click="moveStep(index, -1)" />
|
<el-button :icon="ArrowUp" circle size="small" :disabled="index === 0" aria-label="上移一步" @click="moveStep(index, -1)" />
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@ -141,7 +144,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, ref, watch } from 'vue'
|
import { reactive, ref, watch } from 'vue'
|
||||||
import { ArrowDown, ArrowUp, CopyDocument, Delete, Location, MoreFilled, Plus } from '@element-plus/icons-vue'
|
import { ArrowDown, ArrowUp, CopyDocument, Delete, Location, MoreFilled, Plus, VideoPlay } from '@element-plus/icons-vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getAgvStations, getAgvStatus, getArmJointState, getArmStatus } from '@/api/flow/flow'
|
import { getAgvStations, getAgvStatus, getArmJointState, getArmStatus } from '@/api/flow/flow'
|
||||||
import { getRobotDevicesByRobotId, getRobotList } from '@/api/inspection/robot'
|
import { getRobotDevicesByRobotId, getRobotList } from '@/api/inspection/robot'
|
||||||
@ -435,6 +438,48 @@ const parseJsonFields = (step) => {
|
|||||||
return params
|
return params
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildStandaloneStep = (step, index) => {
|
||||||
|
if (!step?.type) throw new Error(`步骤 ${index + 1} 未选择动作`)
|
||||||
|
const params = parseJsonFields(step)
|
||||||
|
fieldsForType(step.type).filter((field) => field.required !== false).forEach((field) => {
|
||||||
|
if (params[field.key] === '' || params[field.key] == null) {
|
||||||
|
throw new Error(`步骤 ${index + 1} 的${field.label}不能为空`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
stepId: 'step_1',
|
||||||
|
stepName: getActionQueueStepName(step, index),
|
||||||
|
type: step.type,
|
||||||
|
timeoutMs: Number(step.timeoutMs || 0),
|
||||||
|
params,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const runStep = (index) => {
|
||||||
|
try {
|
||||||
|
const step = formData.steps[index]
|
||||||
|
const executionStep = buildStandaloneStep(step, index)
|
||||||
|
const stepTimeoutMs = Math.max(0, Number(executionStep.timeoutMs || 0))
|
||||||
|
document.dispatchEvent(new CustomEvent('singleNodeExecution', {
|
||||||
|
bubbles: false,
|
||||||
|
cancelable: false,
|
||||||
|
detail: {
|
||||||
|
name: getActionQueueStepName(step, index),
|
||||||
|
nodeType: 'EDGE',
|
||||||
|
action: 'SYSTEM_EXECUTE_ACTION_QUEUE',
|
||||||
|
nodeId: `${props.data.id}:queue-step:${step.localKey || index}`,
|
||||||
|
nodeParams: [
|
||||||
|
{ name: 'actionId', type: 'input', input: '', required: false },
|
||||||
|
{ name: 'totalTimeoutMs', type: 'input', componentType: 'number', input: Math.max(1000, stepTimeoutMs + 5000), required: true },
|
||||||
|
{ name: 'steps', type: 'input', input: [executionStep], required: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.warning(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const buildQueueDefinition = () => {
|
const buildQueueDefinition = () => {
|
||||||
if (!formData.executionTarget.roleKey) throw new Error('请选择执行机器人角色')
|
if (!formData.executionTarget.roleKey) throw new Error('请选择执行机器人角色')
|
||||||
if (!formData.steps.length) throw new Error('请添加至少一个动作步骤')
|
if (!formData.steps.length) throw new Error('请添加至少一个动作步骤')
|
||||||
|
|||||||
@ -50,21 +50,51 @@
|
|||||||
<el-input v-model="formData.mediaUrl" type="textarea" :rows="3" placeholder="请输入 HTTP/HTTPS 媒体地址" />
|
<el-input v-model="formData.mediaUrl" type="textarea" :rows="3" placeholder="请输入 HTTP/HTTPS 媒体地址" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="isVideo" label="补充分析要求">
|
<el-form-item v-if="isVideo" label="通过标准" prop="instruction" required>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="formData.instruction"
|
v-model="formData.instruction"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="4"
|
:rows="4"
|
||||||
maxlength="2000"
|
maxlength="2000"
|
||||||
show-word-limit
|
show-word-limit
|
||||||
placeholder="可选,例如:判断仪表是否点亮,并说明依据"
|
placeholder="例如:车辆启动后,仪表必须在3秒内点亮"
|
||||||
/>
|
/>
|
||||||
|
<div class="field-hint">未观察到完整证据或置信度不足时,结果按未通过处理。</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item v-if="isVideo" label="分析模式">
|
<el-form-item v-if="isVideo" label="分析模式">
|
||||||
<el-segmented v-model="formData.analysisMode" :options="analysisModes" block />
|
<el-segmented v-model="formData.analysisMode" :options="analysisModes" block @change="handleAnalysisModeChange" />
|
||||||
<div class="field-hint">自动模式优先快速分析,结果不完整时自动切换精确分析。</div>
|
<div class="field-hint">{{ modeDescription }}</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-collapse v-if="isVideo" v-model="advancedSections" class="analysis-tuning">
|
||||||
|
<el-collapse-item name="tuning">
|
||||||
|
<template #title>
|
||||||
|
<div class="analysis-tuning__title">
|
||||||
|
<span>高级设置</span>
|
||||||
|
<el-tag v-if="isTuned" size="small" type="warning" effect="plain">已微调</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="analysis-tuning__body">
|
||||||
|
<div class="analysis-tuning__toolbar">
|
||||||
|
<span>当前模式参数</span>
|
||||||
|
<el-button :icon="Refresh" text type="primary" @click.stop="restorePreset">恢复默认</el-button>
|
||||||
|
</div>
|
||||||
|
<el-form-item label="抽取帧数">
|
||||||
|
<el-slider v-model="formData.analysisTuning.maxFrames" :min="2" :max="16" :step="1" show-input />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="图片最大宽度">
|
||||||
|
<el-slider v-model="formData.analysisTuning.maxWidth" :min="640" :max="1280" :step="64" show-input />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="通过置信度">
|
||||||
|
<el-slider v-model="formData.analysisTuning.confidenceThreshold" :min="0.5" :max="0.95" :step="0.05" show-input />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="formData.analysisMode === 'AUTO'" label="低置信度转精确分析">
|
||||||
|
<el-switch v-model="formData.analysisTuning.fallbackToAccurate" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
</el-form>
|
</el-form>
|
||||||
|
|
||||||
<el-divider content-position="left">节点输出</el-divider>
|
<el-divider content-position="left">节点输出</el-divider>
|
||||||
@ -79,6 +109,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
import { Refresh } from '@element-plus/icons-vue'
|
||||||
import { getInput } from '@/utils/flow'
|
import { getInput } from '@/utils/flow'
|
||||||
|
|
||||||
const props = defineProps({ data: Object })
|
const props = defineProps({ data: Object })
|
||||||
@ -87,6 +118,12 @@ const emit = defineEmits(['save-success', 'save-error'])
|
|||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const mediaCascaderRef = ref()
|
const mediaCascaderRef = ref()
|
||||||
const quoteOptions = ref([])
|
const quoteOptions = ref([])
|
||||||
|
const advancedSections = ref([])
|
||||||
|
const analysisPresets = {
|
||||||
|
FAST: { maxFrames: 6, maxWidth: 896, confidenceThreshold: 0.7, fallbackToAccurate: false },
|
||||||
|
AUTO: { maxFrames: 8, maxWidth: 960, confidenceThreshold: 0.75, fallbackToAccurate: true },
|
||||||
|
ACCURATE: { maxFrames: 10, maxWidth: 1120, confidenceThreshold: 0.8, fallbackToAccurate: false },
|
||||||
|
}
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
profileCode: '',
|
profileCode: '',
|
||||||
mediaMode: 'quote',
|
mediaMode: 'quote',
|
||||||
@ -95,6 +132,7 @@ const formData = reactive({
|
|||||||
mediaUrl: '',
|
mediaUrl: '',
|
||||||
instruction: '',
|
instruction: '',
|
||||||
analysisMode: 'AUTO',
|
analysisMode: 'AUTO',
|
||||||
|
analysisTuning: { ...analysisPresets.AUTO },
|
||||||
})
|
})
|
||||||
|
|
||||||
const isVideo = computed(() => props.data?.properties?.action === 'VIDEO_ANALYZE')
|
const isVideo = computed(() => props.data?.properties?.action === 'VIDEO_ANALYZE')
|
||||||
@ -109,16 +147,28 @@ const mediaModes = [
|
|||||||
{ label: '直接输入地址', value: 'input' },
|
{ label: '直接输入地址', value: 'input' },
|
||||||
]
|
]
|
||||||
const analysisModes = [
|
const analysisModes = [
|
||||||
{ label: '自动', value: 'AUTO' },
|
|
||||||
{ label: '快速', value: 'FAST' },
|
{ label: '快速', value: 'FAST' },
|
||||||
|
{ label: '均衡', value: 'AUTO' },
|
||||||
{ label: '精确', value: 'ACCURATE' },
|
{ label: '精确', value: 'ACCURATE' },
|
||||||
]
|
]
|
||||||
|
const modeDescriptions = {
|
||||||
|
FAST: '使用轻量模型直接判断,适合动作明显、批量执行的场景。',
|
||||||
|
AUTO: '先快速判断,低置信度时自动切换精确分析。',
|
||||||
|
ACCURATE: '使用高精度模型分析更多画面细节,耗时较长。',
|
||||||
|
}
|
||||||
|
const modeDescription = computed(() => modeDescriptions[formData.analysisMode] || '')
|
||||||
|
const isTuned = computed(() => {
|
||||||
|
const preset = analysisPresets[formData.analysisMode] || analysisPresets.AUTO
|
||||||
|
return Object.keys(preset).some((key) => formData.analysisTuning[key] !== preset[key])
|
||||||
|
})
|
||||||
const profileOptions = computed(() => isVideo.value
|
const profileOptions = computed(() => isVideo.value
|
||||||
? [{ label: '爱玛测试视频分析', value: 'aima.power_video.v1' }]
|
? [{ label: '爱玛测试视频分析', value: 'aima.power_video.v1' }]
|
||||||
: [{ label: '爱玛车辆声音事件识别', value: 'aima.power_state.v1' }])
|
: [{ label: '爱玛车辆声音事件识别', value: 'aima.power_state.v1' }])
|
||||||
const outputSummary = computed(() => isVideo.value
|
const outputSummary = computed(() => isVideo.value
|
||||||
? [
|
? [
|
||||||
{ name: 'passed', desc: '是否通过' },
|
{ name: 'passed', desc: '是否通过' },
|
||||||
|
{ name: 'confidence', desc: '判定置信度' },
|
||||||
|
{ name: 'evidenceSufficient', desc: '证据是否充分' },
|
||||||
{ name: 'conclusion', desc: '分析结论' },
|
{ name: 'conclusion', desc: '分析结论' },
|
||||||
{ name: 'summary', desc: '分析摘要' },
|
{ name: 'summary', desc: '分析摘要' },
|
||||||
{ name: 'events', desc: '关键事件列表' },
|
{ name: 'events', desc: '关键事件列表' },
|
||||||
@ -147,6 +197,46 @@ const rules = {
|
|||||||
: Promise.reject(new Error('请输入有效的 HTTP/HTTPS 地址')),
|
: Promise.reject(new Error('请输入有效的 HTTP/HTTPS 地址')),
|
||||||
trigger: 'blur',
|
trigger: 'blur',
|
||||||
}],
|
}],
|
||||||
|
instruction: [{
|
||||||
|
validator: (_, value) => !isVideo.value || String(value || '').trim()
|
||||||
|
? Promise.resolve()
|
||||||
|
: Promise.reject(new Error('请输入明确的通过标准')),
|
||||||
|
trigger: 'blur',
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
|
||||||
|
const restorePreset = () => {
|
||||||
|
Object.assign(formData.analysisTuning, analysisPresets[formData.analysisMode] || analysisPresets.AUTO)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAnalysisModeChange = () => restorePreset()
|
||||||
|
|
||||||
|
const parseStoredTuning = (value) => {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) return value
|
||||||
|
if (typeof value !== 'string' || !value.trim()) return {}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value)
|
||||||
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clamp = (value, minimum, maximum, fallback) => {
|
||||||
|
const number = Number(value)
|
||||||
|
return Number.isFinite(number) ? Math.min(maximum, Math.max(minimum, number)) : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeStoredTuning = (value, preset) => {
|
||||||
|
const stored = parseStoredTuning(value)
|
||||||
|
return {
|
||||||
|
maxFrames: Math.round(clamp(stored.maxFrames, 2, 16, preset.maxFrames)),
|
||||||
|
maxWidth: Math.round(clamp(stored.maxWidth, 640, 1280, preset.maxWidth)),
|
||||||
|
confidenceThreshold: clamp(stored.confidenceThreshold, 0.5, 0.95, preset.confidenceThreshold),
|
||||||
|
fallbackToAccurate: typeof stored.fallbackToAccurate === 'boolean'
|
||||||
|
? stored.fallbackToAccurate
|
||||||
|
: preset.fallbackToAccurate,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadQuoteOptions = (visible) => {
|
const loadQuoteOptions = (visible) => {
|
||||||
@ -164,13 +254,17 @@ const initData = () => {
|
|||||||
const media = params.find(item => item.name === mediaName.value)
|
const media = params.find(item => item.name === mediaName.value)
|
||||||
const instruction = params.find(item => item.name === 'instruction')
|
const instruction = params.find(item => item.name === 'instruction')
|
||||||
const analysisMode = params.find(item => item.name === 'analysisMode')
|
const analysisMode = params.find(item => item.name === 'analysisMode')
|
||||||
|
const analysisTuning = params.find(item => item.name === 'analysisTuning')
|
||||||
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
formData.profileCode = profile?.input || profileOptions.value[0]?.value || ''
|
||||||
formData.mediaMode = media?.type === 'input' ? 'input' : 'quote'
|
formData.mediaMode = media?.type === 'input' ? 'input' : 'quote'
|
||||||
formData.mediaQuote = Array.isArray(media?.quote) ? [...media.quote] : []
|
formData.mediaQuote = Array.isArray(media?.quote) ? [...media.quote] : []
|
||||||
formData.mediaQuoteType = media?.quoteType || 'output'
|
formData.mediaQuoteType = media?.quoteType || 'output'
|
||||||
formData.mediaUrl = media?.input || ''
|
formData.mediaUrl = media?.input || ''
|
||||||
formData.instruction = instruction?.input || ''
|
formData.instruction = instruction?.input || ''
|
||||||
formData.analysisMode = analysisMode?.input || 'AUTO'
|
formData.analysisMode = analysisPresets[analysisMode?.input] ? analysisMode.input : 'AUTO'
|
||||||
|
const preset = analysisPresets[formData.analysisMode] || analysisPresets.AUTO
|
||||||
|
Object.assign(formData.analysisTuning, normalizeStoredTuning(analysisTuning?.input, preset))
|
||||||
|
if (formData.analysisMode !== 'AUTO') formData.analysisTuning.fallbackToAccurate = false
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.data, initData, { immediate: true, deep: true })
|
watch(() => props.data, initData, { immediate: true, deep: true })
|
||||||
@ -203,7 +297,7 @@ const validateAndSave = async () => {
|
|||||||
name: 'instruction',
|
name: 'instruction',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
input: formData.instruction.trim(),
|
input: formData.instruction.trim(),
|
||||||
required: false,
|
required: true,
|
||||||
disabled: true,
|
disabled: true,
|
||||||
})
|
})
|
||||||
nodeParams.push({
|
nodeParams.push({
|
||||||
@ -215,6 +309,13 @@ const validateAndSave = async () => {
|
|||||||
componentType: 'select',
|
componentType: 'select',
|
||||||
selectOptions: analysisModes,
|
selectOptions: analysisModes,
|
||||||
})
|
})
|
||||||
|
nodeParams.push({
|
||||||
|
name: 'analysisTuning',
|
||||||
|
type: 'input',
|
||||||
|
input: { ...formData.analysisTuning },
|
||||||
|
required: false,
|
||||||
|
disabled: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
const properties = { ...props.data.properties, nodeParams }
|
const properties = { ...props.data.properties, nodeParams }
|
||||||
delete properties.executionTarget
|
delete properties.executionTarget
|
||||||
@ -241,4 +342,12 @@ defineExpose({ validateAndSave })
|
|||||||
.output-list { display: grid; gap: 8px; }
|
.output-list { display: grid; gap: 8px; }
|
||||||
.output-item { display: flex; gap: 12px; align-items: baseline; color: var(--el-text-color-regular); font-size: 13px; }
|
.output-item { display: flex; gap: 12px; align-items: baseline; color: var(--el-text-color-regular); font-size: 13px; }
|
||||||
.output-item code { min-width: 92px; color: var(--el-color-primary); }
|
.output-item code { min-width: 92px; color: var(--el-color-primary); }
|
||||||
|
.analysis-tuning { margin-bottom: 18px; border-top: 1px solid var(--el-border-color-lighter); }
|
||||||
|
.analysis-tuning :deep(.el-collapse-item__header) { min-height: 48px; height: auto; }
|
||||||
|
.analysis-tuning :deep(.el-collapse-item__content) { padding-bottom: 4px; }
|
||||||
|
.analysis-tuning__title { display: flex; min-width: 0; align-items: center; gap: 8px; font-weight: 650; }
|
||||||
|
.analysis-tuning__body { padding: 4px 2px 0; }
|
||||||
|
.analysis-tuning__toolbar { display: flex; min-height: 32px; margin-bottom: 10px; align-items: center; justify-content: space-between; color: var(--el-text-color-secondary); font-size: 12px; }
|
||||||
|
.analysis-tuning :deep(.el-slider) { --el-slider-button-size: 16px; }
|
||||||
|
.analysis-tuning :deep(.el-slider__runway.show-input) { margin-right: 18px; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -522,12 +522,16 @@ function handler(params) {
|
|||||||
nodeParams: [
|
nodeParams: [
|
||||||
{ name: "profileCode", type: "input", input: "aima.power_video.v1", required: true, disabled: true },
|
{ name: "profileCode", type: "input", input: "aima.power_video.v1", required: true, disabled: true },
|
||||||
{ name: "videoUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
|
{ name: "videoUrl", type: "quote", input: "", quote: "", required: true, disabled: true },
|
||||||
{ name: "instruction", type: "input", input: "", required: false, disabled: true },
|
{ name: "instruction", type: "input", input: "", required: true, disabled: true },
|
||||||
{ name: "analysisMode", type: "input", input: "AUTO", required: false, disabled: true },
|
{ name: "analysisMode", type: "input", input: "AUTO", required: false, disabled: true },
|
||||||
|
{ name: "analysisTuning", type: "input", input: { maxFrames: 8, maxWidth: 960, confidenceThreshold: 0.75, fallbackToAccurate: true }, required: false, disabled: true },
|
||||||
],
|
],
|
||||||
outputType: "json",
|
outputType: "json",
|
||||||
outputParams: [
|
outputParams: [
|
||||||
{ name: "passed", type: "boolean", desc: "是否通过", disabled: true },
|
{ name: "passed", type: "boolean", desc: "是否通过", disabled: true },
|
||||||
|
{ name: "confidence", type: "number", desc: "判定置信度", disabled: true },
|
||||||
|
{ name: "evidenceSufficient", type: "boolean", desc: "证据是否充分", disabled: true },
|
||||||
|
{ name: "decisionReason", type: "string", desc: "判定原因", disabled: true },
|
||||||
{ name: "conclusion", type: "string", desc: "分析结论", disabled: true },
|
{ name: "conclusion", type: "string", desc: "分析结论", disabled: true },
|
||||||
{ name: "summary", type: "string", desc: "分析摘要", disabled: true },
|
{ name: "summary", type: "string", desc: "分析摘要", disabled: true },
|
||||||
{ name: "events", type: "array<object>", desc: "关键事件", children: [], disabled: true },
|
{ name: "events", type: "array<object>", desc: "关键事件", children: [], disabled: true },
|
||||||
|
|||||||
@ -185,6 +185,8 @@
|
|||||||
:node-params="formData.nodeParams"
|
:node-params="formData.nodeParams"
|
||||||
:robot-options="singleNodeRobotOptions"
|
:robot-options="singleNodeRobotOptions"
|
||||||
:initial-robot-id="singleNodeInitialRobotId"
|
:initial-robot-id="singleNodeInitialRobotId"
|
||||||
|
:executing-step-index="singleQueueStepExecutingIndex"
|
||||||
|
@execute-step="executeActionQueueStep"
|
||||||
/>
|
/>
|
||||||
<el-form
|
<el-form
|
||||||
v-else
|
v-else
|
||||||
@ -1107,6 +1109,7 @@ const rules = reactive({});
|
|||||||
const executeForm = ref();
|
const executeForm = ref();
|
||||||
const nodeAction = ref("");
|
const nodeAction = ref("");
|
||||||
const singleNodeExecuting = ref(false);
|
const singleNodeExecuting = ref(false);
|
||||||
|
const singleQueueStepExecutingIndex = ref(-1);
|
||||||
const activeSingleNodeId = ref("");
|
const activeSingleNodeId = ref("");
|
||||||
// 单节点机器人选择按节点缓存;新节点默认沿用最近一次选择。
|
// 单节点机器人选择按节点缓存;新节点默认沿用最近一次选择。
|
||||||
const singleNodeRobotCache = new Map();
|
const singleNodeRobotCache = new Map();
|
||||||
@ -1277,6 +1280,7 @@ const singleNodeExecution = async (e) => {
|
|||||||
visible.value = true;
|
visible.value = true;
|
||||||
nodeAction.value = normalizedAction;
|
nodeAction.value = normalizedAction;
|
||||||
singleNodeExecuting.value = false;
|
singleNodeExecuting.value = false;
|
||||||
|
singleQueueStepExecutingIndex.value = -1;
|
||||||
stationLoading.value = false;
|
stationLoading.value = false;
|
||||||
if (effectiveNodeType === "EDGE") {
|
if (effectiveNodeType === "EDGE") {
|
||||||
await loadSingleNodeRobots();
|
await loadSingleNodeRobots();
|
||||||
@ -1372,6 +1376,26 @@ const execute = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const executeActionQueueStep = async (index) => {
|
||||||
|
if (!actionQueueSingleRunRef.value?.validateStep?.(index)) return;
|
||||||
|
rememberSingleNodeRobot();
|
||||||
|
singleNodeExecuting.value = true;
|
||||||
|
singleQueueStepExecutingIndex.value = index;
|
||||||
|
try {
|
||||||
|
const res = await flowAction({
|
||||||
|
action: "SYSTEM_EXECUTE_ACTION_QUEUE",
|
||||||
|
payload: actionQueueSingleRunRef.value.getStepPayload(index),
|
||||||
|
});
|
||||||
|
if (res.code === 200) ElMessage.success("当前步骤执行成功");
|
||||||
|
else ElMessage.error(res.msg);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("动作队列步骤执行失败:", error);
|
||||||
|
} finally {
|
||||||
|
singleNodeExecuting.value = false;
|
||||||
|
singleQueueStepExecutingIndex.value = -1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const showParamsDrawer = ref(false);
|
const showParamsDrawer = ref(false);
|
||||||
const paramsDrawerData = ref({});
|
const paramsDrawerData = ref({});
|
||||||
|
|
||||||
|
|||||||
@ -142,7 +142,7 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body>
|
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body>
|
||||||
<taskLog :list="logDialogData" :height="300" />
|
<taskLog v-loading="logDialogLoading" :list="logDialogData" :height="300" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
|
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
|
||||||
@ -175,6 +175,7 @@ import { addRunTask, getRunTaskList, startRunTask, rerunRunTask, pauseRunTask, s
|
|||||||
import { ElMessage, ElMessageBox } from "element-plus";
|
import { ElMessage, ElMessageBox } from "element-plus";
|
||||||
import { formatDate } from "@/utils/index";
|
import { formatDate } from "@/utils/index";
|
||||||
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数
|
import { debounce } from 'lodash'; // 引入 lodash 的防抖函数
|
||||||
|
import { fetchAllPagedRows } from '@/utils/pagedRequest'
|
||||||
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
|
import RuntimeAssignmentsEditor from '@/views/flow/components/RuntimeAssignmentsEditor.vue'
|
||||||
import RuntimeParamsEditor from '@/views/flow/components/RuntimeParamsEditor.vue'
|
import RuntimeParamsEditor from '@/views/flow/components/RuntimeParamsEditor.vue'
|
||||||
import { getFlowParamsApi } from '@/api/device/terminal.js'
|
import { getFlowParamsApi } from '@/api/device/terminal.js'
|
||||||
@ -319,7 +320,7 @@ const taskLogList = ref({})
|
|||||||
* @param taskInstanceId
|
* @param taskInstanceId
|
||||||
*/
|
*/
|
||||||
const getTaskLogList = async (taskInstanceId) => {
|
const getTaskLogList = async (taskInstanceId) => {
|
||||||
const res = await getTaskLogListApi({ taskInstanceId })
|
const res = await fetchAllPagedRows(getTaskLogListApi, { taskInstanceId })
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
taskLogList.value[taskInstanceId] = res.rows
|
taskLogList.value[taskInstanceId] = res.rows
|
||||||
}
|
}
|
||||||
@ -547,11 +548,20 @@ const subscribeDebounced = debounce(async (sub) => {
|
|||||||
|
|
||||||
const logDialogVisible = ref(false)
|
const logDialogVisible = ref(false)
|
||||||
const logDialogData = ref([])
|
const logDialogData = ref([])
|
||||||
|
const logDialogLoading = ref(false)
|
||||||
|
let logDialogRequestId = 0
|
||||||
const handleOpenLog = async (row) => {
|
const handleOpenLog = async (row) => {
|
||||||
|
const requestId = ++logDialogRequestId
|
||||||
logDialogVisible.value = true
|
logDialogVisible.value = true
|
||||||
const res = await getTaskLogListApi({ taskInstanceId: row.id })
|
logDialogData.value = []
|
||||||
if (res.code === 200) {
|
logDialogLoading.value = true
|
||||||
logDialogData.value = res.rows
|
try {
|
||||||
|
const res = await fetchAllPagedRows(getTaskLogListApi, { taskInstanceId: row.id })
|
||||||
|
if (requestId === logDialogRequestId && res.code === 200) {
|
||||||
|
logDialogData.value = res.rows
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === logDialogRequestId) logDialogLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const refreshAfterSocketReconnect = () => {
|
const refreshAfterSocketReconnect = () => {
|
||||||
|
|||||||
@ -154,7 +154,7 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body>
|
<el-dialog title="任务执行日志" v-model="logDialogVisible" width="500px" append-to-body>
|
||||||
<taskLog :list="logDialogData" :height="300" />
|
<taskLog v-loading="logDialogLoading" :list="logDialogData" :height="300" />
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
|
<el-dialog title="本次执行安排" v-model="startDialogVisible" width="820px" append-to-body>
|
||||||
@ -176,6 +176,7 @@
|
|||||||
import SvgIcon from "@/components/SvgIcon";
|
import SvgIcon from "@/components/SvgIcon";
|
||||||
import taskSteps from "./task-steps.vue";
|
import taskSteps from "./task-steps.vue";
|
||||||
import taskLog from "./task-log.vue";
|
import taskLog from "./task-log.vue";
|
||||||
|
import { fetchAllPagedRows } from '@/utils/pagedRequest'
|
||||||
import { useContainerHeight } from "@/hooks/tableHeight";
|
import { useContainerHeight } from "@/hooks/tableHeight";
|
||||||
import { getTaskList, getTestCaseByTaskId } from "@/api/intelligenceTest/taskManage";
|
import { getTaskList, getTestCaseByTaskId } from "@/api/intelligenceTest/taskManage";
|
||||||
import { getPhoneList } from "@/api/intelligenceTest/mobilePhone";
|
import { getPhoneList } from "@/api/intelligenceTest/mobilePhone";
|
||||||
@ -362,7 +363,7 @@ const taskLogList = ref({})
|
|||||||
* @param taskInstanceId
|
* @param taskInstanceId
|
||||||
*/
|
*/
|
||||||
const getTaskLogList = async (taskInstanceId) => {
|
const getTaskLogList = async (taskInstanceId) => {
|
||||||
const res = await getTaskLogListApi({ taskInstanceId })
|
const res = await fetchAllPagedRows(getTaskLogListApi, { taskInstanceId })
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
taskLogList.value[taskInstanceId] = res.rows
|
taskLogList.value[taskInstanceId] = res.rows
|
||||||
}
|
}
|
||||||
@ -589,11 +590,20 @@ const subscribeDebounced = debounce(async (sub) => {
|
|||||||
|
|
||||||
const logDialogVisible = ref(false)
|
const logDialogVisible = ref(false)
|
||||||
const logDialogData = ref([])
|
const logDialogData = ref([])
|
||||||
|
const logDialogLoading = ref(false)
|
||||||
|
let logDialogRequestId = 0
|
||||||
const handleOpenLog = async (row) => {
|
const handleOpenLog = async (row) => {
|
||||||
|
const requestId = ++logDialogRequestId
|
||||||
logDialogVisible.value = true
|
logDialogVisible.value = true
|
||||||
const res = await getTaskLogListApi({ taskInstanceId: row.id })
|
logDialogData.value = []
|
||||||
if (res.code === 200) {
|
logDialogLoading.value = true
|
||||||
logDialogData.value = res.rows
|
try {
|
||||||
|
const res = await fetchAllPagedRows(getTaskLogListApi, { taskInstanceId: row.id })
|
||||||
|
if (requestId === logDialogRequestId && res.code === 200) {
|
||||||
|
logDialogData.value = res.rows
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === logDialogRequestId) logDialogLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const refreshAfterSocketReconnect = () => {
|
const refreshAfterSocketReconnect = () => {
|
||||||
|
|||||||
@ -166,7 +166,7 @@
|
|||||||
@close="cancel"
|
@close="cancel"
|
||||||
@open="getLogList"
|
@open="getLogList"
|
||||||
>
|
>
|
||||||
<el-timeline>
|
<el-timeline v-loading="logLoading">
|
||||||
<el-timeline-item
|
<el-timeline-item
|
||||||
v-for="(item, index) in logList"
|
v-for="(item, index) in logList"
|
||||||
:key="index.id"
|
:key="index.id"
|
||||||
@ -246,6 +246,7 @@ import TableTags from "@/components/TableTags/index.vue";
|
|||||||
import { appendParamsToPath } from "@/utils/fn.js";
|
import { appendParamsToPath } from "@/utils/fn.js";
|
||||||
import { getTaskConfig } from "@/api/test/config.js";
|
import { getTaskConfig } from "@/api/test/config.js";
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
|
import { fetchAllPagedRows } from '@/utils/pagedRequest'
|
||||||
|
|
||||||
import { useContainerHeight } from "@/hooks/tableHeight";
|
import { useContainerHeight } from "@/hooks/tableHeight";
|
||||||
|
|
||||||
@ -282,7 +283,7 @@ const data = reactive({
|
|||||||
},
|
},
|
||||||
logQueryParams: {
|
logQueryParams: {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 100000,
|
pageSize: 200,
|
||||||
instId: null,
|
instId: null,
|
||||||
},
|
},
|
||||||
rules: {},
|
rules: {},
|
||||||
@ -339,7 +340,7 @@ function cancel() {
|
|||||||
function reset() {
|
function reset() {
|
||||||
logQueryParams.value = {
|
logQueryParams.value = {
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 10,
|
pageSize: 200,
|
||||||
instId: null,
|
instId: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -430,13 +431,18 @@ function handleExport() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 查询实例日志列表 */
|
/** 查询实例日志列表 */
|
||||||
function getLogList() {
|
async function getLogList() {
|
||||||
logLoading.value = true;
|
logLoading.value = true;
|
||||||
getInst(logQueryParams.value).then((response) => {
|
logList.value = [];
|
||||||
|
try {
|
||||||
|
const response = await fetchAllPagedRows(getInst, {
|
||||||
|
instId: logQueryParams.value.instId,
|
||||||
|
});
|
||||||
logList.value = response.rows;
|
logList.value = response.rows;
|
||||||
logTotal.value = response.total;
|
logTotal.value = response.total;
|
||||||
|
} finally {
|
||||||
logLoading.value = false;
|
logLoading.value = false;
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleOpenLogDialog(row) {
|
function handleOpenLogDialog(row) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user