forked from SmartBench/CMVR-IOT-UI
feat(flow): 优化动作队列参数界面并添加状态标准化功能
- 将动作步骤界面改为折叠面板形式提升用户体验 - 添加动作类型标签显示和步骤帮助功能 - 实现机器人设备参数读取助手对话框 - 为不同动作类型设置默认超时时间 - 统一任务状态数据类型为字符串格式 - 修复多处状态比较的类型匹配问题
This commit is contained in:
parent
3b820be743
commit
3ccf3e2c00
@ -23,15 +23,22 @@
|
||||
<el-button type="primary" plain @click="addStep"><el-icon><Plus /></el-icon>添加步骤</el-button>
|
||||
</div>
|
||||
|
||||
<div v-for="(step, index) in formData.steps" :key="step.localKey" class="queue-step">
|
||||
<el-collapse v-model="activeStepKeys" class="queue-steps">
|
||||
<el-collapse-item v-for="(step, index) in formData.steps" :key="step.localKey" :name="step.localKey">
|
||||
<template #title>
|
||||
<div class="queue-step__header">
|
||||
<div class="queue-step__title">
|
||||
<span>步骤 {{ index + 1 }}</span>
|
||||
<div>
|
||||
<span class="queue-step__action">{{ actionLabel(step.type) }}</span>
|
||||
</div>
|
||||
<div class="queue-step__actions" @click.stop>
|
||||
<el-button :icon="ArrowUp" circle size="small" :disabled="index === 0" title="上移" @click="moveStep(index, -1)" />
|
||||
<el-button :icon="ArrowDown" circle size="small" :disabled="index === formData.steps.length - 1" title="下移" @click="moveStep(index, 1)" />
|
||||
<el-button :icon="Delete" circle size="small" type="danger" plain title="删除" @click="removeStep(index)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="queue-step">
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="动作" required>
|
||||
<el-select v-model="step.type" style="width: 100%" @change="handleStepTypeChange(step)">
|
||||
@ -49,12 +56,25 @@
|
||||
</el-select>
|
||||
<div v-if="!matchingSlots(step.type).length" class="queue-editor__warning">当前角色没有对应设备用途。</div>
|
||||
</el-form-item>
|
||||
<div v-if="stepHelperLabel(step.type)" class="queue-step__helper">
|
||||
<el-button type="primary" plain :icon="Location" @click="openStepHelper(step)">
|
||||
{{ stepHelperLabel(step.type) }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-form-item label="步骤超时(毫秒)">
|
||||
<el-input-number v-model="step.timeoutMs" :min="0" :max="2147483647" style="width: 100%" />
|
||||
</el-form-item>
|
||||
<template v-for="field in fieldsForType(step.type)" :key="field.key">
|
||||
<el-form-item :label="field.label" :required="field.required !== false">
|
||||
<el-select v-if="field.options" v-model="step.params[field.key]" style="width: 100%">
|
||||
<el-select
|
||||
v-if="field.key === 'stationId' && step.stationOptions?.length"
|
||||
v-model="step.params[field.key]"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-option v-for="option in step.stationOptions" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
<el-select v-else-if="field.options" v-model="step.params[field.key]" style="width: 100%">
|
||||
<el-option v-for="option in field.options" :key="option.value" :label="option.label" :value="option.value" />
|
||||
</el-select>
|
||||
<el-input-number
|
||||
@ -70,14 +90,38 @@
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
<el-empty v-if="!formData.steps.length" description="请添加至少一个动作步骤" :image-size="72" />
|
||||
|
||||
<el-dialog v-model="helperDialogVisible" :title="stepHelperLabel(helperStep?.type)" width="460" append-to-body>
|
||||
<div class="queue-helper__tip">在线设备仅用于读取参数,不会写入流程定义。</div>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="机器人" required>
|
||||
<el-select v-model="helperForm.robotId" filterable style="width: 100%" @change="loadHelperDevices">
|
||||
<el-option v-for="item in helperRobotOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="执行设备" required>
|
||||
<el-select v-model="helperForm.deviceId" filterable :loading="helperDeviceLoading" style="width: 100%">
|
||||
<el-option v-for="item in helperDeviceOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="helperDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="helperSubmitting" @click="applyStepHelper">确认读取</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { ArrowDown, ArrowUp, Delete, Plus } from '@element-plus/icons-vue'
|
||||
import { ArrowDown, ArrowUp, Delete, Location, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getAgvStations, getAgvStatus, getArmJointState, getArmStatus } from '@/api/flow/flow'
|
||||
import { getRobotDevicesByRobotId, getRobotList } from '@/api/inspection/robot'
|
||||
import {
|
||||
createDeviceSlot,
|
||||
createResourceKey,
|
||||
@ -86,7 +130,7 @@ import {
|
||||
getStartNode,
|
||||
pruneUnusedNodeResources,
|
||||
} from '@/utils/flowResources'
|
||||
import { DEVICE_KIND } from '@/utils/robotDevice'
|
||||
import { DEVICE_KIND, toDeviceOptions } from '@/utils/robotDevice'
|
||||
|
||||
const props = defineProps({ data: { type: Object, required: true } })
|
||||
const emit = defineEmits(['save-success', 'save-error'])
|
||||
@ -128,16 +172,39 @@ const fieldsByType = {
|
||||
const defaultParams = {
|
||||
DELAY: { durationMs: 1000 },
|
||||
ARM_MOVE_J: { positions: '[0, 0, 0, 0, 0, 0]', velocity: 0.1, acceleration: 0.2 },
|
||||
ARM_MOVE_L: { x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0, frame: 'BASE', velocity: 0.1, acceleration: 0.2 },
|
||||
ARM_MOVE_L: { x: 0, y: 0, z: 0, rx: 0, ry: 0, rz: 0, frame: 'BASE', velocity: 2.1, acceleration: 2.2 },
|
||||
AGV_NAVIGATE_TO_POSE: { x: 0, y: 0, theta: 0, maxSpeed: 0.5 },
|
||||
AGV_NAVIGATE_TO_STATION: { stationId: '', maxSpeed: 0.5 },
|
||||
AGV_FOLLOW_PATH: { path: '[]', maxSpeed: 0.5 },
|
||||
}
|
||||
const defaultTimeoutByType = {
|
||||
DELAY: 5000,
|
||||
ARM_MOVE_J: 10000,
|
||||
ARM_MOVE_L: 3000,
|
||||
AGV_NAVIGATE_TO_POSE: 60000,
|
||||
AGV_NAVIGATE_TO_STATION: 60000,
|
||||
AGV_FOLLOW_PATH: 120000,
|
||||
}
|
||||
|
||||
const roles = ref([])
|
||||
const formData = reactive({ actionId: '', totalTimeoutMs: 60000, steps: [], executionTarget: {} })
|
||||
const activeStepKeys = ref([])
|
||||
const helperDialogVisible = ref(false)
|
||||
const helperStep = ref(null)
|
||||
const helperRobotOptions = ref([])
|
||||
const helperDeviceOptions = ref([])
|
||||
const helperDeviceLoading = ref(false)
|
||||
const helperSubmitting = ref(false)
|
||||
const helperForm = reactive({ robotId: '', deviceId: '' })
|
||||
const fieldsForType = (type) => fieldsByType[type] || []
|
||||
const actionLabel = (type) => actionTypes.find((item) => item.value === type)?.label || '未选择动作'
|
||||
const deviceKindForType = (type) => type?.startsWith('ARM_') ? DEVICE_KIND.ARM : (type?.startsWith('AGV_') ? DEVICE_KIND.AGV : null)
|
||||
const stepHelperLabel = (type) => ({
|
||||
ARM_MOVE_J: '读取当前关节位置',
|
||||
ARM_MOVE_L: '读取当前末端位姿',
|
||||
AGV_NAVIGATE_TO_POSE: '读取底盘当前位置',
|
||||
AGV_NAVIGATE_TO_STATION: '加载底盘站点',
|
||||
}[type] || '')
|
||||
const selectedRole = () => roles.value.find((role) => role.roleKey === formData.executionTarget.roleKey)
|
||||
const matchingSlots = (type) => (selectedRole()?.deviceSlots || []).filter((slot) => Number(slot.deviceKind) === deviceKindForType(type))
|
||||
|
||||
@ -150,9 +217,12 @@ const normalizeStep = (source = {}, index = 0) => {
|
||||
localKey: source.localKey || createResourceKey('queue_step'),
|
||||
stepId: source.stepId || `step_${index + 1}`,
|
||||
type: source.type || 'DELAY',
|
||||
timeoutMs: Number(source.timeoutMs || 0),
|
||||
timeoutMs: source.timeoutMs === undefined || source.timeoutMs === null
|
||||
? defaultTimeoutByType[source.type || 'DELAY']
|
||||
: Number(source.timeoutMs),
|
||||
deviceBindingKey: source.deviceBindingKey || createResourceKey('queue_device'),
|
||||
deviceSlotKey: source.deviceSlotKey || '',
|
||||
stationOptions: [],
|
||||
params,
|
||||
}
|
||||
}
|
||||
@ -167,6 +237,7 @@ const initData = () => {
|
||||
formData.executionTarget = JSON.parse(JSON.stringify(properties.executionTarget || {}))
|
||||
formData.executionTarget.roleKey ||= roles.value[0]?.roleKey || ''
|
||||
formData.steps.forEach((step) => ensureStepSlot(step, false))
|
||||
activeStepKeys.value = []
|
||||
}
|
||||
|
||||
const persistRoles = () => {
|
||||
@ -197,18 +268,121 @@ const ensureStepSlot = (step, createWhenMissing = true) => {
|
||||
step.deviceSlotKey = slot?.slotKey || ''
|
||||
}
|
||||
|
||||
const addStep = () => formData.steps.push(normalizeStep({}, formData.steps.length))
|
||||
const removeStep = (index) => formData.steps.splice(index, 1)
|
||||
const addStep = () => {
|
||||
const step = normalizeStep({}, formData.steps.length)
|
||||
formData.steps.push(step)
|
||||
activeStepKeys.value = [...activeStepKeys.value, step.localKey]
|
||||
}
|
||||
const removeStep = (index) => {
|
||||
const [removed] = formData.steps.splice(index, 1)
|
||||
activeStepKeys.value = activeStepKeys.value.filter((key) => key !== removed?.localKey)
|
||||
}
|
||||
const moveStep = (index, offset) => {
|
||||
const [step] = formData.steps.splice(index, 1)
|
||||
formData.steps.splice(index + offset, 0, step)
|
||||
}
|
||||
const handleStepTypeChange = (step) => {
|
||||
step.params = { ...(defaultParams[step.type] || {}) }
|
||||
step.timeoutMs = defaultTimeoutByType[step.type] || 0
|
||||
step.stationOptions = []
|
||||
ensureStepSlot(step)
|
||||
}
|
||||
const handleRoleChange = () => formData.steps.forEach((step) => ensureStepSlot(step))
|
||||
|
||||
const loadHelperRobots = async () => {
|
||||
const response = await getRobotList({ pageNum: 1, pageSize: 1000, connectStatus: '1' })
|
||||
helperRobotOptions.value = (response.rows || []).map((robot) => ({
|
||||
value: robot.robotId,
|
||||
label: `${robot.robotName || robot.robotId} (${robot.robotId})`,
|
||||
}))
|
||||
}
|
||||
|
||||
const loadHelperDevices = async (robotId) => {
|
||||
helperForm.deviceId = ''
|
||||
helperDeviceOptions.value = []
|
||||
if (!robotId || !helperStep.value) return
|
||||
helperDeviceLoading.value = true
|
||||
try {
|
||||
const response = await getRobotDevicesByRobotId(robotId, {
|
||||
deviceKind: deviceKindForType(helperStep.value.type),
|
||||
})
|
||||
const devices = Array.isArray(response.data) ? response.data : []
|
||||
helperDeviceOptions.value = toDeviceOptions(devices)
|
||||
if (devices.length) helperForm.deviceId = devices[0].deviceId
|
||||
else ElMessage.warning('当前机器人没有匹配的在线设备')
|
||||
} finally {
|
||||
helperDeviceLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const openStepHelper = async (step) => {
|
||||
helperStep.value = step
|
||||
helperForm.deviceId = ''
|
||||
helperDialogVisible.value = true
|
||||
try {
|
||||
await loadHelperRobots()
|
||||
if (helperForm.robotId && helperRobotOptions.value.some((item) => item.value === helperForm.robotId)) {
|
||||
await loadHelperDevices(helperForm.robotId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载机器人设备失败:', error)
|
||||
ElMessage.error('加载机器人设备失败')
|
||||
}
|
||||
}
|
||||
|
||||
const stationOptions = (stations) => (Array.isArray(stations) ? stations : [])
|
||||
.filter((station) => station?.id !== undefined && station?.id !== null && String(station.id).trim())
|
||||
.map((station) => {
|
||||
const value = String(station.id).trim()
|
||||
const description = String(station.description || '').trim()
|
||||
return { value, label: description ? `${value} - ${description}` : value }
|
||||
})
|
||||
|
||||
const applyStepHelper = async () => {
|
||||
const step = helperStep.value
|
||||
if (!helperForm.robotId || !helperForm.deviceId) {
|
||||
ElMessage.warning('请选择机器人和执行设备')
|
||||
return
|
||||
}
|
||||
helperSubmitting.value = true
|
||||
try {
|
||||
const target = { robotId: helperForm.robotId, deviceId: helperForm.deviceId }
|
||||
if (step.type === 'ARM_MOVE_J') {
|
||||
const response = await getArmJointState(target)
|
||||
const positions = response?.data?.position || response?.data?.positions
|
||||
if (!Array.isArray(positions) || !positions.length) throw new Error('机械臂未返回有效的关节位置')
|
||||
step.params.positions = JSON.stringify(positions)
|
||||
} else if (step.type === 'ARM_MOVE_L') {
|
||||
const response = await getArmStatus(target)
|
||||
const pose = response?.data
|
||||
if (!pose) throw new Error('机械臂未返回当前末端位姿')
|
||||
;['x', 'y', 'z', 'rx', 'ry', 'rz'].forEach((key) => {
|
||||
if (pose[key] !== undefined && pose[key] !== null) step.params[key] = Number(pose[key])
|
||||
})
|
||||
} else if (step.type === 'AGV_NAVIGATE_TO_POSE') {
|
||||
const response = await getAgvStatus(target)
|
||||
const pose = response?.data?.pose
|
||||
if (!pose) throw new Error('底盘未返回当前位置')
|
||||
;['x', 'y', 'theta'].forEach((key) => {
|
||||
if (pose[key] !== undefined && pose[key] !== null) step.params[key] = Number(pose[key])
|
||||
})
|
||||
} else if (step.type === 'AGV_NAVIGATE_TO_STATION') {
|
||||
const response = await getAgvStations(target)
|
||||
const options = stationOptions(response?.data)
|
||||
if (!options.length) throw new Error('底盘未返回可用站点')
|
||||
step.stationOptions = options
|
||||
if (!options.some((option) => option.value === step.params.stationId)) step.params.stationId = ''
|
||||
ElMessage.success(`已加载${options.length}个站点`)
|
||||
}
|
||||
helperDialogVisible.value = false
|
||||
} catch (error) {
|
||||
console.error('读取动作参数失败:', error)
|
||||
ElMessage.error(error?.message || '读取动作参数失败')
|
||||
} finally {
|
||||
helperSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const parseJsonFields = (step) => {
|
||||
const params = { ...step.params }
|
||||
for (const key of ['positions', 'path']) {
|
||||
@ -286,8 +460,18 @@ defineExpose({ validateAndSave })
|
||||
.queue-editor__heading { color: #344054; font-size: 14px; font-weight: 650; }
|
||||
.queue-editor__hint { margin-top: 3px; color: #7a8494; font-size: 12px; }
|
||||
.queue-editor__toolbar { display: flex; margin: 14px 0 12px; align-items: center; justify-content: space-between; }
|
||||
.queue-step { margin-bottom: 12px; padding: 14px; border: 1px solid #dfe4ec; border-radius: 8px; background: #fff; }
|
||||
.queue-step__header { display: flex; margin-bottom: 12px; align-items: center; justify-content: space-between; color: #1f2937; font-weight: 650; }
|
||||
.queue-step__header .el-button + .el-button { margin-left: 6px; }
|
||||
.queue-steps { border-top: 0; }
|
||||
.queue-steps :deep(.el-collapse-item) { margin-bottom: 10px; border: 1px solid #dfe4ec; border-radius: 8px; overflow: hidden; }
|
||||
.queue-steps :deep(.el-collapse-item__header) { height: 54px; padding: 0 12px; border-bottom: 0; }
|
||||
.queue-steps :deep(.el-collapse-item__wrap) { border-top: 1px solid #edf0f4; border-bottom: 0; }
|
||||
.queue-steps :deep(.el-collapse-item__content) { padding-bottom: 0; }
|
||||
.queue-step { padding: 14px; background: #fff; }
|
||||
.queue-step__header { display: flex; width: 100%; min-width: 0; align-items: center; justify-content: space-between; color: #1f2937; font-weight: 650; }
|
||||
.queue-step__title { display: flex; min-width: 0; align-items: center; gap: 10px; }
|
||||
.queue-step__action { overflow: hidden; color: #667085; font-size: 12px; font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.queue-step__actions { display: flex; flex: 0 0 auto; margin-right: 8px; align-items: center; }
|
||||
.queue-step__actions .el-button + .el-button { margin-left: 6px; }
|
||||
.queue-step__helper { margin: -2px 0 16px; }
|
||||
.queue-editor__warning { color: var(--el-color-warning); font-size: 12px; }
|
||||
.queue-helper__tip { margin: -4px 0 16px; padding: 9px 12px; border-left: 3px solid var(--el-color-primary); background: var(--el-fill-color-light); color: #667085; font-size: 12px; line-height: 18px; }
|
||||
</style>
|
||||
|
||||
@ -211,6 +211,11 @@ const rules = reactive({
|
||||
const taskList = ref([]);
|
||||
const runTaskList = ref([]);
|
||||
|
||||
const normalizeTaskStatus = (tasks = []) => tasks.map(item => ({
|
||||
...item,
|
||||
status: String(item.status),
|
||||
}))
|
||||
|
||||
const getTaskData = () => {
|
||||
getTaskList({
|
||||
pageNum: 1,
|
||||
@ -259,8 +264,9 @@ const getList = () => {
|
||||
statusList: ['1', '3', '5'],
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
runTaskList.value = res.rows;
|
||||
runingTaskList.value = res.rows.filter(item => item.status !== '5')
|
||||
const tasks = normalizeTaskStatus(res.rows)
|
||||
runTaskList.value = tasks;
|
||||
runingTaskList.value = tasks.filter(item => item.status !== '5')
|
||||
for (let i = 0; i < runTaskList.value.length; i++) {
|
||||
getPointByTaskId(runTaskList.value[i].taskId)
|
||||
getTaskLogList(runTaskList.value[i].id)
|
||||
@ -324,7 +330,7 @@ const queryParams = reactive({
|
||||
* @param task
|
||||
*/
|
||||
const startTask = async (task) => {
|
||||
if (task.status === '3') {
|
||||
if (String(task.status) === '3') {
|
||||
let res = await resumeRunTask(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已恢复");
|
||||
@ -436,7 +442,7 @@ const getHistoryList = () => {
|
||||
pageSize: queryParams.pageSize,
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
tableDataList.value = res.rows;
|
||||
tableDataList.value = normalizeTaskStatus(res.rows);
|
||||
total.value = res.total;
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
@ -233,6 +233,11 @@ const phoneList = ref([]);
|
||||
|
||||
const runTaskList = ref([]);
|
||||
|
||||
const normalizeTaskStatus = (tasks = []) => tasks.map(item => ({
|
||||
...item,
|
||||
status: String(item.status),
|
||||
}))
|
||||
|
||||
const getTaskData = () => {
|
||||
getTaskList({
|
||||
pageNum: 1,
|
||||
@ -301,8 +306,9 @@ const getList = () => {
|
||||
statusList: ['1', '3', '5'],
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
runTaskList.value = res.rows;
|
||||
runingTaskList.value = res.rows.filter(item => item.status !== '5')
|
||||
const tasks = normalizeTaskStatus(res.rows)
|
||||
runTaskList.value = tasks;
|
||||
runingTaskList.value = tasks.filter(item => item.status !== '5')
|
||||
for (let i = 0; i < runTaskList.value.length; i++) {
|
||||
getCaseByTaskId(runTaskList.value[i].taskId)
|
||||
getTaskLogList(runTaskList.value[i].id)
|
||||
@ -366,7 +372,7 @@ const queryParams = reactive({
|
||||
* @param task
|
||||
*/
|
||||
const startTask = async (task) => {
|
||||
if (task.status === '3') {
|
||||
if (String(task.status) === '3') {
|
||||
let res = await resumeTaskinstance(task.id);
|
||||
if (res.code === 200) {
|
||||
ElMessage.success("任务已恢复");
|
||||
@ -477,7 +483,7 @@ const getHistoryList = () => {
|
||||
pageSize: queryParams.pageSize,
|
||||
}).then((res) => {
|
||||
if (res.code === 200) {
|
||||
tableDataList.value = res.rows;
|
||||
tableDataList.value = normalizeTaskStatus(res.rows);
|
||||
total.value = res.total;
|
||||
}
|
||||
loading.value = false;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user