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] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E5=85=AC=E5=85=B1?= =?UTF-8?q?=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 @@ + + + + + + + 输入 + + + + + + + + + + deleteTopLevelItem(path, 'inputParams')" + /> + + + + + + + + + 输入 + + + + + + + + + + + + + + + + + + + + + + + + + + + + visibleChange(visible, index, property.quote) + " + @change="(value) => cascaderChange(value, index)" + /> + + + + + + + + + + + + + 输出 + + + + + + + + + + + deleteTopLevelItem(path, 'outputParams')" + /> + + + + + + + + 关闭 + 确定 + + + + + + \ 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" /> - e.stopPropagation()" @keydown="handleInputKeydown"> - - - - 输入 - - - - - - - - - - - - - - - - - - - - - - - - - - - - visibleChange(visible, index, property.quote) - " - @change="(value) => cascaderChange(value, index)" - /> - - - - - - - - - - - - - 输出 - - - - - - - - - - - - - - - + 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: {