feat: remake most of forntend
							
								
								
									
										93
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -8,9 +8,8 @@
 | 
			
		||||
      "name": "fpga-weblab",
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@svgdotjs/svg.js": "^3.2.4",
 | 
			
		||||
        "@types/lodash": "^4.17.16",
 | 
			
		||||
        "@wokwi/elements": "^1.7.0",
 | 
			
		||||
        "all": "^0.0.0",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "log-symbols": "^7.0.0",
 | 
			
		||||
        "pinia": "^3.0.1",
 | 
			
		||||
@@ -999,21 +998,6 @@
 | 
			
		||||
        "@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",
 | 
			
		||||
@@ -1344,6 +1328,16 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@svgdotjs/svg.js": {
 | 
			
		||||
      "version": "3.2.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz",
 | 
			
		||||
      "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "github",
 | 
			
		||||
        "url": "https://github.com/sponsors/Fuzzyma"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@tailwindcss/node": {
 | 
			
		||||
      "version": "4.1.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
 | 
			
		||||
@@ -1642,21 +1636,6 @@
 | 
			
		||||
        "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",
 | 
			
		||||
@@ -1997,19 +1976,6 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "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",
 | 
			
		||||
@@ -2017,12 +1983,6 @@
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/all": {
 | 
			
		||||
      "version": "0.0.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/all/-/all-0.0.0.tgz",
 | 
			
		||||
      "integrity": "sha512-0oKlfNVv2d+d7c1gwjGspzgbwot47PGQ4b3v1ccx4mR8l9P/Y6E6Dr/yE8lNT63EcAKEbHo6UG3odDpC/NQcKw==",
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/ansi-styles": {
 | 
			
		||||
      "version": "6.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
 | 
			
		||||
@@ -3019,37 +2979,6 @@
 | 
			
		||||
        "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",
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,8 @@
 | 
			
		||||
    "postgen-api": "pkill server"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@svgdotjs/svg.js": "^3.2.4",
 | 
			
		||||
    "@types/lodash": "^4.17.16",
 | 
			
		||||
    "@wokwi/elements": "^1.7.0",
 | 
			
		||||
    "all": "^0.0.0",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "log-symbols": "^7.0.0",
 | 
			
		||||
    "pinia": "^3.0.1",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						@@ -1,39 +1,56 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import iconMenu from "./assets/menu.svg";
 | 
			
		||||
import Sidebar from "./components/Sidebar.vue";
 | 
			
		||||
import Navbar from "./components/Navbar.vue";
 | 
			
		||||
import { useThemeStore } from "./stores/theme";
 | 
			
		||||
import { ref, provide, onMounted } from "vue";
 | 
			
		||||
 | 
			
		||||
const theme = useThemeStore();
 | 
			
		||||
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" }, // 新增工程界面入口
 | 
			
		||||
];
 | 
			
		||||
// 主题切换状态管理
 | 
			
		||||
const isDarkMode = ref(window.matchMedia('(prefers-color-scheme: dark)').matches);
 | 
			
		||||
 | 
			
		||||
// 初始化主题设置
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 应用初始主题
 | 
			
		||||
  applyTheme();
 | 
			
		||||
  
 | 
			
		||||
  // 监听系统主题变化
 | 
			
		||||
  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
 | 
			
		||||
    // 跟随系统变化
 | 
			
		||||
    isDarkMode.value = e.matches;
 | 
			
		||||
    applyTheme();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 应用主题到文档
 | 
			
		||||
