Skip to content

Commit a00717b

Browse files
feat: add node tip
1 parent a14f427 commit a00717b

7 files changed

Lines changed: 96 additions & 69 deletions

File tree

‎ui/src/workflow-canvas/WORKFLOW_README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ src/workflow-canvas/
2424
├── config/ # 节点数据、映射、常量及预留的本地化配置
2525
├── core/ # 稳定的画布内核与所有节点共用的基础能力
2626
│ ├── edge/ # 普通边、循环边及边删除按钮
27-
│ └── node-container/ # 节点容器及其私有的条件、操作下拉组件
27+
│ └── node-container/ # 节点容器、锚点按钮及私有的条件、操作下拉组件
2828
├── icons/ # 节点图标及图标解析工具
2929
├── node-menu/ # 基础组件、工具和智能体节点菜单及其菜单配置
3030
├── nodes/ # 已迁入节点的注册文件和 Vue 实现
@@ -49,6 +49,11 @@ src/workflow-canvas/
4949
`visible-change` 状态,并在节点组件卸载时调用 `reset()`;不要通过 Teleport 到 `body` 或增加
5050
`z-index` 绕过画布的缩放和 SVG 绘制顺序。
5151

52+
锚点按钮与 `el-tooltip` 集中在 `core/node-container/NodeAnchor.vue`。`workflow-node.ts` 负责
53+
锚点坐标、连接状态,并将 LogicFlow 组件的挂载、Props 更新和卸载同步到现有 Vue Teleport
54+
容器;`teleport.connect()` 的可选第五个参数用于传入组件 Props。节点容器
55+
保留菜单开关与外部点击关闭逻辑,不维护锚点 tooltip 的状态或虚拟触发器。
56+
5257
### `config/`
5358

5459
`config` 是随业务持续维护的配置层,增加、删除或调整节点时通常会更新:

‎ui/src/workflow-canvas/core/edge/DeleteEdgeButton.vue‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const deleteEdge = () => {
1212
</script>
1313
<template>
1414
<div class="workflow-node-delete-icon" @mouseup.stop @click.stop @click="deleteEdge">
15-
<MkIcon name="icon_close_bold_outlined" class="text-white!" />
15+
<MkIcon name="icon_close_bold_outlined" class="text-white!" :size="12"/>
1616
</div>
1717
</template>
1818
<style lang="scss">
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<script setup lang="ts">
2+
import { ref } from 'vue'
3+
import type { Model } from '@logicflow/core'
4+
import type { WorkflowNodeModel } from '../workflow-node'
5+
6+
defineOptions({ name: 'WorkflowNodeAnchor' })
7+
8+
const props = defineProps<{
9+
anchorData: Model.AnchorConfig
10+
canOpenNodeMenu: boolean
11+
connected: boolean
12+
nodeModel: WorkflowNodeModel
13+
}>()
14+
const tooltipSuppressed = ref(false)
15+
16+
function openNodeMenu() {
17+
tooltipSuppressed.value = true
18+
if (props.canOpenNodeMenu) props.nodeModel.openNodeMenu?.(props.anchorData)
19+
}
20+
</script>
21+
22+
<template>
23+
<el-tooltip :disabled="!canOpenNodeMenu || tooltipSuppressed" :enterable="false" placement="top">
24+
<template #content>点击添加节点<br />拖拽连接节点</template>
25+
<div
26+
class="workflow-node-anchor"
27+
:class="{
28+
'is-right': anchorData.type === 'right',
29+
'is-connected': connected,
30+
'is-abnormal': anchorData.id?.endsWith('_exception_right'),
31+
}"
32+
:data-node-menu-node-id="nodeModel.id"
33+
:data-node-menu-anchor-id="anchorData.id"
34+
@click="openNodeMenu"
35+
@pointerdown="tooltipSuppressed = true"
36+
@mouseleave="tooltipSuppressed = false"
37+
>
38+
<MkIcon v-if="!connected || anchorData.type === 'right'" name="icon_add_bold_outlined" class="text-white!" :size="12" />
39+
</div>
40+
</el-tooltip>
41+
</template>

