feat: add project view
This commit is contained in:
		
							
								
								
									
										79
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										79
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,14 +1,15 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "fpga-weblab",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "fpga-weblab",
 | 
			
		||||
      "version": "0.0.0",
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/lodash": "^4.17.16",
 | 
			
		||||
        "@wokwi/elements": "^1.7.0",
 | 
			
		||||
        "all": "^0.0.0",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "log-symbols": "^7.0.0",
 | 
			
		||||
@@ -998,6 +999,21 @@
 | 
			
		||||
        "@jridgewell/sourcemap-codec": "^1.4.14"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lit-labs/ssr-dom-shim": {
 | 
			
		||||
      "version": "1.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.3.0.tgz",
 | 
			
		||||
      "integrity": "sha512-nQIWonJ6eFAvUUrSlwyHDm/aE8PBDu5kRpL0vHMg6K8fK3Diq1xdPjTnsJSwxABhaZ+5eBi1btQB5ShUTKo4nQ==",
 | 
			
		||||
      "license": "BSD-3-Clause"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@lit/reactive-element": {
 | 
			
		||||
      "version": "1.6.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-1.6.3.tgz",
 | 
			
		||||
      "integrity": "sha512-QuTgnG52Poic7uM1AN5yJ09QMe0O28e10XzSvWDz02TJiiKee4stsiownEIadWm8nYzyDAyT+gKzUoZmiWQtsQ==",
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lit-labs/ssr-dom-shim": "^1.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@polka/url": {
 | 
			
		||||
      "version": "1.0.0-next.29",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
 | 
			
		||||
