From 0d70a0ff038d47f3b55bcc5d44f100e349816c56 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Tue, 17 Mar 2026 16:30:40 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=96=B0=E5=A2=9E=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=BD=93=E5=89=8D=E5=BE=AA=E7=8E=AF=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E8=8A=82=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/flow.js | 26 +- .../MechanicalArm/components/UrdfViewer.vue | 2 +- src/views/flow/config.js | 26 +- src/views/flow/index.vue | 2 +- src/views/flow/nodes/function/currentLoop.vue | 460 ++++++++++++++++++ .../flow/nodes/function/currentLoopNode.js | 219 +++++++++ src/views/flow/nodes/function/index.js | 2 + src/views/flow/nodes/function/sleep.vue | 13 + 8 files changed, 730 insertions(+), 20 deletions(-) create mode 100644 src/views/flow/nodes/function/currentLoop.vue create mode 100644 src/views/flow/nodes/function/currentLoopNode.js diff --git a/src/utils/flow.js b/src/utils/flow.js index 68474d7..48ff78a 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -1,17 +1,17 @@ export const getInput = (nodeId, isStrict = true) => { const data = lf.getGraphData(); - let flag = false - if (isStrict) { - data.edges.forEach(item => { - if (item.targetNodeId === nodeId) { - flag = true - } - }); - } else { - flag = true - } + // let flag = false + // if (isStrict) { + // data.edges.forEach(item => { + // if (item.targetNodeId === nodeId) { + // flag = true + // } + // }); + // } else { + // flag = true + // } - if (flag) { + // if (flag) { const arr = [] const filterNodes = ['sleep', 'stopLoop', 'subEnd', 'subStart', 'branch', 'end'] data.nodes.forEach(item => { @@ -51,8 +51,8 @@ export const getInput = (nodeId, isStrict = true) => { arr.push(obj) }) return arr - } - return flag + // } + // return flag } const transformTree = (arr, parent = [], type) => { diff --git a/src/views/device/register/components/MechanicalArm/components/UrdfViewer.vue b/src/views/device/register/components/MechanicalArm/components/UrdfViewer.vue index 836c781..bdcc52b 100644 --- a/src/views/device/register/components/MechanicalArm/components/UrdfViewer.vue +++ b/src/views/device/register/components/MechanicalArm/components/UrdfViewer.vue @@ -216,7 +216,7 @@ const loadRobot = async () => { // 4. 坐标轴修正 robot.rotation.x = -Math.PI / 2 - robot.rotation.z = -Math.PI / 2 + robot.rotation.z = Math.PI / 2 robot.updateMatrixWorld(true) // 应用颜色 diff --git a/src/views/flow/config.js b/src/views/flow/config.js index 953ed03..c7847bb 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -226,7 +226,7 @@ export const collapseList = [ { name: "deviceId", type: "input", input: "", componentType: 'select', selectOptions: deviceOptions(), disabled: true } ], outputType: 'video', - outputParams: [{ name: 'videoUrl', type: 'array', children: [], desc: '视频播放地址数组'}] + outputParams: [{ name: 'videoUrl', type: 'array', children: [], desc: '视频播放地址数组', disabled: true}] } ], }, @@ -293,7 +293,7 @@ export const collapseList = [ { name: "body", type: "input", input: "", disabled: true, required: true }, ], outputType: 'json', - outputParams: [{ name: 'result', type: 'Object', children: [], desc: 'HTTP请求结果'}] + outputParams: [{ name: 'result', type: 'Object', children: [], desc: 'HTTP请求结果', disabled: true}] }, { icon: expressionSvg, @@ -306,8 +306,24 @@ export const collapseList = [ { name: "input", type: "input", input: "", required: true } ], outputType: 'json', - outputParams: [{ name: 'ret', type: 'Object', children: [], desc: 'js节点返回'}] - } + outputParams: [{ name: 'ret', type: 'Object', children: [], desc: 'js节点返回', disabled: true}] + }, + { + icon: microphoneSvg, + name: "获取当前循环对象", + type: "currentLoop", + desc: "获取当前循环对象", + action: 'GET_CURRENT_OBJECT', + nodeType: 'getCurrentObject', + nodeParams: [ + { name: "array", type: "input", input: "", disabled: true } + ], + outputType: 'json', + outputParams: [ + { name: 'index', type: 'number', desc: '索引', disabled: true}, + { name: 'object', type: 'Object', children: [], desc: '当前循环对象', disabled: true} + ] + }, ], }, { @@ -382,7 +398,7 @@ export const collapseList = [ { name: "vehType", type: "input", input: "su7", disabled: true }, ], outputType: 'json', - outputParams: [{ name: 'coordinates', type: 'Object', children: [], desc: '坐标对象'}] + outputParams: [{ name: 'coordinates', type: 'Object', children: [], desc: '坐标对象', disabled: true}] }, { icon: dialogueSvg, name: "tts语音合成", diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue index 612c036..f6d36c9 100644 --- a/src/views/flow/index.vue +++ b/src/views/flow/index.vue @@ -211,7 +211,7 @@ const dragover = (e) => { const drop = (e) => { let node = e.dataTransfer.getData("flowNode"); node = JSON.parse(node); - if (["stopLoop", "subEnd", "subStart"].includes(node.type)) { + if (["stopLoop", "subEnd", "subStart", "current"].includes(node.type)) { ElMessage.warning("此节点只能拖拽到循环节点的循环体中"); return; } diff --git a/src/views/flow/nodes/function/currentLoop.vue b/src/views/flow/nodes/function/currentLoop.vue new file mode 100644 index 0000000..1470b2f --- /dev/null +++ b/src/views/flow/nodes/function/currentLoop.vue @@ -0,0 +1,460 @@ + + + + + diff --git a/src/views/flow/nodes/function/currentLoopNode.js b/src/views/flow/nodes/function/currentLoopNode.js new file mode 100644 index 0000000..0daeb97 --- /dev/null +++ b/src/views/flow/nodes/function/currentLoopNode.js @@ -0,0 +1,219 @@ +// 导入 HtmlNode 及其模型,为后续继承做准备 +import { HtmlNode, HtmlNodeModel } from "@logicflow/core"; +// 导入 Vue 相关方法,用于渲染组件 +import { createApp, h, nextTick } from "vue"; +import ElementPlus from "element-plus"; +// 导入 Vue 组件 +// @ts-ignore +import CurrentLoop from "./currentLoop.vue"; +import OuterNode from "../../components/OuterNode.vue"; +import JsonViewer from 'vue3-json-viewer' + +/** + * 定义一个 元素的 HTML 节点类,继承自 HtmlNode + * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑 + */ +class CurrentLoopNodeHtmlNode extends HtmlNode { + resizeObserver = null; + isMounted; // 标记组件是否已挂载 + r; // 渲染函数 + app; // Vue 应用实例 + container = null; + + static reusePool = new Map(); // 节点复用池 + + /** + * 构造函数 + * @param props 传递给节点的属性,包括模型、图模型等 + */ + constructor(props) { + super(props); + this.initVueApp(props); + } + + initVueApp(props) { + this.isMounted = false; + + this.hideAnchor = false; + this.autoExpand = true; // 防止锚点被折叠 + this.anchorsPreset = "default"; // 重置锚点预设 + // 创建 元素的渲染函数 + this.r = h(OuterNode, { + model: props.model, + component: CurrentLoop, + properties: { + ...props.model.getProperties(), + }, + onContentChange: this.handleContentChange.bind(this), + onBindRef: this.handleComponentInstance.bind(this) + }); + + // 创建 Vue 应用实例,并指定渲染函数 + this.app = createApp({ + render: () => this.r, + }); + } + + /** + * 将 HTML 内容设置到指定的根元素上 + * @param rootEl 根元素 + */ + async setHtml(rootEl) { + const nodeId = this.props.model.id; + if (CurrentLoopNodeHtmlNode.reusePool.has(nodeId)) { + rootEl.appendChild(CurrentLoopNodeHtmlNode.reusePool.get(nodeId)); + return; + } + + if (!this.isMounted) { + this.isMounted = true; + this.container = document.createElement("div"); + this.container.style.display = "inline-block"; // 关键:确保容器自适应内容 + + rootEl.appendChild(this.container); + this.app.use(ElementPlus); + this.app.use(JsonViewer); + + this.app.mount(this.container); + await nextTick(); + this.setupSizeObserver(); + CurrentLoopNodeHtmlNode.reusePool.set(nodeId, this.container); + } else { + this.r.component.props.properties = this.props.model.getProperties(); + } + } + + /** + * 获取节点文本内容 + * 对于元素,返回 null,因为其内容由特定组件渲染 + * @returns {null} + */ + getText() { + return null; + } + + handleComponentInstance(data) { + // 将组件实例保存到节点模型 + this.props.model.setComponentInstance(data) + } + + handleContentChange() { + // 内容变化时强制更新尺寸 + this.updateNodeSize(); + } + + // 渲染完成后获取实际尺寸 + updateNodeSize() { + if (this.container) { + const { SCALE_X, SCALE_Y } = this.props.graphModel.transformModel; + const node = this.container.querySelector(".node__container"); + const rect = node.getBoundingClientRect(); + this.props.model.updateSize(rect.width / SCALE_X, rect.height / SCALE_Y); + } + } + + setupSizeObserver() { + // 首次渲染立即检测 + requestAnimationFrame(() => { + this.updateNodeSize(); + // 持续监听变化 + this.resizeObserver = new ResizeObserver(() => { + this.updateNodeSize(); + }); + this.resizeObserver.observe(this.container); + }); + } + + // 组件卸载时移除监听 + onDestroy() { + this.resizeObserver?.disconnect(); + } +} + +/** + * 定义一个元素的 HTML 模型类,继承自 HtmlNodeModel + * 该类主要设置节点的属性和样式 + */ +class CurrentLoopNodeHtmlModel extends HtmlNodeModel { + initNodeData(data) { + super.initNodeData(data); + } + + // 保存组件实例引用 + setComponentInstance(instance) { + this.componentInstance = instance; + } + + // 验证表单 + async validateForm() { + const result = this.componentInstance.validateForm() + return result + } + + setCustomProperties() { + this.componentInstance.setNodeProperties() + } + + /** + * 设置节点属性 + * 包括宽度、高度、文本编辑属性等 + */ + setAttributes() { + // 初始设置为0,后续动态更新 + this.width = 0; + this.height = 0; + this.text.editable = false; + } + + updateSize(width, height) { + this.width = width + 24; + this.height = height + 50; + this.initNodeData(this); // 触发节点重绘 + } + + // 定义节点只有左右两个锚点. 锚点位置通过中心点和宽度算出来。 + getDefaultAnchor() { + let _a = this, + x = _a.x, + y = _a.y, + width = _a.width, + height = _a.height; + return [ + { + x: x + width / 2 + 4, + y: y, + name: "right", + id: "".concat(this.id, "_1"), + properties: { connectionType: "source" }, + }, + { + x: x - width / 2, + y: y, + name: "left", + id: "".concat(this.id, "_3"), + properties: { connectionType: "target" }, + }, + ]; + } + + /** + * 获取节点轮廓样式 + * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果 + * @returns {object} 节点轮廓样式 + */ + getOutlineStyle() { + const style = super.getOutlineStyle(); + style.stroke = "none"; + style.hover.stroke = "none"; + return style; + } +} + +// 导出方法注册 +export function registerCurrentLoopNode(lf) { + lf.register({ + type: "currentLoop", + view: CurrentLoopNodeHtmlNode, + model: CurrentLoopNodeHtmlModel + }); +} diff --git a/src/views/flow/nodes/function/index.js b/src/views/flow/nodes/function/index.js index 9fb774d..feb499d 100644 --- a/src/views/flow/nodes/function/index.js +++ b/src/views/flow/nodes/function/index.js @@ -6,6 +6,7 @@ import { registerSubStartNode } from './subStart' import { registerSleepNode } from './sleep' import { registerHttpNode } from './httpNode' import { registerCodeNode } from './codeNode' +import { registerCurrentLoopNode } from './currentLoopNode' export const registerFunction = (lf) => { registerLoopBodyNode(lf) @@ -16,4 +17,5 @@ export const registerFunction = (lf) => { registerSleepNode(lf) registerHttpNode(lf) registerCodeNode(lf) + registerCurrentLoopNode(lf) } \ No newline at end of file diff --git a/src/views/flow/nodes/function/sleep.vue b/src/views/flow/nodes/function/sleep.vue index 4f8ed8b..97379fa 100644 --- a/src/views/flow/nodes/function/sleep.vue +++ b/src/views/flow/nodes/function/sleep.vue @@ -94,10 +94,16 @@ :prop="`nodeParams.${index}.quote`" > @@ -217,6 +223,13 @@ const visibleChange = (value) => { } }; +const cascaderRefs = ref([]); +const cascaderChange = (value, index) => { + const selectedOptions = cascaderRefs.value[index].getCheckedNodes(true); + formData.nodeParams[index].quote = value; + formData.nodeParams[index].quoteType = selectedOptions[0].data.type; +}; + const validateForm = async () => { try { const result = await dynamicForm.value.validate();