From 265bb79ba33f2b99e36c3f22d652dbace6db129b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Thu, 5 Mar 2026 21:40:05 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=B9=B6=E9=87=8D?= =?UTF-8?q?=E6=9E=84=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=B9=95=E5=B8=83UI?= =?UTF-8?q?=E8=A1=A8=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/ai/workflow/WorkflowDesign.vue | 36 +- .../vite-config/src/plugins/vxe-table.ts | 13 +- .../src/components/TinyflowCore.svelte | 471 ++++++++++++++++-- .../tinyflow-ui/src/components/Toolbar.svelte | 211 ++------ .../components/core/FlowConnectionLine.svelte | 34 ++ .../src/components/core/FlowEdge.svelte | 51 ++ .../src/components/core/FlowLinePath.svelte | 30 ++ .../src/components/core/FlowMarkerDefs.svelte | 41 ++ .../src/components/core/NodePicker.svelte | 345 +++++++++++++ .../src/components/utils/nodePalette.ts | 136 +++++ .../tinyflow-ui/src/styles/tinyflow.less | 288 +++++++++-- .../packages/tinyflow-ui/src/types.ts | 1 + 12 files changed, 1384 insertions(+), 273 deletions(-) create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowConnectionLine.svelte create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowEdge.svelte create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowLinePath.svelte create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowMarkerDefs.svelte create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodePicker.svelte create mode 100644 easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/nodePalette.ts diff --git a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue index da280d0..2ec9a53 100644 --- a/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue +++ b/easyflow-ui-admin/app/src/views/ai/workflow/WorkflowDesign.vue @@ -6,7 +6,7 @@ import {usePreferences} from '@easyflow/preferences'; import {getOptions, sortNodes} from '@easyflow/utils'; import {getIconByValue} from '#/views/ai/model/modelUtils/defaultIcon'; -import {ArrowLeft, CircleCheck, Close, Position} from '@element-plus/icons-vue'; +import {ArrowLeft, CircleCheck, Close} from '@element-plus/icons-vue'; import {Tinyflow} from '@tinyflow-ai/vue'; import {ElButton, ElDrawer, ElMessage, ElSkeleton} from 'element-plus'; @@ -512,7 +512,7 @@ function onAsyncExecute(info: any) { -
+
{{ $t('aiWorkflow.check') }} - - {{ $t('button.runTest') }} -
@@ -598,20 +592,30 @@ function onAsyncExecute(info: any) { diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/Toolbar.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/Toolbar.svelte index b16a80e..59123f7 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/components/Toolbar.svelte +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/Toolbar.svelte @@ -1,183 +1,58 @@ -
-
-
- { - showType = item.value.toString(); - }} - /> -
- -
-
- {#each baseNodes as node} - - {/each} -
-
- {#each customNodes as node} - - {/each} -
-
+
+ {#if panelVisible} +
+
+ {/if} - +
diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowConnectionLine.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowConnectionLine.svelte new file mode 100644 index 0000000..525199e --- /dev/null +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowConnectionLine.svelte @@ -0,0 +1,34 @@ + + +{#if connection.current.inProgress && path} + + +{/if} diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowEdge.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowEdge.svelte new file mode 100644 index 0000000..2d54397 --- /dev/null +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowEdge.svelte @@ -0,0 +1,51 @@ + + + + +{#if interactionWidth > 0} + +{/if} + +{#if label} + + {label} + +{/if} diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowLinePath.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowLinePath.svelte new file mode 100644 index 0000000..acaa4cb --- /dev/null +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowLinePath.svelte @@ -0,0 +1,30 @@ + + + diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowMarkerDefs.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowMarkerDefs.svelte new file mode 100644 index 0000000..84e2729 --- /dev/null +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/FlowMarkerDefs.svelte @@ -0,0 +1,41 @@ + + + + + + + + + diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodePicker.svelte b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodePicker.svelte new file mode 100644 index 0000000..ddde2d1 --- /dev/null +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/core/NodePicker.svelte @@ -0,0 +1,345 @@ + + +
+ + +
+ {#if groupedNodes.length === 0} +
暂无可用节点
+ {:else} + {#each groupedNodes as [category, categoryNodes]} +
+
{category}
+
+ {#each categoryNodes as node} + + {/each} +
+
+ {/each} + {/if} +
+
+ + diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/nodePalette.ts b/easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/nodePalette.ts new file mode 100644 index 0000000..dccd1a8 --- /dev/null +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/components/utils/nodePalette.ts @@ -0,0 +1,136 @@ +import type {Node} from '@xyflow/svelte'; + +import type {TinyflowOptions} from '#types'; + +export type NodePaletteItem = { + icon?: string; + title: string; + type: string; + sortNo?: number; + description?: string; + category: string; + extra?: Partial; +}; + +const BUILT_IN_NODES: NodePaletteItem[] = [ + { + icon: '', + title: '开始节点', + type: 'startNode', + sortNo: 100, + description: '开始定义输入参数', + category: '输入输出' + }, + { + icon: '', + title: '循环', + type: 'loopNode', + sortNo: 200, + description: '用于循环执行任务', + category: '逻辑' + }, + { + icon: '', + title: '条件判断', + type: 'conditionNode', + sortNo: 250, + description: '根据参数值分流到不同分支', + category: '逻辑' + }, + { + icon: '', + title: '大模型', + type: 'llmNode', + sortNo: 300, + description: '使用大模型处理问题', + category: '模型与流程' + }, + { + icon: '', + title: '知识库', + type: 'knowledgeNode', + sortNo: 400, + description: '通过知识库获取内容', + category: '数据与集成' + }, + { + icon: '', + title: '搜索引擎', + type: 'searchEngineNode', + sortNo: 500, + description: '通过搜索引擎搜索内容', + category: '模型与流程' + }, + { + icon: '', + title: 'Http 请求', + type: 'httpNode', + sortNo: 600, + description: '通过 HTTP 请求获取数据', + category: '数据与集成' + }, + { + icon: '', + title: '动态代码', + type: 'codeNode', + sortNo: 700, + description: '动态执行代码', + category: '逻辑' + }, + { + icon: '', + title: '内容模板', + type: 'templateNode', + sortNo: 800, + description: '通过模板引擎生成内容', + category: '逻辑' + }, + { + icon: '', + title: '用户确认', + type: 'confirmNode', + sortNo: 900, + description: '确认继续或选择内容', + category: '输入输出' + }, + { + icon: '', + title: '结束节点', + type: 'endNode', + sortNo: 1000, + description: '结束定义输出参数', + category: '输入输出' + } +]; + +export function getAvailableNodes(options?: TinyflowOptions) { + const nodes: NodePaletteItem[] = [...BUILT_IN_NODES]; + const customNodes = options?.customNodes; + + if (customNodes) { + const keys = Object.keys(customNodes).sort((a, b) => { + return (customNodes[a].sortNo || 0) - (customNodes[b].sortNo || 0); + }); + + for (let key of keys) { + const item = customNodes[key]; + nodes.push({ + icon: item.icon, + title: item.title, + type: key, + sortNo: item.sortNo, + description: item.description, + category: item.group === 'tools' ? '扩展工具' : '基础节点' + }); + } + } + + const hiddenNodes = typeof options?.hiddenNodes === 'function' + ? options?.hiddenNodes() + : options?.hiddenNodes; + + const hiddenSet = new Set(Array.isArray(hiddenNodes) ? hiddenNodes : []); + const filtered = nodes.filter((node) => !hiddenSet.has(node.type)); + filtered.sort((a, b) => (a.sortNo || 0) - (b.sortNo || 0)); + return filtered; +} diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/styles/tinyflow.less b/easyflow-ui-admin/packages/tinyflow-ui/src/styles/tinyflow.less index 4369b9e..106ee13 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/styles/tinyflow.less +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/styles/tinyflow.less @@ -1,4 +1,3 @@ - .svelte-flow__nodes { .svelte-flow__node { @@ -84,6 +83,25 @@ } } +.tf-flow-line-path { + stroke: var(--xy-edge-stroke); + stroke-width: var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default)); +} + +.tf-flow-line-path--animated { + stroke-dasharray: 8 6; + animation: tf-edge-flow 1.2s linear infinite; +} + +@keyframes tf-edge-flow { + from { + stroke-dashoffset: 0; + } + to { + stroke-dashoffset: -14; + } +} + .tf-node-wrapper { border-radius: 5px; @@ -99,62 +117,240 @@ display: none; } -.tf-toolbar { +.tf-bottom-dock { z-index: 200; position: absolute; - top: 10px; - left: 10px; + left: 50%; + bottom: var(--tf-toolbar-bottom, 20px); + transform: translateX(-50%); display: flex; - gap: 5px; - transition: transform 0.5s ease, opacity 0.5s ease; - transform: translateX(calc(-100% + 20px)); /* 完全移出视口 */ + align-items: center; + gap: 8px; +} - &.show { - transform: translateX(0); +// 单行统一工具栏 +.tf-unified-bar { + display: inline-flex; + align-items: center; + height: 40px; + border: 1px solid var(--tf-border-color); + border-radius: 10px; + background: var(--tf-bg-surface); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + padding: 0 4px; + gap: 2px; + + > * { + flex-shrink: 0; + } +} + +// 分割线 +.tf-bar-divider { + width: 1px; + height: 20px; + background: var(--tf-border-color); + margin: 0 4px; + flex-shrink: 0; +} + +// 图标按钮 +.tf-bar-btn { + all: unset; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 7px; + color: var(--tf-text-secondary); + cursor: pointer; + transition: background 0.15s, color 0.15s; + flex-shrink: 0; + + svg { + width: 17px; + height: 17px; + fill: currentColor; + pointer-events: none; } - &-container { - background: var(--tf-bg-surface); + &:hover { + background: var(--tf-bg-hover); + color: var(--tf-text-primary); + } + + &.tf-bar-btn-active { + color: var(--tf-primary-color); + background: var(--tf-bg-hover); + } +} + +.tf-bar-run-btn { + all: unset; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 32px; + padding: 0 14px; + border-radius: 8px; + background: #13b33f; + color: #fff; + font-size: 14px; + font-weight: 600; + line-height: 1; + cursor: pointer; + flex-shrink: 0; + transition: filter 0.15s; + + svg { + width: 14px; + height: 14px; + fill: currentColor; + pointer-events: none; + } + + &:hover { + filter: brightness(0.95); + } +} + +.tf-bar-add-btn { + all: unset; + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + height: 32px; + padding: 0 12px; + border-radius: 8px; + background: #e9edff; + color: var(--tf-primary-color); + font-size: 13px; + font-weight: 600; + line-height: 1; + cursor: pointer; + transition: filter 0.15s, background 0.15s; + + &:hover { + filter: brightness(0.98); + background: #dde5ff; + } +} + +// 百分比缩放选择器单元 +.tf-zoom-select-wrap { + position: relative; + display: inline-flex; + align-items: center; + padding: 0 6px 0 8px; + gap: 4px; + height: 32px; + border-radius: 7px; + cursor: pointer; + transition: background 0.15s; + + &:hover { + background: var(--tf-bg-hover); + } + + .tf-zoom-icon { + width: 14px; + height: 14px; + fill: var(--tf-text-secondary); + pointer-events: none; + flex-shrink: 0; + } + + .tf-zoom-select { + appearance: none; + -webkit-appearance: none; + border: none; + background: transparent; + font-size: 13px; + font-weight: 500; + color: var(--tf-text-primary); + cursor: pointer; + outline: none; + padding: 0; + min-width: 48px; + text-align: center; + line-height: 1; + } + + .tf-zoom-chevron { + width: 14px; + height: 14px; + fill: var(--tf-text-secondary); + pointer-events: none; + flex-shrink: 0; + } +} + +.tf-zoom-select-simple { + appearance: none; + -webkit-appearance: none; + border: none; + outline: none; + background: transparent; + height: 32px; + min-width: 64px; + padding: 0 8px; + border-radius: 7px; + font-size: 13px; + font-weight: 500; + line-height: 1; + color: var(--tf-text-primary); + cursor: pointer; + text-align: center; + + &:hover { + background: var(--tf-bg-hover); + } +} + + +.tf-toolbar { + position: relative; + display: inline-flex; + align-items: center; + + &-panel { + width: min(520px, calc(100vw - 40px)); + max-height: min(390px, calc(100vh - 220px)); border: 1px solid var(--tf-border-color); - border-radius: 5px; + border-radius: 12px; + background: var(--tf-bg-surface); box-shadow: var(--tf-shadow-soft); - padding: 10px; - width: fit-content; + padding: 8px; + display: flex; + flex-direction: column; + gap: 10px; + position: absolute; + left: 50%; + bottom: calc(100% + 8px); + transform: translateX(-50%); + z-index: 260; + overflow: hidden; + } - &-header { - display: flex; - } + &-trigger { + border: none; + } - &-body { - display: flex; - margin-top: 20px; + &-trigger-icon { + display: inline-flex; + align-items: center; + justify-content: center; - .tf-toolbar-container-base, .tf-toolbar-container-tools { - display: flex; - flex-direction: column; - gap: 4px; - flex-grow: 1; - - .tf-btn { - border: none; - width: 100%; - justify-content: flex-start; - height: 40px; - gap: 10px; - cursor: grabbing; - border-radius: 5px; - - svg { - width: 20px; - height: 20px; - fill: var(--tf-primary-color); - } - - &:hover { - background: var(--tf-bg-hover); - } - } - } + svg { + width: 16px; + height: 16px; + fill: currentColor; } } } diff --git a/easyflow-ui-admin/packages/tinyflow-ui/src/types.ts b/easyflow-ui-admin/packages/tinyflow-ui/src/types.ts index 06fd475..b1c7547 100644 --- a/easyflow-ui-admin/packages/tinyflow-ui/src/types.ts +++ b/easyflow-ui-admin/packages/tinyflow-ui/src/types.ts @@ -94,6 +94,7 @@ export type TinyflowOptions = { //type : node customNodes?: Record; onNodeExecute?: (node: Node) => void; + onRunTest?: () => void | Promise; hiddenNodes?: string[] | (() => string[]); onDataChange?: (data: TinyflowData) => void; };