From 5440b127ee40393aa8d352ab9fee43707f612305 Mon Sep 17 00:00:00 2001 From: zhanghao <774378400@qq.com> Date: Thu, 23 Jul 2026 16:05:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BA=BA=E5=B7=A5=E5=B7=A1=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/flow.js | 55 +++ src/views/flow/components/ParamsDrawer.vue | 2 + .../components/params/RecognizeNodeParams.vue | 327 ++++++++++++++++++ src/views/flow/config.js | 36 ++ src/views/flow/nodes/function/index.js | 2 + src/views/flow/nodes/function/recognize.js | 213 ++++++++++++ src/views/flow/nodes/function/recognize.vue | 124 +++++++ 7 files changed, 759 insertions(+) create mode 100644 src/views/flow/components/params/RecognizeNodeParams.vue create mode 100644 src/views/flow/nodes/function/recognize.js create mode 100644 src/views/flow/nodes/function/recognize.vue diff --git a/src/utils/flow.js b/src/utils/flow.js index 677076b..4fee791 100644 --- a/src/utils/flow.js +++ b/src/utils/flow.js @@ -366,5 +366,60 @@ export const transformSdAgentNodeData = (source) => { } }); + return result; +} + +export const transformRecognizeNodeData = (source) => { + // 初始化结果对象 + const result = { + nodeParams: [] + }; + + // 遍历每一个顶级节点 + source.forEach(item => { + const { name, children } = item; + if (!name) return; // 过滤无name的无效节点 + + // 处理普通节点(config/headers/params):直接取 children 数组 + if (name !== 'alarmRules') { + result['nodeParams'].push(item); + return; + } + + if (name === 'alarmRules') { + result[name] = children || []; + return; + } + + // 专门处理 tts 节点(特殊结构) + // if (name === 'alarmRules') { + // const bodyObj = { + // level: '', + // operator: '', + // threshold: '', + // message: '' + // }; + + // // 遍历body的子项,赋值到对应字段 + // children?.forEach(child => { + // const childName = child.name; + // if (childName === 'level') { + // bodyObj.level = child.input; + // } + // if (childName === 'operator') { + // bodyObj.operator = child.input; + // } + // if (childName === 'threshold') { + // bodyObj.threshold = child.input; + // } + // if (childName === 'message') { + // bodyObj.message = child.input; + // } + // }); + + // result.alarmRules = bodyObj; + // } + }); + return result; } \ No newline at end of file diff --git a/src/views/flow/components/ParamsDrawer.vue b/src/views/flow/components/ParamsDrawer.vue index 3e325b9..b071c70 100644 --- a/src/views/flow/components/ParamsDrawer.vue +++ b/src/views/flow/components/ParamsDrawer.vue @@ -26,6 +26,7 @@ import BasicNodeParams from './params/BasicNodeParams.vue' import CodeNodeParams from './params/CodeNodeParams.vue' import HttpNodeParams from './params/HttpNodeParams.vue' import SdAgentNodeParams from './params/SdAgentNodeParams.vue' +import RecognizeNodeParams from './params/RecognizeNodeParams.vue' const props = defineProps({ drawer: Boolean, @@ -43,6 +44,7 @@ const currentComponent = computed(() => { if (type === 'code') return CodeNodeParams if (type === 'http') return HttpNodeParams if (type === 'sdAgent') return SdAgentNodeParams + if (type === 'recognize') return RecognizeNodeParams return null }) diff --git a/src/views/flow/components/params/RecognizeNodeParams.vue b/src/views/flow/components/params/RecognizeNodeParams.vue new file mode 100644 index 0000000..5c9f766 --- /dev/null +++ b/src/views/flow/components/params/RecognizeNodeParams.vue @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + + + + + + + + + + visibleChange(visible, index, property.quote)" + @change="(value) => cascaderChange(value, index, RecognizeNodeData, 'nodeParams')" /> + + + + + + + + + + + + + + + + + + + + + + + + + + visibleChange(visible, index, property.quote) + " @change="(value) => cascaderChange(value, index, RecognizeNodeData, 'alarmRules')" /> + + + + + + + + + 输出参数 + + + + + + addRecognizeItem(e)" class="addFormItem"> + + + + + + + + deleteRecognizeItem(path, 'outputParams')" /> + + + + + + + + + + \ No newline at end of file diff --git a/src/views/flow/config.js b/src/views/flow/config.js index db81c74..5ed2307 100644 --- a/src/views/flow/config.js +++ b/src/views/flow/config.js @@ -436,6 +436,42 @@ function handler(params) { outputParams: [], outputType: 'json' }, + { + icon: dialogueSvg, + name: "巡检仪表读数识别", + type: "recognize", + desc: "巡检仪表读数识别", + action: 'INSPECTION_METER_RECOGNIZE', + nodeType: "LLM", + outputType: 'json', + nodeParams: [ + { name: 'imageUrl', type: "input", input: "", disabled: true }, + { name: 'resultName', type: "input", input: "", disabled: true }, + { name: "alarmRules", type: "input", input: "", children: [ + { name: 'level', type: "input", componentType: 'number', max: 3, step: 1, input: 1, disabled: true }, + { name: 'operator', type: "input", input: "", disabled: true }, + { name: 'threshold', type: "input", componentType: 'number', max: 3, step: 1, input: 1, disabled: true }, + { name: 'message', type: "input", input: "", disabled: true }, + ]} + ], + outputParams: [] + }, + { + icon: cameraSvg, + name: "人工巡检判断", + type: "serviceNode", + desc: "创建人工巡检判断任务", + action: 'INSPECTION_MANUAL_REVIEW_CREATE', + nodeType: 'EDGE', + nodeParams: [ + { name: "resultName", type: "input", input: "", disabled: true }, + { name: "videoUrl", type: "input", input: "", disabled: true }, + { name: "reviewCriteria", type: "input", input: "", disabled: true }, + { name: "defaultAlarmLevel", type: "input", componentType: 'number', max: 3, step: 1, input: 1, disabled: true }, + ], + outputParams: [], + outputType: 'json' + }, ], }, { diff --git a/src/views/flow/nodes/function/index.js b/src/views/flow/nodes/function/index.js index 3de78bd..1e889bb 100644 --- a/src/views/flow/nodes/function/index.js +++ b/src/views/flow/nodes/function/index.js @@ -8,6 +8,7 @@ import { registerHttpNode } from './httpNode' import { registerCodeNode } from './codeNode' import { registerCurrentLoopNode } from './currentLoopNode' import { registerSdAgentNode } from './sdAgent' +import { registerRecognizeNode } from './recognize' export const registerFunction = (lf) => { registerLoopBodyNode(lf) @@ -20,4 +21,5 @@ export const registerFunction = (lf) => { registerCodeNode(lf) registerCurrentLoopNode(lf) registerSdAgentNode(lf) + registerRecognizeNode(lf) } \ No newline at end of file diff --git a/src/views/flow/nodes/function/recognize.js b/src/views/flow/nodes/function/recognize.js new file mode 100644 index 0000000..1e798d0 --- /dev/null +++ b/src/views/flow/nodes/function/recognize.js @@ -0,0 +1,213 @@ +// 导入 HtmlNode 及其模型,为后续继承做准备 +import { HtmlNode, HtmlNodeModel } from "@logicflow/core"; +// 导入 Vue 相关方法,用于渲染组件 +import { createApp, h, nextTick } from "vue"; +import ElementPlus from "element-plus"; +// 导入 Vue 组件 +// @ts-ignore +import Recognize from "./recognize.vue"; +import OuterNode from "../../components/OuterNode.vue"; +import JsonViewer from 'vue3-json-viewer' + +/** + * 定义一个 元素的 HTML 节点类,继承自 HtmlNode + * 该类负责在 HTML 中渲染 元素,并处理其交互逻辑 + */ +class RecognizeNodeHtmlNode 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: Recognize, + 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 (RecognizeNodeHtmlNode.reusePool.has(nodeId)) { + rootEl.appendChild(RecognizeNodeHtmlNode.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(); + RecognizeNodeHtmlNode.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 RecognizeNodeHtmlModel extends HtmlNodeModel { + initNodeData(data) { + super.initNodeData(data); + } + + // 保存组件实例引用 + setComponentInstance(instance) { + this.componentInstance = instance; + } + + closePopover() { + this.componentInstance.closePopover() + } + + /** + * 设置节点属性 + * 包括宽度、高度、文本编辑属性等 + */ + 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 registerRecognizeNode(lf) { + lf.register({ + type: "recognize", + view: RecognizeNodeHtmlNode, + model: RecognizeNodeHtmlModel + }); +} diff --git a/src/views/flow/nodes/function/recognize.vue b/src/views/flow/nodes/function/recognize.vue new file mode 100644 index 0000000..420399e --- /dev/null +++ b/src/views/flow/nodes/function/recognize.vue @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + +