CanvasEdge.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. <script setup lang="ts">
  2. import { ref, watch, computed } from 'vue'
  3. import { BaseEdge, EdgeLabelRenderer, type EdgeProps, type Connection } from '@vue-flow/core'
  4. import { IconButton } from '@repo/ui'
  5. import type { CanvasExecutionStatus } from '../../../Interface'
  6. import { getExecutionStatusColor } from '../../../utils/execution-status'
  7. import { getEdgeRenderData } from './utils/getEdgeRenderData'
  8. import { useVueFlowContext } from '../../../hooks/useVueFlowContext'
  9. defineOptions({
  10. inheritAttrs: false
  11. })
  12. const emit = defineEmits<{
  13. delete: [connection: Connection]
  14. add: [connection: Connection]
  15. }>()
  16. type CanvasEdgeProps = EdgeProps & {
  17. readOnly?: boolean
  18. hovered?: boolean
  19. bringToFront?: boolean
  20. }
  21. const props = defineProps<CanvasEdgeProps>()
  22. const { vueFlow } = useVueFlowContext()
  23. // 缩放补偿:工具栏大小随缩放调整,保持视觉一致
  24. const zoom = computed(() => vueFlow.viewport.value.zoom)
  25. const zoomCompensationFactor = computed(() => 1 / zoom.value)
  26. const EDGE_STROKE_WIDTH = '1.5px'
  27. // 执行状态
  28. const executionStatus = computed<CanvasExecutionStatus>(
  29. () => (props.data?.executionStatus as CanvasExecutionStatus) || 'idle'
  30. )
  31. const isPendingEdge = computed(() => !!props.data?.pending)
  32. const isRunningEdge = computed(() => executionStatus.value === 'running')
  33. // 边线路径数据(含反向连接绕行处理)
  34. const renderData = computed(() =>
  35. getEdgeRenderData({
  36. sourceX: props.sourceX,
  37. sourceY: props.sourceY,
  38. sourcePosition: props.sourcePosition,
  39. targetX: props.targetX,
  40. targetY: props.targetY,
  41. targetPosition: props.targetPosition
  42. })
  43. )
  44. const segments = computed(() => renderData.value.segments)
  45. const labelPosition = computed(() => renderData.value.labelPosition)
  46. // CSS 变量驱动的颜色,便于暗黑模式适配
  47. const edgeColor = computed(() => {
  48. if (isPendingEdge.value) return '#409EFF'
  49. return getExecutionStatusColor(executionStatus.value)
  50. })
  51. const edgeStyles = computed(() => {
  52. const status = executionStatus.value
  53. const styles: Record<string, string> = {
  54. '--canvas-edge-color': edgeColor.value,
  55. '--canvas-edge-stroke-width': EDGE_STROKE_WIDTH
  56. }
  57. if (status !== 'idle' && !isPendingEdge.value) {
  58. styles['--canvas-edge-filter'] = `drop-shadow(0 0 6px ${edgeColor.value}55)`
  59. }
  60. return styles
  61. })
  62. const edgeClasses = computed(() => ({
  63. 'canvas-edge-path': true,
  64. 'is-pending': isPendingEdge.value,
  65. 'is-running': isRunningEdge.value
  66. }))
  67. // 延迟 hover(避免鼠标快速划过时闪烁)
  68. const delayedHovered = ref(props.hovered)
  69. const delayedHoveredSetTimeoutRef = ref<ReturnType<typeof setTimeout> | null>(null)
  70. const delayedHoveredTimeout = 600
  71. const connection = computed<Connection>(() => ({
  72. source: props.source,
  73. target: props.target,
  74. sourceHandle: props.sourceHandleId,
  75. targetHandle: props.targetHandleId,
  76. id: props.id
  77. }))
  78. watch(
  79. () => props.hovered,
  80. (isHovered) => {
  81. if (isHovered) {
  82. if (delayedHoveredSetTimeoutRef.value) clearTimeout(delayedHoveredSetTimeoutRef.value)
  83. delayedHovered.value = true
  84. } else {
  85. delayedHoveredSetTimeoutRef.value = setTimeout(() => {
  86. delayedHovered.value = false
  87. }, delayedHoveredTimeout)
  88. }
  89. },
  90. { immediate: true }
  91. )
  92. const renderToolbar = computed(() => delayedHovered.value && !props.readOnly)
  93. const toolbarStyle = computed(() => ({
  94. pointerEvents: 'all' as const,
  95. position: 'absolute' as const,
  96. transform: `translate(-50%, -50%) translate(${labelPosition.value[0]}px, ${labelPosition.value[1]}px) scale(${zoomCompensationFactor.value})`
  97. }))
  98. const onAdd = () => emit('add', connection.value)
  99. const onDelete = () => emit('delete', connection.value)
  100. </script>
  101. <template>
  102. <!-- 多段路径(反向连接时会有两段) -->
  103. <BaseEdge
  104. v-for="(segment, index) in segments"
  105. :key="`${id}-${index}`"
  106. :path="segment[0]"
  107. :marker-end="markerEnd"
  108. :interaction-width="40"
  109. :style="edgeStyles"
  110. :class="edgeClasses"
  111. />
  112. <EdgeLabelRenderer>
  113. <div :style="toolbarStyle" class="nodrag nopan">
  114. <div v-if="isPendingEdge" class="text-sm text-gray-500">pendding...</div>
  115. <div v-if="renderToolbar && !isPendingEdge" class="flex">
  116. <IconButton :edge-add-btn="id" icon="lucide:plus" size="small" square @click="onAdd" />
  117. <IconButton icon="lucide:brush-cleaning" size="small" square @click="onDelete" />
  118. </div>
  119. </div>
  120. </EdgeLabelRenderer>
  121. </template>
  122. <style>
  123. /* ── 基础边线样式 ── */
  124. .canvas-edge-path {
  125. stroke: var(--canvas-edge-color, #b1b1b7);
  126. stroke-width: var(--canvas-edge-stroke-width, 2px);
  127. stroke-linecap: round;
  128. filter: var(--canvas-edge-filter, none);
  129. transition:
  130. stroke 180ms ease,
  131. stroke-width 180ms ease,
  132. filter 180ms ease,
  133. opacity 180ms ease;
  134. }
  135. /* ── Pending: 虚线流动 ── */
  136. .canvas-edge-path.is-pending {
  137. opacity: 0.7;
  138. stroke-dasharray: 8 6;
  139. animation: edge-loading-dash 3.6s linear infinite;
  140. }
  141. /* ── Running: 快速流动虚线 ── */
  142. .canvas-edge-path.is-running {
  143. stroke-dasharray: 6 4;
  144. animation: edge-running-dash 1s linear infinite;
  145. }
  146. @keyframes edge-loading-dash {
  147. from {
  148. stroke-dashoffset: 0;
  149. }
  150. to {
  151. stroke-dashoffset: -28;
  152. }
  153. }
  154. @keyframes edge-running-dash {
  155. from {
  156. stroke-dashoffset: 0;
  157. }
  158. to {
  159. stroke-dashoffset: -20;
  160. }
  161. }
  162. </style>