feat(flow): 优化流程节点配置和单节点执行功能

- 移除未使用的 start.svg 和 end.svg 图标导入
- 删除未使用的 booleanOptions 配置函数
- 为 SPEAKER_PLAYAUDIO 节点添加 nodeType 和 audioPath 参数配置
- 重构单节点参数表单渲染逻辑,使用 computed 属性优化字段映射
- 添加单节点执行状态加载指示器和防重复提交功能
- 实现单节点参数验证和数据构建功能
- 修复流程图边连接逻辑,优化相同边删除操作
- 简化旧流程数据获取逻辑,移除不必要的变量赋值
- 更新节点执行权限判断逻辑,支持更多节点类型过滤
- 优化单节点执行时的动作名称处理和节点类型识别逻辑
- 调整单节点表单样式,移除垂直居中对齐属性
This commit is contained in:
lixiaolong 2026-08-07 10:48:18 +08:00
parent b332a5461e
commit 84e1354c88
3 changed files with 93 additions and 45 deletions

View File

@ -87,7 +87,22 @@ const isEditing = ref(false);
const localName = ref(""); const localName = ref("");
const nameInputRef = ref(); const nameInputRef = ref();
const isSelectArea = computed(() => props.nodeType === "selectArea"); const isSelectArea = computed(() => props.nodeType === "selectArea");
const canExecute = computed(() => Boolean(props.nodeType) && !["NONE", "selectArea"].includes(props.nodeType)); const flowContextActions = new Set([
"NONE",
"START",
"END",
"SUB_START",
"SUB_END",
"START_LOOP",
"STOP_LOOP",
"BRANCH",
"GET_CURRENT_OBJECT",
]);
const canExecute = computed(() => {
if (isSelectArea.value) return false;
const action = String(props.nodeProperties?.action || "").toUpperCase();
return Boolean(action) && !flowContextActions.has(action);
});
const typeLabel = computed(() => { const typeLabel = computed(() => {
if (!props.nodeType || ["NONE", "selectArea"].includes(props.nodeType)) return ""; if (!props.nodeType || ["NONE", "selectArea"].includes(props.nodeType)) return "";
return { EDGE: "设备", LLM: "模型", AE: "评估" }[props.nodeType] || props.nodeType; return { EDGE: "设备", LLM: "模型", AE: "评估" }[props.nodeType] || props.nodeType;

View File

@ -3,8 +3,6 @@ import videoSvg from './icon/video.svg'
import loopSvg from './icon/loop.svg' import loopSvg from './icon/loop.svg'
import stopLoopSvg from './icon/stopLoop.svg' import stopLoopSvg from './icon/stopLoop.svg'
import switchSvg from './icon/switch.svg' import switchSvg from './icon/switch.svg'
import startSvg from './icon/start.svg'
import endSvg from './icon/end.svg'
import microphoneSvg from './icon/microphone.svg' import microphoneSvg from './icon/microphone.svg'
import sleepSvg from './icon/sleep.svg' import sleepSvg from './icon/sleep.svg'
import planSvg from './icon/plan.svg' import planSvg from './icon/plan.svg'
@ -14,11 +12,6 @@ import audioSvg from './icon/audio.svg'
import touchSvg from './icon/touch.svg' import touchSvg from './icon/touch.svg'
import expressionSvg from './icon/expression.svg' import expressionSvg from './icon/expression.svg'
const booleanOptions = () => [
{ value: false, label: '否' },
{ value: true, label: '是' }
]
const httpMethodOptions = () => { const httpMethodOptions = () => {
return [{ return [{
value: 'POST', value: 'POST',
@ -464,8 +457,10 @@ function handler(params) {
type: "serviceNode", type: "serviceNode",
desc: "用于播放音频的节点", desc: "用于播放音频的节点",
action: 'SPEAKER_PLAYAUDIO', action: 'SPEAKER_PLAYAUDIO',
nodeType: 'EDGE',
nodeParams: [ nodeParams: [
{ name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true } { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: [], disabled: true },
{ name: "audioPath", type: "input", input: "", disabled: true }
], ],
outputType: 'json' outputType: 'json'
}, },

View File

@ -180,17 +180,15 @@
label-width="auto" label-width="auto"
:disabled="flowStore.disableForm" :disabled="flowStore.disableForm"
> >
<div v-for="(property, index) in formData.nodeParams" :key="index"> <div v-for="(field, index) in singleNodeFields" :key="field.key">
<el-row class="single-node-param-row"> <el-row class="single-node-param-row">
<el-form-item <el-form-item
class="single-node-param-name" class="single-node-param-name"
:label="index === 0 ? '参数名' : ''" :label="index === 0 ? '参数名' : ''"
:prop="`nodeParams.${index}.name`"
:rules="[{ required: true, message: '请输入参数名', trigger: 'blur' }]"
> >
<el-input <el-input
:disabled="property?.disabled || false" :disabled="field.param?.disabled || false"
v-model="property.name" v-model="field.param.name"
placeholder="请输入" placeholder="请输入"
clearable clearable
/> />
@ -198,27 +196,25 @@
<el-form-item <el-form-item
class="single-node-param-value" class="single-node-param-value"
:label="index === 0 ? '参数值' : ''" :label="index === 0 ? '参数值' : ''"
:prop="`nodeParams.${index}.input`"
:rules="[{ required: true, message: '请输入参数值', trigger: 'blur' }]"
> >
<el-input-number <el-input-number
v-if="property.componentType === 'number'" v-if="field.param.componentType === 'number'"
v-model="property.input" v-model="field.param.input"
:min="0" :min="0"
:max="property.max || Infinity" :max="field.param.max || Infinity"
:step="property.step || 1" :step="field.param.step || 1"
:step-strictly="true" :step-strictly="true"
placeholder="请输入" placeholder="请输入"
clearable clearable
/> />
<el-select <el-select
v-model="property.input" v-model="field.param.input"
v-else-if="property.componentType === 'select'" v-else-if="field.param.componentType === 'select'"
:disabled="property.deviceLocked" :disabled="field.param.deviceLocked"
@change="property.name === 'robotId' && handleSingleNodeRobotChange(property.input)" @change="field.param.name === 'robotId' && handleSingleNodeRobotChange(field.param.input)"
> >
<el-option <el-option
v-for="item in property.selectOptions" v-for="item in field.param.selectOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
@ -226,14 +222,14 @@
</el-select> </el-select>
<el-input <el-input
v-else v-else
v-model="property.input" v-model="field.param.input"
autosize autosize
type="textarea" type="textarea"
resize="none" resize="none"
placeholder="请输入" placeholder="请输入"
clearable clearable
/> />
<div v-if="property.name === 'deviceId' && property.deviceLocked" class="device-lock-hint"> <div v-if="field.param.name === 'deviceId' && field.param.deviceLocked" class="device-lock-hint">
已自动匹配当前机器人设备 已自动匹配当前机器人设备
</div> </div>
</el-form-item> </el-form-item>
@ -243,7 +239,7 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="visible = false">取消</el-button> <el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="execute"> 确定 </el-button> <el-button type="primary" :loading="singleNodeExecuting" @click="execute"> 确定 </el-button>
</div> </div>
</template> </template>
</el-dialog> </el-dialog>
@ -628,7 +624,9 @@ const validEdge = (data) => {
} }
if (sourceName === "right" && targetName === "left") { if (sourceName === "right" && targetName === "left") {
if (hasSameEdge(data, data.id)) lf.deleteEdge(data.id); if (hasSameEdge(data, data.id)) {
lf.deleteEdge(data.id);
}
return; return;
} else if ( } else if (
(sourceName === "right" && targetName === "right") || (sourceName === "right" && targetName === "right") ||
@ -654,8 +652,7 @@ const validEdge = (data) => {
const instId = ref(null); const instId = ref(null);
const changeState = (value) => { const changeState = (value) => {
const flowData = JSON.parse(JSON.stringify(lf.getGraphData())); oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()));
oldFlowData = flowData;
isOpen.value = false; isOpen.value = false;
flowStore.updateDisableForm(true); flowStore.updateDisableForm(true);
flowState.value = value.type; flowState.value = value.type;
@ -763,7 +760,7 @@ const registerBeforeUnload = () => {
window.addEventListener("beforeunload", handleBeforeUnload); window.addEventListener("beforeunload", handleBeforeUnload);
}; };
const handleBeforeUnload = (event) => { const handleBeforeUnload = (_event) => {
if (flowState.value === "pause" && !flowInfoData.value.isLog) { if (flowState.value === "pause" && !flowInfoData.value.isLog) {
flowStopFn(); flowStopFn();
} }
@ -879,8 +876,7 @@ const defaultProps = {
}; };
const showNodeRelationship = () => { const showNodeRelationship = () => {
const { nodes } = lf.getGraphData(); const { nodes } = lf.getGraphData();
const outputData = convertToTree(nodes); treeData.value = convertToTree(nodes);
treeData.value = outputData;
}; };
const handleTreeNodeClick = (node) => { const handleTreeNodeClick = (node) => {
@ -902,8 +898,47 @@ const formData = reactive({
const rules = reactive({}); const rules = reactive({});
const executeForm = ref(); const executeForm = ref();
const nodeAction = ref(""); const nodeAction = ref("");
const singleNodeExecuting = ref(false);
const stationLoading = ref(false); const stationLoading = ref(false);
const singleNodeRobotOptions = ref([]); const singleNodeRobotOptions = ref([]);
const flattenSingleNodeParams = (params, parentPath = [], fields = []) => {
(params || []).forEach((param, index) => {
const name = String(param?.name || "").trim();
const path = [...parentPath, name || `param-${index}`];
if (Array.isArray(param?.children)) {
flattenSingleNodeParams(param.children, path, fields);
return;
}
if (!name && (param?.input === "" || param?.input === null || param?.input === undefined)) return;
fields.push({ key: `${path.join(".")}-${index}`, param });
});
return fields;
};
const singleNodeFields = computed(() => flattenSingleNodeParams(formData.nodeParams));
const buildSingleNodePayload = (params) => {
const payload = {};
(params || []).forEach((param) => {
const name = String(param?.name || "").trim();
if (!name) return;
payload[name] = Array.isArray(param.children)
? buildSingleNodePayload(param.children)
: param.input;
});
return payload;
};
const validateSingleNodeParams = () => {
for (const { param } of singleNodeFields.value) {
if (!String(param?.name || "").trim()) {
ElMessage.warning("请输入参数名");
return false;
}
if (param.input === "" || param.input === null || param.input === undefined) {
ElMessage.warning(`请输入参数 ${param.name} 的值`);
return false;
}
}
return true;
};
const singleNodeRobotId = computed(() => const singleNodeRobotId = computed(() =>
formData.nodeParams.find((item) => item.name === "robotId")?.input formData.nodeParams.find((item) => item.name === "robotId")?.input
); );
@ -1002,11 +1037,14 @@ const loadSingleNodeRobots = async () => {
const singleNodeExecution = async (e) => { const singleNodeExecution = async (e) => {
const { name, nodeType, nodeParams, action } = e.detail; const { name, nodeType, nodeParams, action } = e.detail;
const executionParams = JSON.parse(JSON.stringify(nodeParams || [])); const executionParams = JSON.parse(JSON.stringify(nodeParams || []));
const normalizedAction = String(action || "").toUpperCase();
const effectiveNodeType = nodeType || (getDeviceKindForAction(normalizedAction) ? "EDGE" : "");
nodeName.value = name; nodeName.value = name;
visible.value = true; visible.value = true;
nodeAction.value = action; nodeAction.value = normalizedAction;
singleNodeExecuting.value = false;
stationLoading.value = false; stationLoading.value = false;
if (nodeType === "EDGE") { if (effectiveNodeType === "EDGE") {
await loadSingleNodeRobots(); await loadSingleNodeRobots();
formData.nodeParams = [ formData.nodeParams = [
{ {
@ -1022,7 +1060,7 @@ const singleNodeExecution = async (e) => {
} else { } else {
formData.nodeParams = executionParams; formData.nodeParams = executionParams;
} }
if (action === "AGV_MOVE_TO_STATION") { if (nodeAction.value === "AGV_MOVE_TO_STATION") {
const stationParam = formData.nodeParams.find((item) => item.name === "stationId"); const stationParam = formData.nodeParams.find((item) => item.name === "stationId");
if (stationParam) { if (stationParam) {
stationParam.input = ""; stationParam.input = "";
@ -1042,21 +1080,22 @@ const execute = async () => {
return; return;
} }
} }
const result = await executeForm.value.validate(); if (!validateSingleNodeParams()) return;
if (result) { singleNodeExecuting.value = true;
const obj = {}; try {
formData.nodeParams.forEach((item) => {
obj[item.name] = item.input;
});
const res = await flowAction({ const res = await flowAction({
action: nodeAction.value, action: nodeAction.value,
payload: obj, payload: buildSingleNodePayload(formData.nodeParams),
}); });
if (res.code === 200) { if (res.code === 200) {
ElMessage.success("执行成功"); ElMessage.success("执行成功");
} else { } else {
ElMessage.error(res.msg); ElMessage.error(res.msg);
} }
} catch (error) {
console.error("单节点执行失败:", error);
} finally {
singleNodeExecuting.value = false;
} }
}; };
@ -1347,7 +1386,6 @@ onUnmounted(() => {
.single-node-param-row { .single-node-param-row {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center;
justify-content: space-between; justify-content: space-between;
gap: 24px; gap: 24px;
align-items: start; align-items: start;