| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- <script setup lang="ts">
- import { ref, watch, computed } from 'vue'
- import { BaseEdge, EdgeLabelRenderer, type EdgeProps, type Connection } from '@vue-flow/core'
- import { IconButton } from '@repo/ui'
- import type { CanvasExecutionStatus } from '../../../Interface'
- import { getExecutionStatusColor } from '../../../utils/execution-status'
- import { getEdgeRenderData } from './utils/getEdgeRenderData'
- import { useVueFlowContext } from '../../../hooks/useVueFlowContext'
- defineOptions({
- inheritAttrs: false
- })
- const emit = defineEmits<{
- delete: [connection: Connection]
- add: [connection: Connection]
- }>()
- type CanvasEdgeProps = EdgeProps & {
- readOnly?: boolean
- hovered?: boolean
- bringToFront?: boolean
- }
- const props = defineProps<CanvasEdgeProps>()
- const { vueFlow } = useVueFlowContext()
- // 缩放补偿:工具栏大小随缩放调整,保持视觉一致
- const zoom = computed(() => vueFlow.viewport.value.zoom)
- const zoomCompensationFactor = computed(() => 1 / zoom.value)
- const EDGE_STROKE_WIDTH = '1.5px'
- // 执行状态
- const executionStatus = computed<CanvasExecutionStatus>(
- () => (props.data?.executionStatus as CanvasExecutionStatus) || 'idle'
- )
- const isPendingEdge = computed(() => !!props.data?.pending)
- const isRunningEdge = computed(() => executionStatus.value === 'running')
- // 边线路径数据(含反向连接绕行处理)
- const renderData = computed(() =>
- getEdgeRenderData({
- sourceX: props.sourceX,
- sourceY: props.sourceY,
- sourcePosition: props.sourcePosition,
- targetX: props.targetX,
- targetY: props.targetY,
- targetPosition: props.targetPosition
- })
- )
- const segments = computed(() => renderData.value.segments)
- const labelPosition = computed(() => renderData.value.labelPosition)
- // CSS 变量驱动的颜色,便于暗黑模式适配
- const edgeColor = computed(() => {
- if (isPendingEdge.value) return '#409EFF'
- return getExecutionStatusColor(executionStatus.value)
- })
- const edgeStyles = computed(() => {
- const status = executionStatus.value
- const styles: Record<string, string> = {
- '--canvas-edge-color': edgeColor.value,
- '--canvas-edge-stroke-width': EDGE_STROKE_WIDTH
- }
- if (status !== 'idle' && !isPendingEdge.value) {
- styles['--canvas-edge-filter'] = `drop-shadow(0 0 6px ${edgeColor.value}55)`
- }
- return styles
- })
- const edgeClasses = computed(() => ({
- 'canvas-edge-path': true,
- 'is-pending': isPendingEdge.value,
- 'is-running': isRunningEdge.value
- }))
- // 延迟 hover(避免鼠标快速划过时闪烁)
- const delayedHovered = ref(props.hovered)
- const delayedHoveredSetTimeoutRef = ref<ReturnType<typeof setTimeout> | null>(null)
- const delayedHoveredTimeout = 600
- const connection = computed<Connection>(() => ({
- source: props.source,
- target: props.target,
- sourceHandle: props.sourceHandleId,
- targetHandle: props.targetHandleId,
- id: props.id
- }))
- watch(
- () => props.hovered,
- (isHovered) => {
- if (isHovered) {
- if (delayedHoveredSetTimeoutRef.value) clearTimeout(delayedHoveredSetTimeoutRef.value)
- delayedHovered.value = true
- } else {
- delayedHoveredSetTimeoutRef.value = setTimeout(() => {
- delayedHovered.value = false
- }, delayedHoveredTimeout)
- }
- },
- { immediate: true }
- )
- const renderToolbar = computed(() => delayedHovered.value && !props.readOnly)
- const toolbarStyle = computed(() => ({
- pointerEvents: 'all' as const,
- position: 'absolute' as const,
- transform: `translate(-50%, -50%) translate(${labelPosition.value[0]}px, ${labelPosition.value[1]}px) scale(${zoomCompensationFactor.value})`
- }))
- const onAdd = () => emit('add', connection.value)
- const onDelete = () => emit('delete', connection.value)
- </script>
- <template>
- <!-- 多段路径(反向连接时会有两段) -->
- <BaseEdge
- v-for="(segment, index) in segments"
- :key="`${id}-${index}`"
- :path="segment[0]"
- :marker-end="markerEnd"
- :interaction-width="40"
- :style="edgeStyles"
- :class="edgeClasses"
- />
- <EdgeLabelRenderer>
- <div :style="toolbarStyle" class="nodrag nopan">
- <div v-if="isPendingEdge" class="text-sm text-gray-500">pendding...</div>
- <div v-if="renderToolbar && !isPendingEdge" class="flex">
- <IconButton :edge-add-btn="id" icon="lucide:plus" size="small" square @click="onAdd" />
- <IconButton icon="lucide:brush-cleaning" size="small" square @click="onDelete" />
- </div>
- </div>
- </EdgeLabelRenderer>
- </template>
- <style>
- /* ── 基础边线样式 ── */
- .canvas-edge-path {
- stroke: var(--canvas-edge-color, #b1b1b7);
- stroke-width: var(--canvas-edge-stroke-width, 2px);
- stroke-linecap: round;
- filter: var(--canvas-edge-filter, none);
- transition:
- stroke 180ms ease,
- stroke-width 180ms ease,
- filter 180ms ease,
- opacity 180ms ease;
- }
- /* ── Pending: 虚线流动 ── */
- .canvas-edge-path.is-pending {
- opacity: 0.7;
- stroke-dasharray: 8 6;
- animation: edge-loading-dash 3.6s linear infinite;
- }
- /* ── Running: 快速流动虚线 ── */
- .canvas-edge-path.is-running {
- stroke-dasharray: 6 4;
- animation: edge-running-dash 1s linear infinite;
- }
- @keyframes edge-loading-dash {
- from {
- stroke-dashoffset: 0;
- }
- to {
- stroke-dashoffset: -28;
- }
- }
- @keyframes edge-running-dash {
- from {
- stroke-dashoffset: 0;
- }
- to {
- stroke-dashoffset: -20;
- }
- }
- </style>
|