| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526 |
- <template>
- <div
- class="relative h-full w-full"
- ref="workflowWrapperRef"
- @drop="onDrop"
- @contextmenu="handleWorkflowContextMenu"
- >
- <Workflow
- ref="workflowRef"
- :id="workflow?.id"
- :workflow="workflowWithExecutionState"
- :nodeMap="nodeMap"
- :autoFit="false"
- @click:node="handleSelectNode"
- @dblclick:node="handleNodeClick"
- @create:node="handleNodeCreate"
- @create:connection:end="onCreateConnection"
- @drag-and-drop="handleDrop"
- @run:node="handleRunNode"
- @update:nodes:position="handleUpdateNodesPosition"
- @update:node:attrs="handleUpdateNodeProps"
- @delete:node="handleDeleteNode"
- @delete:connection="handleDeleteEdge"
- @dragover="onDragOver"
- @dragleave="onDragLeave"
- @create:connection:cancelled="onConnectionOpenNodeLibary"
- @click:connection:add="handleClickConectionAdd"
- @viewport:change="handleViewportChange"
- class="bg-#f5f5f5"
- >
- <Toolbar
- @create:node="handleNodeCreate"
- @run="handleRunAgent"
- @chat="handleOpenWorkflowChat"
- :env-vars="workflow?.env_variables || []"
- :run-nodes="toolbarRunNodes"
- :can-chat="canRunWorkflowChat"
- @change-env-vars="handleChangeEnvVars"
- />
- </Workflow>
- <StartNodeGuide
- v-if="showStartNodeGuide"
- @create-node="handleStartNodeGuideCreate"
- @close="hideStartNodeGuide"
- />
- </div>
- <RunWorkflow
- v-model:visible="runVisible"
- :workflow="workflow"
- :close-on-run="closeRunWorkflowOnSubmit"
- :input-only="runWorkflowInputOnly"
- :active-node-id="runWorkflowNodeId"
- :initial-tab="runWorkflowInitialTab"
- @run-started="handleWorkflowRunStarted"
- />
- <ChatDrawer
- v-model:visible="workflowChatVisible"
- :workflow="workflow"
- :start-node="workflowChatStartNode"
- :visible-variables="workflowChatVisibleVariables"
- :input-values="workflowChatInputValues"
- :json-drafts="workflowChatJsonDrafts"
- :validation-errors="workflowChatValidationErrors"
- :base-params="workflowChatBaseParams"
- :is-running="workflowChatRunning"
- @validate-send="handleWorkflowChatValidateSend"
- @run-started="handleWorkflowChatRunStarted"
- @cancel="handleWorkflowChatCancel"
- />
- <Setter
- :id="nodeID"
- :workflow="workflow"
- :active-tab="setterActiveTab"
- @update:node:data="handleUpdateNode"
- @run-node="handleRunNode"
- v-model:visible="setterVisible"
- />
- <el-popover
- v-if="libaryRefferenceRef"
- :visible="showNodeLibary"
- trigger="manual"
- placement="bottom-start"
- :show-arrow="false"
- :append-to="workflowWrapperRef"
- :virtual-ref="libaryRefferenceRef"
- :popper-options="nodeLibaryPopperOptions"
- width="360px"
- virtual-triggering
- >
- <div ref="nodeLibraryPanelRef" class="node-library-popover-panel">
- <NodeLibary
- @add-node="handleNodeCreateFromLibrary"
- :parent-node-type="nodeLibaryParentType"
- hide-start
- ignore-drag
- />
- </div>
- </el-popover>
- <Teleport to="body">
- <div
- v-if="contextMenuVisible"
- ref="contextMenuRef"
- class="workflow-context-menu"
- :style="contextMenuStyle"
- @contextmenu.prevent
- >
- <button type="button" class="workflow-context-menu__item" @click="handleContextAddNode">
- <Icon icon="lucide:plus" class="workflow-context-menu__icon">+</Icon>
- <span>{{ t('pages.editor.addNode') }}</span>
- </button>
- <button type="button" class="workflow-context-menu__item" @click="handleContextAddStickyNote">
- <Icon icon="lucide:file-plus-corner" class="workflow-context-menu__icon"></Icon>
- <span>{{ t('pages.editor.addNote') }}</span>
- </button>
- <button
- type="button"
- class="workflow-context-menu__item"
- :class="{ 'is-disabled': !selectedNodeId }"
- @click="handleContextRunNode"
- >
- <Icon icon="lucide:play" class="workflow-context-menu__icon"></Icon>
- <span>{{ t('pages.editor.testRun') }}</span>
- </button>
- </div>
- </Teleport>
- </template>
- <script setup lang="ts">
- import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
- import { ElMessage, ElMessageBox } from 'element-plus'
- import { agent } from '@repo/api-service'
- import { cloneDeep, isEqual } from 'lodash-es'
- import RunWorkflow from '@/features/RunWorkflow/index.vue'
- import ChatDrawer from '@/features/ChatDrawer/index.vue'
- import NodeLibary from '@/features/nodeLibary/index.vue'
- import Toolbar from '@/features/toolbar/index.vue'
- import Setter from '@/features/setter/index.vue'
- import StartNodeGuide from './StartNodeGuide.vue'
- import { nodeMap } from '@/nodes'
- import { getNodeDisplayName } from '@/nodes/i18n'
- import { useI18n } from '@/composables/useI18n'
- import { useRunnerStore } from '@/store/modules/runner.store'
- import { buildExecuteParams, createEmptyValue } from '@/features/RunWorkflow/utils'
- import { Workflow, useDragAndDrop } from '@repo/workflow'
- import { useDebounceFn } from '@vueuse/core'
- import { Icon } from '@repo/ui'
- import type {
- CanvasExecutionStatus,
- Connection,
- ConnectStartEvent,
- IWorkflow,
- IWorkflowNode,
- XYPosition
- } from '@repo/workflow'
- import type { StartVariable } from '@/nodes/src/start'
- interface Props {
- workflow: IWorkflow
- reloadWorkflow: (agentId: string) => Promise<void>
- }
- const props = defineProps<Props>()
- const { t } = useI18n()
- // 将运行器状态映射为画布展示用的状态值。
- const mapNodeExecutionStatus = (status?: string): CanvasExecutionStatus => {
- if (status === 'running') {
- return 'running'
- }
- if (status === 'success') {
- return 'success'
- }
- if (status === 'failed') {
- return 'warning'
- }
- if (status === 'suspended') {
- return 'suspended'
- }
- return 'idle'
- }
- const MIN_NODE_RUNNING_EFFECT_MS = 500
- const runnerStore = useRunnerStore()
- const workflowWrapperRef = ref<HTMLElement>()
- const workflowRef = ref<InstanceType<typeof Workflow>>()
- const showNodeLibary = ref(false)
- const libaryRefferenceRef = ref<HTMLElement>()
- const nodeLibaryPopoverAnchorRef = ref<HTMLElement>()
- const nodeLibraryPanelRef = ref<HTMLElement>()
- const nodeLibaryPopperOptions = {
- modifiers: [
- { name: 'flip', enabled: false },
- { name: 'preventOverflow', enabled: false }
- ]
- }
- const contextMenuRef = ref<HTMLElement>()
- const contextMenuVisible = ref(false)
- const contextMenuPosition = ref({ x: 0, y: 0 })
- const contextMenuFlowPosition = ref<XYPosition>({ x: 0, y: 0 })
- const dismissedStartNodeGuideWorkflowId = ref('')
- const runVisible = ref(false)
- const closeRunWorkflowOnSubmit = ref(false)
- const runWorkflowInputOnly = ref(false)
- const runWorkflowNodeId = ref('')
- const runWorkflowInitialTab = ref<'input' | 'trigger' | 'result' | 'detail' | 'trace'>('input')
- const workflowChatVisible = ref(false)
- const workflowChatInputValues = ref<Record<string, any>>({})
- const workflowChatJsonDrafts = ref<Record<string, string>>({})
- const workflowChatValidationErrors = ref<Record<string, string>>({})
- const workflowChatBaseParams = ref<Record<string, any>>({})
- const setterVisible = ref(false)
- const nodeID = ref('')
- const setterActiveTab = ref<'setting' | 'last-run'>('setting')
- const displayNodeExecutionStatus = ref<Record<string, CanvasExecutionStatus>>({})
- const shouldAutoCenterAfterInit = ref(true)
- const peddingHandlePayload = ref<{
- by?: 'node' | 'edge'
- handle?: ConnectStartEvent
- position: XYPosition
- event?: MouseEvent
- parentId?: string
- connection?: Connection & { id: string }
- }>()
- const runningStatusStartedAt = new Map<string, number>()
- const pendingNodeStatusTimers = new Map<string, number>()
- let hideNodeLibaryTimer: number | undefined
- const pendingEdges = ref<
- Array<Connection & { id: string; type?: string; data?: Record<string, unknown> }>
- >([])
- const pendingNodes = ref<IWorkflowNode[]>([])
- const selectedNodeId = computed(() => {
- const selectedNode = props.workflow?.nodes?.find((node) => node.selected)
- return selectedNode?.id || ''
- })
- const contextMenuStyle = computed(() => ({
- left: `${contextMenuPosition.value.x}px`,
- top: `${contextMenuPosition.value.y}px`
- }))
- const hasWorkflowNodes = computed(() => {
- return !!(props.workflow?.nodes?.length || pendingNodes.value.length)
- })
- const showStartNodeGuide = computed(() => {
- return (
- !!props.workflow?.id &&
- !hasWorkflowNodes.value &&
- dismissedStartNodeGuideWorkflowId.value !== props.workflow.id
- )
- })
- const removeNodeLibaryPopoverAnchor = () => {
- nodeLibaryPopoverAnchorRef.value?.remove()
- nodeLibaryPopoverAnchorRef.value = undefined
- }
- const createNodeLibaryPopoverAnchor = (position?: { x: number; y: number }) => {
- removeNodeLibaryPopoverAnchor()
- if (!position) {
- return undefined
- }
- const anchor = document.createElement('div')
- anchor.style.position = 'fixed'
- anchor.style.left = `${position.x}px`
- anchor.style.top = `${position.y}px`
- anchor.style.width = '1px'
- anchor.style.height = '1px'
- anchor.style.pointerEvents = 'none'
- document.body.appendChild(anchor)
- nodeLibaryPopoverAnchorRef.value = anchor
- return anchor
- }
- const closeContextMenu = () => {
- contextMenuVisible.value = false
- }
- const getFlowPositionFromMouseEvent = (event: MouseEvent): XYPosition => {
- const screenToFlowCoordinate = workflowRef.value?.getVueFlow()?.screenToFlowCoordinate
- if (screenToFlowCoordinate) {
- return screenToFlowCoordinate({
- x: event.clientX,
- y: event.clientY
- })
- }
- const viewport = workflowRef.value?.getVueFlow()?.viewport?.value
- const bounds = workflowWrapperRef.value?.getBoundingClientRect()
- if (viewport && bounds) {
- return {
- x: (event.clientX - bounds.left - viewport.x) / viewport.zoom,
- y: (event.clientY - bounds.top - viewport.y) / viewport.zoom
- }
- }
- return { x: 0, y: 0 }
- }
- // 在为节点安排新的状态切换前,先清理旧的延时任务。
- const clearPendingNodeStatusTimer = (nodeId: string) => {
- const timer = pendingNodeStatusTimers.get(nodeId)
- if (timer) {
- window.clearTimeout(timer)
- pendingNodeStatusTimers.delete(nodeId)
- }
- }
- // 将节点执行状态写入展示缓存,空闲状态则直接清理。
- const applyDisplayedNodeStatus = (nodeId: string, status: CanvasExecutionStatus) => {
- if (status === 'idle') {
- delete displayNodeExecutionStatus.value[nodeId]
- return
- }
- displayNodeExecutionStatus.value[nodeId] = status
- }
- // 保证 running 态至少展示一小段时间,避免状态闪烁。
- const syncDisplayedNodeStatus = (nodeId: string, nextStatus: CanvasExecutionStatus) => {
- const currentStatus = displayNodeExecutionStatus.value[nodeId] || 'idle'
- if (nextStatus === 'running') {
- clearPendingNodeStatusTimer(nodeId)
- runningStatusStartedAt.set(nodeId, Date.now())
- applyDisplayedNodeStatus(nodeId, 'running')
- return
- }
- const runningStartedAt = runningStatusStartedAt.get(nodeId)
- const shouldKeepRunning =
- currentStatus === 'running' &&
- typeof runningStartedAt === 'number' &&
- Date.now() - runningStartedAt < MIN_NODE_RUNNING_EFFECT_MS
- if (shouldKeepRunning) {
- clearPendingNodeStatusTimer(nodeId)
- const delay = MIN_NODE_RUNNING_EFFECT_MS - (Date.now() - runningStartedAt)
- const timer = window.setTimeout(() => {
- applyDisplayedNodeStatus(nodeId, nextStatus)
- runningStatusStartedAt.delete(nodeId)
- pendingNodeStatusTimers.delete(nodeId)
- }, delay)
- pendingNodeStatusTimers.set(nodeId, timer)
- return
- }
- clearPendingNodeStatusTimer(nodeId)
- applyDisplayedNodeStatus(nodeId, nextStatus)
- runningStatusStartedAt.delete(nodeId)
- }
- // 在运行器切换或组件卸载时重置所有执行态缓存。
- const resetDisplayedNodeStatuses = () => {
- pendingNodeStatusTimers.forEach((timer) => window.clearTimeout(timer))
- pendingNodeStatusTimers.clear()
- runningStatusStartedAt.clear()
- displayNodeExecutionStatus.value = {}
- }
- watch(
- () => runnerStore.currentRunnerKey,
- () => {
- resetDisplayedNodeStatuses()
- }
- )
- watch(
- () => runnerStore.nodes.map((item) => ({ nodeId: item.nodeId, status: item.status })),
- (nodeStates) => {
- const activeNodeIds = new Set(nodeStates.map((item) => item.nodeId))
- nodeStates.forEach((item) => {
- syncDisplayedNodeStatus(item.nodeId, mapNodeExecutionStatus(item.status))
- })
- Object.keys(displayNodeExecutionStatus.value).forEach((nodeId) => {
- if (!activeNodeIds.has(nodeId)) {
- syncDisplayedNodeStatus(nodeId, 'idle')
- }
- })
- },
- { immediate: true, deep: true }
- )
- // 将运行时执行状态合并到当前工作流数据,再交给画布渲染。
- const workflowWithExecutionState = computed(() => {
- const baseWorkflow = props.workflow
- const nodes: IWorkflow['nodes'] = (baseWorkflow.nodes || []).map((node): IWorkflowNode => {
- const executionStatus: CanvasExecutionStatus =
- displayNodeExecutionStatus.value[node.id] || 'idle'
- return {
- ...node,
- executionStatus,
- data: {
- ...(node.data || {}),
- executionStatus
- }
- }
- })
- const stableNodeIds = new Set((baseWorkflow.nodes || []).map((node) => node.id))
- const displayPendingNodes = pendingNodes.value.filter((node) => !stableNodeIds.has(node.id))
- const stableEdgeKeys = new Set(
- (baseWorkflow.edges || []).map(
- (edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}`
- )
- )
- const displayPendingEdges = pendingEdges.value.filter(
- (edge) => !stableEdgeKeys.has(`${edge.source}->${edge.target}->${edge.sourceHandle || ''}`)
- )
- return {
- ...baseWorkflow,
- nodes: [...nodes, ...displayPendingNodes],
- edges: [...(baseWorkflow.edges || []), ...displayPendingEdges]
- } as IWorkflow
- })
- const getNodeTypeById = (id?: string) => {
- if (!id) {
- return ''
- }
- const node = props.workflow?.nodes?.find((item) => item.id === id)
- return (node as any)?.nodeType || (node as any)?.data?.nodeType || ''
- }
- const getStartVariables = (node?: IWorkflowNode | null): StartVariable[] => {
- const variables = (node as any)?.data?.variables
- return Array.isArray(variables) ? variables : []
- }
- const workflowChatStartNode = computed<IWorkflowNode | null>(() => {
- return (
- (props.workflow?.nodes || []).find((node) => {
- const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
- return nodeType === 'start'
- }) || null
- )
- })
- const workflowChatStartVariables = computed<StartVariable[]>(() =>
- getStartVariables(workflowChatStartNode.value)
- )
- const workflowChatVisibleVariables = computed(() =>
- workflowChatStartVariables.value.filter((item) => !item.is_hide)
- )
- const canRunWorkflowChat = computed(() => !!workflowChatStartNode.value)
- const workflowChatRunning = computed(
- () => runnerStore.status === 'connecting' || runnerStore.status === 'running'
- )
- const nodeLibaryParentType = computed(() => getNodeTypeById(peddingHandlePayload.value?.parentId))
- const toolbarRunNodes = computed(() => {
- return (props.workflow?.nodes || [])
- .filter((node) => {
- const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
- return ['start', 'trigger-schedule', 'trigger-webhook'].includes(nodeType || '')
- })
- .map((node) => {
- const nodeType = ((node as any)?.nodeType || (node as any)?.data?.nodeType || '') as string
- return {
- id: node.id,
- name: node.name || getNodeDisplayName(nodeType) || nodeType,
- nodeType
- }
- })
- })
- const { onDragOver, onDrop, onDragLeave } = useDragAndDrop({
- id: props.workflow.id,
- addNodes: (node) => {
- handleNodeCreate(node)
- }
- })
- // 统一处理画布内节点相关接口的成功与失败提示。
- const handleApiResult = (response: any, successMessage?: string, errorMessage?: string) => {
- if (response?.isSuccess) {
- if (successMessage) {
- ElMessage.success(successMessage)
- }
- return true
- }
- if (response?.code === 0 && response?.error) {
- ElMessage.error(response.error)
- return false
- }
- if (errorMessage) {
- ElMessage.error(errorMessage)
- }
- return false
- }
- // 在提交节点更新前,整理出后端需要的标准节点结构。
- const buildUpdateNodePayload = (node: any) => {
- return {
- ...node,
- appAgentId: props.workflow.id,
- parentId: node.parentId || node.data?.parentId || '',
- position: node.position || { x: 20, y: 30 },
- width: node.width ?? node.data?.width ?? 96,
- height: node.height ?? node.data?.height ?? 96,
- selected: !!node.selected,
- nodeType: node.data?.nodeType || node.nodeType,
- zIndex: node.zIndex ?? 1
- }
- }
- // 将跨循环边重新绑定到可见的父节点上,保证连线位置正确。
- const normalizeEdgeEndpoints = (
- edge: { source: string; target: string },
- nodes: Array<{ id: string; parentId?: string }>
- ) => {
- const sourceNode = nodes.find((node) => node.id === edge.source)
- const targetNode = nodes.find((node) => node.id === edge.target)
- const sourceParentId = sourceNode?.parentId || ''
- const targetParentId = targetNode?.parentId || ''
- if (sourceParentId && sourceParentId !== targetParentId) {
- return {
- source: sourceParentId,
- target: edge.target
- }
- }
- if (targetParentId && targetParentId !== sourceParentId) {
- return {
- source: edge.source,
- target: targetParentId
- }
- }
- return edge
- }
- // 新建连线时复用同一套端点归一化逻辑。
- const normalizeConnectionEndpoints = (connection: Connection) => {
- return normalizeEdgeEndpoints(connection, props.workflow?.nodes || [])
- }
- const openSetter = (id: string, tab: 'setting' | 'last-run' = 'setting') => {
- nodeID.value = id
- setterActiveTab.value = tab
- setterVisible.value = true
- }
- const centerWorkflowIntoView = async () => {
- const vueFlow = workflowRef.value?.getVueFlow()
- if (!vueFlow || !props.workflow?.nodes?.length) {
- return false
- }
- await nextTick()
- await new Promise<void>((resolve) => {
- window.requestAnimationFrame(() => resolve())
- })
- await vueFlow.fitView({
- padding: 0.2,
- duration: 240,
- maxZoom: 1
- })
- return true
- }
- const openRunWorkflow = async (
- nodeId: string,
- initialTab: 'input' | 'trigger' | 'result' | 'detail' | 'trace',
- options?: {
- closeOnRun?: boolean
- inputOnly?: boolean
- }
- ) => {
- runWorkflowNodeId.value = nodeId
- runWorkflowInitialTab.value = initialTab
- closeRunWorkflowOnSubmit.value = !!options?.closeOnRun
- runWorkflowInputOnly.value = !!options?.inputOnly
- runVisible.value = false
- await nextTick()
- runVisible.value = true
- }
- // 从节点级操作入口直接运行指定节点。
- const handleRunNode = async (id: string) => {
- if (!props.workflow?.id) {
- ElMessage.warning(t('pages.nodeView.messages.selectNodeFirst'))
- return
- }
- const targetNode = props.workflow?.nodes?.find((node) => node.id === id)
- const nodeType = (targetNode as any)?.nodeType || (targetNode as any)?.data?.nodeType
- if (nodeType === 'start') {
- await openRunWorkflow(id, 'input', {
- closeOnRun: true,
- inputOnly: true
- })
- return
- }
- if (['trigger-schedule', 'trigger-webhook'].includes(nodeType || '')) {
- try {
- const response = await agent.postAgentDoExecute({
- appAgentId: props.workflow.id,
- start_node_id: id,
- is_debugger: true,
- responseType: 'ws',
- params: {}
- })
- const agentRunnerKey = response?.result
- if (agentRunnerKey) {
- runnerStore.startRunner(agentRunnerKey, id)
- await openRunWorkflow(id, 'trigger')
- return
- }
- ElMessage.error(t('pages.nodeView.messages.runFailed'))
- } catch (error) {
- console.error('postDoTestNodeRunner error', error)
- ElMessage.error(t('pages.nodeView.messages.runFailed'))
- }
- return
- }
- closeRunWorkflowOnSubmit.value = false
- runWorkflowInputOnly.value = false
- try {
- const response = await agent.postAgentDoExecute({
- appAgentId: props.workflow.id,
- start_node_id: id,
- is_debugger: true,
- responseType: 'ws',
- // 如果是start用户输入则传入数据
- params: {}
- })
- const agentRunnerKey = response?.result
- if (agentRunnerKey) {
- runnerStore.startRunner(agentRunnerKey, id)
- openSetter(id, 'last-run')
- }
- } catch (error) {
- console.error('postDoTestNodeRunner error', error)
- ElMessage.error(t('pages.nodeView.messages.runFailed'))
- }
- }
- /**
- * 运行智能体
- */
- const handleRunAgent = (id?: string) => {
- const targetNode =
- (id && props.workflow?.nodes?.find((node) => node.id === id)) ||
- (props.workflow?.nodes || []).find((node) => {
- const nodeType = (node as any)?.nodeType || (node as any)?.data?.nodeType
- return ['start', 'trigger-schedule', 'trigger-webhook'].includes(nodeType || '')
- })
- if (!props.workflow?.id || !targetNode?.id) {
- ElMessage.warning(t('pages.nodeView.messages.missingTrigger'))
- return
- }
- const nodeType = (targetNode as any)?.nodeType || (targetNode as any)?.data?.nodeType
- if (nodeType === 'start') {
- openRunWorkflow(targetNode.id, 'input')
- return
- }
- handleRunNode(targetNode.id)
- }
- function formatJsonDraft(value: unknown, fallback = '{}') {
- if (value === undefined || value === null || value === '') return fallback
- try {
- return JSON.stringify(value, null, 2)
- } catch {
- return fallback
- }
- }
- function resetWorkflowChatValidation() {
- workflowChatValidationErrors.value = {}
- }
- function initializeWorkflowChatInputValues() {
- const values: Record<string, any> = {}
- const drafts: Record<string, string> = {}
- workflowChatStartVariables.value.forEach((variable) => {
- const initialValue = cloneDeep(
- variable.default_value !== undefined
- ? variable.default_value
- : createEmptyValue(variable.formType)
- )
- if (variable.formType === 'json_object') {
- values[variable.name] =
- initialValue && typeof initialValue === 'object' && !Array.isArray(initialValue)
- ? initialValue
- : {}
- drafts[variable.name] = formatJsonDraft(values[variable.name], '{}')
- return
- }
- values[variable.name] = initialValue
- })
- workflowChatInputValues.value = values
- workflowChatJsonDrafts.value = drafts
- }
- const buildWorkflowChatParams = () => {
- const params = buildExecuteParams({
- startVariables: workflowChatStartVariables.value,
- inputValues: workflowChatInputValues.value,
- jsonDrafts: workflowChatJsonDrafts.value,
- validationErrors: workflowChatValidationErrors.value,
- translateFieldRequired: (name) =>
- t('pages.runWorkflow.fieldRequired', {
- name
- }),
- translateInvalidJson: () => t('pages.runWorkflow.invalidJson'),
- translateFieldTooLong: (name, max) =>
- t('pages.runWorkflow.fieldTooLong', {
- name,
- max
- })
- })
- if (!params) {
- ElMessage.warning(t('pages.runWorkflow.inputPanel.completeRequired'))
- return false
- }
- return params
- }
- const handleOpenWorkflowChat = () => {
- if (!workflowChatStartNode.value) {
- ElMessage.warning(t('pages.nodeView.messages.missingTrigger'))
- return
- }
- initializeWorkflowChatInputValues()
- resetWorkflowChatValidation()
- workflowChatBaseParams.value = {}
- workflowChatVisible.value = true
- }
- const handleWorkflowChatValidateSend = (done: (params: Record<string, any> | false) => void) => {
- done(buildWorkflowChatParams())
- }
- const handleWorkflowChatRunStarted = (id: string) => {
- runWorkflowNodeId.value = id
- }
- const handleWorkflowChatCancel = () => {
- runnerStore.stopRunner()
- }
- const createStickyNoteNode = (position: XYPosition = { x: 600, y: 300 }) => {
- props.workflow?.nodes.push({
- appAgentId: props.workflow.id,
- type: 'canvas-node',
- zIndex: -1,
- nodeType: 'stickyNote',
- position,
- id: `stickyNote_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`,
- name: t('pages.nodeView.stickyNote.name'),
- remark: '',
- data: {
- id: '',
- version: ['1.0.0'],
- inputs: [],
- outputs: [],
- position,
- nodeType: 'stickyNote',
- content: t('pages.nodeView.stickyNote.content'),
- width: 400,
- height: 200,
- color: '#fff5d6'
- }
- })
- }
- const getViewportCenterFlowPosition = (): XYPosition | undefined => {
- const viewport = workflowRef.value?.getVueFlow()?.viewport
- if (!viewport) {
- return undefined
- }
- return {
- x: (-viewport.value.x + window.innerWidth / 2) / viewport.value.zoom,
- y: (-viewport.value.y + window.innerHeight / 2) / viewport.value.zoom
- }
- }
- const hideStartNodeGuide = () => {
- dismissedStartNodeGuideWorkflowId.value = props.workflow?.id || ''
- }
- const handleStartNodeGuideCreate = (type: string) => {
- const position = getViewportCenterFlowPosition()
- handleNodeCreate(position ? { type, position } : { type })
- }
- watch(
- () => props.workflow?.nodes,
- async (nodes) => {
- if (!shouldAutoCenterAfterInit.value || !nodes?.length) {
- return
- }
- const centered = await centerWorkflowIntoView()
- if (centered) {
- shouldAutoCenterAfterInit.value = false
- }
- },
- { flush: 'post' }
- )
- /**
- * 创建新节点
- */
- const handleNodeCreate = (value: { type: string; position?: XYPosition } | string) => {
- if (typeof value === 'string') {
- if (value === 'stickyNote') {
- createStickyNoteNode()
- }
- return
- }
- if (value.type === 'stickyNote') {
- createStickyNoteNode(value.position)
- return
- }
- const nodeToAdd = nodeMap[value.type]?.schema
- const nodeType = nodeToAdd?.nodeType || nodeToAdd?.data?.nodeType || value.type
- const defaultNodeTitle = getNodeDisplayName(nodeType)
- const viewport = workflowRef.value?.getVueFlow()?.viewport
- const parentNodeType = getNodeTypeById(peddingHandlePayload.value?.parentId)
- const isLoopContainer = ['loop', 'iteration'].includes(parentNodeType)
- if (value.type === 'loop-end' && !isLoopContainer) {
- ElMessage.warning(t('pages.nodeView.messages.loopEndOnlyInside'))
- onHideNodeLibary()
- return
- }
- if (isLoopContainer && ['loop', 'iteration'].includes(value.type)) {
- ElMessage.warning(t('pages.nodeView.messages.noNestedLoop'))
- onHideNodeLibary()
- return
- }
- if (nodeToAdd) {
- const hasExplicitPosition = !!value.position
- const newNodeParam: any = {
- ...nodeToAdd,
- appAgentId: props.workflow?.id || '',
- position: value.position,
- name: defaultNodeTitle,
- data: {
- ...(nodeToAdd.data || {}),
- title: nodeToAdd.data?.title || defaultNodeTitle
- }
- }
- if (!hasExplicitPosition && !peddingHandlePayload.value && viewport) {
- newNodeParam.position = {
- x: (-viewport.value.x + window.innerWidth / 2) / viewport.value.zoom,
- y: (-viewport.value.y + window.innerHeight / 2) / viewport.value.zoom
- }
- }
- if (peddingHandlePayload.value) {
- const { position, handle, parentId } = peddingHandlePayload.value
- newNodeParam.position = position
- newNodeParam.prevNodeId = handle?.nodeId
- newNodeParam.parentId = parentId
- // 包含下划线/纯UUID的需要传值
- if (
- handle?.handleId?.includes('_') ||
- (handle?.handleId && !handle.handleId.includes('-source'))
- ) {
- newNodeParam.nodeHandleId = handle?.handleId
- }
- }
- if (!newNodeParam.position) {
- newNodeParam.position = nodeToAdd.position
- }
- if (peddingHandlePayload.value?.by === 'edge' && peddingHandlePayload.value?.connection) {
- const { connection } = peddingHandlePayload.value
- const params = {
- edgeId: connection.id,
- newNode: newNodeParam
- }
- const pendingNodeId = `pending-node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
- pendingNodes.value.push({
- ...newNodeParam,
- id: pendingNodeId,
- name: 'pendding...',
- type: 'canvas-node',
- selected: false,
- data: {
- ...(newNodeParam.data || {}),
- pending: true
- }
- } as IWorkflowNode)
- agent
- .postAgentDoNewAgentNodeWithEdge(params)
- .then(async (response) => {
- if (
- handleApiResult(
- response,
- t('pages.nodeView.messages.nodeAdded'),
- t('pages.nodeView.messages.addNodeFailed')
- )
- ) {
- await props.reloadWorkflow(props.workflow.id)
- }
- })
- .catch((error) => {
- console.error('postAgentDoNewAgentNodeWithEdge error', error)
- ElMessage.error(t('pages.nodeView.messages.addNodeFailed'))
- })
- .finally(() => {
- pendingNodes.value = pendingNodes.value.filter((node) => node.id !== pendingNodeId)
- })
- onHideNodeLibary()
- return
- }
- onHideNodeLibary()
- const pendingNodeId = `pending-node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
- pendingNodes.value.push({
- ...newNodeParam,
- id: pendingNodeId,
- name: 'pendding...',
- type: 'canvas-node',
- selected: false,
- data: {
- ...(newNodeParam.data || {}),
- pending: true
- }
- } as IWorkflowNode)
- agent
- .postAgentDoNewAgentNode(newNodeParam)
- .then(async (response) => {
- if (
- handleApiResult(
- response,
- t('pages.nodeView.messages.nodeAdded'),
- t('pages.nodeView.messages.addNodeFailed')
- )
- ) {
- await props.reloadWorkflow(props.workflow.id)
- }
- })
- .catch((error) => {
- console.error('postDoNewAgentNode error', error)
- ElMessage.error(t('pages.nodeView.messages.addNodeFailed'))
- })
- .finally(() => {
- pendingNodes.value = pendingNodes.value.filter((node) => node.id !== pendingNodeId)
- })
- }
- }
- const handleNodeCreateFromLibrary = (value: { type: string; position?: XYPosition } | string) => {
- if (typeof value === 'string') {
- handleNodeCreate(value)
- return
- }
- handleNodeCreate({
- ...value,
- position: value.position || contextMenuFlowPosition.value
- })
- }
- const openNodeLibraryAtContextMenu = async () => {
- await nextTick()
- if (hideNodeLibaryTimer) {
- window.clearTimeout(hideNodeLibaryTimer)
- hideNodeLibaryTimer = undefined
- }
- libaryRefferenceRef.value = createNodeLibaryPopoverAnchor(contextMenuPosition.value)
- peddingHandlePayload.value = {
- position: contextMenuFlowPosition.value
- }
- showNodeLibary.value = true
- }
- const handleContextAddNode = () => {
- closeContextMenu()
- void openNodeLibraryAtContextMenu()
- }
- const handleContextAddStickyNote = () => {
- closeContextMenu()
- handleNodeCreate({
- type: 'stickyNote',
- position: contextMenuFlowPosition.value
- })
- }
- const handleContextRunNode = () => {
- if (!selectedNodeId.value) {
- ElMessage.warning(t('pages.nodeView.messages.selectNodeFirst'))
- closeContextMenu()
- return
- }
- closeContextMenu()
- void handleRunNode(selectedNodeId.value)
- }
- const handleWorkflowContextMenu = (event: MouseEvent) => {
- const target = event.target as HTMLElement | null
- if (target?.closest('.workflow-context-menu')) {
- return
- }
- const wrapperEl = workflowWrapperRef.value
- if (!wrapperEl?.contains(target)) {
- return
- }
- event.preventDefault()
- onHideNodeLibary()
- contextMenuFlowPosition.value = getFlowPositionFromMouseEvent(event)
- contextMenuPosition.value = {
- x: event.clientX,
- y: event.clientY
- }
- contextMenuVisible.value = true
- }
- // 双击节点时打开对应的配置面板。
- const handleNodeClick = (id: string, _position: XYPosition) => {
- openSetter(id, 'setting')
- }
- const handleWorkflowRunStarted = (id: string) => {
- if (!closeRunWorkflowOnSubmit.value) {
- return
- }
- openSetter(id, 'last-run')
- }
- // 将拖拽落点转换成统一的节点创建流程。
- const handleDrop = (position: XYPosition, event: DragEvent) => {
- const type = event.dataTransfer?.getData('application/x-node-type')
- if (!type) return
- handleNodeCreate({ type, position })
- }
- // 持久化新建连线;若相同起终点已存在,则不重复创建。
- const onCreateConnection = async (connection: Connection) => {
- const { sourceHandle } = connection
- const { source, target } = normalizeConnectionEndpoints(connection)
- const edgeKey = `${source}->${target}->${sourceHandle || ''}`
- const params: {
- appAgentId: string
- source: string
- target: string
- zIndex: number
- sourceHandle?: string
- } = {
- appAgentId: props.workflow.id,
- source,
- target,
- zIndex: 1
- }
- // 包含下划线或者无-source的需要传值
- if (
- (sourceHandle && sourceHandle.includes('_')) ||
- (sourceHandle && !sourceHandle.includes('-source'))
- ) {
- params.sourceHandle = sourceHandle
- }
- const existsInWorkflow = props.workflow?.edges.some(
- (edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}` === edgeKey
- )
- const existsInPending = pendingEdges.value.some(
- (edge) => `${edge.source}->${edge.target}->${edge.sourceHandle || ''}` === edgeKey
- )
- if (existsInWorkflow || existsInPending) {
- return
- }
- const pendingId = `pending-edge-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
- pendingEdges.value.push({
- id: pendingId,
- type: 'canvas-edge',
- source,
- target,
- sourceHandle: params.sourceHandle,
- targetHandle: connection.targetHandle,
- data: {
- pending: true
- }
- })
- try {
- const response = await agent.postAgentDoNewEdge(params)
- if (
- handleApiResult(
- response,
- t('pages.nodeView.messages.edgeCreated'),
- t('pages.nodeView.messages.createEdgeFailed')
- )
- ) {
- await props.reloadWorkflow(props.workflow.id)
- }
- } catch (error) {
- console.error('postAgentDoNewEdge error', error)
- ElMessage.error(t('pages.nodeView.messages.createEdgeFailed'))
- } finally {
- pendingEdges.value = pendingEdges.value.filter((edge) => edge.id !== pendingId)
- }
- }
- // 在拖拽结束后持久化节点坐标变化。
- const handleUpdateNodesPosition = (events: { id: string; position: XYPosition }[]) => {
- events?.forEach(({ id, position }) => {
- const node = props.workflow?.nodes.find((item) => item.id === id)
- if (node) {
- if (node.position?.x === position.x && node.position?.y === position.y) {
- return
- }
- node.position = position
- agent
- .postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
- .then((response) => {
- handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))
- })
- .catch((error) => {
- console.error('postDoUpdateAgentNode error', error)
- })
- }
- })
- }
- // 保持本地工作流中同一时间仅有一个节点处于选中状态。
- const handleSelectNode = (id: string) => {
- props.workflow?.nodes.forEach((node) => {
- node.selected = false
- })
- const node = props.workflow?.nodes.find((item) => item.id === id)
- if (node) {
- node.selected = true
- }
- }
- // 持久化配置面板提交的节点数据变更。
- const handleUpdateNode = (node: IWorkflowNode) => {
- if (node) {
- // 根据节点配置的 inputs/outputs 数量动态调整高度(超过 2 个端口时增加高度)
- const schema = nodeMap[node.nodeType]
- if (schema) {
- const data = node.data || {}
- const inputs =
- typeof schema.inputs === 'function' ? schema.inputs(data as any) : schema.inputs || []
- const outputs =
- typeof schema.outputs === 'function' ? schema.outputs(data as any) : schema.outputs || []
- const maxPorts = Math.max(inputs.length || 0, outputs.length || 0)
- // todo: 特殊情况处理
- if (maxPorts > 2) {
- const extraPorts = maxPorts - 2
- const baseHeight = 96
- const perPortOffset = 32
- node.height = baseHeight + extraPorts * perPortOffset
- }
- }
- const workflowNode = props.workflow?.nodes.find((item) => item.id === node.id)
- const syncedNode = workflowNode || node
- if (workflowNode) {
- const mergedData = {
- ...(workflowNode.data || {}),
- ...(node.data || {})
- }
- Object.assign(workflowNode, node)
- workflowNode.data = mergedData
- }
- agent
- .postAgentDoUpdateAgentNode(buildUpdateNodePayload(syncedNode))
- .then((response: any) => {
- if (!handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))) {
- return
- }
- const responseNode = response?.result
- if (!responseNode || typeof responseNode !== 'object') {
- return
- }
- const latestNode = props.workflow?.nodes.find(
- (item) => item.id === (responseNode.id || node.id)
- )
- if (!latestNode) {
- return
- }
- const mergedData = {
- ...(latestNode.data || {}),
- ...(responseNode.data || {})
- }
- Object.assign(latestNode, responseNode)
- latestNode.data = mergedData
- })
- .catch((error) => {
- console.error('postDoUpdateAgentNode error', error)
- })
- }
- }
- // 持久化画布自身抛出的轻量节点属性更新。
- const handleUpdateNodeProps = (id: string, attrs: Record<string, unknown>) => {
- const node = props.workflow?.nodes.find((item) => item.id === id)
- if (node) {
- const keys = Object.keys(attrs || {})
- const meaningfulKeys = keys.filter((key) => !['selected', 'dragging'].includes(key))
- if (meaningfulKeys.length === 0) {
- return
- }
- if (node.data?.nodeType === 'stickyNote') {
- Object.assign(node.data, attrs)
- } else {
- Object.assign(node, attrs)
- }
- agent
- .postAgentDoUpdateAgentNode(buildUpdateNodePayload(node))
- .then((response) => {
- handleApiResult(response, undefined, t('pages.nodeView.messages.updateNodeFailed'))
- })
- .catch((error) => {
- console.error('postDoUpdateAgentNode error', error)
- })
- }
- }
- // 确认后删除节点,并重新加载工作流图数据。
- const handleDeleteNode = async (id: string) => {
- const index = props.workflow.nodes.findIndex((node) => node.id === id)
- if (index !== -1) {
- ElMessageBox.confirm(t('common.confirmDelete.message'), t('common.confirmDelete.title'), {
- confirmButtonText: t('common.confirm'),
- cancelButtonText: t('common.cancel'),
- type: 'warning'
- }).then(async () => {
- await agent.postAgentDoDeleteAgentNode({
- id
- })
- await props.reloadWorkflow(props.workflow.id)
- })
- }
- }
- // 确认后删除连线,并重新加载工作流图数据。
- const handleDeleteEdge = async (connection: Connection & { id: string }) => {
- if (connection.id) {
- ElMessageBox.confirm(t('common.confirmDelete.message'), t('common.confirmDelete.title'), {
- confirmButtonText: t('common.confirm'),
- cancelButtonText: t('common.cancel'),
- type: 'warning'
- }).then(async () => {
- await agent.postAgentDoDeleteEdge({
- id: connection.id
- })
- await props.reloadWorkflow(props.workflow.id)
- })
- }
- }
- // 保存工具栏里修改的环境变量,并刷新当前工作流。
- const handleChangeEnvVars = async (
- envVars: {
- name: string
- value: string
- type: 'string' | 'number' | 'boolean' | 'object' | 'array'
- }[]
- ) => {
- const response = await agent.postAgentDoSaveAgentVariables({
- appAgentId: props.workflow.id,
- conversation_variables: [],
- env_variables: envVars
- })
- handleApiResult(
- response,
- t('pages.nodeView.messages.envSaved'),
- t('pages.nodeView.messages.saveEnvFailed')
- )
- await props.reloadWorkflow(props.workflow.id)
- }
- // 连线创建被取消时,打开节点库以继续补全流程。
- const onConnectionOpenNodeLibary = async (payload: {
- handle: ConnectStartEvent
- position: XYPosition
- event: MouseEvent
- parentId?: string
- }) => {
- await nextTick()
- libaryRefferenceRef.value = createNodeLibaryPopoverAnchor({
- x: payload.event.clientX,
- y: payload.event.clientY
- })
- showNodeLibary.value = true
- peddingHandlePayload.value = payload
- }
- // 关闭节点库弹层时,清理相关的临时状态。
- const onHideNodeLibary = () => {
- showNodeLibary.value = false
- if (hideNodeLibaryTimer) {
- window.clearTimeout(hideNodeLibaryTimer)
- }
- hideNodeLibaryTimer = window.setTimeout(() => {
- peddingHandlePayload.value = undefined
- libaryRefferenceRef.value = undefined
- removeNodeLibaryPopoverAnchor()
- hideNodeLibaryTimer = undefined
- }, 500)
- }
- const onGlobalPointerDown = (event: PointerEvent) => {
- if (contextMenuVisible.value) {
- const target = event.target as Node | null
- if (!target || !contextMenuRef.value?.contains(target)) {
- closeContextMenu()
- }
- }
- if (!showNodeLibary.value) {
- return
- }
- const target = event.target as Node | null
- if (!target) {
- onHideNodeLibary()
- return
- }
- const panelEl = nodeLibraryPanelRef.value
- if (panelEl?.contains(target)) {
- return
- }
- const anchorEl = libaryRefferenceRef.value
- if (anchorEl?.contains(target)) {
- return
- }
- onHideNodeLibary()
- }
- const onGlobalKeyDown = (event: KeyboardEvent) => {
- if (event.key === 'Escape') {
- closeContextMenu()
- if (showNodeLibary.value) {
- onHideNodeLibary()
- }
- }
- }
- const onGlobalScroll = () => {
- closeContextMenu()
- }
- // 根据边上加号按钮的位置打开节点库弹层。
- const handleClickConectionAdd = (connection: Connection & { id: string }, parentId?: string) => {
- const el = document.querySelector(`[edge-add-btn="${connection.id}"]`) as HTMLElement
- const screenToFlowCoordinate = workflowRef.value?.getVueFlow()?.screenToFlowCoordinate
- if (el && screenToFlowCoordinate) {
- const rect = el.getBoundingClientRect()
- const position = screenToFlowCoordinate({
- x: rect.left,
- y: rect.top
- })
- libaryRefferenceRef.value = createNodeLibaryPopoverAnchor({
- x: rect.left,
- y: rect.top
- })
- showNodeLibary.value = true
- peddingHandlePayload.value = {
- by: 'edge',
- position: parentId ? { x: 50, y: 50 } : position,
- connection,
- parentId
- }
- }
- }
- /**
- * 视图切换
- */
- const handleViewportChange = useDebounceFn((viewport: { x: number; y: number; zoom: number }) => {
- if (!isEqual(viewport, props.workflow?.viewport)) {
- agent.postAgentDoEditAgent({
- data: {
- ...props.workflow,
- viewPort: viewport
- }
- })
- }
- }, 1000)
- onBeforeUnmount(() => {
- if (hideNodeLibaryTimer) {
- window.clearTimeout(hideNodeLibaryTimer)
- }
- removeNodeLibaryPopoverAnchor()
- resetDisplayedNodeStatuses()
- document.removeEventListener('pointerdown', onGlobalPointerDown, true)
- document.removeEventListener('keydown', onGlobalKeyDown)
- window.removeEventListener('scroll', onGlobalScroll, true)
- })
- onMounted(() => {
- document.addEventListener('pointerdown', onGlobalPointerDown, true)
- document.addEventListener('keydown', onGlobalKeyDown)
- window.addEventListener('scroll', onGlobalScroll, true)
- })
- </script>
- <style scoped lang="less">
- .node-library-popover-panel {
- max-height: min(520px, calc(100vh - 48px));
- overflow-y: auto;
- }
- .workflow-context-menu {
- position: fixed;
- z-index: 3000;
- min-width: 148px;
- padding: 6px;
- border: 1px solid var(--border-light);
- border-radius: 8px;
- background: var(--bg-base);
- box-shadow: var(--shadow-md);
- }
- .workflow-context-menu__item {
- display: flex;
- align-items: center;
- gap: 8px;
- width: 100%;
- height: 34px;
- padding: 0 10px;
- border: 0;
- border-radius: 6px;
- background: transparent;
- color: var(--text-primary);
- font-size: 13px;
- line-height: 1;
- text-align: left;
- cursor: pointer;
- }
- .workflow-context-menu__item:hover:not(.is-disabled) {
- background: var(--bg-container);
- color: #296dff;
- }
- .workflow-context-menu__item.is-disabled {
- color: var(--text-tertiary);
- cursor: not-allowed;
- }
- .workflow-context-menu__icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 16px;
- height: 16px;
- font-size: 12px;
- font-weight: 700;
- }
- </style>
|