CMVR-BID/src/views/flow/components/params/ActionQueueParams.vue
lixiaolong 6be8bc9bfd feat(flow): 添加动作队列步骤独立执行功能和媒体分析增强
- 在动作队列参数组件中添加单独执行按钮,允许独立运行指定步骤
- 实现步骤验证和构建逻辑,支持单步执行的参数处理
- 更新媒体分析参数界面,添加高级设置和参数微调功能
- 增加视频分析的置信度、通过标准等配置选项
- 优化任务执行日志显示,添加分析结果的结构化展示
- 集成分页请求工具,改进日志加载性能和用户体验
- 添加执行步骤索引跟踪,确保执行状态正确管理
- 实现防重复ID机制,保证分页数据完整性
- 新增分析结果解析和可视化展示功能
- 扩展媒体分析类型的处理和结果显示
2026-08-17 16:05:42 +08:00

583 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="queue-editor">
<div class="queue-editor__heading">执行设置</div>
<el-form label-position="top">
<el-form-item label="由谁执行" required>
<el-select v-model="formData.executionTarget.roleKey" style="width: 100%" @change="handleRoleChange">
<el-option v-for="role in roles" :key="role.roleKey" :label="role.displayName" :value="role.roleKey" />
</el-select>
</el-form-item>
<el-form-item label="队列总超时(毫秒)" required>
<el-input-number v-model="formData.totalTimeoutMs" :min="1" :max="2147483647" style="width: 100%" />
</el-form-item>
<el-form-item label="动作标识(可选)">
<el-input v-model="formData.actionId" placeholder="留空时执行端自动生成" />
</el-form-item>
</el-form>
<div class="queue-editor__toolbar">
<div>
<div class="queue-editor__heading">动作步骤</div>
<div class="queue-editor__hint">步骤会在边缘端严格按顺序执行。</div>
</div>
<el-button type="primary" plain @click="addStep"><el-icon><Plus /></el-icon>添加步骤</el-button>
</div>
<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 class="queue-step__index">{{ index + 1 }}</span>
<span class="queue-step__name">{{ getActionQueueStepName(step, index) }}</span>
<span class="queue-step__action">{{ actionLabel(step.type) }}</span>
</div>
<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-button :icon="ArrowUp" circle size="small" :disabled="index === 0" aria-label="上移一步" @click="moveStep(index, -1)" />
</el-tooltip>
<el-tooltip content="下移一步" placement="top">
<el-button :icon="ArrowDown" circle size="small" :disabled="index === formData.steps.length - 1" aria-label="下移一步" @click="moveStep(index, 1)" />
</el-tooltip>
<el-tooltip content="复制步骤" placement="top">
<el-button :icon="CopyDocument" circle size="small" aria-label="复制步骤" @click="copyStep(index)" />
</el-tooltip>
<el-dropdown v-if="step.sourceNode" trigger="click" @command="(placement) => requestExtractStep(index, placement)">
<el-button :icon="MoreFilled" circle size="small" aria-label="移出队列" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="before">移到队列前并连接</el-dropdown-item>
<el-dropdown-item command="after">移到队列后并连接</el-dropdown-item>
<el-dropdown-item command="detached" divided>仅移出,不连线</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-tooltip content="删除步骤" placement="top">
<el-button :icon="Delete" circle size="small" type="danger" plain aria-label="删除步骤" @click="removeStep(index)" />
</el-tooltip>
</div>
</div>
</template>
<div class="queue-step">
<el-form label-position="top">
<el-form-item label="步骤名称">
<el-input v-model="step.stepName" maxlength="50" show-word-limit placeholder="例如:移动到取料位" />
</el-form-item>
<el-form-item label="动作" required>
<el-select v-model="step.type" style="width: 100%" @change="handleStepTypeChange(step)">
<el-option v-for="item in actionTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item v-if="deviceKindForType(step.type)" label="使用设备" required>
<el-select v-model="step.deviceSlotKey" style="width: 100%">
<el-option
v-for="slot in matchingSlots(step.type)"
:key="slot.slotKey"
:label="slot.displayName"
:value="slot.slotKey"
/>
</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.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
v-else-if="field.number"
v-model="step.params[field.key]"
:min="field.min"
:max="field.max ?? Infinity"
:step="field.step || 1"
style="width: 100%"
/>
<el-input v-else v-model="step.params[field.key]" :type="field.multiline ? 'textarea' : 'text'" :rows="3" />
</el-form-item>
</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, CopyDocument, Delete, Location, MoreFilled, Plus, VideoPlay } 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,
FLOW_NODE_RESOURCE_OWNER,
getResourceRoles,
getStartNode,
pruneUnusedNodeResources,
} from '@/utils/flowResources'
import { DEVICE_KIND, toDeviceOptions } from '@/utils/robotDevice'
import { copyActionQueueStep, getActionQueueStepName, moveActionQueueStep } from '@/utils/actionQueue'
const props = defineProps({ data: { type: Object, required: true } })
const emit = defineEmits(['save-success', 'save-error', 'extract-step'])
const actionTypes = [
{ value: 'DELAY', label: '等待' },
{ value: 'ARM_MOVE_J', label: '机械臂关节运动' },
{ value: 'ARM_MOVE_L', label: '机械臂直线运动' },
{ value: 'AGV_NAVIGATE_TO_POSE', label: '底盘导航到坐标' },
{ value: 'AGV_NAVIGATE_TO_STATION', label: '底盘导航到站点' },
{ value: 'AGV_FOLLOW_PATH', label: '底盘按路径导航' },
]
const commonMotionFields = [
{ key: 'velocity', label: '速度', number: true, min: 0, step: 0.01, required: false },
{ key: 'acceleration', label: '加速度', number: true, min: 0, step: 0.01, required: false },
]
const fieldsByType = {
DELAY: [{ key: 'durationMs', label: '等待时长(毫秒)', number: true, min: 0 }],
ARM_MOVE_J: [{ key: 'positions', label: '关节位置(JSON 数组)', multiline: true }, ...commonMotionFields],
ARM_MOVE_L: [
...['x', 'y', 'z', 'rx', 'ry', 'rz'].map((key) => ({ key, label: key.toUpperCase(), number: true })),
{ key: 'frame', label: '坐标系', options: ['BASE', 'TOOL', 'WORLD', 'USER'].map((value) => ({ value, label: value })) },
...commonMotionFields,
],
AGV_NAVIGATE_TO_POSE: [
...['x', 'y', 'theta'].map((key) => ({ key, label: key === 'theta' ? '角度' : key.toUpperCase(), number: true })),
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
],
AGV_NAVIGATE_TO_STATION: [
{ key: 'stationId', label: '站点 ID' },
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
],
AGV_FOLLOW_PATH: [
{ key: 'path', label: '路径段(JSON 数组)', multiline: true },
{ key: 'maxSpeed', label: '最大速度', number: true, min: 0, step: 0.01, required: false },
],
}
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: 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))
const normalizeStep = (source = {}, index = 0) => {
const params = { ...(defaultParams[source.type || 'DELAY'] || {}), ...(source.params || {}) }
for (const key of ['positions', 'path']) {
if (Array.isArray(params[key])) params[key] = JSON.stringify(params[key])
}
return {
localKey: source.localKey || createResourceKey('queue_step'),
stepId: source.stepId || `step_${index + 1}`,
stepName: String(source.stepName || '').trim() || `步骤 ${index + 1}`,
type: source.type || 'DELAY',
timeoutMs: source.timeoutMs === undefined || source.timeoutMs === null
? defaultTimeoutByType[source.type || 'DELAY']
: Number(source.timeoutMs),
deviceBindingKey: source.deviceBindingKey || createResourceKey('queue_device'),
deviceSlotKey: source.deviceSlotKey || '',
stationOptions: [],
...(source.sourceNode ? { sourceNode: JSON.parse(JSON.stringify(source.sourceNode)) } : {}),
params,
}
}
const initData = () => {
roles.value = getResourceRoles()
const properties = props.data.properties || {}
const params = Object.fromEntries((properties.nodeParams || []).map((item) => [item.name, item.input]))
formData.actionId = params.actionId || ''
formData.totalTimeoutMs = Number(params.totalTimeoutMs || 60000)
formData.steps = (Array.isArray(params.steps) ? params.steps : []).map(normalizeStep)
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 = () => {
const graph = lf.getGraphData()
const start = getStartNode(graph)
if (!start) return
lf.setProperties(start.id, { ...start.properties, resourceRoles: JSON.parse(JSON.stringify(roles.value)) })
}
const ensureStepSlot = (step, createWhenMissing = true) => {
const kind = deviceKindForType(step.type)
if (!kind) {
step.deviceSlotKey = ''
return
}
const role = selectedRole()
if (!role) return
const options = matchingSlots(step.type)
if (options.some((slot) => slot.slotKey === step.deviceSlotKey)) return
let slot = options[0]
if (!slot && createWhenMissing) {
slot = createDeviceSlot(kind, role.deviceSlots || [])
slot.managedBy = FLOW_NODE_RESOURCE_OWNER
role.deviceSlots ||= []
role.deviceSlots.push(slot)
persistRoles()
}
step.deviceSlotKey = slot?.slotKey || ''
}
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) => {
moveActionQueueStep(formData.steps, index, offset)
}
const copyStep = (index) => {
const source = formData.steps[index]
if (!source) return
const copied = copyActionQueueStep(source, {
localKey: createResourceKey('queue_step'),
stepId: `step_${formData.steps.length + 1}`,
deviceBindingKey: createResourceKey('queue_device'),
stationOptions: [],
})
formData.steps.splice(index + 1, 0, copied)
activeStepKeys.value = [...new Set([...activeStepKeys.value, copied.localKey])]
}
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']) {
if (typeof params[key] !== 'string') continue
try {
const parsed = JSON.parse(params[key])
if (!Array.isArray(parsed)) throw new Error()
params[key] = parsed
} catch {
throw new Error(`${fieldsForType(step.type).find((field) => field.key === key)?.label || key}格式不正确`)
}
}
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 = () => {
if (!formData.executionTarget.roleKey) throw new Error('请选择执行机器人角色')
if (!formData.steps.length) throw new Error('请添加至少一个动作步骤')
const steps = formData.steps.map((step, index) => {
if (!step.type) throw new Error(`步骤 ${index + 1} 未选择动作`)
if (deviceKindForType(step.type) && !step.deviceSlotKey) 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_${index + 1}`,
stepName: String(step.stepName || '').trim() || `步骤 ${index + 1}`,
type: step.type,
timeoutMs: Number(step.timeoutMs || 0),
...(deviceKindForType(step.type) ? {
deviceBindingKey: step.deviceBindingKey,
deviceSlotKey: step.deviceSlotKey,
} : {}),
...(step.sourceNode ? { sourceNode: JSON.parse(JSON.stringify(step.sourceNode)) } : {}),
params,
}
})
return {
steps,
executionTarget: {
roleKey: formData.executionTarget.roleKey,
dispatchPolicy: 'ONE',
deviceBindings: steps.filter((step) => step.deviceBindingKey).map((step) => ({
parameterName: step.deviceBindingKey,
slotKey: step.deviceSlotKey,
})),
},
}
}
const persistQueueDefinition = ({ steps, executionTarget }) => {
lf.setProperties(props.data.id, {
...props.data.properties,
executionTarget,
nodeParams: [
{ name: 'actionId', type: 'input', input: formData.actionId, required: false },
{ name: 'totalTimeoutMs', type: 'input', componentType: 'number', input: formData.totalTimeoutMs, required: true },
{ name: 'steps', type: 'input', input: steps, required: true },
],
})
}
const requestExtractStep = (index, placement) => {
try {
persistQueueDefinition(buildQueueDefinition())
emit('extract-step', { queueId: props.data.id, index, placement })
} catch (error) {
ElMessage.warning(error.message)
}
}
const validateAndSave = async () => {
try {
persistQueueDefinition(buildQueueDefinition())
const cleanup = pruneUnusedNodeResources(lf.getGraphData())
if (cleanup.start) {
lf.setProperties(cleanup.start.id, {
...cleanup.start.properties,
resourceRoles: cleanup.start.properties.resourceRoles,
})
}
emit('save-success')
} catch (error) {
ElMessage.warning(error.message)
emit('save-error')
}
}
watch(() => props.data, initData, { immediate: true, deep: true })
defineExpose({ validateAndSave })
</script>
<style scoped lang="scss">
.queue-editor { padding-top: 16px; }
.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-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) { min-height: 54px; height: auto; padding: 8px 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__index { display: inline-flex; width: 24px; height: 24px; flex: 0 0 24px; align-items: center; justify-content: center; border-radius: 50%; background: #eef4ff; color: var(--el-color-primary); font-size: 12px; }
.queue-step__name { overflow: hidden; max-width: 150px; text-overflow: ellipsis; white-space: nowrap; }
.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>