feat: Enhance equipment components with pin functionality and constraint management
- Added pin support to MechanicalButton, enabling pin-click events and componentId handling. - Updated Pin component to manage constraint states and colors dynamically. - Integrated SMT_LED with pin functionality, allowing LED state to respond to constraints. - Enhanced Wire component to reflect constraint colors and manage wire states based on pin connections. - Introduced wireManager for managing wire states and constraints. - Implemented a constraints store for managing and notifying constraint state changes across components. - Updated component configuration to remove appearance options and clarify constraint descriptions. - Improved ProjectView to handle optional chaining for props and ensure robust data handling. - Initialized constraint communication in main application entry point.
This commit is contained in:
		@@ -8,6 +8,9 @@
 | 
			
		||||
/* eslint-disable */
 | 
			
		||||
// ReSharper disable InconsistentNaming
 | 
			
		||||
 | 
			
		||||
import { batchSetConstraintStates, notifyConstraintChange } from './stores/constraints';
 | 
			
		||||
import type { ConstraintLevel } from './stores/constraints';
 | 
			
		||||
 | 
			
		||||
export class Client {
 | 
			
		||||
    private http: { fetch(url: RequestInfo, init?: RequestInit): Promise<Response> };
 | 
			
		||||
    private baseUrl: string;
 | 
			
