From 5da44c55883d8a90fce17e8c2e314dc639997440 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Mon, 13 Apr 2026 10:31:55 +0800 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E8=8A=82=E7=82=B9=E5=8F=82=E6=95=B0=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/flow.js | 31 + .../flow/components/FormItemRecursive.vue | 221 ++++++++ src/views/flow/components/NodeTitle.vue | 4 +- src/views/flow/components/OuterNode.vue | 84 +-- src/views/flow/components/ParamsDrawer.vue | 530 ++++++++++++++++++ src/views/flow/config.js | 5 +- src/views/flow/index.vue | 36 +- src/views/flow/nodes/common/serviceNode.vue | 508 +---------------- src/views/flow/nodes/common/startNode.js | 407 +++++++------- src/views/flow/nodes/common/startNode.vue | 276 +-------- vite.config.js | 4 +- 11 files changed, 1074 insertions(+), 1032 deletions(-) create mode 100644 src/views/flow/components/FormItemRecursive.vue create mode 100644 src/views/flow/components/ParamsDrawer.vue diff --git a/src/utils/flow.js b/src/utils/flow.js index 48ff78a..ddcced4 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -263,4 +263,35 @@ export const convertToTree = (data) => { }); return result; +} + + +export const filterEmptyName = (obj) => { + // 1. 如果是数组,遍历每一项递归处理 + if (Array.isArray(obj)) { + return obj + .map(item => filterEmptyName(item)) // 先递归处理子项 + .filter(item => item !== null); // 过滤掉被剔除的空项 + } + + // 2. 如果是对象,先检查 name 是否为空,为空直接返回 null(剔除) + if (typeof obj === 'object' && obj !== null) { + // 核心:name 为空字符串 → 直接剔除这个对象 + if (obj.name === '') { + return null; + } + + // 创建新对象,保留原有属性 + const newObj = { ...obj }; + + // 如果有 children,递归过滤子节点 + if (newObj.children && Array.isArray(newObj.children)) { + newObj.children = filterEmptyName(newObj.children); + } + + return newObj; + } + + // 基础类型直接返回 + return obj; } \ No newline at end of file diff --git a/src/views/flow/components/FormItemRecursive.vue b/src/views/flow/components/FormItemRecursive.vue new file mode 100644 index 0000000..e615217 --- /dev/null +++ b/src/views/flow/components/FormItemRecursive.vue @@ -0,0 +1,221 @@ + + + + + + \ No newline at end of file diff --git a/src/views/flow/components/NodeTitle.vue b/src/views/flow/components/NodeTitle.vue index 5e6a4ab..9ef336c 100644 --- a/src/views/flow/components/NodeTitle.vue +++ b/src/views/flow/components/NodeTitle.vue @@ -44,7 +44,7 @@ - - + --> -
- -
- - - - - \ No newline at end of file diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue new file mode 100644 index 0000000..82cff94 --- /dev/null +++ b/src/views/flow/components/ParamsDrawer.vue @@ -0,0 +1,530 @@ + + + + \ No newline at end of file diff --git a/src/views/flow/config.js b/src/views/flow/config.js index 505faa9..3c5ccc8 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -398,7 +398,10 @@ 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: [ + { name: 'x', type: 'number', desc: 'x坐标' }, + { name: 'y', type: 'number', desc: 'y坐标' } + ], desc: '坐标对象', }] }, { icon: dialogueSvg, name: "tts语音合成", diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue index fdb5466..187dbaa 100644 --- a/src/views/flow/index.vue +++ b/src/views/flow/index.vue @@ -95,6 +95,12 @@ @changeState="changeState" :flowId="flowInfoData.itemId" /> + + @@ -185,6 +191,7 @@ import "@logicflow/core/lib/style/index.css"; import "@logicflow/extension/lib/style/index.css"; import { lfConfig, registerCustomizeNode } from "./config"; import TestRun from "./components/TestRun.vue"; +import ParamsDrawer from "./components/ParamsDrawer.vue"; import Aside from "./components/Aside.vue"; import { ElMessage } from "element-plus"; import { useFlowStore } from "@/store/modules/flow"; @@ -614,16 +621,16 @@ const initFlow = () => { }); // 监听历史记录变化 - lf.on("history:change", ({ data }) => { - if (flowStore.disableForm) { - return; - } - const flowData = lf.getGraphData(); - if (JSON.stringify(oldFlowData) === JSON.stringify(flowData)) { - return; - } - flowState.value = "updateData"; - }); + // lf.on("history:change", ({ data }) => { + // if (flowStore.disableForm) { + // return; + // } + // const flowData = lf.getGraphData(); + // if (JSON.stringify(oldFlowData) === JSON.stringify(flowData)) { + // return; + // } + // flowState.value = "updateData"; + // }); // 监听子节点拖动事件 lf.on("node:mousemove", ({ data, e }) => { @@ -641,6 +648,12 @@ const initFlow = () => { node.setCustomProperties && node.setCustomProperties(); }); + lf.on("node:dbclick", ({ data, e }) => { + showParamsDrawer.value = true; + paramsDrawerData.value = data; + console.log(data) + }); + lfRef.value = lf; window.lf = lf; } @@ -757,6 +770,9 @@ const execute = async () => { } } +const showParamsDrawer = ref(false); +const paramsDrawerData = ref({}) + onMounted(() => { justLoadFlow(); initFlow(); diff --git a/src/views/flow/nodes/common/serviceNode.vue b/src/views/flow/nodes/common/serviceNode.vue index eba9341..4038cec 100644 --- a/src/views/flow/nodes/common/serviceNode.vue +++ b/src/views/flow/nodes/common/serviceNode.vue @@ -41,192 +41,20 @@ :nodeName="props.properties.name" :nodeDesc="props.properties.desc" :zoom-state="nodeZoom" - @zoom="zoom" + @setNodeName="setNodeName" /> -
-
-
-
-
输入
-
-
- - - -
-
-
- -
- - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-
-
-
-
-
输出
-
-
- - - -
-
- -
- - - -
-
-
+ diff --git a/src/views/flow/nodes/common/startNode.js b/src/views/flow/nodes/common/startNode.js index 50621d6..2c1a70f 100644 --- a/src/views/flow/nodes/common/startNode.js +++ b/src/views/flow/nodes/common/startNode.js @@ -1,209 +1,200 @@ -// 导入 HtmlNode 及其模型,为后续继承做准备 -import { HtmlNode, HtmlNodeModel } from '@logicflow/core'; -// 导入 Vue 相关方法,用于渲染组件 -import { createApp, h, nextTick } from 'vue'; -import ElementPlus from 'element-plus' -// 导入 Vue 组件 -import StartNode from './startNode.vue'; -import OuterNode from "../../components/OuterNode.vue"; -import JsonViewer from 'vue3-json-viewer' - -/** - * 定义一个 元素的 HTML 节点类,继承自 HtmlNode - * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑 - */ -class StartNodeHtmlNode 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: StartNode, - 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 (StartNodeHtmlNode.reusePool.has(nodeId)) { - rootEl.appendChild(StartNodeHtmlNode.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) // 关键:单独注册ElementPlus - this.app.use(JsonViewer) - this.app.mount(this.container); - await nextTick(); - this.setupSizeObserver(); - StartNodeHtmlNode.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__box') - 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 StartNodeHtmlModel 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; - this.deletable = 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 - 24, y: y - 25, name: 'right', id: "".concat(this.id, "_1"), properties: { connectionType: 'source' }, onlyAsSource: true }, - ]; - } - - /** - * 获取节点轮廓样式 - * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果 - * @returns {object} 节点轮廓样式 - */ - getOutlineStyle() { - const style = super.getOutlineStyle(); - style.stroke = 'none'; - style.hover.stroke = 'none'; - return style; - } -} - -// 导出方法注册 -export function registerStartNode(lf) { - lf.register({ - type: "start", - view: StartNodeHtmlNode, - model: StartNodeHtmlModel, - events: { - remove: (event) => { - // 阻止默认的删除事件 - event.preventDefault(); - // 可以在此处添加自定义的删除逻辑,例如弹出提示框 - alert('此节点不可删除'); - }, - } - }) +// 导入 HtmlNode 及其模型,为后续继承做准备 +import { HtmlNode, HtmlNodeModel } from '@logicflow/core'; +// 导入 Vue 相关方法,用于渲染组件 +import { createApp, h, nextTick } from 'vue'; +import ElementPlus from 'element-plus' +// 导入 Vue 组件 +import StartNode from './startNode.vue'; +import OuterNode from "../../components/OuterNode.vue"; +import JsonViewer from 'vue3-json-viewer' + +/** + * 定义一个 元素的 HTML 节点类,继承自 HtmlNode + * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑 + */ +class StartNodeHtmlNode 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: StartNode, + 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 (StartNodeHtmlNode.reusePool.has(nodeId)) { + rootEl.appendChild(StartNodeHtmlNode.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) // 关键:单独注册ElementPlus + this.app.use(JsonViewer) + this.app.mount(this.container); + await nextTick(); + this.setupSizeObserver(); + StartNodeHtmlNode.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__box') + 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 StartNodeHtmlModel extends HtmlNodeModel { + initNodeData(data) { + super.initNodeData(data); + } + + // 保存组件实例引用 + setComponentInstance(instance) { + this.componentInstance = instance; + } + + /** + * 设置节点属性 + * 包括宽度、高度、文本编辑属性等 + */ + setAttributes() { + // 初始设置为0,后续动态更新 + this.width = 0; + this.height = 0; + this.text.editable = false; + this.deletable = 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 - 24, y: y - 25, name: 'right', id: "".concat(this.id, "_1"), properties: { connectionType: 'source' }, onlyAsSource: true }, + ]; + } + + /** + * 获取节点轮廓样式 + * 覆盖父类方法,设置 stroke 属性为 none,以适应特定的视觉效果 + * @returns {object} 节点轮廓样式 + */ + getOutlineStyle() { + const style = super.getOutlineStyle(); + style.stroke = 'none'; + style.hover.stroke = 'none'; + return style; + } +} + +// 导出方法注册 +export function registerStartNode(lf) { + lf.register({ + type: "start", + view: StartNodeHtmlNode, + model: StartNodeHtmlModel, + effect: ['status'], + events: { + remove: (event) => { + // 阻止默认的删除事件 + event.preventDefault(); + // 可以在此处添加自定义的删除逻辑,例如弹出提示框 + alert('此节点不可删除'); + }, + } + }) } \ No newline at end of file diff --git a/src/views/flow/nodes/common/startNode.vue b/src/views/flow/nodes/common/startNode.vue index e593359..9b0ea6f 100644 --- a/src/views/flow/nodes/common/startNode.vue +++ b/src/views/flow/nodes/common/startNode.vue @@ -16,75 +16,19 @@ Start -
- - - - - - - - -
工作流的起始节点,用于设定启动工作流需要的信息
-
-
-
-
-
输入
-
-
- - - -
-
-
- - - -
-
diff --git a/vite.config.js b/vite.config.js index 5757441..723fabc 100644 --- a/vite.config.js +++ b/vite.config.js @@ -24,8 +24,8 @@ export default defineConfig(({mode, command}) => { 'typescript', ], include: [ - // 确保其他依赖被优化 - 'monaco-editor/esm/vs/language/typescript/monaco.contribution', + // 确保其他依赖被优化 + 'monaco-editor/esm/vs/language/typescript/monaco.contribution', ], }, resolve: { From 19185288125e859f12179c739c7a638e642b56f7 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Mon, 13 Apr 2026 11:27:13 +0800 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=E5=BE=AA=E7=8E=AF=E8=8A=82?= =?UTF-8?q?=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/flow/components/ParamsDrawer.vue | 4 +- src/views/flow/config.js | 10 +- src/views/flow/index.vue | 22 +- src/views/flow/nodes/common/serviceNode.js | 6 +- src/views/flow/nodes/function/currentLoop.vue | 217 ------- .../flow/nodes/function/currentLoopNode.js | 6 +- src/views/flow/nodes/function/loop.js | 552 +++++++++--------- src/views/flow/nodes/function/loop.vue | 34 +- 8 files changed, 324 insertions(+), 527 deletions(-) diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index 82cff94..b69047f 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -42,7 +42,7 @@ -
+
@@ -342,7 +342,7 @@ watch( () => { if (props.data.type === "start") { formData.inputParams = props.data.properties?.inputParams || []; - } else if (props.data.type === "serviceNode") { + } else if (['currentLoop', 'serviceNode'].includes(props.data.type)) { formData.nodeParams = props.data.properties.nodeParams; formData.outputParams = props.data.properties.outputParams; console.log('formData', formData) diff --git a/src/views/flow/config.js b/src/views/flow/config.js index 3c5ccc8..e52acdd 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -306,7 +306,7 @@ export const collapseList = [ { name: "input", type: "input", input: "", required: true } ], outputType: 'json', - outputParams: [{ name: 'result', type: 'Object', children: [], desc: 'js节点返回'}] + outputParams: [{ name: 'result', type: 'object', children: [], desc: 'js节点返回'}] }, { icon: microphoneSvg, @@ -316,12 +316,14 @@ export const collapseList = [ action: 'GET_CURRENT_OBJECT', nodeType: 'getCurrentObject', nodeParams: [ - { name: "array", type: "input", input: "", disabled: true } + { name: "array", type: "input", input: "" } ], outputType: 'json', outputParams: [ - { name: 'index', type: 'number', desc: '索引', disabled: true}, - { name: 'object', type: 'Object', children: [], desc: '当前循环对象', } + { name: 'index', type: 'number', desc: '索引'}, + { name: 'object', type: 'object', children: [ + { name: 'test', type: 'string', desc: '占位属性' } + ], desc: '当前循环对象', } ] }, ], diff --git a/src/views/flow/index.vue b/src/views/flow/index.vue index 187dbaa..1688fd2 100644 --- a/src/views/flow/index.vue +++ b/src/views/flow/index.vue @@ -282,15 +282,15 @@ const testRun = async () => { } let allValid = true; - for (const node of nodes) { - const nodeModel = lf.getNodeModelById(node.id); - if (nodeModel && nodeModel.validateForm) { - const result = await nodeModel.validateForm(); - if (!result.valid) { - allValid = false; - } - } - } + // for (const node of nodes) { + // const nodeModel = lf.getNodeModelById(node.id); + // if (nodeModel && nodeModel.validateForm) { + // const result = await nodeModel.validateForm(); + // if (!result.valid) { + // allValid = false; + // } + // } + // } if (allValid) { isOpen.value = true; @@ -645,7 +645,9 @@ const initFlow = () => { lf.on("node:mouseleave", ({ data }) => { const node = lf.getNodeModelById(data.id); - node.setCustomProperties && node.setCustomProperties(); + if (node.setCustomProperties) { + node.setCustomProperties(); + } }); lf.on("node:dbclick", ({ data, e }) => { diff --git a/src/views/flow/nodes/common/serviceNode.js b/src/views/flow/nodes/common/serviceNode.js index faa5eca..b351ef7 100644 --- a/src/views/flow/nodes/common/serviceNode.js +++ b/src/views/flow/nodes/common/serviceNode.js @@ -150,9 +150,9 @@ class ServiceNodeHtmlModel extends HtmlNodeModel { return result } - setCustomProperties() { - this.componentInstance.setNodeProperties() - } + // setCustomProperties() { + // this.componentInstance.setNodeProperties() + // } /** * 设置节点属性 diff --git a/src/views/flow/nodes/function/currentLoop.vue b/src/views/flow/nodes/function/currentLoop.vue index 1470b2f..43acc2f 100644 --- a/src/views/flow/nodes/function/currentLoop.vue +++ b/src/views/flow/nodes/function/currentLoop.vue @@ -26,119 +26,8 @@ :nodeName="props.properties.name" :nodeDesc="props.properties.desc" :zoom-state="nodeZoom" - @zoom="zoom" @setNodeName="setNodeName" /> -
-
-
-
-
输入
-
-
- - - -
-
-
- -
- - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-
-
-
输出
-
- -
- - - - - - - - - - - - - -
-
@@ -146,7 +35,6 @@ import { ref, reactive, onMounted, onUnmounted, nextTick } from "vue"; import { getInput, initNodeZoom } from "@/utils/flow"; import { useFlowStore } from "@/store/modules/flow"; -import { Plus } from "@element-plus/icons-vue"; import NodeTitle from "../../components/NodeTitle.vue"; import NodeState from "../../components/NodeState.vue"; import "vue3-json-viewer/dist/index.css"; @@ -160,46 +48,11 @@ const props = defineProps({ const emits = defineEmits(["contentChange"]); -const flowStore = useFlowStore(); const formData = reactive({ nodeParams: [], }); -const rules = reactive({}); - -const quoteOptions = ref([]); -const handleTypeChange = (index) => { - if (formData.nodeParams[index].type === "input") { - formData.nodeParams[index].quote = ""; - } else { - formData.nodeParams[index].input = ""; - const option = getInput(props.model.id); - if (option) { - quoteOptions.value = option; - } - } -}; - -const dynamicForm = ref(); - -const setNodeProperties = async () => { - try { - const result = await dynamicForm.value.validate(); - if (result) { - const data = toRaw(formData); - const properties = lf.getProperties(props.model.id); - lf.setProperties(props.model.id, { - ...properties, - ...data, - zoom: nodeZoom.value - }); - emits("contentChange"); - } - } catch { - dynamicForm.value.clearValidate(); - } -}; const setNodeName = (name) => { const properties = lf.getProperties(props.model.id); @@ -214,72 +67,6 @@ const nodeOperatingStatus = ref("NORMAL"); const inputJsonData = ref({}); const outputJsonData = ref({}); -const visibleChange = (value) => { - if (value) { - const option = getInput(props.model.id); - if (option) { - quoteOptions.value = option; - } - } -}; - -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(); - if (result) { - return { valid: true, message: "验证通过" }; - } else { - return { valid: false, message: "验证失败" }; - } - } catch { - return { valid: false, message: "验证失败" }; - } -}; - -const addFormItem = () => { - formData.nodeParams.push({ - name: "", - type: "", - desc: "", - required: false, - children: [], - }); - emits("contentChange"); -}; - -const nodeZoom = ref(props?.properties?.zoom ?? true) -const zoom = (flag) => { - nodeZoom.value = flag - initNodeZoom(props.model.id, nodeZoom.value, '.node__box') -} - -watch( - () => props.properties, - () => { - if (props.properties.nodeParams && props.properties.nodeParams.length > 0) { - formData.nodeParams = props.properties.nodeParams; - const option = getInput(props.model.id); - if (option) { - quoteOptions.value = option; - } - nextTick(() => { - initNodeZoom(props.model.id, props?.properties?.zoom ?? true, '.node__box', true) - }) - } - }, - { - immediate: true, - deep: true, - } -); - onMounted(() => { emitter.on("changeNodeState", (data) => { if (data.nodeId === props.model.id) { @@ -314,10 +101,6 @@ onUnmounted(() => { emitter.off("contentChange"); }); -defineExpose({ - validateForm, - setNodeProperties, -}); + + \ No newline at end of file diff --git a/src/views/flow/config.js b/src/views/flow/config.js index e52acdd..80d1593 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -306,7 +306,10 @@ export const collapseList = [ { name: "input", type: "input", input: "", required: true } ], outputType: 'json', - outputParams: [{ name: 'result', type: 'object', children: [], desc: 'js节点返回'}] + outputParams: [{ name: 'result', type: 'object', children: [ + {name: 'type', type: 'number', desc: '占位属性'}, + {name: 'message', type: 'string', desc: '占位属性'} + ], desc: 'js节点返回'}] }, { icon: microphoneSvg, diff --git a/src/views/flow/nodes/function/codeNode.js b/src/views/flow/nodes/function/codeNode.js index 703ce9f..fd83fd1 100644 --- a/src/views/flow/nodes/function/codeNode.js +++ b/src/views/flow/nodes/function/codeNode.js @@ -144,16 +144,6 @@ class CodeNodeHtmlModel extends HtmlNodeModel { this.componentInstance = instance; } - // 验证表单 - async validateForm() { - const result = this.componentInstance.validateForm() - return result - } - - setCustomProperties() { - this.componentInstance.setNodeProperties() - } - /** * 设置节点属性 * 包括宽度、高度、文本编辑属性等 diff --git a/src/views/flow/nodes/function/codeNode.vue b/src/views/flow/nodes/function/codeNode.vue index 01a154a..e1eef4c 100644 --- a/src/views/flow/nodes/function/codeNode.vue +++ b/src/views/flow/nodes/function/codeNode.vue @@ -40,196 +40,17 @@ :nodeType="props.properties.nodeType || 'NONE'" :nodeName="props.properties.name" :nodeDesc="props.properties.desc" - :zoom-state="nodeZoom" - @zoom="zoom" @setNodeName="setNodeName" /> -
-
-
-
-
输入
-
-
- - - -
-
-
- -
- - - - - - - - - - - - - - - - - - - - - -
-
-
-
-
-
-
-
-
-
-
-
-
输出
-
-
- - - -
-
- -
- - - -
-
-
From 75f64e8d9fc47726c781ef033c3f6e45a6b5b297 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Tue, 14 Apr 2026 11:35:42 +0800 Subject: [PATCH 04/14] =?UTF-8?q?feat:=20http=E3=80=81loop=E3=80=81sleep?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/flow/components/ParamsDrawer.vue | 626 +++++++++++++++++++-- src/views/flow/config.js | 22 +- src/views/flow/nodes/function/httpNode.js | 14 +- src/views/flow/nodes/function/httpNode.vue | 505 +---------------- src/views/flow/nodes/function/loop.js | 38 +- src/views/flow/nodes/function/loop.vue | 238 +------- src/views/flow/nodes/function/sleep.vue | 356 +----------- 7 files changed, 665 insertions(+), 1134 deletions(-) diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index f482552..9fa8067 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -43,7 +43,7 @@
-
+
From 88460d3bb12d26d35a0faa8bc99f0fcd5b5dd97b Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Tue, 14 Apr 2026 16:17:52 +0800 Subject: [PATCH 05/14] =?UTF-8?q?feat:=20http=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 | 49 ++++ src/views/flow/components/NodeTitle.vue | 5 +- src/views/flow/components/ParamsDrawer.vue | 254 +++++++++++++----- src/views/flow/config.js | 51 ++-- src/views/flow/index.vue | 33 ++- .../flow/nodes/common/FormItemRecursive.vue | 217 --------------- 6 files changed, 284 insertions(+), 325 deletions(-) delete mode 100644 src/views/flow/nodes/common/FormItemRecursive.vue diff --git a/src/utils/flow.js b/src/utils/flow.js index ddcced4..fb1372f 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -294,4 +294,53 @@ export const filterEmptyName = (obj) => { // 基础类型直接返回 return obj; +} + +/** + * 将原始表单配置数组 转换为 目标对象格式 + * @param {Array} source - 原始数据数组 + * @returns {Object} 转换后的目标格式对象 + */ +export const transformHttpNodeData = (source) => { + // 初始化结果对象 + const result = {}; + + // 遍历每一个顶级节点 + source.forEach(item => { + const { name, children } = item; + if (!name) return; // 过滤无name的无效节点 + + // 处理普通节点(config/headers/params):直接取 children 数组 + if (name !== 'body') { + result[name] = children || []; + return; + } + + // 专门处理 body 节点(特殊结构) + if (name === 'body') { + const bodyObj = { + type: '', + json: '', + formData: [] + }; + + // 遍历body的子项,赋值到对应字段 + children?.forEach(child => { + const childName = child.name; + if (childName === 'bodyType') { + bodyObj.type = child.input; + } + if (childName === 'json') { + bodyObj.json = child.input; + } + if (childName === 'formData') { + bodyObj.formData = child.children || []; + } + }); + + result.body = bodyObj; + } + }); + + return result; } \ No newline at end of file diff --git a/src/views/flow/components/NodeTitle.vue b/src/views/flow/components/NodeTitle.vue index 9ef336c..2d47b0d 100644 --- a/src/views/flow/components/NodeTitle.vue +++ b/src/views/flow/components/NodeTitle.vue @@ -6,9 +6,7 @@
{{ nodeName }} - +
@@ -213,6 +211,7 @@ const imageUrl = () => { .el-input { margin-left: 12px; + width: 120px; } .input_name_select { diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index 9fa8067..f4a2f9b 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -4,7 +4,9 @@ custom-class="custom-drawer-header" class="no-header-margin" :title="props.data.properties?.name || '参数配置'" - :before-close="handleClose" + :close-on-click-modal="false" + :close-on-press-escape="false" + :show-close="false" >
@@ -169,6 +171,7 @@ :icon="Minus" circle size="small" + :disabled="property?.disabled || false" @click="handleDelete(index)" class="deleteBtn" /> @@ -343,6 +346,7 @@ circle size="small" @click="handleDelete(index)" + :disabled="property?.disabled || false" class="deleteBtn" /> @@ -541,7 +545,7 @@ +
@@ -656,9 +667,9 @@ +
@@ -762,6 +780,7 @@ size="small" @click="(e) => addHttpNodeFormItem(e, 'body')" class="addFormItem" + v-if="httpNodeData.body.type === 'form-data'" > @@ -775,12 +794,12 @@
-
+
+
@@ -884,8 +910,7 @@
@@ -893,9 +918,9 @@ - - \ No newline at end of file From 8c1820e1a60cae5316fee5a9a070bc080acbcaa1 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Tue, 14 Apr 2026 17:22:08 +0800 Subject: [PATCH 06/14] =?UTF-8?q?feat:=20=E8=8A=82=E7=82=B9=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/flow/components/NodeState.vue | 307 +++++++++--------- src/views/flow/components/ParamsDrawer.vue | 24 +- src/views/flow/config.js | 4 +- src/views/flow/nodes/common/serviceNode.vue | 6 +- src/views/flow/nodes/common/startNode.vue | 10 +- src/views/flow/nodes/function/codeNode.vue | 6 +- src/views/flow/nodes/function/currentLoop.vue | 10 +- src/views/flow/nodes/function/httpNode.vue | 8 +- src/views/flow/nodes/function/sleep.vue | 15 +- src/views/flow/nodes/function/switch.vue | 6 +- vite.config.js | 6 +- 11 files changed, 220 insertions(+), 182 deletions(-) diff --git a/src/views/flow/components/NodeState.vue b/src/views/flow/components/NodeState.vue index 6cc7929..572c702 100644 --- a/src/views/flow/components/NodeState.vue +++ b/src/views/flow/components/NodeState.vue @@ -1,151 +1,158 @@ - - - \ No newline at end of file diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index f4a2f9b..86831a9 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -889,7 +889,7 @@ @visible-change=" (visible) => visibleChange(visible, index, property.quote) " - @change="(value) => cascaderChange(value, index)" + @change="(value) => cascaderChange(value, index, 'httpBody')" /> @@ -955,6 +955,8 @@ const confirm = async () => { ElMessage.error('JSON中存在错误,请修正后再保存!'); return; } + } else { + } const nodeParams = [] nodeParams.push({ name: "config", type: "input", input: "", children: httpNodeData.value.config, disabled: true, required: true }) @@ -1065,6 +1067,9 @@ const addFormItem = (e, type) => { } else if (type === 'nodeParams' || type === 'outputParams') { obj.input = '' } + if (!formData[type]) { + formData[type] = []; + } formData[type].push(obj); }; @@ -1086,10 +1091,15 @@ const handleTypeChange = (index) => { }; const cascaderRefs = ref([]); -const cascaderChange = (value, index) => { +const cascaderChange = (value, index, type = 'default') => { const selectedOptions = cascaderRefs.value[index].getCheckedNodes(true); - formData.nodeParams[index].quote = value; - formData.nodeParams[index].quoteType = selectedOptions[0].data.type; + if (type === 'default') { + formData.nodeParams[index].quote = value; + formData.nodeParams[index].quoteType = selectedOptions[0].data.type; + } else { + formData.nodeParams.find(item => item.name === 'body').children.find(child => child.name === 'formData').children[index].quote = value; + formData.nodeParams.find(item => item.name === 'body').children.find(child => child.name === 'formData').children[index].quoteType = selectedOptions[0].data.type; + } }; const visibleChange = (value, index, quote) => { @@ -1240,8 +1250,14 @@ const httpNodeData = ref({ const addHttpNodeFormItem = (e, type) => { e.stopPropagation(); if (type === 'body') { + if (!httpNodeData.value.body.formData) { + httpNodeData.value.body.formData = []; + } httpNodeData.value.body.formData.push({ name: "", type: "input", input: ""}); } else { + if (!httpNodeData.value[type]) { + httpNodeData.value[type] = []; + } httpNodeData.value[type].push({ name: "", type: "input", input: ""}); } }; diff --git a/src/views/flow/config.js b/src/views/flow/config.js index ca410bc..f6d0b46 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -307,9 +307,7 @@ export const collapseList = [ { name: "body", type: "input", input: "", children: [ { name: 'bodyType', type: "input", input: "json", disabled: true }, { name: 'json', type: "input", input: '{}' }, - { name: 'formData', type: "input", input: "", children: [ - { name: "", type: "input", input: ""} - ]} + { name: 'formData', type: "input", input: "", children: []} ], required: true }, ], outputType: 'json', diff --git a/src/views/flow/nodes/common/serviceNode.vue b/src/views/flow/nodes/common/serviceNode.vue index 4038cec..0a29d39 100644 --- a/src/views/flow/nodes/common/serviceNode.vue +++ b/src/views/flow/nodes/common/serviceNode.vue @@ -1,6 +1,6 @@ @@ -203,7 +186,7 @@ import { flowPause, flowResume, flowStop, - flowAction + flowAction, } from "@/api/device/flow"; import { getDetect } from "@/api/test/detect"; import { Location } from "@element-plus/icons-vue"; @@ -233,9 +216,11 @@ const drop = (e) => { }, }); - if (!["selectArea", 'branch', 'stopLoop', 'subStart', 'subEnd'].includes(newNode.type)) { + if ( + !["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(newNode.type) + ) { showParamsDrawer.value = true; - console.log('newNode', newNode) + console.log("newNode", newNode); paramsDrawerData.value = newNode; } }; @@ -444,8 +429,7 @@ const loopFlowView = async (instId, taskId = null) => { } else { if (!flowInfoData.value.isLog) { if (["PAUSED", "STOPPED"].includes(arr[arr.length - 1]?.status)) { - flowState.value = - arr[arr.length - 1]?.status === "PAUSED" ? "pause" : "stop"; + flowState.value = arr[arr.length - 1]?.status === "PAUSED" ? "pause" : "stop"; } else { flowState.value = "testRunError"; flowStore.updateDisableForm(false); @@ -562,10 +546,7 @@ const initFlow = () => { keys: ["backspace", "delete"], // 覆盖删除键 callback: () => { const activeElem = document.activeElement; - if ( - activeElem.tagName === "INPUT" || - activeElem.tagName === "TEXTAREA" - ) { + if (activeElem.tagName === "INPUT" || activeElem.tagName === "TEXTAREA") { return; // 不处理输入框内的删除 } @@ -657,10 +638,12 @@ const initFlow = () => { }); lf.on("node:dbclick", ({ data, e }) => { - if (["selectArea", 'branch', 'stopLoop', 'subStart', 'subEnd'].includes(data.type)) { + if ( + ["selectArea", "branch", "stopLoop", "subStart", "subEnd"].includes(data.type) + ) { return; } - console.log('data', data) + console.log("data", data); showParamsDrawer.value = true; paramsDrawerData.value = data; }); @@ -734,55 +717,65 @@ const fitView = () => { }; const nodeName = ref(""); -const visible = ref(false) +const visible = ref(false); const formData = reactive({ nodeParams: [], -}) -const rules = reactive({}) -const executeForm = ref() -const nodeAction = ref('') +}); +const rules = reactive({}); +const executeForm = ref(); +const nodeAction = ref(""); const singleNodeExecution = (e) => { const { name, nodeType, nodeParams, action } = e.detail; nodeName.value = name; visible.value = true; - nodeAction.value = action - if (nodeType === 'EDGE') { + nodeAction.value = action; + if (nodeType === "EDGE") { formData.nodeParams = [ { disabled: true, input: "", name: "terminalId", - type: "input" + type: "input", }, - ...nodeParams - ] + ...nodeParams, + ]; } else { - formData.nodeParams = nodeParams + formData.nodeParams = nodeParams; } }; const execute = async () => { const result = await executeForm.value.validate(); if (result) { - const obj = {} - formData.nodeParams.forEach(item => { - obj[item.name] = item.input - }) + const obj = {}; + formData.nodeParams.forEach((item) => { + obj[item.name] = item.input; + }); const res = await flowAction({ action: nodeAction.value, - payload: obj - }) + payload: obj, + }); if (res.code === 200) { - ElMessage.success('执行成功') + ElMessage.success("执行成功"); } else { - ElMessage.error(res.msg) + ElMessage.error(res.msg); } } -} +}; const showParamsDrawer = ref(false); -const paramsDrawerData = ref({}) +const paramsDrawerData = ref({}); + +const clearRun = () => { + const { nodes } = lf.getGraphData(); + nodes.forEach((item) => { + const node = lf.getNodeModelById(item.id); + if (node.closePopover) { + node.closePopover(); + } + }); +} onMounted(() => { justLoadFlow(); @@ -876,8 +869,6 @@ onUnmounted(() => { } } } - - diff --git a/src/views/flow/nodes/common/serviceNode.js b/src/views/flow/nodes/common/serviceNode.js index b351ef7..529dbb8 100644 --- a/src/views/flow/nodes/common/serviceNode.js +++ b/src/views/flow/nodes/common/serviceNode.js @@ -144,16 +144,10 @@ class ServiceNodeHtmlModel extends HtmlNodeModel { this.componentInstance = instance; } - // 验证表单 - async validateForm() { - const result = this.componentInstance.validateForm() - return result + closePopover() { + this.componentInstance.closePopover() } - // setCustomProperties() { - // this.componentInstance.setNodeProperties() - // } - /** * 设置节点属性 * 包括宽度、高度、文本编辑属性等 diff --git a/src/views/flow/nodes/common/serviceNode.vue b/src/views/flow/nodes/common/serviceNode.vue index 0a29d39..db8b26f 100644 --- a/src/views/flow/nodes/common/serviceNode.vue +++ b/src/views/flow/nodes/common/serviceNode.vue @@ -1,6 +1,6 @@