@@ -1626,6 +1642,21 @@
 | 
			
		||||
        "undici-types": "~6.21.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/react": {
 | 
			
		||||
      "version": "19.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
 | 
			
		||||
      "integrity": "sha512-oxLPMytKchWGbnQM9O7D67uPa9paTNxO7jVoNMXgkkErULBPhPARCfkKL9ytcIJJRGjbsVwW4ugJzyFFvm/Tiw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "csstype": "^3.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@types/trusted-types": {
 | 
			
		||||
      "version": "2.0.7",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
 | 
			
		||||
      "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@vitejs/plugin-vue": {
 | 
			
		||||
      "version": "5.2.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
 | 
			
		||||
@@ -1966,6 +1997,19 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@wokwi/elements": {
 | 
			
		||||
      "version": "1.7.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@wokwi/elements/-/elements-1.7.0.tgz",
 | 
			
		||||
      "integrity": "sha512-8vgePMFmcmTldUvnCfkIMZ4tijy6H7e34JPQsdJPcJXUWdrp8XGEYnFETY6qrAI1Icm7SQChcarG/NpEV0Zn9Q==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/react": ">=16",
 | 
			
		||||
        "lit": "^2.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=16"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/alien-signals": {
 | 
			
		||||
      "version": "1.0.13",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
 | 
			
		||||
@@ -2975,6 +3019,37 @@
 | 
			
		||||
        "url": "https://opencollective.com/parcel"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lit": {
 | 
			
		||||
      "version": "2.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lit/-/lit-2.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-4Sc3OFX9QHOJaHbmTMk28SYgVxLN3ePDjg7hofEft2zWlehFL3LiAuapWc4U/kYwMYJSh2hTCPZ6/LIC7ii0MA==",
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lit/reactive-element": "^1.6.0",
 | 
			
		||||
        "lit-element": "^3.3.0",
 | 
			
		||||
        "lit-html": "^2.8.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lit-element": {
 | 
			
		||||
      "version": "3.3.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-3.3.3.tgz",
 | 
			
		||||
      "integrity": "sha512-XbeRxmTHubXENkV4h8RIPyr8lXc+Ff28rkcQzw3G6up2xg5E8Zu1IgOWIwBLEQsu3cOVFqdYwiVi0hv0SlpqUA==",
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@lit-labs/ssr-dom-shim": "^1.1.0",
 | 
			
		||||
        "@lit/reactive-element": "^1.3.0",
 | 
			
		||||
        "lit-html": "^2.8.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lit-html": {
 | 
			
		||||
      "version": "2.8.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-2.8.0.tgz",
 | 
			
		||||
      "integrity": "sha512-o9t+MQM3P4y7M7yNzqAyjp7z+mQGa4NS4CxiyLqFPyFWyc4O+nodLrkrxSaCTrla6M5YOLaT3RpbbqjszB5g3Q==",
 | 
			
		||||
      "license": "BSD-3-Clause",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@types/trusted-types": "^2.0.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lodash": {
 | 
			
		||||
      "version": "4.17.21",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@types/lodash": "^4.17.16",
 | 
			
		||||
    "@wokwi/elements": "^1.7.0",
 | 
			
		||||
    "all": "^0.0.0",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "log-symbols": "^7.0.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ const items = [
 | 
			
		||||
  { id: 1, icon: iconMenu, text: "用户界面", page: "/user" },
 | 
			
		||||
  { id: 2, icon: iconMenu, text: "ComponentTest", page: "/test" },
 | 
			
		||||
  { id: 3, icon: iconMenu, text: "JtagTest", page: "/test/jtag" },
 | 
			
		||||
  { id: 4, icon: iconMenu, text: "工程界面", page: "/project" }, // 新增工程界面入口
 | 
			
		||||
];
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										370
									
								
								src/components/DiagramCanvas.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										370
									
								
								src/components/DiagramCanvas.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,370 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex-1 min-w-[60%] bg-base-200 relative overflow-auto" ref="canvasContainer" 
 | 
			
		||||
      @mousedown="handleCanvasMouseDown" 
 | 
			
		||||
      @mousedown.middle.prevent="startMiddleDrag"
 | 
			
		||||
      @mousemove="onDrag" 
 | 
			
		||||
      @mouseup="stopDrag" 
 | 
			
		||||
      @mouseleave="stopDrag"
 | 
			
		||||
      @wheel="onZoom">
 | 
			
		||||
    <div 
 | 
			
		||||
      ref="canvas" 
 | 
			
		||||
      class="diagram-canvas" 
 | 
			
		||||
      :style="{transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`}">
 | 
			
		||||
      <!-- wokwi-elements FPGA开发板 -->
 | 
			
		||||
      <wokwi-fpga-board class="absolute top-10 left-10" style="width:600px;height:400px;"></wokwi-fpga-board>
 | 
			
		||||
        <!-- 放置其他元器件的区域 -->
 | 
			
		||||
      <div v-for="component in components" :key="component.id" 
 | 
			
		||||
          class="absolute cursor-move component-wrapper" 
 | 
			
		||||
          :class="{
 | 
			
		||||
            'component-hover': hoveredComponent === component.id,
 | 
			
		||||
            'component-selected': selectedComponent === component.id
 | 
			
		||||
          }"
 | 
			
		||||
          :style="{top: component.y + 'px', left: component.x + 'px', width: 'auto', height: 'auto'}"
 | 
			
		||||
          @mousedown.left.stop="startComponentDrag($event, component)"
 | 
			
		||||
          @mouseover="hoveredComponent = component.id"
 | 
			
		||||
          @mouseleave="hoveredComponent = null">
 | 
			
		||||
        <component :is="component.type"></component>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- 缩放指示器 -->
 | 
			
		||||
    <div class="absolute bottom-2 right-2 bg-base-100 px-2 py-1 rounded-md opacity-70">
 | 
			
		||||
      {{ Math.round(scale * 100) }}%
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// 引入wokwi-elements
 | 
			
		||||
import "@wokwi/elements";
 | 
			
		||||
import { ref, reactive, onMounted, watch } from 'vue';
 | 
			
		||||
 | 
			
		||||
// 定义组件接受的属性
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  initialComponents?: Array<ComponentItem>,
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits(['component-selected', 'component-moved']);
 | 
			
		||||
 | 
			
		||||
// 定义组件接口
 | 
			
		||||
interface ComponentItem {
 | 
			
		||||
  id: string;
 | 
			
		||||
  type: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 画布位置和缩放
 | 
			
		||||
const position = reactive({ x: 0, y: 0 });
 | 
			
		||||
const scale = ref(1);
 | 
			
		||||
const isDragging = ref(false);
 | 
			
		||||
const isMiddleDragging = ref(false); // 是否在使用中键拖拽
 | 
			
		||||
const dragStart = reactive({ x: 0, y: 0 });
 | 
			
		||||
const canvas = ref(null);
 | 
			
		||||
const canvasContainer = ref(null);
 | 
			
		||||
 | 
			
		||||
// 元器件管理
 | 
			
		||||
const components = ref<ComponentItem[]>([]);
 | 
			
		||||
const draggingComponent = ref<ComponentItem | null>(null);
 | 
			
		||||
 | 
			
		||||
// 监听props变化,更新本地组件数组
 | 
			
		||||
watch(() => props.initialComponents, (newComponents) => {
 | 
			
		||||
  if (newComponents) {
 | 
			
		||||
    // 通过创建深拷贝来断开引用连接
 | 
			
		||||
    components.value = JSON.parse(JSON.stringify(newComponents));
 | 
			
		||||
  }
 | 
			
		||||
}, { immediate: true, deep: true });
 | 
			
		||||
const componentDragOffset = reactive({ x: 0, y: 0 });
 | 
			
		||||
const hoveredComponent = ref(null); // 鼠标悬停的元器件ID
 | 
			
		||||
const selectedComponent = ref(null); // 当前选中的元器件ID
 | 
			
		||||
 | 
			
		||||
// 画布拖拽
 | 
			
		||||
function startDrag(e) {
 | 
			
		||||
  // 只处理左键拖拽
 | 
			
		||||
  if (e.button !== 0) return;
 | 
			
		||||
 | 
			
		||||
  // 确保其他拖拽状态被重置
 | 
			
		||||
  isMiddleDragging.value = false;
 | 
			
		||||
  
 | 
			
		||||
  isDragging.value = true;
 | 
			
		||||
  dragStart.x = e.clientX - position.x;
 | 
			
		||||
  dragStart.y = e.clientY - position.y;
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 中键拖拽画布(无论点击到哪里)
 | 
			
		||||
function startMiddleDrag(e) {
 | 
			
		||||
  // 确保是中键
 | 
			
		||||
  if (e.button !== 1) return;
 | 
			
		||||
  
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
  e.stopPropagation();
 | 
			
		||||
  
 | 
			
		||||
  // 确保其他拖拽状态被重置
 | 
			
		||||
  isDragging.value = false;
 | 
			
		||||
  
 | 
			
		||||
  // 设置中键拖拽状态
 | 
			
		||||
  isMiddleDragging.value = true;
 | 
			
		||||
  
 | 
			
		||||
  // 记录起始位置
 | 
			
		||||
  dragStart.x = e.clientX - position.x;
 | 
			
		||||
  dragStart.y = e.clientY - position.y;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理画布鼠标按下事件
 | 
			
		||||
function handleCanvasMouseDown(e) {
 | 
			
		||||
  // 如果不是左键,则不做处理
 | 
			
		||||
  if (e.button !== 0) return;
 | 
			
		||||
  
 | 
			
		||||
  // 如果是直接点击画布(而不是元器件),清除选中状态
 | 
			
		||||
  if (e.target === canvasContainer.value || e.target === canvas.value) {
 | 
			
		||||
    selectedComponent.value = null;
 | 
			
		||||
    emit('component-selected', null);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 继续处理拖拽
 | 
			
		||||
  startDrag(e);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onDrag(e) {
 | 
			
		||||
  // 如果左键或中键拖拽正在进行中
 | 
			
		||||
  if (isDragging.value || isMiddleDragging.value) {
 | 
			
		||||
    // 防止拖拽过程中选中文本
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    
 | 
			
		||||
    position.x = e.clientX - dragStart.x;
 | 
			
		||||
    position.y = e.clientY - dragStart.y;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stopDrag() {
 | 
			
		||||
  isDragging.value = false;
 | 
			
		||||
  isMiddleDragging.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 画布缩放
 | 
			
		||||
function onZoom(e) {
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
  const delta = e.deltaY > 0 ? -0.1 : 0.1;
 | 
			
		||||
  const newScale = Math.max(0.3, Math.min(3, scale.value + delta));
 | 
			
		||||
  
 | 
			
		||||
  // 保持鼠标位置不变的缩放
 | 
			
		||||
  if (canvas.value && canvasContainer.value) {
 | 
			
		||||
    const rect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
    const mouseX = e.clientX - rect.left;
 | 
			
		||||
    const mouseY = e.clientY - rect.top;
 | 
			
		||||
    
 | 
			
		||||
    // 计算鼠标在画布中的相对位置
 | 
			
		||||
    const mouseXInCanvas = (mouseX - position.x) / scale.value;
 | 
			
		||||
    const mouseYInCanvas = (mouseY - position.y) / scale.value;
 | 
			
		||||
    
 | 
			
		||||
    // 调整位置以保持鼠标位置不变
 | 
			
		||||
    position.x = mouseX - mouseXInCanvas * newScale;
 | 
			
		||||
    position.y = mouseY - mouseYInCanvas * newScale;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  scale.value = newScale;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 元器件拖拽
 | 
			
		||||
function startComponentDrag(e, component) {
 | 
			
		||||
  // 确保只处理左键拖拽元器件
 | 
			
		||||
  if (e.button !== 0) return;
 | 
			
		||||
  
 | 
			
		||||
  e.stopPropagation();
 | 
			
		||||
  draggingComponent.value = component;
 | 
			
		||||
  
 | 
			
		||||
  // 设置选中元器件并通知父组件
 | 
			
		||||
  selectedComponent.value = component.id;
 | 
			
		||||
  emit('component-selected', component);
 | 
			
		||||
  
 | 
			
		||||
  // 保存起始位置和鼠标位置
 | 
			
		||||
  const initialX = component.x;
 | 
			
		||||
  const initialY = component.y;
 | 
			
		||||
  const startX = e.clientX;
 | 
			
		||||
  const startY = e.clientY;
 | 
			
		||||
  
 | 
			
		||||
  const mouseMoveHandler = (moveEvent) => {
 | 
			
		||||
    if (!draggingComponent.value) return;
 | 
			
		||||
    
 | 
			
		||||
    // 计算鼠标移动的距离(在屏幕坐标系中)
 | 
			
		||||
    const dx = moveEvent.clientX - startX;
 | 
			
		||||
    const dy = moveEvent.clientY - startY;
 | 
			
		||||
    
 | 
			
		||||
    // 将移动距离转换为画布坐标系中的距离
 | 
			
		||||
    const canvasDx = dx / scale.value;
 | 
			
		||||
    const canvasDy = dy / scale.value;
 | 
			
		||||
    
 | 
			
		||||
    // 更新组件位置(相对于初始位置的增量)
 | 
			
		||||
    draggingComponent.value.x = initialX + canvasDx;
 | 
			
		||||
    draggingComponent.value.y = initialY + canvasDy;
 | 
			
		||||
    
 | 
			
		||||
    // 通知父组件元器件位置已变化
 | 
			
		||||
    emit('component-moved', { 
 | 
			
		||||
      id: draggingComponent.value.id,
 | 
			
		||||
      x: draggingComponent.value.x,
 | 
			
		||||
      y: draggingComponent.value.y
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  const mouseUpHandler = () => {
 | 
			
		||||
    draggingComponent.value = null;
 | 
			
		||||
    // 保持选中状态,不清除 selectedComponent
 | 
			
		||||
    window.removeEventListener('mousemove', mouseMoveHandler);
 | 
			
		||||
    window.removeEventListener('mouseup', mouseUpHandler);
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  window.addEventListener('mousemove', mouseMoveHandler);
 | 
			
		||||
  window.addEventListener('mouseup', mouseUpHandler);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 自定义FPGA开发板组件(未实现,只是示例)
 | 
			
		||||
customElements.define('wokwi-fpga-board', class extends HTMLElement {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super();
 | 
			
		||||
    this.attachShadow({ mode: 'open' });
 | 
			
		||||
    this.shadowRoot.innerHTML = `
 | 
			
		||||
      <style>
 | 
			
		||||
        :host {
 | 
			
		||||
          display: block;
 | 
			
		||||
          width: 600px;
 | 
			
		||||
          height: 400px;
 | 
			
		||||
          background-color: #2e7d32;
 | 
			
		||||
          border: 8px solid #1b5e20;
 | 
			
		||||
          border-radius: 10px;
 | 
			
		||||
          position: relative;
 | 
			
		||||
        }
 | 
			
		||||
        .board-label {
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          top: 20px;
 | 
			
		||||
          left: 20px;
 | 
			
		||||
          color: white;
 | 
			
		||||
          font-family: Arial, sans-serif;
 | 
			
		||||
          font-size: 18px;
 | 
			
		||||
          font-weight: bold;
 | 
			
		||||
        }
 | 
			
		||||
        .board-components {
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          top: 60px;
 | 
			
		||||
          left: 40px;
 | 
			
		||||
          right: 40px;
 | 
			
		||||
          bottom: 40px;
 | 
			
		||||
          background-color: #388e3c;
 | 
			
		||||
          border-radius: 5px;
 | 
			
		||||
        }
 | 
			
		||||
        .pins {
 | 
			
		||||
          position: absolute;
 | 
			
		||||
          height: 10px;
 | 
			
		||||
          display: flex;
 | 
			
		||||
          gap: 10px;
 | 
			
		||||
        }
 | 
			
		||||
        .pins.top {
 | 
			
		||||
          top: 0;
 | 
			
		||||
          left: 80px;
 | 
			
		||||
          right: 80px;
 | 
			
		||||
        }
 | 
			
		||||
        .pins.bottom {
 | 
			
		||||
          bottom: 0;
 | 
			
		||||
          left: 80px;
 | 
			
		||||
          right: 80px;
 | 
			
		||||
        }
 | 
			
		||||
        .pin {
 | 
			
		||||
          width: 10px;
 | 
			
		||||
          height: 20px;
 | 
			
		||||
          background-color: silver;
 | 
			
		||||
          border-radius: 2px;
 | 
			
		||||
        }
 | 
			
		||||
      </style>
 | 
			
		||||
      <div class="board-label">FPGA开发板</div>
 | 
			
		||||
      <div class="board-components"></div>
 | 
			
		||||
      <div class="pins top">
 | 
			
		||||
        ${Array(20).fill().map(() => '<div class="pin"></div>').join('')}
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="pins bottom">
 | 
			
		||||
        ${Array(20).fill().map(() => '<div class="pin"></div>').join('')}
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 公开方法,允许外部添加组件
 | 
			
		||||
function addComponent(component: ComponentItem) {
 | 
			
		||||
  components.value.push(component);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 公开方法,允许外部重置画布
 | 
			
		||||
function resetCanvas() {
 | 
			
		||||
  position.x = 0;
 | 
			
		||||
  position.y = 0;
 | 
			
		||||
  scale.value = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 暴露给父组件的方法
 | 
			
		||||
defineExpose({
 | 
			
		||||
  addComponent,
 | 
			
		||||
  resetCanvas
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.diagram-canvas {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 4000px;
 | 
			
		||||
  height: 4000px;
 | 
			
		||||
  transform-origin: 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;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -3,13 +3,15 @@ import LoginView from "../views/LoginView.vue";
 | 
			
		||||
import UserView from "../views/UserView.vue";
 | 
			
		||||
import TestView from "../views/TestView.vue";
 | 
			
		||||
import JtagTest from "../views/JtagTest.vue";
 | 
			
		||||
import ProjectView from "../views/ProjectView.vue";
 | 
			
		||||
 | 
			
		||||
const routes = [
 | 
			
		||||
  { path: "/", redirect: "/user" },
 | 
			
		||||
  { path: "/login", name: "Login", component: LoginView },
 | 
			
		||||
  { path: "/user", name: "User", component: UserView },
 | 
			
		||||
  { path: "/test", name: "Test", component: TestView },
 | 
			
		||||
  { path: "/test/jtag", name:"JtagTest", component: JtagTest}
 | 
			
		||||
  { path: "/test/jtag", name:"JtagTest", component: JtagTest},
 | 
			
		||||
  { path: "/project", name: "Project", component: ProjectView } // 新增工程界面
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										137
									
								
								src/views/ProjectView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/views/ProjectView.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,137 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="h-screen w-screen flex flex-col">
 | 
			
		||||
    <!-- 顶部工具栏 -->
 | 
			
		||||
    <div class="flex items-center p-2 border-b border-base-300 bg-base-100">
 | 
			
		||||
      <h2 class="text-xl font-bold mr-auto">FPGA 工程界面</h2>
 | 
			
		||||
      <button class="btn btn-circle btn-primary" @click="openComponentsMenu">
 | 
			
		||||
        <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>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-1 overflow-hidden">
 | 
			
		||||
      <!-- 左侧图形化区域 -->
 | 
			
		||||
      <DiagramCanvas 
 | 
			
		||||
        ref="diagramCanvas"
 | 
			
		||||
        :initialComponents="components"
 | 
			
		||||
        @component-selected="handleComponentSelected"
 | 
			
		||||
        @component-moved="handleComponentMoved"
 | 
			
		||||
      />
 | 
			
		||||
      
 | 
			
		||||
      <!-- 右侧编辑区域 -->
 | 
			
		||||
      <div class="w-[40%] bg-base-100 border-l flex flex-col p-4">
 | 
			
		||||
        <h3 class="text-lg font-bold mb-4">属性编辑器</h3>
 | 
			
		||||
        <div v-if="!selectedComponent" class="text-gray-400">选择元器件以编辑属性</div>
 | 
			
		||||
        <div v-else>
 | 
			
		||||
          <div class="mb-2">编辑元器件: {{ getComponentName(selectedComponent) }}</div>
 | 
			
		||||
          <!-- 这里可以添加元器件的属性编辑表单 -->
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 元器件选择菜单 -->
 | 
			
		||||
    <div v-if="showComponentsMenu" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" @click.self="showComponentsMenu = false">
 | 
			
		||||
      <div class="bg-base-100 p-4 rounded-lg shadow-xl max-w-3xl max-h-[80vh] overflow-auto">
 | 
			
		||||
        <div class="flex justify-between items-center mb-4">
 | 
			
		||||
          <h3 class="text-xl font-bold">选择元器件</h3>
 | 
			
		||||
          <button class="btn btn-ghost btn-sm" @click="showComponentsMenu = false">
 | 
			
		||||
            <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="18" y1="6" x2="6" y2="18"></line>
 | 
			
		||||
              <line x1="6" y1="6" x2="18" y2="18"></line>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="grid grid-cols-2 md:grid-cols-3 gap-4">
 | 
			
		||||
          <div v-for="(component, index) in availableComponents" :key="index" 
 | 
			
		||||
              class="border p-4 rounded-lg flex flex-col items-center hover:bg-base-200 cursor-pointer"
 | 
			
		||||
              @click="addComponent(component)">
 | 
			
		||||
            <div class="flex-1 flex items-center justify-center p-2">
 | 
			
		||||
              <component :is="component.type" style="transform: scale(0.5);"></component>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="mt-2 text-center">{{ component.name }}</div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// 引入wokwi-elements和组件
 | 
			
		||||
import "@wokwi/elements";
 | 
			
		||||
import { ref, reactive } from 'vue';
 | 
			
		||||
import DiagramCanvas from '@/components/DiagramCanvas.vue';
 | 
			
		||||
 | 
			
		||||
// 元器件管理
 | 
			
		||||
const showComponentsMenu = ref(false);
 | 
			
		||||
interface ComponentItem {
 | 
			
		||||
  id: string;
 | 
			
		||||
  type: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
}
 | 
			
		||||
const components = ref<ComponentItem[]>([]);
 | 
			
		||||
const selectedComponent = ref<string | null>(null);
 | 
			
		||||
const selectedComponentData = ref<ComponentItem | null>(null);
 | 
			
		||||
const diagramCanvas = ref(null);
 | 
			
		||||
 | 
			
		||||
// 可用元器件列表
 | 
			
		||||
const availableComponents = [
 | 
			
		||||
  { type: 'wokwi-led', name: 'LED灯' },
 | 
			
		||||
  { type: 'wokwi-resistor', name: '电阻' },
 | 
			
		||||
  { type: 'wokwi-pushbutton', name: '按钮' },
 | 
			
		||||
  { type: 'wokwi-7segment', name: '7段数码管' },
 | 
			
		||||
  { type: 'wokwi-arduino-uno', name: 'Arduino Uno' },
 | 
			
		||||
  { type: 'wokwi-servo', name: '舵机' },
 | 
			
		||||
  { type: 'wokwi-lcd1602', name: 'LCD显示屏' },
 | 
			
		||||
  { type: 'wokwi-dht22', name: '温湿度传感器' },
 | 
			
		||||
  { type: 'wokwi-buzzer', name: '蜂鸣器' }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// 打开元器件选择菜单
 | 
			
		||||
function openComponentsMenu() {
 | 
			
		||||
  showComponentsMenu.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加新元器件
 | 
			
		||||
function addComponent(componentTemplate) {
 | 
			
		||||
  const newComponent = {
 | 
			
		||||
    id: `component-${Date.now()}`,
 | 
			
		||||
    type: componentTemplate.type,
 | 
			
		||||
    name: componentTemplate.name,
 | 
			
		||||
    x: 100,
 | 
			
		||||
    y: 100
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  components.value.push(newComponent);
 | 
			
		||||
  // 由于我们使用的是响应式数据绑定,不需要再次调用 diagramCanvas 的 addComponent
 | 
			
		||||
  // DiagramCanvas 组件通过 :initialComponents="components" 已经接收到更新
 | 
			
		||||
  
 | 
			
		||||
  showComponentsMenu.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理组件选中事件
 | 
			
		||||
function handleComponentSelected(component) {
 | 
			
		||||
  selectedComponent.value = component ? component.id : null;
 | 
			
		||||
  selectedComponentData.value = component;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理组件移动事件
 | 
			
		||||
function handleComponentMoved(moveData) {
 | 
			
		||||
  const component = components.value.find(c => c.id === moveData.id);
 | 
			
		||||
  if (component) {
 | 
			
		||||
    component.x = moveData.x;
 | 
			
		||||
    component.y = moveData.y;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取组件名称
 | 
			
		||||
function getComponentName(componentId) {
 | 
			
		||||
  const component = components.value.find(c => c.id === componentId);
 | 
			
		||||
  return component ? component.name : '';
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
		Reference in New Issue
	
	Block a user