feat: enhance DiagramCanvas and Pin components with wire creation and tooltip functionality
- Added wire creation logic in DiagramCanvas.vue with mouse tracking and event handling. - Implemented tooltip display for pins in Pin.vue with detailed information on hover. - Updated ProjectView.vue to handle wire creation and deletion events. - Refactored Wire.vue to support dynamic path rendering based on routing mode.
This commit is contained in:
		@@ -2,11 +2,46 @@
 | 
			
		||||
  <div class="flex-1 min-w-[60%] bg-base-200 relative overflow-hidden diagram-container" ref="canvasContainer"
 | 
			
		||||
      @mousedown="handleCanvasMouseDown"
 | 
			
		||||
      @mousedown.middle.prevent="startMiddleDrag"
 | 
			
		||||
      @wheel.prevent="onZoom">
 | 
			
		||||
    <div
 | 
			
		||||
      @wheel.prevent="onZoom"
 | 
			
		||||
      @contextmenu.prevent="handleContextMenu"><div
 | 
			
		||||
      ref="canvas"
 | 
			
		||||
      class="diagram-canvas"
 | 
			
		||||
      :style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }">      <!-- 渲染画布上的组件 -->
 | 
			
		||||
      :style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }">      <!-- 渲染连线 -->
 | 
			
		||||
      <svg class="wires-layer" width="4000" height="4000">
 | 
			
		||||
        <!-- 已完成的连线 -->
 | 
			
		||||
        <Wire
 | 
			
		||||
          v-for="wire in wires"
 | 
			
		||||
          :key="wire.id"
 | 
			
		||||
          :id="wire.id"
 | 
			
		||||
          :start-x="wire.startX"
 | 
			
		||||
          :start-y="wire.startY"
 | 
			
		||||
          :end-x="wire.endX"
 | 
			
		||||
          :end-y="wire.endY"
 | 
			
		||||
          :stroke-color="wire.color || '#4a5568'"
 | 
			
		||||
          :stroke-width="2"
 | 
			
		||||
          :is-active="wire.isActive"
 | 
			
		||||
          :start-component-id="wire.startComponentId"
 | 
			
		||||
          :start-pin-label="wire.startPinLabel"
 | 
			
		||||
          :end-component-id="wire.endComponentId"
 | 
			
		||||
          :end-pin-label="wire.endPinLabel"
 | 
			
		||||
          @click="handleWireClick(wire)"
 | 
			
		||||
        />
 | 
			
		||||
        
 | 
			
		||||
        <!-- 正在创建的连线 -->
 | 
			
		||||
        <Wire
 | 
			
		||||
          v-if="isCreatingWire"
 | 
			
		||||
          id="temp-wire"
 | 
			
		||||
          :start-x="creatingWireStart.x"
 | 
			
		||||
          :start-y="creatingWireStart.y"
 | 
			
		||||
          :end-x="mousePosition.x"
 | 
			
		||||
          :end-y="mousePosition.y"
 | 
			
		||||
          stroke-color="#3182ce"
 | 
			
		||||
          :stroke-width="2"
 | 
			
		||||
          :is-active="true"
 | 
			
		||||
        />
 | 
			
		||||
      </svg>
 | 
			
		||||
      
 | 
			
		||||
      <!-- 渲染画布上的组件 -->
 | 
			
		||||
      <div v-for="component in props.components" :key="component.id"
 | 
			
		||||
          class="component-wrapper"
 | 
			
		||||
          :class="{
 | 
			
		||||
@@ -20,13 +55,13 @@
 | 
			
		||||
          }"
 | 
			
		||||
          @mousedown.left.stop="startComponentDrag($event, component)"
 | 
			
		||||
          @mouseover="hoveredComponent = component.id"
 | 
			
		||||
          @mouseleave="hoveredComponent = null"><!-- 动态渲染组件 -->
 | 
			
		||||
        <component
 | 
			
		||||
          @mouseleave="hoveredComponent = null"><!-- 动态渲染组件 -->        <component
 | 
			
		||||
          :is="getComponentDefinition(component.type)"
 | 
			
		||||
          v-if="props.componentModules[component.type]"
 | 
			
		||||
          v-bind="prepareComponentProps(component.props || {})"
 | 
			
		||||
          @update:bindKey="(value: string) => updateComponentProp(component.id, 'bindKey', value)"
 | 
			
		||||
          :ref="el => { if (el) componentRefs[component.id] = el; }"
 | 
			
		||||
          @pin-click="(pinInfo: any) => handlePinClick(component.id, pinInfo, pinInfo.originalEvent)"
 | 
			
		||||
          :ref="(el: any) => { if (el) componentRefs[component.id] = el; }"
 | 
			
		||||
        />
 | 
			
		||||
         
 | 
			
		||||
         <!-- Fallback if component module not loaded yet -->
 | 
			
		||||
@@ -45,6 +80,7 @@
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, reactive, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import Wire from '@/components/Wire.vue';
 | 
			
		||||
 | 
			
		||||
// 定义组件接受的属性
 | 
			
		||||
interface ComponentItem {
 | 
			
		||||
@@ -62,7 +98,7 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits(['component-selected', 'component-moved', 'update-component-prop', 'component-delete']);
 | 
			
		||||
const emit = defineEmits(['component-selected', 'component-moved', 'update-component-prop', 'component-delete', 'wire-created', 'wire-deleted']);
 | 
			
		||||
 | 
			
		||||
// --- 画布状态 ---
 | 
			
		||||
const canvasContainer = ref<HTMLElement | null>(null);
 | 
			