		||||
@@ -866,4 +869,57 @@ function throwException(message: string, status: number, response: string, heade
 | 
			
		||||
        throw result;
 | 
			
		||||
    else
 | 
			
		||||
        throw new ApiException(message, status, response, headers, null);
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 约束通信相关方法
 | 
			
		||||
export function receiveConstraintUpdates(constraints: Record<string, ConstraintLevel>) {
 | 
			
		||||
  // 批量更新约束状态
 | 
			
		||||
  batchSetConstraintStates(constraints);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function sendConstraintUpdate(constraint: string, level: ConstraintLevel) {
 | 
			
		||||
  // 向后端发送约束状态变化
 | 
			
		||||
  console.log(`发送约束 ${constraint} 状态变化为 ${level}`);
 | 
			
		||||
  
 | 
			
		||||
  // TODO: 实际的WebSocket或HTTP请求发送约束变化
 | 
			
		||||
  // 例如:
 | 
			
		||||
  // socket.emit('constraintUpdate', { constraint, level });
 | 
			
		||||
  // 或
 | 
			
		||||
  // fetch('/api/constraints', { 
 | 
			
		||||
  //   method: 'POST', 
 | 
			
		||||
  //   body: JSON.stringify({ constraint, level }),
 | 
			
		||||
  //   headers: { 'Content-Type': 'application/json' }
 | 
			
		||||
  // });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 初始化约束通信
 | 
			
		||||
export function initConstraintCommunication() {
 | 
			
		||||
  // 监听服务器发来的约束状态变化
 | 
			
		||||
  // 示例:
 | 
			
		||||
  // socket.on('constraintUpdates', (data) => {
 | 
			
		||||
  //   receiveConstraintUpdates(data);
 | 
			
		||||
  // });
 | 
			
		||||
  
 | 
			
		||||
  // 模拟接收一些初始约束状态
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    receiveConstraintUpdates({
 | 
			
		||||
      'A1': 'high',
 | 
			
		||||
      'A2': 'low',
 | 
			
		||||
      'A3': 'undefined'
 | 
			
		||||
    });
 | 
			
		||||
  }, 1000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 覆盖全局notifyConstraintChange,加入发送逻辑
 | 
			
		||||
const originalNotifyConstraintChange = notifyConstraintChange;
 | 
			
		||||
const wrappedNotifyConstraintChange = (constraint: string, level: ConstraintLevel) => {
 | 
			
		||||
  // 调用原始方法更新本地状态
 | 
			
		||||
  originalNotifyConstraintChange(constraint, level);
 | 
			
		||||
  
 | 
			
		||||
  // 向后端发送更新
 | 
			
		||||
  sendConstraintUpdate(constraint, level);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 替换全局方法
 | 
			
		||||
(window as any).__notifyConstraintChange = notifyConstraintChange;
 | 
			
		||||
(window as any).notifyConstraintChange = wrappedNotifyConstraintChange;
 | 
			
		||||
@@ -1,80 +0,0 @@
 | 
			
		||||
.diagram-canvas {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 4000px;
 | 
			
		||||
  height: 4000px;
 | 
			
		||||
  transform-origin: 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 将网格线应用到容器而不是画布上,确保覆盖整个可视区域 */
 | 
			
		||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
 | 
			
		||||
  background-image: 
 | 
			
		||||
    linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px);
 | 
			
		||||
  background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
 | 
			
		||||
  background-position: 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 为黑暗模式设置不同的网格线颜色 */
 | 
			
		||||
:root[data-theme="dark"] .flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
 | 
			
		||||
  background-image: 
 | 
			
		||||
    linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
 | 
			
		||||
  background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
 | 
			
		||||
  background-position: 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 禁用滚动条 */
 | 
			
		||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto::-webkit-scrollbar {
 | 
			
		||||
  display: none; /* Chrome, Safari, Opera */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
 | 
			
		||||
  -ms-overflow-style: none;  /* IE and Edge */
 | 
			
		||||
  scrollbar-width: none;  /* Firefox */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 元器件容器样式 */
 | 
			
		||||
.component-wrapper {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-block; /* 确保元素宽度基于内容 */
 | 
			
		||||
  max-width: fit-content; /* 强制宽度适应内容 */
 | 
			
		||||
  max-height: fit-content; /* 强制高度适应内容 */
 | 
			
		||||
  overflow: visible; /* 允许内容溢出(用于显示边框) */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 悬停状态 */
 | 
			
		||||
.component-hover::before {
 | 
			
		||||
  content: '';
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: -4px;
 | 
			
		||||
  left: -4px;
 | 
			
		||||
  right: -4px;
 | 
			
		||||
  bottom: -4px;
 | 
			
		||||
  border: 3px dashed #3498db;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  box-sizing: content-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 选中状态 */
 | 
			
		||||
.component-selected::before {
 | 
			
		||||
  content: '';
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: -4px;
 | 
			
		||||
  left: -4px;
 | 
			
		||||
  right: -4px;
 | 
			
		||||
  bottom: -4px;
 | 
			
		||||
  border: 4px dashed #e74c3c;
 | 
			
		||||
  border-color: #e74c3c #f39c12 #3498db #2ecc71;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  box-sizing: content-box;
 | 
			
		||||
}
 | 
			
		||||
@@ -19,13 +19,10 @@
 | 
			
		||||
          :end-y="wire.endY"
 | 
			
		||||
          :stroke-color="wire.color || '#4a5568'"
 | 
			
		||||
          :stroke-width="2"
 | 
			
		||||
          :is-active="wireSelectedId === wire.id"
 | 
			
		||||
          :is-active="false"
 | 
			
		||||
          :start-component-id="wire.startComponentId"
 | 
			
		||||
          :start-pin-label="wire.startPinLabel"
 | 
			
		||||
          :end-component-id="wire.endComponentId"
 | 
			
		||||
          :end-pin-label="wire.endPinLabel"
 | 
			
		||||
          :constraint="wire.constraint"
 | 
			
		||||
          @click="handleWireClick(wire)"
 | 
			
		||||
        />
 | 
			
		||||
        
 | 
			
		||||
        <!-- 正在创建的连线 -->
 | 
			
		||||
@@ -82,6 +79,15 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, reactive, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import WireComponent from '@/components/equipments/Wire.vue';
 | 
			
		||||
import { wires, addWire, deleteWire, updateWiresConstraintByPin, findWiresByPin, isCreatingWire, creatingWireStart, creatingWireStartInfo, mousePosition, resetWireCreation, setWireCreationStart, setMousePosition } from './wireManager';
 | 
			
		||||
import type { WireItem } from './wireManager';
 | 
			
		||||
 | 
			
		||||
// 右键菜单处理函数(如无特殊需求可为空实现)
 | 
			
		||||
function handleContextMenu(e: MouseEvent) {
 | 
			
		||||
  // 可根据需要自定义右键菜单逻辑
 | 
			
		||||
  // 目前只是阻止默认行为
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件接受的属性
 | 
			
		||||
interface ComponentItem {
 | 
			
		||||
@@ -113,7 +119,6 @@ const selectedComponentId = ref<string | null>(null);
 | 
			
		||||
const hoveredComponent = ref<string | null>(null);
 | 
			
		||||
const draggingComponentId = ref<string | null>(null);
 | 
			
		||||
const componentDragOffset = reactive({ x: 0, y: 0 });
 | 
			
		||||
const wireSelectedId = ref<string | null>(null);
 | 
			
		||||
 | 
			
		||||
// 组件引用跟踪
 | 
			
		||||
const componentRefs = ref<Record<string, any>>({});
 | 
			
		||||
@@ -174,25 +179,9 @@ const getComponentDefinition = (type: string) => {
 | 
			
		||||
// 准备组件属性,确保类型正确
 | 
			
		||||
function prepareComponentProps(props: Record<string, any>, componentId?: string): Record<string, any> {
 | 
			
		||||
  const result: Record<string, any> = { ...props };
 | 
			
		||||
  
 | 
			
		||||
  // 添加组件ID属性,用于唯一标识针脚
 | 
			
		||||
  if (componentId) {
 | 
			
		||||
    result.componentId = componentId;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 确保某些属性的类型正确
 | 
			
		||||
  for (const key in result) {
 | 
			
		||||
    let value = result[key];
 | 
			
		||||
    // 只要不是 null/undefined 且不是 string,就强制转字符串
 | 
			
		||||
    if (
 | 
			
		||||
      (key === 'style' || key === 'direction' || key === 'type') &&
 | 
			
		||||
      value != null &&
 | 
			
		||||
      typeof value !== 'string'
 | 
			
		||||
    ) {
 | 
			
		||||
      result[key] = String(value);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -360,6 +349,19 @@ function stopComponentDrag() {
 | 
			
		||||
 | 
			
		||||
// 更新组件属性
 | 
			
		||||
function updateComponentProp(componentId: string, propName: string, value: any) {
 | 
			
		||||
  // 如果是引脚约束变更,自动调用updatePinConstraint
 | 
			
		||||
  if (propName === 'constraint') {
 | 
			
		||||
    // 查找该组件的Pin label(假设label唯一,或可扩展为多Pin)
 | 
			
		||||
    const componentRef = componentRefs.value[componentId];
 | 
			
		||||
    if (componentRef && componentRef.getInfo) {
 | 
			
		||||
      const pinInfo = componentRef.getInfo();
 | 
			
		||||
      if (pinInfo) {
 | 
			
		||||
        updatePinConstraint(componentId, value);
 | 
			
		||||
        return; // 已处理,无需再emit
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // 其它属性正常emit
 | 
			
		||||
  emit('update-component-prop', { id: componentId, propName, value });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -391,94 +393,75 @@ defineExpose({
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// --- 连线状态 ---
 | 
			
		||||
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;
 | 
			
		||||
  strokeWidth?: number;
 | 
			
		||||
  routingMode?: 'auto' | 'orthogonal' | 'direct';
 | 
			
		||||
  showLabel?: boolean;
 | 
			
		||||
// 处理连线创建事件
 | 
			
		||||
function handleWireCreated(wireData: any) {
 | 
			
		||||
  addWire(wireData);
 | 
			
		||||
  emit('wire-created', wireData);
 | 
			
		||||
}
 | 
			
		||||
// 删除连线
 | 
			
		||||
function handleWireDeleted(wireId: string) {
 | 
			
		||||
  deleteWire(wireId);
 | 
			
		||||
  emit('wire-deleted', wireId);
 | 
			
		||||
}
 | 
			
		||||
// 更新Pin的约束时同步Wire
 | 
			
		||||
function updatePinConstraint(componentId: 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) {
 | 
			
		||||
      emit('update-component-prop', { 
 | 
			
		||||
        id: componentId, 
 | 
			
		||||
        propName: 'constraint', 
 | 
			
		||||
        value: constraint 
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // 同步所有相关Wire的constraint
 | 
			
		||||
  updateWiresConstraintByPin(componentId, constraint);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
  setMousePosition(
 | 
			
		||||
    (e.clientX - containerRect.left - position.x) / scale.value,
 | 
			
		||||
    (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})`);
 | 
			
		||||
    
 | 
			
		||||
    // 添加鼠标移动监听
 | 
			
		||||
    setWireCreationStart(pinCanvasX, pinCanvasY, componentId, pinInfo.label, pinInfo.constraint);
 | 
			
		||||
    document.addEventListener('mousemove', onCreatingWireMouseMove);
 | 
			
		||||
      } else {    // 完成连线
 | 
			
		||||
  } else {
 | 
			
		||||
    if (componentId === creatingWireStartInfo.componentId && pinInfo.label === creatingWireStartInfo.pinLabel) {
 | 
			
		||||
      // 如果点了同一个Pin,取消连线
 | 
			
		||||
      cancelWireCreation();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 获取起点和终点的约束
 | 
			
		||||
    const startConstraint = creatingWireStartInfo.constraint || '';
 | 
			
		||||
    const endConstraint = pinInfo.constraint || '';
 | 
			
		||||
@@ -531,15 +514,14 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
 | 
			
		||||
    // 完成连线
 | 
			
		||||
    completeWireCreation(
 | 
			
		||||
      componentId, 
 | 
			
		||||
      pinInfo.label, 
 | 
			
		||||
      finalConstraint,
 | 
			
		||||
      pinInfo
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    // 更新两个Pin的约束
 | 
			
		||||
    updatePinConstraint(creatingWireStartInfo.componentId, creatingWireStartInfo.pinLabel, finalConstraint);
 | 
			
		||||
    updatePinConstraint(componentId, pinInfo.label, finalConstraint);
 | 
			
		||||
    }
 | 
			
		||||
    updatePinConstraint(creatingWireStartInfo.componentId, finalConstraint);
 | 
			
		||||
    updatePinConstraint(componentId, finalConstraint);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生成随机约束名
 | 
			
		||||
@@ -548,31 +530,8 @@ function generateRandomConstraint() {
 | 
			
		||||
  return `$auto_constraint_${randomId}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新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) {
 | 
			
		||||
function completeWireCreation(endComponentId: string, constraint: string, endPinInfo?: any) {
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
@@ -585,11 +544,11 @@ function completeWireCreation(endComponentId: string, endPinLabel: string, const
 | 
			
		||||
  // 尝试使用 getPinPosition 获取精确的针脚位置
 | 
			
		||||
  const endComponentRef = componentRefs.value[endComponentId];
 | 
			
		||||
  if (endComponentRef && endComponentRef.getPinPosition) {
 | 
			
		||||
    const pinPosition = endComponentRef.getPinPosition(endPinLabel);
 | 
			
		||||
    const pinPosition = endComponentRef.getPinPosition(endComponentId);
 | 
			
		||||
    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})`);
 | 
			
		||||
      console.log(`通过 getPinPosition 获取终点针脚 ${endComponentId} 的画布坐标: (${endX}, ${endY})`);
 | 
			
		||||
    } else {    console.warn(`getPinPosition 返回 null,将使用备选方法`);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (endPinInfo && endPinInfo.position) {
 | 
			
		||||
@@ -607,9 +566,7 @@ function completeWireCreation(endComponentId: string, endPinLabel: string, const
 | 
			
		||||
    endX: endX,
 | 
			
		||||
    endY: endY,
 | 
			
		||||
    startComponentId: creatingWireStartInfo.componentId,
 | 
			
		||||
    startPinLabel: creatingWireStartInfo.pinLabel,
 | 
			
		||||
    endComponentId: endComponentId,
 | 
			
		||||
    endPinLabel: endPinLabel,
 | 
			
		||||
    color: '#4a5568',
 | 
			
		||||
    constraint: constraint,
 | 
			
		||||
    routingMode: 'orthogonal',
 | 
			
		||||
@@ -629,7 +586,7 @@ function completeWireCreation(endComponentId: string, endPinLabel: string, const
 | 
			
		||||
 | 
			
		||||
// 取消连线创建
 | 
			
		||||
function cancelWireCreation() {
 | 
			
		||||
  isCreatingWire.value = false;
 | 
			
		||||
  resetWireCreation();
 | 
			
		||||
  document.removeEventListener('mousemove', onCreatingWireMouseMove);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -638,36 +595,51 @@ function onCreatingWireMouseMove(e: MouseEvent) {
 | 
			
		||||
  updateMousePosition(e);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理连线点击
 | 
			
		||||
function handleWireClick(wire: WireItem) {
 | 
			
		||||
  wireSelectedId.value = wire.id;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 根据针脚位置更新连线
 | 
			
		||||
function updateWireWithPinPositions(wire: WireItem) {
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
  // 更新起点
 | 
			
		||||
  if (wire.startComponentId && wire.startPinLabel) {
 | 
			
		||||
  if (wire.startComponentId) {
 | 
			
		||||
    const startComponentRef = componentRefs.value[wire.startComponentId];
 | 
			
		||||
    if (startComponentRef && startComponentRef.getPinPosition) {
 | 
			
		||||
      const pinPosition = startComponentRef.getPinPosition(wire.startPinLabel);
 | 
			
		||||
      const pinPosition = startComponentRef.getPinPosition(wire.startComponentId);
 | 
			
		||||
      if (pinPosition) {
 | 
			
		||||
        wire.startX = (pinPosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
        wire.startY = (pinPosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
      } else {
 | 
			
		||||
        console.warn(`无法获取组件${wire.startComponentId}的针脚${wire.startComponentId}位置`);
 | 
			
		||||
        // 尝试多次获取位置(针对初次渲染可能有延迟的情况)
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          const retryPosition = startComponentRef.getPinPosition(wire.startComponentId);
 | 
			
		||||
          if (retryPosition) {
 | 
			
		||||
            wire.startX = (retryPosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
            wire.startY = (retryPosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
          }
 | 
			
		||||
        }, 100);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 更新终点
 | 
			
		||||
  if (wire.endComponentId && wire.endPinLabel) {
 | 
			
		||||
  if (wire.endComponentId) {
 | 
			
		||||
    const endComponentRef = componentRefs.value[wire.endComponentId];
 | 
			
		||||
    if (endComponentRef && endComponentRef.getPinPosition) {
 | 
			
		||||
      const pinPosition = endComponentRef.getPinPosition(wire.endPinLabel);
 | 
			
		||||
      const pinPosition = endComponentRef.getPinPosition(wire.endComponentId);
 | 
			
		||||
      if (pinPosition) {
 | 
			
		||||
        wire.endX = (pinPosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
        wire.endY = (pinPosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
      } else {
 | 
			
		||||
        console.warn(`无法获取组件${wire.endComponentId}的针脚${wire.endComponentId}位置`);
 | 
			
		||||
        // 尝试多次获取位置(针对初次渲染可能有延迟的情况)
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
          const retryPosition = endComponentRef.getPinPosition(wire.endComponentId);
 | 
			
		||||
          if (retryPosition) {
 | 
			
		||||
            wire.endX = (retryPosition.x - containerRect.left - position.x) / scale.value;
 | 
			
		||||
            wire.endY = (retryPosition.y - containerRect.top - position.y) / scale.value;
 | 
			
		||||
          }
 | 
			
		||||
        }, 100);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -725,16 +697,6 @@ function handleKeyDown(e: KeyboardEvent) {
 | 
			
		||||
    // 取消连线创建
 | 
			
		||||
    cancelWireCreation();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 删除选中连线
 | 
			
		||||
  if (wireSelectedId.value && (e.key === 'Delete' || e.key === 'Backspace')) {
 | 
			
		||||
    const idx = wires.value.findIndex(w => w.id === wireSelectedId.value);
 | 
			
		||||
    if (idx !== -1) {
 | 
			
		||||
      const deletedWire = wires.value.splice(idx, 1)[0];
 | 
			
		||||
      emit('wire-deleted', deletedWire.id);
 | 
			
		||||
      wireSelectedId.value = null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
@@ -757,11 +719,11 @@ onUnmounted(() => {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  background-image: 
 | 
			
		||||
  /* background-image: 
 | 
			
		||||
    linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px);
 | 
			
		||||
    linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px); */
 | 
			
		||||
  background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
 | 
			
		||||
  background-position: 0 0;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
@@ -827,13 +789,13 @@ onUnmounted(() => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 为黑暗模式设置不同的网格线颜色 */
 | 
			
		||||
:root[data-theme="dark"] .diagram-container {
 | 
			
		||||
/* :root[data-theme="dark"] .diagram-container {
 | 
			
		||||
  background-image: 
 | 
			
		||||
    linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
 | 
			
		||||
}
 | 
			
		||||
} */
 | 
			
		||||
 | 
			
		||||
/* 深度选择器 - 默认阻止SVG内部元素的鼠标事件,但允许SVG本身和特定交互元素 */
 | 
			
		||||
.component-wrapper :deep(svg) {
 | 
			
		||||
@@ -860,7 +822,7 @@ onUnmounted(() => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.wire-active {
 | 
			
		||||
  stroke: #ff9800 !important;
 | 
			
		||||
  filter: drop-shadow(0 0 4px #ff9800cc);
 | 
			
		||||
  stroke: #0099ff !important;
 | 
			
		||||
  filter: drop-shadow(0 0 4px #0099ffcc);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -76,13 +76,15 @@
 | 
			
		||||
      pointerEvents: 'auto'
 | 
			
		||||
    }">
 | 
			
		||||
      <Pin
 | 
			
		||||
        ref="pinRef"
 | 
			
		||||
        direction="output"
 | 
			
		||||
        type="digital"  
 | 
			
		||||
        appearance="None"
 | 
			
		||||
        :label="props.label"
 | 
			
		||||
        :constraint="props.constraint"
 | 
			
		||||
        :size="0.8"
 | 
			
		||||
        :componentId="props.componentId"
 | 
			
		||||
        @value-change="handlePinValueChange"
 | 
			
		||||
        @pin-click="handlePinClick"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -91,15 +93,18 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
 | 
			
		||||
import Pin from './Pin.vue';
 | 
			
		||||
import { notifyConstraintChange } from '../../stores/constraints';
 | 
			
		||||
 | 
			
		||||
const pinRef = ref<any>(null);
 | 
			
		||||
 | 
			
		||||
// 从Pin组件继承属性
 | 
			
		||||
interface PinProps {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  componentId?: string; // 添加componentId属性
 | 
			
		||||
  // 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
 | 
			
		||||
  direction?: 'input' | 'output' | 'inout';
 | 
			
		||||
  type?: 'digital' | 'analog';
 | 
			
		||||
  appearance?: 'None' | 'Dip' | 'SMT';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 按钮特有属性
 | 
			
		||||
@@ -118,10 +123,10 @@ const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  buttonText: '',
 | 
			
		||||
  label: 'BTN',
 | 
			
		||||
  constraint: '',
 | 
			
		||||
  componentId: 'button-default', // 添加默认componentId
 | 
			
		||||
  // 这些值会被覆盖,但需要默认值以满足类型要求
 | 
			
		||||
  direction: 'output',
 | 
			
		||||
  type: 'digital',
 | 
			
		||||
  appearance: 'Dip'
 | 
			
		||||
  type: 'digital'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
@@ -142,7 +147,8 @@ const emit = defineEmits([
 | 
			
		||||
  'press',
 | 
			
		||||
  'release',
 | 
			
		||||
  'click',
 | 
			
		||||
  'value-change'
 | 
			
		||||
  'value-change',
 | 
			
		||||
  'pin-click'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 内部状态
 | 
			
		||||
@@ -155,6 +161,11 @@ function handlePinValueChange(value: any) {
 | 
			
		||||
  emit('value-change', value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理Pin点击事件
 | 
			
		||||
function handlePinClick(info: any) {
 | 
			
		||||
  emit('pin-click', info);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 按键状态逻辑 ---
 | 
			
		||||
function toggleButtonState(isPressed: boolean) {
 | 
			
		||||
  isKeyPressed.value = isPressed;
 | 
			
		||||
@@ -163,9 +174,17 @@ function toggleButtonState(isPressed: boolean) {
 | 
			
		||||
  // 发出事件通知父组件
 | 
			
		||||
  if (isPressed) {
 | 
			
		||||
    emit('press');
 | 
			
		||||
    // 如果有约束,通知约束状态变化为高电平
 | 
			
		||||
    if (props.constraint) {
 | 
			
		||||
      notifyConstraintChange(props.constraint, 'high');
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    emit('release');
 | 
			
		||||
    emit('click');
 | 
			
		||||
    // 如果有约束,通知约束状态变化为低电平
 | 
			
		||||
    if (props.constraint) {
 | 
			
		||||
      notifyConstraintChange(props.constraint, 'low');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -196,11 +215,18 @@ defineExpose({
 | 
			
		||||
    // 继承自Pin的属性
 | 
			
		||||
    label: props.label,
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    componentId: props.componentId, // 添加componentId
 | 
			
		||||
    // 固定的Pin属性
 | 
			
		||||
    direction: 'output',
 | 
			
		||||
    type: 'digital',
 | 
			
		||||
    appearance: 'None'
 | 
			
		||||
  })
 | 
			
		||||
    type: 'digital'
 | 
			
		||||
  }),
 | 
			
		||||
  // 代理 getPinPosition 到内部 Pin
 | 
			
		||||
  getPinPosition: (pinLabel: string) => {
 | 
			
		||||
    if (pinRef.value && pinRef.value.getPinPosition) {
 | 
			
		||||
      return pinRef.value.getPinPosition(pinLabel);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,71 +1,35 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <svg 
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
    :width="width" 
 | 
			
		||||
    :height="height" 
 | 
			
		||||
    :width="width"
 | 
			
		||||
    :height="height"
 | 
			
		||||
    :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
 | 
			
		||||
    class="pin-component"
 | 
			
		||||
    :data-component-id="componentId"
 | 
			
		||||
    :data-component-id="props.componentId"
 | 
			
		||||
    :data-pin-label="props.label"
 | 
			
		||||
  >    <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
 | 
			
		||||
      <g v-if="props.appearance === 'None'">
 | 
			
		||||
  >
 | 
			
		||||
    <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
 | 
			
		||||
      <g>
 | 
			
		||||
        <g transform="translate(-12.5, -12.5)">
 | 
			
		||||
          <circle
 | 
			
		||||
            style="fill:#909090"
 | 
			
		||||
            :style="{ fill: pinColor }"
 | 
			
		||||
            cx="12.5"
 | 
			
		||||
            cy="12.5"
 | 
			
		||||
            r="3.75"
 | 
			
		||||
            class="interactive"
 | 
			
		||||
            @click.stop="handlePinClick"
 | 
			
		||||
            :data-pin-element="`${props.componentId}-${props.label}`" />
 | 
			
		||||
        </g>
 | 
			
		||||
      </g>      
 | 
			
		||||
      <g v-else-if="props.appearance === 'Dip'">
 | 
			
		||||
        <!-- 使用inkscape创建的SVG替代原有Dip样式 -->
 | 
			
		||||
        <g transform="translate(-12.5, -12.5)">
 | 
			
		||||
          <rect
 | 
			
		||||
            :style="`fill:${props.type === 'analog' ? '#2a6099' : '#000000'};fill-opacity:0.772973`"
 | 
			
		||||
            width="25"
 | 
			
		||||
            height="25"
 | 
			
		||||
            x="0"
 | 
			
		||||
            y="0"
 | 
			
		||||
            rx="2.5" />
 | 
			
		||||
          <circle
 | 
			
		||||
            style="fill:#ecececc5;fill-opacity:0.772973"
 | 
			
		||||
            cx="12.5"
 | 
			
		||||
            cy="12.5"
 | 
			
		||||
            r="3.75"
 | 
			
		||||
            class="interactive"
 | 
			
		||||
            @click.stop="handlePinClick"
 | 
			
		||||
            :data-pin-element="`${props.componentId}-${props.label}`" />
 | 
			
		||||
          <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'">
 | 
			
		||||
        <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>
 | 
			
		||||
        <circle
 | 
			
		||||
          fill="#ecececc5"
 | 
			
		||||
          cx="10"
 | 
			
		||||
          cy="0"
 | 
			
		||||
          r="3.75"
 | 
			
		||||
          class="interactive"
 | 
			
		||||
          @click.stop="handlePinClick"
 | 
			
		||||
          :data-pin-element="`${props.componentId}-${props.label}`"
 | 
			
		||||
          />
 | 
			
		||||
            :data-pin-element="`${props.componentId}`" />
 | 
			
		||||
        </g>
 | 
			
		||||
      </g>
 | 
			
		||||
    </g>
 | 
			
		||||
  </svg>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed, reactive } from 'vue';
 | 
			
		||||
import { ref, computed, reactive, watch, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import { getConstraintColor, getConstraintState, onConstraintStateChange, notifyConstraintChange } from '../../stores/constraints';
 | 
			
		||||
 | 
			
		||||
// 生成唯一ID
 | 
			
		||||
const uniqueId = `pin-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
@@ -73,7 +37,6 @@ interface Props {
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  direction?: 'input' | 'output' | 'inout';
 | 
			
		||||
  type?: 'digital' | 'analog';
 | 
			
		||||
  appearance?: 'None' | 'Dip' | 'SMT';
 | 
			
		||||
  componentId?: string; // 添加组件ID属性,用于唯一标识
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -83,7 +46,6 @@ const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  constraint: '',
 | 
			
		||||
  direction: 'input',
 | 
			
		||||
  type: 'digital',
 | 
			
		||||
  appearance: 'Dip',
 | 
			
		||||
  componentId: 'pin-default' // 默认ID
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -104,42 +66,64 @@ function handlePinClick(event: MouseEvent) {
 | 
			
		||||
    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
 | 
			
		||||
    },
 | 
			
		||||
    // 获取原始事件
 | 
			
		||||
    position: pinCenter,
 | 
			
		||||
    originalEvent: event
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size);
 | 
			
		||||
const height = computed(() => {
 | 
			
		||||
  if (props.appearance === 'None') return 20 * props.size;
 | 
			
		||||
  if (props.appearance === 'Dip') return 30 * props.size; // 调整Dip样式高度
 | 
			
		||||
  return 60 * props.size;
 | 
			
		||||
});
 | 
			
		||||
const viewBoxWidth = computed(() => props.appearance === 'None' ? 40 : 30);
 | 
			
		||||
const viewBoxHeight = computed(() => {
 | 
			
		||||
  if (props.appearance === 'None') return 20;
 | 
			
		||||
  if (props.appearance === 'Dip') return 30; // 调整Dip样式视图高度
 | 
			
		||||
  return 60;
 | 
			
		||||
});
 | 
			
		||||
const width = computed(() => 40 * props.size);
 | 
			
		||||
const height = computed(() => 20 * props.size);
 | 
			
		||||
const viewBoxWidth = computed(() => 40);
 | 
			
		||||
const viewBoxHeight = computed(() => 20);
 | 
			
		||||
 | 
			
		||||
const getColorByType = computed(() => {
 | 
			
		||||
  return props.type === 'analog' ? '#2a6099' : '#444';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 根据约束电平状态计算引脚颜色
 | 
			
		||||
const pinColor = computed(() => {
 | 
			
		||||
  return getConstraintColor(props.constraint) || getColorByType.value;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 监听约束状态变化
 | 
			
		||||
let unsubscribe: (() => void) | null = null;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 监听约束状态变化
 | 
			
		||||
  if (props.constraint) {
 | 
			
		||||
    unsubscribe = onConstraintStateChange((constraint, level) => {
 | 
			
		||||
      if (constraint === props.constraint) {
 | 
			
		||||
        emit('value-change', { constraint, level });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  if (unsubscribe) {
 | 
			
		||||
    unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(() => props.constraint, (newConstraint, oldConstraint) => {
 | 
			
		||||
  if (unsubscribe) {
 | 
			
		||||
    unsubscribe();
 | 
			
		||||
    unsubscribe = null;
 | 
			
		||||
  }
 | 
			
		||||
  if (newConstraint) {
 | 
			
		||||
    unsubscribe = onConstraintStateChange((constraint, level) => {
 | 
			
		||||
      if (constraint === newConstraint) {
 | 
			
		||||
        emit('value-change', { constraint, level });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function updateAnalogValue(value: number) {
 | 
			
		||||
  if (props.type !== 'analog') return;
 | 
			
		||||
  analogValue.value = Math.max(0, Math.min(1, value));
 | 
			
		||||
@@ -158,38 +142,39 @@ defineExpose({
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    direction: props.direction,
 | 
			
		||||
    type: props.type,
 | 
			
		||||
    appearance: props.appearance
 | 
			
		||||
  }),  // 添加获取针脚位置的方法
 | 
			
		||||
  getPinPosition: (pinLabel: string) => {
 | 
			
		||||
    // 在 Pin 组件中,只有一个针脚,所以直接检查标签是否匹配
 | 
			
		||||
    if (pinLabel !== props.label) return null;
 | 
			
		||||
    
 | 
			
		||||
    // 使用组件ID和针脚标签的组合作为唯一标识符
 | 
			
		||||
    const selector = `[data-pin-element="${props.componentId}-${props.label}"]`;
 | 
			
		||||
    
 | 
			
		||||
    const pinElement = document.querySelector(selector) as SVGElement;
 | 
			
		||||
    if (pinElement) {
 | 
			
		||||
      // 获取针脚元素的位置
 | 
			
		||||
      const rect = pinElement.getBoundingClientRect();
 | 
			
		||||
    componentId: props.componentId
 | 
			
		||||
  }),
 | 
			
		||||
  getPinPosition: (componentId: string) => {
 | 
			
		||||
    if (componentId !== props.componentId) return null;
 | 
			
		||||
    console.log('getPinPosition', componentId, props.componentId);
 | 
			
		||||
    const uniqueSelector = `[data-pin-element="${props.componentId}"]`;
 | 
			
		||||
    console.log('uniqueSelector', uniqueSelector);
 | 
			
		||||
    const pinElements = document.querySelectorAll(uniqueSelector);
 | 
			
		||||
    console.log('pinElements', pinElements);
 | 
			
		||||
    if (pinElements.length === 0) return null;
 | 
			
		||||
    if (pinElements.length === 1) {
 | 
			
		||||
      const rect = pinElements[0].getBoundingClientRect();
 | 
			
		||||
      return {
 | 
			
		||||
        x: rect.left + rect.width / 2,
 | 
			
		||||
        y: rect.top + rect.height / 2
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 如果找不到特定元素,使用SVG元素位置
 | 
			
		||||
    const svgSelector = `svg.pin-component[data-component-id="${props.componentId}"][data-pin-label="${props.label}"]`;
 | 
			
		||||
    
 | 
			
		||||
    const svgElement = document.querySelector(svgSelector) as SVGElement;
 | 
			
		||||
    if (!svgElement) return null;
 | 
			
		||||
    
 | 
			
		||||
    const svgRect = svgElement.getBoundingClientRect();
 | 
			
		||||
    
 | 
			
		||||
    // 根据针脚类型和方向计算相对位置
 | 
			
		||||
    let pinX = svgRect.left + svgRect.width / 2;
 | 
			
		||||
    let pinY = svgRect.top + svgRect.height / 2;
 | 
			
		||||
    
 | 
			
		||||
    return { x: pinX, y: pinY };
 | 
			
		||||
    for (const pinElement of pinElements) {
 | 
			
		||||
      let parentSvg = pinElement.closest('svg.pin-component');
 | 
			
		||||
      if (!parentSvg) continue;
 | 
			
		||||
      if (parentSvg.getAttribute('data-component-id') === props.componentId) {
 | 
			
		||||
        const rect = pinElement.getBoundingClientRect();
 | 
			
		||||
        return {
 | 
			
		||||
          x: rect.left + rect.width / 2,
 | 
			
		||||
          y: rect.top + rect.height / 2
 | 
			
		||||
        };
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const rect = pinElements[0].getBoundingClientRect();
 | 
			
		||||
    return {
 | 
			
		||||
      x: rect.left + rect.width / 2,
 | 
			
		||||
      y: rect.top + rect.height / 2
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -14,16 +14,15 @@
 | 
			
		||||
      <rect width="90" height="50" x="5" y="5" fill="#222" rx="3" ry="3" />
 | 
			
		||||
      
 | 
			
		||||
      <!-- LED 发光部分 -->
 | 
			
		||||
      <rect 
 | 
			
		||||
      <rect
 | 
			
		||||
        width="70" 
 | 
			
		||||
        height="30" 
 | 
			
		||||
        x="15" 
 | 
			
		||||
        y="15" 
 | 
			
		||||
        :fill="ledColor" 
 | 
			
		||||
        :style="{ opacity: isOn ? brightness/100 : 0.2 }" 
 | 
			
		||||
        :style="{ opacity: isOn ? 1 : 0.2 }" 
 | 
			
		||||
        rx="15" 
 | 
			
		||||
        ry="15"
 | 
			
		||||
        @click="toggleLed"
 | 
			
		||||
        class="interactive"
 | 
			
		||||
      />
 | 
			
		||||
      
 | 
			
		||||
@@ -35,46 +34,64 @@
 | 
			
		||||
        x="12" 
 | 
			
		||||
        y="12" 
 | 
			
		||||
        :fill="ledColor" 
 | 
			
		||||
        :style="{ opacity: brightness/100 * 0.3 }" 
 | 
			
		||||
        :style="{ opacity: 0.3 }" 
 | 
			
		||||
        rx="18" 
 | 
			
		||||
        ry="18"
 | 
			
		||||
        filter="blur(5px)"
 | 
			
		||||
        class="glow"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
    <!-- 新增:数字输入引脚Pin,放在LED左侧居中 -->
 | 
			
		||||
    <div style="position:absolute;left:-18px;top:50%;transform:translateY(-50%);">
 | 
			
		||||
      <Pin
 | 
			
		||||
        ref="pinRef"
 | 
			
		||||
        v-bind="props"
 | 
			
		||||
        @pin-click="$emit('pin-click', $event)"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed, watch } from 'vue';
 | 
			
		||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
 | 
			
		||||
import Pin from './Pin.vue';
 | 
			
		||||
 | 
			
		||||
// LED特有属性
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  initialOn?: boolean;
 | 
			
		||||
  brightness?: number;
 | 
			
		||||
// --- 关键:暴露getPinPosition,代理到内部Pin ---
 | 
			
		||||
const pinRef = ref<any>(null);
 | 
			
		||||
 | 
			
		||||
// 从Pin组件继承属性
 | 
			
		||||
interface PinProps {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  componentId?: string; // 添加componentId属性
 | 
			
		||||
  // 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
 | 
			
		||||
  direction?: 'input' | 'output' | 'inout';
 | 
			
		||||
  type?: 'digital' | 'analog';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组件属性定义
 | 
			
		||||
// LED特有属性
 | 
			
		||||
interface LEDProps {
 | 
			
		||||
  size?: number;
 | 
			
		||||
  color?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组合两个接口
 | 
			
		||||
interface Props extends PinProps, LEDProps {}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1,
 | 
			
		||||
  color: 'red',
 | 
			
		||||
  initialOn: false,
 | 
			
		||||
  brightness: 80,  // 亮度默认为80%
 | 
			
		||||
  constraint: ''
 | 
			
		||||
  constraint: '',
 | 
			
		||||
  label: 'LED',
 | 
			
		||||
  componentId: ''
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 100 * props.size);
 | 
			
		||||
const height = computed(() => 60 * props.size);
 | 
			
		||||
 | 
			
		||||
// 内部状态
 | 
			
		||||
const isOn = ref(props.initialOn);
 | 
			
		||||
const brightness = ref(props.brightness);
 | 
			
		||||
const isOn = ref(false);
 | 
			
		||||
 | 
			
		||||
// LED 颜色映射表
 | 
			
		||||
const colorMap: Record<string, string> = {
 | 
			
		||||
  'red': '#ff3333',
 | 
			
		||||
  'green': '#33ff33',
 | 
			
		||||
@@ -85,70 +102,64 @@ const colorMap: Record<string, string> = {
 | 
			
		||||
  'purple': '#9933ff'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 计算实际LED颜色
 | 
			
		||||
const ledColor = computed(() => {
 | 
			
		||||
  return colorMap[props.color.toLowerCase()] || props.color;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
  'toggle',
 | 
			
		||||
  'brightness-change',
 | 
			
		||||
  'value-change'
 | 
			
		||||
]);
 | 
			
		||||
// 监听约束状态变化
 | 
			
		||||
let unsubscribe: (() => void) | null = null;
 | 
			
		||||
 | 
			
		||||
// 手动切换LED状态
 | 
			
		||||
function toggleLed() {
 | 
			
		||||
  isOn.value = !isOn.value;
 | 
			
		||||
  emit('toggle', isOn.value);
 | 
			
		||||
  emit('value-change', {
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    brightness: brightness.value
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 设置亮度
 | 
			
		||||
function setBrightness(value: number) {
 | 
			
		||||
  // 限制亮度值在0-100范围内
 | 
			
		||||
  brightness.value = Math.max(0, Math.min(100, value));
 | 
			
		||||
  emit('brightness-change', brightness.value);
 | 
			
		||||
  emit('value-change', {
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    brightness: brightness.value
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 手动设置LED开关状态
 | 
			
		||||
function setLedState(on: boolean) {
 | 
			
		||||
  isOn.value = on;
 | 
			
		||||
  emit('toggle', isOn.value);
 | 
			
		||||
  emit('value-change', {
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    brightness: brightness.value
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听props变化
 | 
			
		||||
watch(() => props.brightness, (newVal) => {
 | 
			
		||||
  brightness.value = newVal;
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (props.constraint) {
 | 
			
		||||
    unsubscribe = onConstraintStateChange((constraint, level) => {
 | 
			
		||||
      if (constraint === props.constraint) {
 | 
			
		||||
        isOn.value = (level === 'high');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    // 初始化LED状态
 | 
			
		||||
    const currentState = getConstraintState(props.constraint);
 | 
			
		||||
    isOn.value = (currentState === 'high');
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(() => props.initialOn, (newVal) => {
 | 
			
		||||
  isOn.value = newVal;
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  if (unsubscribe) {
 | 
			
		||||
    unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(() => props.constraint, (newConstraint) => {
 | 
			
		||||
  if (unsubscribe) {
 | 
			
		||||
    unsubscribe();
 | 
			
		||||
    unsubscribe = null;
 | 
			
		||||
  }
 | 
			
		||||
  if (newConstraint) {
 | 
			
		||||
    unsubscribe = onConstraintStateChange((constraint, level) => {
 | 
			
		||||
      if (constraint === newConstraint) {
 | 
			
		||||
        isOn.value = (level === 'high');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    // 初始化LED状态
 | 
			
		||||
    const currentState = getConstraintState(newConstraint);
 | 
			
		||||
    isOn.value = (currentState === 'high');
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 向外暴露方法
 | 
			
		||||
defineExpose({
 | 
			
		||||
  toggleLed,
 | 
			
		||||
  setBrightness,
 | 
			
		||||
  setLedState,
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
    // LED特有属性
 | 
			
		||||
    color: props.color,
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    brightness: brightness.value,
 | 
			
		||||
    constraint: props.constraint
 | 
			
		||||
  })
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    componentId: props.componentId,
 | 
			
		||||
    direction: 'input',
 | 
			
		||||
    type: 'digital'
 | 
			
		||||
  }),
 | 
			
		||||
  getPinPosition: (componentId: string) => {
 | 
			
		||||
    if (pinRef.value && pinRef.value.getPinPosition) {
 | 
			
		||||
      return pinRef.value.getPinPosition(componentId);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,8 @@
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed, defineEmits, reactive } from 'vue';
 | 
			
		||||
import { computed, defineEmits, reactive, watch, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import { getConstraintColor, getConstraintState, onConstraintStateChange } from '../../stores/constraints';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  id: string;
 | 
			
		||||
@@ -53,7 +54,17 @@ const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  constraint: ''
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const computedStroke = computed(() => props.isActive ? '#ff9800' : props.strokeColor);
 | 
			
		||||
// 响应约束状态变化的颜色
 | 
			
		||||
const constraintColor = computed(() => {
 | 
			
		||||
  if (!props.constraint) return props.strokeColor;
 | 
			
		||||
  return getConstraintColor(props.constraint);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际使用的颜色:isActive优先,其次是constraint电平颜色,最后是默认色
 | 
			
		||||
const computedStroke = computed(() => {
 | 
			
		||||
  if (props.isActive) return '#ff9800';
 | 
			
		||||
  return constraintColor.value || props.strokeColor;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits(['click', 'update:active', 'update:position']);
 | 
			
		||||
 | 
			
		||||
@@ -95,6 +106,44 @@ function calculateOrthogonalPath(startX: number, startY: number, endX: number, e
 | 
			
		||||
    return `M ${startX} ${startY} L ${startX} ${middleY} L ${endX} ${middleY} L ${endX} ${endY}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听约束状态变化
 | 
			
		||||
let unsubscribe: (() => void) | null = null;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 监听约束状态变化
 | 
			
		||||
  if (props.constraint) {
 | 
			
		||||
    unsubscribe = onConstraintStateChange((constraint, level) => {
 | 
			
		||||
      if (constraint === props.constraint) {
 | 
			
		||||
        // 约束状态变化,触发重新渲染
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  // 清理监听
 | 
			
		||||
  if (unsubscribe) {
 | 
			
		||||
    unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 监听约束属性变化
 | 
			
		||||
watch(() => props.constraint, (newConstraint, oldConstraint) => {
 | 
			
		||||
  if (unsubscribe) {
 | 
			
		||||
    unsubscribe();
 | 
			
		||||
    unsubscribe = null;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (newConstraint) {
 | 
			
		||||
    unsubscribe = onConstraintStateChange((constraint, level) => {
 | 
			
		||||
      if (constraint === newConstraint) {
 | 
			
		||||
        // 约束状态变化,触发重新渲染
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 暴露方法,用于获取这条连线的信息
 | 
			
		||||
defineExpose({  id: props.id,
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
 
 | 
			
		||||
@@ -154,18 +154,6 @@ const componentConfigs: Record<string, ComponentConfig> = {
 | 
			
		||||
          { value: 'analog', label: 'analog' }
 | 
			
		||||
        ],
 | 
			
		||||
        description: '引脚的模数特性,数字或模拟'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'appearance',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        label: '引脚样式',
 | 
			
		||||
        default: 'Dip',
 | 
			
		||||
        options: [
 | 
			
		||||
          { value: 'None', label: 'None' },
 | 
			
		||||
          { value: 'Dip', label: 'Dip' },
 | 
			
		||||
          { value: 'SMT', label: 'SMT' }
 | 
			
		||||
        ],
 | 
			
		||||
        description: '引脚的外观样式,不影响功能'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
@@ -313,9 +301,9 @@ const componentConfigs: Record<string, ComponentConfig> = {
 | 
			
		||||
      {
 | 
			
		||||
        name: 'constraint',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        label: '连接约束',
 | 
			
		||||
        label: '引脚约束',
 | 
			
		||||
        default: '',
 | 
			
		||||
        description: '相同约束字符串的组件将被视为有电气连接'
 | 
			
		||||
        description: '相同约束字符串的引脚将被视为有电气连接'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
@@ -341,7 +329,7 @@ const componentConfigs: Record<string, ComponentConfig> = {
 | 
			
		||||
        default: '#4a5568',
 | 
			
		||||
        description: '线条颜色,使用CSS颜色值'
 | 
			
		||||
      },
 | 
			
		||||
      { 
 | 
			
		||||
      {
 | 
			
		||||
        name: 'strokeWidth', 
 | 
			
		||||
        type: 'number', 
 | 
			
		||||
        label: '线条宽度', 
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										86
									
								
								src/components/wireManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/components/wireManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
			
		||||
import { ref, reactive } from 'vue';
 | 
			
		||||
 | 
			
		||||
export interface WireItem {
 | 
			
		||||
  id: string;
 | 
			
		||||
  startX: number;
 | 
			
		||||
  startY: number;
 | 
			
		||||
  endX: number;
 | 
			
		||||
  endY: number;
 | 
			
		||||
  startComponentId: string;
 | 
			
		||||
  endComponentId?: string;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  isActive?: boolean;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  strokeWidth?: number;
 | 
			
		||||
  routingMode?: 'auto' | 'orthogonal' | 'direct';
 | 
			
		||||
  showLabel?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 全局wires状态
 | 
			
		||||
export const wires = ref<WireItem[]>([]);
 | 
			
		||||
 | 
			
		||||
// 添加新连线
 | 
			
		||||
export function addWire(wire: WireItem) {
 | 
			
		||||
  wires.value.push(wire);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除连线
 | 
			
		||||
export function deleteWire(wireId: string) {
 | 
			
		||||
  const idx = wires.value.findIndex(w => w.id === wireId);
 | 
			
		||||
  if (idx !== -1) wires.value.splice(idx, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 批量更新与某个引脚相关的所有Wire的约束
 | 
			
		||||
export function updateWiresConstraintByPin(componentId: string, newConstraint: string) {
 | 
			
		||||
  wires.value.forEach(wire => {
 | 
			
		||||
    if (
 | 
			
		||||
      (wire.startComponentId === componentId) ||
 | 
			
		||||
      (wire.endComponentId === componentId)
 | 
			
		||||
    ) {
 | 
			
		||||
      wire.constraint = newConstraint;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 查找与某个引脚相关的所有Wire
 | 
			
		||||
export function findWiresByPin(componentId: string) {
 | 
			
		||||
  return wires.value.filter(wire =>
 | 
			
		||||
    (wire.startComponentId === componentId) ||
 | 
			
		||||
    (wire.endComponentId === componentId)
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 临时连线相关状态
 | 
			
		||||
export const isCreatingWire = ref(false);
 | 
			
		||||
export const creatingWireStart = reactive({ x: 0, y: 0 });
 | 
			
		||||
export const creatingWireStartInfo = reactive({
 | 
			
		||||
  componentId: '',
 | 
			
		||||
  pinLabel: '',
 | 
			
		||||
  constraint: ''
 | 
			
		||||
});
 | 
			
		||||
export const mousePosition = reactive({ x: 0, y: 0 });
 | 
			
		||||
 | 
			
		||||
export function resetWireCreation() {
 | 
			
		||||
  isCreatingWire.value = false;
 | 
			
		||||
  creatingWireStart.x = 0;
 | 
			
		||||
  creatingWireStart.y = 0;
 | 
			
		||||
  creatingWireStartInfo.componentId = '';
 | 
			
		||||
  creatingWireStartInfo.pinLabel = '';
 | 
			
		||||
  creatingWireStartInfo.constraint = '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setWireCreationStart(x: number, y: number, componentId: string, pinLabel: string, constraint: string) {
 | 
			
		||||
  isCreatingWire.value = true;
 | 
			
		||||
  creatingWireStart.x = x;
 | 
			
		||||
  creatingWireStart.y = y;
 | 
			
		||||
  creatingWireStartInfo.componentId = componentId;
 | 
			
		||||
  creatingWireStartInfo.pinLabel = pinLabel;
 | 
			
		||||
  creatingWireStartInfo.constraint = constraint;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setMousePosition(x: number, y: number) {
 | 
			
		||||
  mousePosition.x = x;
 | 
			
		||||
  mousePosition.y = y;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 其它Wire相关操作可继续扩展...
 | 
			
		||||
@@ -5,6 +5,10 @@ import { createPinia } from 'pinia'
 | 
			
		||||
 | 
			
		||||
import App from '@/App.vue'
 | 
			
		||||
import router from './router'
 | 
			
		||||
import { initConstraintCommunication } from './APIClient'
 | 
			
		||||
 | 
			
		||||
const app = createApp(App).use(router).use(createPinia()).mount('#app')
 | 
			
		||||
 | 
			
		||||
// 初始化约束通信
 | 
			
		||||
initConstraintCommunication();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										82
									
								
								src/stores/constraints.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								src/stores/constraints.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
import { ref, reactive } from 'vue';
 | 
			
		||||
 | 
			
		||||
// 约束电平状态类型
 | 
			
		||||
export type ConstraintLevel = 'high' | 'low' | 'undefined';
 | 
			
		||||
 | 
			
		||||
// 约束状态存储
 | 
			
		||||
const constraintStates = reactive<Record<string, ConstraintLevel>>({});
 | 
			
		||||
 | 
			
		||||
// 约束颜色映射
 | 
			
		||||
export const constraintColors = {
 | 
			
		||||
  high: '#ff3333', // 高电平为红色
 | 
			
		||||
  low: '#3333ff',  // 低电平为蓝色
 | 
			
		||||
  undefined: '#999999' // 未定义为灰色
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 获取约束状态
 | 
			
		||||
export function getConstraintState(constraint: string): ConstraintLevel {
 | 
			
		||||
  if (!constraint) return 'undefined';
 | 
			
		||||
  return constraintStates[constraint] || 'undefined';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 设置约束状态
 | 
			
		||||
export function setConstraintState(constraint: string, level: ConstraintLevel) {
 | 
			
		||||
  if (!constraint) return;
 | 
			
		||||
  constraintStates[constraint] = level;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 批量设置约束状态
 | 
			
		||||
export function batchSetConstraintStates(states: Record<string, ConstraintLevel>) {
 | 
			
		||||
  // 收集发生变化的约束
 | 
			
		||||
  const changedConstraints: [string, ConstraintLevel][] = [];
 | 
			
		||||
  
 | 
			
		||||
  // 更新状态并收集变化
 | 
			
		||||
  Object.entries(states).forEach(([constraint, level]) => {
 | 
			
		||||
    if (constraintStates[constraint] !== level) {
 | 
			
		||||
      constraintStates[constraint] = level;
 | 
			
		||||
      changedConstraints.push([constraint, level]);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // 通知所有变化
 | 
			
		||||
  changedConstraints.forEach(([constraint, level]) => {
 | 
			
		||||
    stateChangeCallbacks.forEach(callback => callback(constraint, level));
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取约束对应的颜色
 | 
			
		||||
export function getConstraintColor(constraint: string): string {
 | 
			
		||||
  const state = getConstraintState(constraint);
 | 
			
		||||
  return constraintColors[state];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 清除所有约束状态
 | 
			
		||||
export function clearAllConstraintStates() {
 | 
			
		||||
  Object.keys(constraintStates).forEach(key => {
 | 
			
		||||
    delete constraintStates[key];
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取所有约束状态
 | 
			
		||||
export function getAllConstraintStates(): Record<string, ConstraintLevel> {
 | 
			
		||||
  return { ...constraintStates };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 注册约束状态变化回调
 | 
			
		||||
const stateChangeCallbacks: ((constraint: string, level: ConstraintLevel) => void)[] = [];
 | 
			
		||||
 | 
			
		||||
export function onConstraintStateChange(callback: (constraint: string, level: ConstraintLevel) => void) {
 | 
			
		||||
  stateChangeCallbacks.push(callback);
 | 
			
		||||
  return () => {
 | 
			
		||||
    const index = stateChangeCallbacks.indexOf(callback);
 | 
			
		||||
    if (index > -1) {
 | 
			
		||||
      stateChangeCallbacks.splice(index, 1);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 触发约束变化
 | 
			
		||||
export function notifyConstraintChange(constraint: string, level: ConstraintLevel) {
 | 
			
		||||
  setConstraintState(constraint, level);
 | 
			
		||||
  stateChangeCallbacks.forEach(callback => callback(constraint, level));
 | 
			
		||||
}
 | 
			
		||||
@@ -50,7 +50,7 @@
 | 
			
		||||
                type="text"
 | 
			
		||||
                :placeholder="prop.label || prop.name"
 | 
			
		||||
                class="input input-bordered input-sm w-full"
 | 
			
		||||
                :value="selectedComponentData.props[prop.name]"
 | 
			
		||||
                :value="selectedComponentData.props?.[prop.name]"
 | 
			
		||||
                @input="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).value)"
 | 
			
		||||
              />
 | 
			
		||||
              <input
 | 
			
		||||
@@ -58,14 +58,14 @@
 | 
			
		||||
                type="number"
 | 
			
		||||
                :placeholder="prop.label || prop.name"
 | 
			
		||||
                class="input input-bordered input-sm w-full"
 | 
			
		||||
                :value="selectedComponentData.props[prop.name]"
 | 
			
		||||
                :value="selectedComponentData.props?.[prop.name]"
 | 
			
		||||
                @input="updateComponentProp(selectedComponentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
 | 
			
		||||
              />              <!-- 可以为 boolean 添加 checkbox,为 color 添加 color picker 等 -->
 | 
			
		||||
               <div v-else-if="prop.type === 'boolean'" class="flex items-center">
 | 
			
		||||
                 <input
 | 
			
		||||
                    type="checkbox"
 | 
			
		||||
                    class="checkbox checkbox-sm mr-2"
 | 
			
		||||
                    :checked="selectedComponentData.props[prop.name]"
 | 
			
		||||
                    :checked="selectedComponentData.props?.[prop.name]"
 | 
			
		||||
                    @change="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
 | 
			
		||||
                  />
 | 
			
		||||
                  <span>{{ prop.label || prop.name }}</span>
 | 
			
		||||
@@ -73,12 +73,12 @@
 | 
			
		||||
              <select
 | 
			
		||||
                v-else-if="prop.type === 'select' && prop.options"
 | 
			
		||||
                class="select select-bordered select-sm w-full"
 | 
			
		||||
                :value="selectedComponentData.props[prop.name]"
 | 
			
		||||
                :value="selectedComponentData.props?.[prop.name]"
 | 
			
		||||
                @change="(event) => {
 | 
			
		||||
                  const selectElement = event.target as HTMLSelectElement;
 | 
			
		||||
                  const value = selectElement.value;
 | 
			
		||||
                  console.log('选择的值:', value, '类型:', typeof value);
 | 
			
		||||
                  updateComponentProp(selectedComponentData.id, prop.name, value);
 | 
			
		||||
                  if (selectedComponentData) {updateComponentProp(selectedComponentData.id, prop.name, value);}
 | 
			
		||||
                }"
 | 
			
		||||
              >
 | 
			
		||||
                <option v-for="option in prop.options" :key="option.value" :value="option.value">
 | 
			
		||||
@@ -143,6 +143,7 @@ interface ComponentModule {
 | 
			
		||||
      type: string;
 | 
			
		||||
      label?: string;
 | 
			
		||||
      default: any;
 | 
			
		||||
      options?: Array<{ value: any; label: string }>; // 添加 options 字段用于 select 类型
 | 
			
		||||
    }>;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user