‎ui/src/workflow-canvas/core/node-container/index.vue‎

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,6 @@ const showAnchor = ref(false)
4747
const nodeMenuDragClosing = ref(false)
4848
const nodeMenuRef = ref<InstanceType<typeof NodeMenu>>()
4949
const anchorData = ref<Model.AnchorConfig>()
50-
const anchorTooltipRef = ref<HTMLElement>()
51-
const anchorTooltipVisible = ref(false)
52-
// 通过虚拟触发器将 LogicFlow 锚点交给 Element Plus 定位提示。
53-
const setAnchorTooltip = (anchorElement?: HTMLElement) => {
54-
if (anchorElement) anchorTooltipRef.value = anchorElement
55-
anchorTooltipVisible.value = Boolean(anchorElement)
56-
}
5750
const dropdownMenuStyle = computed(() => {
5851
return { top: anchorData.value ? anchorData.value.y - model.y + model.height / 2 + 'px' : '0px' }
5952
})
@@ -247,7 +240,6 @@ const highlightedStepName = (contentText: string) => {
247240
}
248241
}
249242
onMounted(() => {
250-
model.setAnchorTooltip = setAnchorTooltip
251243
model.openNodeMenu = (anchor: Model.AnchorConfig) => {
252244
showAnchor.value && anchorData.value?.id === anchor.id ? closeNodeMenu() : openNodeMenu(anchor)
253245
}
@@ -273,7 +265,6 @@ const stepContainerRef = ref<HTMLElement>()
273265
onBeforeUnmount(() => {
274266
closeNodeMenu()
275267
model.openNodeMenu = undefined
276-
model.setAnchorTooltip = undefined
277268
disposeSelectionReaction()
278269
resizeObserver?.disconnect()
279270
resizeObserver = null
@@ -358,18 +349,6 @@ onBeforeUnmount(() => {
358349
</div>
359350
</div>
360351

361-
<el-tooltip
362-
virtual-triggering
363-
:virtual-ref="anchorTooltipRef"
364-
:visible="anchorTooltipVisible && !showAnchor"
365-
:teleported="false"
366-
:enterable="false"
367-
effect="dark"
368-
placement="top"
369-
>
370-
<template #content>点击添加节点<br />拖拽连接节点</template>
371-
</el-tooltip>
372-
373352
<el-collapse-transition>
374353
<NodeMenu
375354
v-if="showAnchor"

‎ui/src/workflow-canvas/core/teleport.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@ type ProvideFactory<NodeModel extends TeleportNodeModel> = () => TeleportProvide
99
let active = false
1010
const teleportItems = reactive<Record<string, Component>>({})
1111

12-
export function connect<NodeModel extends TeleportNodeModel>(id: string, component: Component, container: HTMLDivElement, get_provide: ProvideFactory<NodeModel>) {
12+
export function connect<NodeModel extends TeleportNodeModel>(
13+
id: string,
14+
component: Component,
15+
container: HTMLDivElement,
16+
get_provide: ProvideFactory<NodeModel>,
17+
props: object = {},
18+
) {
1319
if (active) {
14-
teleportItems[id] = markRaw(defineComponent({ render: () => h(Teleport, { to: container }, [h(component)]), provide: () => get_provide() }))
20+
teleportItems[id] = markRaw(
21+
defineComponent({ render: () => h(Teleport, { to: container }, [h(component, props)]), provide: () => get_provide() }),
22+
)
1523
}
1624
}
1725

‎ui/src/workflow-canvas/core/workflow-node.ts‎

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
import { createApp, reactive, type Component } from 'vue'
1+
import { createApp, reactive, shallowReactive, type Component } from 'vue'
22
import { cloneDeep } from 'lodash'
3-
import { h as createLogicFlowElement, HtmlNode, HtmlNodeModel, type GraphModel, type IHtmlNodeProperties, type Model } from '@logicflow/core'
3+
import {
4+
Component as LogicFlowComponent,
5+
createRef,
6+
h as createLogicFlowElement,
7+
HtmlNode,
8+
HtmlNodeModel,
9+
type GraphModel,
10+
type IHtmlNodeProperties,
11+
type Model,
12+
} from '@logicflow/core'
413
import { nodeDict } from '@/workflow-canvas/config/node-mapping'
514
import { WorkflowKind, WorkflowNodeType, type WorkflowNodeField } from '@/workflow-canvas/types'
615
import { connect, disconnect } from './teleport'
16+
import NodeAnchor from './node-container/NodeAnchor.vue'
717

818
type NodeViewProps = ConstructorParameters<typeof HtmlNode>[0]
919
type NodeFieldGroup = Record<string, WorkflowNodeField[]>
@@ -24,6 +34,31 @@ interface WorkflowNodeProperties extends IHtmlNodeProperties {
2434
user_input_field_list?: unknown[]
2535
}
2636

37+
/** 将 LogicFlow 锚点的挂载、更新和卸载同步到 Vue 组件。 */
38+
class WorkflowNodeAnchor extends LogicFlowComponent<InstanceType<typeof NodeAnchor>['$props']> {
39+
private readonly containerRef = createRef<HTMLDivElement>()
40+
private readonly anchorProps = shallowReactive({ ...this.props })
41+
private readonly teleportId = `${this.props.nodeModel.graphModel.flowId}:${this.props.nodeModel.id}:anchor:${this.props.anchorData.id}`
42+
43+
componentDidMount() {
44+
if (this.containerRef.current) {
45+
connect(this.teleportId, NodeAnchor, this.containerRef.current, () => ({}), this.anchorProps)
46+
}
47+
}
48+
49+
componentDidUpdate() {
50+
Object.assign(this.anchorProps, this.props)
51+
}
52+
53+
componentWillUnmount() {
54+
disconnect(this.teleportId)
55+
}
56+
57+
render() {
58+
return createLogicFlowElement('div', { ref: this.containerRef })
59+
}
60+
}
61+
2762
export class WorkflowNodeView extends HtmlNode {
2863
private nodeApp?: ReturnType<typeof createApp>
2964
private readonly vueComponent: Component
@@ -69,38 +104,7 @@ export class WorkflowNodeView extends HtmlNode {
69104
return createLogicFlowElement(
70105
'foreignObject',
71106
{ ...anchorData, className: 'workflow-node-anchor-wrapper', x: x - 12, y: y - 12, width: 24, height: 24 },
72-
[
73-
createLogicFlowElement(
74-
'div',
75-
{
76-
'data-node-menu-node-id': nodeModel.id,
77-
'data-node-menu-anchor-id': anchorData.id,
78-
className: `workflow-node-anchor${type === 'right' ? ' is-right' : ''}${connected ? ' is-connected' : ''}${anchorData.id?.endsWith('_exception_right') ? ' is-abnormal' : ''}`,
79-
onClick: () => {
80-
if (canOpenNodeMenu) nodeModel.openNodeMenu?.(anchorData)
81-
},
82-
onMouseEnter: (event: MouseEvent) => {
83-
if (canOpenNodeMenu && event.currentTarget instanceof HTMLElement) nodeModel.setAnchorTooltip?.(event.currentTarget)
84-
},
85-
onMouseLeave: () => nodeModel.setAnchorTooltip?.(),
86-
onPointerDown: () => nodeModel.setAnchorTooltip?.(),
87-
},
88-
connected && type !== 'right'
89-
? []
90-
: [
91-
createLogicFlowElement(
92-
'svg',
93-
{
94-
'aria-hidden': 'true',
95-
className: 'workflow-node-add-icon',
96-
fill: 'currentColor',
97-
focusable: 'false',
98-
},
99-
[createLogicFlowElement('use', { href: '#icon_add_bold_outlined' })],
100-
),
101-
],
102-
),
103-
],
107+
[createLogicFlowElement(WorkflowNodeAnchor, { key: anchorData.id, anchorData, canOpenNodeMenu, connected, nodeModel })],
104108
)
105109
}
106110

@@ -134,7 +138,6 @@ export class WorkflowNodeModel extends HtmlNodeModel<WorkflowNodeProperties> {
134138
private upNodeFieldDict?: NodeFieldGroup
135139

136140
openNodeMenu?: (anchorData: Model.AnchorConfig) => void
137-
setAnchorTooltip?: (anchorElement?: HTMLElement) => void
138141
validate?: () => Promise<unknown>
139142

140143
setAttributes() {

‎ui/src/workflow-canvas/style.scss‎

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
}
2929
}
3030

31-
3231
/* anchor */
3332
// 连接节点按钮
3433
.workflow-node-anchor-wrapper {
@@ -73,12 +72,4 @@
7372
border: 2px solid var(--el-color-warning);
7473
}
7574

76-
.workflow-node-add-icon {
77-
height: calc(var(--spacing) * 6);
78-
transform-origin: center;
79-
width: calc(var(--spacing) * 6);
80-
color: white;
81-
padding: 4px;
82-
transition: opacity 0.2s ease;
83-
}
8475
}

0 commit comments

Comments
 (0)