Compare commits
No commits in common. "cad4184a2f31eff097dc2fbc15d6d70ace79a7f0" and "06633c3a8043ce9aa170dc86655598e5f91ade65" have entirely different histories.
cad4184a2f
...
06633c3a80
@ -17,7 +17,6 @@ class WebSocketManager {
|
||||
this.processingMessages = false;
|
||||
this.maxQueueSize = 100;
|
||||
this.status = false;
|
||||
this.channelSubscriptions = new Set();
|
||||
}
|
||||
|
||||
connect() {
|
||||
@ -34,7 +33,6 @@ class WebSocketManager {
|
||||
this.status = true;
|
||||
this.startHeartbeat();
|
||||
this.processMessageQueue();
|
||||
this.restoreChannelSubscriptions();
|
||||
this.dispatch('open');
|
||||
};
|
||||
|
||||
@ -161,31 +159,13 @@ class WebSocketManager {
|
||||
}
|
||||
|
||||
send(payload) {
|
||||
if (payload?.type === 'channel_subscription' && payload.channel) {
|
||||
if (payload.action === 'subscribe') {
|
||||
this.channelSubscriptions.add(payload.channel);
|
||||
} else if (payload.action === 'unsubscribe') {
|
||||
this.channelSubscriptions.delete(payload.channel);
|
||||
}
|
||||
}
|
||||
if (this.ws && this.ws.readyState === 1) {
|
||||
this.ws.send(JSON.stringify(payload));
|
||||
} else if (payload?.type !== 'channel_subscription') {
|
||||
} else {
|
||||
console.warn('WebSocket 未连接,消息发送失败');
|
||||
}
|
||||
}
|
||||
|
||||
restoreChannelSubscriptions() {
|
||||
if (!this.ws || this.ws.readyState !== 1) return;
|
||||
this.channelSubscriptions.forEach((channel) => {
|
||||
this.ws.send(JSON.stringify({
|
||||
type: 'channel_subscription',
|
||||
action: 'subscribe',
|
||||
channel,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.isManualClose = true;
|
||||
this.status = false;
|
||||
|
||||
@ -1,346 +1,5 @@
|
||||
const deepClone = (value) => JSON.parse(JSON.stringify(value))
|
||||
|
||||
const ACTION_QUEUE_ACTION = 'SYSTEM_EXECUTE_ACTION_QUEUE'
|
||||
const REFERENCE_KEYS = new Set(['quote', 'nameQuote'])
|
||||
const DEFAULT_TOTAL_TIMEOUT_BUFFER_MS = 5000
|
||||
|
||||
const createKey = (prefix) => {
|
||||
const random = globalThis.crypto?.randomUUID?.().replace(/-/g, '')
|
||||
|| `${Date.now()}${Math.random().toString(16).slice(2)}`
|
||||
return `${prefix}_${random}`
|
||||
}
|
||||
|
||||
const paramsToObject = (node) => Object.fromEntries(
|
||||
(node?.properties?.nodeParams || []).map((param) => [param.name, param.input]),
|
||||
)
|
||||
|
||||
const numberValue = (value, fallback = 0) => {
|
||||
if (value === '' || value === null || value === undefined) return fallback
|
||||
const result = Number(value)
|
||||
return Number.isFinite(result) ? result : fallback
|
||||
}
|
||||
|
||||
const arrayValue = (value) => {
|
||||
if (Array.isArray(value)) return deepClone(value)
|
||||
if (typeof value !== 'string') return value
|
||||
try {
|
||||
const result = JSON.parse(value)
|
||||
return Array.isArray(result) ? result : value
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const nodeToQueueStep = (node) => {
|
||||
const action = node?.properties?.action
|
||||
const params = paramsToObject(node)
|
||||
const common = {
|
||||
stepName: node?.properties?.name || '导入步骤',
|
||||
sourceNode: deepClone(node),
|
||||
}
|
||||
if (node?.type === 'sleep' || String(action).toLowerCase() === 'sleep') {
|
||||
const durationMs = numberValue(params.delayMs)
|
||||
return {
|
||||
...common,
|
||||
type: 'DELAY',
|
||||
timeoutMs: Math.max(durationMs + 1000, 5000),
|
||||
params: { durationMs },
|
||||
}
|
||||
}
|
||||
if (action === 'AGV_MOVE_TO_POINT') {
|
||||
return {
|
||||
...common,
|
||||
type: 'AGV_NAVIGATE_TO_POSE',
|
||||
timeoutMs: 60000,
|
||||
params: {
|
||||
x: numberValue(params.x),
|
||||
y: numberValue(params.y),
|
||||
theta: numberValue(params.theta),
|
||||
maxSpeed: numberValue(params.maxSpeed, 0.5),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (action === 'AGV_MOVE_TO_STATION') {
|
||||
return {
|
||||
...common,
|
||||
type: 'AGV_NAVIGATE_TO_STATION',
|
||||
timeoutMs: 60000,
|
||||
params: {
|
||||
stationId: params.stationId ?? '',
|
||||
maxSpeed: numberValue(params.maxSpeed, 0.5),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (action === 'ARM_MOVE_TO_POINT') {
|
||||
return {
|
||||
...common,
|
||||
type: 'ARM_MOVE_L',
|
||||
timeoutMs: 3000,
|
||||
params: {
|
||||
x: numberValue(params.x),
|
||||
y: numberValue(params.y),
|
||||
z: numberValue(params.z),
|
||||
rx: numberValue(params.rx),
|
||||
ry: numberValue(params.ry),
|
||||
rz: numberValue(params.rz),
|
||||
frame: params.frame || 'BASE',
|
||||
velocity: numberValue(params.velocity, 2.1),
|
||||
acceleration: numberValue(params.acceleration, 2.2),
|
||||
},
|
||||
}
|
||||
}
|
||||
if (action === 'ARM_MOVE_TO_J') {
|
||||
return {
|
||||
...common,
|
||||
type: 'ARM_MOVE_J',
|
||||
timeoutMs: 10000,
|
||||
params: {
|
||||
positions: arrayValue(params.target),
|
||||
velocity: numberValue(params.velocity, 0.1),
|
||||
acceleration: numberValue(params.acceleration, 0.2),
|
||||
},
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const validateConvertibleNodeParams = (node) => {
|
||||
const action = node?.properties?.action
|
||||
const params = paramsToObject(node)
|
||||
const requiredByAction = {
|
||||
AGV_MOVE_TO_POINT: ['x', 'y', 'theta'],
|
||||
AGV_MOVE_TO_STATION: ['stationId'],
|
||||
ARM_MOVE_TO_POINT: ['x', 'y', 'z', 'rx', 'ry', 'rz'],
|
||||
ARM_MOVE_TO_J: ['target'],
|
||||
}
|
||||
const missing = (requiredByAction[action] || []).find((name) => (
|
||||
params[name] === '' || params[name] === null || params[name] === undefined
|
||||
))
|
||||
if (missing) return `节点[${node.properties?.name || node.id}]的参数 ${missing} 尚未配置`
|
||||
if (action === 'ARM_MOVE_TO_J') {
|
||||
const positions = arrayValue(params.target)
|
||||
if (!Array.isArray(positions) || positions.length === 0) return '机械臂关节位置必须是非空 JSON 数组'
|
||||
}
|
||||
if ((node?.type === 'sleep' || String(action).toLowerCase() === 'sleep')
|
||||
&& numberValue(params.delayMs) < 0) return '睡眠时长不能小于 0'
|
||||
return ''
|
||||
}
|
||||
|
||||
const hasReference = (value, referencedNodeId, parentKey = '') => {
|
||||
if (Array.isArray(value)) {
|
||||
if (REFERENCE_KEYS.has(parentKey) && value[0] === referencedNodeId) return true
|
||||
return value.some((item) => hasReference(item, referencedNodeId))
|
||||
}
|
||||
if (!value || typeof value !== 'object') return false
|
||||
return Object.entries(value).some(([key, item]) => hasReference(item, referencedNodeId, key))
|
||||
}
|
||||
|
||||
const hasDynamicInput = (node) => (node?.properties?.nodeParams || []).some((param) => (
|
||||
param?.type === 'quote'
|
||||
|| (Array.isArray(param?.quote) && param.quote.length > 0)
|
||||
))
|
||||
|
||||
const getNodeParam = (node, name, fallback) => (
|
||||
node.properties?.nodeParams?.find((param) => param.name === name)?.input ?? fallback
|
||||
)
|
||||
|
||||
const setNodeParam = (node, name, input, defaults = {}) => {
|
||||
node.properties ||= {}
|
||||
node.properties.nodeParams ||= []
|
||||
const param = node.properties.nodeParams.find((item) => item.name === name)
|
||||
if (param) param.input = input
|
||||
else node.properties.nodeParams.push({ name, type: 'input', input, ...defaults })
|
||||
}
|
||||
|
||||
const queueSteps = (queueNode) => {
|
||||
const value = getNodeParam(queueNode, 'steps', [])
|
||||
return Array.isArray(value) ? deepClone(value) : []
|
||||
}
|
||||
|
||||
const edgeExists = (edges, sourceNodeId, targetNodeId) => edges.some((edge) => (
|
||||
edge.sourceNodeId === sourceNodeId && edge.targetNodeId === targetNodeId
|
||||
))
|
||||
|
||||
const createEdge = (sourceNodeId, targetNodeId, sourceAnchorId, targetAnchorId) => ({
|
||||
id: createKey('edge'),
|
||||
type: 'bezier',
|
||||
sourceNodeId,
|
||||
targetNodeId,
|
||||
sourceAnchorId: sourceAnchorId || `${sourceNodeId}_1`,
|
||||
targetAnchorId: targetAnchorId || `${targetNodeId}_3`,
|
||||
})
|
||||
|
||||
const normalizeQueueStepIds = (steps) => steps.map((step, index) => ({
|
||||
...step,
|
||||
stepId: `step_${index + 1}`,
|
||||
}))
|
||||
|
||||
const updateQueueDefinition = (queueNode, steps) => {
|
||||
const normalizedSteps = normalizeQueueStepIds(steps)
|
||||
setNodeParam(queueNode, 'steps', normalizedSteps, { required: true })
|
||||
const timeoutSum = normalizedSteps.reduce((sum, step) => sum + Math.max(0, numberValue(step.timeoutMs)), 0)
|
||||
const currentTimeout = numberValue(getNodeParam(queueNode, 'totalTimeoutMs', 60000), 60000)
|
||||
setNodeParam(
|
||||
queueNode,
|
||||
'totalTimeoutMs',
|
||||
Math.max(currentTimeout, timeoutSum + DEFAULT_TOTAL_TIMEOUT_BUFFER_MS),
|
||||
{ componentType: 'number', required: true },
|
||||
)
|
||||
const bindings = normalizedSteps.filter((step) => step.deviceBindingKey && step.deviceSlotKey).map((step) => ({
|
||||
parameterName: step.deviceBindingKey,
|
||||
slotKey: step.deviceSlotKey,
|
||||
}))
|
||||
queueNode.properties.executionTarget = {
|
||||
...(queueNode.properties.executionTarget || {}),
|
||||
dispatchPolicy: 'ONE',
|
||||
deviceBindings: bindings,
|
||||
}
|
||||
}
|
||||
|
||||
export const isNodeConvertibleToActionQueue = (node) => Boolean(nodeToQueueStep(node))
|
||||
|
||||
/**
|
||||
* Converts one workflow node into an edge action-queue step and rewrites its former
|
||||
* linear position out of the graph. The source definition is retained for extraction.
|
||||
*/
|
||||
export const appendNodeToActionQueue = (sourceGraph, nodeId, queueId) => {
|
||||
const graph = deepClone(sourceGraph || { nodes: [], edges: [] })
|
||||
const node = graph.nodes.find((item) => item.id === nodeId)
|
||||
const queueNode = graph.nodes.find((item) => item.id === queueId)
|
||||
if (!node || !queueNode) return { ok: false, message: '未找到要移动的节点或动作队列' }
|
||||
if (node.id === queueNode.id) return { ok: false, message: '动作队列不能放入自身' }
|
||||
if (queueNode.properties?.action !== ACTION_QUEUE_ACTION) return { ok: false, message: '目标节点不是动作队列' }
|
||||
const step = nodeToQueueStep(node)
|
||||
if (!step) return { ok: false, message: `节点[${node.properties?.name || node.id}]暂不支持放入动作队列` }
|
||||
const paramError = validateConvertibleNodeParams(node)
|
||||
if (paramError) return { ok: false, message: paramError }
|
||||
if (node.properties?.parentId !== queueNode.properties?.parentId) {
|
||||
return { ok: false, message: '节点和动作队列必须位于同一个流程或循环体中' }
|
||||
}
|
||||
if (hasDynamicInput(node)) return { ok: false, message: '引用其他节点参数的节点暂不能放入动作队列' }
|
||||
const referencedBy = graph.nodes.find((item) => item.id !== nodeId && hasReference(item.properties, nodeId))
|
||||
if (referencedBy) {
|
||||
return { ok: false, message: `节点[${referencedBy.properties?.name || referencedBy.id}]正在引用该节点的输出` }
|
||||
}
|
||||
|
||||
const incoming = graph.edges.filter((edge) => edge.targetNodeId === nodeId)
|
||||
const outgoing = graph.edges.filter((edge) => edge.sourceNodeId === nodeId)
|
||||
if (incoming.length > 1 || outgoing.length > 1) {
|
||||
return { ok: false, message: '存在分支连线的节点不能放入动作队列' }
|
||||
}
|
||||
if (incoming.length === 1
|
||||
&& outgoing.length === 1
|
||||
&& incoming[0].sourceNodeId === outgoing[0].targetNodeId) {
|
||||
return { ok: false, message: '节点处于回路中,不能放入动作队列' }
|
||||
}
|
||||
|
||||
const nodeRoleKey = node.properties?.executionTarget?.roleKey
|
||||
const queueRoleKey = queueNode.properties?.executionTarget?.roleKey
|
||||
if (nodeRoleKey && queueRoleKey && nodeRoleKey !== queueRoleKey) {
|
||||
return { ok: false, message: '节点与动作队列使用的机器人角色不同' }
|
||||
}
|
||||
if (nodeRoleKey && !queueRoleKey) queueNode.properties.executionTarget = { roleKey: nodeRoleKey }
|
||||
|
||||
const deviceSlotKey = node.properties?.executionTarget?.deviceSlotKey
|
||||
if (deviceSlotKey) {
|
||||
step.deviceBindingKey = createKey('queue_device')
|
||||
step.deviceSlotKey = deviceSlotKey
|
||||
}
|
||||
step.stepId = `step_${queueSteps(queueNode).length + 1}`
|
||||
const steps = [...queueSteps(queueNode), step]
|
||||
updateQueueDefinition(queueNode, steps)
|
||||
|
||||
graph.nodes = graph.nodes.filter((item) => item.id !== nodeId)
|
||||
graph.nodes.forEach((item) => {
|
||||
if (Array.isArray(item.children)) item.children = item.children.filter((childId) => childId !== nodeId)
|
||||
})
|
||||
graph.edges = graph.edges.filter((edge) => edge.sourceNodeId !== nodeId && edge.targetNodeId !== nodeId)
|
||||
|
||||
const predecessorId = incoming[0]?.sourceNodeId
|
||||
const successorId = outgoing[0]?.targetNodeId
|
||||
if (predecessorId && successorId && predecessorId !== successorId
|
||||
&& !edgeExists(graph.edges, predecessorId, successorId)) {
|
||||
graph.edges.push(createEdge(
|
||||
predecessorId,
|
||||
successorId,
|
||||
incoming[0].sourceAnchorId,
|
||||
outgoing[0].targetAnchorId,
|
||||
))
|
||||
}
|
||||
return { ok: true, graph, queueId, step }
|
||||
}
|
||||
|
||||
const remapNodeAnchors = (node, previousId, nextId) => {
|
||||
const properties = node.properties || (node.properties = {})
|
||||
if (!Array.isArray(properties.anchor)) return
|
||||
properties.anchor = properties.anchor.map((anchor) => ({
|
||||
...anchor,
|
||||
id: String(anchor.id || '').startsWith(`${previousId}_`)
|
||||
? `${nextId}${String(anchor.id).slice(previousId.length)}`
|
||||
: anchor.id,
|
||||
}))
|
||||
}
|
||||
|
||||
/** Restores an imported queue step as a workflow node. */
|
||||
export const extractActionQueueStep = (sourceGraph, queueId, stepIndex, placement = 'detached') => {
|
||||
const graph = deepClone(sourceGraph || { nodes: [], edges: [] })
|
||||
const queueNode = graph.nodes.find((item) => item.id === queueId)
|
||||
if (!queueNode) return { ok: false, message: '动作队列不存在' }
|
||||
const steps = queueSteps(queueNode)
|
||||
const step = steps[stepIndex]
|
||||
if (!step) return { ok: false, message: '要移出的步骤不存在' }
|
||||
if (!step.sourceNode) return { ok: false, message: '该步骤不是由工作流节点移入,无法还原为原节点' }
|
||||
|
||||
const node = deepClone(step.sourceNode)
|
||||
const previousId = node.id
|
||||
node.id = createKey('node')
|
||||
remapNodeAnchors(node, previousId, node.id)
|
||||
delete node.children
|
||||
node.properties ||= {}
|
||||
node.properties.parentId = queueNode.properties?.parentId
|
||||
if (!node.properties.parentId) delete node.properties.parentId
|
||||
const horizontalOffset = placement === 'before' ? -460 : 460
|
||||
node.x = numberValue(queueNode.x) + (placement === 'detached' ? 0 : horizontalOffset)
|
||||
node.y = numberValue(queueNode.y) + (placement === 'detached' ? 260 : 0)
|
||||
|
||||
const incoming = graph.edges.filter((edge) => edge.targetNodeId === queueId)
|
||||
const outgoing = graph.edges.filter((edge) => edge.sourceNodeId === queueId)
|
||||
if ((placement === 'before' && incoming.length > 1) || (placement === 'after' && outgoing.length > 1)) {
|
||||
return { ok: false, message: '动作队列存在多条相关连线,请选择“仅移出,不连线”' }
|
||||
}
|
||||
|
||||
graph.nodes.push(node)
|
||||
if (steps.length === 1) {
|
||||
graph.nodes = graph.nodes.filter((item) => item.id !== queueId)
|
||||
graph.edges.forEach((edge) => {
|
||||
if (edge.sourceNodeId === queueId) {
|
||||
edge.sourceNodeId = node.id
|
||||
edge.sourceAnchorId = `${node.id}_1`
|
||||
}
|
||||
if (edge.targetNodeId === queueId) {
|
||||
edge.targetNodeId = node.id
|
||||
edge.targetAnchorId = `${node.id}_3`
|
||||
}
|
||||
})
|
||||
return { ok: true, graph, node, removedQueue: true }
|
||||
}
|
||||
|
||||
steps.splice(stepIndex, 1)
|
||||
updateQueueDefinition(queueNode, steps)
|
||||
if (placement === 'before') {
|
||||
graph.edges = graph.edges.filter((edge) => edge.targetNodeId !== queueId)
|
||||
const previous = incoming[0]
|
||||
if (previous) graph.edges.push(createEdge(previous.sourceNodeId, node.id, previous.sourceAnchorId, `${node.id}_3`))
|
||||
graph.edges.push(createEdge(node.id, queueId, `${node.id}_1`, `${queueId}_3`))
|
||||
} else if (placement === 'after') {
|
||||
graph.edges = graph.edges.filter((edge) => edge.sourceNodeId !== queueId)
|
||||
const next = outgoing[0]
|
||||
graph.edges.push(createEdge(queueId, node.id, `${queueId}_1`, `${node.id}_3`))
|
||||
if (next) graph.edges.push(createEdge(node.id, next.targetNodeId, `${node.id}_1`, next.targetAnchorId))
|
||||
}
|
||||
return { ok: true, graph, node, removedQueue: false }
|
||||
}
|
||||
|
||||
export const getActionQueueStepName = (step, index) => {
|
||||
const name = String(step?.stepName || '').trim()
|
||||
return name || `步骤 ${index + 1}`
|
||||
|
||||
@ -20,7 +20,6 @@
|
||||
:data="props.data"
|
||||
@save-success="handleSaveSuccess"
|
||||
@save-error="handleSaveError"
|
||||
@extract-step="handleExtractStep"
|
||||
/>
|
||||
</el-drawer>
|
||||
</template>
|
||||
@ -46,7 +45,7 @@ const props = defineProps({
|
||||
data: Object
|
||||
})
|
||||
|
||||
const emits = defineEmits(['close', 'extract-step'])
|
||||
const emits = defineEmits(['close'])
|
||||
const paramsComponentRef = ref(null)
|
||||
|
||||
/**
|
||||
@ -107,8 +106,6 @@ const handleSaveError = () => {
|
||||
emits('close')
|
||||
}
|
||||
|
||||
const handleExtractStep = (payload) => emits('extract-step', payload)
|
||||
|
||||
// 组件卸载时清理事件监听
|
||||
// 注意:emitter.off 应在各子组件中自行清理,父组件不再重复
|
||||
</script>
|
||||
|
||||
@ -42,16 +42,6 @@
|
||||
<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>
|
||||
@ -141,7 +131,7 @@
|
||||
|
||||
<script setup>
|
||||
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, 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'
|
||||
@ -157,7 +147,7 @@ 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 emit = defineEmits(['save-success', 'save-error'])
|
||||
|
||||
const actionTypes = [
|
||||
{ value: 'DELAY', label: '等待' },
|
||||
@ -248,7 +238,6 @@ const normalizeStep = (source = {}, index = 0) => {
|
||||
deviceBindingKey: source.deviceBindingKey || createResourceKey('queue_device'),
|
||||
deviceSlotKey: source.deviceSlotKey || '',
|
||||
stationOptions: [],
|
||||
...(source.sourceNode ? { sourceNode: JSON.parse(JSON.stringify(source.sourceNode)) } : {}),
|
||||
params,
|
||||
}
|
||||
}
|
||||
@ -435,66 +424,46 @@ const parseJsonFields = (step) => {
|
||||
return params
|
||||
}
|
||||
|
||||
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}不能为空`)
|
||||
const validateAndSave = async () => {
|
||||
try {
|
||||
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,
|
||||
} : {}),
|
||||
params,
|
||||
}
|
||||
})
|
||||
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: {
|
||||
const 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())
|
||||
}
|
||||
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 cleanup = pruneUnusedNodeResources(lf.getGraphData())
|
||||
if (cleanup.start) {
|
||||
lf.setProperties(cleanup.start.id, {
|
||||
|
||||
@ -132,10 +132,7 @@
|
||||
@drop="drop"
|
||||
>
|
||||
<template #node-flow-node="nodeProps">
|
||||
<FlowNode
|
||||
v-bind="nodeProps"
|
||||
:queue-drop-active="nodeProps.id === queueDropTargetId"
|
||||
/>
|
||||
<FlowNode v-bind="nodeProps" />
|
||||
</template>
|
||||
<template #edge-flow-edge="edgeProps">
|
||||
<FlowEdge v-bind="edgeProps" />
|
||||
@ -159,7 +156,6 @@
|
||||
:drawer="true"
|
||||
:data="paramsDrawerData"
|
||||
@close="showParamsDrawer = false"
|
||||
@extract-step="handleExtractQueueStep"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -291,10 +287,6 @@ import {
|
||||
pruneUnusedNodeResources,
|
||||
validateFlowResources,
|
||||
} from "@/utils/flowResources";
|
||||
import {
|
||||
appendNodeToActionQueue,
|
||||
extractActionQueueStep,
|
||||
} from "@/utils/actionQueue";
|
||||
import {
|
||||
flowDeploy,
|
||||
flowView,
|
||||
@ -326,8 +318,6 @@ const lfRef = ref(null);
|
||||
const vueFlow = useVueFlow("main-flow");
|
||||
const lf = createFlowFacade(vueFlow);
|
||||
const loopChildDragContexts = new Map();
|
||||
const actionQueueDragOrigins = new Map();
|
||||
const queueDropTargetId = ref("");
|
||||
const { onFlowEvent } = useFlowEvents();
|
||||
window.lf = lf;
|
||||
|
||||
@ -462,71 +452,12 @@ const eventClientPoint = (event) => {
|
||||
return { x: pointer.clientX, y: pointer.clientY };
|
||||
};
|
||||
|
||||
const findActionQueueAtPoint = (point, draggedNodeId) => (
|
||||
(vueFlow.getNodes.value || []).find((candidate) => {
|
||||
if (candidate.id === draggedNodeId || candidate.data?.flowType !== "actionQueue") return false;
|
||||
const position = candidate.computedPosition || candidate.position;
|
||||
const width = Number(candidate.dimensions?.width || candidate.data?.size?.width || 372);
|
||||
const height = Number(candidate.dimensions?.height || candidate.data?.size?.height || 180);
|
||||
return point.x >= position.x
|
||||
&& point.x <= position.x + width
|
||||
&& point.y >= position.y
|
||||
&& point.y <= position.y + height;
|
||||
}) || null
|
||||
);
|
||||
|
||||
const queueAtDragEvent = (nodeId, event) => {
|
||||
const clientPoint = eventClientPoint(event);
|
||||
if (!clientPoint) return null;
|
||||
return findActionQueueAtPoint(
|
||||
lf.getPointByClient(clientPoint.x, clientPoint.y).canvasOverlayPosition,
|
||||
nodeId,
|
||||
);
|
||||
};
|
||||
|
||||
const renderQueueGraphChange = (graph, selectedNodeId) => {
|
||||
const cleanup = pruneUnusedNodeResources(graph);
|
||||
lf.render(cleanup.graph, { resetHistory: false, replaceNodes: true });
|
||||
nextTick(() => {
|
||||
lf.markChanged();
|
||||
if (selectedNodeId) lf.selectElementById(selectedNodeId);
|
||||
});
|
||||
};
|
||||
|
||||
const restoreDraggedNodePosition = (nodeId) => {
|
||||
const origin = actionQueueDragOrigins.get(nodeId);
|
||||
if (!origin) return;
|
||||
const graph = lf.getGraphData();
|
||||
const node = graph.nodes.find((item) => item.id === nodeId);
|
||||
if (!node) return;
|
||||
node.x = origin.x;
|
||||
node.y = origin.y;
|
||||
lf.render(graph, { resetHistory: false, replaceNodes: true });
|
||||
nextTick(() => lf.markChanged());
|
||||
};
|
||||
|
||||
const moveNodeIntoActionQueue = (nodeId, queueId) => {
|
||||
const result = appendNodeToActionQueue(lf.getGraphData(), nodeId, queueId);
|
||||
if (!result.ok) {
|
||||
restoreDraggedNodePosition(nodeId);
|
||||
ElMessage.warning(result.message);
|
||||
return false;
|
||||
}
|
||||
if (paramsDrawerData.value?.id === nodeId) showParamsDrawer.value = false;
|
||||
nodeErrorList.value = nodeErrorList.value.filter((id) => id !== nodeId);
|
||||
renderQueueGraphChange(result.graph, queueId);
|
||||
ElMessage.success(`已将节点转换为队列第 ${result.step.stepId.replace("step_", "")} 步`);
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理 handleNodeDragStart 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} { node 调用方传入的 { node 参数
|
||||
* @param {*} event } 调用方传入的 event } 参数
|
||||
*/
|
||||
const handleNodeDragStart = ({ node, event }) => {
|
||||
const graphNode = lf.getGraphData().nodes.find((item) => item.id === node.id);
|
||||
if (graphNode) actionQueueDragOrigins.set(node.id, { x: graphNode.x, y: graphNode.y });
|
||||
const drag = lf.prepareLoopChildDrag(node.id);
|
||||
const clientPoint = eventClientPoint(event);
|
||||
if (!drag || !clientPoint) return;
|
||||
@ -557,10 +488,7 @@ const updateLoopChildDrag = ({ node, event }) => {
|
||||
* 处理 handleNodeDrag 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
* @param {*} payload 事件数据
|
||||
*/
|
||||
const handleNodeDrag = (payload) => {
|
||||
updateLoopChildDrag(payload);
|
||||
queueDropTargetId.value = queueAtDragEvent(payload.node.id, payload.event)?.id || "";
|
||||
};
|
||||
const handleNodeDrag = (payload) => updateLoopChildDrag(payload);
|
||||
|
||||
/**
|
||||
* 处理 handleNodeDragStop 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
@ -570,14 +498,6 @@ const handleNodeDragStop = (payload) => {
|
||||
const { node } = payload;
|
||||
updateLoopChildDrag(payload);
|
||||
loopChildDragContexts.delete(node.id);
|
||||
const queueNode = queueAtDragEvent(node.id, payload.event);
|
||||
queueDropTargetId.value = "";
|
||||
if (queueNode) {
|
||||
moveNodeIntoActionQueue(node.id, queueNode.id);
|
||||
actionQueueDragOrigins.delete(node.id);
|
||||
return;
|
||||
}
|
||||
actionQueueDragOrigins.delete(node.id);
|
||||
const vueNode = vueFlow.findNode(node.id);
|
||||
if (
|
||||
vueNode
|
||||
@ -1375,20 +1295,6 @@ const execute = async () => {
|
||||
const showParamsDrawer = ref(false);
|
||||
const paramsDrawerData = ref({});
|
||||
|
||||
const handleExtractQueueStep = ({ queueId, index, placement }) => {
|
||||
const result = extractActionQueueStep(lf.getGraphData(), queueId, index, placement);
|
||||
if (!result.ok) {
|
||||
ElMessage.warning(result.message);
|
||||
return;
|
||||
}
|
||||
showParamsDrawer.value = false;
|
||||
if (result.removedQueue) {
|
||||
nodeErrorList.value = nodeErrorList.value.filter((id) => id !== queueId);
|
||||
}
|
||||
renderQueueGraphChange(result.graph, result.node.id);
|
||||
ElMessage.success(result.removedQueue ? "已用原节点替换空队列" : "步骤已移出动作队列");
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理 clearRun 对应的数据或交互;作用范围仅限当前组件或模块。
|
||||
*/
|
||||
@ -1845,7 +1751,6 @@ onUnmounted(() => {
|
||||
flowDisposed = true;
|
||||
clearFlowPollTimer();
|
||||
loopChildDragContexts.clear();
|
||||
actionQueueDragOrigins.clear();
|
||||
if (window.lf === lf) delete window.lf;
|
||||
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
@ -5,10 +5,7 @@
|
||||
<template>
|
||||
<div
|
||||
class="flow-node-shell"
|
||||
:class="[`flow-node-shell--${flowType}`, {
|
||||
'flow-node-shell--selected': selected,
|
||||
'flow-node-shell--queue-drop-active': queueDropActive,
|
||||
}]"
|
||||
:class="[`flow-node-shell--${flowType}`, { 'flow-node-shell--selected': selected }]"
|
||||
>
|
||||
<Handle
|
||||
v-if="showTarget"
|
||||
@ -89,7 +86,6 @@ const props = defineProps({
|
||||
id: String,
|
||||
data: Object,
|
||||
selected: Boolean,
|
||||
queueDropActive: Boolean,
|
||||
connectable: [Boolean, Number, String, Function],
|
||||
});
|
||||
|
||||
@ -263,11 +259,6 @@ onMounted(() => bindComponent(componentRef.value));
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
&--queue-drop-active .flow-node-content {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 0 0 3px rgba(47, 128, 237, 0.35), 0 10px 24px rgba(47, 128, 237, 0.18);
|
||||
}
|
||||
|
||||
&--start,
|
||||
&--subStart { --node-accent: #239565; }
|
||||
|
||||
|
||||
@ -501,6 +501,7 @@ const subscribeDebounced = debounce(async (sub) => {
|
||||
// 清理旧回调
|
||||
if (socketChannelCallbacks.value[channel]) {
|
||||
socket.off(channel, socketChannelCallbacks.value[channel]);
|
||||
console.log('清理旧回调:', channel);
|
||||
}
|
||||
|
||||
const callback = (data) => {
|
||||
@ -554,21 +555,15 @@ const handleOpenLog = async (row) => {
|
||||
logDialogData.value = res.rows
|
||||
}
|
||||
}
|
||||
const refreshAfterSocketReconnect = () => {
|
||||
getList()
|
||||
getHistoryList()
|
||||
}
|
||||
onMounted(() => {
|
||||
getTaskData();
|
||||
getList();
|
||||
getHistoryList();
|
||||
subscribeDebounced(true)
|
||||
socket?.on('open', refreshAfterSocketReconnect)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
subscribeDebounced(false)
|
||||
socket?.off('open', refreshAfterSocketReconnect)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -543,9 +543,11 @@ const subscribeDebounced = debounce(async (sub) => {
|
||||
// 清理旧回调
|
||||
if (socketChannelCallbacks.value[channel]) {
|
||||
socket.off(channel, socketChannelCallbacks.value[channel]);
|
||||
console.log('清理旧回调:', channel);
|
||||
}
|
||||
|
||||
const callback = (data) => {
|
||||
console.log('data', data)
|
||||
if (data !== '500') {
|
||||
if (data.status !== 0) {
|
||||
const steps = pointList_runningTask.value[data.taskId] || []
|
||||
@ -596,10 +598,6 @@ const handleOpenLog = async (row) => {
|
||||
logDialogData.value = res.rows
|
||||
}
|
||||
}
|
||||
const refreshAfterSocketReconnect = () => {
|
||||
getList()
|
||||
getHistoryList()
|
||||
}
|
||||
onMounted(() => {
|
||||
getTaskData();
|
||||
getPhoneData();
|
||||
@ -607,12 +605,10 @@ onMounted(() => {
|
||||
getList();
|
||||
getHistoryList();
|
||||
subscribeDebounced(true)
|
||||
socket?.on('open', refreshAfterSocketReconnect)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
subscribeDebounced(false)
|
||||
socket?.off('open', refreshAfterSocketReconnect)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user