const applyTheme = () => {
 | 
			
		||||
  document.documentElement.setAttribute('data-theme', isDarkMode.value ? 'night' : 'winter');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 切换主题
 | 
			
		||||
const toggleTheme = () => {
 | 
			
		||||
  isDarkMode.value = !isDarkMode.value;
 | 
			
		||||
  applyTheme();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 提供主题状态和切换方法给子组件
 | 
			
		||||
provide('theme', {
 | 
			
		||||
  isDarkMode,
 | 
			
		||||
  toggleTheme
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div :data-theme="theme.currentTheme">
 | 
			
		||||
  <div>
 | 
			
		||||
    <header class="relative">
 | 
			
		||||
      <div class="fixed left-0 top-0 z-50 hidden">
 | 
			
		||||
        <Sidebar :items="items" />
 | 
			
		||||
      </div>
 | 
			
		||||
      <Navbar></Navbar>
 | 
			
		||||
    </header>
 | 
			
		||||
 | 
			
		||||
    <main>
 | 
			
		||||
      <RouterView />
 | 
			
		||||
    </main>
 | 
			
		||||
 | 
			
		||||
    <footer class="footer footer-center p-4 bg-base-300 text-base-content">
 | 
			
		||||
    </main>    <footer class="footer footer-center p-4 bg-base-300 text-base-content">
 | 
			
		||||
      <div>
 | 
			
		||||
        <p>Copyright © 2023 - All right reserved by OurEDA</p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </footer>
 | 
			
		||||
  </div>
 | 
			
		||||
    </footer>  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
@import "./assets/main.css";
 | 
			
		||||
/* 特定于App.vue的样式 */
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,3 +6,26 @@
 | 
			
		||||
 | 
			
		||||
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
 | 
			
		||||
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));
 | 
			
		||||
 | 
			
		||||
/* 禁止所有图像和SVG选择 */
 | 
			
		||||
img, svg {
 | 
			
		||||
  user-select: none !important;
 | 
			
		||||
  -webkit-user-select: none !important;
 | 
			
		||||
  -moz-user-select: none !important;
 | 
			
		||||
  -ms-user-select: none !important;
 | 
			
		||||
  pointer-events: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 允许组件中的交互元素接收事件 */
 | 
			
		||||
.component-wrapper img, 
 | 
			
		||||
.component-wrapper svg {
 | 
			
		||||
  pointer-events: auto !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 禁止双击选择文本 */
 | 
			
		||||
.no-select {
 | 
			
		||||
  user-select: none !important;
 | 
			
		||||
  -webkit-user-select: none !important;
 | 
			
		||||
  -moz-user-select: none !important;
 | 
			
		||||
  -ms-user-select: none !important;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										277
									
								
								src/components/ComponentSelector.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,277 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <!-- 元器件选择菜单 (Drawer) -->
 | 
			
		||||
    <div class="drawer drawer-end z-50">
 | 
			
		||||
      <input id="component-drawer" type="checkbox" class="drawer-toggle" v-model="showComponentsMenu" />
 | 
			
		||||
      <div class="drawer-side">
 | 
			
		||||
        <label for="component-drawer" aria-label="close sidebar" class="drawer-overlay !bg-opacity-50"></label>
 | 
			
		||||
        <div class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col">
 | 
			
		||||
          <!-- 菜单头部 -->
 | 
			
		||||
          <div class="p-6 border-b border-base-300 flex justify-between items-center">
 | 
			
		||||
            <h3 class="text-xl font-bold text-primary flex items-center gap-2">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-primary">
 | 
			
		||||
                <circle cx="12" cy="12" r="10"></circle>
 | 
			
		||||
                <path d="M12 8v8"></path>
 | 
			
		||||
                <path d="M8 12h8"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              添加元器件
 | 
			
		||||
            </h3>
 | 
			
		||||
            <label for="component-drawer" class="btn btn-ghost btn-sm btn-circle" @click="closeMenu">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" 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>
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          <!-- 搜索框 -->
 | 
			
		||||
          <div class="px-6 py-4 border-b border-base-300">
 | 
			
		||||
            <div class="join w-full">
 | 
			
		||||
              <div class="join-item flex-1 relative">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="text"
 | 
			
		||||
                  placeholder="搜索元器件..."
 | 
			
		||||
                  class="input input-bordered input-sm w-full pl-10"
 | 
			
		||||
                  v-model="searchQuery"
 | 
			
		||||
                  @keyup.enter="searchComponents"
 | 
			
		||||
                />
 | 
			
		||||
                <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content opacity-60">
 | 
			
		||||
                  <circle cx="11" cy="11" r="8"></circle>
 | 
			
		||||
                  <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
 | 
			
		||||
                </svg>
 | 
			
		||||
              </div>
 | 
			
		||||
              <button class="btn btn-sm join-item" @click="searchComponents">搜索</button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          <!-- 元器件列表 -->
 | 
			
		||||
          <div class="px-6 py-4 overflow-auto flex-1">
 | 
			
		||||
            <div v-if="filteredComponents.length > 0" class="grid grid-cols-2 gap-4">
 | 
			
		||||
              <div v-for="(component, index) in filteredComponents" :key="index"
 | 
			
		||||
                  class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
 | 
			
		||||
                  @click="addComponent(component)">
 | 
			
		||||
                <div class="card-body p-3 items-center text-center">
 | 
			
		||||
                  <div class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2">
 | 
			
		||||
                    <!-- 直接使用组件作为预览 -->
 | 
			
		||||
                    <component 
 | 
			
		||||
                      v-if="componentModules[component.type]"
 | 
			
		||||
                      :is="componentModules[component.type].default"
 | 
			
		||||
                      class="component-preview"
 | 
			
		||||
                      :size="getPreviewSize(component.type)"
 | 
			
		||||
                    />
 | 
			
		||||
                    <!-- 加载中状态 -->
 | 
			
		||||
                    <span v-else class="text-xs text-gray-400">加载中...</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <h3 class="card-title text-sm mt-2">{{ component.name }}</h3>
 | 
			
		||||
                  <p class="text-xs opacity-70">{{ component.type }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- 无搜索结果 -->
 | 
			
		||||
            <div v-else class="py-16 text-center">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="mx-auto text-base-300 mb-3">
 | 
			
		||||
                <circle cx="11" cy="11" r="8"></circle>
 | 
			
		||||
                <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
 | 
			
		||||
                <line x1="8" y1="11" x2="14" y2="11"></line>
 | 
			
		||||
              </svg>
 | 
			
		||||
              <p class="text-base-content opacity-70">没有找到匹配的元器件</p>
 | 
			
		||||
              <button class="btn btn-sm btn-ghost mt-3" @click="searchQuery = ''">
 | 
			
		||||
                清除搜索
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
          
 | 
			
		||||
          <!-- 底部操作区 -->
 | 
			
		||||
          <div class="p-4 border-t border-base-300 bg-base-200 flex justify-between">
 | 
			
		||||
            <label for="component-drawer" class="btn btn-sm btn-ghost" @click="closeMenu">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-1">
 | 
			
		||||
                <path d="M19 12H5M12 19l-7-7 7-7"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              返回
 | 
			
		||||
            </label>
 | 
			
		||||
            <label for="component-drawer" class="btn btn-sm btn-primary" @click="closeMenu">
 | 
			
		||||
              完成
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed, shallowRef, onMounted } from 'vue';
 | 
			
		||||
import { getComponentConfig } from '@/components/equipments/componentConfig';
 | 
			
		||||
 | 
			
		||||
// Props 定义
 | 
			
		||||
interface Props {
 | 
			
		||||
  open: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<Props>();
 | 
			
		||||
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits(['close', 'add-component', 'update:open']);
 | 
			
		||||
 | 
			
		||||
// --- 搜索功能 ---
 | 
			
		||||
const searchQuery = ref('');
 | 
			
		||||
 | 
			
		||||
// --- 可用元器件列表 ---
 | 
			
		||||
const availableComponents = [
 | 
			
		||||
  { type: 'MechanicalButton', name: '机械按钮' },
 | 
			
		||||
  { type: 'Switch', name: '开关' },
 | 
			
		||||
  { type: 'Pin', name: '引脚' },
 | 
			
		||||
  { type: 'SMT_LED', name: '贴片LED' },
 | 
			
		||||
  { type: 'HDMI', name: 'HDMI接口' },
 | 
			
		||||
  { type: 'DDR', name: 'DDR内存' },
 | 
			
		||||
  { type: 'ETH', name: '以太网接口' },
 | 
			
		||||
  { type: 'SD', name: 'SD卡插槽' },
 | 
			
		||||
  { type: 'SFP', name: 'SFP光纤模块' },
 | 
			
		||||
  { type: 'SMA', name: 'SMA连接器' },
 | 
			
		||||
  { type: 'MotherBoard', name: '主板' }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// 显示/隐藏组件菜单
 | 
			
		||||
const showComponentsMenu = computed({
 | 
			
		||||
  get: () => props.open,
 | 
			
		||||
  set: (value) => emit('update:open', value)
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 组件模块缓存
 | 
			
		||||
const componentModules = shallowRef<Record<string, any>>({});
 | 
			
		||||
 | 
			
		||||
// 动态加载组件定义
 | 
			
		||||
async function loadComponentModule(type: string) {
 | 
			
		||||
  if (!componentModules.value[type]) {
 | 
			
		||||
    try {
 | 
			
		||||
      // 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
 | 
			
		||||
      const module = await import(`../components/equipments/${type}.vue`);
 | 
			
		||||
      
 | 
			
		||||
      // 将模块添加到缓存中
 | 
			
		||||
      componentModules.value = {
 | 
			
		||||
        ...componentModules.value,
 | 
			
		||||
        [type]: module
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      console.log(`Loaded module for ${type}:`, module);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`Failed to load component module ${type}:`, error);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return componentModules.value[type];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 预加载组件模块
 | 
			
		||||
async function preloadComponentModules() {
 | 
			
		||||
  for (const component of availableComponents) {
 | 
			
		||||
    try {
 | 
			
		||||
      await loadComponentModule(component.type);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`Failed to preload component ${component.type}:`, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取组件预览时适合的尺寸
 | 
			
		||||
function getPreviewSize(componentType: string): number {
 | 
			
		||||
  // 根据组件类型返回适当的预览尺寸
 | 
			
		||||
  const previewSizes: Record<string, number> = {
 | 
			
		||||
    'MechanicalButton': 0.4,  // 按钮较大,需要更小尺寸
 | 
			
		||||
    'Switch': 0.35,           // 开关较大,需要更小尺寸
 | 
			
		||||
    'Pin': 0.8,               // 引脚较小,可以大一些
 | 
			
		||||
    'SMT_LED': 0.7,           // LED可以保持适中
 | 
			
		||||
    'HDMI': 0.5,              // HDMI接口较大
 | 
			
		||||
    'DDR': 0.5,               // DDR内存较大
 | 
			
		||||
    'ETH': 0.5,               // 以太网接口较大
 | 
			
		||||
    'SD': 0.6,                // SD卡插槽适中
 | 
			
		||||
    'SFP': 0.4,               // SFP光纤模块较大
 | 
			
		||||
    'SMA': 0.7,               // SMA连接器可以适中
 | 
			
		||||
    'MotherBoard': 0.13       // 主板最大,需要最小尺寸
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  // 返回对应尺寸,如果没有特定配置则返回默认值0.5
 | 
			
		||||
  return previewSizes[componentType] || 0.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 搜索组件
 | 
			
		||||
function searchComponents() {
 | 
			
		||||
  // 根据用户输入过滤可用组件列表
 | 
			
		||||
  // 实际逻辑已经在 filteredComponents 计算属性中实现
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 关闭菜单
 | 
			
		||||
function closeMenu() {
 | 
			
		||||
  showComponentsMenu.value = false;
 | 
			
		||||
  emit('close');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加新元器件
 | 
			
		||||
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;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 再加载组件模块以便后续使用
 | 
			
		||||
  await loadComponentModule(componentTemplate.type);
 | 
			
		||||
 | 
			
		||||
  // 发送添加组件事件给父组件
 | 
			
		||||
  emit('add-component', {
 | 
			
		||||
    type: componentTemplate.type,
 | 
			
		||||
    name: componentTemplate.name,
 | 
			
		||||
    props: defaultProps
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // 关闭菜单
 | 
			
		||||
  closeMenu();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 过滤后的元器件列表 (用于菜单)
 | 
			
		||||
const filteredComponents = computed(() => {
 | 
			
		||||
  if (!searchQuery.value) {
 | 
			
		||||
    return availableComponents;
 | 
			
		||||
  }
 | 
			
		||||
  const query = searchQuery.value.toLowerCase();
 | 
			
		||||
  return availableComponents.filter(component =>
 | 
			
		||||
    component.name.toLowerCase().includes(query) ||
 | 
			
		||||
    component.type.toLowerCase().includes(query)
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 生命周期钩子
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 预加载组件模块
 | 
			
		||||
  preloadComponentModules();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
/* 组件预览样式 */
 | 
			
		||||
.component-preview {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  max-height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 动画效果 */
 | 
			
		||||
.animate-slideUp {
 | 
			
		||||
  animation: slideUp 0.3s ease-out forwards;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slideUp {
 | 
			
		||||
  from {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transform: translateY(20px);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    transform: translateY(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										80
									
								
								src/components/DiagramCanvas.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,80 @@
 | 
			
		||||
.diagram-canvas {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 4000px;
 | 
			
		||||
  height: 4000px;
 | 
			
		||||
  transform-origin: 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 将网格线应用到容器而不是画布上,确保覆盖整个可视区域 */
 | 
			
		||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
 | 
			
		||||
  background-image: 
 | 
			
		||||
    linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px);
 | 
			
		||||
  background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
 | 
			
		||||
  background-position: 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 为黑暗模式设置不同的网格线颜色 */
 | 
			
		||||
:root[data-theme="dark"] .flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
 | 
			
		||||
  background-image: 
 | 
			
		||||
    linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
 | 
			
		||||
  background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
 | 
			
		||||
  background-position: 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 禁用滚动条 */
 | 
			
		||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto::-webkit-scrollbar {
 | 
			
		||||
  display: none; /* Chrome, Safari, Opera */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto {
 | 
			
		||||
  -ms-overflow-style: none;  /* IE and Edge */
 | 
			
		||||
  scrollbar-width: none;  /* Firefox */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 元器件容器样式 */
 | 
			
		||||
.component-wrapper {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-block; /* 确保元素宽度基于内容 */
 | 
			
		||||
  max-width: fit-content; /* 强制宽度适应内容 */
 | 
			
		||||
  max-height: fit-content; /* 强制高度适应内容 */
 | 
			
		||||
  overflow: visible; /* 允许内容溢出(用于显示边框) */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 悬停状态 */
 | 
			
		||||
.component-hover::before {
 | 
			
		||||
  content: '';
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: -4px;
 | 
			
		||||
  left: -4px;
 | 
			
		||||
  right: -4px;
 | 
			
		||||
  bottom: -4px;
 | 
			
		||||
  border: 3px dashed #3498db;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  box-sizing: content-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 选中状态 */
 | 
			
		||||
.component-selected::before {
 | 
			
		||||
  content: '';
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: -4px;
 | 
			
		||||
  left: -4px;
 | 
			
		||||
  right: -4px;
 | 
			
		||||
  bottom: -4px;
 | 
			
		||||
  border: 4px dashed #e74c3c;
 | 
			
		||||
  border-color: #e74c3c #f39c12 #3498db #2ecc71;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  box-sizing: content-box;
 | 
			
		||||
}
 | 
			
		||||
@@ -1,370 +1,450 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="flex-1 min-w-[60%] bg-base-200 relative overflow-auto" ref="canvasContainer" 
 | 
			
		||||
  <div class="flex-1 min-w-[60%] bg-base-200 relative overflow-hidden diagram-container" ref="canvasContainer"
 | 
			
		||||
      @mousedown="handleCanvasMouseDown"
 | 
			
		||||
      @mousedown.middle.prevent="startMiddleDrag"
 | 
			
		||||
      @mousemove="onDrag" 
 | 
			
		||||
      @mouseup="stopDrag" 
 | 
			
		||||
      @mouseleave="stopDrag"
 | 
			
		||||
      @wheel="onZoom">
 | 
			
		||||
      @wheel.prevent="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" 
 | 
			
		||||
      :style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }">      <!-- 渲染画布上的组件 -->
 | 
			
		||||
      <div v-for="component in props.components" :key="component.id"
 | 
			
		||||
          class="component-wrapper"
 | 
			
		||||
          :class="{
 | 
			
		||||
            'component-hover': hoveredComponent === component.id,
 | 
			
		||||
            'component-selected': selectedComponent === component.id
 | 
			
		||||
            'component-selected': selectedComponentId === component.id
 | 
			
		||||
          }"
 | 
			
		||||
          :style="{ 
 | 
			
		||||
            top: component.y + 'px', 
 | 
			
		||||
            left: component.x + 'px',
 | 
			
		||||
            zIndex: selectedComponentId === component.id ? 999 : 1 
 | 
			
		||||
          }"
 | 
			
		||||
          :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>
 | 
			
		||||
          @mouseleave="hoveredComponent = null"><!-- 动态渲染组件 -->
 | 
			
		||||
        <component
 | 
			
		||||
          :is="getComponentDefinition(component.type)"
 | 
			
		||||
          v-if="props.componentModules[component.type]"
 | 
			
		||||
          v-bind="prepareComponentProps(component.props || {})"
 | 
			
		||||
          @update:bindKey="(value: string) => updateComponentProp(component.id, 'bindKey', value)"
 | 
			
		||||
          :ref="el => { if (el) componentRefs[component.id] = el; }"
 | 
			
		||||
        />
 | 
			
		||||
         
 | 
			
		||||
         <!-- Fallback if component module not loaded yet -->
 | 
			
		||||
         <div v-else class="p-2 text-xs text-gray-400 border border-dashed border-gray-400">
 | 
			
		||||
           Loading {{ component.type }}...
 | 
			
		||||
         </div>
 | 
			
		||||
      </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 class="absolute bottom-4 right-4 bg-base-100 px-3 py-1.5 rounded-md shadow-md z-20" style="opacity: 0.9;">
 | 
			
		||||
      <span class="text-sm font-medium">{{ Math.round(scale * 100) }}%</span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// 引入wokwi-elements
 | 
			
		||||
import "@wokwi/elements";
 | 
			
		||||
import { ref, reactive, onMounted, watch } from 'vue';
 | 
			
		||||
import { ref, reactive, onMounted, onUnmounted } 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;
 | 
			
		||||
  props?: Record<string, any>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 画布位置和缩放
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  components: ComponentItem[],
 | 
			
		||||
  componentModules: Record<string, any>
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits(['component-selected', 'component-moved', 'update-component-prop', 'component-delete']);
 | 
			
		||||
 | 
			
		||||
// --- 画布状态 ---
 | 
			
		||||
const canvasContainer = ref<HTMLElement | null>(null);
 | 
			
		||||
const canvas = ref<HTMLElement | null>(null);
 | 
			
		||||
const position = reactive({ x: 0, y: 0 });
 | 
			
		||||
const scale = ref(1);
 | 
			
		||||
const isDragging = ref(false);
 | 
			
		||||
const isMiddleDragging = 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 selectedComponentId = ref<string | null>(null);
 | 
			
		||||
const hoveredComponent = ref<string | null>(null);
 | 
			
		||||
const draggingComponentId = ref<string | null>(null);
 | 
			
		||||
const componentDragOffset = reactive({ x: 0, y: 0 });
 | 
			
		||||
const hoveredComponent = ref(null); // 鼠标悬停的元器件ID
 | 
			
		||||
const selectedComponent = ref(null); // 当前选中的元器件ID
 | 
			
		||||
 | 
			
		||||
// 画布拖拽
 | 
			
		||||
function startDrag(e) {
 | 
			
		||||
  // 只处理左键拖拽
 | 
			
		||||
  if (e.button !== 0) return;
 | 
			
		||||
// 组件引用跟踪
 | 
			
		||||
const componentRefs = ref<Record<string, any>>({});
 | 
			
		||||
 | 
			
		||||
  // 确保其他拖拽状态被重置
 | 
			
		||||
  isMiddleDragging.value = false;
 | 
			
		||||
// --- 缩放功能 ---
 | 
			
		||||
const MIN_SCALE = 0.2;
 | 
			
		||||
const MAX_SCALE = 3.0;
 | 
			
		||||
 | 
			
		||||
  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) {
 | 
			
		||||
    // 防止拖拽过程中选中文本
 | 
			
		||||
function onZoom(e: WheelEvent) {
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
  
 | 
			
		||||
    position.x = e.clientX - dragStart.x;
 | 
			
		||||
    position.y = e.clientY - dragStart.y;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  
 | 
			
		||||
function stopDrag() {
 | 
			
		||||
  isDragging.value = false;
 | 
			
		||||
  isMiddleDragging.value = false;
 | 
			
		||||
}
 | 
			
		||||
  // 获取容器的位置
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
// 画布缩放
 | 
			
		||||
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));
 | 
			
		||||
  // 计算鼠标在容器内的相对位置
 | 
			
		||||
  const mouseX = e.clientX - containerRect.left;
 | 
			
		||||
  const mouseY = e.clientY - containerRect.top;
 | 
			
		||||
  
 | 
			
		||||
  // 保持鼠标位置不变的缩放
 | 
			
		||||
  if (canvas.value && canvasContainer.value) {
 | 
			
		||||
    const rect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
    const mouseX = e.clientX - rect.left;
 | 
			
		||||
    const mouseY = e.clientY - rect.top;
 | 
			
		||||
  // 计算鼠标在画布坐标系中的位置
 | 
			
		||||
  const mouseXCanvas = (mouseX - position.x) / scale.value;
 | 
			
		||||
  const mouseYCanvas = (mouseY - position.y) / scale.value;
 | 
			
		||||
  
 | 
			
		||||
    // 计算鼠标在画布中的相对位置
 | 
			
		||||
    const mouseXInCanvas = (mouseX - position.x) / scale.value;
 | 
			
		||||
    const mouseYInCanvas = (mouseY - position.y) / scale.value;
 | 
			
		||||
  // 计算缩放值
 | 
			
		||||
  const zoomFactor = 1.1; // 每次放大/缩小10%
 | 
			
		||||
  const direction = e.deltaY > 0 ? -1 : 1;
 | 
			
		||||
  
 | 
			
		||||
    // 调整位置以保持鼠标位置不变
 | 
			
		||||
    position.x = mouseX - mouseXInCanvas * newScale;
 | 
			
		||||
    position.y = mouseY - mouseYInCanvas * newScale;
 | 
			
		||||
  }
 | 
			
		||||
  // 计算新的缩放值
 | 
			
		||||
  let newScale = direction > 0 ? scale.value * zoomFactor : scale.value / zoomFactor;
 | 
			
		||||
  newScale = Math.max(MIN_SCALE, Math.min(newScale, MAX_SCALE));
 | 
			
		||||
  
 | 
			
		||||
  // 计算新的位置,使鼠标指针位置在缩放前后保持不变
 | 
			
		||||
  position.x = mouseX - mouseXCanvas * newScale;
 | 
			
		||||
  position.y = mouseY - mouseYCanvas * newScale;
 | 
			
		||||
  
 | 
			
		||||
  // 更新缩放值
 | 
			
		||||
  scale.value = newScale;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 元器件拖拽
 | 
			
		||||
function startComponentDrag(e, component) {
 | 
			
		||||
  // 确保只处理左键拖拽元器件
 | 
			
		||||
  if (e.button !== 0) return;
 | 
			
		||||
// --- 动态组件渲染 ---
 | 
			
		||||
const getComponentDefinition = (type: string) => {
 | 
			
		||||
  const module = props.componentModules[type];
 | 
			
		||||
  if (!module) return null;
 | 
			
		||||
  
 | 
			
		||||
  e.stopPropagation();
 | 
			
		||||
  draggingComponent.value = component;
 | 
			
		||||
  // 确保我们返回一个有效的组件定义
 | 
			
		||||
  if (module.default) {
 | 
			
		||||
    return module.default;
 | 
			
		||||
  } else if (module.__esModule && module.default) {
 | 
			
		||||
    // 有时 Vue 的动态导入会将默认导出包装在 __esModule 属性下
 | 
			
		||||
    return module.default;
 | 
			
		||||
  } else {
 | 
			
		||||
    console.warn(`Module for ${type} found but default export is missing`, module);
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
  // 设置选中元器件并通知父组件
 | 
			
		||||
  selectedComponent.value = component.id;
 | 
			
		||||
// 准备组件属性,确保类型正确
 | 
			
		||||
function prepareComponentProps(props: Record<string, any>): Record<string, any> {
 | 
			
		||||
  const result: Record<string, any> = {};
 | 
			
		||||
  for (const key in props) {
 | 
			
		||||
    let value = props[key];
 | 
			
		||||
    // 只要不是 null/undefined 且不是 string,就强制转字符串
 | 
			
		||||
    if (
 | 
			
		||||
      (key === 'style' || key === 'direction' || key === 'type') &&
 | 
			
		||||
      value != null &&
 | 
			
		||||
      typeof value !== 'string'
 | 
			
		||||
    ) {
 | 
			
		||||
      value = String(value);
 | 
			
		||||
    }
 | 
			
		||||
    result[key] = value;
 | 
			
		||||
  }
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 画布交互逻辑 ---
 | 
			
		||||
function handleCanvasMouseDown(e: MouseEvent) {
 | 
			
		||||
  // 如果是直接点击画布(而不是元器件),清除选中状态
 | 
			
		||||
  if (e.target === canvasContainer.value || e.target === canvas.value) {
 | 
			
		||||
    if (selectedComponentId.value !== null) {
 | 
			
		||||
      selectedComponentId.value = null;
 | 
			
		||||
      emit('component-selected', null);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 左键拖拽画布逻辑
 | 
			
		||||
  if (e.button === 0 && (e.target === canvasContainer.value || e.target === canvas.value)) {
 | 
			
		||||
    startDrag(e);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 左键拖拽画布
 | 
			
		||||
function startDrag(e: MouseEvent) {
 | 
			
		||||
  if (e.button !== 0 || draggingComponentId.value) return;
 | 
			
		||||
 | 
			
		||||
  isDragging.value = true;
 | 
			
		||||
  isMiddleDragging.value = false;
 | 
			
		||||
  dragStart.x = e.clientX - position.x;
 | 
			
		||||
  dragStart.y = e.clientY - position.y;
 | 
			
		||||
  
 | 
			
		||||
  document.addEventListener('mousemove', onDrag);
 | 
			
		||||
  document.addEventListener('mouseup', stopDrag);
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 中键拖拽画布
 | 
			
		||||
function startMiddleDrag(e: MouseEvent) {
 | 
			
		||||
  if (e.button !== 1) return;
 | 
			
		||||
 | 
			
		||||
  isMiddleDragging.value = true;
 | 
			
		||||
  isDragging.value = false;
 | 
			
		||||
  draggingComponentId.value = null;
 | 
			
		||||
  
 | 
			
		||||
  dragStart.x = e.clientX - position.x;
 | 
			
		||||
  dragStart.y = e.clientY - position.y;
 | 
			
		||||
  
 | 
			
		||||
  document.addEventListener('mousemove', onDrag);
 | 
			
		||||
  document.addEventListener('mouseup', stopDrag);
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 拖拽画布过程
 | 
			
		||||
function onDrag(e: MouseEvent) {
 | 
			
		||||
  if (!isDragging.value && !isMiddleDragging.value) return;
 | 
			
		||||
 | 
			
		||||
  position.x = e.clientX - dragStart.x;
 | 
			
		||||
  position.y = e.clientY - dragStart.y;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 停止拖拽画布
 | 
			
		||||
function stopDrag() {
 | 
			
		||||
  isDragging.value = false;
 | 
			
		||||
  isMiddleDragging.value = false;
 | 
			
		||||
  
 | 
			
		||||
  document.removeEventListener('mousemove', onDrag);
 | 
			
		||||
  document.removeEventListener('mouseup', stopDrag);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 组件拖拽交互 ---
 | 
			
		||||
function startComponentDrag(e: MouseEvent, component: ComponentItem) {
 | 
			
		||||
  const target = e.target as HTMLElement;
 | 
			
		||||
  
 | 
			
		||||
  // 检查点击的是否为交互元素 (如按钮、开关等)
 | 
			
		||||
  const isInteractiveElement = (
 | 
			
		||||
    target.tagName === 'rect' || 
 | 
			
		||||
    target.tagName === 'circle' || 
 | 
			
		||||
    target.tagName === 'path' ||
 | 
			
		||||
    target.hasAttribute('fill-opacity') || 
 | 
			
		||||
    (typeof target.className === 'string' && 
 | 
			
		||||
      (target.className.includes('glow') || target.className.includes('interactive')))
 | 
			
		||||
  );
 | 
			
		||||
  
 | 
			
		||||
  // 仍然选中组件,无论是否为交互元素
 | 
			
		||||
  if (selectedComponentId.value !== component.id) {
 | 
			
		||||
    selectedComponentId.value = component.id;
 | 
			
		||||
    emit('component-selected', component);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 保存起始位置和鼠标位置
 | 
			
		||||
  const initialX = component.x;
 | 
			
		||||
  const initialY = component.y;
 | 
			
		||||
  const startX = e.clientX;
 | 
			
		||||
  const startY = e.clientY;
 | 
			
		||||
  // 如果是交互元素,则不启动拖拽
 | 
			
		||||
  if (isInteractiveElement || e.button !== 0) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  const mouseMoveHandler = (moveEvent) => {
 | 
			
		||||
    if (!draggingComponent.value) return;
 | 
			
		||||
  // 阻止事件冒泡
 | 
			
		||||
  e.stopPropagation();
 | 
			
		||||
  
 | 
			
		||||
    // 计算鼠标移动的距离(在屏幕坐标系中)
 | 
			
		||||
    const dx = moveEvent.clientX - startX;
 | 
			
		||||
    const dy = moveEvent.clientY - startY;
 | 
			
		||||
  // 设置拖拽状态
 | 
			
		||||
  draggingComponentId.value = component.id;
 | 
			
		||||
  isDragging.value = false;
 | 
			
		||||
  isMiddleDragging.value = false;
 | 
			
		||||
 | 
			
		||||
    // 将移动距离转换为画布坐标系中的距离
 | 
			
		||||
    const canvasDx = dx / scale.value;
 | 
			
		||||
    const canvasDy = dy / scale.value;
 | 
			
		||||
  // 获取容器位置
 | 
			
		||||
  if (!canvasContainer.value) return;
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
    // 更新组件位置(相对于初始位置的增量)
 | 
			
		||||
    draggingComponent.value.x = initialX + canvasDx;
 | 
			
		||||
    draggingComponent.value.y = initialY + canvasDy;
 | 
			
		||||
  // 计算鼠标在画布坐标系中的位置
 | 
			
		||||
  const mouseX_canvas = (e.clientX - containerRect.left - position.x) / scale.value;
 | 
			
		||||
  const mouseY_canvas = (e.clientY - containerRect.top - position.y) / scale.value;
 | 
			
		||||
 | 
			
		||||
    // 通知父组件元器件位置已变化
 | 
			
		||||
  // 计算鼠标相对于组件左上角的偏移量
 | 
			
		||||
  componentDragOffset.x = mouseX_canvas - component.x;
 | 
			
		||||
  componentDragOffset.y = mouseY_canvas - component.y;
 | 
			
		||||
 | 
			
		||||
  // 添加全局监听器
 | 
			
		||||
  document.addEventListener('mousemove', onComponentDrag);
 | 
			
		||||
  document.addEventListener('mouseup', stopComponentDrag);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 拖拽组件过程
 | 
			
		||||
function onComponentDrag(e: MouseEvent) {
 | 
			
		||||
  if (!draggingComponentId.value || !canvasContainer.value) return;
 | 
			
		||||
 | 
			
		||||
  // 防止触发组件内部的事件
 | 
			
		||||
  e.stopPropagation();
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
 | 
			
		||||
  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
  
 | 
			
		||||
  // 计算鼠标在画布坐标系中的位置
 | 
			
		||||
  const mouseX_canvas = (e.clientX - containerRect.left - position.x) / scale.value;
 | 
			
		||||
  const mouseY_canvas = (e.clientY - containerRect.top - position.y) / scale.value;
 | 
			
		||||
 | 
			
		||||
  // 计算组件新位置
 | 
			
		||||
  const newX = mouseX_canvas - componentDragOffset.x;
 | 
			
		||||
  const newY = mouseY_canvas - componentDragOffset.y;
 | 
			
		||||
 | 
			
		||||
  // 通知父组件更新位置
 | 
			
		||||
  emit('component-moved', {
 | 
			
		||||
      id: draggingComponent.value.id,
 | 
			
		||||
      x: draggingComponent.value.x,
 | 
			
		||||
      y: draggingComponent.value.y
 | 
			
		||||
    id: draggingComponentId.value,
 | 
			
		||||
    x: Math.round(newX),
 | 
			
		||||
    y: Math.round(newY),
 | 
			
		||||
  });
 | 
			
		||||
  };
 | 
			
		||||
  
 | 
			
		||||
  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 stopComponentDrag() {
 | 
			
		||||
  draggingComponentId.value = null;
 | 
			
		||||
  
 | 
			
		||||
// 公开方法,允许外部添加组件
 | 
			
		||||
function addComponent(component: ComponentItem) {
 | 
			
		||||
  components.value.push(component);
 | 
			
		||||
  document.removeEventListener('mousemove', onComponentDrag);
 | 
			
		||||
  document.removeEventListener('mouseup', stopComponentDrag);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 公开方法,允许外部重置画布
 | 
			
		||||
function resetCanvas() {
 | 
			
		||||
  position.x = 0;
 | 
			
		||||
  position.y = 0;
 | 
			
		||||
  scale.value = 1;
 | 
			
		||||
// 更新组件属性
 | 
			
		||||
function updateComponentProp(componentId: string, propName: string, value: any) {
 | 
			
		||||
  emit('update-component-prop', { id: componentId, propName, value });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取组件引用,用于外部访问
 | 
			
		||||
function getComponentRef(componentId: string) {
 | 
			
		||||
  const component = props.components.find(c => c.id === componentId);
 | 
			
		||||
  if (!component) return null;
 | 
			
		||||
  
 | 
			
		||||
  // 查找组件的引用
 | 
			
		||||
  return componentRefs.value[component.id] || null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 暴露给父组件的方法
 | 
			
		||||
defineExpose({
 | 
			
		||||
  addComponent,
 | 
			
		||||
  resetCanvas
 | 
			
		||||
  getComponentRef
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// --- 生命周期钩子 ---
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 初始化中心位置
 | 
			
		||||
  if (canvasContainer.value) {
 | 
			
		||||
    position.x = canvasContainer.value.clientWidth / 2;
 | 
			
		||||
    position.y = canvasContainer.value.clientHeight / 2;
 | 
			
		||||
  }
 | 
			
		||||
  if (canvasContainer.value) {
 | 
			
		||||
    canvasContainer.value.addEventListener('wheel', onZoom);
 | 
			
		||||
  }
 | 
			
		||||
  // 添加键盘事件监听器
 | 
			
		||||
  window.addEventListener('keydown', handleKeyDown);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 处理键盘事件
 | 
			
		||||
function handleKeyDown(e: KeyboardEvent) {
 | 
			
		||||
  // 如果当前有选中的组件,并且按下了Delete键
 | 
			
		||||
  if (selectedComponentId.value && (e.key === 'Delete' || e.key === 'Backspace')) {
 | 
			
		||||
    // 触发删除元器件事件
 | 
			
		||||
    emit('component-delete', selectedComponentId.value);
 | 
			
		||||
    // 清除选中状态
 | 
			
		||||
    selectedComponentId.value = null;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  // 清理事件监听器
 | 
			
		||||
  document.removeEventListener('mousemove', onComponentDrag);
 | 
			
		||||
  document.removeEventListener('mouseup', stopComponentDrag);
 | 
			
		||||
  document.removeEventListener('mousemove', onDrag);
 | 
			
		||||
  document.removeEventListener('mouseup', stopDrag);
 | 
			
		||||
  if (canvasContainer.value) {
 | 
			
		||||
    canvasContainer.value.removeEventListener('wheel', onZoom);
 | 
			
		||||
  }
 | 
			
		||||
  // 移除键盘事件监听器
 | 
			
		||||
  window.removeEventListener('keydown', handleKeyDown);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.diagram-container {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  background-image: 
 | 
			
		||||
    linear-gradient(to right, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(100, 100, 100, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to right, rgba(80, 80, 80, 0.2) 100px, transparent 100px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(80, 80, 80, 0.2) 100px, transparent 100px);
 | 
			
		||||
  background-size: 20px 20px, 20px 20px, 100px 100px, 100px 100px;
 | 
			
		||||
  background-position: 0 0;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.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 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 元器件容器样式 */
 | 
			
		||||
.component-wrapper {
 | 
			
		||||
  position: relative;
 | 
			
		||||
  padding: 5px;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
  display: inline-block; /* 确保元素宽度基于内容 */
 | 
			
		||||
  max-width: fit-content; /* 强制宽度适应内容 */
 | 
			
		||||
  max-height: fit-content; /* 强制高度适应内容 */
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  padding: 0; /* 移除内边距,确保元素大小与内容完全匹配 */
 | 
			
		||||
  box-sizing: content-box; /* 使用content-box确保内容尺寸不受padding影响 */
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  overflow: visible; /* 允许内容溢出(用于显示边框) */
 | 
			
		||||
  cursor: move; /* 显示移动光标 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 悬停状态 */
 | 
			
		||||
.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;
 | 
			
		||||
/* 悬停状态 - 使用outline而非伪元素 */
 | 
			
		||||
.component-hover {
 | 
			
		||||
  outline: 2px dashed #3498db;
 | 
			
		||||
  outline-offset: 2px;
 | 
			
		||||
  z-index: 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 选中状态 */
 | 
			
		||||
.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;
 | 
			
		||||
/* 选中状态 - 使用outline而非伪元素 */
 | 
			
		||||
.component-selected {
 | 
			
		||||
  outline: 3px dashed;
 | 
			
		||||
  outline-color: #e74c3c #f39c12 #3498db #2ecc71;
 | 
			
		||||
  outline-offset: 3px;
 | 
			
		||||
  z-index: 999 !important; /* 使用更高的z-index确保始终在顶层 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 为黑暗模式设置不同的网格线颜色 */
 | 
			
		||||
:root[data-theme="dark"] .diagram-container {
 | 
			
		||||
  background-image: 
 | 
			
		||||
    linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
			
		||||
    linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
 | 
			
		||||
    linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 深度选择器 - 默认阻止SVG内部元素的鼠标事件,但允许SVG本身和特定交互元素 */
 | 
			
		||||
.component-wrapper :deep(svg) {
 | 
			
		||||
  pointer-events: auto; /* 确保SVG本身可以接收鼠标事件用于拖拽 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.component-wrapper :deep(svg *:not([class*="interactive"]):not(rect.glow):not(circle[fill-opacity]):not([fill-opacity])) {
 | 
			
		||||
  pointer-events: none; /* 非交互元素不接收鼠标事件 */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 允许特定SVG元素接收鼠标事件,用于交互 */
 | 
			
		||||
.component-wrapper :deep(svg circle[fill-opacity]),
 | 
			
		||||
.component-wrapper :deep(svg rect[fill-opacity]),
 | 
			
		||||
.component-wrapper :deep(svg rect[class*="glow"]),
 | 
			
		||||
.component-wrapper :deep(svg rect.glow),
 | 
			
		||||
.component-wrapper :deep(svg [class*="interactive"]),
 | 
			
		||||
.component-wrapper :deep(button),
 | 
			
		||||
.component-wrapper :deep(input) {
 | 
			
		||||
  pointer-events: auto !important;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,35 +1,81 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="navbar bg-base-100 shadow-sm">
 | 
			
		||||
  <div class="navbar bg-base-100 shadow-xl">
 | 
			
		||||
    <div class="navbar-start">
 | 
			
		||||
      <div class="dropdown">
 | 
			
		||||
        <div tabindex="0" role="button" class="btn btn-ghost hidden">
 | 
			
		||||
        <div tabindex="0" role="button" class="btn btn-ghost hover:bg-primary hover:bg-opacity-20 transition-all duration-300">
 | 
			
		||||
          <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
 | 
			
		||||
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
 | 
			
		||||
          </svg>
 | 
			
		||||
        </div>
 | 
			
		||||
        <ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
 | 
			
		||||
          <li><a>Item 1</a></li>
 | 
			
		||||
          <li>
 | 
			
		||||
            <a>Parent</a>
 | 
			
		||||
            <ul class="p-2">
 | 
			
		||||
              <li><a>Submenu 1</a></li>
 | 
			
		||||
              <li><a>Submenu 2</a></li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        <ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-lg z-50 mt-3 w-52 p-2 shadow-lg transition-all duration-300 ease-in-out">
 | 
			
		||||
          <li class="my-1 hover:translate-x-1 transition-all duration-300">
 | 
			
		||||
            <router-link to="/" class="text-base font-medium">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
 | 
			
		||||
                <polyline points="9 22 9 12 15 12 15 22"/>
 | 
			
		||||
              </svg>
 | 
			
		||||
              首页
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="my-1 hover:translate-x-1 transition-all duration-300">
 | 
			
		||||
            <router-link to="/user" class="text-base font-medium">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
 | 
			
		||||
                <circle cx="12" cy="7" r="4"></circle>
 | 
			
		||||
              </svg>
 | 
			
		||||
              用户界面
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="my-1 hover:translate-x-1 transition-all duration-300">
 | 
			
		||||
            <router-link to="/project" class="text-base font-medium">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
 | 
			
		||||
                <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              工程界面
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="my-1 hover:translate-x-1 transition-all duration-300">
 | 
			
		||||
            <router-link to="/test" class="text-base font-medium">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              测试功能
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li class="my-1 hover:translate-x-1 transition-all duration-300">
 | 
			
		||||
            <router-link to="/test/jtag" class="text-base font-medium">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
 | 
			
		||||
                <line x1="8" y1="21" x2="16" y2="21"></line>
 | 
			
		||||
                <line x1="12" y1="17" x2="12" y2="21"></line>
 | 
			
		||||
              </svg>
 | 
			
		||||
              JTAG测试
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </li>
 | 
			
		||||
          <li><a>Item 3</a></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="navbar-center lg:flex">
 | 
			
		||||
      <a class="btn btn-ghost text-xl">FPGA Web Lab</a>
 | 
			
		||||
      <router-link to="/" class="btn btn-ghost text-xl font-bold transition-all duration-300 hover:scale-105">
 | 
			
		||||
        <span class="text-primary">FPGA</span> Web Lab
 | 
			
		||||
      </router-link>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="navbar-end">
 | 
			
		||||
      <a class="btn btn-soft w-20 mx-10">注册</a>
 | 
			
		||||
      <a class="btn btn-primary w-25">登录</a>
 | 
			
		||||
      <router-link to="/login" class="btn btn-primary text-base-100 transition-all duration-300 hover:scale-105 hover:shadow-lg mr-3">
 | 
			
		||||
        登录
 | 
			
		||||
      </router-link>
 | 
			
		||||
      <div class="ml-2 transition-all duration-500 hover:rotate-12">
 | 
			
		||||
        <ThemeControlButton />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import ThemeControlButton from "./ThemeControlButton.vue";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
@import "../assets/main.css";
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +1,46 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <label class="swap swap-rotate">
 | 
			
		||||
      <!-- this hidden checkbox controls the state -->
 | 
			
		||||
      <input type="checkbox" value="synthwave" @click="theme.toggleTheme" :checked="checkState" />
 | 
			
		||||
  <div class="theme-control-wrapper">
 | 
			
		||||
    <label class="swap swap-rotate theme-toggle">      <!-- this hidden checkbox controls the state -->
 | 
			
		||||
      <input type="checkbox" value="synthwave" @click="toggleTheme" :checked="checkState" />
 | 
			
		||||
 | 
			
		||||
      <!-- sun icon -->
 | 
			
		||||
      <svg class="swap-off h-10 w-10 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
 | 
			
		||||
      <div class="sun-wrapper swap-off">
 | 
			
		||||
        <svg 
 | 
			
		||||
          class="sun-icon h-9 w-9 fill-current text-yellow-500 transform transition-all duration-500 ease-in-out hover:scale-110"
 | 
			
		||||
          xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
          viewBox="0 0 24 24">
 | 
			
		||||
          <path
 | 
			
		||||
            d="M5.64,17l-.71.71a1,1,0,0,0,0,1.41,1,1,0,0,0,1.41,0l.71-.71A1,1,0,0,0,5.64,17ZM5,12a1,1,0,0,0-1-1H3a1,1,0,0,0,0,2H4A1,1,0,0,0,5,12Zm7-7a1,1,0,0,0,1-1V3a1,1,0,0,0-2,0V4A1,1,0,0,0,12,5ZM5.64,7.05a1,1,0,0,0,.7.29,1,1,0,0,0,.71-.29,1,1,0,0,0,0-1.41l-.71-.71A1,1,0,0,0,4.93,6.34Zm12,.29a1,1,0,0,0,.7-.29l.71-.71a1,1,0,1,0-1.41-1.41L17,5.64a1,1,0,0,0,0,1.41A1,1,0,0,0,17.66,7.34ZM21,11H20a1,1,0,0,0,0,2h1a1,1,0,0,0,0-2Zm-9,8a1,1,0,0,0-1,1v1a1,1,0,0,0,2,0V20A1,1,0,0,0,12,19ZM18.36,17A1,1,0,0,0,17,18.36l.71.71a1,1,0,0,0,1.41,0,1,1,0,0,0,0-1.41ZM12,6.5A5.5,5.5,0,1,0,17.5,12,5.51,5.51,0,0,0,12,6.5Zm0,9A3.5,3.5,0,1,1,15.5,12,3.5,3.5,0,0,1,12,15.5Z" />
 | 
			
		||||
        </svg>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- moon icon -->
 | 
			
		||||
      <svg class="swap-on h-10 w-10 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
 | 
			
		||||
      <div class="moon-wrapper swap-on">
 | 
			
		||||
        <svg 
 | 
			
		||||
          class="moon-icon h-9 w-9 fill-current text-indigo-400 transform transition-all duration-500 ease-in-out hover:scale-110" 
 | 
			
		||||
          xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
          viewBox="0 0 24 24">
 | 
			
		||||
          <path
 | 
			
		||||
            d="M21.64,13a1,1,0,0,0-1.05-.14,8.05,8.05,0,0,1-3.37.73A8.15,8.15,0,0,1,9.08,5.49a8.59,8.59,0,0,1,.25-2A1,1,0,0,0,8,2.36,10.14,10.14,0,1,0,22,14.05,1,1,0,0,0,21.64,13Zm-9.5,6.69A8.14,8.14,0,0,1,7.08,5.22v.27A10.15,10.15,0,0,0,17.22,15.63a9.79,9.79,0,0,0,2.1-.22A8.11,8.11,0,0,1,12.14,19.73Z" />
 | 
			
		||||
        </svg>
 | 
			
		||||
      </div>
 | 
			
		||||
    </label>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useThemeStore } from '@/stores/theme';
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { inject, computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
const theme = useThemeStore();
 | 
			
		||||
// 注入由 App.vue 提供的主题相关函数和状态
 | 
			
		||||
const { isDarkMode, toggleTheme } = inject('theme') as {
 | 
			
		||||
  isDarkMode: { value: boolean },
 | 
			
		||||
  toggleTheme: () => void
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 计算复选框的选中状态
 | 
			
		||||
const checkState = computed(() => {
 | 
			
		||||
  return theme.isDarkTheme()
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
  return isDarkMode.value;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										48
									
								
								src/components/equipments/DDR.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
<template>  <div class="ddr-component" :style="{ width: width + 'px', height: height + 'px' }">
 | 
			
		||||
    <img 
 | 
			
		||||
      src="../equipments/svg/ddr.svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      alt="DDR内存"
 | 
			
		||||
      class="svg-image"
 | 
			
		||||
      draggable="false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 120 * props.size);
 | 
			
		||||
const height = computed(() => 80 * props.size);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.ddr-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
  -moz-user-select: none; /* Firefox */
 | 
			
		||||
  -ms-user-select: none; /* IE/Edge */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-image {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
  pointer-events: none; /* 禁止鼠标交互 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										48
									
								
								src/components/equipments/ETH.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
<template>  <div class="eth-component" :style="{ width: width + 'px', height: height + 'px' }">
 | 
			
		||||
    <img 
 | 
			
		||||
      src="../equipments/svg/eth.svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      alt="以太网接口"
 | 
			
		||||
      class="svg-image"
 | 
			
		||||
      draggable="false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 100 * props.size);
 | 
			
		||||
const height = computed(() => 60 * props.size);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.eth-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
  -moz-user-select: none; /* Firefox */
 | 
			
		||||
  -ms-user-select: none; /* IE/Edge */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-image {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
  pointer-events: none; /* 禁止鼠标交互 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										48
									
								
								src/components/equipments/HDMI.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
<template>  <div class="hdmi-component" :style="{ width: width + 'px', height: height + 'px' }">
 | 
			
		||||
    <img 
 | 
			
		||||
      src="../equipments/svg/hdmi.svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      alt="HDMI接口"
 | 
			
		||||
      class="svg-image"
 | 
			
		||||
      draggable="false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 100 * props.size);
 | 
			
		||||
const height = computed(() => 50 * props.size);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.hdmi-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
  -moz-user-select: none; /* Firefox */
 | 
			
		||||
  -ms-user-select: none; /* IE/Edge */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-image {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
  pointer-events: none; /* 禁止鼠标交互 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,7 +1,13 @@
 | 
			
		||||
<template>  
 | 
			
		||||
  <div class="relative" :style="{width: `${props.width}px`, height: `${props.height}px`}">
 | 
			
		||||
    <!-- 简化的SVG按钮 -->
 | 
			
		||||
    <svg xmlns="http://www.w3.org/2000/svg" :width="props.width" :height="props.height" viewBox="0 0 1600 1600">
 | 
			
		||||
  <div class="button-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
 | 
			
		||||
    <svg 
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      viewBox="400 400 800 800"
 | 
			
		||||
      class="mechanical-button"
 | 
			
		||||
    >
 | 
			
		||||
      <!-- defs 和按钮底座保持不变 -->
 | 
			
		||||
      <defs>
 | 
			
		||||
        <filter id="btn-shadow">
 | 
			
		||||
          <feGaussianBlur in="SourceAlpha" stdDeviation="20" result="blur" />
 | 
			
		||||
@@ -42,13 +48,12 @@
 | 
			
		||||
        fill-opacity="0.9"
 | 
			
		||||
        @mousedown="toggleButtonState(true)"
 | 
			
		||||
        @mouseup="toggleButtonState(false)"
 | 
			
		||||
        @contextmenu.prevent="openContextMenu($event)"
 | 
			
		||||
        style="pointer-events: auto; transition: all 20ms ease-in-out;"
 | 
			
		||||
        @mouseleave="toggleButtonState(false)"
 | 
			
		||||
        style="pointer-events: auto; transition: all 20ms ease-in-out; cursor: pointer;"
 | 
			
		||||
      />
 | 
			
		||||
      
 | 
			
		||||
      <!-- 按键文字 -->
 | 
			
		||||
      <text
 | 
			
		||||
        v-if="bindKey"
 | 
			
		||||
        v-if="displayText"
 | 
			
		||||
        x="800"
 | 
			
		||||
        y="800"
 | 
			
		||||
        font-size="310"
 | 
			
		||||
@@ -57,98 +62,169 @@
 | 
			
		||||
        fill="#ccc"
 | 
			
		||||
        style="font-family: Arial; filter: url(#btn-shadow); user-select: none; pointer-events: none; mix-blend-mode: overlay;"
 | 
			
		||||
      >
 | 
			
		||||
        {{ bindKey.toUpperCase() }}
 | 
			
		||||
        {{ displayText }}
 | 
			
		||||
      </text>
 | 
			
		||||
    </svg>
 | 
			
		||||
    
 | 
			
		||||
    <!-- 使用DaisyUI的卡片组件实现上下文菜单 -->
 | 
			
		||||
    <div v-if="showContextMenu" 
 | 
			
		||||
         class="card card-compact fixed z-50 shadow-lg bg-base-100 border border-base-300"
 | 
			
		||||
         :style="{ top: contextMenuY + 'px', left: contextMenuX + 'px' }"
 | 
			
		||||
         @click.stop>
 | 
			
		||||
      <div class="card-body p-0">
 | 
			
		||||
        <button class="btn btn-ghost justify-start normal-case w-full h-full" @click="startBinding">
 | 
			
		||||
          <span v-if="isBinding">请输入</span>
 | 
			
		||||
          <span v-else>绑定按键: {{ bindKey ? bindKey.toUpperCase() : '未绑定' }}</span>
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
    <!-- 嵌入Pin组件,覆盖在按钮上 -->
 | 
			
		||||
    <div class="pin-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: '80%',
 | 
			
		||||
      left: '50%', 
 | 
			
		||||
      transform: 'translate(-50%, 20%)',
 | 
			
		||||
      zIndex: 3,
 | 
			
		||||
      pointerEvents: 'auto'
 | 
			
		||||
    }">
 | 
			
		||||
      <Pin
 | 
			
		||||
        direction="output"
 | 
			
		||||
        type="digital"  
 | 
			
		||||
        appearance="None"
 | 
			
		||||
        :label="props.label"
 | 
			
		||||
        :constraint="props.constraint"
 | 
			
		||||
        :size="0.8"
 | 
			
		||||
        @value-change="handlePinValueChange"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, onMounted, onUnmounted } from 'vue';
 | 
			
		||||
import { ref, onMounted, onUnmounted, computed } from 'vue';
 | 
			
		||||
import Pin from './Pin.vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  width?: string | number
 | 
			
		||||
  height?: string | number
 | 
			
		||||
// 从Pin组件继承属性
 | 
			
		||||
interface PinProps {
 | 
			
		||||
  label?: string;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  // 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
 | 
			
		||||
  direction?: 'input' | 'output' | 'inout';
 | 
			
		||||
  type?: 'digital' | 'analog';
 | 
			
		||||
  appearance?: 'None' | 'Dip' | 'SMT';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 按钮特有属性
 | 
			
		||||
interface ButtonProps {
 | 
			
		||||
  size?: number;
 | 
			
		||||
  bindKey?: string;
 | 
			
		||||
  buttonText?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组合两个接口
 | 
			
		||||
interface Props extends PinProps, ButtonProps {}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  width: 160,
 | 
			
		||||
  height: 160,
 | 
			
		||||
})
 | 
			
		||||
  size: 1,
 | 
			
		||||
  bindKey: '',
 | 
			
		||||
  buttonText: '',
 | 
			
		||||
  label: 'BTN',
 | 
			
		||||
  constraint: '',
 | 
			
		||||
  // 这些值会被覆盖,但需要默认值以满足类型要求
 | 
			
		||||
  direction: 'output',
 | 
			
		||||
  type: 'digital',
 | 
			
		||||
  appearance: 'Dip'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const bindKey = ref('');
 | 
			
		||||
let isKeyPressed = false;
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 160 * props.size);
 | 
			
		||||
const height = computed(() => 160 * props.size);
 | 
			
		||||
 | 
			
		||||
// 计算文本显示内容
 | 
			
		||||
const displayText = computed(() => {
 | 
			
		||||
  if (props.buttonText) return props.buttonText;
 | 
			
		||||
  return props.bindKey ? props.bindKey.toUpperCase() : '';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
  'update:bindKey',
 | 
			
		||||
  'update:label',
 | 
			
		||||
  'update:constraint',
 | 
			
		||||
  'press',
 | 
			
		||||
  'release',
 | 
			
		||||
  'click',
 | 
			
		||||
  'value-change'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 内部状态
 | 
			
		||||
const isKeyPressed = ref(false);
 | 
			
		||||
const btnHeight = ref(200);
 | 
			
		||||
const colorMatrix = ref("1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0");
 | 
			
		||||
const isBinding = ref(false);
 | 
			
		||||
const showContextMenu = ref(false);
 | 
			
		||||
const contextMenuX = ref(0);
 | 
			
		||||
const contextMenuY = ref(0);
 | 
			
		||||
 | 
			
		||||
// 处理Pin值变化
 | 
			
		||||
function handlePinValueChange(value: any) {
 | 
			
		||||
  emit('value-change', value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 按键状态逻辑 ---
 | 
			
		||||
function toggleButtonState(isPressed: boolean) {
 | 
			
		||||
  btnHeight.value = isPressed ? 210 : 200;
 | 
			
		||||
  colorMatrix.value = isPressed 
 | 
			
		||||
    ? "1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 0.7 0"
 | 
			
		||||
    : "1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0";
 | 
			
		||||
  isKeyPressed.value = isPressed;
 | 
			
		||||
  btnHeight.value = isPressed ? 180 : 200;
 | 
			
		||||
  
 | 
			
		||||
  // 发出事件通知父组件
 | 
			
		||||
  if (isPressed) {
 | 
			
		||||
    emit('press');
 | 
			
		||||
  } else {
 | 
			
		||||
    emit('release');
 | 
			
		||||
    emit('click');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function openContextMenu(e: MouseEvent) {
 | 
			
		||||
  contextMenuX.value = e.clientX;
 | 
			
		||||
  contextMenuY.value = e.clientY;
 | 
			
		||||
  showContextMenu.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function closeContextMenu() {
 | 
			
		||||
  showContextMenu.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function startBinding() {
 | 
			
		||||
  if (isBinding.value) return;
 | 
			
		||||
  isBinding.value = true;
 | 
			
		||||
  window.addEventListener('keydown', onBindingKeyDown, { once: true });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onBindingKeyDown(e: KeyboardEvent) {
 | 
			
		||||
  bindKey.value = e.key.toLowerCase();
 | 
			
		||||
  isBinding.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleKeyDown(e: KeyboardEvent) {
 | 
			
		||||
  if (e.key.toLowerCase() === bindKey.value && !isKeyPressed) {
 | 
			
		||||
    isKeyPressed = true;
 | 
			
		||||
// 处理键盘事件
 | 
			
		||||
function handleKeyDown(event: KeyboardEvent) {
 | 
			
		||||
  if (event.key === props.bindKey) {
 | 
			
		||||
    toggleButtonState(true);
 | 
			
		||||
    setTimeout(() => toggleButtonState(false), 150);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleKeyUp(e: KeyboardEvent) {
 | 
			
		||||
  if (e.key.toLowerCase() === bindKey.value) {
 | 
			
		||||
    isKeyPressed = false;
 | 
			
		||||
    toggleButtonState(false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 生命周期钩子 ---
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  window.addEventListener('keydown', handleKeyDown);
 | 
			
		||||
  window.addEventListener('keyup', handleKeyUp);
 | 
			
		||||
  window.addEventListener('click', closeContextMenu);
 | 
			
		||||
  document.addEventListener('keydown', handleKeyDown);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  window.removeEventListener('keydown', handleKeyDown);
 | 
			
		||||
  window.removeEventListener('keyup', handleKeyUp);
 | 
			
		||||
  window.removeEventListener('click', closeContextMenu);
 | 
			
		||||
  document.removeEventListener('keydown', handleKeyDown);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 向外暴露方法
 | 
			
		||||
defineExpose({
 | 
			
		||||
  toggleButtonState,
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
    // 按钮特有属性
 | 
			
		||||
    bindKey: props.bindKey,
 | 
			
		||||
    buttonText: props.buttonText,
 | 
			
		||||
    // 继承自Pin的属性
 | 
			
		||||
    label: props.label,
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    // 固定的Pin属性
 | 
			
		||||
    direction: 'output',
 | 
			
		||||
    type: 'digital',
 | 
			
		||||
    appearance: 'None'
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.button-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mechanical-button {
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  line-height: 0;
 | 
			
		||||
  font-size: 0;
 | 
			
		||||
  box-sizing: content-box;
 | 
			
		||||
  overflow: visible;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.pin-wrapper {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										220
									
								
								src/components/equipments/MotherBoard.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,220 @@
 | 
			
		||||
<template>  <div class="motherboard-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">    <!-- 主板 SVG -->
 | 
			
		||||
    <img 
 | 
			
		||||
      src="../equipments/svg/motherboard.svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      alt="主板"
 | 
			
		||||
      class="svg-image"
 | 
			
		||||
      draggable="false"
 | 
			
		||||
    />
 | 
			
		||||
    
 | 
			
		||||
    <!-- 嵌入各种组件 -->    <!-- HDMI -->
 | 
			
		||||
    <div class="component-wrapper hdmi-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: `${140 * props.size}px`,
 | 
			
		||||
      left: `${-48 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <HDMI :size="1.5*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- HDMI -->
 | 
			
		||||
    <div class="component-wrapper hdmi-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: `${260 * props.size}px`,
 | 
			
		||||
      left: `${-48 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <HDMI :size="1.5*props.size" />
 | 
			
		||||
    </div>    <!-- ETH -->
 | 
			
		||||
    <div class="component-wrapper eth-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: `${365 * props.size}px`,
 | 
			
		||||
      left: `${-10 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <ETH :size="1.5*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- DDR -->
 | 
			
		||||
    <div class="component-wrapper ddr-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: `${224 * props.size}px`,
 | 
			
		||||
      right: `${250 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <DDR :size="1.2*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
      <!-- SD -->
 | 
			
		||||
    <div class="component-wrapper sd-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${130 * props.size}px`,
 | 
			
		||||
      right: `${172 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <SD :size="1.2*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- SFP -->
 | 
			
		||||
    <div class="component-wrapper sfp-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${210 * props.size}px`,
 | 
			
		||||
      right: `${-46 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <SFP :size="1.84*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- SFP -->
 | 
			
		||||
    <div class="component-wrapper sfp-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${290 * props.size}px`,
 | 
			
		||||
      right: `${-46 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <SFP :size="1.84*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
      <!-- SMA -->
 | 
			
		||||
    <div class="component-wrapper sma-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: `${110 * props.size}px`,
 | 
			
		||||
      right: `${204 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <SMA :size="0.75*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- SMA -->
 | 
			
		||||
    <div class="component-wrapper sma-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: `${170 * props.size}px`,
 | 
			
		||||
      right: `${204 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <SMA :size="0.75*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- SMA -->
 | 
			
		||||
    <div class="component-wrapper sma-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: `${250 * props.size}px`,
 | 
			
		||||
      right: `${204 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <SMA :size="0.75*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- SMA -->
 | 
			
		||||
    <div class="component-wrapper sma-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      top: `${310 * props.size}px`,
 | 
			
		||||
      right: `${204 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <SMA :size="0.75*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <!-- BUTTON -->
 | 
			
		||||
    <div class="component-wrapper button-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${140 * props.size}px`,
 | 
			
		||||
      right: `${430 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <MechanicalButton :size="0.175*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="component-wrapper button-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${140 * props.size}px`,
 | 
			
		||||
      right: `${397 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <MechanicalButton :size="0.175*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="component-wrapper button-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${140 * props.size}px`,
 | 
			
		||||
      right: `${364 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <MechanicalButton :size="0.175*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="component-wrapper button-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${140 * props.size}px`,
 | 
			
		||||
      right: `${331 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <MechanicalButton :size="0.175*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="component-wrapper button-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${140 * props.size}px`,
 | 
			
		||||
      right: `${298 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <MechanicalButton :size="0.175*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="component-wrapper button-wrapper" :style="{
 | 
			
		||||
      position: 'absolute',
 | 
			
		||||
      bottom: `${140 * props.size}px`,
 | 
			
		||||
      right: `${265 * props.size}px`,
 | 
			
		||||
      zIndex: 10
 | 
			
		||||
    }">
 | 
			
		||||
      <MechanicalButton :size="0.175*props.size" />
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import HDMI from './HDMI.vue';
 | 
			
		||||
import DDR from './DDR.vue';
 | 
			
		||||
import ETH from './ETH.vue';
 | 
			
		||||
import SD from './SD.vue';
 | 
			
		||||
import SFP from './SFP.vue';
 | 
			
		||||
import SMA from './SMA.vue';
 | 
			
		||||
import MechanicalButton from './MechanicalButton.vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 800 * props.size);
 | 
			
		||||
const height = computed(() => 600 * props.size);
 | 
			
		||||
 | 
			
		||||
// 向外暴露方法
 | 
			
		||||
defineExpose({
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
    size: props.size
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.motherboard-container {
 | 
			
		||||
  display: block;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
  -moz-user-select: none; /* Firefox */
 | 
			
		||||
  -ms-user-select: none; /* IE/Edge */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-image {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
  pointer-events: none; /* 禁止鼠标交互 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.component-wrapper {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										131
									
								
								src/components/equipments/Pin.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,131 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <svg 
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
    :width="width" 
 | 
			
		||||
    :height="height" 
 | 
			
		||||
    :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
 | 
			
		||||
    class="pin-component"
 | 
			
		||||
  >
 | 
			
		||||
    <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
 | 
			
		||||
      <g v-if="props.appearance === 'None'">
 | 
			
		||||
        <g transform="translate(-12.5, -12.5)" class="interactive">
 | 
			
		||||
          <circle
 | 
			
		||||
            style="fill:#909090"
 | 
			
		||||
            cx="12.5"
 | 
			
		||||
            cy="12.5"
 | 
			
		||||
            r="3.75" />
 | 
			
		||||
        </g>
 | 
			
		||||
      </g>      
 | 
			
		||||
      <g v-else-if="props.appearance === 'Dip'">
 | 
			
		||||
        <!-- 使用inkscape创建的SVG替代原有Dip样式 -->
 | 
			
		||||
        <g transform="translate(-12.5, -12.5)" class="interactive">
 | 
			
		||||
          <rect
 | 
			
		||||
            :style="`fill:${props.type === 'analog' ? '#2a6099' : '#000000'};fill-opacity:0.772973`"
 | 
			
		||||
            width="25"
 | 
			
		||||
            height="25"
 | 
			
		||||
            x="0"
 | 
			
		||||
            y="0"
 | 
			
		||||
            rx="2.5" />
 | 
			
		||||
          <circle
 | 
			
		||||
            style="fill:#ecececc5;fill-opacity:0.772973"
 | 
			
		||||
            cx="12.5"
 | 
			
		||||
            cy="12.5"
 | 
			
		||||
            r="3.75" />
 | 
			
		||||
          <text
 | 
			
		||||
            style="font-size:6.85px;text-align:start;fill:#ffffff;fill-opacity:0.772973"
 | 
			
		||||
            x="7.3"
 | 
			
		||||
            y="7"
 | 
			
		||||
            xml:space="preserve">{{ props.label }}</text>
 | 
			
		||||
        </g>
 | 
			
		||||
      </g>
 | 
			
		||||
      <g v-else-if="props.appearance === 'SMT'">
 | 
			
		||||
        <rect x="-20" y="-10" width="40" height="20" fill="#aaa" rx="2" ry="2" />
 | 
			
		||||
        <rect x="-18" y="-8" width="36" height="16" :fill="getColorByType" rx="1" ry="1" />
 | 
			
		||||
        <rect x="-16" y="-6" width="26" height="12" :fill="getColorByType" rx="1" ry="1" />
 | 
			
		||||
        <text text-anchor="middle" dominant-baseline="middle" font-size="8" fill="white" x="-3">{{ props.label }}</text>
 | 
			
		||||
      </g>
 | 
			
		||||
    </g>
 | 
			
		||||
  </svg>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
  label?: string;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
  direction?: 'input' | 'output' | 'inout';
 | 
			
		||||
  type?: 'digital' | 'analog';
 | 
			
		||||
  appearance?: 'None' | 'Dip' | 'SMT';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1,
 | 
			
		||||
  label: 'PIN',
 | 
			
		||||
  constraint: '',
 | 
			
		||||
  direction: 'input',
 | 
			
		||||
  type: 'digital',
 | 
			
		||||
  appearance: 'Dip'
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
  'value-change'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 内部状态
 | 
			
		||||
const analogValue = ref(0);
 | 
			
		||||
 | 
			
		||||
const width = computed(() => props.appearance === 'None' ? 40 * props.size : 30 * props.size);
 | 
			
		||||
const height = computed(() => {
 | 
			
		||||
  if (props.appearance === 'None') return 20 * props.size;
 | 
			
		||||
  if (props.appearance === 'Dip') return 30 * props.size; // 调整Dip样式高度
 | 
			
		||||
  return 60 * props.size;
 | 
			
		||||
});
 | 
			
		||||
const viewBoxWidth = computed(() => props.appearance === 'None' ? 40 : 30);
 | 
			
		||||
const viewBoxHeight = computed(() => {
 | 
			
		||||
  if (props.appearance === 'None') return 20;
 | 
			
		||||
  if (props.appearance === 'Dip') return 30; // 调整Dip样式视图高度
 | 
			
		||||
  return 60;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const getColorByType = computed(() => {
 | 
			
		||||
  return props.type === 'analog' ? '#2a6099' : '#444';
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function updateAnalogValue(value: number) {
 | 
			
		||||
  if (props.type !== 'analog') return;
 | 
			
		||||
  analogValue.value = Math.max(0, Math.min(1, value));
 | 
			
		||||
  emit('value-change', {
 | 
			
		||||
    label: props.label,
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    value: analogValue.value
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
  setAnalogValue: updateAnalogValue,
 | 
			
		||||
  getAnalogValue: () => analogValue.value,
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
    label: props.label,
 | 
			
		||||
    constraint: props.constraint,
 | 
			
		||||
    direction: props.direction,
 | 
			
		||||
    type: props.type,
 | 
			
		||||
    appearance: props.appearance
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.pin-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
}
 | 
			
		||||
.interactive {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  transition: filter 0.2s;
 | 
			
		||||
}
 | 
			
		||||
.interactive:hover {
 | 
			
		||||
  filter: brightness(1.2);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										48
									
								
								src/components/equipments/SD.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
<template>  <div class="sd-component" :style="{ width: width + 'px', height: height + 'px' }">
 | 
			
		||||
    <img 
 | 
			
		||||
      src="../equipments/svg/sd.svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      alt="SD卡插槽"
 | 
			
		||||
      class="svg-image"
 | 
			
		||||
      draggable="false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 80 * props.size);
 | 
			
		||||
const height = computed(() => 60 * props.size);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.sd-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
  -moz-user-select: none; /* Firefox */
 | 
			
		||||
  -ms-user-select: none; /* IE/Edge */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-image {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
  pointer-events: none; /* 禁止鼠标交互 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										48
									
								
								src/components/equipments/SFP.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
<template>  <div class="sfp-component" :style="{ width: width + 'px', height: height + 'px' }">
 | 
			
		||||
    <img 
 | 
			
		||||
      src="../equipments/svg/sfp.svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      alt="SFP光纤模块"
 | 
			
		||||
      class="svg-image"
 | 
			
		||||
      draggable="false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 120 * props.size);
 | 
			
		||||
const height = computed(() => 40 * props.size);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.sfp-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
  -moz-user-select: none; /* Firefox */
 | 
			
		||||
  -ms-user-select: none; /* IE/Edge */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-image {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
  pointer-events: none; /* 禁止鼠标交互 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										48
									
								
								src/components/equipments/SMA.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
			
		||||
<template>  <div class="sma-component" :style="{ width: width + 'px', height: height + 'px' }">
 | 
			
		||||
    <img 
 | 
			
		||||
      src="../equipments/svg/sma.svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      alt="SMA连接器"
 | 
			
		||||
      class="svg-image"
 | 
			
		||||
      draggable="false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 40 * props.size);
 | 
			
		||||
const height = computed(() => 40 * props.size);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.sma-component {
 | 
			
		||||
  display: block;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none; /* Safari */
 | 
			
		||||
  -moz-user-select: none; /* Firefox */
 | 
			
		||||
  -ms-user-select: none; /* IE/Edge */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.svg-image {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
  pointer-events: none; /* 禁止鼠标交互 */
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										193
									
								
								src/components/equipments/SMT_LED.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,193 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="led-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
 | 
			
		||||
    <svg 
 | 
			
		||||
      xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
      :width="width" 
 | 
			
		||||
      :height="height" 
 | 
			
		||||
      viewBox="0 0 100 60"
 | 
			
		||||
      class="smt-led"
 | 
			
		||||
    >
 | 
			
		||||
      <!-- LED 基座 -->
 | 
			
		||||
      <rect width="100" height="60" x="0" y="0" fill="#333" rx="5" ry="5" />
 | 
			
		||||
      
 | 
			
		||||
      <!-- LED 主体 -->
 | 
			
		||||
      <rect width="90" height="50" x="5" y="5" fill="#222" rx="3" ry="3" />
 | 
			
		||||
      
 | 
			
		||||
      <!-- LED 发光部分 -->
 | 
			
		||||
      <rect 
 | 
			
		||||
        width="70" 
 | 
			
		||||
        height="30" 
 | 
			
		||||
        x="15" 
 | 
			
		||||
        y="15" 
 | 
			
		||||
        :fill="ledColor" 
 | 
			
		||||
        :style="{ opacity: isOn ? brightness/100 : 0.2 }" 
 | 
			
		||||
        rx="15" 
 | 
			
		||||
        ry="15"
 | 
			
		||||
        @click="toggleLed"
 | 
			
		||||
        class="interactive"
 | 
			
		||||
      />
 | 
			
		||||
      
 | 
			
		||||
      <!-- LED 光晕效果 -->
 | 
			
		||||
      <rect 
 | 
			
		||||
        v-if="isOn"
 | 
			
		||||
        width="76" 
 | 
			
		||||
        height="36" 
 | 
			
		||||
        x="12" 
 | 
			
		||||
        y="12" 
 | 
			
		||||
        :fill="ledColor" 
 | 
			
		||||
        :style="{ opacity: brightness/100 * 0.3 }" 
 | 
			
		||||
        rx="18" 
 | 
			
		||||
        ry="18"
 | 
			
		||||
        filter="blur(5px)"
 | 
			
		||||
        class="glow"
 | 
			
		||||
      />
 | 
			
		||||
    </svg>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed, watch } from 'vue';
 | 
			
		||||
 | 
			
		||||
// LED特有属性
 | 
			
		||||
interface Props {
 | 
			
		||||
  size?: number;
 | 
			
		||||
  color?: string;
 | 
			
		||||
  initialOn?: boolean;
 | 
			
		||||
  brightness?: number;
 | 
			
		||||
  constraint?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 组件属性定义
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  size: 1,
 | 
			
		||||
  color: 'red',
 | 
			
		||||
  initialOn: false,
 | 
			
		||||
  brightness: 80,  // 亮度默认为80%
 | 
			
		||||
  constraint: ''
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => 100 * props.size);
 | 
			
		||||
const height = computed(() => 60 * props.size);
 | 
			
		||||
 | 
			
		||||
// 内部状态
 | 
			
		||||
const isOn = ref(props.initialOn);
 | 
			
		||||
const brightness = ref(props.brightness);
 | 
			
		||||
 | 
			
		||||
// LED 颜色映射表
 | 
			
		||||
const colorMap: Record<string, string> = {
 | 
			
		||||
  'red': '#ff3333',
 | 
			
		||||
  'green': '#33ff33',
 | 
			
		||||
  'blue': '#3333ff',
 | 
			
		||||
  'yellow': '#ffff33',
 | 
			
		||||
  'orange': '#ff9933',
 | 
			
		||||
  'white': '#ffffff',
 | 
			
		||||
  'purple': '#9933ff'
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 计算实际LED颜色
 | 
			
		||||
const ledColor = computed(() => {
 | 
			
		||||
  return colorMap[props.color.toLowerCase()] || props.color;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
  'toggle',
 | 
			
		||||
  'brightness-change',
 | 
			
		||||
  'value-change'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 手动切换LED状态
 | 
			
		||||
function toggleLed() {
 | 
			
		||||
  isOn.value = !isOn.value;
 | 
			
		||||
  emit('toggle', isOn.value);
 | 
			
		||||
  emit('value-change', {
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    brightness: brightness.value
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 设置亮度
 | 
			
		||||
function setBrightness(value: number) {
 | 
			
		||||
  // 限制亮度值在0-100范围内
 | 
			
		||||
  brightness.value = Math.max(0, Math.min(100, value));
 | 
			
		||||
  emit('brightness-change', brightness.value);
 | 
			
		||||
  emit('value-change', {
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    brightness: brightness.value
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 手动设置LED开关状态
 | 
			
		||||
function setLedState(on: boolean) {
 | 
			
		||||
  isOn.value = on;
 | 
			
		||||
  emit('toggle', isOn.value);
 | 
			
		||||
  emit('value-change', {
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    brightness: brightness.value
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 监听props变化
 | 
			
		||||
watch(() => props.brightness, (newVal) => {
 | 
			
		||||
  brightness.value = newVal;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
watch(() => props.initialOn, (newVal) => {
 | 
			
		||||
  isOn.value = newVal;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 向外暴露方法
 | 
			
		||||
defineExpose({
 | 
			
		||||
  toggleLed,
 | 
			
		||||
  setBrightness,
 | 
			
		||||
  setLedState,
 | 
			
		||||
  getInfo: () => ({
 | 
			
		||||
    // LED特有属性
 | 
			
		||||
    color: props.color,
 | 
			
		||||
    isOn: isOn.value,
 | 
			
		||||
    brightness: brightness.value,
 | 
			
		||||
    constraint: props.constraint
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.led-container {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.smt-led {
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  line-height: 0;
 | 
			
		||||
  font-size: 0;
 | 
			
		||||
  box-sizing: content-box;
 | 
			
		||||
  overflow: visible;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
  -webkit-user-select: none;
 | 
			
		||||
  -moz-user-select: none;
 | 
			
		||||
  -ms-user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.interactive {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  transition: all 0.2s ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.interactive:hover {
 | 
			
		||||
  filter: brightness(1.2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.glow {
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,6 +1,13 @@
 | 
			
		||||
// filepath: c:\_Project\FPGA_WebLab\FPGA_WebLab\src\components\equipments\Switch.vue
 | 
			
		||||
<template>
 | 
			
		||||
  <svg xmlns="http://www.w3.org/2000/svg" :width="props.width" :height="props.height" viewBox="0 0 16 16">
 | 
			
		||||
    <def>
 | 
			
		||||
  <svg 
 | 
			
		||||
    xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
    :width="width" 
 | 
			
		||||
    :height="height" 
 | 
			
		||||
    :viewBox="`4 6 ${props.switchCount + 2} 4`"
 | 
			
		||||
    class="dip-switch"
 | 
			
		||||
  >
 | 
			
		||||
    <defs>
 | 
			
		||||
      <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
 | 
			
		||||
        <feFlood result="flood" flood-color="#f08a5d" flood-opacity="1"></feFlood>
 | 
			
		||||
        <feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
 | 
			
		||||
@@ -15,49 +22,130 @@
 | 
			
		||||
          <feMergeNode in="SourceGraphic" />
 | 
			
		||||
        </feMerge>
 | 
			
		||||
      </filter>
 | 
			
		||||
    </def>
 | 
			
		||||
    </defs>
 | 
			
		||||
    
 | 
			
		||||
    <g>
 | 
			
		||||
      <rect width="8" height="4" x="4" y="6" fill="#c01401" rx="0.1" />
 | 
			
		||||
      <text fill="white" font-size="0.7" x="4.25" y="6.75">ON</text>
 | 
			
		||||
      <!-- 红色背景随开关数量变化宽度 -->
 | 
			
		||||
      <rect :width="props.switchCount + 2" height="4" x="4" y="6" fill="#c01401" rx="0.1" />
 | 
			
		||||
      <text v-if="props.showLabels" fill="white" font-size="0.7" x="4.25" y="6.75">ON</text>
 | 
			
		||||
      
 | 
			
		||||
      <g>
 | 
			
		||||
        <rect class="glow" @click="toggleBtnStatus(0)" width="0.7" height="2" fill="#68716f" x="5.15" y="7" rx="0.1" />
 | 
			
		||||
        <rect class="glow" @click="toggleBtnStatus(1)" width="0.7" height="2" fill="#68716f" x="6.15" y="7" rx="0.1" />
 | 
			
		||||
        <rect class="glow" @click="toggleBtnStatus(2)" width="0.7" height="2" fill="#68716f" x="7.15" y="7" rx="0.1" />
 | 
			
		||||
        <rect class="glow" @click="toggleBtnStatus(3)" width="0.7" height="2" fill="#68716f" x="8.15" y="7" rx="0.1" />
 | 
			
		||||
        <rect class="glow" @click="toggleBtnStatus(4)" width="0.7" height="2" fill="#68716f" x="9.15" y="7" rx="0.1" />
 | 
			
		||||
        <rect class="glow" @click="toggleBtnStatus(5)" width="0.7" height="2" fill="#68716f" x="10.15" y="7" rx="0.1" />
 | 
			
		||||
        <template v-for="(_, index) in Array(props.switchCount)" :key="index">
 | 
			
		||||
          <rect 
 | 
			
		||||
            class="glow interactive" 
 | 
			
		||||
            @click="toggleBtnStatus(index)" 
 | 
			
		||||
            width="0.7" 
 | 
			
		||||
            height="2" 
 | 
			
		||||
            fill="#68716f" 
 | 
			
		||||
            :x="5.15 + index" 
 | 
			
		||||
            y="7" 
 | 
			
		||||
            rx="0.1" 
 | 
			
		||||
          />
 | 
			
		||||
          <text 
 | 
			
		||||
            v-if="props.showLabels"
 | 
			
		||||
            :x="5.5 + index" 
 | 
			
		||||
            y="9.5" 
 | 
			
		||||
            font-size="0.4" 
 | 
			
		||||
            text-anchor="middle"
 | 
			
		||||
            fill="#444"
 | 
			
		||||
          >
 | 
			
		||||
            {{ index + 1 }}
 | 
			
		||||
          </text>
 | 
			
		||||
        </template>
 | 
			
		||||
      </g>
 | 
			
		||||
      
 | 
			
		||||
      <g>
 | 
			
		||||
        <rect @click="toggleBtnStatus(0)" width="0.65" height="0.65" fill="white" x="5.175" :y="btnLocation[0]" rx="0.1"
 | 
			
		||||
          opacity="1" />
 | 
			
		||||
        <rect @click="toggleBtnStatus(1)" width="0.65" height="0.65" fill="white" x="6.175" :y="btnLocation[1]" rx="0.1"
 | 
			
		||||
          opacity="1" />
 | 
			
		||||
        <rect @click="toggleBtnStatus(2)" width="0.65" height="0.65" fill="white" x="7.175" :y="btnLocation[2]" rx="0.1"
 | 
			
		||||
          opacity="1" />
 | 
			
		||||
        <rect @click="toggleBtnStatus(3)" width="0.65" height="0.65" fill="white" x="8.175" :y="btnLocation[3]" rx="0.1"
 | 
			
		||||
          opacity="1" />
 | 
			
		||||
        <rect @click="toggleBtnStatus(4)" width="0.65" height="0.65" fill="white" x="9.175" :y="btnLocation[4]" rx="0.1"
 | 
			
		||||
          opacity="1" />
 | 
			
		||||
        <rect @click="toggleBtnStatus(5)" width="0.65" height="0.65" fill="white" x="10.175" :y="btnLocation[5]"
 | 
			
		||||
          rx="0.1" opacity="1" />
 | 
			
		||||
        <template v-for="(location, index) in btnLocation" :key="`btn-${index}`">
 | 
			
		||||
          <rect 
 | 
			
		||||
            class="interactive"
 | 
			
		||||
            @click="toggleBtnStatus(index)" 
 | 
			
		||||
            width="0.65" 
 | 
			
		||||
            height="0.65" 
 | 
			
		||||
            fill="white" 
 | 
			
		||||
            :x="5.175 + index" 
 | 
			
		||||
            :y="location" 
 | 
			
		||||
            rx="0.1"
 | 
			
		||||
            opacity="1" 
 | 
			
		||||
          />
 | 
			
		||||
        </template>
 | 
			
		||||
      </g>
 | 
			
		||||
    </g>
 | 
			
		||||
  </svg>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref } from "vue";
 | 
			
		||||
import { computed, ref, watch } from "vue";
 | 
			
		||||
 | 
			
		||||
interface Props {
 | 
			
		||||
  width?: string | number;
 | 
			
		||||
  height?: string | number;
 | 
			
		||||
  size?: number;
 | 
			
		||||
  switchCount?: number;
 | 
			
		||||
  // 新增属性
 | 
			
		||||
  initialValues?: boolean[] | string; // 开关的初始状态,可以是布尔数组或逗号分隔的字符串
 | 
			
		||||
  showLabels?: boolean;      // 是否显示标签
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<Props>(), {
 | 
			
		||||
  width: 160,
 | 
			
		||||
  height: 160,
 | 
			
		||||
  size: 1,
 | 
			
		||||
  switchCount: 6,
 | 
			
		||||
  initialValues: () => [],
 | 
			
		||||
  showLabels: true
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 计算实际宽高
 | 
			
		||||
const width = computed(() => {
 | 
			
		||||
  // 每个开关占用25px宽度,再加上两侧边距(20px)
 | 
			
		||||
  return (props.switchCount * 25 + 20) * props.size;
 | 
			
		||||
});
 | 
			
		||||
const height = computed(() => 85 * props.size); // 高度保持固定比例
 | 
			
		||||
 | 
			
		||||
// 定义发出的事件
 | 
			
		||||
const emit = defineEmits(['change', 'switch-toggle']);
 | 
			
		||||
 | 
			
		||||
// 解析初始值,支持字符串和数组两种格式
 | 
			
		||||
const parseInitialValues = () => {
 | 
			
		||||
  if (Array.isArray(props.initialValues)) {
 | 
			
		||||
    return [...props.initialValues].slice(0, props.switchCount);
 | 
			
		||||
  } else if (typeof props.initialValues === 'string' && props.initialValues.trim() !== '') {
 | 
			
		||||
    // 将逗号分隔的字符串转换为布尔数组
 | 
			
		||||
    const values = props.initialValues.split(',')
 | 
			
		||||
      .map(val => val.trim() === '1' || val.trim().toLowerCase() === 'true')
 | 
			
		||||
      .slice(0, props.switchCount);
 | 
			
		||||
    
 | 
			
		||||
    // 如果数组长度小于开关数量,用 false 填充
 | 
			
		||||
    while (values.length < props.switchCount) {
 | 
			
		||||
      values.push(false);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    return values;
 | 
			
		||||
  }
 | 
			
		||||
  // 默认返回全部为 false 的数组
 | 
			
		||||
  return Array(props.switchCount).fill(false);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 初始化按钮状态
 | 
			
		||||
const btnStatus = ref(parseInitialValues());
 | 
			
		||||
 | 
			
		||||
// 监听 switchCount 变化,调整开关状态数组
 | 
			
		||||
watch(() => props.switchCount, (newCount) => {
 | 
			
		||||
  if (newCount !== btnStatus.value.length) {
 | 
			
		||||
    // 如果新数量大于当前数量,则扩展数组
 | 
			
		||||
    if (newCount > btnStatus.value.length) {
 | 
			
		||||
      btnStatus.value = [
 | 
			
		||||
        ...btnStatus.value,
 | 
			
		||||
        ...Array(newCount - btnStatus.value.length).fill(false)
 | 
			
		||||
      ];
 | 
			
		||||
    } else {
 | 
			
		||||
      // 如果新数量小于当前数量,则截断数组
 | 
			
		||||
      btnStatus.value = btnStatus.value.slice(0, newCount);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}, { immediate: true });
 | 
			
		||||
 | 
			
		||||
// 监听 initialValues 变化,更新开关状态
 | 
			
		||||
watch(() => props.initialValues, () => {
 | 
			
		||||
  btnStatus.value = parseInitialValues();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const btnStatus = ref([false, false, false, false, false, false]);
 | 
			
		||||
const btnLocation = computed(() => {
 | 
			
		||||
  return btnStatus.value.map((status) => {
 | 
			
		||||
    return status ? 7.025 : 8.325;
 | 
			
		||||
@@ -65,20 +153,58 @@ const btnLocation = computed(() => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function setBtnStatus(btnNum: number, isOn: boolean): void {
 | 
			
		||||
  if (btnNum >= 0 && btnNum < btnStatus.value.length) {
 | 
			
		||||
    btnStatus.value[btnNum] = isOn;
 | 
			
		||||
    emit('change', { index: btnNum, value: isOn, states: [...btnStatus.value] });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toggleBtnStatus(btnNum: number): void {
 | 
			
		||||
  if (btnNum >= 0 && btnNum < btnStatus.value.length) {
 | 
			
		||||
    btnStatus.value[btnNum] = !btnStatus.value[btnNum];
 | 
			
		||||
    emit('switch-toggle', { 
 | 
			
		||||
      index: btnNum, 
 | 
			
		||||
      value: btnStatus.value[btnNum], 
 | 
			
		||||
      states: [...btnStatus.value] 
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 一次性设置所有开关状态
 | 
			
		||||
function setAllStates(states: boolean[]): void {
 | 
			
		||||
  const newStates = states.slice(0, props.switchCount);
 | 
			
		||||
  while (newStates.length < props.switchCount) {
 | 
			
		||||
    newStates.push(false);
 | 
			
		||||
  }
 | 
			
		||||
  btnStatus.value = newStates;
 | 
			
		||||
  emit('change', { states: [...btnStatus.value] });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 暴露组件方法和状态
 | 
			
		||||
defineExpose({
 | 
			
		||||
  setBtnStatus,
 | 
			
		||||
  toggleBtnStatus,
 | 
			
		||||
  setAllStates,
 | 
			
		||||
  getBtnStatus: () => [...btnStatus.value]
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
.dip-switch {
 | 
			
		||||
  display: block;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  line-height: 0; /* 移除行高导致的额外间距 */
 | 
			
		||||
  font-size: 0; /* 防止文本节点造成的间距 */
 | 
			
		||||
  box-sizing: content-box;
 | 
			
		||||
  overflow: visible;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
rect {
 | 
			
		||||
  transition: all 100ms ease-in-out;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.glow:hover {
 | 
			
		||||
  filter: url(#glow);
 | 
			
		||||
.interactive {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										327
									
								
								src/components/equipments/componentConfig.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,327 @@
 | 
			
		||||
// 组件配置声明
 | 
			
		||||
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: '引脚的模数特性,数字或模拟'
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'appearance',
 | 
			
		||||
        type: 'select',
 | 
			
		||||
        label: '引脚样式',
 | 
			
		||||
        default: 'Dip',
 | 
			
		||||
        options: [
 | 
			
		||||
          { value: 'None', label: 'None' },
 | 
			
		||||
          { value: 'Dip', label: 'Dip' },
 | 
			
		||||
          { value: 'SMT', label: 'SMT' }
 | 
			
		||||
        ],
 | 
			
		||||
        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: '相同约束字符串的组件将被视为有电气连接'
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 获取组件配置的函数
 | 
			
		||||
export function getComponentConfig(type: string): ComponentConfig | null {
 | 
			
		||||
  return componentConfigs[type] || null;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										27
									
								
								src/components/equipments/svg/button.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,27 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg 
 | 
			
		||||
  xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
  width="160" 
 | 
			
		||||
  height="160" 
 | 
			
		||||
  viewBox="400 400 800 800"
 | 
			
		||||
>
 | 
			
		||||
  <!-- 按钮底座 -->
 | 
			
		||||
  <rect width="800" height="800" x="400" y="400" fill="#464646" rx="20" />
 | 
			
		||||
  <rect width="700" height="700" x="450" y="450" fill="#eaeaea" rx="20" />
 | 
			
		||||
 | 
			
		||||
  <!-- 装饰螺丝 -->
 | 
			
		||||
  <circle r="20" cx="1075" cy="1075" fill="#171717" />
 | 
			
		||||
  <circle r="20" cx="1075" cy="525" fill="#171717" />
 | 
			
		||||
  <circle r="20" cx="525" cy="525" fill="#171717" />
 | 
			
		||||
  <circle r="20" cx="525" cy="1075" fill="#171717" />
 | 
			
		||||
 | 
			
		||||
  <!-- 按钮主体 -->
 | 
			
		||||
  <circle r="220" cx="800" cy="800" fill="black" />
 | 
			
		||||
  <circle
 | 
			
		||||
    r="200"
 | 
			
		||||
    cx="800"
 | 
			
		||||
    cy="800"
 | 
			
		||||
    fill="#4b4b4b"
 | 
			
		||||
    fill-opacity="0.9"
 | 
			
		||||
  />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 754 B  | 
							
								
								
									
										16147
									
								
								src/components/equipments/svg/ddr.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 MiB  | 
							
								
								
									
										16286
									
								
								src/components/equipments/svg/eth.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 MiB  | 
							
								
								
									
										16126
									
								
								src/components/equipments/svg/hdmi.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 MiB  | 
							
								
								
									
										38
									
								
								src/components/equipments/svg/led.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,38 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg 
 | 
			
		||||
  xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
  width="100" 
 | 
			
		||||
  height="60" 
 | 
			
		||||
  viewBox="0 0 100 60"
 | 
			
		||||
>
 | 
			
		||||
  <!-- LED 基座 -->
 | 
			
		||||
  <rect width="100" height="60" x="0" y="0" fill="#333" rx="5" ry="5" />
 | 
			
		||||
  
 | 
			
		||||
  <!-- LED 主体 -->
 | 
			
		||||
  <rect width="90" height="50" x="5" y="5" fill="#222" rx="3" ry="3" />
 | 
			
		||||
  
 | 
			
		||||
  <!-- LED 发光部分 -->
 | 
			
		||||
  <rect 
 | 
			
		||||
    width="70" 
 | 
			
		||||
    height="30" 
 | 
			
		||||
    x="15" 
 | 
			
		||||
    y="15" 
 | 
			
		||||
    fill="#ff3333" 
 | 
			
		||||
    opacity="0.8" 
 | 
			
		||||
    rx="15" 
 | 
			
		||||
    ry="15"
 | 
			
		||||
  />
 | 
			
		||||
  
 | 
			
		||||
  <!-- LED 光晕效果 -->
 | 
			
		||||
  <rect 
 | 
			
		||||
    width="76" 
 | 
			
		||||
    height="36" 
 | 
			
		||||
    x="12" 
 | 
			
		||||
    y="12" 
 | 
			
		||||
    fill="#ff3333" 
 | 
			
		||||
    opacity="0.2" 
 | 
			
		||||
    rx="18" 
 | 
			
		||||
    ry="18"
 | 
			
		||||
    filter="blur(3px)"
 | 
			
		||||
  />
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 708 B  | 
							
								
								
									
										291
									
								
								src/components/equipments/svg/motherboard.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 26 KiB  | 
							
								
								
									
										43
									
								
								src/components/equipments/svg/pin_dip.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,43 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
 | 
			
		||||
 | 
			
		||||
<svg
 | 
			
		||||
   width="4.9999995mm"
 | 
			
		||||
   height="5mm"
 | 
			
		||||
   viewBox="0 0 4.9999995 5"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg1"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
			
		||||
  <defs
 | 
			
		||||
     id="defs1" />
 | 
			
		||||
  <g
 | 
			
		||||
     id="layer1"
 | 
			
		||||
     transform="translate(-10,-10.000002)">
 | 
			
		||||
    <rect
 | 
			
		||||
       style="fill:#000000;fill-opacity:0.772973;stroke-width:0.265;stroke-dasharray:none"
 | 
			
		||||
       id="rect1"
 | 
			
		||||
       width="5"
 | 
			
		||||
       height="5"
 | 
			
		||||
       x="10"
 | 
			
		||||
       y="10"
 | 
			
		||||
       rx="0.5"
 | 
			
		||||
       onclick="" />
 | 
			
		||||
    <circle
 | 
			
		||||
       style="fill:#ececec;fill-opacity:0.772973;stroke-width:0.264999;stroke-dasharray:none"
 | 
			
		||||
       id="path1"
 | 
			
		||||
       cx="12.5"
 | 
			
		||||
       cy="12.5"
 | 
			
		||||
       r="0.75" />
 | 
			
		||||
    <text
 | 
			
		||||
       xml:space="preserve"
 | 
			
		||||
       style="font-size:1.3717px;text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;fill:#ffffff;fill-opacity:0.772973;stroke-width:0.114488;stroke-dasharray:none"
 | 
			
		||||
       x="11.473036"
 | 
			
		||||
       y="11.410812"
 | 
			
		||||
       id="text2"><tspan
 | 
			
		||||
         id="tspan2"
 | 
			
		||||
         style="fill:#ffffff;stroke-width:0.114488"
 | 
			
		||||
         x="11.473036"
 | 
			
		||||
         y="11.410812">Pn</tspan></text>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										16120
									
								
								src/components/equipments/svg/sd.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 MiB  | 
							
								
								
									
										19
									
								
								src/components/equipments/svg/sfp.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,19 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
 | 
			
		||||
 | 
			
		||||
<svg
 | 
			
		||||
   width="53.600014mm"
 | 
			
		||||
   height="16.279999mm"
 | 
			
		||||
   viewBox="0 0 53.600014 16.279999"
 | 
			
		||||
   version="1.1"
 | 
			
		||||
   id="svg1"
 | 
			
		||||
   xml:space="preserve"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg"><defs
 | 
			
		||||
     id="defs1" /><g
 | 
			
		||||
     id="layer1"
 | 
			
		||||
     transform="translate(-86.17357,-29.131738)"><g
 | 
			
		||||
       id="g4493"><path
 | 
			
		||||
         d="m 89.863578,29.151737 v 0.44 h -3.690004 v 2.54 h 0.44 v 3.95 h -0.44 v 2.97 h 0.43 v 3.95 h -0.43 v 2.1 h 3.770004 v 0.31 h 3.95 v -0.38 h 3.95 v 0.38 h 4.220002 v -0.35 h 3.95 v 0.35 h 3.95 v -0.41 h 3.95 v 0.41 h 3.66 v -0.45 h 3.95 v 0.45 h 3.95 v -0.51 h 3.95 v 0.51 h 4.22 v -0.48 h 3.95 v 0.48 h 2.18 v -16.28 h -2.26 v 0.28 h -3.95 v -0.28 h -4.22 v 0.25 h -3.95 v -0.25 h -3.95 v 0.31 h -3.95 v -0.31 h -3.66 v 0.35 h -3.95 v -0.35 h -3.95 v 0.41 h -3.95 v -0.41 h -4.220002 v 0.38 h -3.95 v -0.38 z m 21.090002,2.82 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.82,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m -28.710002,0.03 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.680002,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 29.17,0.21 h 6.25 c 0.22,0 0.39,0.11 0.39,0.25 0,0.14 -0.17,0.25 -0.39,0.25 h -6.25 c -0.22,0 -0.39,-0.11 -0.39,-0.25 0,-0.14 0.17,-0.25 0.39,-0.25 z m -0.07,2 h 6.25 c 0.22,0 0.39,0.11 0.39,0.25 0,0.14 -0.17,0.25 -0.39,0.25 h -6.25 c -0.22,0 -0.39,-0.11 -0.39,-0.25 0,-0.14 0.17,-0.25 0.39,-0.25 z m -15.17,2.3 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.82,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m -28.710002,0.03 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.680002,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 24.42,3.36 h 6.25 c 0.22,0 0.39,0.11 0.39,0.25 0,0.14 -0.17,0.25 -0.39,0.25 h -6.25 c -0.22,0 -0.39,-0.11 -0.39,-0.25 0,-0.14 0.17,-0.25 0.39,-0.25 z m -19.84,1.31 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.82,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m -28.710002,0.03 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 9.680002,0.02 c 0.52,0 0.94,0.42 0.94,0.94 0,0.52 -0.42,0.94 -0.94,0.94 -0.52,0 -0.94,-0.42 -0.94,-0.94 0,-0.52 0.42,-0.94 0.94,-0.94 z m 28.99,0.63 h 6.25 c 0.22,0 0.39,0.11 0.39,0.25 0,0.14 -0.17,0.25 -0.39,0.25 h -6.25 c -0.22,0 -0.39,-0.11 -0.39,-0.25 0,-0.14 0.17,-0.25 0.39,-0.25 z"
 | 
			
		||||
         style="font-variation-settings:'wght' 700;fill:#ececec;fill-opacity:1;stroke-width:0.518999;stroke-linecap:round;stroke-linejoin:round;paint-order:fill markers stroke"
 | 
			
		||||
         id="path4484-9-9" /></g></g></svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 3.4 KiB  | 
							
								
								
									
										16133
									
								
								src/components/equipments/svg/sma.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.5 MiB  | 
							
								
								
									
										33
									
								
								src/components/equipments/svg/switch.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,33 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg 
 | 
			
		||||
  xmlns="http://www.w3.org/2000/svg" 
 | 
			
		||||
  width="200" 
 | 
			
		||||
  height="100" 
 | 
			
		||||
  viewBox="0 0 200 100"
 | 
			
		||||
>
 | 
			
		||||
  <!-- 开关基座 -->
 | 
			
		||||
  <rect width="200" height="100" x="0" y="0" fill="#444" rx="5" ry="5" />
 | 
			
		||||
  
 | 
			
		||||
  <!-- 开关面板 -->
 | 
			
		||||
  <rect width="180" height="80" x="10" y="10" fill="#333" rx="4" ry="4" />
 | 
			
		||||
  
 | 
			
		||||
  <!-- 开关拨片示例 (6个) -->
 | 
			
		||||
  <g fill="#ddd">
 | 
			
		||||
    <rect width="20" height="40" x="25" y="30" rx="2" ry="2" />
 | 
			
		||||
    <rect width="20" height="40" x="55" y="30" rx="2" ry="2" />
 | 
			
		||||
    <rect width="20" height="40" x="85" y="30" rx="2" ry="2" />
 | 
			
		||||
    <rect width="20" height="40" x="115" y="30" rx="2" ry="2" />
 | 
			
		||||
    <rect width="20" height="40" x="145" y="30" rx="2" ry="2" />
 | 
			
		||||
    <rect width="20" height="40" x="175" y="30" rx="2" ry="2" />
 | 
			
		||||
  </g>
 | 
			
		||||
  
 | 
			
		||||
  <!-- 开关标签 -->
 | 
			
		||||
  <g fill="#fff" font-size="8" text-anchor="middle">
 | 
			
		||||
    <text x="25" y="80">1</text>
 | 
			
		||||
    <text x="55" y="80">2</text>
 | 
			
		||||
    <text x="85" y="80">3</text>
 | 
			
		||||
    <text x="115" y="80">4</text>
 | 
			
		||||
    <text x="145" y="80">5</text>
 | 
			
		||||
    <text x="175" y="80">6</text>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
@@ -1,4 +1,4 @@
 | 
			
		||||
import { createMemoryHistory, createRouter } from "vue-router";
 | 
			
		||||
import { createWebHistory, createRouter } from "vue-router";
 | 
			
		||||
import LoginView from "../views/LoginView.vue";
 | 
			
		||||
import UserView from "../views/UserView.vue";
 | 
			
		||||
import TestView from "../views/TestView.vue";
 | 
			
		||||
@@ -16,7 +16,7 @@ const routes = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createMemoryHistory(),
 | 
			
		||||
  history: createWebHistory(),
 | 
			
		||||
  routes,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,67 @@
 | 
			
		||||
import { ref, computed } from 'vue'
 | 
			
		||||
import { ref, computed, watch } from 'vue'
 | 
			
		||||
import { defineStore } from 'pinia'
 | 
			
		||||
 | 
			
		||||
// 本地存储主题的键名
 | 
			
		||||
const THEME_STORAGE_KEY = 'fpga-weblab-theme'
 | 
			
		||||
 | 
			
		||||
export const useThemeStore = defineStore('theme', () => {
 | 
			
		||||
  const allTheme = ["winter", "night"]
 | 
			
		||||
  const darkTheme = "night";
 | 
			
		||||
  const lightTheme = "winter";
 | 
			
		||||
  const currentTheme = ref("night")
 | 
			
		||||
  
 | 
			
		||||
  // 尝试从本地存储中获取保存的主题
 | 
			
		||||
  const getSavedTheme = (): string | null => {
 | 
			
		||||
    return localStorage.getItem(THEME_STORAGE_KEY)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 检测系统主题偏好
 | 
			
		||||
  const getPreferredTheme = (): string => {
 | 
			
		||||
    const savedTheme = getSavedTheme()
 | 
			
		||||
    // 如果有保存的主题设置,优先使用
 | 
			
		||||
    if (savedTheme && allTheme.includes(savedTheme)) {
 | 
			
		||||
      return savedTheme
 | 
			
		||||
    }
 | 
			
		||||
    // 否则检测系统主题模式
 | 
			
		||||
    return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches 
 | 
			
		||||
      ? darkTheme : lightTheme
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  // 初始化主题为首选主题
 | 
			
		||||
  const currentTheme = ref(getPreferredTheme())
 | 
			
		||||
 | 
			
		||||
  // 保存主题到本地存储
 | 
			
		||||
  const saveTheme = (theme: string) => {
 | 
			
		||||
    localStorage.setItem(THEME_STORAGE_KEY, theme)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 当主题变化时,保存到本地存储
 | 
			
		||||
  watch(currentTheme, (newTheme) => {
 | 
			
		||||
    saveTheme(newTheme)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // 添加系统主题变化的监听
 | 
			
		||||
  const setupThemeListener = () => {
 | 
			
		||||
    if (window.matchMedia) {
 | 
			
		||||
      const colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)')
 | 
			
		||||
      const handler = (e: MediaQueryListEvent) => {
 | 
			
		||||
        // 只有当用户没有手动设置过主题时,才跟随系统变化
 | 
			
		||||
        if (!getSavedTheme()) {
 | 
			
		||||
          currentTheme.value = e.matches ? darkTheme : lightTheme
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // 添加主题变化监听器
 | 
			
		||||
      colorSchemeQuery.addEventListener('change', handler)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  function setTheme(theme: string) {
 | 
			
		||||
    const isContained: boolean = allTheme.includes(theme)
 | 
			
		||||
    if (isContained) {
 | 
			
		||||
      currentTheme.value = theme
 | 
			
		||||
      saveTheme(theme) // 保存主题到本地存储
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      console.error('Not have such theme: ${theme}')
 | 
			
		||||
      console.error(`Not have such theme: ${theme}`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -25,6 +73,7 @@ export const useThemeStore = defineStore('theme', () => {
 | 
			
		||||
    } else {
 | 
			
		||||
      currentTheme.value = lightTheme;
 | 
			
		||||
    }
 | 
			
		||||
    // 主题切换时自动保存(通过 watch 函数实现)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function isDarkTheme(): boolean {
 | 
			
		||||
@@ -35,13 +84,19 @@ export const useThemeStore = defineStore('theme', () => {
 | 
			
		||||
    return currentTheme.value == lightTheme
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 初始化时设置系统主题变化监听器
 | 
			
		||||
  if (typeof window !== 'undefined') {
 | 
			
		||||
    setupThemeListener()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    allTheme,
 | 
			
		||||
    currentTheme,
 | 
			
		||||
    setTheme,
 | 
			
		||||
    toggleTheme,
 | 
			
		||||
    isDarkTheme,
 | 
			
		||||
    isLightTheme
 | 
			
		||||
    isLightTheme,
 | 
			
		||||
    setupThemeListener
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +1,79 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="bg-base-200 min-h-screen">
 | 
			
		||||
<template>  <div class="bg-base-200 min-h-screen">
 | 
			
		||||
    <main class="hero min-h-screen bg-base-200">
 | 
			
		||||
      <div class="hero-content flex-col lg:flex-row-reverse">
 | 
			
		||||
        <img src="https://placehold.co/600x400" class="max-w-sm rounded-lg shadow-2xl" />
 | 
			
		||||
        <div>
 | 
			
		||||
          <h1 class="text-5xl font-bold">Welcome to FPGA Web Lab!</h1>
 | 
			
		||||
          <p class="py-6">
 | 
			
		||||
            Prototype and simulate electronic circuits in your browser.
 | 
			
		||||
      <div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 py-10 px-4">
 | 
			
		||||
        <!-- 图片容器 -->
 | 
			
		||||
        <div class="image-container relative w-full max-w-sm hover:scale-105 hover:-rotate-1 transition-transform duration-500 ease-in-out">
 | 
			
		||||
          <img src="https://placehold.co/600x400" class="w-full rounded-2xl shadow-2xl border-4 border-base-300 transition-shadow duration-300 hover:shadow-primary" />
 | 
			
		||||
          <!-- 这里使用relative定位,限制覆盖层只在图片容器内 -->
 | 
			
		||||
          <div class="absolute inset-0 bg-primary opacity-10 rounded-2xl pointer-events-none"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
          <!-- 内容容器 -->
 | 
			
		||||
        <div class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out">
 | 
			
		||||
          <h1 class="text-4xl md:text-5xl font-bold mb-3 relative group">
 | 
			
		||||
            <span class="relative z-10 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
 | 
			
		||||
              Welcome to 
 | 
			
		||||
            </span>
 | 
			
		||||
            <span class="text-base-content">FPGA Web Lab!</span>
 | 
			
		||||
            <span class="absolute bottom-0 left-0 w-0 h-1 bg-primary transition-all duration-500 ease-in-out group-hover:w-3/4"></span>
 | 
			
		||||
          </h1>
 | 
			
		||||
          
 | 
			
		||||
          <p class="py-6 text-lg opacity-80 leading-relaxed">
 | 
			
		||||
            Prototype and simulate electronic circuits in your browser with our modern, intuitive interface. Create, test, and share your FPGA designs seamlessly.
 | 
			
		||||
          </p>
 | 
			
		||||
          <button class="btn btn-primary">Get Started</button>
 | 
			
		||||
            <div class="flex flex-wrap gap-4 actions-container">
 | 
			
		||||
            <router-link to="/project" class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
 | 
			
		||||
                <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              进入工程界面
 | 
			
		||||
            </router-link>
 | 
			
		||||
            
 | 
			
		||||
            <router-link to="/login" class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
 | 
			
		||||
                <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              登录
 | 
			
		||||
            </router-link>
 | 
			
		||||
            
 | 
			
		||||
            <router-link to="/user" class="btn btn-accent text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
 | 
			
		||||
                <circle cx="12" cy="7" r="4"></circle>
 | 
			
		||||
              </svg>
 | 
			
		||||
              用户中心
 | 
			
		||||
            </router-link>
 | 
			
		||||
              <router-link to="/test" class="btn btn-info text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
 | 
			
		||||
                <polyline points="17 21 17 13 7 13 7 21"></polyline>
 | 
			
		||||
                <polyline points="7 3 7 8 15 8"></polyline>
 | 
			
		||||
              </svg>
 | 
			
		||||
              测试功能
 | 
			
		||||
            </router-link>
 | 
			
		||||
            
 | 
			
		||||
            <router-link to="/test/jtag" class="btn btn-warning text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
 | 
			
		||||
                <line x1="8" y1="21" x2="16" y2="21"></line>
 | 
			
		||||
                <line x1="12" y1="17" x2="12" y2="21"></line>
 | 
			
		||||
              </svg>
 | 
			
		||||
              JTAG测试
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </div>
 | 
			
		||||
            <div class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md">
 | 
			
		||||
            <p class="text-sm">
 | 
			
		||||
              <span class="font-semibold text-primary">提示:</span> 您可以在工程界面中创建、编辑和测试您的FPGA项目,使用我们简洁直观的界面轻松进行硬件设计。
 | 
			
		||||
            </p>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </main>
 | 
			
		||||
 | 
			
		||||
    <div class="fixed bottom-10 right-10 btn btn-circle">
 | 
			
		||||
      <ThemeControlButton class=""></ThemeControlButton>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import ThemeControlButton from "@/components/ThemeControlButton.vue";
 | 
			
		||||
import "@/router";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,14 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="h-screen w-screen flex justify-center">
 | 
			
		||||
  <div class="h-screen flex flex-col overflow-hidden">
 | 
			
		||||
    <!-- 主要内容 -->
 | 
			
		||||
    <div class="flex-1 flex justify-center">
 | 
			
		||||
      <div class="h-full w-32"></div>
 | 
			
		||||
 | 
			
		||||
    <div class="h-full w-[70%] shadow-2xl flex">
 | 
			
		||||
      <div class="h-full w-[70%] shadow-2xl flex items-start p-4">
 | 
			
		||||
        <button class="btn btn-primary h-10 w-30">获取ID Code</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,10 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <main>
 | 
			
		||||
    <div class="flex items-center justify-center min-h-screen">
 | 
			
		||||
      <div class="relative w-full max-w-md">
 | 
			
		||||
        <LoginCard />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </main>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,18 @@
 | 
			
		||||
<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">
 | 
			
		||||
  <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"
 | 
			
		||||
          @component-moved="handleComponentMoved"
 | 
			
		||||
          @update-component-prop="updateComponentProp"
 | 
			
		||||
          @component-delete="handleComponentDelete"
 | 
			
		||||
        />
 | 
			
		||||
        <!-- 添加元器件按钮 -->
 | 
			
		||||
        <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>
 | 
			
		||||
@@ -11,117 +20,239 @@
 | 
			
		||||
        </button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    <div class="flex flex-1 overflow-hidden">
 | 
			
		||||
      <!-- 左侧图形化区域 -->
 | 
			
		||||
      <DiagramCanvas 
 | 
			
		||||
        ref="diagramCanvas"
 | 
			
		||||
        :initialComponents="components"
 | 
			
		||||
        @component-selected="handleComponentSelected"
 | 
			
		||||
        @component-moved="handleComponentMoved"
 | 
			
		||||
      />
 | 
			
		||||
      <!-- 拖拽分割线 -->
 | 
			
		||||
      <div
 | 
			
		||||
        class="resizer cursor-col-resize bg-base-300 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
 | 
			
		||||
        @mousedown="startResize"
 | 
			
		||||
      ></div>
 | 
			
		||||
 | 
			
		||||
      <!-- 右侧编辑区域 -->
 | 
			
		||||
      <div class="w-[40%] bg-base-100 border-l flex flex-col p-4">
 | 
			
		||||
      <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="!selectedComponent" class="text-gray-400">选择元器件以编辑属性</div>
 | 
			
		||||
        <div v-if="!selectedComponentData" class="text-gray-400">选择元器件以编辑属性</div>
 | 
			
		||||
        <div v-else>
 | 
			
		||||
          <div class="mb-2">编辑元器件: {{ getComponentName(selectedComponent) }}</div>
 | 
			
		||||
          <!-- 这里可以添加元器件的属性编辑表单 -->
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
          <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>
 | 
			
		||||
          </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 v-if="selectedComponentConfig && selectedComponentConfig.props" class="space-y-4">
 | 
			
		||||
            <div v-for="prop in selectedComponentConfig.props" :key="prop.name" class="form-control">
 | 
			
		||||
              <label class="label">
 | 
			
		||||
                <span class="label-text">{{ prop.label || prop.name }}</span>
 | 
			
		||||
              </label>
 | 
			
		||||
              <!-- 根据 prop 类型选择输入控件 -->
 | 
			
		||||
              <input
 | 
			
		||||
                v-if="prop.type === 'string'"
 | 
			
		||||
                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)"
 | 
			
		||||
              />
 | 
			
		||||
              <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 等 -->
 | 
			
		||||
               <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)"
 | 
			
		||||
                  />
 | 
			
		||||
                  <span>{{ prop.label || prop.name }}</span>
 | 
			
		||||
               </div>              <!-- 下拉选择框 -->
 | 
			
		||||
              <select
 | 
			
		||||
                v-else-if="prop.type === 'select' && prop.options"
 | 
			
		||||
                class="select select-bordered select-sm w-full"
 | 
			
		||||
                :value="selectedComponentData.props[prop.name]"
 | 
			
		||||
                @change="(event) => {
 | 
			
		||||
                  const selectElement = event.target as HTMLSelectElement;
 | 
			
		||||
                  const value = selectElement.value;
 | 
			
		||||
                  console.log('选择的值:', value, '类型:', typeof value);
 | 
			
		||||
                  updateComponentProp(selectedComponentData.id, prop.name, value);
 | 
			
		||||
                }"
 | 
			
		||||
              >
 | 
			
		||||
                <option v-for="option in prop.options" :key="option.value" :value="option.value">
 | 
			
		||||
                  {{ option.label }}
 | 
			
		||||
                </option>
 | 
			
		||||
              </select>
 | 
			
		||||
 | 
			
		||||
        <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>
 | 
			
		||||
              <p v-else class="text-xs text-warning">不支持的属性类型: {{ prop.type }}</p>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
           <div v-else-if="selectedComponentData && !selectedComponentConfig" class="text-gray-500 text-sm">
 | 
			
		||||
             正在加载组件配置...
 | 
			
		||||
           </div>
 | 
			
		||||
           <div v-else-if="selectedComponentData && selectedComponentConfig && (!selectedComponentConfig.props || selectedComponentConfig.props.length === 0)" class="text-gray-500 text-sm">
 | 
			
		||||
             此组件没有可配置的属性。
 | 
			
		||||
           </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 元器件选择组件 -->
 | 
			
		||||
    <ComponentSelector 
 | 
			
		||||
      :open="showComponentsMenu"
 | 
			
		||||
      @update:open="showComponentsMenu = $event"
 | 
			
		||||
      @add-component="handleAddComponent"
 | 
			
		||||
      @close="showComponentsMenu = false"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// 引入wokwi-elements和组件
 | 
			
		||||
import "@wokwi/elements";
 | 
			
		||||
import { ref, reactive } from 'vue';
 | 
			
		||||
// import "@wokwi/elements"; // 不再需要全局引入 wokwi
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
// 元器件管理
 | 
			
		||||
// --- 元器件管理 ---
 | 
			
		||||
const showComponentsMenu = ref(false);
 | 
			
		||||
interface ComponentItem {
 | 
			
		||||
  id: string;
 | 
			
		||||
  type: string;
 | 
			
		||||
  type: string; // 现在是组件的文件名或标识符,例如 'MechanicalButton'
 | 
			
		||||
  name: string;
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  props?: Record<string, any>; // 添加 props 字段来存储组件实例的属性
 | 
			
		||||
}
 | 
			
		||||
const components = ref<ComponentItem[]>([]);
 | 
			
		||||
const selectedComponent = ref<string | null>(null);
 | 
			
		||||
const selectedComponentData = ref<ComponentItem | null>(null);
 | 
			
		||||
const selectedComponentId = ref<string | null>(null); // 重命名为 selectedComponentId
 | 
			
		||||
const selectedComponentData = computed(() => { // 改为计算属性
 | 
			
		||||
  return components.value.find(c => c.id === selectedComponentId.value) || 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: '蜂鸣器' }
 | 
			
		||||
];
 | 
			
		||||
// 存储动态导入的组件模块
 | 
			
		||||
interface ComponentModule {
 | 
			
		||||
  default: any;
 | 
			
		||||
  config?: {
 | 
			
		||||
    props?: Array<{
 | 
			
		||||
      name: string;
 | 
			
		||||
      type: string;
 | 
			
		||||
      label?: string;
 | 
			
		||||
      default: any;
 | 
			
		||||
    }>;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 打开元器件选择菜单
 | 
			
		||||
const componentModules = shallowRef<Record<string, ComponentModule>>({});
 | 
			
		||||
const selectedComponentConfig = shallowRef<ComponentModule['config'] | null>(null); // 存储选中组件的配置
 | 
			
		||||
 | 
			
		||||
// 动态加载组件定义
 | 
			
		||||
async function loadComponentModule(type: string) {
 | 
			
		||||
  if (!componentModules.value[type]) {
 | 
			
		||||
    try {
 | 
			
		||||
      // 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
 | 
			
		||||
      const module = await import(`../components/equipments/${type}.vue`);
 | 
			
		||||
      
 | 
			
		||||
      // 使用 markRaw 包装模块,避免不必要的响应式处理
 | 
			
		||||
      componentModules.value = {
 | 
			
		||||
        ...componentModules.value,
 | 
			
		||||
        [type]: module
 | 
			
		||||
      };
 | 
			
		||||
      
 | 
			
		||||
      console.log(`Loaded module for ${type}:`, module);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`Failed to load component module ${type}:`, error);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return componentModules.value[type];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 分割面板 ---
 | 
			
		||||
const leftPanelWidth = ref(60);
 | 
			
		||||
const isResizing = ref(false);
 | 
			
		||||
 | 
			
		||||
// 分割面板拖拽相关函数
 | 
			
		||||
function startResize(e: MouseEvent) {
 | 
			
		||||
  isResizing.value = true;
 | 
			
		||||
  document.addEventListener('mousemove', onResize);
 | 
			
		||||
  document.addEventListener('mouseup', stopResize);
 | 
			
		||||
  e.preventDefault(); // 防止文本选择
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function onResize(e: MouseEvent) {
 | 
			
		||||
  if (!isResizing.value) return;
 | 
			
		||||
  
 | 
			
		||||
  // 获取容器宽度和鼠标位置
 | 
			
		||||
  const container = document.querySelector('.flex-1.overflow-hidden') as HTMLElement;
 | 
			
		||||
  if (!container) return;
 | 
			
		||||
  
 | 
			
		||||
  const containerWidth = container.clientWidth;
 | 
			
		||||
  const mouseX = e.clientX;
 | 
			
		||||
  
 | 
			
		||||
  // 计算左侧面板应占的百分比
 | 
			
		||||
  let newWidth = (mouseX / containerWidth) * 100;
 | 
			
		||||
  
 | 
			
		||||
  // 限制最小宽度和最大宽度
 | 
			
		||||
  newWidth = Math.max(20, Math.min(newWidth, 80));
 | 
			
		||||
  
 | 
			
		||||
  // 更新宽度
 | 
			
		||||
  leftPanelWidth.value = newWidth;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function stopResize() {
 | 
			
		||||
  isResizing.value = false;
 | 
			
		||||
  document.removeEventListener('mousemove', onResize);
 | 
			
		||||
  document.removeEventListener('mouseup', stopResize);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 元器件操作 ---
 | 
			
		||||
function openComponentsMenu() {
 | 
			
		||||
  showComponentsMenu.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加新元器件
 | 
			
		||||
function addComponent(componentTemplate) {
 | 
			
		||||
  const newComponent = {
 | 
			
		||||
// 处理 ComponentSelector 组件添加元器件事件
 | 
			
		||||
async function handleAddComponent(componentData: { type: string; name: string; props: Record<string, any> }) {
 | 
			
		||||
  // 加载组件模块以便后续使用
 | 
			
		||||
  await loadComponentModule(componentData.type);
 | 
			
		||||
 | 
			
		||||
  const newComponent: ComponentItem = {
 | 
			
		||||
    id: `component-${Date.now()}`,
 | 
			
		||||
    type: componentTemplate.type,
 | 
			
		||||
    name: componentTemplate.name,
 | 
			
		||||
    x: 100,
 | 
			
		||||
    y: 100
 | 
			
		||||
    type: componentData.type,
 | 
			
		||||
    name: componentData.name,
 | 
			
		||||
    x: 100, // 或者计算画布中心位置
 | 
			
		||||
    y: 100,
 | 
			
		||||
    props: componentData.props, // 使用从 ComponentSelector 传递的默认属性
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  components.value.push(newComponent);
 | 
			
		||||
  // 由于我们使用的是响应式数据绑定,不需要再次调用 diagramCanvas 的 addComponent
 | 
			
		||||
  // DiagramCanvas 组件通过 :initialComponents="components" 已经接收到更新
 | 
			
		||||
  
 | 
			
		||||
  showComponentsMenu.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理组件选中事件
 | 
			
		||||
function handleComponentSelected(component) {
 | 
			
		||||
  selectedComponent.value = component ? component.id : null;
 | 
			
		||||
  selectedComponentData.value = component;
 | 
			
		||||
async function handleComponentSelected(componentData: ComponentItem | 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}`);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 同时加载组件模块以备用
 | 
			
		||||
    await loadComponentModule(componentData.type);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理组件移动事件
 | 
			
		||||
function handleComponentMoved(moveData) {
 | 
			
		||||
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;
 | 
			
		||||
@@ -129,9 +260,95 @@ function handleComponentMoved(moveData) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取组件名称
 | 
			
		||||
function getComponentName(componentId) {
 | 
			
		||||
  const component = components.value.find(c => c.id === componentId);
 | 
			
		||||
  return component ? component.name : '';
 | 
			
		||||
// 处理组件删除事件
 | 
			
		||||
function handleComponentDelete(componentId: string) {
 | 
			
		||||
  // 查找要删除的组件索引
 | 
			
		||||
  const index = components.value.findIndex(c => c.id === componentId);
 | 
			
		||||
  if (index !== -1) {
 | 
			
		||||
    // 从数组中移除该组件
 | 
			
		||||
    components.value.splice(index, 1);
 | 
			
		||||
    // 如果删除的是当前选中的组件,清除选中状态
 | 
			
		||||
    if (selectedComponentId.value === componentId) {
 | 
			
		||||
      selectedComponentId.value = null;
 | 
			
		||||
      selectedComponentConfig.value = null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新组件属性的方法,处理字符串类型的初始值特殊格式
 | 
			
		||||
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;
 | 
			
		||||
  }
 | 
			
		||||
  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;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // 直接更新属性值
 | 
			
		||||
    component.props[propName] = value;
 | 
			
		||||
    
 | 
			
		||||
    console.log(`Updated ${componentId} prop ${propName} to:`, value, typeof value);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 生命周期钩子 ---
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 无需在这里预加载组件,ComponentSelector 组件会处理这部分逻辑
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  document.removeEventListener('mousemove', onResize);
 | 
			
		||||
  document.removeEventListener('mouseup', stopResize);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="postcss">
 | 
			
		||||
/* 样式保持不变 */
 | 
			
		||||
@import "../assets/main.css";
 | 
			
		||||
 | 
			
		||||
/* 分割线样式 */
 | 
			
		||||
.resizer {
 | 
			
		||||
  width: 6px;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  background-color: var(--b3);
 | 
			
		||||
  cursor: col-resize;
 | 
			
		||||
  transition: background-color 0.3s;
 | 
			
		||||
  z-index: 10;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.resizer:hover, .resizer:active {
 | 
			
		||||
  width: 6px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 调整大小时应用全局样式 */
 | 
			
		||||
:global(body.resizing) {
 | 
			
		||||
  cursor: col-resize;
 | 
			
		||||
  user-select: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.animate-slideRight {
 | 
			
		||||
  animation: slideRight 0.3s ease-out forwards;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slideRight {
 | 
			
		||||
  from {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transform: translateX(30px);
 | 
			
		||||
  }
 | 
			
		||||
  to {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    transform: translateX(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-screen h-screen">
 | 
			
		||||
  <div class="h-screen overflow-hidden">
 | 
			
		||||
    <Switch width="1400" height="360" />
 | 
			
		||||
    <MechanicalButton width="1400" height="360" />
 | 
			
		||||
    <PopButton></PopButton>
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import UploadCard from "@/components/UploadCard.vue";
 | 
			
		||||
import Sidebar from "../components/Sidebar.vue";
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,14 @@ import autoprefixer from 'autoprefixer'
 | 
			
		||||
// https://vite.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [
 | 
			
		||||
    vue(),
 | 
			
		||||
    vue({
 | 
			
		||||
      template: {
 | 
			
		||||
        compilerOptions: {
 | 
			
		||||
          // 将所有 wokwi- 开头的标签视为自定义元素
 | 
			
		||||
          isCustomElement: (tag) => tag.startsWith('wokwi-')
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }),
 | 
			
		||||
    vueJsx(),
 | 
			
		||||
    vueDevTools(),
 | 
			
		||||
  ],
 | 
			
		||||
 
 | 
			
		||||