|
@@ -1,15 +1,11 @@
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, watch, computed } from 'vue'
|
|
import { ref, watch, computed } from 'vue'
|
|
|
-import {
|
|
|
|
|
- BaseEdge,
|
|
|
|
|
- EdgeLabelRenderer,
|
|
|
|
|
- getBezierPath,
|
|
|
|
|
- type EdgeProps,
|
|
|
|
|
- type Connection
|
|
|
|
|
-} from '@vue-flow/core'
|
|
|
|
|
|
|
+import { BaseEdge, EdgeLabelRenderer, type EdgeProps, type Connection } from '@vue-flow/core'
|
|
|
import { IconButton } from '@repo/ui'
|
|
import { IconButton } from '@repo/ui'
|
|
|
import type { CanvasExecutionStatus } from '../../../Interface'
|
|
import type { CanvasExecutionStatus } from '../../../Interface'
|
|
|
import { getExecutionStatusColor } from '../../../utils/execution-status'
|
|
import { getExecutionStatusColor } from '../../../utils/execution-status'
|
|
|
|
|
+import { getEdgeRenderData } from './utils/getEdgeRenderData'
|
|
|
|
|
+import { useVueFlowContext } from '../../../hooks/useVueFlowContext'
|
|
|
|
|
|
|
|
defineOptions({
|
|
defineOptions({
|
|
|
inheritAttrs: false
|
|
inheritAttrs: false
|
|
@@ -23,28 +19,64 @@ const emit = defineEmits<{
|
|
|
type CanvasEdgeProps = EdgeProps & {
|
|
type CanvasEdgeProps = EdgeProps & {
|
|
|
readOnly?: boolean
|
|
readOnly?: boolean
|
|
|
hovered?: boolean
|
|
hovered?: boolean
|
|
|
- bringToFront?: boolean // Determines if entire edges layer should be brought to front
|
|
|
|
|
|
|
+ bringToFront?: boolean
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const props = defineProps<CanvasEdgeProps>()
|
|
const props = defineProps<CanvasEdgeProps>()
|
|
|
-const path = computed(() => getBezierPath(props))
|
|
|
|
|
|
|
+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>(
|
|
const executionStatus = computed<CanvasExecutionStatus>(
|
|
|
() => (props.data?.executionStatus as CanvasExecutionStatus) || 'idle'
|
|
() => (props.data?.executionStatus as CanvasExecutionStatus) || 'idle'
|
|
|
)
|
|
)
|
|
|
const isPendingEdge = computed(() => !!props.data?.pending)
|
|
const isPendingEdge = computed(() => !!props.data?.pending)
|
|
|
-const edgeStyle = computed(() => {
|
|
|
|
|
|
|
+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 status = executionStatus.value
|
|
|
- const color = getExecutionStatusColor(status)
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- stroke: isPendingEdge.value ? '#409EFF' : color,
|
|
|
|
|
- strokeWidth: 1.6,
|
|
|
|
|
- opacity: isPendingEdge.value ? 0.7 : 1,
|
|
|
|
|
- filter: status === 'idle' ? 'none' : `drop-shadow(0 0 8px ${color}55)`,
|
|
|
|
|
- transition: 'stroke 180ms ease, stroke-width 180ms ease, filter 180ms ease, opacity 180ms ease'
|
|
|
|
|
|
|
+ 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 delayedHovered = ref(props.hovered)
|
|
|
const delayedHoveredSetTimeoutRef = ref<ReturnType<typeof setTimeout> | null>(null)
|
|
const delayedHoveredSetTimeoutRef = ref<ReturnType<typeof setTimeout> | null>(null)
|
|
|
const delayedHoveredTimeout = 600
|
|
const delayedHoveredTimeout = 600
|
|
@@ -74,33 +106,30 @@ watch(
|
|
|
|
|
|
|
|
const renderToolbar = computed(() => delayedHovered.value && !props.readOnly)
|
|
const renderToolbar = computed(() => delayedHovered.value && !props.readOnly)
|
|
|
|
|
|
|
|
-const onAdd = () => {
|
|
|
|
|
- emit('add', connection.value)
|
|
|
|
|
-}
|
|
|
|
|
|
|
+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 onDelete = () => {
|
|
|
|
|
- emit('delete', connection.value)
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const onAdd = () => emit('add', connection.value)
|
|
|
|
|
+const onDelete = () => emit('delete', connection.value)
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
<template>
|
|
|
|
|
+ <!-- 多段路径(反向连接时会有两段) -->
|
|
|
<BaseEdge
|
|
<BaseEdge
|
|
|
- :path="path[0]"
|
|
|
|
|
- :interaction-width="40"
|
|
|
|
|
|
|
+ v-for="(segment, index) in segments"
|
|
|
|
|
+ :key="`${id}-${index}`"
|
|
|
|
|
+ :path="segment[0]"
|
|
|
:marker-end="markerEnd"
|
|
:marker-end="markerEnd"
|
|
|
- :style="edgeStyle"
|
|
|
|
|
- :class="['canvas-edge-path', { 'is-pending': isPendingEdge }]"
|
|
|
|
|
|
|
+ :interaction-width="40"
|
|
|
|
|
+ :style="edgeStyles"
|
|
|
|
|
+ :class="edgeClasses"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
<EdgeLabelRenderer>
|
|
<EdgeLabelRenderer>
|
|
|
- <div
|
|
|
|
|
- :style="{
|
|
|
|
|
- pointerEvents: 'all',
|
|
|
|
|
- position: 'absolute',
|
|
|
|
|
- transform: `translate(-50%, -50%) translate(${path[1]}px,${path[2]}px)`
|
|
|
|
|
- }"
|
|
|
|
|
- class="nodrag nopan"
|
|
|
|
|
- >
|
|
|
|
|
|
|
+ <div :style="toolbarStyle" class="nodrag nopan">
|
|
|
<div v-if="isPendingEdge" class="text-sm text-gray-500">pendding...</div>
|
|
<div v-if="isPendingEdge" class="text-sm text-gray-500">pendding...</div>
|
|
|
<div v-if="renderToolbar && !isPendingEdge" class="flex">
|
|
<div v-if="renderToolbar && !isPendingEdge" class="flex">
|
|
|
<IconButton :edge-add-btn="id" icon="lucide:plus" size="small" square @click="onAdd" />
|
|
<IconButton :edge-add-btn="id" icon="lucide:plus" size="small" square @click="onAdd" />
|
|
@@ -111,6 +140,32 @@ const onDelete = () => {
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<style>
|
|
<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 {
|
|
@keyframes edge-loading-dash {
|
|
|
from {
|
|
from {
|
|
|
stroke-dashoffset: 0;
|
|
stroke-dashoffset: 0;
|
|
@@ -120,8 +175,12 @@ const onDelete = () => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.canvas-edge-path.is-pending {
|
|
|
|
|
- stroke-dasharray: 8 6;
|
|
|
|
|
- animation: edge-loading-dash 3.6s linear infinite;
|
|
|
|
|
|
|
+@keyframes edge-running-dash {
|
|
|
|
|
+ from {
|
|
|
|
|
+ stroke-dashoffset: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ to {
|
|
|
|
|
+ stroke-dashoffset: -20;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|