feat: remake most of forntend
							
								
								
									
										93
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						@@ -8,9 +8,8 @@
 | 
				
			|||||||
      "name": "fpga-weblab",
 | 
					      "name": "fpga-weblab",
 | 
				
			||||||
      "version": "0.1.0",
 | 
					      "version": "0.1.0",
 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
 | 
					        "@svgdotjs/svg.js": "^3.2.4",
 | 
				
			||||||
        "@types/lodash": "^4.17.16",
 | 
					        "@types/lodash": "^4.17.16",
 | 
				
			||||||
        "@wokwi/elements": "^1.7.0",
 | 
					 | 
				
			||||||
        "all": "^0.0.0",
 | 
					 | 
				
			||||||
        "lodash": "^4.17.21",
 | 
					        "lodash": "^4.17.21",
 | 
				
			||||||
        "log-symbols": "^7.0.0",
 | 
					        "log-symbols": "^7.0.0",
 | 
				
			||||||
        "pinia": "^3.0.1",
 | 
					        "pinia": "^3.0.1",
 | 
				
			||||||
@@ -999,21 +998,6 @@
 | 
				
			|||||||
        "@jridgewell/sourcemap-codec": "^1.4.14"
 | 
					        "@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": {
 | 
					    "node_modules/@polka/url": {
 | 
				
			||||||
      "version": "1.0.0-next.29",
 | 
					      "version": "1.0.0-next.29",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
 | 
				
			||||||
@@ -1344,6 +1328,16 @@
 | 
				
			|||||||
        "url": "https://github.com/sponsors/sindresorhus"
 | 
					        "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": {
 | 
					    "node_modules/@tailwindcss/node": {
 | 
				
			||||||
      "version": "4.1.4",
 | 
					      "version": "4.1.4",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.4.tgz",
 | 
				
			||||||
@@ -1642,21 +1636,6 @@
 | 
				
			|||||||
        "undici-types": "~6.21.0"
 | 
					        "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": {
 | 
					    "node_modules/@vitejs/plugin-vue": {
 | 
				
			||||||
      "version": "5.2.3",
 | 
					      "version": "5.2.3",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz",
 | 
					      "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": {
 | 
					    "node_modules/alien-signals": {
 | 
				
			||||||
      "version": "1.0.13",
 | 
					      "version": "1.0.13",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz",
 | 
				
			||||||
@@ -2017,12 +1983,6 @@
 | 
				
			|||||||
      "dev": true,
 | 
					      "dev": true,
 | 
				
			||||||
      "license": "MIT"
 | 
					      "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": {
 | 
					    "node_modules/ansi-styles": {
 | 
				
			||||||
      "version": "6.2.1",
 | 
					      "version": "6.2.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
 | 
				
			||||||
@@ -3019,37 +2979,6 @@
 | 
				
			|||||||
        "url": "https://opencollective.com/parcel"
 | 
					        "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": {
 | 
					    "node_modules/lodash": {
 | 
				
			||||||
      "version": "4.17.21",
 | 
					      "version": "4.17.21",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,9 +14,8 @@
 | 
				
			|||||||
    "postgen-api": "pkill server"
 | 
					    "postgen-api": "pkill server"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
 | 
					    "@svgdotjs/svg.js": "^3.2.4",
 | 
				
			||||||
    "@types/lodash": "^4.17.16",
 | 
					    "@types/lodash": "^4.17.16",
 | 
				
			||||||
    "@wokwi/elements": "^1.7.0",
 | 
					 | 
				
			||||||
    "all": "^0.0.0",
 | 
					 | 
				
			||||||
    "lodash": "^4.17.21",
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
    "log-symbols": "^7.0.0",
 | 
					    "log-symbols": "^7.0.0",
 | 
				
			||||||
    "pinia": "^3.0.1",
 | 
					    "pinia": "^3.0.1",
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										57
									
								
								src/App.vue
									
									
									
									
									
								
							
							
						
						@@ -1,39 +1,56 @@
 | 
				
			|||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import iconMenu from "./assets/menu.svg";
 | 
					 | 
				
			||||||
import Sidebar from "./components/Sidebar.vue";
 | 
					 | 
				
			||||||
import Navbar from "./components/Navbar.vue";
 | 
					import Navbar from "./components/Navbar.vue";
 | 
				
			||||||
import { useThemeStore } from "./stores/theme";
 | 
					import { ref, provide, onMounted } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const theme = useThemeStore();
 | 
					// 主题切换状态管理
 | 
				
			||||||
const items = [
 | 
					const isDarkMode = ref(window.matchMedia('(prefers-color-scheme: dark)').matches);
 | 
				
			||||||
  { id: 1, icon: iconMenu, text: "用户界面", page: "/user" },
 | 
					
 | 
				
			||||||
  { id: 2, icon: iconMenu, text: "ComponentTest", page: "/test" },
 | 
					// 初始化主题设置
 | 
				
			||||||
  { id: 3, icon: iconMenu, text: "JtagTest", page: "/test/jtag" },
 | 
					onMounted(() => {
 | 
				
			||||||
  { id: 4, icon: iconMenu, text: "工程界面", page: "/project" }, // 新增工程界面入口
 | 
					  // 应用初始主题
 | 
				
			||||||
];
 | 
					  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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div :data-theme="theme.currentTheme">
 | 
					  <div>
 | 
				
			||||||
    <header class="relative">
 | 
					    <header class="relative">
 | 
				
			||||||
      <div class="fixed left-0 top-0 z-50 hidden">
 | 
					 | 
				
			||||||
        <Sidebar :items="items" />
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      <Navbar></Navbar>
 | 
					      <Navbar></Navbar>
 | 
				
			||||||
    </header>
 | 
					    </header>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <main>
 | 
					    <main>
 | 
				
			||||||
      <RouterView />
 | 
					      <RouterView />
 | 
				
			||||||
    </main>
 | 
					    </main>    <footer class="footer footer-center p-4 bg-base-300 text-base-content">
 | 
				
			||||||
 | 
					 | 
				
			||||||
    <footer class="footer footer-center p-4 bg-base-300 text-base-content">
 | 
					 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        <p>Copyright © 2023 - All right reserved by OurEDA</p>
 | 
					        <p>Copyright © 2023 - All right reserved by OurEDA</p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </footer>
 | 
					    </footer>  </div>
 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
@import "./assets/main.css";
 | 
					/* 特定于App.vue的样式 */
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,3 +6,26 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
 | 
					@custom-variant dark (&:where([data-theme=night], [data-theme=night] *));
 | 
				
			||||||
@custom-variant light (&:where([data-theme=winter], [data-theme=winter] *));
 | 
					@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>
 | 
					<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="handleCanvasMouseDown"
 | 
				
			||||||
      @mousedown.middle.prevent="startMiddleDrag"
 | 
					      @mousedown.middle.prevent="startMiddleDrag"
 | 
				
			||||||
      @mousemove="onDrag" 
 | 
					      @wheel.prevent="onZoom">
 | 
				
			||||||
      @mouseup="stopDrag" 
 | 
					 | 
				
			||||||
      @mouseleave="stopDrag"
 | 
					 | 
				
			||||||
      @wheel="onZoom">
 | 
					 | 
				
			||||||
    <div
 | 
					    <div
 | 
				
			||||||
      ref="canvas"
 | 
					      ref="canvas"
 | 
				
			||||||
      class="diagram-canvas"
 | 
					      class="diagram-canvas"
 | 
				
			||||||
      :style="{transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`}">
 | 
					      :style="{ transform: `translate(${position.x}px, ${position.y}px) scale(${scale})` }">      <!-- 渲染画布上的组件 -->
 | 
				
			||||||
      <!-- wokwi-elements FPGA开发板 -->
 | 
					      <div v-for="component in props.components" :key="component.id"
 | 
				
			||||||
      <wokwi-fpga-board class="absolute top-10 left-10" style="width:600px;height:400px;"></wokwi-fpga-board>
 | 
					          class="component-wrapper"
 | 
				
			||||||
        <!-- 放置其他元器件的区域 -->
 | 
					 | 
				
			||||||
      <div v-for="component in components" :key="component.id" 
 | 
					 | 
				
			||||||
          class="absolute cursor-move component-wrapper" 
 | 
					 | 
				
			||||||
          :class="{
 | 
					          :class="{
 | 
				
			||||||
            'component-hover': hoveredComponent === component.id,
 | 
					            '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)"
 | 
					          @mousedown.left.stop="startComponentDrag($event, component)"
 | 
				
			||||||
          @mouseover="hoveredComponent = component.id"
 | 
					          @mouseover="hoveredComponent = component.id"
 | 
				
			||||||
          @mouseleave="hoveredComponent = null">
 | 
					          @mouseleave="hoveredComponent = null"><!-- 动态渲染组件 -->
 | 
				
			||||||
        <component :is="component.type"></component>
 | 
					        <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>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- 缩放指示器 -->
 | 
					    <!-- 缩放指示器 -->
 | 
				
			||||||
    <div class="absolute bottom-2 right-2 bg-base-100 px-2 py-1 rounded-md opacity-70">
 | 
					    <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;">
 | 
				
			||||||
      {{ Math.round(scale * 100) }}%
 | 
					      <span class="text-sm font-medium">{{ Math.round(scale * 100) }}%</span>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
// 引入wokwi-elements
 | 
					import { ref, reactive, onMounted, onUnmounted } from 'vue';
 | 
				
			||||||
import "@wokwi/elements";
 | 
					 | 
				
			||||||
import { ref, reactive, onMounted, watch } from 'vue';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 定义组件接受的属性
 | 
					// 定义组件接受的属性
 | 
				
			||||||
const props = defineProps<{
 | 
					 | 
				
			||||||
  initialComponents?: Array<ComponentItem>,
 | 
					 | 
				
			||||||
}>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 定义组件发出的事件
 | 
					 | 
				
			||||||
const emit = defineEmits(['component-selected', 'component-moved']);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 定义组件接口
 | 
					 | 
				
			||||||
interface ComponentItem {
 | 
					interface ComponentItem {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
  type: string;
 | 
					  type: string;
 | 
				
			||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  x: number;
 | 
					  x: number;
 | 
				
			||||||
  y: 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 position = reactive({ x: 0, y: 0 });
 | 
				
			||||||
const scale = ref(1);
 | 
					const scale = ref(1);
 | 
				
			||||||
const isDragging = ref(false);
 | 
					const isDragging = ref(false);
 | 
				
			||||||
const isMiddleDragging = ref(false); // 是否在使用中键拖拽
 | 
					const isMiddleDragging = ref(false);
 | 
				
			||||||
const dragStart = reactive({ x: 0, y: 0 });
 | 
					const dragStart = reactive({ x: 0, y: 0 });
 | 
				
			||||||
const canvas = ref(null);
 | 
					const selectedComponentId = ref<string | null>(null);
 | 
				
			||||||
const canvasContainer = ref(null);
 | 
					const hoveredComponent = ref<string | null>(null);
 | 
				
			||||||
 | 
					const draggingComponentId = ref<string | null>(null);
 | 
				
			||||||
// 元器件管理
 | 
					 | 
				
			||||||
const components = ref<ComponentItem[]>([]);
 | 
					 | 
				
			||||||
const draggingComponent = ref<ComponentItem | null>(null);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 监听props变化,更新本地组件数组
 | 
					 | 
				
			||||||
watch(() => props.initialComponents, (newComponents) => {
 | 
					 | 
				
			||||||
  if (newComponents) {
 | 
					 | 
				
			||||||
    // 通过创建深拷贝来断开引用连接
 | 
					 | 
				
			||||||
    components.value = JSON.parse(JSON.stringify(newComponents));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}, { immediate: true, deep: true });
 | 
					 | 
				
			||||||
const componentDragOffset = reactive({ x: 0, y: 0 });
 | 
					const componentDragOffset = reactive({ x: 0, y: 0 });
 | 
				
			||||||
const hoveredComponent = ref(null); // 鼠标悬停的元器件ID
 | 
					 | 
				
			||||||
const selectedComponent = ref(null); // 当前选中的元器件ID
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 画布拖拽
 | 
					// 组件引用跟踪
 | 
				
			||||||
function startDrag(e) {
 | 
					const componentRefs = ref<Record<string, any>>({});
 | 
				
			||||||
  // 只处理左键拖拽
 | 
					 | 
				
			||||||
  if (e.button !== 0) return;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // 确保其他拖拽状态被重置
 | 
					// --- 缩放功能 ---
 | 
				
			||||||
  isMiddleDragging.value = false;
 | 
					const MIN_SCALE = 0.2;
 | 
				
			||||||
 | 
					const MAX_SCALE = 3.0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isDragging.value = true;
 | 
					function onZoom(e: WheelEvent) {
 | 
				
			||||||
  dragStart.x = e.clientX - position.x;
 | 
					 | 
				
			||||||
  dragStart.y = e.clientY - position.y;
 | 
					 | 
				
			||||||
  e.preventDefault();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 中键拖拽画布(无论点击到哪里)
 | 
					 | 
				
			||||||
function startMiddleDrag(e) {
 | 
					 | 
				
			||||||
  // 确保是中键
 | 
					 | 
				
			||||||
  if (e.button !== 1) return;
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  e.preventDefault();
 | 
					 | 
				
			||||||
  e.stopPropagation();
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  // 确保其他拖拽状态被重置
 | 
					 | 
				
			||||||
  isDragging.value = false;
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  // 设置中键拖拽状态
 | 
					 | 
				
			||||||
  isMiddleDragging.value = true;
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  // 记录起始位置
 | 
					 | 
				
			||||||
  dragStart.x = e.clientX - position.x;
 | 
					 | 
				
			||||||
  dragStart.y = e.clientY - position.y;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// 处理画布鼠标按下事件
 | 
					 | 
				
			||||||
function handleCanvasMouseDown(e) {
 | 
					 | 
				
			||||||
  // 如果不是左键,则不做处理
 | 
					 | 
				
			||||||
  if (e.button !== 0) return;
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  // 如果是直接点击画布(而不是元器件),清除选中状态
 | 
					 | 
				
			||||||
  if (e.target === canvasContainer.value || e.target === canvas.value) {
 | 
					 | 
				
			||||||
    selectedComponent.value = null;
 | 
					 | 
				
			||||||
    emit('component-selected', null);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  // 继续处理拖拽
 | 
					 | 
				
			||||||
  startDrag(e);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function onDrag(e) {
 | 
					 | 
				
			||||||
  // 如果左键或中键拖拽正在进行中
 | 
					 | 
				
			||||||
  if (isDragging.value || isMiddleDragging.value) {
 | 
					 | 
				
			||||||
    // 防止拖拽过程中选中文本
 | 
					 | 
				
			||||||
  e.preventDefault();
 | 
					  e.preventDefault();
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
    position.x = e.clientX - dragStart.x;
 | 
					  if (!canvasContainer.value) return;
 | 
				
			||||||
    position.y = e.clientY - dragStart.y;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
function stopDrag() {
 | 
					  // 获取容器的位置
 | 
				
			||||||
  isDragging.value = false;
 | 
					  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
				
			||||||
  isMiddleDragging.value = false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
// 画布缩放
 | 
					  // 计算鼠标在容器内的相对位置
 | 
				
			||||||
function onZoom(e) {
 | 
					  const mouseX = e.clientX - containerRect.left;
 | 
				
			||||||
  e.preventDefault();
 | 
					  const mouseY = e.clientY - containerRect.top;
 | 
				
			||||||
  const delta = e.deltaY > 0 ? -0.1 : 0.1;
 | 
					 | 
				
			||||||
  const newScale = Math.max(0.3, Math.min(3, scale.value + delta));
 | 
					 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  // 保持鼠标位置不变的缩放
 | 
					  // 计算鼠标在画布坐标系中的位置
 | 
				
			||||||
  if (canvas.value && canvasContainer.value) {
 | 
					  const mouseXCanvas = (mouseX - position.x) / scale.value;
 | 
				
			||||||
    const rect = canvasContainer.value.getBoundingClientRect();
 | 
					  const mouseYCanvas = (mouseY - position.y) / scale.value;
 | 
				
			||||||
    const mouseX = e.clientX - rect.left;
 | 
					 | 
				
			||||||
    const mouseY = e.clientY - rect.top;
 | 
					 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
    // 计算鼠标在画布中的相对位置
 | 
					  // 计算缩放值
 | 
				
			||||||
    const mouseXInCanvas = (mouseX - position.x) / scale.value;
 | 
					  const zoomFactor = 1.1; // 每次放大/缩小10%
 | 
				
			||||||
    const mouseYInCanvas = (mouseY - position.y) / scale.value;
 | 
					  const direction = e.deltaY > 0 ? -1 : 1;
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
    // 调整位置以保持鼠标位置不变
 | 
					  // 计算新的缩放值
 | 
				
			||||||
    position.x = mouseX - mouseXInCanvas * newScale;
 | 
					  let newScale = direction > 0 ? scale.value * zoomFactor : scale.value / zoomFactor;
 | 
				
			||||||
    position.y = mouseY - mouseYInCanvas * newScale;
 | 
					  newScale = Math.max(MIN_SCALE, Math.min(newScale, MAX_SCALE));
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					  // 计算新的位置,使鼠标指针位置在缩放前后保持不变
 | 
				
			||||||
 | 
					  position.x = mouseX - mouseXCanvas * newScale;
 | 
				
			||||||
 | 
					  position.y = mouseY - mouseYCanvas * newScale;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 更新缩放值
 | 
				
			||||||
  scale.value = newScale;
 | 
					  scale.value = newScale;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 元器件拖拽
 | 
					// --- 动态组件渲染 ---
 | 
				
			||||||
function startComponentDrag(e, component) {
 | 
					const getComponentDefinition = (type: string) => {
 | 
				
			||||||
  // 确保只处理左键拖拽元器件
 | 
					  const module = props.componentModules[type];
 | 
				
			||||||
  if (e.button !== 0) return;
 | 
					  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);
 | 
					    emit('component-selected', component);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  // 保存起始位置和鼠标位置
 | 
					  // 如果是交互元素,则不启动拖拽
 | 
				
			||||||
  const initialX = component.x;
 | 
					  if (isInteractiveElement || e.button !== 0) {
 | 
				
			||||||
  const initialY = component.y;
 | 
					    return;
 | 
				
			||||||
  const startX = e.clientX;
 | 
					  }
 | 
				
			||||||
  const startY = e.clientY;
 | 
					 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
  const mouseMoveHandler = (moveEvent) => {
 | 
					  // 阻止事件冒泡
 | 
				
			||||||
    if (!draggingComponent.value) return;
 | 
					  e.stopPropagation();
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
    // 计算鼠标移动的距离(在屏幕坐标系中)
 | 
					  // 设置拖拽状态
 | 
				
			||||||
    const dx = moveEvent.clientX - startX;
 | 
					  draggingComponentId.value = component.id;
 | 
				
			||||||
    const dy = moveEvent.clientY - startY;
 | 
					  isDragging.value = false;
 | 
				
			||||||
 | 
					  isMiddleDragging.value = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // 将移动距离转换为画布坐标系中的距离
 | 
					  // 获取容器位置
 | 
				
			||||||
    const canvasDx = dx / scale.value;
 | 
					  if (!canvasContainer.value) return;
 | 
				
			||||||
    const canvasDy = dy / scale.value;
 | 
					  const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
    // 更新组件位置(相对于初始位置的增量)
 | 
					  // 计算鼠标在画布坐标系中的位置
 | 
				
			||||||
    draggingComponent.value.x = initialX + canvasDx;
 | 
					  const mouseX_canvas = (e.clientX - containerRect.left - position.x) / scale.value;
 | 
				
			||||||
    draggingComponent.value.y = initialY + canvasDy;
 | 
					  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', {
 | 
					  emit('component-moved', {
 | 
				
			||||||
      id: draggingComponent.value.id,
 | 
					    id: draggingComponentId.value,
 | 
				
			||||||
      x: draggingComponent.value.x,
 | 
					    x: Math.round(newX),
 | 
				
			||||||
      y: draggingComponent.value.y
 | 
					    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 {
 | 
					function stopComponentDrag() {
 | 
				
			||||||
  constructor() {
 | 
					  draggingComponentId.value = null;
 | 
				
			||||||
    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>
 | 
					 | 
				
			||||||
    `;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
// 公开方法,允许外部添加组件
 | 
					  document.removeEventListener('mousemove', onComponentDrag);
 | 
				
			||||||
function addComponent(component: ComponentItem) {
 | 
					  document.removeEventListener('mouseup', stopComponentDrag);
 | 
				
			||||||
  components.value.push(component);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 公开方法,允许外部重置画布
 | 
					// 更新组件属性
 | 
				
			||||||
function resetCanvas() {
 | 
					function updateComponentProp(componentId: string, propName: string, value: any) {
 | 
				
			||||||
  position.x = 0;
 | 
					  emit('update-component-prop', { id: componentId, propName, value });
 | 
				
			||||||
  position.y = 0;
 | 
					}
 | 
				
			||||||
  scale.value = 1;
 | 
					
 | 
				
			||||||
 | 
					// 获取组件引用,用于外部访问
 | 
				
			||||||
 | 
					function getComponentRef(componentId: string) {
 | 
				
			||||||
 | 
					  const component = props.components.find(c => c.id === componentId);
 | 
				
			||||||
 | 
					  if (!component) return null;
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					  // 查找组件的引用
 | 
				
			||||||
 | 
					  return componentRefs.value[component.id] || null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 暴露给父组件的方法
 | 
					// 暴露给父组件的方法
 | 
				
			||||||
defineExpose({
 | 
					defineExpose({
 | 
				
			||||||
  addComponent,
 | 
					  getComponentRef
 | 
				
			||||||
  resetCanvas
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- 生命周期钩子 ---
 | 
				
			||||||
 | 
					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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<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 {
 | 
					.diagram-canvas {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
  width: 4000px;
 | 
					  width: 4000px;
 | 
				
			||||||
  height: 4000px;
 | 
					  height: 4000px;
 | 
				
			||||||
  transform-origin: 0 0;
 | 
					  transform-origin: 0 0;
 | 
				
			||||||
}
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					  -webkit-user-select: none;
 | 
				
			||||||
/* 禁用滚动条 */
 | 
					  -moz-user-select: none;
 | 
				
			||||||
.flex-1.min-w-\[60\%\].bg-base-200.relative.overflow-auto::-webkit-scrollbar {
 | 
					  -ms-user-select: none;
 | 
				
			||||||
  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 {
 | 
					.component-wrapper {
 | 
				
			||||||
  position: relative;
 | 
					  position: absolute;
 | 
				
			||||||
  padding: 5px;
 | 
					  padding: 0; /* 移除内边距,确保元素大小与内容完全匹配 */
 | 
				
			||||||
  box-sizing: border-box;
 | 
					  box-sizing: content-box; /* 使用content-box确保内容尺寸不受padding影响 */
 | 
				
			||||||
  display: inline-block; /* 确保元素宽度基于内容 */
 | 
					  display: inline-block;
 | 
				
			||||||
  max-width: fit-content; /* 强制宽度适应内容 */
 | 
					 | 
				
			||||||
  max-height: fit-content; /* 强制高度适应内容 */
 | 
					 | 
				
			||||||
  overflow: visible; /* 允许内容溢出(用于显示边框) */
 | 
					  overflow: visible; /* 允许内容溢出(用于显示边框) */
 | 
				
			||||||
 | 
					  cursor: move; /* 显示移动光标 */
 | 
				
			||||||
 | 
					  user-select: none;
 | 
				
			||||||
 | 
					  -webkit-user-select: none;
 | 
				
			||||||
 | 
					  -moz-user-select: none;
 | 
				
			||||||
 | 
					  -ms-user-select: none;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* 悬停状态 */
 | 
					/* 悬停状态 - 使用outline而非伪元素 */
 | 
				
			||||||
.component-hover::before {
 | 
					.component-hover {
 | 
				
			||||||
  content: '';
 | 
					  outline: 2px dashed #3498db;
 | 
				
			||||||
  position: absolute;
 | 
					  outline-offset: 2px;
 | 
				
			||||||
  top: -4px;
 | 
					  z-index: 2;
 | 
				
			||||||
  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-selected::before {
 | 
					.component-selected {
 | 
				
			||||||
  content: '';
 | 
					  outline: 3px dashed;
 | 
				
			||||||
  position: absolute;
 | 
					  outline-color: #e74c3c #f39c12 #3498db #2ecc71;
 | 
				
			||||||
  top: -4px;
 | 
					  outline-offset: 3px;
 | 
				
			||||||
  left: -4px;
 | 
					  z-index: 999 !important; /* 使用更高的z-index确保始终在顶层 */
 | 
				
			||||||
  right: -4px;
 | 
					}
 | 
				
			||||||
  bottom: -4px;
 | 
					
 | 
				
			||||||
  border: 4px dashed #e74c3c;
 | 
					/* 为黑暗模式设置不同的网格线颜色 */
 | 
				
			||||||
  border-color: #e74c3c #f39c12 #3498db #2ecc71;
 | 
					:root[data-theme="dark"] .diagram-container {
 | 
				
			||||||
  pointer-events: none;
 | 
					  background-image: 
 | 
				
			||||||
  z-index: 1;
 | 
					    linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
				
			||||||
  border-radius: 4px;
 | 
					    linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
 | 
				
			||||||
  box-sizing: content-box;
 | 
					    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>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,35 +1,81 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="navbar bg-base-100 shadow-sm">
 | 
					  <div class="navbar bg-base-100 shadow-xl">
 | 
				
			||||||
    <div class="navbar-start">
 | 
					    <div class="navbar-start">
 | 
				
			||||||
      <div class="dropdown">
 | 
					      <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">
 | 
					          <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" />
 | 
					            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" />
 | 
				
			||||||
          </svg>
 | 
					          </svg>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <ul tabindex="0" class="menu menu-sm dropdown-content bg-base-100 rounded-box z-1 mt-3 w-52 p-2 shadow">
 | 
					        <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><a>Item 1</a></li>
 | 
					          <li class="my-1 hover:translate-x-1 transition-all duration-300">
 | 
				
			||||||
          <li>
 | 
					            <router-link to="/" class="text-base font-medium">
 | 
				
			||||||
            <a>Parent</a>
 | 
					              <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">
 | 
				
			||||||
            <ul class="p-2">
 | 
					                <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
 | 
				
			||||||
              <li><a>Submenu 1</a></li>
 | 
					                <polyline points="9 22 9 12 15 12 15 22"/>
 | 
				
			||||||
              <li><a>Submenu 2</a></li>
 | 
					              </svg>
 | 
				
			||||||
            </ul>
 | 
					              首页
 | 
				
			||||||
 | 
					            </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>
 | 
				
			||||||
          <li><a>Item 3</a></li>
 | 
					 | 
				
			||||||
        </ul>
 | 
					        </ul>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="navbar-center lg:flex">
 | 
					    <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>
 | 
				
			||||||
    <div class="navbar-end">
 | 
					    <div class="navbar-end">
 | 
				
			||||||
      <a class="btn btn-soft w-20 mx-10">注册</a>
 | 
					      <router-link to="/login" class="btn btn-primary text-base-100 transition-all duration-300 hover:scale-105 hover:shadow-lg mr-3">
 | 
				
			||||||
      <a class="btn btn-primary w-25">登录</a>
 | 
					        登录
 | 
				
			||||||
 | 
					      </router-link>
 | 
				
			||||||
 | 
					      <div class="ml-2 transition-all duration-500 hover:rotate-12">
 | 
				
			||||||
 | 
					        <ThemeControlButton />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup lang="ts">
 | 
				
			||||||
 | 
					import ThemeControlButton from "./ThemeControlButton.vue";
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
@import "../assets/main.css";
 | 
					@import "../assets/main.css";
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,34 +1,46 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div>
 | 
					  <div class="theme-control-wrapper">
 | 
				
			||||||
    <label class="swap swap-rotate">
 | 
					    <label class="swap swap-rotate theme-toggle">      <!-- this hidden checkbox controls the state -->
 | 
				
			||||||
      <!-- this hidden checkbox controls the state -->
 | 
					      <input type="checkbox" value="synthwave" @click="toggleTheme" :checked="checkState" />
 | 
				
			||||||
      <input type="checkbox" value="synthwave" @click="theme.toggleTheme" :checked="checkState" />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- sun icon -->
 | 
					      <!-- 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
 | 
					          <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" />
 | 
					            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>
 | 
					        </svg>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- moon icon -->
 | 
					      <!-- 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
 | 
					          <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" />
 | 
					            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>
 | 
					        </svg>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
    </label>
 | 
					    </label>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { useThemeStore } from '@/stores/theme';
 | 
					import { inject, computed } from 'vue';
 | 
				
			||||||
import { computed } from 'vue';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const theme = useThemeStore();
 | 
					// 注入由 App.vue 提供的主题相关函数和状态
 | 
				
			||||||
 | 
					const { isDarkMode, toggleTheme } = inject('theme') as {
 | 
				
			||||||
 | 
					  isDarkMode: { value: boolean },
 | 
				
			||||||
 | 
					  toggleTheme: () => void
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 计算复选框的选中状态
 | 
				
			||||||
const checkState = computed(() => {
 | 
					const checkState = computed(() => {
 | 
				
			||||||
  return theme.isDarkTheme()
 | 
					  return isDarkMode.value;
 | 
				
			||||||
})
 | 
					});
 | 
				
			||||||
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped lang="postcss">
 | 
					<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>  
 | 
					<template>  
 | 
				
			||||||
  <div class="relative" :style="{width: `${props.width}px`, height: `${props.height}px`}">
 | 
					  <div class="button-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
 | 
				
			||||||
    <!-- 简化的SVG按钮 -->
 | 
					    <svg 
 | 
				
			||||||
    <svg xmlns="http://www.w3.org/2000/svg" :width="props.width" :height="props.height" viewBox="0 0 1600 1600">
 | 
					      xmlns="http://www.w3.org/2000/svg" 
 | 
				
			||||||
 | 
					      :width="width" 
 | 
				
			||||||
 | 
					      :height="height" 
 | 
				
			||||||
 | 
					      viewBox="400 400 800 800"
 | 
				
			||||||
 | 
					      class="mechanical-button"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <!-- defs 和按钮底座保持不变 -->
 | 
				
			||||||
      <defs>
 | 
					      <defs>
 | 
				
			||||||
        <filter id="btn-shadow">
 | 
					        <filter id="btn-shadow">
 | 
				
			||||||
          <feGaussianBlur in="SourceAlpha" stdDeviation="20" result="blur" />
 | 
					          <feGaussianBlur in="SourceAlpha" stdDeviation="20" result="blur" />
 | 
				
			||||||
@@ -42,13 +48,12 @@
 | 
				
			|||||||
        fill-opacity="0.9"
 | 
					        fill-opacity="0.9"
 | 
				
			||||||
        @mousedown="toggleButtonState(true)"
 | 
					        @mousedown="toggleButtonState(true)"
 | 
				
			||||||
        @mouseup="toggleButtonState(false)"
 | 
					        @mouseup="toggleButtonState(false)"
 | 
				
			||||||
        @contextmenu.prevent="openContextMenu($event)"
 | 
					        @mouseleave="toggleButtonState(false)"
 | 
				
			||||||
        style="pointer-events: auto; transition: all 20ms ease-in-out;"
 | 
					        style="pointer-events: auto; transition: all 20ms ease-in-out; cursor: pointer;"
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
      
 | 
					 | 
				
			||||||
      <!-- 按键文字 -->
 | 
					      <!-- 按键文字 -->
 | 
				
			||||||
      <text
 | 
					      <text
 | 
				
			||||||
        v-if="bindKey"
 | 
					        v-if="displayText"
 | 
				
			||||||
        x="800"
 | 
					        x="800"
 | 
				
			||||||
        y="800"
 | 
					        y="800"
 | 
				
			||||||
        font-size="310"
 | 
					        font-size="310"
 | 
				
			||||||
@@ -57,98 +62,169 @@
 | 
				
			|||||||
        fill="#ccc"
 | 
					        fill="#ccc"
 | 
				
			||||||
        style="font-family: Arial; filter: url(#btn-shadow); user-select: none; pointer-events: none; mix-blend-mode: overlay;"
 | 
					        style="font-family: Arial; filter: url(#btn-shadow); user-select: none; pointer-events: none; mix-blend-mode: overlay;"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {{ bindKey.toUpperCase() }}
 | 
					        {{ displayText }}
 | 
				
			||||||
      </text>
 | 
					      </text>
 | 
				
			||||||
    </svg>
 | 
					    </svg>
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    <!-- 使用DaisyUI的卡片组件实现上下文菜单 -->
 | 
					    <!-- 嵌入Pin组件,覆盖在按钮上 -->
 | 
				
			||||||
    <div v-if="showContextMenu" 
 | 
					    <div class="pin-wrapper" :style="{
 | 
				
			||||||
         class="card card-compact fixed z-50 shadow-lg bg-base-100 border border-base-300"
 | 
					      position: 'absolute',
 | 
				
			||||||
         :style="{ top: contextMenuY + 'px', left: contextMenuX + 'px' }"
 | 
					      top: '80%',
 | 
				
			||||||
         @click.stop>
 | 
					      left: '50%', 
 | 
				
			||||||
      <div class="card-body p-0">
 | 
					      transform: 'translate(-50%, 20%)',
 | 
				
			||||||
        <button class="btn btn-ghost justify-start normal-case w-full h-full" @click="startBinding">
 | 
					      zIndex: 3,
 | 
				
			||||||
          <span v-if="isBinding">请输入</span>
 | 
					      pointerEvents: 'auto'
 | 
				
			||||||
          <span v-else>绑定按键: {{ bindKey ? bindKey.toUpperCase() : '未绑定' }}</span>
 | 
					    }">
 | 
				
			||||||
        </button>
 | 
					      <Pin
 | 
				
			||||||
      </div>
 | 
					        direction="output"
 | 
				
			||||||
 | 
					        type="digital"  
 | 
				
			||||||
 | 
					        appearance="None"
 | 
				
			||||||
 | 
					        :label="props.label"
 | 
				
			||||||
 | 
					        :constraint="props.constraint"
 | 
				
			||||||
 | 
					        :size="0.8"
 | 
				
			||||||
 | 
					        @value-change="handlePinValueChange"
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import { ref, onMounted, onUnmounted } from 'vue';
 | 
					import { ref, onMounted, onUnmounted, computed } from 'vue';
 | 
				
			||||||
 | 
					import Pin from './Pin.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					// 从Pin组件继承属性
 | 
				
			||||||
  width?: string | number
 | 
					interface PinProps {
 | 
				
			||||||
  height?: string | number
 | 
					  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>(), {
 | 
					const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
  width: 160,
 | 
					  size: 1,
 | 
				
			||||||
  height: 160,
 | 
					  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 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 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) {
 | 
					function toggleButtonState(isPressed: boolean) {
 | 
				
			||||||
  btnHeight.value = isPressed ? 210 : 200;
 | 
					  isKeyPressed.value = isPressed;
 | 
				
			||||||
  colorMatrix.value = isPressed 
 | 
					  btnHeight.value = isPressed ? 180 : 200;
 | 
				
			||||||
    ? "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";
 | 
					  // 发出事件通知父组件
 | 
				
			||||||
 | 
					  if (isPressed) {
 | 
				
			||||||
 | 
					    emit('press');
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    emit('release');
 | 
				
			||||||
 | 
					    emit('click');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function openContextMenu(e: MouseEvent) {
 | 
					// 处理键盘事件
 | 
				
			||||||
  contextMenuX.value = e.clientX;
 | 
					function handleKeyDown(event: KeyboardEvent) {
 | 
				
			||||||
  contextMenuY.value = e.clientY;
 | 
					  if (event.key === props.bindKey) {
 | 
				
			||||||
  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;
 | 
					 | 
				
			||||||
    toggleButtonState(true);
 | 
					    toggleButtonState(true);
 | 
				
			||||||
 | 
					    setTimeout(() => toggleButtonState(false), 150);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handleKeyUp(e: KeyboardEvent) {
 | 
					// --- 生命周期钩子 ---
 | 
				
			||||||
  if (e.key.toLowerCase() === bindKey.value) {
 | 
					 | 
				
			||||||
    isKeyPressed = false;
 | 
					 | 
				
			||||||
    toggleButtonState(false);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
onMounted(() => {
 | 
					onMounted(() => {
 | 
				
			||||||
  window.addEventListener('keydown', handleKeyDown);
 | 
					  document.addEventListener('keydown', handleKeyDown);
 | 
				
			||||||
  window.addEventListener('keyup', handleKeyUp);
 | 
					 | 
				
			||||||
  window.addEventListener('click', closeContextMenu);
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
onUnmounted(() => {
 | 
					onUnmounted(() => {
 | 
				
			||||||
  window.removeEventListener('keydown', handleKeyDown);
 | 
					  document.removeEventListener('keydown', handleKeyDown);
 | 
				
			||||||
  window.removeEventListener('keyup', handleKeyUp);
 | 
					});
 | 
				
			||||||
  window.removeEventListener('click', closeContextMenu);
 | 
					
 | 
				
			||||||
 | 
					// 向外暴露方法
 | 
				
			||||||
 | 
					defineExpose({
 | 
				
			||||||
 | 
					  toggleButtonState,
 | 
				
			||||||
 | 
					  getInfo: () => ({
 | 
				
			||||||
 | 
					    // 按钮特有属性
 | 
				
			||||||
 | 
					    bindKey: props.bindKey,
 | 
				
			||||||
 | 
					    buttonText: props.buttonText,
 | 
				
			||||||
 | 
					    // 继承自Pin的属性
 | 
				
			||||||
 | 
					    label: props.label,
 | 
				
			||||||
 | 
					    constraint: props.constraint,
 | 
				
			||||||
 | 
					    // 固定的Pin属性
 | 
				
			||||||
 | 
					    direction: 'output',
 | 
				
			||||||
 | 
					    type: 'digital',
 | 
				
			||||||
 | 
					    appearance: 'None'
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</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>
 | 
					<template>
 | 
				
			||||||
  <svg xmlns="http://www.w3.org/2000/svg" :width="props.width" :height="props.height" viewBox="0 0 16 16">
 | 
					  <svg 
 | 
				
			||||||
    <def>
 | 
					    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%">
 | 
					      <filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
 | 
				
			||||||
        <feFlood result="flood" flood-color="#f08a5d" flood-opacity="1"></feFlood>
 | 
					        <feFlood result="flood" flood-color="#f08a5d" flood-opacity="1"></feFlood>
 | 
				
			||||||
        <feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
 | 
					        <feComposite in="flood" result="mask" in2="SourceGraphic" operator="in"></feComposite>
 | 
				
			||||||
@@ -15,49 +22,130 @@
 | 
				
			|||||||
          <feMergeNode in="SourceGraphic" />
 | 
					          <feMergeNode in="SourceGraphic" />
 | 
				
			||||||
        </feMerge>
 | 
					        </feMerge>
 | 
				
			||||||
      </filter>
 | 
					      </filter>
 | 
				
			||||||
    </def>
 | 
					    </defs>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    <g>
 | 
					    <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>
 | 
					      <g>
 | 
				
			||||||
        <rect class="glow" @click="toggleBtnStatus(0)" width="0.7" height="2" fill="#68716f" x="5.15" y="7" rx="0.1" />
 | 
					        <template v-for="(_, index) in Array(props.switchCount)" :key="index">
 | 
				
			||||||
        <rect class="glow" @click="toggleBtnStatus(1)" width="0.7" height="2" fill="#68716f" x="6.15" y="7" rx="0.1" />
 | 
					          <rect 
 | 
				
			||||||
        <rect class="glow" @click="toggleBtnStatus(2)" width="0.7" height="2" fill="#68716f" x="7.15" y="7" rx="0.1" />
 | 
					            class="glow interactive" 
 | 
				
			||||||
        <rect class="glow" @click="toggleBtnStatus(3)" width="0.7" height="2" fill="#68716f" x="8.15" y="7" rx="0.1" />
 | 
					            @click="toggleBtnStatus(index)" 
 | 
				
			||||||
        <rect class="glow" @click="toggleBtnStatus(4)" width="0.7" height="2" fill="#68716f" x="9.15" y="7" rx="0.1" />
 | 
					            width="0.7" 
 | 
				
			||||||
        <rect class="glow" @click="toggleBtnStatus(5)" width="0.7" height="2" fill="#68716f" x="10.15" y="7" rx="0.1" />
 | 
					            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>
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      <g>
 | 
					      <g>
 | 
				
			||||||
        <rect @click="toggleBtnStatus(0)" width="0.65" height="0.65" fill="white" x="5.175" :y="btnLocation[0]" rx="0.1"
 | 
					        <template v-for="(location, index) in btnLocation" :key="`btn-${index}`">
 | 
				
			||||||
          opacity="1" />
 | 
					          <rect 
 | 
				
			||||||
        <rect @click="toggleBtnStatus(1)" width="0.65" height="0.65" fill="white" x="6.175" :y="btnLocation[1]" rx="0.1"
 | 
					            class="interactive"
 | 
				
			||||||
          opacity="1" />
 | 
					            @click="toggleBtnStatus(index)" 
 | 
				
			||||||
        <rect @click="toggleBtnStatus(2)" width="0.65" height="0.65" fill="white" x="7.175" :y="btnLocation[2]" rx="0.1"
 | 
					            width="0.65" 
 | 
				
			||||||
          opacity="1" />
 | 
					            height="0.65" 
 | 
				
			||||||
        <rect @click="toggleBtnStatus(3)" width="0.65" height="0.65" fill="white" x="8.175" :y="btnLocation[3]" rx="0.1"
 | 
					            fill="white" 
 | 
				
			||||||
          opacity="1" />
 | 
					            :x="5.175 + index" 
 | 
				
			||||||
        <rect @click="toggleBtnStatus(4)" width="0.65" height="0.65" fill="white" x="9.175" :y="btnLocation[4]" rx="0.1"
 | 
					            :y="location" 
 | 
				
			||||||
          opacity="1" />
 | 
					            rx="0.1"
 | 
				
			||||||
        <rect @click="toggleBtnStatus(5)" width="0.65" height="0.65" fill="white" x="10.175" :y="btnLocation[5]"
 | 
					            opacity="1" 
 | 
				
			||||||
          rx="0.1" opacity="1" />
 | 
					          />
 | 
				
			||||||
 | 
					        </template>
 | 
				
			||||||
      </g>
 | 
					      </g>
 | 
				
			||||||
    </g>
 | 
					    </g>
 | 
				
			||||||
  </svg>
 | 
					  </svg>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import { computed, ref } from "vue";
 | 
					import { computed, ref, watch } from "vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  width?: string | number;
 | 
					  size?: number;
 | 
				
			||||||
  height?: string | number;
 | 
					  switchCount?: number;
 | 
				
			||||||
 | 
					  // 新增属性
 | 
				
			||||||
 | 
					  initialValues?: boolean[] | string; // 开关的初始状态,可以是布尔数组或逗号分隔的字符串
 | 
				
			||||||
 | 
					  showLabels?: boolean;      // 是否显示标签
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const props = withDefaults(defineProps<Props>(), {
 | 
					const props = withDefaults(defineProps<Props>(), {
 | 
				
			||||||
  width: 160,
 | 
					  size: 1,
 | 
				
			||||||
  height: 160,
 | 
					  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(() => {
 | 
					const btnLocation = computed(() => {
 | 
				
			||||||
  return btnStatus.value.map((status) => {
 | 
					  return btnStatus.value.map((status) => {
 | 
				
			||||||
    return status ? 7.025 : 8.325;
 | 
					    return status ? 7.025 : 8.325;
 | 
				
			||||||
@@ -65,20 +153,58 @@ const btnLocation = computed(() => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setBtnStatus(btnNum: number, isOn: boolean): void {
 | 
					function setBtnStatus(btnNum: number, isOn: boolean): void {
 | 
				
			||||||
 | 
					  if (btnNum >= 0 && btnNum < btnStatus.value.length) {
 | 
				
			||||||
    btnStatus.value[btnNum] = isOn;
 | 
					    btnStatus.value[btnNum] = isOn;
 | 
				
			||||||
 | 
					    emit('change', { index: btnNum, value: isOn, states: [...btnStatus.value] });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function toggleBtnStatus(btnNum: number): void {
 | 
					function toggleBtnStatus(btnNum: number): void {
 | 
				
			||||||
 | 
					  if (btnNum >= 0 && btnNum < btnStatus.value.length) {
 | 
				
			||||||
    btnStatus.value[btnNum] = !btnStatus.value[btnNum];
 | 
					    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>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped lang="postcss">
 | 
					<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 {
 | 
					rect {
 | 
				
			||||||
  transition: all 100ms ease-in-out;
 | 
					  transition: all 100ms ease-in-out;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.glow:hover {
 | 
					.interactive {
 | 
				
			||||||
  filter: url(#glow);
 | 
					  cursor: pointer;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
</style>
 | 
					</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 LoginView from "../views/LoginView.vue";
 | 
				
			||||||
import UserView from "../views/UserView.vue";
 | 
					import UserView from "../views/UserView.vue";
 | 
				
			||||||
import TestView from "../views/TestView.vue";
 | 
					import TestView from "../views/TestView.vue";
 | 
				
			||||||
@@ -16,7 +16,7 @@ const routes = [
 | 
				
			|||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const router = createRouter({
 | 
					const router = createRouter({
 | 
				
			||||||
  history: createMemoryHistory(),
 | 
					  history: createWebHistory(),
 | 
				
			||||||
  routes,
 | 
					  routes,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,67 @@
 | 
				
			|||||||
import { ref, computed } from 'vue'
 | 
					import { ref, computed, watch } from 'vue'
 | 
				
			||||||
import { defineStore } from 'pinia'
 | 
					import { defineStore } from 'pinia'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 本地存储主题的键名
 | 
				
			||||||
 | 
					const THEME_STORAGE_KEY = 'fpga-weblab-theme'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const useThemeStore = defineStore('theme', () => {
 | 
					export const useThemeStore = defineStore('theme', () => {
 | 
				
			||||||
  const allTheme = ["winter", "night"]
 | 
					  const allTheme = ["winter", "night"]
 | 
				
			||||||
  const darkTheme = "night";
 | 
					  const darkTheme = "night";
 | 
				
			||||||
  const lightTheme = "winter";
 | 
					  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) {
 | 
					  function setTheme(theme: string) {
 | 
				
			||||||
    const isContained: boolean = allTheme.includes(theme)
 | 
					    const isContained: boolean = allTheme.includes(theme)
 | 
				
			||||||
    if (isContained) {
 | 
					    if (isContained) {
 | 
				
			||||||
      currentTheme.value = theme
 | 
					      currentTheme.value = theme
 | 
				
			||||||
 | 
					      saveTheme(theme) // 保存主题到本地存储
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else {
 | 
					    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 {
 | 
					    } else {
 | 
				
			||||||
      currentTheme.value = lightTheme;
 | 
					      currentTheme.value = lightTheme;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // 主题切换时自动保存(通过 watch 函数实现)
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  function isDarkTheme(): boolean {
 | 
					  function isDarkTheme(): boolean {
 | 
				
			||||||
@@ -35,13 +84,19 @@ export const useThemeStore = defineStore('theme', () => {
 | 
				
			|||||||
    return currentTheme.value == lightTheme
 | 
					    return currentTheme.value == lightTheme
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 初始化时设置系统主题变化监听器
 | 
				
			||||||
 | 
					  if (typeof window !== 'undefined') {
 | 
				
			||||||
 | 
					    setupThemeListener()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    allTheme,
 | 
					    allTheme,
 | 
				
			||||||
    currentTheme,
 | 
					    currentTheme,
 | 
				
			||||||
    setTheme,
 | 
					    setTheme,
 | 
				
			||||||
    toggleTheme,
 | 
					    toggleTheme,
 | 
				
			||||||
    isDarkTheme,
 | 
					    isDarkTheme,
 | 
				
			||||||
    isLightTheme
 | 
					    isLightTheme,
 | 
				
			||||||
 | 
					    setupThemeListener
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,26 +1,79 @@
 | 
				
			|||||||
<template>
 | 
					<template>  <div class="bg-base-200 min-h-screen">
 | 
				
			||||||
  <div class="bg-base-200 min-h-screen">
 | 
					 | 
				
			||||||
    <main class="hero min-h-screen bg-base-200">
 | 
					    <main class="hero min-h-screen bg-base-200">
 | 
				
			||||||
      <div class="hero-content flex-col lg:flex-row-reverse">
 | 
					      <div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-12 py-10 px-4">
 | 
				
			||||||
        <img src="https://placehold.co/600x400" class="max-w-sm rounded-lg shadow-2xl" />
 | 
					        <!-- 图片容器 -->
 | 
				
			||||||
        <div>
 | 
					        <div class="image-container relative w-full max-w-sm hover:scale-105 hover:-rotate-1 transition-transform duration-500 ease-in-out">
 | 
				
			||||||
          <h1 class="text-5xl font-bold">Welcome to FPGA Web Lab!</h1>
 | 
					          <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" />
 | 
				
			||||||
          <p class="py-6">
 | 
					          <!-- 这里使用relative定位,限制覆盖层只在图片容器内 -->
 | 
				
			||||||
            Prototype and simulate electronic circuits in your browser.
 | 
					          <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>
 | 
					          </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>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </main>
 | 
					    </main>
 | 
				
			||||||
 | 
					 | 
				
			||||||
    <div class="fixed bottom-10 right-10 btn btn-circle">
 | 
					 | 
				
			||||||
      <ThemeControlButton class=""></ThemeControlButton>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
import ThemeControlButton from "@/components/ThemeControlButton.vue";
 | 
					 | 
				
			||||||
import "@/router";
 | 
					import "@/router";
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,14 @@
 | 
				
			|||||||
<template>
 | 
					<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-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>
 | 
					        <button class="btn btn-primary h-10 w-30">获取ID Code</button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script lang="ts" setup>
 | 
					<script lang="ts" setup>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,10 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <main>
 | 
					  <main>
 | 
				
			||||||
    <div class="flex items-center justify-center min-h-screen">
 | 
					    <div class="flex items-center justify-center min-h-screen">
 | 
				
			||||||
 | 
					      <div class="relative w-full max-w-md">
 | 
				
			||||||
        <LoginCard />
 | 
					        <LoginCard />
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
  </main>
 | 
					  </main>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,18 @@
 | 
				
			|||||||
<template>
 | 
					<template>
 | 
				
			||||||
  <div class="h-screen w-screen flex flex-col">
 | 
					  <div class="h-screen flex flex-col overflow-hidden">
 | 
				
			||||||
    <!-- 顶部工具栏 -->
 | 
					    <div class="flex flex-1 overflow-hidden relative">
 | 
				
			||||||
    <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>
 | 
					      <div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }">        <DiagramCanvas
 | 
				
			||||||
      <button class="btn btn-circle btn-primary" @click="openComponentsMenu">
 | 
					          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">
 | 
					           <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="12" y1="5" x2="12" y2="19"></line>
 | 
				
			||||||
            <line x1="5" y1="12" x2="19" y2="12"></line>
 | 
					            <line x1="5" y1="12" x2="19" y2="12"></line>
 | 
				
			||||||
@@ -11,117 +20,239 @@
 | 
				
			|||||||
        </button>
 | 
					        </button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="flex flex-1 overflow-hidden">
 | 
					      <!-- 拖拽分割线 -->
 | 
				
			||||||
      <!-- 左侧图形化区域 -->
 | 
					      <div
 | 
				
			||||||
      <DiagramCanvas 
 | 
					        class="resizer cursor-col-resize bg-base-300 hover:bg-primary hover:opacity-70 active:bg-primary active:opacity-90 transition-colors"
 | 
				
			||||||
        ref="diagramCanvas"
 | 
					        @mousedown="startResize"
 | 
				
			||||||
        :initialComponents="components"
 | 
					      ></div>
 | 
				
			||||||
        @component-selected="handleComponentSelected"
 | 
					 | 
				
			||||||
        @component-moved="handleComponentMoved"
 | 
					 | 
				
			||||||
      />
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      <!-- 右侧编辑区域 -->
 | 
					      <!-- 右侧编辑区域 -->
 | 
				
			||||||
      <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>
 | 
					        <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 v-else>
 | 
				
			||||||
          <div class="mb-2">编辑元器件: {{ getComponentName(selectedComponent) }}</div>
 | 
					          <div class="mb-4 pb-4 border-b border-base-300">
 | 
				
			||||||
          <!-- 这里可以添加元器件的属性编辑表单 -->
 | 
					             <h4 class="font-semibold text-lg mb-1">{{ selectedComponentData.name }}</h4>
 | 
				
			||||||
        </div>
 | 
					             <p class="text-xs text-gray-500">ID: {{ selectedComponentData.id }}</p>
 | 
				
			||||||
      </div>
 | 
					             <p class="text-xs text-gray-500">类型: {{ selectedComponentData.type }}</p>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <!-- 元器件选择菜单 -->
 | 
					          <!-- 动态属性表单 -->
 | 
				
			||||||
    <div v-if="showComponentsMenu" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" @click.self="showComponentsMenu = false">
 | 
					          <div v-if="selectedComponentConfig && selectedComponentConfig.props" class="space-y-4">
 | 
				
			||||||
      <div class="bg-base-100 p-4 rounded-lg shadow-xl max-w-3xl max-h-[80vh] overflow-auto">
 | 
					            <div v-for="prop in selectedComponentConfig.props" :key="prop.name" class="form-control">
 | 
				
			||||||
        <div class="flex justify-between items-center mb-4">
 | 
					              <label class="label">
 | 
				
			||||||
          <h3 class="text-xl font-bold">选择元器件</h3>
 | 
					                <span class="label-text">{{ prop.label || prop.name }}</span>
 | 
				
			||||||
          <button class="btn btn-ghost btn-sm" @click="showComponentsMenu = false">
 | 
					              </label>
 | 
				
			||||||
            <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">
 | 
					              <!-- 根据 prop 类型选择输入控件 -->
 | 
				
			||||||
              <line x1="18" y1="6" x2="6" y2="18"></line>
 | 
					              <input
 | 
				
			||||||
              <line x1="6" y1="6" x2="18" y2="18"></line>
 | 
					                v-if="prop.type === 'string'"
 | 
				
			||||||
            </svg>
 | 
					                type="text"
 | 
				
			||||||
          </button>
 | 
					                :placeholder="prop.label || prop.name"
 | 
				
			||||||
        </div>
 | 
					                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">
 | 
					              <p v-else class="text-xs text-warning">不支持的属性类型: {{ prop.type }}</p>
 | 
				
			||||||
          <div v-for="(component, index) in availableComponents" :key="index" 
 | 
					 | 
				
			||||||
              class="border p-4 rounded-lg flex flex-col items-center hover:bg-base-200 cursor-pointer"
 | 
					 | 
				
			||||||
              @click="addComponent(component)">
 | 
					 | 
				
			||||||
            <div class="flex-1 flex items-center justify-center p-2">
 | 
					 | 
				
			||||||
              <component :is="component.type" style="transform: scale(0.5);"></component>
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
            <div class="mt-2 text-center">{{ component.name }}</div>
 | 
					 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
 | 
					           <div 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>
 | 
				
			||||||
 | 
					      </div>    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- 元器件选择组件 -->
 | 
				
			||||||
 | 
					    <ComponentSelector 
 | 
				
			||||||
 | 
					      :open="showComponentsMenu"
 | 
				
			||||||
 | 
					      @update:open="showComponentsMenu = $event"
 | 
				
			||||||
 | 
					      @add-component="handleAddComponent"
 | 
				
			||||||
 | 
					      @close="showComponentsMenu = false"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</template>
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
// 引入wokwi-elements和组件
 | 
					// 引入wokwi-elements和组件
 | 
				
			||||||
import "@wokwi/elements";
 | 
					// import "@wokwi/elements"; // 不再需要全局引入 wokwi
 | 
				
			||||||
import { ref, reactive } from 'vue';
 | 
					import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // 引入 defineAsyncComponent 和 shallowRef
 | 
				
			||||||
import DiagramCanvas from '@/components/DiagramCanvas.vue';
 | 
					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);
 | 
					const showComponentsMenu = ref(false);
 | 
				
			||||||
interface ComponentItem {
 | 
					interface ComponentItem {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
  type: string;
 | 
					  type: string; // 现在是组件的文件名或标识符,例如 'MechanicalButton'
 | 
				
			||||||
  name: string;
 | 
					  name: string;
 | 
				
			||||||
  x: number;
 | 
					  x: number;
 | 
				
			||||||
  y: number;
 | 
					  y: number;
 | 
				
			||||||
 | 
					  props?: Record<string, any>; // 添加 props 字段来存储组件实例的属性
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
const components = ref<ComponentItem[]>([]);
 | 
					const components = ref<ComponentItem[]>([]);
 | 
				
			||||||
const selectedComponent = ref<string | null>(null);
 | 
					const selectedComponentId = ref<string | null>(null); // 重命名为 selectedComponentId
 | 
				
			||||||
const selectedComponentData = ref<ComponentItem | null>(null);
 | 
					const selectedComponentData = computed(() => { // 改为计算属性
 | 
				
			||||||
 | 
					  return components.value.find(c => c.id === selectedComponentId.value) || null;
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
const diagramCanvas = ref(null);
 | 
					const diagramCanvas = ref(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 可用元器件列表
 | 
					// 存储动态导入的组件模块
 | 
				
			||||||
const availableComponents = [
 | 
					interface ComponentModule {
 | 
				
			||||||
  { type: 'wokwi-led', name: 'LED灯' },
 | 
					  default: any;
 | 
				
			||||||
  { type: 'wokwi-resistor', name: '电阻' },
 | 
					  config?: {
 | 
				
			||||||
  { type: 'wokwi-pushbutton', name: '按钮' },
 | 
					    props?: Array<{
 | 
				
			||||||
  { type: 'wokwi-7segment', name: '7段数码管' },
 | 
					      name: string;
 | 
				
			||||||
  { type: 'wokwi-arduino-uno', name: 'Arduino Uno' },
 | 
					      type: string;
 | 
				
			||||||
  { type: 'wokwi-servo', name: '舵机' },
 | 
					      label?: string;
 | 
				
			||||||
  { type: 'wokwi-lcd1602', name: 'LCD显示屏' },
 | 
					      default: any;
 | 
				
			||||||
  { type: 'wokwi-dht22', name: '温湿度传感器' },
 | 
					    }>;
 | 
				
			||||||
  { type: 'wokwi-buzzer', name: '蜂鸣器' }
 | 
					  };
 | 
				
			||||||
];
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 打开元器件选择菜单
 | 
					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() {
 | 
					function openComponentsMenu() {
 | 
				
			||||||
  showComponentsMenu.value = true;
 | 
					  showComponentsMenu.value = true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 添加新元器件
 | 
					// 处理 ComponentSelector 组件添加元器件事件
 | 
				
			||||||
function addComponent(componentTemplate) {
 | 
					async function handleAddComponent(componentData: { type: string; name: string; props: Record<string, any> }) {
 | 
				
			||||||
  const newComponent = {
 | 
					  // 加载组件模块以便后续使用
 | 
				
			||||||
 | 
					  await loadComponentModule(componentData.type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const newComponent: ComponentItem = {
 | 
				
			||||||
    id: `component-${Date.now()}`,
 | 
					    id: `component-${Date.now()}`,
 | 
				
			||||||
    type: componentTemplate.type,
 | 
					    type: componentData.type,
 | 
				
			||||||
    name: componentTemplate.name,
 | 
					    name: componentData.name,
 | 
				
			||||||
    x: 100,
 | 
					    x: 100, // 或者计算画布中心位置
 | 
				
			||||||
    y: 100
 | 
					    y: 100,
 | 
				
			||||||
 | 
					    props: componentData.props, // 使用从 ComponentSelector 传递的默认属性
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  components.value.push(newComponent);
 | 
					  components.value.push(newComponent);
 | 
				
			||||||
  // 由于我们使用的是响应式数据绑定,不需要再次调用 diagramCanvas 的 addComponent
 | 
					 | 
				
			||||||
  // DiagramCanvas 组件通过 :initialComponents="components" 已经接收到更新
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  showComponentsMenu.value = false;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 处理组件选中事件
 | 
					// 处理组件选中事件
 | 
				
			||||||
function handleComponentSelected(component) {
 | 
					async function handleComponentSelected(componentData: ComponentItem | null) {
 | 
				
			||||||
  selectedComponent.value = component ? component.id : null;
 | 
					  selectedComponentId.value = componentData ? componentData.id : null;
 | 
				
			||||||
  selectedComponentData.value = component;
 | 
					  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);
 | 
					  const component = components.value.find(c => c.id === moveData.id);
 | 
				
			||||||
  if (component) {
 | 
					  if (component) {
 | 
				
			||||||
    component.x = moveData.x;
 | 
					    component.x = moveData.x;
 | 
				
			||||||
@@ -129,9 +260,95 @@ function handleComponentMoved(moveData) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// 获取组件名称
 | 
					// 处理组件删除事件
 | 
				
			||||||
function getComponentName(componentId) {
 | 
					function handleComponentDelete(componentId: string) {
 | 
				
			||||||
  const component = components.value.find(c => c.id === componentId);
 | 
					  // 查找要删除的组件索引
 | 
				
			||||||
  return component ? component.name : '';
 | 
					  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>
 | 
					</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>
 | 
					<template>
 | 
				
			||||||
  <div class="w-screen h-screen">
 | 
					  <div class="h-screen overflow-hidden">
 | 
				
			||||||
    <Switch width="1400" height="360" />
 | 
					    <Switch width="1400" height="360" />
 | 
				
			||||||
    <MechanicalButton width="1400" height="360" />
 | 
					    <MechanicalButton width="1400" height="360" />
 | 
				
			||||||
    <PopButton></PopButton>
 | 
					    <PopButton></PopButton>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<script setup lang="ts">
 | 
					<script setup lang="ts">
 | 
				
			||||||
import UploadCard from "@/components/UploadCard.vue";
 | 
					import UploadCard from "@/components/UploadCard.vue";
 | 
				
			||||||
import Sidebar from "../components/Sidebar.vue";
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style scoped>
 | 
					<style scoped>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,14 @@ import autoprefixer from 'autoprefixer'
 | 
				
			|||||||
// https://vite.dev/config/
 | 
					// https://vite.dev/config/
 | 
				
			||||||
export default defineConfig({
 | 
					export default defineConfig({
 | 
				
			||||||
  plugins: [
 | 
					  plugins: [
 | 
				
			||||||
    vue(),
 | 
					    vue({
 | 
				
			||||||
 | 
					      template: {
 | 
				
			||||||
 | 
					        compilerOptions: {
 | 
				
			||||||
 | 
					          // 将所有 wokwi- 开头的标签视为自定义元素
 | 
				
			||||||
 | 
					          isCustomElement: (tag) => tag.startsWith('wokwi-')
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }),
 | 
				
			||||||
    vueJsx(),
 | 
					    vueJsx(),
 | 
				
			||||||
    vueDevTools(),
 | 
					    vueDevTools(),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
 
 | 
				
			|||||||