feat: remake most of forntend
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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();
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!canvasContainer.value) return;
|
||||||
|
|
||||||
|
// 获取容器的位置
|
||||||
|
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||||||
|
|
||||||
|
// 计算鼠标在容器内的相对位置
|
||||||
|
const mouseX = e.clientX - containerRect.left;
|
||||||
|
const mouseY = e.clientY - containerRect.top;
|
||||||
|
|
||||||
|
// 计算鼠标在画布坐标系中的位置
|
||||||
|
const mouseXCanvas = (mouseX - position.x) / scale.value;
|
||||||
|
const mouseYCanvas = (mouseY - position.y) / scale.value;
|
||||||
|
|
||||||
|
// 计算缩放值
|
||||||
|
const zoomFactor = 1.1; // 每次放大/缩小10%
|
||||||
|
const direction = e.deltaY > 0 ? -1 : 1;
|
||||||
|
|
||||||
|
// 计算新的缩放值
|
||||||
|
let newScale = direction > 0 ? scale.value * zoomFactor : scale.value / zoomFactor;
|
||||||
|
newScale = Math.max(MIN_SCALE, Math.min(newScale, MAX_SCALE));
|
||||||
|
|
||||||
|
// 计算新的位置,使鼠标指针位置在缩放前后保持不变
|
||||||
|
position.x = mouseX - mouseXCanvas * newScale;
|
||||||
|
position.y = mouseY - mouseYCanvas * newScale;
|
||||||
|
|
||||||
|
// 更新缩放值
|
||||||
|
scale.value = newScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 中键拖拽画布(无论点击到哪里)
|
// --- 动态组件渲染 ---
|
||||||
function startMiddleDrag(e) {
|
const getComponentDefinition = (type: string) => {
|
||||||
// 确保是中键
|
const module = props.componentModules[type];
|
||||||
if (e.button !== 1) return;
|
if (!module) return null;
|
||||||
|
|
||||||
e.preventDefault();
|
// 确保我们返回一个有效的组件定义
|
||||||
e.stopPropagation();
|
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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 确保其他拖拽状态被重置
|
// 准备组件属性,确保类型正确
|
||||||
isDragging.value = false;
|
function prepareComponentProps(props: Record<string, any>): Record<string, any> {
|
||||||
|
const result: Record<string, any> = {};
|
||||||
// 设置中键拖拽状态
|
for (const key in props) {
|
||||||
isMiddleDragging.value = true;
|
let value = props[key];
|
||||||
|
// 只要不是 null/undefined 且不是 string,就强制转字符串
|
||||||
// 记录起始位置
|
if (
|
||||||
dragStart.x = e.clientX - position.x;
|
(key === 'style' || key === 'direction' || key === 'type') &&
|
||||||
dragStart.y = e.clientY - position.y;
|
value != null &&
|
||||||
|
typeof value !== 'string'
|
||||||
|
) {
|
||||||
|
value = String(value);
|
||||||
|
}
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理画布鼠标按下事件
|
// --- 画布交互逻辑 ---
|
||||||
function handleCanvasMouseDown(e) {
|
function handleCanvasMouseDown(e: MouseEvent) {
|
||||||
// 如果不是左键,则不做处理
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
// 如果是直接点击画布(而不是元器件),清除选中状态
|
// 如果是直接点击画布(而不是元器件),清除选中状态
|
||||||
if (e.target === canvasContainer.value || e.target === canvas.value) {
|
if (e.target === canvasContainer.value || e.target === canvas.value) {
|
||||||
selectedComponent.value = null;
|
if (selectedComponentId.value !== null) {
|
||||||
|
selectedComponentId.value = null;
|
||||||
emit('component-selected', null);
|
emit('component-selected', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 继续处理拖拽
|
|
||||||
startDrag(e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDrag(e) {
|
// 左键拖拽画布逻辑
|
||||||
// 如果左键或中键拖拽正在进行中
|
if (e.button === 0 && (e.target === canvasContainer.value || e.target === canvas.value)) {
|
||||||
if (isDragging.value || isMiddleDragging.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();
|
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.x = e.clientX - dragStart.x;
|
||||||
position.y = e.clientY - dragStart.y;
|
position.y = e.clientY - dragStart.y;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// 停止拖拽画布
|
||||||
function stopDrag() {
|
function stopDrag() {
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
isMiddleDragging.value = false;
|
isMiddleDragging.value = false;
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', onDrag);
|
||||||
|
document.removeEventListener('mouseup', stopDrag);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 画布缩放
|
// --- 组件拖拽交互 ---
|
||||||
function onZoom(e) {
|
function startComponentDrag(e: MouseEvent, component: ComponentItem) {
|
||||||
e.preventDefault();
|
const target = e.target as HTMLElement;
|
||||||
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 isInteractiveElement = (
|
||||||
const rect = canvasContainer.value.getBoundingClientRect();
|
target.tagName === 'rect' ||
|
||||||
const mouseX = e.clientX - rect.left;
|
target.tagName === 'circle' ||
|
||||||
const mouseY = e.clientY - rect.top;
|
target.tagName === 'path' ||
|
||||||
|
target.hasAttribute('fill-opacity') ||
|
||||||
|
(typeof target.className === 'string' &&
|
||||||
|
(target.className.includes('glow') || target.className.includes('interactive')))
|
||||||
|
);
|
||||||
|
|
||||||
// 计算鼠标在画布中的相对位置
|
// 仍然选中组件,无论是否为交互元素
|
||||||
const mouseXInCanvas = (mouseX - position.x) / scale.value;
|
if (selectedComponentId.value !== component.id) {
|
||||||
const mouseYInCanvas = (mouseY - position.y) / scale.value;
|
selectedComponentId.value = component.id;
|
||||||
|
|
||||||
// 调整位置以保持鼠标位置不变
|
|
||||||
position.x = mouseX - mouseXInCanvas * newScale;
|
|
||||||
position.y = mouseY - mouseYInCanvas * newScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
scale.value = newScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 元器件拖拽
|
|
||||||
function startComponentDrag(e, component) {
|
|
||||||
// 确保只处理左键拖拽元器件
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
draggingComponent.value = component;
|
|
||||||
|
|
||||||
// 设置选中元器件并通知父组件
|
|
||||||
selectedComponent.value = component.id;
|
|
||||||
emit('component-selected', component);
|
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">
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
|
@ -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 |
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 1.5 MiB |
After Width: | Height: | Size: 1.5 MiB |
|
@ -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 |
After Width: | Height: | Size: 26 KiB |
|
@ -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 |
After Width: | Height: | Size: 1.5 MiB |
|
@ -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 |
After Width: | Height: | Size: 1.5 MiB |
|
@ -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(),
|
||||||
],
|
],
|
||||||
|
|