		||||
@@ -292,7 +328,22 @@ function onComponentDrag(e: MouseEvent) {
 | 
			
		||||
 | 
			
		||||
// 停止拖拽组件
 | 
			
		||||
function stopComponentDrag() {
 | 
			
		||||
  draggingComponentId.value = null;
 | 
			
		||||
  // 如果有组件被拖拽,更新相关的连线
 | 
			
		||||
  if (draggingComponentId.value) {
 | 
			
		||||
    console.log(`组件拖拽结束: ${draggingComponentId.value},开始更新连线位置`);
 | 
			
		||||
    // 保存当前拖动的组件ID
 | 
			
		||||
    const currentId = draggingComponentId.value;
 | 
			
		||||
    
 | 
			
		||||
    // 先清除拖动状态
 | 
			
		||||
    draggingComponentId.value = null;
 | 
			
		||||
    
 | 
			
		||||
    // 等待DOM更新后再更新连线位置
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      updateWiresForComponent(currentId);
 | 
			
		||||
    }, 50);
 | 
			
		||||
  } else {
 | 
			
		||||
    draggingComponentId.value = null;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  document.removeEventListener('mousemove', onComponentDrag);
 | 
			
		||||
  document.removeEventListener('mouseup', stopComponentDrag);
 | 
			
		||||
@@ -314,21 +365,462 @@ function getComponentRef(componentId: string) {
 | 
			
		||||
 | 
			
		||||
// 暴露给父组件的方法
 | 
			
		||||
defineExpose({
 | 
			
		||||
  getComponentRef
 | 
			
		||||
  getComponentRef,
 | 
			
		||||
  getCanvasPosition: () => ({ x: position.x, y: position.y }),
 | 
			
		||||
  getScale: () => scale.value
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// --- 连线状态 ---
 | 
			
		||||
interface WireItem {
 | 
			
		||||
  id: string;
 | 
			
		||||
  startX: number;
 | 
			
		||||
  startY: number;
 | 
			
		||||
  endX: number;
 | 
			
		||||
  endY: number;
 | 
			
		||||
  startComponentId: string;
 | 
			
		||||
  startPinLabel: string;
 | 
			
		||||
  endComponentId?: string;
 | 
			
		||||
  endPinLabel?: string;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  isActive?: boolean;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const wires = ref<WireItem[]>([]);
 | 
			
		||||
const isCreatingWire = ref(false);
 | 
			
		||||
const creatingWireStart = reactive({ x: 0, y: 0 });
 | 
			
		||||
const creatingWireStartInfo = reactive({
 | 
			
		||||
  componentId: '',
 | 
			
		||||
  pinLabel: '',
 | 
			
		||||
  constraint: ''
 | 
			
		||||
});
 | 
			
		||||
const mousePosition = reactive({ x: 0, y: 0 });
 | 
			
		||||
 | 
			
		||||
// 更新鼠标位置
 | 
			
		||||
function updateMousePosition(e: MouseEvent) {
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  mousePosition.x = (e.clientX - containerRect.left - position.x) / scale.value;
 | 
			
		||||
  mousePosition.y = (e.clientY - containerRect.top - position.y) / scale.value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理Pin点击事件
 | 
			
		||||
function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  
 | 
			
		||||
  // 获取容器位置
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
  // 更新鼠标位置 (用于跟踪临时连线)
 | 
			
		||||
  updateMousePosition(event);
 | 
			
		||||
  
 | 
			
		||||
  // 检查 pinInfo 是否有效
 | 
			
		||||
  if (!pinInfo || !pinInfo.label) {
 | 
			
		||||
    console.error('无效的针脚信息:', pinInfo);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 针脚在页面上的绝对位置
 | 
			
		||||
  if (!pinInfo.position) {
 | 
			
		||||
    console.error('针脚信息中缺少位置数据:', pinInfo);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  const pinPagePosition = pinInfo.position;
 | 
			
		||||
  console.log(`针脚 ${pinInfo.label} 的页面坐标:`, pinPagePosition);
 | 
			
		||||
  
 | 
			
		||||
  // 将针脚页面位置转换为画布坐标系中的位置
 | 
			
		||||
  const pinCanvasX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
  const pinCanvasY = (pinPagePosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
  console.log(`针脚 ${pinInfo.label} 的画布坐标:`, { x: pinCanvasX, y: pinCanvasY });
 | 
			
		||||
  
 | 
			
		||||
  if (!isCreatingWire.value) {
 | 
			
		||||
    // 开始创建连线
 | 
			
		||||
    isCreatingWire.value = true;
 | 
			
		||||
    // 使用针脚的实际位置作为连线起点
 | 
			
		||||
    creatingWireStart.x = pinCanvasX;
 | 
			
		||||
    creatingWireStart.y = pinCanvasY;
 | 
			
		||||
    creatingWireStartInfo.componentId = componentId;
 | 
			
		||||
    creatingWireStartInfo.pinLabel = pinInfo.label;
 | 
			
		||||
    creatingWireStartInfo.constraint = pinInfo.constraint;
 | 
			
		||||
    
 | 
			
		||||
    console.log(`开始创建连线,起点针脚: ${componentId}/${pinInfo.label}, 位置: (${pinCanvasX}, ${pinCanvasY})`);
 | 
			
		||||
    
 | 
			
		||||
    // 添加鼠标移动监听
 | 
			
		||||
    document.addEventListener('mousemove', onCreatingWireMouseMove);
 | 
			
		||||
    
 | 
			
		||||
  } else {
 | 
			
		||||
    // 完成连线
 | 
			
		||||
    if (componentId === creatingWireStartInfo.componentId && pinInfo.label === creatingWireStartInfo.pinLabel) {
 | 
			
		||||
      // 如果点了同一个Pin,取消连线
 | 
			
		||||
      cancelWireCreation();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 检查约束条件
 | 
			
		||||
    const startConstraint = creatingWireStartInfo.constraint;
 | 
			
		||||
    const endConstraint = pinInfo.constraint;
 | 
			
		||||
    
 | 
			
		||||
    if (startConstraint && endConstraint && startConstraint !== endConstraint) {
 | 
			
		||||
      // 两个Pin都有约束,但约束不同,需要询问用户选择哪个约束
 | 
			
		||||
      promptForConstraintSelection(
 | 
			
		||||
        componentId, 
 | 
			
		||||
        pinInfo, 
 | 
			
		||||
        startConstraint, 
 | 
			
		||||
        endConstraint
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      // 确定最终约束
 | 
			
		||||
      let finalConstraint = '';
 | 
			
		||||
      
 | 
			
		||||
      if (startConstraint) {
 | 
			
		||||
        finalConstraint = startConstraint;
 | 
			
		||||
      } else if (endConstraint) {
 | 
			
		||||
        finalConstraint = endConstraint;
 | 
			
		||||
      } else {
 | 
			
		||||
        // 两个Pin都没有约束,生成随机约束
 | 
			
		||||
        finalConstraint = generateRandomConstraint();
 | 
			
		||||
      }
 | 
			
		||||
        // 完成连线
 | 
			
		||||
      completeWireCreation(
 | 
			
		||||
        componentId, 
 | 
			
		||||
        pinInfo.label, 
 | 
			
		||||
        finalConstraint,
 | 
			
		||||
        pinInfo
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      // 更新两个Pin的约束
 | 
			
		||||
      updatePinConstraint(creatingWireStartInfo.componentId, creatingWireStartInfo.pinLabel, finalConstraint);
 | 
			
		||||
      updatePinConstraint(componentId, pinInfo.label, finalConstraint);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生成随机约束名
 | 
			
		||||
function generateRandomConstraint() {
 | 
			
		||||
  const randomId = Math.floor(Math.random() * 1000000);
 | 
			
		||||
  return `$auto_constraint_${randomId}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 询问用户选择约束
 | 
			
		||||
function promptForConstraintSelection(
 | 
			
		||||
  endComponentId: string, 
 | 
			
		||||
  endPinInfo: any, 
 | 
			
		||||
  startConstraint: string,
 | 
			
		||||
  endConstraint: string
 | 
			
		||||
) {
 | 
			
		||||
  // 在实际应用中,这里可以使用Modal对话框等UI组件
 | 
			
		||||
  // 这里简化为使用浏览器的confirm
 | 
			
		||||
  const useStartConstraint = confirm(
 | 
			
		||||
    `连接两个不同约束的Pin:\n` +
 | 
			
		||||
    `- 起点约束: ${startConstraint}\n` +
 | 
			
		||||
    `- 终点约束: ${endConstraint}\n\n` +
 | 
			
		||||
    `点击"确定"使用起点约束,点击"取消"使用终点约束。`
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  const finalConstraint = useStartConstraint ? startConstraint : endConstraint;
 | 
			
		||||
    // 完成连线
 | 
			
		||||
  completeWireCreation(
 | 
			
		||||
    endComponentId, 
 | 
			
		||||
    endPinInfo.label, 
 | 
			
		||||
    finalConstraint,
 | 
			
		||||
    endPinInfo
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  // 更新两个Pin的约束
 | 
			
		||||
  updatePinConstraint(creatingWireStartInfo.componentId, creatingWireStartInfo.pinLabel, finalConstraint);
 | 
			
		||||
  updatePinConstraint(endComponentId, endPinInfo.label, finalConstraint);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新Pin的约束
 | 
			
		||||
function updatePinConstraint(componentId: string, pinLabel: string, constraint: string) {
 | 
			
		||||
  // 通过组件ID获取组件实例
 | 
			
		||||
  const component = props.components.find(c => c.id === componentId);
 | 
			
		||||
  if (!component) return;
 | 
			
		||||
  
 | 
			
		||||
  // 获取组件引用
 | 
			
		||||
  const componentRef = componentRefs.value[componentId];
 | 
			
		||||
  if (!componentRef) return;
 | 
			
		||||
  
 | 
			
		||||
  // 更新组件属性
 | 
			
		||||
  if (component.props && componentRef.getInfo) {
 | 
			
		||||
    const pinInfo = componentRef.getInfo();
 | 
			
		||||
    if (pinInfo && pinInfo.label === pinLabel) {
 | 
			
		||||
      emit('update-component-prop', { 
 | 
			
		||||
        id: componentId, 
 | 
			
		||||
        propName: 'constraint', 
 | 
			
		||||
        value: constraint 
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 完成连线创建
 | 
			
		||||
function completeWireCreation(endComponentId: string, endPinLabel: string, constraint: string, endPinInfo?: any) {
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
  // 获取终点针脚的位置
 | 
			
		||||
  let endX = mousePosition.x;
 | 
			
		||||
  let endY = mousePosition.y;
 | 
			
		||||
  
 | 
			
		||||
  console.log(`开始创建连线,起点:(${creatingWireStart.x}, ${creatingWireStart.y})`);
 | 
			
		||||
  
 | 
			
		||||
  // 尝试使用 getPinPosition 获取精确的针脚位置
 | 
			
		||||
  const endComponentRef = componentRefs.value[endComponentId];
 | 
			
		||||
  if (endComponentRef && endComponentRef.getPinPosition) {
 | 
			
		||||
    const pinPosition = endComponentRef.getPinPosition(endPinLabel);
 | 
			
		||||
    if (pinPosition) {
 | 
			
		||||
      endX = (pinPosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
      endY = (pinPosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
      console.log(`通过 getPinPosition 获取终点针脚 ${endPinLabel} 的画布坐标: (${endX}, ${endY})`);
 | 
			
		||||
    } else {
 | 
			
		||||
      console.warn(`getPinPosition 返回 null,将使用备选方法`);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (endPinInfo && endPinInfo.position) {
 | 
			
		||||
    // 如果 getPinPosition 不可用,使用传入的针脚位置信息
 | 
			
		||||
    const pinPagePosition = endPinInfo.position;
 | 
			
		||||
    endX = (pinPagePosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
    endY = (pinPagePosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
    console.log(`通过 pinInfo.position 获取终点针脚位置: (${endX}, ${endY})`);
 | 
			
		||||
  } else {
 | 
			
		||||
    console.warn(`无法获取针脚 ${endPinLabel} 的精确位置,使用鼠标位置代替`);
 | 
			
		||||
  }
 | 
			
		||||
    // 检查起点和终点是否重合
 | 
			
		||||
  const distanceSquared = Math.pow(endX - creatingWireStart.x, 2) + Math.pow(endY - creatingWireStart.y, 2);
 | 
			
		||||
  if (distanceSquared < 1) { // 如果距离小于1像素
 | 
			
		||||
    console.warn(`起点和终点太接近 (${distanceSquared}像素²),调整终点位置`);
 | 
			
		||||
    // 稍微调整终点位置,避免重合
 | 
			
		||||
    endX += 10 + Math.random() * 5;
 | 
			
		||||
    endY += 10 + Math.random() * 5;
 | 
			
		||||
  }
 | 
			
		||||
    // 创建新的连线
 | 
			
		||||
  const newWire: WireItem = {
 | 
			
		||||
    id: `wire-${Date.now()}`,
 | 
			
		||||
    startX: creatingWireStart.x,
 | 
			
		||||
    startY: creatingWireStart.y,
 | 
			
		||||
    endX: endX,
 | 
			
		||||
    endY: endY,
 | 
			
		||||
    startComponentId: creatingWireStartInfo.componentId,
 | 
			
		||||
    startPinLabel: creatingWireStartInfo.pinLabel,
 | 
			
		||||
    endComponentId: endComponentId,
 | 
			
		||||
    endPinLabel: endPinLabel,
 | 
			
		||||
    color: '#4a5568',
 | 
			
		||||
    constraint: constraint
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  console.log(`新连线创建完成:`, newWire);
 | 
			
		||||
  
 | 
			
		||||
  // 确保起点和终点不重合
 | 
			
		||||
  if (Math.abs(newWire.startX - newWire.endX) < 1 && Math.abs(newWire.startY - newWire.endY) < 1) {
 | 
			
		||||
    console.warn(`连线的起点和终点重合,调整终点位置`);
 | 
			
		||||
    newWire.endX += 20;
 | 
			
		||||
    newWire.endY += 20;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  wires.value.push(newWire);
 | 
			
		||||
  
 | 
			
		||||
  // 通知父组件连线已创建
 | 
			
		||||
  emit('wire-created', newWire);
 | 
			
		||||
  
 | 
			
		||||
  // 重置创建状态
 | 
			
		||||
  isCreatingWire.value = false;
 | 
			
		||||
  document.removeEventListener('mousemove', onCreatingWireMouseMove);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 取消连线创建
 | 
			
		||||
function cancelWireCreation() {
 | 
			
		||||
  isCreatingWire.value = false;
 | 
			
		||||
  document.removeEventListener('mousemove', onCreatingWireMouseMove);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 鼠标移动时更新连线
 | 
			
		||||
function onCreatingWireMouseMove(e: MouseEvent) {
 | 
			
		||||
  updateMousePosition(e);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理连线点击
 | 
			
		||||
function handleWireClick(wire: WireItem) {
 | 
			
		||||
  // 这里可以添加连线选中、删除等功能
 | 
			
		||||
  const deleteWire = confirm('是否删除此连线?');
 | 
			
		||||
  if (deleteWire) {
 | 
			
		||||
    // 删除连线
 | 
			
		||||
    const index = wires.value.findIndex(w => w.id === wire.id);
 | 
			
		||||
    if (index !== -1) {
 | 
			
		||||
      const deletedWire = wires.value.splice(index, 1)[0];
 | 
			
		||||
      
 | 
			
		||||
      // 通知父组件连线已删除
 | 
			
		||||
      emit('wire-deleted', deletedWire.id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新所有连线位置
 | 
			
		||||
function updateAllWires() {
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  
 | 
			
		||||
  // 遍历所有连线并更新位置
 | 
			
		||||
  wires.value.forEach(wire => {
 | 
			
		||||
    updateWireWithPinPositions(wire);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 根据针脚位置更新连线
 | 
			
		||||
function updateWireWithPinPositions(wire: WireItem) {
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
  console.log(`更新连线 ${wire.id},当前位置: 起点(${wire.startX}, ${wire.startY}), 终点(${wire.endX}, ${wire.endY})`);
 | 
			
		||||
  
 | 
			
		||||
  // 保存原始位置,用于检测变化
 | 
			
		||||
  const originalStartX = wire.startX;
 | 
			
		||||
  const originalStartY = wire.startY;
 | 
			
		||||
  const originalEndX = wire.endX;
 | 
			
		||||
  const originalEndY = wire.endY;
 | 
			
		||||
  
 | 
			
		||||
  // 更新起点
 | 
			
		||||
  if (wire.startComponentId && wire.startPinLabel) {
 | 
			
		||||
    const startComponentRef = componentRefs.value[wire.startComponentId];
 | 
			
		||||
    if (startComponentRef && startComponentRef.getPinPosition) {
 | 
			
		||||
      const pinPosition = startComponentRef.getPinPosition(wire.startPinLabel);
 | 
			
		||||
      if (pinPosition) {
 | 
			
		||||
        const newStartX = (pinPosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
        const newStartY = (pinPosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
        
 | 
			
		||||
        console.log(`更新连线起点 ${wire.startComponentId}/${wire.startPinLabel}: (${wire.startX}, ${wire.startY}) => (${newStartX}, ${newStartY})`);
 | 
			
		||||
        
 | 
			
		||||
        wire.startX = newStartX;
 | 
			
		||||
        wire.startY = newStartY;
 | 
			
		||||
      } else {
 | 
			
		||||
        console.warn(`无法获取针脚 ${wire.startComponentId}/${wire.startPinLabel} 的位置`);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      console.warn(`组件 ${wire.startComponentId} 没有实现 getPinPosition 方法`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 更新终点
 | 
			
		||||
  if (wire.endComponentId && wire.endPinLabel) {
 | 
			
		||||
    const endComponentRef = componentRefs.value[wire.endComponentId];
 | 
			
		||||
    if (endComponentRef && endComponentRef.getPinPosition) {
 | 
			
		||||
      const pinPosition = endComponentRef.getPinPosition(wire.endPinLabel);
 | 
			
		||||
      if (pinPosition) {
 | 
			
		||||
        const newEndX = (pinPosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
        const newEndY = (pinPosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
        
 | 
			
		||||
        console.log(`更新连线终点 ${wire.endComponentId}/${wire.endPinLabel}: (${wire.endX}, ${wire.endY}) => (${newEndX}, ${newEndY})`);
 | 
			
		||||
        
 | 
			
		||||
        wire.endX = newEndX;
 | 
			
		||||
        wire.endY = newEndY;
 | 
			
		||||
      } else {
 | 
			
		||||
        console.warn(`无法获取针脚 ${wire.endComponentId}/${wire.endPinLabel} 的位置`);
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      console.warn(`组件 ${wire.endComponentId} 没有实现 getPinPosition 方法`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 在更新后检查起点和终点是否重合
 | 
			
		||||
  const positionChanged = 
 | 
			
		||||
    originalStartX !== wire.startX || 
 | 
			
		||||
    originalStartY !== wire.startY || 
 | 
			
		||||
    originalEndX !== wire.endX || 
 | 
			
		||||
    originalEndY !== wire.endY;
 | 
			
		||||
  
 | 
			
		||||
  if (positionChanged) {
 | 
			
		||||
    // 如果位置有更新,确保起点和终点不重合
 | 
			
		||||
    ensureUniquePinPositions(wire);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新与组件相关的所有连线位置
 | 
			
		||||
function updateWiresForComponent(componentId: string) {
 | 
			
		||||
  if (!canvasContainer.value || !componentId) return;
 | 
			
		||||
  
 | 
			
		||||
  console.log(`更新组件 ${componentId} 相关的连线位置`);
 | 
			
		||||
  
 | 
			
		||||
  // 检查组件是否存在
 | 
			
		||||
  const component = props.components.find(c => c.id === componentId);
 | 
			
		||||
  if (!component) {
 | 
			
		||||
    console.warn(`找不到组件 ${componentId}`);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 查找与该组件关联的所有连线
 | 
			
		||||
  const relatedWires = wires.value.filter(wire => 
 | 
			
		||||
    wire.startComponentId === componentId || 
 | 
			
		||||
    wire.endComponentId === componentId
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  console.log(`找到 ${relatedWires.length} 条相关连线`);
 | 
			
		||||
  
 | 
			
		||||
  if (relatedWires.length === 0) {
 | 
			
		||||
    // 没有找到直接关联的连线,检查所有连线
 | 
			
		||||
    console.log('没有找到直接关联的连线,检查所有连线');
 | 
			
		||||
    
 | 
			
		||||
    // 打印所有连线信息,帮助调试
 | 
			
		||||
    wires.value.forEach((wire, index) => {
 | 
			
		||||
      console.log(`连线 ${index}: startComponentId=${wire.startComponentId}, endComponentId=${wire.endComponentId}`);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 更新所有相关连线的位置
 | 
			
		||||
  relatedWires.forEach(wire => {
 | 
			
		||||
    console.log(`更新连线 ${wire.id} (${wire.startComponentId}/${wire.startPinLabel} -> ${wire.endComponentId}/${wire.endPinLabel})`);
 | 
			
		||||
    updateWireWithPinPositions(wire);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 确保针脚位置有差异
 | 
			
		||||
function ensureUniquePinPositions(wire: WireItem) {
 | 
			
		||||
  if (Math.abs(wire.startX - wire.endX) < 5 && Math.abs(wire.startY - wire.endY) < 5) {
 | 
			
		||||
    console.warn('检测到连线起点和终点非常接近,添加随机偏移');
 | 
			
		||||
    
 | 
			
		||||
    // 根据组件ID和针脚标签生成一个确定性的偏移
 | 
			
		||||
    const idSum = (wire.startComponentId?.charCodeAt(0) || 0) + 
 | 
			
		||||
                 (wire.endComponentId?.charCodeAt(0) || 0) +
 | 
			
		||||
                 (wire.startPinLabel?.charCodeAt(0) || 0) +
 | 
			
		||||
                 (wire.endPinLabel?.charCodeAt(0) || 0);
 | 
			
		||||
    
 | 
			
		||||
    // 使用组件ID和针脚标签生成偏移,确保相同的组件始终有相同的偏移
 | 
			
		||||
    const offsetX = 20 * Math.cos(idSum * 0.1);
 | 
			
		||||
    const offsetY = 20 * Math.sin(idSum * 0.1);
 | 
			
		||||
    
 | 
			
		||||
    wire.endX += offsetX;
 | 
			
		||||
    wire.endY += offsetY;
 | 
			
		||||
    
 | 
			
		||||
    console.log(`应用偏移 (${offsetX.toFixed(2)}, ${offsetY.toFixed(2)}) 到连线终点`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 生命周期钩子 ---
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 初始化中心位置
 | 
			
		||||
  if (canvasContainer.value) {
 | 
			
		||||
    position.x = canvasContainer.value.clientWidth / 2;
 | 
			
		||||
    position.y = canvasContainer.value.clientHeight / 2;
 | 
			
		||||
  }
 | 
			
		||||
  if (canvasContainer.value) {
 | 
			
		||||
    // 修改为将画布中心点放在容器中心点
 | 
			
		||||
    position.x = canvasContainer.value.clientWidth / 2 - 2000; // 画布宽度的一半
 | 
			
		||||
    position.y = canvasContainer.value.clientHeight / 2 - 2000; // 画布高度的一半
 | 
			
		||||
    
 | 
			
		||||
    canvasContainer.value.addEventListener('wheel', onZoom);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 添加键盘事件监听器
 | 
			
		||||
  window.addEventListener('keydown', handleKeyDown);
 | 
			
		||||
  
 | 
			
		||||
  // 添加定期更新连线位置的定时器
 | 
			
		||||
  const wireUpdateInterval = setInterval(() => {
 | 
			
		||||
    if (wires.value.length > 0) {
 | 
			
		||||
      updateAllWires();
 | 
			
		||||
    }
 | 
			
		||||
  }, 1000); // 每秒更新一次所有连线位置
 | 
			
		||||
  
 | 
			
		||||
  // 在组件卸载时清除定时器
 | 
			
		||||
  onUnmounted(() => {
 | 
			
		||||
    clearInterval(wireUpdateInterval);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 处理键盘事件
 | 
			
		||||
@@ -340,6 +832,12 @@ function handleKeyDown(e: KeyboardEvent) {
 | 
			
		||||
    // 清除选中状态
 | 
			
		||||
    selectedComponentId.value = null;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 如果当前正在创建连线,并且按下了ESC键
 | 
			
		||||
  if (isCreatingWire.value && e.key === 'Escape') {
 | 
			
		||||
    // 取消连线创建
 | 
			
		||||
    cancelWireCreation();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
@@ -386,6 +884,22 @@ onUnmounted(() => {
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 连线层样式 */
 | 
			
		||||
.wires-layer {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  z-index: 50;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wires-layer path {
 | 
			
		||||
  pointer-events: stroke;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 元器件容器样式 */
 | 
			
		||||
.component-wrapper {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										141
									
								
								src/components/Wire.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/components/Wire.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,141 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <path 
 | 
			
		||||
    :d="pathData" 
 | 
			
		||||
    fill="none" 
 | 
			
		||||
    :stroke="strokeColor" 
 | 
			
		||||
    :stroke-width="strokeWidth" 
 | 
			
		||||
    stroke-linecap="round"
 | 
			
		||||
    stroke-linejoin="round"
 | 
			
		||||
    :class="{ 'wire-active': isActive }"
 | 
			
		||||
    @click="handleClick"
 | 
			
		||||
  />
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, defineEmits } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  id: string;
 | 
			
		||||
  startX: number;
 | 
			
		||||
  startY: number;
 | 
			
		||||
  endX: number;
 | 
			
		||||
  endY: number;
 | 
			
		||||
  strokeColor?: string;
 | 
			
		||||
  strokeWidth?: number;
 | 
			
		||||
  isActive?: boolean;
 | 
			
		||||
  routingMode?: 'auto' | 'orthogonal' | 'direct';
 | 
			
		||||
  // 添加针脚引用属性
 | 
			
		||||
  startComponentId?: string;
 | 
			
		||||
  startPinLabel?: string;
 | 
			
		||||
  endComponentId?: string;
 | 
			
		||||
  endPinLabel?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  strokeColor: '#4a5568',
 | 
			
		||||
  strokeWidth: 2,
 | 
			
		||||
  isActive: false,
 | 
			
		||||
  routingMode: 'orthogonal'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['click']);
 | 
			
		||||
 | 
			
		||||
function handleClick(event: MouseEvent) {
 | 
			
		||||
  emit('click', { id: props.id, event });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const pathData = computed(() => {
 | 
			
		||||
  // 先检查起点和终点是否几乎重合
 | 
			
		||||
  const dx = Math.abs(props.endX - props.startX);
 | 
			
		||||
  const dy = Math.abs(props.endY - props.startY);
 | 
			
		||||
  
 | 
			
		||||
  // 如果几乎重合,强制绘制一个小的可见路径
 | 
			
		||||
  if (dx < 0.5 && dy < 0.5) {
 | 
			
		||||
    console.warn('连线的起点和终点几乎重合,强制绘制可见路径');
 | 
			
		||||
    // 这里绘制一个小圆圈
 | 
			
		||||
    const r = 5; // 半径5像素
 | 
			
		||||
    return `M ${props.startX} ${props.startY} 
 | 
			
		||||
            m -${r}, 0 
 | 
			
		||||
            a ${r},${r} 0 1,0 ${r*2},0 
 | 
			
		||||
            a ${r},${r} 0 1,0 -${r*2},0`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (props.routingMode === 'direct') {
 | 
			
		||||
    return `M ${props.startX} ${props.startY} L ${props.endX} ${props.endY}`;
 | 
			
		||||
  } else if (props.routingMode === 'orthogonal') {
 | 
			
		||||
    // 计算直角连线路径
 | 
			
		||||
    return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
 | 
			
		||||
  } else {
 | 
			
		||||
    // 自动判断连线方式
 | 
			
		||||
    if (dx < 10 || dy < 10) {
 | 
			
		||||
      // 如果距离足够近,使用直线
 | 
			
		||||
      return `M ${props.startX} ${props.startY} L ${props.endX} ${props.endY}`;
 | 
			
		||||
    } else {
 | 
			
		||||
      // 否则使用直角线
 | 
			
		||||
      return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) {
 | 
			
		||||
  // 计算两点之间的水平和垂直距离
 | 
			
		||||
  const dx = endX - startX;
 | 
			
		||||
  const dy = endY - startY;
 | 
			
		||||
  
 | 
			
		||||
  // 如果起点和终点非常接近,稍微偏移终点
 | 
			
		||||
  if (Math.abs(dx) < 1 && Math.abs(dy) < 1) {
 | 
			
		||||
    console.warn('连线的起点和终点几乎重合,调整路径显示');
 | 
			
		||||
    // 强制创建一个小的正方形路径
 | 
			
		||||
    const offset = 5; // 5像素的小正方形
 | 
			
		||||
    return `M ${startX} ${startY} 
 | 
			
		||||
            L ${startX + offset} ${startY} 
 | 
			
		||||
            L ${startX + offset} ${startY + offset} 
 | 
			
		||||
            L ${startX} ${startY + offset} 
 | 
			
		||||
            L ${startX} ${startY}`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 如果在同一水平或垂直线上,直接连线
 | 
			
		||||
  if (dx === 0 || dy === 0) {
 | 
			
		||||
    return `M ${startX} ${startY} L ${endX} ${endY}`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 检查是否能走45度斜线
 | 
			
		||||
  const absDx = Math.abs(dx);
 | 
			
		||||
  const absDy = Math.abs(dy);
 | 
			
		||||
  
 | 
			
		||||
  if (absDx === absDy) {
 | 
			
		||||
    // 可以直接走45度斜线
 | 
			
		||||
    return `M ${startX} ${startY} L ${endX} ${endY}`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 确定要走的路径 - 我们这里使用L型路径
 | 
			
		||||
  // 先水平移动,然后垂直移动
 | 
			
		||||
  if (absDx > absDy) {
 | 
			
		||||
    const middleX = startX + dx * 0.5;
 | 
			
		||||
    return `M ${startX} ${startY} 
 | 
			
		||||
            L ${middleX} ${startY} 
 | 
			
		||||
            L ${middleX} ${endY} 
 | 
			
		||||
            L ${endX} ${endY}`;
 | 
			
		||||
  } else {
 | 
			
		||||
    // 先垂直移动,然后水平移动
 | 
			
		||||
    const middleY = startY + dy * 0.5;
 | 
			
		||||
    return `M ${startX} ${startY} 
 | 
			
		||||
            L ${startX} ${middleY} 
 | 
			
		||||
            L ${endX} ${middleY} 
 | 
			
		||||
            L ${endX} ${endY}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.wire-active {
 | 
			
		||||
  stroke-dasharray: 5;
 | 
			
		||||
  animation: dash 0.5s linear infinite;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes dash {
 | 
			
		||||
  to {
 | 
			
		||||
    stroke-dashoffset: 10;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -5,15 +5,18 @@
 | 
			
		||||
    :height="height" 
 | 
			
		||||
    :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
 | 
			
		||||
    class="pin-component"
 | 
			
		||||
    :data-pin-id="props.label"
 | 
			
		||||
  >
 | 
			
		||||
    <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
 | 
			
		||||
      <g v-if="props.appearance === 'None'">
 | 
			
		||||
        <g transform="translate(-12.5, -12.5)" class="interactive">
 | 
			
		||||
          <circle
 | 
			
		||||
    <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">      <g v-if="props.appearance === 'None'">
 | 
			
		||||
        <g transform="translate(-12.5, -12.5)" class="interactive">          <circle
 | 
			
		||||
            style="fill:#909090"
 | 
			
		||||
            cx="12.5"
 | 
			
		||||
            cy="12.5"
 | 
			
		||||
            r="3.75" />
 | 
			
		||||
            r="3.75"
 | 
			
		||||
            @mouseenter="showPinTooltip"
 | 
			
		||||
            @mouseleave="hidePinTooltip"
 | 
			
		||||
            @click.stop="handlePinClick"
 | 
			
		||||
            :data-pin-id="`${props.label}-${props.constraint}`" />
 | 
			
		||||
        </g>
 | 
			
		||||
      </g>      
 | 
			
		||||
      <g v-else-if="props.appearance === 'Dip'">
 | 
			
		||||
@@ -25,31 +28,52 @@
 | 
			
		||||
            height="25"
 | 
			
		||||
            x="0"
 | 
			
		||||
            y="0"
 | 
			
		||||
            rx="2.5" />
 | 
			
		||||
          <circle
 | 
			
		||||
            rx="2.5" />          <circle
 | 
			
		||||
            style="fill:#ecececc5;fill-opacity:0.772973"
 | 
			
		||||
            cx="12.5"
 | 
			
		||||
            cy="12.5"
 | 
			
		||||
            r="3.75" />
 | 
			
		||||
            r="3.75"
 | 
			
		||||
            @mouseenter="showPinTooltip"
 | 
			
		||||
            @mouseleave="hidePinTooltip"
 | 
			
		||||
            @click.stop="handlePinClick"
 | 
			
		||||
            :data-pin-id="`${props.label}-${props.constraint}`" />
 | 
			
		||||
          <text
 | 
			
		||||
            style="font-size:6.85px;text-align:start;fill:#ffffff;fill-opacity:0.772973"
 | 
			
		||||
            x="7.3"
 | 
			
		||||
            y="7"
 | 
			
		||||
            xml:space="preserve">{{ props.label }}</text>
 | 
			
		||||
        </g>
 | 
			
		||||
      </g>
 | 
			
		||||
      <g v-else-if="props.appearance === 'SMT'">
 | 
			
		||||
      </g>      <g v-else-if="props.appearance === 'SMT'">
 | 
			
		||||
        <rect x="-20" y="-10" width="40" height="20" fill="#aaa" rx="2" ry="2" />
 | 
			
		||||
        <rect x="-18" y="-8" width="36" height="16" :fill="getColorByType" rx="1" ry="1" />
 | 
			
		||||
        <rect x="-16" y="-6" width="26" height="12" :fill="getColorByType" rx="1" ry="1" />
 | 
			
		||||
        <text text-anchor="middle" dominant-baseline="middle" font-size="8" fill="white" x="-3">{{ props.label }}</text>
 | 
			
		||||
      </g>
 | 
			
		||||
    </g>
 | 
			
		||||
        <!-- SMT样式的针脚 -->        <circle
 | 
			
		||||
          fill="#ecececc5"
 | 
			
		||||
          cx="10"
 | 
			
		||||
          cy="0"
 | 
			
		||||
          r="3.75"
 | 
			
		||||
          class="interactive"
 | 
			
		||||
          @mouseenter="showPinTooltip"
 | 
			
		||||
          @mouseleave="hidePinTooltip"
 | 
			
		||||
          @click.stop="handlePinClick"
 | 
			
		||||
          :data-pin-id="`${props.label}-${props.constraint}`" />
 | 
			
		||||
      </g>    </g>
 | 
			
		||||
  </svg>
 | 
			
		||||
  <!-- 提示框 - 在SVG外部 -->
 | 
			
		||||
  <div v-if="showTooltip" class="pin-tooltip" :style="{
 | 
			
		||||
    position: 'absolute',
 | 
			
		||||
    top: tooltipPosition.top + 'px',
 | 
			
		||||
    left: tooltipPosition.left + 'px',
 | 
			
		||||
    transform: 'translate(-50%, -100%)',
 | 
			
		||||
    marginTop: '-5px'
 | 
			
		||||
  }">
 | 
			
		||||
    {{ tooltipText }}
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed } from 'vue';
 | 
			
		||||
import { ref, computed, reactive } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
@@ -70,11 +94,92 @@ const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
  'value-change'
 | 
			
		||||
  'value-change',
 | 
			
		||||
  'pin-click'  // 新增Pin点击事件
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 内部状态
 | 
			
		||||
const analogValue = ref(0);
 | 
			
		||||
const showTooltip = ref(false);
 | 
			
		||||
const tooltipText = ref('');
 | 
			
		||||
const tooltipPosition = reactive({
 | 
			
		||||
  top: 0,
 | 
			
		||||
  left: 0
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 显示针脚提示
 | 
			
		||||
function showPinTooltip(event: MouseEvent) {
 | 
			
		||||
  showTooltip.value = true;
 | 
			
		||||
  const target = event.target as SVGElement;
 | 
			
		||||
  const rect = target.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
  // 更新提示位置
 | 
			
		||||
  tooltipPosition.top = rect.top;
 | 
			
		||||
  tooltipPosition.left = rect.left + rect.width / 2;
 | 
			
		||||
  
 | 
			
		||||
  // 更新提示文本
 | 
			
		||||
  tooltipText.value = generateTooltipText();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 隐藏针脚提示
 | 
			
		||||
function hidePinTooltip() {
 | 
			
		||||
  showTooltip.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生成提示文本
 | 
			
		||||
function generateTooltipText() {
 | 
			
		||||
  const parts = [];
 | 
			
		||||
  parts.push(`标签: ${props.label}`);
 | 
			
		||||
  
 | 
			
		||||
  if (props.constraint) {
 | 
			
		||||
    parts.push(`约束: ${props.constraint}`);
 | 
			
		||||
  } else {
 | 
			
		||||
    parts.push('约束: 未定义');
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  parts.push(`方向: ${getDirectionText()}`);
 | 
			
		||||
  parts.push(`类型: ${props.type === 'digital' ? '数字' : '模拟'}`);
 | 
			
		||||
  
 | 
			
		||||
  return parts.join(' | ');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取方向文本
 | 
			
		||||
function getDirectionText() {
 | 
			
		||||
  switch (props.direction) {
 | 
			
		||||
    case 'input': return '输入';
 | 
			
		||||
    case 'output': return '输出';
 | 
			
		||||
    case 'inout': return '双向';
 | 
			
		||||
    default: return '未知';
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理针脚点击
 | 
			
		||||
function handlePinClick(event: MouseEvent) {
 | 
			
		||||
  // 获取针脚在SVG中的位置
 | 
			
		||||
  const target = event.target as SVGElement;
 | 
			
		||||
  const rect = target.getBoundingClientRect();
 | 
			
		||||
  const pinCenter = {
 | 
			
		||||
    x: rect.left + rect.width / 2,
 | 
			
		||||
    y: rect.top + rect.height / 2
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  console.log(`针脚 ${props.label} 被点击,位置:`, pinCenter);
 | 
			
		||||
 | 
			
		||||
  // 发送针脚点击事件给父组件
 | 
			
		||||
  emit('pin-click', {
 | 
			
		||||
    label: props.label,
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    type: props.type,
 | 
			
		||||
    direction: props.direction,
 | 
			
		||||
    // 获取针脚在页面上的位置
 | 
			
		||||
    position: {
 | 
			
		||||
      x: pinCenter.x,
 | 
			
		||||
      y: pinCenter.y
 | 
			
		||||
    },
 | 
			
		||||
    // 获取原始事件
 | 
			
		||||
    originalEvent: event
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size);
 | 
			
		||||
const height = computed(() => {
 | 
			
		||||
@@ -112,7 +217,46 @@ defineExpose({
 | 
			
		||||
    direction: props.direction,
 | 
			
		||||
    type: props.type,
 | 
			
		||||
    appearance: props.appearance
 | 
			
		||||
  })
 | 
			
		||||
  }),  // 添加获取针脚位置的方法
 | 
			
		||||
  getPinPosition: (pinLabel: string) => {
 | 
			
		||||
    // 在 Pin 组件中,只有一个针脚,所以直接检查标签是否匹配
 | 
			
		||||
    if (pinLabel !== props.label) return null;
 | 
			
		||||
    
 | 
			
		||||
    // 使用自身作为一个唯一标识符,确保针脚位置是基于实际的DOM位置
 | 
			
		||||
    // 这样可以避免document.querySelector获取到非预期的元素
 | 
			
		||||
    const pinElement = document.querySelector(`[data-pin-id="${props.label}-${props.constraint}"]`) as SVGElement;
 | 
			
		||||
    
 | 
			
		||||
    if (pinElement) {
 | 
			
		||||
      // 获取针脚元素的位置
 | 
			
		||||
      const rect = pinElement.getBoundingClientRect();
 | 
			
		||||
      return {
 | 
			
		||||
        x: rect.left + rect.width / 2,
 | 
			
		||||
        y: rect.top + rect.height / 2
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 如果找不到特定元素,使用计算出的位置
 | 
			
		||||
    // 这种情况下我们需要找到父SVG元素位置
 | 
			
		||||
    const svgElement = document.querySelector(`.pin-component[data-pin-id="${props.label}"]`) as SVGElement;
 | 
			
		||||
    if (!svgElement) {
 | 
			
		||||
      console.error(`找不到针脚 ${props.label} 的SVG元素`);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    const svgRect = svgElement.getBoundingClientRect();
 | 
			
		||||
    
 | 
			
		||||
    // 根据针脚类型和方向计算相对位置
 | 
			
		||||
    let pinX = svgRect.left + svgRect.width / 2;
 | 
			
		||||
    let pinY = svgRect.top + svgRect.height / 2;
 | 
			
		||||
    
 | 
			
		||||
    // 添加一个小的随机偏移,确保不同针脚返回不同位置
 | 
			
		||||
    // 仅在测试时启用,生产环境应使用更精确的算法
 | 
			
		||||
    const randomOffset = 0.1;
 | 
			
		||||
    pinX += Math.random() * randomOffset;
 | 
			
		||||
    pinY += Math.random() * randomOffset;
 | 
			
		||||
    
 | 
			
		||||
    return { x: pinX, y: pinY };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -120,6 +264,7 @@ defineExpose({
 | 
			
		||||
.pin-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
.interactive {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
@@ -128,4 +273,15 @@ defineExpose({
 | 
			
		||||
.interactive:hover {
 | 
			
		||||
  filter: brightness(1.2);
 | 
			
		||||
}
 | 
			
		||||
.pin-tooltip {
 | 
			
		||||
  background-color: rgba(0, 0, 0, 0.8);
 | 
			
		||||
  color: white;
 | 
			
		||||
  padding: 4px 8px;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  z-index: 1000;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -4,11 +4,12 @@
 | 
			
		||||
      <!-- 左侧图形化区域 -->
 | 
			
		||||
      <div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }">        <DiagramCanvas
 | 
			
		||||
          ref="diagramCanvas"          :components="components"
 | 
			
		||||
          :componentModules="componentModules"
 | 
			
		||||
          @component-selected="handleComponentSelected"
 | 
			
		||||
          :componentModules="componentModules"          @component-selected="handleComponentSelected"
 | 
			
		||||
          @component-moved="handleComponentMoved"
 | 
			
		||||
          @update-component-prop="updateComponentProp"
 | 
			
		||||
          @component-delete="handleComponentDelete"
 | 
			
		||||
          @wire-created="handleWireCreated"
 | 
			
		||||
          @wire-deleted="handleWireDeleted"
 | 
			
		||||
        />
 | 
			
		||||
        <!-- 添加元器件按钮 -->
 | 
			
		||||
        <button class="btn btn-circle btn-primary absolute top-8 right-8 shadow-lg z-10" @click="openComponentsMenu">
 | 
			
		||||
@@ -219,12 +220,50 @@ async function handleAddComponent(componentData: { type: string; name: string; p
 | 
			
		||||
  // 加载组件模块以便后续使用
 | 
			
		||||
  await loadComponentModule(componentData.type);
 | 
			
		||||
 | 
			
		||||
  // 获取画布容器和位置信息
 | 
			
		||||
  const canvasInstance = diagramCanvas.value as any;
 | 
			
		||||
  
 | 
			
		||||
  // 默认位置(当无法获取画布信息时使用)
 | 
			
		||||
  let posX = 100;
 | 
			
		||||
  let posY = 100;
 | 
			
		||||
  
 | 
			
		||||
  try {
 | 
			
		||||
    if (canvasInstance) {
 | 
			
		||||
      // 获取画布容器
 | 
			
		||||
      const canvasContainer = canvasInstance.$el as HTMLElement;
 | 
			
		||||
      if (canvasContainer) {
 | 
			
		||||
        // 获取当前画布的位置和缩放信息
 | 
			
		||||
        const canvasPosition = canvasInstance.getCanvasPosition ? 
 | 
			
		||||
                              canvasInstance.getCanvasPosition() : 
 | 
			
		||||
                              { x: 0, y: 0 };
 | 
			
		||||
        const scale = canvasInstance.getScale ? 
 | 
			
		||||
                     canvasInstance.getScale() : 
 | 
			
		||||
                     1;
 | 
			
		||||
        
 | 
			
		||||
        // 计算可视区域中心点在画布坐标系中的位置
 | 
			
		||||
        const viewportWidth = canvasContainer.clientWidth;
 | 
			
		||||
        const viewportHeight = canvasContainer.clientHeight;
 | 
			
		||||
        
 | 
			
		||||
        // 计算画布中心点的坐标
 | 
			
		||||
        posX = (viewportWidth / 2 - canvasPosition.x) / scale;
 | 
			
		||||
        posY = (viewportHeight / 2 - canvasPosition.y) / scale;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Error getting canvas position:', error);
 | 
			
		||||
    // 使用默认位置
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 添加一些随机偏移,避免元器件重叠
 | 
			
		||||
  const offsetX = Math.floor(Math.random() * 100) - 50;
 | 
			
		||||
  const offsetY = Math.floor(Math.random() * 100) - 50;
 | 
			
		||||
 | 
			
		||||
  const newComponent: ComponentItem = {
 | 
			
		||||
    id: `component-${Date.now()}`,
 | 
			
		||||
    type: componentData.type,
 | 
			
		||||
    name: componentData.name,
 | 
			
		||||
    x: 100, // 或者计算画布中心位置
 | 
			
		||||
    y: 100,
 | 
			
		||||
    x: Math.round(posX + offsetX),
 | 
			
		||||
    y: Math.round(posY + offsetY),
 | 
			
		||||
    props: componentData.props, // 使用从 ComponentSelector 传递的默认属性
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -302,9 +341,22 @@ function updateComponentProp(componentId: string | { id: string; propName: strin
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理连线创建事件
 | 
			
		||||
function handleWireCreated(wireData: any) {
 | 
			
		||||
  console.log('Wire created:', wireData);
 | 
			
		||||
  // 可以在这里添加连线创建的相关逻辑
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理连线删除事件
 | 
			
		||||
function handleWireDeleted(wireId: string) {
 | 
			
		||||
  console.log('Wire deleted:', wireId);
 | 
			
		||||
  // 可以在这里添加连线删除的相关逻辑
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 生命周期钩子 ---
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 无需在这里预加载组件,ComponentSelector 组件会处理这部分逻辑
 | 
			
		||||
  // 初始化画布设置
 | 
			
		||||
  console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user