Refactor component configuration and diagram management
- Removed the component configuration from `componentConfig.ts` to streamline the codebase. - Introduced a new `diagram.json` file to define the initial structure for diagrams. - Created a `diagramManager.ts` to handle diagram data, including loading, saving, and validating diagram structures. - Updated `ProjectView.vue` to integrate the new diagram management system, including handling component selection and property updates. - Enhanced the component property management to support dynamic attributes and improved error handling. - Added functions for managing connections between components within the diagram.
This commit is contained in:
		@@ -101,7 +101,6 @@
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed, shallowRef, onMounted } from 'vue';
 | 
			
		||||
import { getComponentConfig } from '@/components/equipments/componentConfig';
 | 
			
		||||
 | 
			
		||||
// Props 定义
 | 
			
		||||
interface Props {
 | 
			
		||||
@@ -208,18 +207,22 @@ function closeMenu() {
 | 
			
		||||
 | 
			
		||||
// 添加新元器件
 | 
			
		||||
async function addComponent(componentTemplate: { type: string; name: string }) {
 | 
			
		||||
  // 先从配置文件中获取默认属性
 | 
			
		||||
  const config = getComponentConfig(componentTemplate.type);
 | 
			
		||||
  const defaultProps: Record<string, any> = {};
 | 
			
		||||
  
 | 
			
		||||
  if (config && config.props) {
 | 
			
		||||
    config.props.forEach(prop => {
 | 
			
		||||
      defaultProps[prop.name] = prop.default;
 | 
			
		||||
    });
 | 
			
		||||
  // 先加载组件模块
 | 
			
		||||
  const moduleRef = await loadComponentModule(componentTemplate.type);
 | 
			
		||||
  let defaultProps: Record<string, any> = {};
 | 
			
		||||
 | 
			
		||||
  // 尝试直接调用组件导出的getDefaultProps方法
 | 
			
		||||
  if(moduleRef){
 | 
			
		||||
    if (typeof moduleRef.getDefaultProps === 'function') {
 | 
			
		||||
      defaultProps = moduleRef.getDefaultProps();
 | 
			
		||||
      console.log(`Got default props from ${componentTemplate.type}:`, defaultProps);
 | 
			
		||||
    } else {
 | 
			
		||||
      // 回退到配置文件
 | 
			
		||||
      console.log(`No getDefaultProps found for ${componentTemplate.type}`);
 | 
			
		||||
    }
 | 
			
		||||
  } else{
 | 
			
		||||
    console.log(`Failed to load module for ${componentTemplate.type}`);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 再加载组件模块以便后续使用
 | 
			
		||||
  await loadComponentModule(componentTemplate.type);
 | 
			
		||||
 | 
			
		||||
  // 发送添加组件事件给父组件
 | 
			
		||||
  emit('add-component', {
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										8
									
								
								src/components/diagram.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/components/diagram.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "version": 1,
 | 
			
		||||
  "author": "admin",
 | 
			
		||||
  "editor": "me",
 | 
			
		||||
  "parts": [
 | 
			
		||||
  ],  "connections": [
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										309
									
								
								src/components/diagramManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										309
									
								
								src/components/diagramManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,309 @@
 | 
			
		||||
// 定义 diagram.json 的类型结构
 | 
			
		||||
export interface DiagramData {
 | 
			
		||||
  version: number;
 | 
			
		||||
  author: string;
 | 
			
		||||
  editor: string;
 | 
			
		||||
  parts: DiagramPart[];
 | 
			
		||||
  connections: ConnectionArray[];
 | 
			
		||||
  exportTime?: string; // 导出时的时间戳
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组件部分的类型定义
 | 
			
		||||
export interface DiagramPart {
 | 
			
		||||
  id: string;
 | 
			
		||||
  type: string;
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  attrs: Record<string, any>;
 | 
			
		||||
  rotate: number;
 | 
			
		||||
  group: string;
 | 
			
		||||
  positionlock: boolean;
 | 
			
		||||
  hide: boolean;
 | 
			
		||||
  isOn: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 连接类型定义 - 使用元组类型表示四元素数组
 | 
			
		||||
export type ConnectionArray = [string, string, number, string[]];
 | 
			
		||||
 | 
			
		||||
// 解析连接字符串为组件ID和引脚ID
 | 
			
		||||
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
 | 
			
		||||
  const [componentId, pinId] = connectionPin.split(':');
 | 
			
		||||
  return { componentId, pinId };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 将连接数组转换为适用于渲染的格式
 | 
			
		||||
export function connectionArrayToWireItem(
 | 
			
		||||
  connection: ConnectionArray, 
 | 
			
		||||
  index: number, 
 | 
			
		||||
  startPos = { x: 0, y: 0 }, 
 | 
			
		||||
  endPos = { x: 0, y: 0 }
 | 
			
		||||
): WireItem {
 | 
			
		||||
  const [startPinStr, endPinStr, width, path] = connection;
 | 
			
		||||
  const { componentId: startComponentId, pinId: startPinId } = parseConnectionPin(startPinStr);
 | 
			
		||||
  const { componentId: endComponentId, pinId: endPinId } = parseConnectionPin(endPinStr);
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
    id: `wire-${index}`,
 | 
			
		||||
    startX: startPos.x,
 | 
			
		||||
    startY: startPos.y,
 | 
			
		||||
    endX: endPos.x, 
 | 
			
		||||
    endY: endPos.y,
 | 
			
		||||
    startComponentId,
 | 
			
		||||
    startPinId,
 | 
			
		||||
    endComponentId,
 | 
			
		||||
    endPinId,
 | 
			
		||||
    strokeWidth: width,
 | 
			
		||||
    color: '#4a5568', // 默认颜色
 | 
			
		||||
    routingMode: 'path',
 | 
			
		||||
    pathCommands: path,
 | 
			
		||||
    showLabel: false
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WireItem 接口定义
 | 
			
		||||
export interface WireItem {
 | 
			
		||||
  id: string;
 | 
			
		||||
  startX: number;
 | 
			
		||||
  startY: number;
 | 
			
		||||
  endX: number;
 | 
			
		||||
  endY: number;
 | 
			
		||||
  startComponentId: string;
 | 
			
		||||
  startPinId?: string;
 | 
			
		||||
  endComponentId: string;
 | 
			
		||||
  endPinId?: string;
 | 
			
		||||
  strokeWidth: number;
 | 
			
		||||
  color: string;
 | 
			
		||||
  routingMode: 'orthogonal' | 'path';
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  pathCommands?: string[];
 | 
			
		||||
  showLabel: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 从本地存储加载图表数据
 | 
			
		||||
export async function loadDiagramData(): Promise<DiagramData> {
 | 
			
		||||
  try {
 | 
			
		||||
    // 先尝试从本地存储加载
 | 
			
		||||
    const savedData = localStorage.getItem('diagramData');
 | 
			
		||||
    if (savedData) {
 | 
			
		||||
      return JSON.parse(savedData);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 如果本地存储没有,从文件加载
 | 
			
		||||
    const response = await fetch('/src/components/diagram.json');
 | 
			
		||||
    if (!response.ok) {
 | 
			
		||||
      throw new Error(`Failed to load diagram.json: ${response.statusText}`);
 | 
			
		||||
    }
 | 
			
		||||
    const data = await response.json();
 | 
			
		||||
    return data;
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Error loading diagram data:', error);
 | 
			
		||||
    // 返回空的默认数据结构
 | 
			
		||||
    return createEmptyDiagram();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 创建空的图表数据
 | 
			
		||||
export function createEmptyDiagram(): DiagramData {
 | 
			
		||||
  return {
 | 
			
		||||
    version: 1,
 | 
			
		||||
    author: 'user',
 | 
			
		||||
    editor: 'user',
 | 
			
		||||
    parts: [],
 | 
			
		||||
    connections: []
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 保存图表数据到本地存储
 | 
			
		||||
export function saveDiagramData(data: DiagramData): void {
 | 
			
		||||
  try {
 | 
			
		||||
    localStorage.setItem('diagramData', JSON.stringify(data));
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('Error saving diagram data:', error);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加新组件到图表数据
 | 
			
		||||
export function addPart(data: DiagramData, part: DiagramPart): DiagramData {
 | 
			
		||||
  return {
 | 
			
		||||
    ...data,
 | 
			
		||||
    parts: [...data.parts, part]
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新组件位置
 | 
			
		||||
export function updatePartPosition(
 | 
			
		||||
  data: DiagramData, 
 | 
			
		||||
  partId: string, 
 | 
			
		||||
  x: number, 
 | 
			
		||||
  y: number
 | 
			
		||||
): DiagramData {
 | 
			
		||||
  return {
 | 
			
		||||
    ...data,
 | 
			
		||||
    parts: data.parts.map(part => 
 | 
			
		||||
      part.id === partId 
 | 
			
		||||
        ? { ...part, x, y } 
 | 
			
		||||
        : part
 | 
			
		||||
    )
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新组件属性
 | 
			
		||||
export function updatePartAttribute(
 | 
			
		||||
  data: DiagramData,
 | 
			
		||||
  partId: string,
 | 
			
		||||
  attrName: string,
 | 
			
		||||
  value: any
 | 
			
		||||
): DiagramData {
 | 
			
		||||
  return {
 | 
			
		||||
    ...data,
 | 
			
		||||
    parts: data.parts.map(part => 
 | 
			
		||||
      part.id === partId 
 | 
			
		||||
        ? { 
 | 
			
		||||
            ...part, 
 | 
			
		||||
            attrs: { 
 | 
			
		||||
              ...part.attrs, 
 | 
			
		||||
              [attrName]: value 
 | 
			
		||||
            } 
 | 
			
		||||
          } 
 | 
			
		||||
        : part
 | 
			
		||||
    )
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除组件
 | 
			
		||||
export function deletePart(data: DiagramData, partId: string): DiagramData {
 | 
			
		||||
  return {
 | 
			
		||||
    ...data,
 | 
			
		||||
    parts: data.parts.filter(part => part.id !== partId),
 | 
			
		||||
    // 同时删除与此组件相关的所有连接
 | 
			
		||||
    connections: data.connections.filter(conn => {
 | 
			
		||||
      const [startPin, endPin] = conn;
 | 
			
		||||
      const startCompId = startPin.split(':')[0];
 | 
			
		||||
      const endCompId = endPin.split(':')[0];
 | 
			
		||||
      return startCompId !== partId && endCompId !== partId;
 | 
			
		||||
    })
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加连接
 | 
			
		||||
export function addConnection(
 | 
			
		||||
  data: DiagramData,
 | 
			
		||||
  startComponentId: string,
 | 
			
		||||
  startPinId: string,
 | 
			
		||||
  endComponentId: string,
 | 
			
		||||
  endPinId: string,
 | 
			
		||||
  width: number = 2,
 | 
			
		||||
  path: string[] = []
 | 
			
		||||
): DiagramData {
 | 
			
		||||
  const newConnection: ConnectionArray = [
 | 
			
		||||
    `${startComponentId}:${startPinId}`,
 | 
			
		||||
    `${endComponentId}:${endPinId}`,
 | 
			
		||||
    width,
 | 
			
		||||
    path
 | 
			
		||||
  ];
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
    ...data,
 | 
			
		||||
    connections: [...data.connections, newConnection]
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 删除连接
 | 
			
		||||
export function deleteConnection(
 | 
			
		||||
  data: DiagramData,
 | 
			
		||||
  connectionIndex: number
 | 
			
		||||
): DiagramData {
 | 
			
		||||
  return {
 | 
			
		||||
    ...data,
 | 
			
		||||
    connections: data.connections.filter((_, index) => index !== connectionIndex)
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 查找与组件关联的所有连接
 | 
			
		||||
export function findConnectionsByPart(
 | 
			
		||||
  data: DiagramData,
 | 
			
		||||
  partId: string
 | 
			
		||||
): { connection: ConnectionArray; index: number }[] {
 | 
			
		||||
  return data.connections
 | 
			
		||||
    .map((connection, index) => ({ connection, index }))
 | 
			
		||||
    .filter(({ connection }) => {
 | 
			
		||||
      const [startPin, endPin] = connection;
 | 
			
		||||
      const startCompId = startPin.split(':')[0];
 | 
			
		||||
      const endCompId = endPin.split(':')[0];
 | 
			
		||||
      return startCompId === partId || endCompId === partId;
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 基于组的移动相关组件
 | 
			
		||||
export function moveGroupComponents(
 | 
			
		||||
  data: DiagramData,
 | 
			
		||||
  groupId: string,
 | 
			
		||||
  deltaX: number,
 | 
			
		||||
  deltaY: number
 | 
			
		||||
): DiagramData {
 | 
			
		||||
  if (!groupId) return data;
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
    ...data,
 | 
			
		||||
    parts: data.parts.map(part => 
 | 
			
		||||
      part.group === groupId 
 | 
			
		||||
        ? { ...part, x: part.x + deltaX, y: part.y + deltaY } 
 | 
			
		||||
        : part
 | 
			
		||||
    )
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加验证diagram.json文件的函数
 | 
			
		||||
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
 | 
			
		||||
  const errors: string[] = [];
 | 
			
		||||
  
 | 
			
		||||
  // 检查版本号
 | 
			
		||||
  if (!data.version) {
 | 
			
		||||
    errors.push('缺少version字段');
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 检查parts数组
 | 
			
		||||
  if (!Array.isArray(data.parts)) {
 | 
			
		||||
    errors.push('parts字段不是数组');
 | 
			
		||||
  } else {
 | 
			
		||||
    // 验证parts中的每个对象
 | 
			
		||||
    data.parts.forEach((part: any, index: number) => {
 | 
			
		||||
      if (!part.id) errors.push(`parts[${index}]缺少id`);
 | 
			
		||||
      if (!part.type) errors.push(`parts[${index}]缺少type`);
 | 
			
		||||
      if (typeof part.x !== 'number') errors.push(`parts[${index}]缺少有效的x坐标`);
 | 
			
		||||
      if (typeof part.y !== 'number') errors.push(`parts[${index}]缺少有效的y坐标`);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 检查connections数组
 | 
			
		||||
  if (!Array.isArray(data.connections)) {
 | 
			
		||||
    errors.push('connections字段不是数组');
 | 
			
		||||
  } else {
 | 
			
		||||
    // 验证connections中的每个数组
 | 
			
		||||
    data.connections.forEach((conn: any, index: number) => {
 | 
			
		||||
      if (!Array.isArray(conn) || conn.length < 3) {
 | 
			
		||||
        errors.push(`connections[${index}]不是有效的连接数组`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const [startPin, endPin, width] = conn;
 | 
			
		||||
      
 | 
			
		||||
      if (typeof startPin !== 'string' || !startPin.includes(':')) {
 | 
			
		||||
        errors.push(`connections[${index}]的起始针脚格式无效`);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      if (typeof endPin !== 'string' || !endPin.includes(':')) {
 | 
			
		||||
        errors.push(`connections[${index}]的结束针脚格式无效`);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      if (typeof width !== 'number') {
 | 
			
		||||
        errors.push(`connections[${index}]的宽度不是有效的数字`);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return {
 | 
			
		||||
    isValid: errors.length === 0,
 | 
			
		||||
    errors
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
@@ -79,7 +79,6 @@
 | 
			
		||||
        ref="pinRef"
 | 
			
		||||
        direction="output"
 | 
			
		||||
        type="digital"  
 | 
			
		||||
        :label="props.label"
 | 
			
		||||
        :constraint="props.constraint"
 | 
			
		||||
        :size="0.8"
 | 
			
		||||
        :componentId="props.componentId"
 | 
			
		||||
@@ -99,7 +98,6 @@ const pinRef = ref<any>(null);
 | 
			
		||||
 | 
			
		||||
// 从Pin组件继承属性
 | 
			
		||||
interface PinProps {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  componentId?: string; // 添加componentId属性
 | 
			
		||||
  // 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
 | 
			
		||||
@@ -121,7 +119,6 @@ const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1,
 | 
			
		||||
  bindKey: '',
 | 
			
		||||
  buttonText: '',
 | 
			
		||||
  label: 'BTN',
 | 
			
		||||
  constraint: '',
 | 
			
		||||
  componentId: 'button-default', // 添加默认componentId
 | 
			
		||||
  // 这些值会被覆盖,但需要默认值以满足类型要求
 | 
			
		||||
@@ -142,7 +139,6 @@ const displayText = computed(() => {
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
  'update:bindKey',
 | 
			
		||||
  'update:label',
 | 
			
		||||
  'update:constraint',
 | 
			
		||||
  'press',
 | 
			
		||||
  'release',
 | 
			
		||||
@@ -212,8 +208,6 @@ defineExpose({
 | 
			
		||||
    // 按钮特有属性
 | 
			
		||||
    bindKey: props.bindKey,
 | 
			
		||||
    buttonText: props.buttonText,
 | 
			
		||||
    // 继承自Pin的属性
 | 
			
		||||
    label: props.label,
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    componentId: props.componentId, // 添加componentId
 | 
			
		||||
    // 固定的Pin属性
 | 
			
		||||
@@ -230,6 +224,25 @@ defineExpose({
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
// 添加一个静态方法来获取默认props
 | 
			
		||||
export function getDefaultProps() {
 | 
			
		||||
  return {
 | 
			
		||||
    size: 1,
 | 
			
		||||
    bindKey: '',
 | 
			
		||||
    buttonText: '',
 | 
			
		||||
    pins: [
 | 
			
		||||
      {
 | 
			
		||||
        pinId: 'BTN',
 | 
			
		||||
        constraint: '',
 | 
			
		||||
        x: 80,
 | 
			
		||||
        y: 140
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.button-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,20 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <svg 
 | 
			
		||||
  <svg
 | 
			
		||||
    :width="width"
 | 
			
		||||
    :height="height"
 | 
			
		||||
    :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
 | 
			
		||||
    class="pin-component"
 | 
			
		||||
    :data-component-id="props.componentId"
 | 
			
		||||
    :data-pin-label="props.label"
 | 
			
		||||
  >
 | 
			
		||||
    <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
 | 
			
		||||
      <g>
 | 
			
		||||
        <g transform="translate(-12.5, -12.5)">
 | 
			
		||||
          <circle
 | 
			
		||||
        <g transform="translate(-12.5, -12.5)">        <circle
 | 
			
		||||
            :style="{ fill: pinColor }"
 | 
			
		||||
            cx="12.5"
 | 
			
		||||
            cy="12.5"
 | 
			
		||||
            r="3.75"
 | 
			
		||||
            class="interactive"
 | 
			
		||||
            @click.stop="handlePinClick"
 | 
			
		||||
            :data-pin-element="`${props.componentId}`" />
 | 
			
		||||
            :data-pin-element="`${props.pinId}`" />
 | 
			
		||||
        </g>
 | 
			
		||||
      </g>
 | 
			
		||||
    </g>
 | 
			
		||||
@@ -28,16 +25,13 @@
 | 
			
		||||
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;
 | 
			
		||||
  label?: string;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  direction?: 'input' | 'output' | 'inout';
 | 
			
		||||
  type?: 'digital' | 'analog';
 | 
			
		||||
  componentId?: string; // 添加组件ID属性,用于唯一标识
 | 
			
		||||
  pinId?: string; // 添加引脚ID属性,用于唯一标识
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
@@ -46,7 +40,7 @@ const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  constraint: '',
 | 
			
		||||
  direction: 'input',
 | 
			
		||||
  type: 'digital',
 | 
			
		||||
  componentId: 'pin-default' // 默认ID
 | 
			
		||||
  pinId: 'pin-default', // 默认ID
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
@@ -136,49 +130,51 @@ function updateAnalogValue(value: number) {
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  setAnalogValue: updateAnalogValue,
 | 
			
		||||
  getAnalogValue: () => analogValue.value,
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
  getAnalogValue: () => analogValue.value,  getInfo: () => ({
 | 
			
		||||
    label: props.label,
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    direction: props.direction,
 | 
			
		||||
    type: props.type,
 | 
			
		||||
    componentId: props.componentId
 | 
			
		||||
    pinId: props.pinId
 | 
			
		||||
  }),
 | 
			
		||||
  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();
 | 
			
		||||
  getPinPosition: () => {
 | 
			
		||||
    // 获取当前Pin元素的引脚圆点位置
 | 
			
		||||
    const circle = document.querySelector(`circle[data-pin-element="${props.pinId}"]`);
 | 
			
		||||
    if (circle) {
 | 
			
		||||
      const rect = circle.getBoundingClientRect();
 | 
			
		||||
      return {
 | 
			
		||||
        x: rect.left + rect.width / 2,
 | 
			
		||||
        y: rect.top + rect.height / 2
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    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
 | 
			
		||||
    };
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
// 添加一个静态方法来获取默认props
 | 
			
		||||
export function getDefaultProps() {
 | 
			
		||||
  return {
 | 
			
		||||
    size: 1,
 | 
			
		||||
    label: 'PIN',
 | 
			
		||||
    constraint: '',
 | 
			
		||||
    direction: 'input',
 | 
			
		||||
    type: 'digital',
 | 
			
		||||
    pinId: 'pin-default',
 | 
			
		||||
    componentId: '',
 | 
			
		||||
    pins: [
 | 
			
		||||
      {
 | 
			
		||||
        pinId: 'PIN',
 | 
			
		||||
        constraint: '',
 | 
			
		||||
        x: 0,
 | 
			
		||||
        y: 0
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.pin-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
 
 | 
			
		||||
@@ -17,11 +17,11 @@
 | 
			
		||||
      <rect
 | 
			
		||||
        width="70" 
 | 
			
		||||
        height="30" 
 | 
			
		||||
        x="15" 
 | 
			
		||||
        y="15" 
 | 
			
		||||
        x="15"
 | 
			
		||||
        y="15"
 | 
			
		||||
        :fill="ledColor" 
 | 
			
		||||
        :style="{ opacity: isOn ? 1 : 0.2 }" 
 | 
			
		||||
        rx="15" 
 | 
			
		||||
        rx="15"
 | 
			
		||||
        ry="15"
 | 
			
		||||
        class="interactive"
 | 
			
		||||
      />
 | 
			
		||||
@@ -39,13 +39,19 @@
 | 
			
		||||
        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"
 | 
			
		||||
      />    </svg>
 | 
			
		||||
    <!-- 渲染自定义引脚数组 -->
 | 
			
		||||
    <div v-for="pin in props.pins" :key="pin.pinId"
 | 
			
		||||
         :style="{
 | 
			
		||||
           position: 'absolute',
 | 
			
		||||
           left: `${pin.x}px`,
 | 
			
		||||
           top: `${pin.y}px`,
 | 
			
		||||
           transform: 'translate(-50%, -50%)'
 | 
			
		||||
         }">      <Pin
 | 
			
		||||
        :ref="el => { if(el) pinRefs[pin.pinId] = el }"
 | 
			
		||||
        :label="pin.pinId"
 | 
			
		||||
        :constraint="pin.constraint"
 | 
			
		||||
        :pinId="pin.pinId"
 | 
			
		||||
        @pin-click="$emit('pin-click', $event)"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -57,34 +63,36 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
 | 
			
		||||
import Pin from './Pin.vue';
 | 
			
		||||
 | 
			
		||||
// --- 关键:暴露getPinPosition,代理到内部Pin ---
 | 
			
		||||
const pinRef = ref<any>(null);
 | 
			
		||||
 | 
			
		||||
// 从Pin组件继承属性
 | 
			
		||||
interface PinProps {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  componentId?: string; // 添加componentId属性
 | 
			
		||||
  // 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
 | 
			
		||||
  direction?: 'input' | 'output' | 'inout';
 | 
			
		||||
  type?: 'digital' | 'analog';
 | 
			
		||||
}
 | 
			
		||||
// 存储多个Pin引用
 | 
			
		||||
const pinRefs = ref<Record<string, any>>({});
 | 
			
		||||
 | 
			
		||||
// LED特有属性
 | 
			
		||||
interface LEDProps {
 | 
			
		||||
  size?: number;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  initialOn?: boolean;
 | 
			
		||||
  brightness?: number;
 | 
			
		||||
  pins?: {
 | 
			
		||||
    pinId: string;
 | 
			
		||||
    constraint: string;
 | 
			
		||||
    x: number;
 | 
			
		||||
    y: number;
 | 
			
		||||
  }[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组合两个接口
 | 
			
		||||
interface Props extends PinProps, LEDProps {}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
const props = withDefaults(defineProps<LEDProps>(), {
 | 
			
		||||
  size: 1,
 | 
			
		||||
  color: 'red',
 | 
			
		||||
  constraint: '',
 | 
			
		||||
  label: 'LED',
 | 
			
		||||
  componentId: ''
 | 
			
		||||
  initialOn: false,
 | 
			
		||||
  brightness: 80,
 | 
			
		||||
  pins: () => [
 | 
			
		||||
    {
 | 
			
		||||
      pinId: 'LED',
 | 
			
		||||
      constraint: '',
 | 
			
		||||
      x: 50,
 | 
			
		||||
      y: 30
 | 
			
		||||
    }
 | 
			
		||||
  ]
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const width = computed(() => 100 * props.size);
 | 
			
		||||
@@ -106,18 +114,26 @@ const ledColor = computed(() => {
 | 
			
		||||
  return colorMap[props.color.toLowerCase()] || props.color;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 获取LED的constraint值
 | 
			
		||||
const ledConstraint = computed(() => {
 | 
			
		||||
  if (props.pins && props.pins.length > 0) {
 | 
			
		||||
    return props.pins[0].constraint;
 | 
			
		||||
  }
 | 
			
		||||
  return '';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 监听约束状态变化
 | 
			
		||||
let unsubscribe: (() => void) | null = null;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (props.constraint) {
 | 
			
		||||
  if (ledConstraint.value) {
 | 
			
		||||
    unsubscribe = onConstraintStateChange((constraint, level) => {
 | 
			
		||||
      if (constraint === props.constraint) {
 | 
			
		||||
      if (constraint === ledConstraint.value) {
 | 
			
		||||
        isOn.value = (level === 'high');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    // 初始化LED状态
 | 
			
		||||
    const currentState = getConstraintState(props.constraint);
 | 
			
		||||
    const currentState = getConstraintState(ledConstraint.value);
 | 
			
		||||
    isOn.value = (currentState === 'high');
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
@@ -128,7 +144,7 @@ onUnmounted(() => {
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(() => props.constraint, (newConstraint) => {
 | 
			
		||||
watch(() => ledConstraint.value, (newConstraint) => {
 | 
			
		||||
  if (unsubscribe) {
 | 
			
		||||
    unsubscribe();
 | 
			
		||||
    unsubscribe = null;
 | 
			
		||||
@@ -149,20 +165,49 @@ defineExpose({
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
    color: props.color,
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    componentId: props.componentId,
 | 
			
		||||
    constraint: ledConstraint.value,
 | 
			
		||||
    direction: 'input',
 | 
			
		||||
    type: 'digital'
 | 
			
		||||
  }),
 | 
			
		||||
  getPinPosition: (componentId: string) => {
 | 
			
		||||
    if (pinRef.value && pinRef.value.getPinPosition) {
 | 
			
		||||
      return pinRef.value.getPinPosition(componentId);
 | 
			
		||||
    }
 | 
			
		||||
    return null;
 | 
			
		||||
    type: 'digital',
 | 
			
		||||
    pins: props.pins
 | 
			
		||||
  }),  getPinPosition: (pinId: string) => {
 | 
			
		||||
    // 如果是自定义的引脚ID
 | 
			
		||||
    if (props.pins && props.pins.length > 0) {
 | 
			
		||||
      console.log('Pin ID:', pinId);
 | 
			
		||||
      const customPin = props.pins.find(p => p.pinId === pinId);
 | 
			
		||||
      console.log('Custom Pin:', customPin);
 | 
			
		||||
      console.log('Pin Refs:', pinRefs.value[pinId]);
 | 
			
		||||
      if (customPin) {
 | 
			
		||||
        // 调用对应Pin组件的getPinPosition方法
 | 
			
		||||
        return {
 | 
			
		||||
          x: customPin.x,
 | 
			
		||||
          y: customPin.y
 | 
			
		||||
        }
 | 
			
		||||
      } return null;
 | 
			
		||||
    } return null;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
// 添加一个静态方法来获取默认props
 | 
			
		||||
export function getDefaultProps() {
 | 
			
		||||
  return {
 | 
			
		||||
    size: 1,
 | 
			
		||||
    color: 'red',
 | 
			
		||||
    initialOn: false,
 | 
			
		||||
    brightness: 80,
 | 
			
		||||
    pins: [
 | 
			
		||||
      {
 | 
			
		||||
        pinId: 'LED',
 | 
			
		||||
        constraint: '',
 | 
			
		||||
        x: 50,
 | 
			
		||||
        y: 30
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.led-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
 
 | 
			
		||||
@@ -33,16 +33,18 @@ interface Props {
 | 
			
		||||
  strokeColor?: string;
 | 
			
		||||
  strokeWidth?: number;
 | 
			
		||||
  isActive?: boolean;
 | 
			
		||||
  routingMode?: 'auto' | 'orthogonal' | 'direct';
 | 
			
		||||
  routingMode?: 'auto' | 'orthogonal' | 'direct' | 'path';
 | 
			
		||||
  // 针脚引用属性
 | 
			
		||||
  startComponentId?: string;
 | 
			
		||||
  startPinLabel?: string;
 | 
			
		||||
  startPinId?: string;
 | 
			
		||||
  endComponentId?: string;
 | 
			
		||||
  endPinLabel?: string;
 | 
			
		||||
  endPinId?: string;
 | 
			
		||||
  // 添加约束属性
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  // 显示标签
 | 
			
		||||
  showLabel?: boolean;
 | 
			
		||||
  // 路径命令 - 对应diagram.json中的线放置迷你语言
 | 
			
		||||
  pathCommands?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
@@ -81,9 +83,168 @@ const labelPosition = computed(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const pathData = computed(() => {
 | 
			
		||||
    return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
 | 
			
		||||
  // 如果有路径命令,使用路径布线模式
 | 
			
		||||
  if (props.routingMode === 'path' && props.pathCommands && props.pathCommands.length > 0) {
 | 
			
		||||
    return calculatePathFromCommands(
 | 
			
		||||
      props.startX, 
 | 
			
		||||
      props.startY, 
 | 
			
		||||
      props.endX, 
 | 
			
		||||
      props.endY, 
 | 
			
		||||
      props.pathCommands
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  // 否则使用正交路径
 | 
			
		||||
  return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function calculatePathFromCommands(
 | 
			
		||||
  startX: number, 
 | 
			
		||||
  startY: number, 
 | 
			
		||||
  endX: number, 
 | 
			
		||||
  endY: number, 
 | 
			
		||||
  commands: string[]
 | 
			
		||||
) {
 | 
			
		||||
  // 找到分隔符索引,通常是 "*"
 | 
			
		||||
  const splitterIndex = commands.indexOf('*');
 | 
			
		||||
  if (splitterIndex === -1) {
 | 
			
		||||
    // 如果没有分隔符,回退到正交路径
 | 
			
		||||
    return calculateOrthogonalPath(startX, startY, endX, endY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 分割命令为起点和终点两部分
 | 
			
		||||
  const startCommands = commands.slice(0, splitterIndex);
 | 
			
		||||
  const endCommands = commands.slice(splitterIndex + 1).reverse();
 | 
			
		||||
 | 
			
		||||
  // 从起点开始生成路径
 | 
			
		||||
  let currentX = startX;
 | 
			
		||||
  let currentY = startY;
 | 
			
		||||
  
 | 
			
		||||
  // 处理起点路径命令
 | 
			
		||||
  const pathPoints: [number, number][] = [[currentX, currentY]];
 | 
			
		||||
  
 | 
			
		||||
  // 解析并执行起点命令
 | 
			
		||||
  for (const cmd of startCommands) {
 | 
			
		||||
    const { newX, newY } = executePathCommand(currentX, currentY, cmd);
 | 
			
		||||
    currentX = newX;
 | 
			
		||||
    currentY = newY;
 | 
			
		||||
    pathPoints.push([currentX, currentY]);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 从终点开始反向处理
 | 
			
		||||
  let endCurrentX = endX;
 | 
			
		||||
  let endCurrentY = endY;
 | 
			
		||||
  
 | 
			
		||||
  // 保存终点路径点,最后会反转
 | 
			
		||||
  const endPathPoints: [number, number][] = [[endCurrentX, endCurrentY]];
 | 
			
		||||
  
 | 
			
		||||
  // 解析并执行终点命令(反向)
 | 
			
		||||
  for (const cmd of endCommands) {
 | 
			
		||||
    const { newX, newY } = executePathCommand(endCurrentX, endCurrentY, cmd);
 | 
			
		||||
    endCurrentX = newX;
 | 
			
		||||
    endCurrentY = newY;
 | 
			
		||||
    endPathPoints.push([endCurrentX, endCurrentY]);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 反转终点路径点并去掉第一个(终点)
 | 
			
		||||
  const reversedEndPoints = endPathPoints.slice(1).reverse();
 | 
			
		||||
  
 | 
			
		||||
  // 将两部分路径连接起来
 | 
			
		||||
  const allPoints = [...pathPoints, ...reversedEndPoints];
 | 
			
		||||
  
 | 
			
		||||
  // 如果起点和终点路径没有连接上,添加连接线段
 | 
			
		||||
  if (allPoints.length >= 2) {
 | 
			
		||||
    const startFinalPoint = allPoints[pathPoints.length - 1];
 | 
			
		||||
    const endFirstPoint = allPoints[pathPoints.length];
 | 
			
		||||
    if (startFinalPoint && endFirstPoint && 
 | 
			
		||||
        (startFinalPoint[0] !== endFirstPoint[0] || startFinalPoint[1] !== endFirstPoint[1])) {
 | 
			
		||||
      // 添加连接点 - 这里使用正交连接
 | 
			
		||||
      const middlePoints = generateOrthogonalConnection(
 | 
			
		||||
        startFinalPoint[0], startFinalPoint[1], 
 | 
			
		||||
        endFirstPoint[0], endFirstPoint[1]
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      // 将起点路径、连接路径和终点路径拼接起来
 | 
			
		||||
      allPoints.splice(pathPoints.length, 0, ...middlePoints);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 生成SVG路径
 | 
			
		||||
  if (allPoints.length < 2) {
 | 
			
		||||
    return `M ${startX} ${startY} L ${endX} ${endY}`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  let pathStr = `M ${allPoints[0][0]} ${allPoints[0][1]}`;
 | 
			
		||||
  for (let i = 1; i < allPoints.length; i++) {
 | 
			
		||||
    pathStr += ` L ${allPoints[i][0]} ${allPoints[i][1]}`;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return pathStr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 执行单个路径命令
 | 
			
		||||
function executePathCommand(x: number, y: number, command: string): { newX: number; newY: number } {
 | 
			
		||||
  // 解析命令,例如 "down10", "right20", "downright5" 等
 | 
			
		||||
  if (command.startsWith('right')) {
 | 
			
		||||
    const distance = parseInt(command.substring(5), 10) || 10;
 | 
			
		||||
    return { newX: x + distance, newY: y };
 | 
			
		||||
  } else if (command.startsWith('left')) {
 | 
			
		||||
    const distance = parseInt(command.substring(4), 10) || 10;
 | 
			
		||||
    return { newX: x - distance, newY: y };
 | 
			
		||||
  } else if (command.startsWith('down')) {
 | 
			
		||||
    if (command.startsWith('downright')) {
 | 
			
		||||
      const distance = parseInt(command.substring(9), 10) || 10;
 | 
			
		||||
      return { newX: x + distance, newY: y + distance };
 | 
			
		||||
    } else if (command.startsWith('downleft')) {
 | 
			
		||||
      const distance = parseInt(command.substring(8), 10) || 10;
 | 
			
		||||
      return { newX: x - distance, newY: y + distance };
 | 
			
		||||
    } else {
 | 
			
		||||
      const distance = parseInt(command.substring(4), 10) || 10;
 | 
			
		||||
      return { newX: x, newY: y + distance };
 | 
			
		||||
    }
 | 
			
		||||
  } else if (command.startsWith('up')) {
 | 
			
		||||
    if (command.startsWith('upright')) {
 | 
			
		||||
      const distance = parseInt(command.substring(7), 10) || 10;
 | 
			
		||||
      return { newX: x + distance, newY: y - distance };
 | 
			
		||||
    } else if (command.startsWith('upleft')) {
 | 
			
		||||
      const distance = parseInt(command.substring(6), 10) || 10;
 | 
			
		||||
      return { newX: x - distance, newY: y - distance };
 | 
			
		||||
    } else {
 | 
			
		||||
      const distance = parseInt(command.substring(2), 10) || 10;
 | 
			
		||||
      return { newX: x, newY: y - distance };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 默认情况下不移动
 | 
			
		||||
  return { newX: x, newY: y };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 生成两点之间的正交连接点
 | 
			
		||||
function generateOrthogonalConnection(x1: number, y1: number, x2: number, y2: number): [number, number][] {
 | 
			
		||||
  const dx = x2 - x1;
 | 
			
		||||
  const dy = y2 - y1;
 | 
			
		||||
  
 | 
			
		||||
  if (dx === 0 || dy === 0) {
 | 
			
		||||
    // 如果在同一水平或垂直线上,不需要额外点
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 选择先水平移动还是先垂直移动
 | 
			
		||||
  const middlePoints: [number, number][] = [];
 | 
			
		||||
  
 | 
			
		||||
  if (Math.abs(dx) > Math.abs(dy)) {
 | 
			
		||||
    // 先水平后垂直
 | 
			
		||||
    middlePoints.push([x1 + dx / 2, y1]);
 | 
			
		||||
    middlePoints.push([x1 + dx / 2, y2]);
 | 
			
		||||
  } else {
 | 
			
		||||
    // 先垂直后水平
 | 
			
		||||
    middlePoints.push([x1, y1 + dy / 2]);
 | 
			
		||||
    middlePoints.push([x2, y1 + dy / 2]);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return middlePoints;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 计算正交路径
 | 
			
		||||
function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) {
 | 
			
		||||
  // 计算两点之间的水平和垂直距离
 | 
			
		||||
  const dx = endX - startX;
 | 
			
		||||
@@ -145,13 +306,12 @@ watch(() => props.constraint, (newConstraint, oldConstraint) => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 暴露方法,用于获取这条连线的信息
 | 
			
		||||
defineExpose({  id: props.id,
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
defineExpose({  id: props.id,  getInfo: () => ({
 | 
			
		||||
    id: props.id,
 | 
			
		||||
    startComponentId: props.startComponentId,
 | 
			
		||||
    startPinLabel: props.startPinLabel,
 | 
			
		||||
    startPinId: props.startPinId,
 | 
			
		||||
    endComponentId: props.endComponentId,
 | 
			
		||||
    endPinLabel: props.endPinLabel,
 | 
			
		||||
    endPinId: props.endPinId,
 | 
			
		||||
    constraint: props.constraint
 | 
			
		||||
  }),
 | 
			
		||||
  // 更新连线位置
 | 
			
		||||
 
 | 
			
		||||
@@ -1,363 +0,0 @@
 | 
			
		||||
// 组件配置声明
 | 
			
		||||
export type PropType = 'string' | 'number' | 'boolean' | 'select';
 | 
			
		||||
 | 
			
		||||
// 定义选择类型选项
 | 
			
		||||
export interface PropOption {
 | 
			
		||||
  value: string | number | boolean;
 | 
			
		||||
  label: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PropConfig {
 | 
			
		||||
  name: string;
 | 
			
		||||
  type: string;
 | 
			
		||||
  label: string;
 | 
			
		||||
  default: any;
 | 
			
		||||
  min?: number;
 | 
			
		||||
  max?: number;
 | 
			
		||||
  step?: number;
 | 
			
		||||
  options?: PropOption[];
 | 
			
		||||
  description?: string;
 | 
			
		||||
  category?: string; // 用于在UI中分组属性
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ComponentConfig {
 | 
			
		||||
  props: PropConfig[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 存储所有组件的配置
 | 
			
		||||
const componentConfigs: Record<string, ComponentConfig> = {
 | 
			
		||||
  MechanicalButton: {
 | 
			
		||||
    props: [
 | 
			
		||||
      { 
 | 
			
		||||
        name: 'bindKey', 
 | 
			
		||||
        type: 'string', 
 | 
			
		||||
        label: '绑定按键', 
 | 
			
		||||
        default: '',
 | 
			
		||||
        description: '触发按钮按下的键盘按键'
 | 
			
		||||
      },
 | 
			
		||||
      { 
 | 
			
		||||
        name: 'size', 
 | 
			
		||||
        type: 'number', 
 | 
			
		||||
        label: '大小', 
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: '按钮的相对大小,1代表标准大小'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'buttonText',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        label: '按钮文本',
 | 
			
		||||
        default: '',
 | 
			
		||||
        description: '按钮上显示的自定义文本,优先级高于绑定按键'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'label',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        label: '引脚标签',
 | 
			
		||||
        default: 'BTN',
 | 
			
		||||
        description: '引脚的标签文本'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'constraint',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        label: '引脚约束',
 | 
			
		||||
        default: '',
 | 
			
		||||
        description: '相同约束字符串的引脚将被视为有电气连接'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  Switch: {
 | 
			
		||||
    props: [
 | 
			
		||||
      { 
 | 
			
		||||
        name: 'size', 
 | 
			
		||||
        type: 'number', 
 | 
			
		||||
        label: '大小', 
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: '开关的相对大小,1代表标准大小'
 | 
			
		||||
      },
 | 
			
		||||
      { 
 | 
			
		||||
        name: 'switchCount', 
 | 
			
		||||
        type: 'number', 
 | 
			
		||||
        label: '开关数量', 
 | 
			
		||||
        default: 6,
 | 
			
		||||
        min: 1,
 | 
			
		||||
        max: 12,
 | 
			
		||||
        step: 1,
 | 
			
		||||
        description: '可翻转开关的数量'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'showLabels',
 | 
			
		||||
        type: 'boolean',
 | 
			
		||||
        label: '显示标签',
 | 
			
		||||
        default: true,
 | 
			
		||||
        description: '是否显示开关编号标签'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'initialValues',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        label: '初始状态',
 | 
			
		||||
        default: '',
 | 
			
		||||
        description: '开关的初始状态,格式为逗号分隔的0/1,如"1,0,1"表示第1、3个开关打开'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  Pin: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: '引脚的相对大小,1代表标准大小'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'label',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        label: '引脚标签',
 | 
			
		||||
        default: 'PIN',
 | 
			
		||||
        description: '用于标识引脚的名称'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'constraint',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        label: '引脚约束',
 | 
			
		||||
        default: '',
 | 
			
		||||
        description: '相同约束字符串的引脚将被视为有电气连接'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'direction',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        label: '输入/输出特性',
 | 
			
		||||
        default: 'input',
 | 
			
		||||
        options: [
 | 
			
		||||
          { value: 'input', label: '输入' },
 | 
			
		||||
          { value: 'output', label: '输出' },
 | 
			
		||||
          { value: 'inout', label: '双向' }
 | 
			
		||||
        ],
 | 
			
		||||
        description: '引脚的输入/输出特性'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'type',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        label: '模数特性',
 | 
			
		||||
        default: 'digital',
 | 
			
		||||
        options: [
 | 
			
		||||
          { value: 'digital', label: 'digital' },
 | 
			
		||||
          { value: 'analog', label: 'analog' }
 | 
			
		||||
        ],
 | 
			
		||||
        description: '引脚的模数特性,数字或模拟'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  HDMI: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: 'HDMI接口的相对大小,1代表标准大小'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  DDR: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: 'DDR内存的相对大小,1代表标准大小'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  ETH: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: '以太网接口的相对大小,1代表标准大小'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  SD: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: 'SD卡插槽的相对大小,1代表标准大小'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  SFP: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: 'SFP光纤模块的相对大小,1代表标准大小'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  SMA: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: 'SMA连接器的相对大小,1代表标准大小'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },  MotherBoard: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 2,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: '主板的相对大小,1代表标准大小'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },  SMT_LED: {
 | 
			
		||||
    props: [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'size',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '大小',
 | 
			
		||||
        default: 1,
 | 
			
		||||
        min: 0.5,
 | 
			
		||||
        max: 3,
 | 
			
		||||
        step: 0.1,
 | 
			
		||||
        description: 'LED的相对大小,1代表标准大小'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'color',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        label: '颜色',
 | 
			
		||||
        default: 'red',
 | 
			
		||||
        options: [
 | 
			
		||||
          { value: 'red', label: '红色' },
 | 
			
		||||
          { value: 'green', label: '绿色' },
 | 
			
		||||
          { value: 'blue', label: '蓝色' },
 | 
			
		||||
          { value: 'yellow', label: '黄色' },
 | 
			
		||||
          { value: 'orange', label: '橙色' },
 | 
			
		||||
          { value: 'white', label: '白色' },
 | 
			
		||||
          { value: 'purple', label: '紫色' }
 | 
			
		||||
        ],
 | 
			
		||||
        description: 'LED的颜色'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'initialOn',
 | 
			
		||||
        type: 'boolean',
 | 
			
		||||
        label: '初始状态',
 | 
			
		||||
        default: false,
 | 
			
		||||
        description: 'LED的初始开关状态'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'brightness',
 | 
			
		||||
        type: 'number',
 | 
			
		||||
        label: '亮度(%)',
 | 
			
		||||
        default: 80,
 | 
			
		||||
        min: 0,
 | 
			
		||||
        max: 100,
 | 
			
		||||
        step: 5,
 | 
			
		||||
        description: 'LED的亮度百分比,范围0-100'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'constraint',
 | 
			
		||||
        type: 'string',
 | 
			
		||||
        label: '引脚约束',
 | 
			
		||||
        default: '',
 | 
			
		||||
        description: '相同约束字符串的引脚将被视为有电气连接'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  // 线缆配置
 | 
			
		||||
  Wire: {
 | 
			
		||||
    props: [
 | 
			
		||||
      { 
 | 
			
		||||
        name: 'routingMode', 
 | 
			
		||||
        type: 'select', 
 | 
			
		||||
        label: '路由方式', 
 | 
			
		||||
        default: 'orthogonal',
 | 
			
		||||
        options: [
 | 
			
		||||
          { value: 'orthogonal', label: '直角' },
 | 
			
		||||
          { value: 'direct', label: '直线' },
 | 
			
		||||
          { value: 'auto', label: '自动' }
 | 
			
		||||
        ],
 | 
			
		||||
        description: '线路连接方式' 
 | 
			
		||||
      },
 | 
			
		||||
      { 
 | 
			
		||||
        name: 'strokeColor', 
 | 
			
		||||
        type: 'string', 
 | 
			
		||||
        label: '线条颜色', 
 | 
			
		||||
        default: '#4a5568',
 | 
			
		||||
        description: '线条颜色,使用CSS颜色值'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'strokeWidth', 
 | 
			
		||||
        type: 'number', 
 | 
			
		||||
        label: '线条宽度', 
 | 
			
		||||
        default: 2,
 | 
			
		||||
        min: 1,
 | 
			
		||||
        max: 10,
 | 
			
		||||
        step: 0.5,
 | 
			
		||||
        description: '线条宽度' 
 | 
			
		||||
      },
 | 
			
		||||
      { 
 | 
			
		||||
        name: 'constraint', 
 | 
			
		||||
        type: 'string', 
 | 
			
		||||
        label: '约束名称', 
 | 
			
		||||
        default: '',
 | 
			
		||||
        description: '线路约束名称,用于标识连接关系' 
 | 
			
		||||
      },
 | 
			
		||||
      { 
 | 
			
		||||
        name: 'showLabel', 
 | 
			
		||||
        type: 'boolean', 
 | 
			
		||||
        label: '显示标签', 
 | 
			
		||||
        default: false,
 | 
			
		||||
        description: '是否显示连线上的约束标签' 
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 获取组件配置的函数
 | 
			
		||||
export function getComponentConfig(type: string): ComponentConfig | null {
 | 
			
		||||
  return componentConfigs[type] || null;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,24 +1,19 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="h-screen flex flex-col overflow-hidden">
 | 
			
		||||
    <div class="flex flex-1 overflow-hidden relative">
 | 
			
		||||
      <!-- 左侧图形化区域 -->
 | 
			
		||||
      <div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }">        <DiagramCanvas
 | 
			
		||||
          ref="diagramCanvas"          :components="components"
 | 
			
		||||
          :componentModules="componentModules"          @component-selected="handleComponentSelected"
 | 
			
		||||
      <!-- 左侧图形化区域 -->      
 | 
			
		||||
      <div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }">          <DiagramCanvas
 | 
			
		||||
          ref="diagramCanvas"
 | 
			
		||||
          :componentModules="componentModules"          
 | 
			
		||||
          @component-selected="handleComponentSelected"
 | 
			
		||||
          @component-moved="handleComponentMoved"
 | 
			
		||||
          @update-component-prop="updateComponentProp"
 | 
			
		||||
          @component-delete="handleComponentDelete"
 | 
			
		||||
          @wire-created="handleWireCreated"
 | 
			
		||||
          @wire-deleted="handleWireDeleted"
 | 
			
		||||
          @diagram-updated="handleDiagramUpdated"
 | 
			
		||||
          @open-components="openComponentsMenu"
 | 
			
		||||
          @load-component-module="handleLoadComponentModule"
 | 
			
		||||
        />
 | 
			
		||||
        <!-- 添加元器件按钮 -->
 | 
			
		||||
        <button class="btn btn-circle btn-primary absolute top-8 right-8 shadow-lg z-10" @click="openComponentsMenu">
 | 
			
		||||
          <!-- SVG icon -->
 | 
			
		||||
           <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
 | 
			
		||||
            <line x1="12" y1="5" x2="12" y2="19"></line>
 | 
			
		||||
            <line x1="5" y1="12" x2="19" y2="12"></line>
 | 
			
		||||
          </svg>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 拖拽分割线 -->
 | 
			
		||||
@@ -31,11 +26,11 @@
 | 
			
		||||
      <div class="bg-base-100 flex flex-col p-4 overflow-auto" :style="{ width: (100 - leftPanelWidth) + '%' }">
 | 
			
		||||
        <h3 class="text-lg font-bold mb-4">属性编辑器</h3>
 | 
			
		||||
        <div v-if="!selectedComponentData" class="text-gray-400">选择元器件以编辑属性</div>
 | 
			
		||||
        <div v-else>
 | 
			
		||||
        <div v-else>          
 | 
			
		||||
          <div class="mb-4 pb-4 border-b border-base-300">
 | 
			
		||||
             <h4 class="font-semibold text-lg mb-1">{{ selectedComponentData.name }}</h4>
 | 
			
		||||
             <p class="text-xs text-gray-500">ID: {{ selectedComponentData.id }}</p>
 | 
			
		||||
             <p class="text-xs text-gray-500">类型: {{ selectedComponentData.type }}</p>
 | 
			
		||||
             <h4 class="font-semibold text-lg mb-1">{{ selectedComponentData?.type }}</h4>
 | 
			
		||||
             <p class="text-xs text-gray-500">ID: {{ selectedComponentData?.id }}</p>
 | 
			
		||||
             <p class="text-xs text-gray-500">类型: {{ selectedComponentData?.type }}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 动态属性表单 -->
 | 
			
		||||
@@ -50,30 +45,32 @@
 | 
			
		||||
                type="text"
 | 
			
		||||
                :placeholder="prop.label || prop.name"
 | 
			
		||||
                class="input input-bordered input-sm w-full"
 | 
			
		||||
                :value="selectedComponentData.props?.[prop.name]"
 | 
			
		||||
                @input="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).value)"
 | 
			
		||||
                :value="selectedComponentData?.attrs?.[prop.name]"
 | 
			
		||||
                @input="updateComponentProp(selectedComponentData!.id, prop.name, ($event.target as HTMLInputElement).value)"
 | 
			
		||||
              />
 | 
			
		||||
              <input
 | 
			
		||||
                v-else-if="prop.type === 'number'"
 | 
			
		||||
                type="number"
 | 
			
		||||
                :placeholder="prop.label || prop.name"
 | 
			
		||||
                class="input input-bordered input-sm w-full"
 | 
			
		||||
                :value="selectedComponentData.props?.[prop.name]"
 | 
			
		||||
                @input="updateComponentProp(selectedComponentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
 | 
			
		||||
              />              <!-- 可以为 boolean 添加 checkbox,为 color 添加 color picker 等 -->
 | 
			
		||||
                :value="selectedComponentData?.attrs?.[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]"
 | 
			
		||||
                    @change="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
 | 
			
		||||
                    :checked="selectedComponentData?.attrs?.[prop.name]"
 | 
			
		||||
                    @change="updateComponentProp(selectedComponentData!.id, prop.name, ($event.target as HTMLInputElement).checked)"
 | 
			
		||||
                  />
 | 
			
		||||
                  <span>{{ prop.label || prop.name }}</span>
 | 
			
		||||
               </div>              <!-- 下拉选择框 -->
 | 
			
		||||
               </div>              
 | 
			
		||||
               <!-- 下拉选择框 -->
 | 
			
		||||
              <select
 | 
			
		||||
                v-else-if="prop.type === 'select' && prop.options"
 | 
			
		||||
                class="select select-bordered select-sm w-full"
 | 
			
		||||
                :value="selectedComponentData.props?.[prop.name]"
 | 
			
		||||
                :value="selectedComponentData?.attrs?.[prop.name]"
 | 
			
		||||
                @change="(event) => {
 | 
			
		||||
                  const selectElement = event.target as HTMLSelectElement;
 | 
			
		||||
                  const value = selectElement.value;
 | 
			
		||||
@@ -96,7 +93,8 @@
 | 
			
		||||
             此组件没有可配置的属性。
 | 
			
		||||
           </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>    </div>
 | 
			
		||||
      </div>    
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 元器件选择组件 -->
 | 
			
		||||
    <ComponentSelector 
 | 
			
		||||
@@ -114,29 +112,29 @@
 | 
			
		||||
import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // 引入 defineAsyncComponent 和 shallowRef
 | 
			
		||||
import DiagramCanvas from '@/components/DiagramCanvas.vue';
 | 
			
		||||
import ComponentSelector from '@/components/ComponentSelector.vue';
 | 
			
		||||
import { getComponentConfig } from '@/components/equipments/componentConfig';
 | 
			
		||||
import type { ComponentConfig } from '@/components/equipments/componentConfig';
 | 
			
		||||
import type { DiagramData, DiagramPart, ConnectionArray } from '@/components/diagramManager';
 | 
			
		||||
import { validateDiagramData } from '@/components/diagramManager';
 | 
			
		||||
 | 
			
		||||
// --- 元器件管理 ---
 | 
			
		||||
const showComponentsMenu = ref(false);
 | 
			
		||||
interface ComponentItem {
 | 
			
		||||
  id: string;
 | 
			
		||||
  type: string; // 现在是组件的文件名或标识符,例如 'MechanicalButton'
 | 
			
		||||
  name: string;
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  props?: Record<string, any>; // 添加 props 字段来存储组件实例的属性
 | 
			
		||||
}
 | 
			
		||||
const components = ref<ComponentItem[]>([]);
 | 
			
		||||
const selectedComponentId = ref<string | null>(null); // 重命名为 selectedComponentId
 | 
			
		||||
const selectedComponentData = computed(() => { // 改为计算属性
 | 
			
		||||
  return components.value.find(c => c.id === selectedComponentId.value) || null;
 | 
			
		||||
const diagramData = ref<DiagramData>({
 | 
			
		||||
  version: 1,
 | 
			
		||||
  author: 'admin',
 | 
			
		||||
  editor: 'me',
 | 
			
		||||
  parts: [],
 | 
			
		||||
  connections: []
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const selectedComponentId = ref<string | null>(null);
 | 
			
		||||
const selectedComponentData = computed(() => {
 | 
			
		||||
  return diagramData.value.parts.find(p => p.id === selectedComponentId.value) || null;
 | 
			
		||||
});
 | 
			
		||||
const diagramCanvas = ref(null);
 | 
			
		||||
 | 
			
		||||
// 存储动态导入的组件模块
 | 
			
		||||
interface ComponentModule {
 | 
			
		||||
  default: any;
 | 
			
		||||
  getDefaultProps?: () => Record<string, any>;
 | 
			
		||||
  config?: {
 | 
			
		||||
    props?: Array<{
 | 
			
		||||
      name: string;
 | 
			
		||||
@@ -173,6 +171,12 @@ async function loadComponentModule(type: string) {
 | 
			
		||||
  return componentModules.value[type];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理组件模块加载请求
 | 
			
		||||
async function handleLoadComponentModule(type: string) {
 | 
			
		||||
  console.log('Handling load component module request for:', type);
 | 
			
		||||
  await loadComponentModule(type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 分割面板 ---
 | 
			
		||||
const leftPanelWidth = ref(60);
 | 
			
		||||
const isResizing = ref(false);
 | 
			
		||||
@@ -259,54 +263,140 @@ async function handleAddComponent(componentData: { type: string; name: string; p
 | 
			
		||||
  const offsetX = Math.floor(Math.random() * 100) - 50;
 | 
			
		||||
  const offsetY = Math.floor(Math.random() * 100) - 50;
 | 
			
		||||
 | 
			
		||||
  const newComponent: ComponentItem = {
 | 
			
		||||
  // 将原有的 ComponentItem 转换为 DiagramPart
 | 
			
		||||
  const newComponent: DiagramPart = {
 | 
			
		||||
    id: `component-${Date.now()}`,
 | 
			
		||||
    type: componentData.type,
 | 
			
		||||
    name: componentData.name,
 | 
			
		||||
    x: Math.round(posX + offsetX),
 | 
			
		||||
    y: Math.round(posY + offsetY),
 | 
			
		||||
    props: componentData.props, // 使用从 ComponentSelector 传递的默认属性
 | 
			
		||||
    attrs: componentData.props, // 使用从 ComponentSelector 传递的默认属性
 | 
			
		||||
    rotate: 0,
 | 
			
		||||
    group: '',
 | 
			
		||||
    positionlock: false,
 | 
			
		||||
    hide: false,
 | 
			
		||||
    isOn: false
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  components.value.push(newComponent);
 | 
			
		||||
  console.log('Adding new component:', newComponent);
 | 
			
		||||
  // 通过 diagramCanvas 添加组件
 | 
			
		||||
  if (canvasInstance && canvasInstance.getDiagramData && canvasInstance.setDiagramData) {
 | 
			
		||||
    const currentData = canvasInstance.getDiagramData();
 | 
			
		||||
    currentData.parts.push(newComponent);
 | 
			
		||||
    canvasInstance.setDiagramData(currentData);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理组件选中事件
 | 
			
		||||
async function handleComponentSelected(componentData: ComponentItem | null) {
 | 
			
		||||
async function handleComponentSelected(componentData: DiagramPart | null) {
 | 
			
		||||
  selectedComponentId.value = componentData ? componentData.id : null;
 | 
			
		||||
  selectedComponentConfig.value = null; // 重置配置
 | 
			
		||||
 | 
			
		||||
  if (componentData) {
 | 
			
		||||
    // 从配置文件中获取组件配置
 | 
			
		||||
    const config = getComponentConfig(componentData.type);
 | 
			
		||||
    if (config) {
 | 
			
		||||
      selectedComponentConfig.value = config;
 | 
			
		||||
      console.log(`Config for ${componentData.type}:`, config);
 | 
			
		||||
    } else {
 | 
			
		||||
      console.warn(`No config found for component type ${componentData.type}`);
 | 
			
		||||
    }
 | 
			
		||||
    // 先加载组件模块
 | 
			
		||||
    const moduleRef = await loadComponentModule(componentData.type);
 | 
			
		||||
    
 | 
			
		||||
    // 同时加载组件模块以备用
 | 
			
		||||
    await loadComponentModule(componentData.type);
 | 
			
		||||
    if (moduleRef) {
 | 
			
		||||
      try {
 | 
			
		||||
        // 尝试使用组件导出的getDefaultProps方法获取配置
 | 
			
		||||
        if (typeof moduleRef.getDefaultProps === 'function') {
 | 
			
		||||
          // 从getDefaultProps方法构建配置
 | 
			
		||||
          const defaultProps = moduleRef.getDefaultProps();
 | 
			
		||||
          const propConfigs = [];
 | 
			
		||||
          
 | 
			
		||||
          for (const [propName, propValue] of Object.entries(defaultProps)) {
 | 
			
		||||
            // 跳过pins属性,它是一个特殊的数组属性
 | 
			
		||||
            if (propName === 'pins') continue;
 | 
			
		||||
            
 | 
			
		||||
            // 根据属性类型创建配置
 | 
			
		||||
            let propType = typeof propValue;
 | 
			
		||||
            let propConfig: any = {
 | 
			
		||||
              name: propName,
 | 
			
		||||
              label: propName.charAt(0).toUpperCase() + propName.slice(1), // 首字母大写作为标签
 | 
			
		||||
              default: propValue
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            // 根据值类型设置表单控件类型
 | 
			
		||||
            if (propType === 'string') {
 | 
			
		||||
              propConfig.type = 'string';
 | 
			
		||||
            } else if (propType === 'number') {
 | 
			
		||||
              propConfig.type = 'number';
 | 
			
		||||
              propConfig.min = 0;
 | 
			
		||||
              propConfig.max = 100;
 | 
			
		||||
              propConfig.step = 0.1;
 | 
			
		||||
            } else if (propType === 'boolean') {
 | 
			
		||||
              propConfig.type = 'boolean';
 | 
			
		||||
            } else if (propType === 'object' && propValue !== null && propValue.hasOwnProperty('options')) {
 | 
			
		||||
              // 如果是含有options的对象,认为它是select类型
 | 
			
		||||
              propConfig.type = 'select';
 | 
			
		||||
              propConfig.options = propValue.options;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            propConfigs.push(propConfig);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          selectedComponentConfig.value = { props: propConfigs };
 | 
			
		||||
          console.log(`Built config for ${componentData.type} from getDefaultProps:`, selectedComponentConfig.value);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.warn(`Component ${componentData.type} does not export getDefaultProps method.`);
 | 
			
		||||
          // 创建一个空配置,只显示组件提供的属性
 | 
			
		||||
          const attrs = componentData.attrs || {};
 | 
			
		||||
          const propConfigs = [];
 | 
			
		||||
          
 | 
			
		||||
          for (const [propName, propValue] of Object.entries(attrs)) {
 | 
			
		||||
            // 跳过pins属性
 | 
			
		||||
            if (propName === 'pins') continue;
 | 
			
		||||
              // 根据属性值类型创建配置
 | 
			
		||||
            let propType = typeof propValue;
 | 
			
		||||
            let propConfig: any = {
 | 
			
		||||
              name: propName,
 | 
			
		||||
              label: propName.charAt(0).toUpperCase() + propName.slice(1), // 首字母大写作为标签
 | 
			
		||||
              default: propValue || '',
 | 
			
		||||
              type: propType
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            propConfigs.push(propConfig);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          selectedComponentConfig.value = { props: propConfigs };
 | 
			
		||||
        }
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(`Error building config for ${componentData.type}:`, error);
 | 
			
		||||
        selectedComponentConfig.value = { props: [] };
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      console.warn(`Module for component ${componentData.type} not found.`);
 | 
			
		||||
      selectedComponentConfig.value = { props: [] };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理图表数据更新事件
 | 
			
		||||
function handleDiagramUpdated(data: DiagramData) {
 | 
			
		||||
  diagramData.value = data;
 | 
			
		||||
  console.log('Diagram data updated:', data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理组件移动事件
 | 
			
		||||
function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
 | 
			
		||||
  const component = components.value.find(c => c.id === moveData.id);
 | 
			
		||||
  if (component) {
 | 
			
		||||
    component.x = moveData.x;
 | 
			
		||||
    component.y = moveData.y;
 | 
			
		||||
  const part = diagramData.value.parts.find(p => p.id === moveData.id);
 | 
			
		||||
  if (part) {
 | 
			
		||||
    part.x = moveData.x;
 | 
			
		||||
    part.y = moveData.y;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理组件删除事件
 | 
			
		||||
function handleComponentDelete(componentId: string) {
 | 
			
		||||
  // 查找要删除的组件索引
 | 
			
		||||
  const index = components.value.findIndex(c => c.id === componentId);
 | 
			
		||||
  const index = diagramData.value.parts.findIndex(p => p.id === componentId);
 | 
			
		||||
  if (index !== -1) {
 | 
			
		||||
    // 从数组中移除该组件
 | 
			
		||||
    components.value.splice(index, 1);
 | 
			
		||||
    diagramData.value.parts.splice(index, 1);
 | 
			
		||||
    
 | 
			
		||||
    // 同时删除与该组件相关的所有连接
 | 
			
		||||
    diagramData.value.connections = diagramData.value.connections.filter(
 | 
			
		||||
      connection => !connection[0].startsWith(`${componentId}:`) && !connection[1].startsWith(`${componentId}:`)
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    // 如果删除的是当前选中的组件,清除选中状态
 | 
			
		||||
    if (selectedComponentId.value === componentId) {
 | 
			
		||||
      selectedComponentId.value = null;
 | 
			
		||||
@@ -315,29 +405,29 @@ function handleComponentDelete(componentId: string) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新组件属性的方法,处理字符串类型的初始值特殊格式
 | 
			
		||||
function updateComponentProp(componentId: string | { id: string; propName: string; value: any }, propName?: string, value?: any) {
 | 
			
		||||
  // 处理来自 DiagramCanvas 的事件
 | 
			
		||||
  if (typeof componentId === 'object') {
 | 
			
		||||
    const { id, propName: name, value: val } = componentId;
 | 
			
		||||
    componentId = id;
 | 
			
		||||
    propName = name;
 | 
			
		||||
    value = val;
 | 
			
		||||
// 更新组件属性的方法
 | 
			
		||||
function updateComponentProp(componentId: string, propName: string, value: any) {
 | 
			
		||||
  const canvasInstance = diagramCanvas.value as any;
 | 
			
		||||
  if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.setDiagramData) {
 | 
			
		||||
    console.error('Canvas instance not available for property update');
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  const component = components.value.find(c => c.id === componentId);
 | 
			
		||||
  if (component && propName !== undefined) {
 | 
			
		||||
    if (!component.props) {
 | 
			
		||||
      component.props = {}; 
 | 
			
		||||
  
 | 
			
		||||
  // 检查值是否为对象,如果是对象并有value属性,则使用该属性值
 | 
			
		||||
  if (value !== null && typeof value === 'object' && 'value' in value) {
 | 
			
		||||
    value = value.value;
 | 
			
		||||
  }
 | 
			
		||||
  const currentData = canvasInstance.getDiagramData();
 | 
			
		||||
  const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
 | 
			
		||||
  
 | 
			
		||||
  if (part) {
 | 
			
		||||
    if (!part.attrs) {
 | 
			
		||||
      part.attrs = {};
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 检查值是否为对象,如果是对象并有value属性,则使用该属性值
 | 
			
		||||
    if (value !== null && typeof value === 'object' && 'value' in value) {
 | 
			
		||||
      value = value.value;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 直接更新属性值
 | 
			
		||||
    component.props[propName] = value;
 | 
			
		||||
    part.attrs[propName] = value;
 | 
			
		||||
    
 | 
			
		||||
    canvasInstance.setDiagramData(currentData);
 | 
			
		||||
    console.log(`Updated ${componentId} prop ${propName} to:`, value, typeof value);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -345,19 +435,69 @@ function updateComponentProp(componentId: string | { id: string; propName: strin
 | 
			
		||||
// 处理连线创建事件
 | 
			
		||||
function handleWireCreated(wireData: any) {
 | 
			
		||||
  console.log('Wire created:', wireData);
 | 
			
		||||
  // 连线已在DiagramCanvas.vue中完成约束处理
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理连线删除事件
 | 
			
		||||
function handleWireDeleted(wireId: string) {
 | 
			
		||||
  console.log('Wire deleted:', wireId);
 | 
			
		||||
  // 可以在这里添加连线删除的相关逻辑
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 导出当前diagram数据
 | 
			
		||||
function exportDiagram() {
 | 
			
		||||
  // 直接使用DiagramCanvas组件提供的导出功能
 | 
			
		||||
  const canvasInstance = diagramCanvas.value as any;
 | 
			
		||||
  if (canvasInstance && canvasInstance.exportDiagram) {
 | 
			
		||||
    canvasInstance.exportDiagram();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 消息提示 ---
 | 
			
		||||
const showNotification = ref(false);
 | 
			
		||||
const notificationMessage = ref('');
 | 
			
		||||
const notificationType = ref<'success' | 'error' | 'info'>('info');
 | 
			
		||||
 | 
			
		||||
function showToast(message: string, type: 'success' | 'error' | 'info' = 'info', duration = 3000) {  const canvasInstance = diagramCanvas.value as any;
 | 
			
		||||
  if (canvasInstance && canvasInstance.showToast) {
 | 
			
		||||
    canvasInstance.showToast(message, type, duration);
 | 
			
		||||
  } else {
 | 
			
		||||
    // 后备方案:使用原来的通知系统
 | 
			
		||||
    notificationMessage.value = message;
 | 
			
		||||
    notificationType.value = type;
 | 
			
		||||
    showNotification.value = true;
 | 
			
		||||
    
 | 
			
		||||
    // 设置自动消失
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      showNotification.value = false;
 | 
			
		||||
    }, duration);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
// 显示通知
 | 
			
		||||
 | 
			
		||||
// --- 生命周期钩子 ---
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  // 初始化画布设置
 | 
			
		||||
  console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value);
 | 
			
		||||
  
 | 
			
		||||
  // 获取初始图表数据
 | 
			
		||||
  const canvasInstance = diagramCanvas.value as any;
 | 
			
		||||
  if (canvasInstance && canvasInstance.getDiagramData) {
 | 
			
		||||
    diagramData.value = canvasInstance.getDiagramData();
 | 
			
		||||
    
 | 
			
		||||
    // 预加载所有使用的组件模块,以确保它们在渲染时可用
 | 
			
		||||
    const componentTypes = new Set<string>();
 | 
			
		||||
    diagramData.value.parts.forEach(part => {
 | 
			
		||||
      componentTypes.add(part.type);
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    console.log('Preloading component modules:', Array.from(componentTypes));
 | 
			
		||||
    
 | 
			
		||||
    // 并行加载所有组件模块
 | 
			
		||||
    await Promise.all(
 | 
			
		||||
      Array.from(componentTypes).map(type => loadComponentModule(type))
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    console.log('All component modules loaded');
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user