Compare commits

...

5 Commits

Author SHA1 Message Date
099d44663d Merge branch 'csharp' 2025-05-13 13:00:37 +08:00
alivender
91838ff632 add: DDS virtual component 2025-05-11 14:41:38 +08:00
alivender
6a786c1519 feat: Refactor code structure for improved readability and maintainability 2025-05-09 19:28:43 +08:00
alivender
d4b34bd6d4 Merge branch 'master' of ssh://git.swordlost.top:222/SikongJueluo/FPGA_WebLab into dpp 2025-05-07 15:43:04 +08:00
alivender
47cfe17d16 Refactor component configuration and diagram management
- Removed the component configuration from `componentConfig.ts` to streamline the codebase.
- Introduced a new `diagram.json` file to define the initial structure for diagrams.
- Created a `diagramManager.ts` to handle diagram data, including loading, saving, and validating diagram structures.
- Updated `ProjectView.vue` to integrate the new diagram management system, including handling component selection and property updates.
- Enhanced the component property management to support dynamic attributes and improved error handling.
- Added functions for managing connections between components within the diagram.
2025-05-07 15:42:35 +08:00
22 changed files with 5854 additions and 1261 deletions

98
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"mathjs": "^14.4.0",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tinypool": "^1.0.2", "tinypool": "^1.0.2",
"ts-log": "^2.2.7", "ts-log": "^2.2.7",
@@ -475,6 +476,15 @@
"@babel/core": "^7.0.0-0" "@babel/core": "^7.0.0-0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.27.0", "version": "7.27.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.0.tgz",
@@ -2144,6 +2154,19 @@
], ],
"license": "CC-BY-4.0" "license": "CC-BY-4.0"
}, },
"node_modules/complex.js": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
"integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==",
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/convert-source-map": { "node_modules/convert-source-map": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2245,6 +2268,12 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
"integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
"license": "MIT"
},
"node_modules/default-browser": { "node_modules/default-browser": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
@@ -2392,6 +2421,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==",
"license": "MIT"
},
"node_modules/estree-walker": { "node_modules/estree-walker": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
@@ -2681,6 +2716,12 @@
"node": ">=16" "node": ">=16"
} }
}, },
"node_modules/javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
"license": "MIT"
},
"node_modules/jiti": { "node_modules/jiti": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz",
@@ -3034,6 +3075,42 @@
"@jridgewell/sourcemap-codec": "^1.5.0" "@jridgewell/sourcemap-codec": "^1.5.0"
} }
}, },
"node_modules/mathjs": {
"version": "14.4.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.4.0.tgz",
"integrity": "sha512-CpoYDhNENefjIG9wU9epr+0pBHzlaySfpWcblZdAf5qXik/j/U8eSmx/oNbmXO0F5PyfwPGVD/wK4VWsTho1SA==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.26.10",
"complex.js": "^2.2.5",
"decimal.js": "^10.4.3",
"escape-latex": "^1.2.0",
"fraction.js": "^5.2.1",
"javascript-natural-sort": "^0.7.1",
"seedrandom": "^3.0.5",
"tiny-emitter": "^2.1.0",
"typed-function": "^4.2.1"
},
"bin": {
"mathjs": "bin/cli.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mathjs/node_modules/fraction.js": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.2.tgz",
"integrity": "sha512-uXBDv5knpYmv/2gLzWQ5mBHGBRk9wcKTeWu6GLTUEQfjCxO09uM/mHDrojlL+Q1mVGIIFo149Gba7od1XPgSzQ==",
"license": "MIT",
"engines": {
"node": ">= 12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/rawify"
}
},
"node_modules/memorystream": { "node_modules/memorystream": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz",
@@ -3443,6 +3520,12 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"license": "MIT"
},
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -3577,6 +3660,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.13", "version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
@@ -3625,6 +3714,15 @@
"integrity": "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw==", "integrity": "sha512-HjX/7HxQe2bXkbp8pHTjy4Ir9eHIDnDDsLDphhGqy6I9iZ/vD4QXWEIlrVRZsEX+kS2jIiiF/mnl0nKnPTiYFw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/typed-function": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
"integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==",
"license": "MIT",
"engines": {
"node": ">= 18"
}
},
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.7.3", "version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",

View File

@@ -18,6 +18,7 @@
"@types/lodash": "^4.17.16", "@types/lodash": "^4.17.16",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"log-symbols": "^7.0.0", "log-symbols": "^7.0.0",
"mathjs": "^14.4.0",
"pinia": "^3.0.1", "pinia": "^3.0.1",
"tinypool": "^1.0.2", "tinypool": "^1.0.2",
"ts-log": "^2.2.7", "ts-log": "^2.2.7",

View File

@@ -0,0 +1,197 @@
<script setup lang="ts">
// 定义属性接口
interface Props {
title: string;
isExpanded?: boolean;
status?: 'default' | 'success' | 'error';
}
const props = withDefaults(defineProps<Props>(), {
isExpanded: false,
status: 'default'
});
const emit = defineEmits<{
(e: 'update:isExpanded', value: boolean): void
}>();
// 切换展开/收起状态
const toggleExpand = () => {
emit('update:isExpanded', !props.isExpanded);
};
// 动画处理函数
const enter = (element: Element, done: () => void) => {
if (element instanceof HTMLElement) {
const height = element.scrollHeight;
element.style.height = '0px';
// 触发重绘
element.offsetHeight;
element.style.height = height + 'px';
element.addEventListener('transitionend', () => {
done();
}, { once: true });
} else {
done();
}
};
const afterEnter = (element: Element) => {
if (element instanceof HTMLElement) {
element.style.height = 'auto';
}
};
const leave = (element: Element, done: () => void) => {
if (element instanceof HTMLElement) {
const height = element.scrollHeight;
element.style.height = height + 'px';
// 触发重绘
element.offsetHeight;
element.style.height = '0px';
element.addEventListener('transitionend', () => {
done();
}, { once: true });
} else {
done();
}
};
</script>
<template>
<div class="section" :class="[`status-${status}`]">
<div class="section-header" @click="toggleExpand">
<h2>{{ title }}</h2>
<span class="expand-icon" :class="{ 'is-expanded': isExpanded }"></span>
</div>
<transition
name="collapse"
@enter="enter"
@after-enter="afterEnter"
@leave="leave"
>
<div v-show="isExpanded" class="section-content">
<div class="section-inner">
<slot></slot>
</div>
</div>
</transition>
</div>
</template>
<style scoped>
.section {
margin-bottom: var(--spacing-md, 0.75rem);
border: 1px solid hsl(var(--b3));
border-radius: var(--radius-md, 0.375rem);
overflow: hidden;
background-color: hsl(var(--b1));
box-shadow: var(--shadow-sm, 0 1px 2px 0 rgba(0, 0, 0, 0.05));
transition: all var(--transition-normal, 0.3s);
}
/* 默认状态 */
.section.status-default {
border-color: hsl(var(--b3));
}
/* 成功状态 */
.section.status-success {
animation: borderPulseSuccess 2s cubic-bezier(0.4, 0, 0.2, 1);
border-color: hsl(var(--su));
box-shadow: 0px 0px 3px hsl(var(--su));
}
/* 失败状态 */
.section.status-error {
animation: borderPulseError 2s cubic-bezier(0.4, 0, 0.2, 1);
border-color: hsl(var(--er));
box-shadow: 0px 0px 3px hsl(var(--er));
}
/* 信息状态 */
.section.status-info {
animation: borderPulseInfo 2s cubic-bezier(0.4, 0, 0.2, 1);
border-color: hsl(var(--in));
box-shadow: 0px 0px 3px hsl(var(--in));
}
@keyframes borderPulseSuccess {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;}
50% { border-color: hsl(var(--su)); box-shadow: 0px 0px 5px hsl(var(--su));}
100% { border-color: hsl(var(--su)); box-shadow: 0px 0px 3px hsl(var(--su));}
}
@keyframes borderPulseError {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;}
50% { border-color: hsl(var(--er)); box-shadow: 0px 0px 5px hsl(var(--er));}
100% { border-color: hsl(var(--er)); box-shadow: 0px 0px 3px hsl(var(--er));}
}
@keyframes borderPulseInfo {
0% { border-color: hsl(var(--b3)); box-shadow: 0px 0px 0px transparent;}
50% { border-color: hsl(var(--in)); box-shadow: 0px 0px 5px hsl(var(--in));}
100% { border-color: hsl(var(--in)); box-shadow: 0px 0px 3px hsl(var(--in));}
}
.section-header {
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 0.75rem);
background-color: hsl(var(--b1));
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
border-bottom: 1px solid hsl(var(--b3));
transition: all var(--transition-normal, 0.3s);
}
.section-header:hover {
background-color: hsl(var(--b2));
}
.section-header h2 {
margin: 0;
font-size: 1.1em;
font-weight: 500;
color: hsl(var(--p));
transition: color var(--transition-normal, 0.3s);
}
.expand-icon {
font-size: 16px;
color: hsl(var(--bc));
transition: all var(--transition-normal, 0.3s);
}
.expand-icon.is-expanded {
transform: rotate(90deg);
}
.section-content {
overflow: hidden;
transition: all var(--transition-normal, 0.3s);
background-color: transparent;
}
.section-inner {
padding: var(--spacing-md, 0.75rem);
color: hsl(var(--bc));
}
@media (max-width: 900px) {
.section-inner {
padding: var(--spacing-sm, 0.5rem);
}
}
.content-wrapper {
overflow: visible; /* 允许内容溢出 */
}
.card-body {
overflow: visible; /* 允许内容溢出 */
}
</style>

View File

@@ -5,8 +5,7 @@
<input id="component-drawer" type="checkbox" class="drawer-toggle" v-model="showComponentsMenu" /> <input id="component-drawer" type="checkbox" class="drawer-toggle" v-model="showComponentsMenu" />
<div class="drawer-side"> <div class="drawer-side">
<label for="component-drawer" aria-label="close sidebar" class="drawer-overlay !bg-opacity-50"></label> <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="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"> <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"> <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"> <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">
@@ -24,13 +23,20 @@
</label> </label>
</div> </div>
<!-- 导航栏 -->
<div class="tabs tabs-boxed bg-base-200 mx-6 mt-4 rounded-box">
<a class="tab" :class="{ 'tab-active': activeTab === 'components' }" @click="activeTab = 'components'">元器件</a>
<a class="tab" :class="{ 'tab-active': activeTab === 'templates' }" @click="activeTab = 'templates'">模板</a>
<a class="tab" :class="{ 'tab-active': activeTab === 'virtual' }" @click="activeTab = 'virtual'">虚拟外设</a>
</div>
<!-- 搜索框 --> <!-- 搜索框 -->
<div class="px-6 py-4 border-b border-base-300"> <div class="px-6 py-4 border-b border-base-300">
<div class="join w-full"> <div class="join w-full">
<div class="join-item flex-1 relative"> <div class="join-item flex-1 relative">
<input <input
type="text" type="text"
placeholder="搜索元器件..." placeholder="搜索..."
class="input input-bordered input-sm w-full pl-10" class="input input-bordered input-sm w-full pl-10"
v-model="searchQuery" v-model="searchQuery"
@keyup.enter="searchComponents" @keyup.enter="searchComponents"
@@ -44,8 +50,8 @@
</div> </div>
</div> </div>
<!-- 元器件列表 --> <!-- 元器件列表 (组件选项卡) -->
<div class="px-6 py-4 overflow-auto flex-1"> <div v-if="activeTab === 'components'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredComponents.length > 0" class="grid grid-cols-2 gap-4"> <div v-if="filteredComponents.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(component, index) in filteredComponents" :key="index" <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" class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@@ -81,6 +87,71 @@
</div> </div>
</div> </div>
<!-- 模板列表 (模板选项卡) -->
<div v-if="activeTab === 'templates'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredTemplates.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(template, index) in filteredTemplates" :key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addTemplate(template)">
<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">
<img :src="template.thumbnailUrl || '/placeholder-template.png'" alt="Template thumbnail" class="max-h-full max-w-full object-contain" />
</div>
<h3 class="card-title text-sm mt-2">{{ template.name }}</h3>
<p class="text-xs opacity-70">{{ template.description || '模板' }}</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 v-if="activeTab === 'virtual'" class="px-6 py-4 overflow-auto flex-1">
<div v-if="filteredVirtualDevices.length > 0" class="grid grid-cols-2 gap-4">
<div v-for="(device, index) in filteredVirtualDevices" :key="index"
class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
@click="addComponent(device)">
<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[device.type]"
:is="componentModules[device.type].default"
class="component-preview"
:size="getPreviewSize(device.type)"
/>
<!-- 加载中状态 -->
<span v-else class="text-xs text-gray-400">加载中...</span>
</div>
<h3 class="card-title text-sm mt-2">{{ device.name }}</h3>
<p class="text-xs opacity-70">{{ device.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"> <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"> <label for="component-drawer" class="btn btn-sm btn-ghost" @click="closeMenu">
@@ -101,7 +172,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, shallowRef, onMounted } from 'vue'; import { ref, computed, shallowRef, onMounted } from 'vue';
import { getComponentConfig } from '@/components/equipments/componentConfig';
// Props 定义 // Props 定义
interface Props { interface Props {
@@ -111,7 +181,10 @@ interface Props {
const props = defineProps<Props>(); const props = defineProps<Props>();
// 定义组件发出的事件 // 定义组件发出的事件
const emit = defineEmits(['close', 'add-component', 'update:open']); const emit = defineEmits(['close', 'add-component', 'add-template', 'update:open']);
// 当前激活的选项卡
const activeTab = ref('components');
// --- 搜索功能 --- // --- 搜索功能 ---
const searchQuery = ref(''); const searchQuery = ref('');
@@ -122,15 +195,33 @@ const availableComponents = [
{ type: 'Switch', name: '开关' }, { type: 'Switch', name: '开关' },
{ type: 'Pin', name: '引脚' }, { type: 'Pin', name: '引脚' },
{ type: 'SMT_LED', name: '贴片LED' }, { type: 'SMT_LED', name: '贴片LED' },
{ type: 'SevenSegmentDisplay', name: '数码管' },
{ type: 'HDMI', name: 'HDMI接口' }, { type: 'HDMI', name: 'HDMI接口' },
{ type: 'DDR', name: 'DDR内存' }, { type: 'DDR', name: 'DDR内存' },
{ type: 'ETH', name: '以太网接口' }, { type: 'ETH', name: '以太网接口' },
{ type: 'SD', name: 'SD卡插槽' }, { type: 'SD', name: 'SD卡插槽' },
{ type: 'SFP', name: 'SFP光纤模块' }, { type: 'SFP', name: 'SFP光纤模块' },
{ type: 'SMA', name: 'SMA连接器' }, { type: 'SMA', name: 'SMA连接器' },
{ type: 'MotherBoard', name: '主板' } { type: 'MotherBoard', name: '主板' },
{ type: 'PG2L100H_FBG676', name: 'PG2L100H FBG676芯片' }
]; ];
// --- 可用虚拟外设列表 ---
const availableVirtualDevices = [
{ type: 'DDS', name: '信号发生器' }
];
// --- 可用模板列表 ---
const availableTemplates = ref([
{
name: 'PG2L100H 基础开发板',
id: 'PG2L100H_Pango100pro',
description: '包含主板和两个LED的基本设置',
path: '/src/components/equipments/templates/PG2L100H_Pango100pro.json',
thumbnailUrl: '/src/components/equipments/svg/motherboard.svg'
}
]);
// 显示/隐藏组件菜单 // 显示/隐藏组件菜单
const showComponentsMenu = computed({ const showComponentsMenu = computed({
get: () => props.open, get: () => props.open,
@@ -164,6 +255,7 @@ async function loadComponentModule(type: string) {
// 预加载组件模块 // 预加载组件模块
async function preloadComponentModules() { async function preloadComponentModules() {
// 加载基础组件
for (const component of availableComponents) { for (const component of availableComponents) {
try { try {
await loadComponentModule(component.type); await loadComponentModule(component.type);
@@ -171,6 +263,15 @@ async function preloadComponentModules() {
console.error(`Failed to preload component ${component.type}:`, error); console.error(`Failed to preload component ${component.type}:`, error);
} }
} }
// 加载虚拟外设组件
for (const device of availableVirtualDevices) {
try {
await loadComponentModule(device.type);
} catch (error) {
console.error(`Failed to preload virtual device ${device.type}:`, error);
}
}
} }
// 获取组件预览时适合的尺寸 // 获取组件预览时适合的尺寸
@@ -181,13 +282,15 @@ function getPreviewSize(componentType: string): number {
'Switch': 0.35, // 开关较大,需要更小尺寸 'Switch': 0.35, // 开关较大,需要更小尺寸
'Pin': 0.8, // 引脚较小,可以大一些 'Pin': 0.8, // 引脚较小,可以大一些
'SMT_LED': 0.7, // LED可以保持适中 'SMT_LED': 0.7, // LED可以保持适中
'SevenSegmentDisplay': 0.4, // 数码管较大,需要较小尺寸
'HDMI': 0.5, // HDMI接口较大 'HDMI': 0.5, // HDMI接口较大
'DDR': 0.5, // DDR内存较大 'DDR': 0.5, // DDR内存较大
'ETH': 0.5, // 以太网接口较大 'ETH': 0.5, // 以太网接口较大
'SD': 0.6, // SD卡插槽适中 'SD': 0.6, // SD卡插槽适中
'SFP': 0.4, // SFP光纤模块较大 'SFP': 0.4, // SFP光纤模块较大
'SMA': 0.7, // SMA连接器可以适中 'SMA': 0.7, // SMA连接器可以适中
'MotherBoard': 0.13 // 主板最大,需要最小尺寸 'MotherBoard': 0.13, // 主板最大,需要最小尺寸
'DDS': 0.3 // 信号发生器较大,需要较小尺寸
}; };
// 返回对应尺寸如果没有特定配置则返回默认值0.5 // 返回对应尺寸如果没有特定配置则返回默认值0.5
@@ -208,18 +311,22 @@ function closeMenu() {
// 添加新元器件 // 添加新元器件
async function addComponent(componentTemplate: { type: string; name: string }) { async function addComponent(componentTemplate: { type: string; name: string }) {
// 先从配置文件中获取默认属性 // 先加载组件模块
const config = getComponentConfig(componentTemplate.type); const moduleRef = await loadComponentModule(componentTemplate.type);
const defaultProps: Record<string, any> = {}; let defaultProps: Record<string, any> = {};
if (config && config.props) { // 尝试直接调用组件导出的getDefaultProps方法
config.props.forEach(prop => { if(moduleRef){
defaultProps[prop.name] = prop.default; if (typeof moduleRef.getDefaultProps === 'function') {
}); defaultProps = moduleRef.getDefaultProps();
console.log(`Got default props from ${componentTemplate.type}:`, defaultProps);
} else {
// 回退到配置文件
console.log(`No getDefaultProps found for ${componentTemplate.type}`);
}
} else{
console.log(`Failed to load module for ${componentTemplate.type}`);
} }
// 再加载组件模块以便后续使用
await loadComponentModule(componentTemplate.type);
// 发送添加组件事件给父组件 // 发送添加组件事件给父组件
emit('add-component', { emit('add-component', {
@@ -232,9 +339,36 @@ async function addComponent(componentTemplate: { type: string; name: string }) {
closeMenu(); closeMenu();
} }
// 添加模板
async function addTemplate(template: any) {
try {
// 加载模板JSON文件
const response = await fetch(template.path);
if (!response.ok) {
throw new Error(`Failed to load template: ${response.statusText}`);
}
const templateData = await response.json();
console.log('加载模板:', templateData);
// 发出事件,将模板数据传递给父组件
emit('add-template', {
id: template.id,
name: template.name,
template: templateData
});
// 关闭菜单
closeMenu();
} catch (error) {
console.error('加载模板出错:', error);
alert('无法加载模板文件,请检查控制台错误信息');
}
}
// 过滤后的元器件列表 (用于菜单) // 过滤后的元器件列表 (用于菜单)
const filteredComponents = computed(() => { const filteredComponents = computed(() => {
if (!searchQuery.value) { if (!searchQuery.value || activeTab.value !== 'components') {
return availableComponents; return availableComponents;
} }
const query = searchQuery.value.toLowerCase(); const query = searchQuery.value.toLowerCase();
@@ -244,6 +378,30 @@ const filteredComponents = computed(() => {
); );
}); });
// 过滤后的模板列表 (用于菜单)
const filteredTemplates = computed(() => {
if (!searchQuery.value || activeTab.value !== 'templates') {
return availableTemplates.value;
}
const query = searchQuery.value.toLowerCase();
return availableTemplates.value.filter(template =>
template.name.toLowerCase().includes(query) ||
(template.description && template.description.toLowerCase().includes(query))
);
});
// 过滤后的虚拟外设列表 (用于菜单)
const filteredVirtualDevices = computed(() => {
if (!searchQuery.value || activeTab.value !== 'virtual') {
return availableVirtualDevices;
}
const query = searchQuery.value.toLowerCase();
return availableVirtualDevices.filter(device =>
device.name.toLowerCase().includes(query) ||
device.type.toLowerCase().includes(query)
);
});
// 生命周期钩子 // 生命周期钩子
onMounted(() => { onMounted(() => {
// 预加载组件模块 // 预加载组件模块

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,195 @@
<template>
<div class="property-editor">
<div v-if="!componentData" class="text-gray-400">选择元器件以编辑属性</div>
<div v-else>
<div class="mb-4 pb-4 border-b border-base-300">
<h4 class="font-semibold text-lg mb-1">{{ componentData?.type }}</h4>
<p class="text-xs text-base-content opacity-70">ID: {{ componentData?.id }}</p>
<p class="text-xs text-base-content opacity-70">类型: {{ componentData?.type }}</p>
</div>
<!-- 通用属性部分 -->
<CollapsibleSection
title="通用属性"
v-model:isExpanded="generalPropsExpanded"
status="default"
>
<div class="space-y-4">
<div v-for="prop in getGeneralProps()" :key="prop.name" class="form-control">
<label class="label">
<span class="label-text">{{ prop.label || prop.name }}</span>
</label>
<!-- 根据 prop 类型选择输入控件 -->
<input
v-if="prop.type === 'number'"
type="number"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly"
:min="prop.min"
:max="prop.max"
:step="prop.step"
@input="updateDirectProp(componentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
/>
<input
v-else-if="prop.type === 'string'"
type="text"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly"
@input="updateDirectProp(componentData.id, prop.name, ($event.target as HTMLInputElement).value)"
/>
<div v-else-if="prop.type === 'boolean'" class="flex items-center">
<input
type="checkbox"
class="checkbox checkbox-sm mr-2"
:checked="getPropValue(componentData, prop.name)"
:disabled="prop.isReadOnly"
@change="updateDirectProp(componentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
/>
<span>{{ prop.label || prop.name }}</span>
</div>
</div>
</div>
</CollapsibleSection>
<!-- 组件特有属性部分 -->
<CollapsibleSection
title="组件特有属性"
v-model:isExpanded="componentPropsExpanded"
status="default"
class="mt-4"
>
<div v-if="componentConfig && componentConfig.props" class="space-y-4">
<div v-for="prop in getComponentProps()" :key="prop.name" class="form-control">
<label class="label">
<span class="label-text">{{ prop.label || prop.name }}</span>
</label>
<!-- 根据 prop 类型选择输入控件 -->
<input
v-if="prop.type === 'string'"
type="text"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly"
@input="updateProp(componentData.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="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly"
:min="prop.min"
:max="prop.max"
:step="prop.step"
@input="updateProp(componentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
/>
<div v-else-if="prop.type === 'boolean'" class="flex items-center">
<input
type="checkbox"
class="checkbox checkbox-sm mr-2"
:checked="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly"
@change="updateProp(componentData.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="componentData?.attrs?.[prop.name]"
:disabled="prop.isReadOnly"
@change="(event) => {
const selectElement = event.target as HTMLSelectElement;
const value = selectElement.value;
if (componentData) {
updateProp(componentData.id, prop.name, value);
}
}"
>
<option v-for="option in prop.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<p v-else class="text-xs text-warning">不支持的属性类型: {{ prop.type }}</p>
</div>
</div>
<div v-else-if="componentData && !componentConfig" class="text-base-content opacity-70 text-sm">
正在加载组件配置...
</div>
<div v-else-if="componentData && componentConfig && getComponentProps().length === 0" class="text-base-content opacity-70 text-sm">
此组件没有特有属性可配置
</div>
</CollapsibleSection>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CollapsibleSection from './CollapsibleSection.vue';
import { type DiagramPart } from '@/components/diagramManager';
import {
type PropertyConfig,
getPropValue
} from '@/components/equipments/componentConfig';
// 定义属性
const props = defineProps<{
componentData: DiagramPart | null;
componentConfig: { props: PropertyConfig[] } | null;
}>();
// 定义事件
const emit = defineEmits<{
(e: 'updateProp', componentId: string, propName: string, value: any): void;
(e: 'updateDirectProp', componentId: string, propName: string, value: any): void;
}>();
// 控制折叠面板状态
const generalPropsExpanded = ref(true);
const componentPropsExpanded = ref(true);
// 更新组件属性方法
function updateProp(componentId: string, propName: string, value: any) {
emit('updateProp', componentId, propName, value);
}
// 更新组件的直接属性
function updateDirectProp(componentId: string, propName: string, value: any) {
emit('updateDirectProp', componentId, propName, value);
}
// 获取通用属性(直接属性)
function getGeneralProps(): PropertyConfig[] {
return props.componentConfig?.props.filter(p => p.isDirectProp && p.name !== 'pins') || [];
}
// 获取组件特有属性(非直接属性)
function getComponentProps(): PropertyConfig[] {
return props.componentConfig?.props.filter(p => !p.isDirectProp && p.name !== 'pins') || [];
}
</script>
<style scoped>
.property-editor {
width: 100%;
}
/* 添加一些垂直间距 */
.form-control {
margin-bottom: 0.5rem;
}
/* 针对黑暗模式的文本颜色调整 */
:deep(.label-text) {
color: hsl(var(--bc));
opacity: 0.9;
}
</style>

View File

@@ -0,0 +1,210 @@
<template> <div class="property-panel">
<CollapsibleSection
title="基本属性"
v-model:isExpanded="propertySectionExpanded"
status="default"
>
<PropertyEditor
:componentData="componentData"
:componentConfig="componentConfig"
@updateProp="(componentId, propName, value) => $emit('updateProp', componentId, propName, value)"
@updateDirectProp="(componentId, propName, value) => $emit('updateDirectProp', componentId, propName, value)"
/>
</CollapsibleSection>
<!-- 信号发生器DDS特殊属性编辑器 -->
<div v-if="isDDSComponent">
<DDSPropertyEditor
v-model="ddsProperties"
@update:modelValue="updateDDSProperties"
/>
</div>
<!-- 如果选中的组件有pins属性则显示引脚配置区域 -->
<CollapsibleSection
v-if="hasPinsProperty"
title="引脚配置"
v-model:isExpanded="pinsSectionExpanded"
status="default"
>
<div class="space-y-4 p-2">
<!-- 显示现有的pins -->
<div v-for="(pin, index) in componentPins" :key="index" class="pin-item p-2 border rounded-md bg-base-200">
<div class="font-medium mb-2">引脚 #{{ index + 1 }}</div>
<div class="grid grid-cols-2 gap-2">
<div class="form-control">
<label class="label">
<span class="label-text text-xs">ID</span>
</label>
<input
type="text"
v-model="componentPins[index].pinId"
class="input input-bordered input-sm w-full"
placeholder="引脚ID"
@change="updatePins"
/>
</div>
<div class="form-control">
<label class="label">
<span class="label-text text-xs">约束条件</span>
</label>
<input
type="text"
v-model="componentPins[index].constraint"
class="input input-bordered input-sm w-full"
placeholder="约束条件"
@change="updatePins"
/>
</div>
</div>
</div>
</div>
</CollapsibleSection>
<!-- 未来可以在这里添加更多的分区 -->
<!-- 例如
<CollapsibleSection
title="连线管理"
v-model:isExpanded="wireSectionExpanded"
status="default"
>
<div>连线管理内容</div>
</CollapsibleSection>
-->
</div>
</template>
<script setup lang="ts">
import { type DiagramPart } from '@/components/diagramManager';
import { type PropertyConfig } from '@/components/equipments/componentConfig';
import CollapsibleSection from './CollapsibleSection.vue';
import PropertyEditor from './PropertyEditor.vue';
import DDSPropertyEditor from './equipments/DDSPropertyEditor.vue';
import { ref, computed, watch } from 'vue';
// 定义Pin接口
interface Pin {
pinId: string;
constraint: string;
x: number;
y: number;
}
// 定义属性
const props = defineProps<{
componentData: DiagramPart | null;
componentConfig: { props: PropertyConfig[] } | null;
}>();
// 控制各个分区的展开状态
const propertySectionExpanded = ref(true);
const pinsSectionExpanded = ref(false);
const wireSectionExpanded = ref(false);
// DDS特殊属性
const ddsProperties = ref({
frequency: 1000,
phase: 0,
waveform: 'sine',
customWaveformPoints: []
});
// 本地维护一个pins数组副本
const componentPins = ref<Pin[]>([]);
// 监听组件变化更新本地pins数据
watch(() => props.componentData?.attrs?.pins, (newPins) => {
if (newPins) {
componentPins.value = JSON.parse(JSON.stringify(newPins));
} else {
componentPins.value = [];
}
}, { deep: true, immediate: true });
// 监听DDS组件数据变化更新特殊属性
watch(() => props.componentData?.attrs, (newAttrs) => {
if (newAttrs && isDDSComponent.value) {
ddsProperties.value = {
frequency: newAttrs.frequency || 1000,
phase: newAttrs.phase || 0,
waveform: newAttrs.waveform || 'sine',
customWaveformPoints: newAttrs.customWaveformPoints || []
};
}
}, { deep: true, immediate: true });
// 计算属性检查组件是否有pins属性
const hasPinsProperty = computed(() => {
if (!props.componentData || !props.componentData.attrs) {
return false;
}
// 检查配置中是否有pins属性
if (props.componentConfig && props.componentConfig.props) {
return props.componentConfig.props.some(prop => prop.name === 'pins' && prop.isArrayType);
}
// 或直接检查attrs中是否有pins属性
return 'pins' in props.componentData.attrs;
});
// 计算属性检查组件是否为DDS组件
const isDDSComponent = computed(() => {
return props.componentData?.type === 'DDS';
});
// 定义事件
const emit = defineEmits<{
(e: 'updateProp', componentId: string, propName: string, value: any): void;
(e: 'updateDirectProp', componentId: string, propName: string, value: any): void;
}>();
// 更新pins属性
function updatePins() {
if (props.componentData && props.componentData.id) {
emit('updateProp', props.componentData.id, 'pins', componentPins.value);
}
}
// 监听DDS组件数据变化更新特殊属性
watch(() => props.componentData?.attrs, (newAttrs) => {
if (newAttrs && isDDSComponent.value) {
ddsProperties.value = {
frequency: newAttrs.frequency || 1000,
phase: newAttrs.phase || 0,
waveform: newAttrs.waveform || 'sine',
customWaveformPoints: newAttrs.customWaveformPoints || []
};
}
}, { deep: true, immediate: true });
// 更新DDS属性
function updateDDSProperties(newProperties: any) {
ddsProperties.value = newProperties;
if (props.componentData && props.componentData.id) {
// 将各个属性单独更新,而不是作为一个整体
emit('updateProp', props.componentData.id, 'frequency', newProperties.frequency);
emit('updateProp', props.componentData.id, 'phase', newProperties.phase);
emit('updateProp', props.componentData.id, 'waveform', newProperties.waveform);
emit('updateProp', props.componentData.id, 'customWaveformPoints', newProperties.customWaveformPoints);
}
}
</script>
<style scoped>
.property-panel {
width: 100%;
height: 100%;
overflow-y: auto;
}
.pin-item {
transition: all 0.2s ease;
}
.pin-item:hover {
box-shadow: 0 0 5px rgba(0, 0, 0, 0.1);
}
</style>

View File

@@ -0,0 +1,8 @@
{
"version": 1,
"author": "admin",
"editor": "me",
"parts": [
], "connections": [
]
}

View File

@@ -0,0 +1,331 @@
// 定义 diagram.json 的类型结构
export interface DiagramData {
version: number;
author: string;
editor: string;
parts: DiagramPart[];
connections: ConnectionArray[];
exportTime?: string; // 导出时的时间戳
}
// 组件部分的类型定义
export interface DiagramPart {
id: string;
type: string;
x: number;
y: number;
attrs: Record<string, any>;
rotate: number;
group: string;
positionlock: boolean;
hidepins: boolean;
isOn: boolean;
index?: number; // 显示层级,数值越大显示越靠前
}
// 连接类型定义 - 使用元组类型表示四元素数组
export type ConnectionArray = [string, string, number, string[]];
// 解析连接字符串为组件ID和引脚ID
export function parseConnectionPin(connectionPin: string): { componentId: string; pinId: string } {
const [componentId, pinId] = connectionPin.split(':');
return { componentId, pinId };
}
// 将连接数组转换为适用于渲染的格式
export function connectionArrayToWireItem(
connection: ConnectionArray,
index: number,
startPos = { x: 0, y: 0 },
endPos = { x: 0, y: 0 }
): WireItem {
const [startPinStr, endPinStr, width, path] = connection;
const { componentId: startComponentId, pinId: startPinId } = parseConnectionPin(startPinStr);
const { componentId: endComponentId, pinId: endPinId } = parseConnectionPin(endPinStr);
return {
id: `wire-${index}`,
startX: startPos.x,
startY: startPos.y,
endX: endPos.x,
endY: endPos.y,
startComponentId,
startPinId,
endComponentId,
endPinId,
strokeWidth: width,
color: '#4a5568', // 默认颜色
routingMode: 'path',
pathCommands: path,
showLabel: false
};
}
// WireItem 接口定义
export interface WireItem {
id: string;
startX: number;
startY: number;
endX: number;
endY: number;
startComponentId: string;
startPinId?: string;
endComponentId: string;
endPinId?: string;
strokeWidth: number;
color: string;
routingMode: 'orthogonal' | 'path';
constraint?: string;
pathCommands?: string[];
showLabel: boolean;
}
// 从本地存储加载图表数据
export async function loadDiagramData(): Promise<DiagramData> {
try {
// 先尝试从本地存储加载
const savedData = localStorage.getItem('diagramData');
if (savedData) {
return JSON.parse(savedData);
}
// 如果本地存储没有,从文件加载
const response = await fetch('/src/components/diagram.json');
if (!response.ok) {
throw new Error(`Failed to load diagram.json: ${response.statusText}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Error loading diagram data:', error);
// 返回空的默认数据结构
return createEmptyDiagram();
}
}
// 创建空的图表数据
export function createEmptyDiagram(): DiagramData {
return {
version: 1,
author: 'user',
editor: 'user',
parts: [],
connections: []
};
}
// 保存图表数据到本地存储
export function saveDiagramData(data: DiagramData): void {
try {
localStorage.setItem('diagramData', JSON.stringify(data));
} catch (error) {
console.error('Error saving diagram data:', error);
}
}
// 添加新组件到图表数据
export function addPart(data: DiagramData, part: DiagramPart): DiagramData {
return {
...data,
parts: [...data.parts, part]
};
}
// 更新组件位置
export function updatePartPosition(
data: DiagramData,
partId: string,
x: number,
y: number
): DiagramData {
return {
...data,
parts: data.parts.map(part =>
part.id === partId
? { ...part, x, y }
: part
)
};
}
// 更新组件属性
export function updatePartAttribute(
data: DiagramData,
partId: string,
attrName: string,
value: any
): DiagramData {
return {
...data,
parts: data.parts.map(part =>
part.id === partId
? {
...part,
attrs: {
...part.attrs,
[attrName]: value
}
}
: part
)
};
}
// 删除组件及同组组件
export function deletePart(data: DiagramData, partId: string): DiagramData {
// 首先找到要删除的组件
const component = data.parts.find(part => part.id === partId);
if (!component) return data;
// 收集需要删除的组件ID列表
const componentsToDelete: string[] = [partId];
// 如果组件属于一个组,则找出所有同组的组件
if (component.group && component.group !== '') {
const groupMembers = data.parts.filter(
p => p.group === component.group && p.id !== partId
);
// 将同组组件ID添加到删除列表
componentsToDelete.push(...groupMembers.map(p => p.id));
console.log(`删除组件 ${partId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
}
return {
...data,
// 删除所有标记的组件
parts: data.parts.filter(part => !componentsToDelete.includes(part.id)),
// 删除与这些组件相关的所有连接
connections: data.connections.filter(conn => {
const [startPin, endPin] = conn;
const startCompId = startPin.split(':')[0];
const endCompId = endPin.split(':')[0];
// 检查连接两端的组件是否在删除列表中
return !componentsToDelete.includes(startCompId) && !componentsToDelete.includes(endCompId);
})
};
}
// 添加连接
export function addConnection(
data: DiagramData,
startComponentId: string,
startPinId: string,
endComponentId: string,
endPinId: string,
width: number = 2,
path: string[] = []
): DiagramData {
const newConnection: ConnectionArray = [
`${startComponentId}:${startPinId}`,
`${endComponentId}:${endPinId}`,
width,
path
];
return {
...data,
connections: [...data.connections, newConnection]
};
}
// 删除连接
export function deleteConnection(
data: DiagramData,
connectionIndex: number
): DiagramData {
return {
...data,
connections: data.connections.filter((_, index) => index !== connectionIndex)
};
}
// 查找与组件关联的所有连接
export function findConnectionsByPart(
data: DiagramData,
partId: string
): { connection: ConnectionArray; index: number }[] {
return data.connections
.map((connection, index) => ({ connection, index }))
.filter(({ connection }) => {
const [startPin, endPin] = connection;
const startCompId = startPin.split(':')[0];
const endCompId = endPin.split(':')[0];
return startCompId === partId || endCompId === partId;
});
}
// 基于组的移动相关组件
export function moveGroupComponents(
data: DiagramData,
groupId: string,
deltaX: number,
deltaY: number
): DiagramData {
if (!groupId) return data;
return {
...data,
parts: data.parts.map(part =>
part.group === groupId
? { ...part, x: part.x + deltaX, y: part.y + deltaY }
: part
)
};
}
// 添加验证diagram.json文件的函数
export function validateDiagramData(data: any): { isValid: boolean; errors: string[] } {
const errors: string[] = [];
// 检查版本号
if (!data.version) {
errors.push('缺少version字段');
}
// 检查parts数组
if (!Array.isArray(data.parts)) {
errors.push('parts字段不是数组');
} else {
// 验证parts中的每个对象
data.parts.forEach((part: any, index: number) => {
if (!part.id) errors.push(`parts[${index}]缺少id`);
if (!part.type) errors.push(`parts[${index}]缺少type`);
if (typeof part.x !== 'number') errors.push(`parts[${index}]缺少有效的x坐标`);
if (typeof part.y !== 'number') errors.push(`parts[${index}]缺少有效的y坐标`);
});
}
// 检查connections数组
if (!Array.isArray(data.connections)) {
errors.push('connections字段不是数组');
} else {
// 验证connections中的每个数组
data.connections.forEach((conn: any, index: number) => {
if (!Array.isArray(conn) || conn.length < 3) {
errors.push(`connections[${index}]不是有效的连接数组`);
return;
}
const [startPin, endPin, width] = conn;
if (typeof startPin !== 'string' || !startPin.includes(':')) {
errors.push(`connections[${index}]的起始针脚格式无效`);
}
if (typeof endPin !== 'string' || !endPin.includes(':')) {
errors.push(`connections[${index}]的结束针脚格式无效`);
}
if (typeof width !== 'number') {
errors.push(`connections[${index}]的宽度不是有效的数字`);
}
});
}
return {
isValid: errors.length === 0,
errors
};
}

View File

@@ -0,0 +1,339 @@
<template>
<div class="dds-component" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
<svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 300 200"
class="dds-device"
> <!-- 信号发生器外壳扩大屏幕部分 -->
<rect width="300" height="180" rx="10" ry="10" fill="#2a323c" stroke="#444" stroke-width="2" />
<!-- 信号发生器显示屏扩大屏幕 -->
<rect x="20" y="20" width="260" height="140" rx="5" ry="5" fill="#1a1f25" stroke="#555" stroke-width="1" />
<!-- 波形显示 -->
<path :d="currentWaveformPath" stroke="lime" stroke-width="2" fill="none" />
<!-- 信息显示区域 -->
<text x="30" y="40" fill="#0f0" font-size="14">{{ displayFrequency }}</text>
<text x="200" y="40" fill="#0f0" font-size="14">φ: {{ phase }}°</text>
</svg>
<!-- 输出引脚 -->
<div
v-for="pin in pins"
:key="pin.pinId"
:style="{
position: 'absolute',
left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)'
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`"
>
<Pin
:ref="el => { if(el) pinRefs[pin.pinId] = el }"
:label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
@pin-click="$emit('pin-click', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue';
import Pin from './Pin.vue';
// 存储Pin引用
const pinRefs = ref<Record<string, any>>({});
// DDS属性
interface DDSProps {
size?: number;
pins?: {
pinId: string;
constraint: string;
x: number;
y: number;
}[];
frequency?: number;
phase?: number;
timebase?: number; // 添加时基属性
waveform?: string;
savedWaveforms?: string[];
customWaveformPoints?: number[][];
}
const props = withDefaults(defineProps<DDSProps>(), {
size: 1,
pins: () => [
{ pinId: 'OUT', constraint: '', x: 300, y: 90 }, // 调整输出引脚位置
],
frequency: 1000,
phase: 0,
timebase: 1, // 默认时基为1
waveform: 'sine',
savedWaveforms: () => ['sine', 'square', 'triangle', 'sawtooth'],
customWaveformPoints: () => []
});
// 组件尺寸
const width = computed(() => 300 * props.size);
const height = computed(() => 180 * props.size); // 减小整体高度
// 波形状态
const frequency = ref(props.frequency);
const phase = ref(props.phase);
const timebase = ref(props.timebase || 1); // 添加时基参数默认为1
const currentWaveformIndex = ref(0);
const waveformNames = ['正弦波', '方波', '三角波', '锯齿波'];
const waveforms = ['sine', 'square', 'triangle', 'sawtooth'];
// 波形函数集合
interface WaveformFunction {
(x: number, width: number, height: number, phaseRad: number): number;
}
interface WaveformFunctions {
[key: string]: WaveformFunction;
}
const waveformFunctions: WaveformFunctions = {
// 正弦波函数: sin(2π*x + φ)
sine: (x: number, width: number, height: number, phaseRad: number): number => {
return height/2 * Math.sin(2 * Math.PI * (x / width) * 2 + phaseRad);
},
// 方波函数: 周期性的高低电平
square: (x: number, width: number, height: number, phaseRad: number): number => {
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
return normX < 0.5 ? height/4 : -height/4;
},
// 三角波函数: 线性上升和下降
triangle: (x: number, width: number, height: number, phaseRad: number): number => {
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
return height/2 - height * Math.abs(2 * normX - 1);
},
// 锯齿波函数: 线性上升,瞬间下降
sawtooth: (x: number, width: number, height: number, phaseRad: number): number => {
const normX = (x / width + phaseRad / (2 * Math.PI)) % 1;
return height/2 - height/2 * (2 * normX);
}
};
// 计算当前显示频率
const displayFrequency = computed(() => {
if (frequency.value >= 1000000) {
return `${(frequency.value / 1000000).toFixed(2)} MHz`;
} else if (frequency.value >= 1000) {
return `${(frequency.value / 1000).toFixed(2)} kHz`;
} else {
return `${frequency.value.toFixed(2)} Hz`;
}
});
// 格式化时基显示
function formatTimebase(tb: number): string {
if (tb < 0.1) {
return `${(tb * 1000).toFixed(0)} ms/div`;
} else if (tb < 1) {
return `${(tb * 1000).toFixed(0)} ms/div`;
} else {
return `${tb.toFixed(1)} s/div`;
}
}
// 计算当前显示时基
const displayTimebase = computed(() => formatTimebase(timebase.value));
// 生成波形路径
const currentWaveformPath = computed(() => {
const width = 240;
const height = 100; // 更大的波形显示高度,因为我们增加了屏幕高度
const xOffset = 30;
const yOffset = 50; // 上移位置以适应新布局
const currentWaveform = waveforms[currentWaveformIndex.value];
const phaseRadians = phase.value * Math.PI / 180;
// 时基和频率共同影响周期数量
// 频率因素 - 频率越高,一个屏幕内显示的周期越多
// 使用对数缩放可以更好地表示广泛范围的频率变化
const freqLog = Math.log10(frequency.value) - 2; // 从100Hz开始作为基准
const frequencyFactor = Math.max(0.1, Math.min(10, freqLog)); // 限制在合理范围内
// 时基影响周期数量 - 时基越小,显示的周期越多
const timebaseFactor = 1 / timebase.value;
// 组合因素
const scaleFactor = timebaseFactor * frequencyFactor;
let path = `M${xOffset},${yOffset + height/2}`;
// 使用波形函数生成路径
const waveFunction = waveformFunctions[currentWaveform];
// 生成路径点
for (let x = 0; x <= width; x++) {
// 应用组合缩放因素 - 影响x轴的缩放
const scaledX = x * scaleFactor;
const y = waveFunction(scaledX, width, height, phaseRadians);
path += ` L${x + xOffset},${yOffset + height/2 - y}`;
}
return path;
});
// 波形操作函数
function selectWaveform(index: number) {
currentWaveformIndex.value = index;
}
function increaseFrequency() {
if (frequency.value < 10) {
frequency.value += 0.1;
} else if (frequency.value < 100) {
frequency.value += 1;
} else if (frequency.value < 1000) {
frequency.value += 10;
} else if (frequency.value < 10000) {
frequency.value += 100;
} else if (frequency.value < 100000) {
frequency.value += 1000;
} else {
frequency.value += 10000;
}
frequency.value = Math.min(frequency.value, 10000000); // 最大10MHz
}
function decreaseFrequency() {
if (frequency.value <= 10) {
frequency.value -= 0.1;
} else if (frequency.value <= 100) {
frequency.value -= 1;
} else if (frequency.value <= 1000) {
frequency.value -= 10;
} else if (frequency.value <= 10000) {
frequency.value -= 100;
} else if (frequency.value <= 100000) {
frequency.value -= 1000;
} else {
frequency.value -= 10000;
}
frequency.value = Math.max(frequency.value, 0.1); // 最小0.1Hz
frequency.value = parseFloat(frequency.value.toFixed(1)); // 修复浮点数精度问题
}
function increasePhase() {
phase.value += 15;
if (phase.value >= 360) {
phase.value -= 360;
}
}
function decreasePhase() {
phase.value -= 15;
if (phase.value < 0) {
phase.value += 360;
}
}
// 监听props变化
watch(
() => props.frequency,
(newValue) => {
if (newValue !== undefined && newValue !== frequency.value) {
frequency.value = newValue;
}
}
);
watch(
() => props.phase,
(newValue) => {
if (newValue !== undefined && newValue !== phase.value) {
phase.value = newValue;
}
}
);
watch(
() => props.timebase,
(newValue) => {
if (newValue !== undefined && newValue !== timebase.value) {
timebase.value = newValue;
}
}
);
watch(
() => props.waveform,
(newValue) => {
if (newValue !== undefined) {
const index = waveforms.indexOf(newValue);
if (index !== -1) {
currentWaveformIndex.value = index;
}
}
}
);
onMounted(() => {
// 初始化波形类型
if (props.waveform) {
const index = waveforms.indexOf(props.waveform);
if (index !== -1) {
currentWaveformIndex.value = index;
}
}
// 初始化时基
if (props.timebase !== undefined) {
timebase.value = props.timebase;
}
});
// 暴露属性和方法
defineExpose({
frequency,
phase,
timebase,
currentWaveformIndex,
selectWaveform,
increaseFrequency,
decreaseFrequency,
increasePhase,
decreasePhase
});
</script>
<style scoped>
.dds-component {
display: inline-block;
position: relative;
}
</style>
<!-- 导出默认属性函数供外部使用 -->
<script lang="ts">
export function getDefaultProps() {
return {
size: 1,
pins: [
{ pinId: 'OUT', constraint: '', x: 300, y: 90 }, // 调整输出引脚位置
],
frequency: 1000,
phase: 0,
timebase: 1, // 添加默认时基
waveform: 'sine',
savedWaveforms: ['sine', 'square', 'triangle', 'sawtooth'],
customWaveformPoints: []
};
}
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -26,9 +26,7 @@
<stop stop-color="#171717" offset="0" /> <stop stop-color="#171717" offset="0" />
<stop stop-color="#4b4b4b" offset="1" /> <stop stop-color="#4b4b4b" offset="1" />
</linearGradient> </linearGradient>
</defs> </defs> <!-- 按钮底座 -->
<!-- 按钮底座 -->
<rect width="800" height="800" x="400" y="400" fill="#464646" rx="20" /> <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" /> <rect width="700" height="700" x="450" y="450" fill="#eaeaea" rx="20" />
@@ -51,9 +49,9 @@
@mouseleave="toggleButtonState(false)" @mouseleave="toggleButtonState(false)"
style="pointer-events: auto; transition: all 20ms ease-in-out; cursor: pointer;" style="pointer-events: auto; transition: all 20ms ease-in-out; cursor: pointer;"
/> />
<!-- 按键文字 --> <!-- 按键文字 - 仅显示绑定的按键 -->
<text <text
v-if="displayText" v-if="bindKeyDisplay"
x="800" x="800"
y="800" y="800"
font-size="310" font-size="310"
@@ -62,25 +60,30 @@
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;"
> >
{{ displayText }} {{ bindKeyDisplay }}
</text> </text>
</svg> </svg>
<!-- 嵌入Pin组件覆盖在按钮上 --> <!-- 渲染自定义引脚数组 -->
<div class="pin-wrapper" :style="{ <div v-for="pin in props.pins" :key="pin.pinId"
:style="{
position: 'absolute', position: 'absolute',
top: '80%', left: `${pin.x * props.size}px`,
left: '50%', top: `${pin.y * props.size}px`,
transform: 'translate(-50%, 20%)', transform: 'translate(-50%, -50%)',
zIndex: 3, zIndex: 3,
pointerEvents: 'auto' pointerEvents: 'auto'
}"> }"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`">
<Pin <Pin
ref="pinRef" :ref="el => { if(el) pinRefs[pin.pinId] = el }"
direction="output" direction="output"
type="digital" type="digital"
:label="props.label" :label="pin.pinId"
:constraint="props.constraint" :constraint="pin.constraint"
:pinId="pin.pinId"
:size="0.8" :size="0.8"
:componentId="props.componentId" :componentId="props.componentId"
@value-change="handlePinValueChange" @value-change="handlePinValueChange"
@@ -95,54 +98,46 @@ import { ref, onMounted, onUnmounted, computed } from 'vue';
import Pin from './Pin.vue'; import Pin from './Pin.vue';
import { notifyConstraintChange } from '../../stores/constraints'; import { notifyConstraintChange } from '../../stores/constraints';
const pinRef = ref<any>(null); // 存储多个Pin引用
const pinRefs = ref<Record<string, any>>({});
// 从Pin组件继承属性
interface PinProps {
label?: string;
constraint?: string;
componentId?: string; // 添加componentId属性
// 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
}
// 按钮特有属性 // 按钮特有属性
interface ButtonProps { interface ButtonProps {
size?: number; size?: number;
bindKey?: string; bindKey?: string;
buttonText?: string; componentId?: string;
pins?: {
pinId: string;
constraint: string;
x: number;
y: number;
}[];
} }
// 组合两个接口 const props = withDefaults(defineProps<ButtonProps>(), {
interface Props extends PinProps, ButtonProps {}
const props = withDefaults(defineProps<Props>(), {
size: 1, size: 1,
bindKey: '', bindKey: '',
buttonText: '', componentId: 'button-default',
label: 'BTN', pins: () => [
{
pinId: 'BTN',
constraint: '', constraint: '',
componentId: 'button-default', // 添加默认componentId x: 80,
// 这些值会被覆盖,但需要默认值以满足类型要求 y: 140
direction: 'output', }
type: 'digital' ]
}); });
// 计算实际宽高 // 计算实际宽高
const width = computed(() => 160 * props.size); const width = computed(() => 160 * props.size);
const height = computed(() => 160 * props.size); const height = computed(() => 160 * props.size);
// 计算文本显示内容 // 显示绑定的按键
const displayText = computed(() => { const bindKeyDisplay = computed(() => props.bindKey ? props.bindKey.toUpperCase() : '');
if (props.buttonText) return props.buttonText;
return props.bindKey ? props.bindKey.toUpperCase() : '';
});
// 定义组件发出的事件 // 定义组件发出的事件
const emit = defineEmits([ const emit = defineEmits([
'update:bindKey', 'update:bindKey',
'update:label',
'update:constraint', 'update:constraint',
'press', 'press',
'release', 'release',
@@ -175,15 +170,24 @@ function toggleButtonState(isPressed: boolean) {
if (isPressed) { if (isPressed) {
emit('press'); emit('press');
// 如果有约束,通知约束状态变化为高电平 // 如果有约束,通知约束状态变化为高电平
if (props.constraint) { // 对所有引脚应用相同的状态
notifyConstraintChange(props.constraint, 'high'); if (props.pins) {
props.pins.forEach(pin => {
if (pin.constraint) {
notifyConstraintChange(pin.constraint, 'high');
}
});
} }
} else { } else {
emit('release'); emit('release');
emit('click'); emit('click');
// 如果有约束,通知约束状态变化为低电平 // 如果有约束,通知约束状态变化为低电平
if (props.constraint) { if (props.pins) {
notifyConstraintChange(props.constraint, 'low'); props.pins.forEach(pin => {
if (pin.constraint) {
notifyConstraintChange(pin.constraint, 'low');
}
});
} }
} }
} }
@@ -211,25 +215,62 @@ defineExpose({
getInfo: () => ({ getInfo: () => ({
// 按钮特有属性 // 按钮特有属性
bindKey: props.bindKey, bindKey: props.bindKey,
buttonText: props.buttonText, componentId: props.componentId,
// 继承自Pin的属性 pins: props.pins
label: props.label,
constraint: props.constraint,
componentId: props.componentId, // 添加componentId
// 固定的Pin属性
direction: 'output',
type: 'digital'
}), }),
// 代理 getPinPosition 到内部 Pin // 获取引脚位置
getPinPosition: (pinLabel: string) => { getPinPosition: (pinId: string) => {
if (pinRef.value && pinRef.value.getPinPosition) { console.log(`[MechanicalButton] 调用getPinPosition寻找pinId: ${pinId}`);
return pinRef.value.getPinPosition(pinLabel); console.log(`[MechanicalButton] 组件ID: ${props.componentId}, 当前尺寸: ${props.size}, 组件宽高: ${width.value}x${height.value}`);
console.log(`[MechanicalButton] 当前存在的pins:`, props.pins);
// 如果是自定义的引脚ID
if (props.pins && props.pins.length > 0) {
const customPin = props.pins.find(p => p.pinId === pinId);
if (customPin) {
console.log(`[MechanicalButton] 找到自定义引脚: ${pinId},配置位置:`, { x: customPin.x, y: customPin.y });
// 考虑组件尺寸的缩放
// 这里的x和y是针对标准尺寸size=1的坐标需要根据实际size调整
const scaledX = customPin.x * props.size;
const scaledY = customPin.y * props.size;
console.log(`[MechanicalButton] 返回缩放后的坐标:`, { x: scaledX, y: scaledY });
return {
x: scaledX,
y: scaledY
};
} else {
console.log(`[MechanicalButton] 未找到pinId: ${pinId}的引脚配置`);
} }
} else {
console.log(`[MechanicalButton] 没有配置任何引脚`);
}
console.log(`[MechanicalButton] 返回null未找到引脚`);
return null; return null;
} }
}); });
</script> </script>
<script lang="ts">
// 添加一个静态方法来获取默认props
export function getDefaultProps() {
return {
size: 1,
bindKey: '',
pins: [
{
pinId: 'BTN',
constraint: '',
x: 80,
y: 140
}
]
};
}
</script>
<style scoped> <style scoped>
.button-container { .button-container {
display: flex; display: flex;

View File

@@ -1,181 +1,31 @@
<template> <div class="motherboard-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <!-- 主板 SVG --> <template>
<img <div class="motherboard-container" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }">
src="../equipments/svg/motherboard.svg" <svg
xmlns="http://www.w3.org/2000/svg"
:width="width" :width="width"
:height="height" :height="height"
alt="主板" :viewBox="`0 0 800 600`"
class="svg-image" class="motherboard-svg"
draggable="false" >
<image
href="../equipments/svg/motherboard.svg"
width="100%"
height="100%"
preserveAspectRatio="xMidYMid meet"
/> />
</svg>
<!-- 嵌入各种组件 --> <!-- 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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; 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 { // 主板特有属性
interface MotherBoardProps {
size?: number; size?: number;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<MotherBoardProps>(), {
size: 1 size: 1
}); });
@@ -186,11 +36,23 @@ const height = computed(() => 600 * props.size);
// 向外暴露方法 // 向外暴露方法
defineExpose({ defineExpose({
getInfo: () => ({ getInfo: () => ({
size: props.size size: props.size,
}) type: 'motherboard'
}),
// 主板没有引脚但为了接口一致性提供一个空的getPinPosition方法
getPinPosition: () => null
}); });
</script> </script>
<script lang="ts">
// 添加一个静态方法来获取默认props
export function getDefaultProps() {
return {
size: 1
};
}
</script>
<style scoped> <style scoped>
.motherboard-container { .motherboard-container {
display: block; display: block;
@@ -201,20 +63,9 @@ defineExpose({
-ms-user-select: none; /* IE/Edge */ -ms-user-select: none; /* IE/Edge */
} }
.svg-image { .motherboard-svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain;
pointer-events: none; /* 禁止鼠标交互 */ 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> </style>

View File

@@ -0,0 +1,181 @@
<template>
<div class="chip-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 400 400"
class="fbg676-chip"
> <!-- 芯片外边框 - 用多段path代替rect这样可以实现缺角 -->
<path
d="M30,0 H390 Q400,0 400,10 V390 Q400,400 390,400 H10 Q0,400 0,390 V30 L30,0 Z"
fill="#1C4E2D"
/>
<!-- 芯片内层 - 增大尺寸 -->
<rect width="280" height="280" x="60" y="60" fill="#0F1211" rx="2" ry="2" />
</svg>
<!-- 渲染芯片引脚 --> <div v-for="pin in computedPins" :key="pin.pinId"
:style="{
position: 'absolute',
left: `${(pin.x || 0) * props.size * 0.37}px`,
top: `${(pin.y || 0) * props.size * 0.37}px`,
transform: 'translate(-50%, -50%)'
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${(pin.x || 0) * props.size * 0.37}`"
:data-pin-y="`${(pin.y || 0) * props.size * 0.37}`"> <Pin
:ref="el => { if(el) pinRefs[pin.pinId] = el }"
:label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
:size="0.35"
@pin-click="$emit('pin-click', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import Pin from './Pin.vue';
// 存储多个Pin引用
const pinRefs = ref<Record<string, any>>({});
// 芯片特有属性
interface ChipProps {
size?: number;
pins?: {
pinId: string;
constraint: string;
x?: number; // x坐标现在是可选的
y?: number; // y坐标现在是可选的
}[];
}
const props = withDefaults(defineProps<ChipProps>(), {
size: 1,
pins: () => []
});
// 计算尺寸
const width = computed(() => 400 * props.size * 0.37);
const height = computed(() => 400 * props.size * 0.37);
// 生成676个引脚26x26
const computedPins = computed(() => {
// 设置BGA矩阵布局参数
const pinRows = 26;
const pinColumns = 26;
const margin = 25; // 四周边距
const xStart = margin;
const yStart = margin;
const xEnd = 400 - margin;
const yEnd = 400 - margin;
const xStep = (xEnd - xStart) / (pinColumns - 1);
const yStep = (yEnd - yStart) / (pinRows - 1);
// 如果提供了pins则合并引脚信息和位置信息
if (props.pins && props.pins.length > 0) {
// 复制一份提供的pins
const mergedPins = [...props.pins];
// 对于不包含xy坐标的引脚生成默认的xy坐标
for (let i = 0; i < mergedPins.length; i++) {
const pin = mergedPins[i];
// 如果没有提供xy坐标根据引脚索引计算位置
if (pin.x === undefined || pin.y === undefined) {
const row = Math.floor(i / pinColumns);
const col = i % pinColumns;
pin.x = xStart + col * xStep;
pin.y = yStart + row * yStep;
}
}
return mergedPins;
}
// 否则生成默认的676个引脚按BGA格式矩阵排列
const pins = [];
let pinIndex = 0;
// 生成BGA封装的矩阵引脚
for (let row = 0; row < pinRows; row++) {
for (let col = 0; col < pinColumns; col++) {
pins.push({
pinId: `pin_${pinIndex++}`,
constraint: '',
x: xStart + col * xStep,
y: yStart + row * yStep
});
}
}
return pins;
});
defineExpose({
getInfo: () => ({
chipType: 'PG2L100H_FBG676',
direction: 'inout',
type: 'digital',
pins: computedPins.value
}),
getPinPosition: (pinId: string) => {
// 返回指定引脚ID的位置
if (computedPins.value && computedPins.value.length > 0) {
const customPin = computedPins.value.find(p => p.pinId === pinId);
if (customPin && customPin.x !== undefined && customPin.y !== undefined) {
// 考虑组件尺寸的缩放应用0.37的系数确保居中对齐
const scaledX = customPin.x * props.size * 0.37;
const scaledY = customPin.y * props.size * 0.37;
return {
x: scaledX,
y: scaledY
};
}
return null;
}
return null;
}
});
</script>
<script lang="ts">
// 添加一个静态方法来获取默认props
export function getDefaultProps() {
return {
size: 1,
pins: [] // 默认不提供引脚由computedPins计算生成
};
}
</script>
<style scoped>
.chip-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;
}
.fbg676-chip {
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;
}
</style>

View File

@@ -4,20 +4,28 @@
:height="height" :height="height"
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight" :viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
class="pin-component" class="pin-component"
:data-component-id="props.componentId" > <g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
:data-pin-label="props.label"
>
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
<g> <g>
<g transform="translate(-12.5, -12.5)"> <g transform="translate(-12.5, -12.5)"> <!-- 添加一个透明的更大区域来增强点击能力但比原来小一点 -->
<circle
cx="12.5"
cy="12.5"
r="5"
fill="transparent"
class="interactive"
@click.stop="handlePinClick"
@mousedown.stop
@touchstart.stop
:data-pin-element="`${props.pinId}`"
:data-component-id="props.componentId" />
<!-- 实际可见的引脚圆点 -->
<circle <circle
:style="{ fill: pinColor }" :style="{ fill: pinColor }"
cx="12.5" cx="12.5"
cy="12.5" cy="12.5"
r="3.75" r="3.75"
class="interactive" pointer-events="none"
@click.stop="handlePinClick" :data-pin-visual="`${props.pinId}`" />
:data-pin-element="`${props.componentId}`" />
</g> </g>
</g> </g>
</g> </g>
@@ -28,16 +36,14 @@
import { ref, computed, reactive, watch, onMounted, onUnmounted } from 'vue'; import { ref, computed, reactive, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintColor, getConstraintState, onConstraintStateChange, notifyConstraintChange } from '../../stores/constraints'; import { getConstraintColor, getConstraintState, onConstraintStateChange, notifyConstraintChange } from '../../stores/constraints';
// 生成唯一ID
const uniqueId = `pin-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
interface Props { interface Props {
size?: number; size?: number;
label?: string; label?: string;
constraint?: string; constraint?: string;
direction?: 'input' | 'output' | 'inout'; direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog'; type?: 'digital' | 'analog';
componentId?: string; // 添加组件ID属性用于唯一标识 pinId?: string; // 添加引脚ID属性用于唯一标识
componentId?: string; // 添加组件ID属性关联到父组件
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@@ -46,7 +52,8 @@ const props = withDefaults(defineProps<Props>(), {
constraint: '', constraint: '',
direction: 'input', direction: 'input',
type: 'digital', type: 'digital',
componentId: 'pin-default' // 默认ID pinId: 'pin-default', // 默认ID
componentId: '' // 默认空字符串
}); });
const emit = defineEmits([ const emit = defineEmits([
@@ -76,10 +83,10 @@ function handlePinClick(event: MouseEvent) {
}); });
} }
const width = computed(() => 40 * props.size); const width = computed(() => 15 * props.size);
const height = computed(() => 20 * props.size); const height = computed(() => 15 * props.size);
const viewBoxWidth = computed(() => 40); const viewBoxWidth = computed(() => 15);
const viewBoxHeight = computed(() => 20); const viewBoxHeight = computed(() => 15);
const getColorByType = computed(() => { const getColorByType = computed(() => {
return props.type === 'analog' ? '#2a6099' : '#444'; return props.type === 'analog' ? '#2a6099' : '#444';
@@ -142,54 +149,59 @@ defineExpose({
constraint: props.constraint, constraint: props.constraint,
direction: props.direction, direction: props.direction,
type: props.type, type: props.type,
componentId: props.componentId pinId: props.pinId
}), }),
getPinPosition: (componentId: string) => { getPinPosition: () => {
if (componentId !== props.componentId) return null; // Pin组件自身的getPinPosition应该返回相对于父组件的位置
console.log('getPinPosition', componentId, props.componentId); // 在MechanicalButton等组件中已经明确传递了position所以这里实际上可能不会被使用
const uniqueSelector = `[data-pin-element="${props.componentId}"]`;
console.log('uniqueSelector', uniqueSelector);
const pinElements = document.querySelectorAll(uniqueSelector);
console.log('pinElements', pinElements);
if (pinElements.length === 0) return null;
if (pinElements.length === 1) {
const rect = pinElements[0].getBoundingClientRect();
return { return {
x: rect.left + rect.width / 2, x: viewBoxWidth.value / 2,
y: rect.top + rect.height / 2 y: viewBoxHeight.value / 2
};
}
for (const pinElement of pinElements) {
let parentSvg = pinElement.closest('svg.pin-component');
if (!parentSvg) continue;
if (parentSvg.getAttribute('data-component-id') === props.componentId) {
const rect = pinElement.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
};
}
}
const rect = pinElements[0].getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 2
}; };
} }
}); });
</script> </script>
<script lang="ts">
// 添加一个静态方法来获取默认props
export function getDefaultProps() {
return {
size: 1,
label: 'PIN',
constraint: '',
direction: 'input',
type: 'digital',
pinId: 'pin-default',
componentId: '',
pins: [
{
pinId: 'PIN',
constraint: '',
x: 0,
y: 0
}
]
};
}
</script>
<style scoped> <style scoped>
.pin-component { .pin-component {
display: block; display: block;
user-select: none; user-select: none;
position: relative; position: relative;
z-index: 5; /* 提高引脚组件的z-index */
pointer-events: auto; /* 确保可以接收点击事件 */
overflow: visible; /* 确保可以看到引脚 */
} }
.interactive { .interactive {
cursor: pointer; cursor: pointer;
transition: filter 0.2s; transition: filter 0.2s;
pointer-events: auto; /* 确保可以接收点击事件 */
} }
.interactive:hover { .interactive:hover {
filter: brightness(1.2); filter: brightness(1.2);
stroke: rgba(255, 255, 255, 0.3); /* 添加边框以便更容易看到点击区域 */
stroke-width: 1;
} }
</style> </style>

View File

@@ -39,13 +39,23 @@
ry="18" ry="18"
filter="blur(5px)" filter="blur(5px)"
class="glow" class="glow"
/> /> </svg>
</svg> <!-- 渲染自定义引脚数组 -->
<!-- 新增数字输入引脚Pin放在LED左侧居中 --> <div v-for="pin in props.pins" :key="pin.pinId"
<div style="position:absolute;left:-18px;top:50%;transform:translateY(-50%);"> :style="{
position: 'absolute',
left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)'
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`">
<Pin <Pin
ref="pinRef" :ref="el => { if(el) pinRefs[pin.pinId] = el }"
v-bind="props" :label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
@pin-click="$emit('pin-click', $event)" @pin-click="$emit('pin-click', $event)"
/> />
</div> </div>
@@ -57,34 +67,34 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints'; import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
import Pin from './Pin.vue'; import Pin from './Pin.vue';
// --- 关键暴露getPinPosition代理到内部Pin --- // 存储多个Pin引用
const pinRef = ref<any>(null); const pinRefs = ref<Record<string, any>>({});
// 从Pin组件继承属性
interface PinProps {
label?: string;
constraint?: string;
componentId?: string; // 添加componentId属性
// 这些属性被预设为固定值,但仍然包含在类型中以便完整继承
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
}
// LED特有属性 // LED特有属性
interface LEDProps { interface LEDProps {
size?: number; size?: number;
color?: string; color?: string;
brightness?: number;
pins?: {
pinId: string;
constraint: string;
x: number;
y: number;
}[];
} }
// 组合两个接口 const props = withDefaults(defineProps<LEDProps>(), {
interface Props extends PinProps, LEDProps {}
const props = withDefaults(defineProps<Props>(), {
size: 1, size: 1,
color: 'red', color: 'red',
brightness: 80,
pins: () => [
{
pinId: 'LED',
constraint: '', constraint: '',
label: 'LED', x: 50,
componentId: '' y: 30
}
]
}); });
const width = computed(() => 100 * props.size); const width = computed(() => 100 * props.size);
@@ -106,18 +116,26 @@ const ledColor = computed(() => {
return colorMap[props.color.toLowerCase()] || props.color; return colorMap[props.color.toLowerCase()] || props.color;
}); });
// 获取LED的constraint值
const ledConstraint = computed(() => {
if (props.pins && props.pins.length > 0) {
return props.pins[0].constraint;
}
return '';
});
// 监听约束状态变化 // 监听约束状态变化
let unsubscribe: (() => void) | null = null; let unsubscribe: (() => void) | null = null;
onMounted(() => { onMounted(() => {
if (props.constraint) { if (ledConstraint.value) {
unsubscribe = onConstraintStateChange((constraint, level) => { unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === props.constraint) { if (constraint === ledConstraint.value) {
isOn.value = (level === 'high'); isOn.value = (level === 'high');
} }
}); });
// 初始化LED状态 // 初始化LED状态
const currentState = getConstraintState(props.constraint); const currentState = getConstraintState(ledConstraint.value);
isOn.value = (currentState === 'high'); isOn.value = (currentState === 'high');
} }
}); });
@@ -128,7 +146,7 @@ onUnmounted(() => {
} }
}); });
watch(() => props.constraint, (newConstraint) => { watch(() => ledConstraint.value, (newConstraint) => {
if (unsubscribe) { if (unsubscribe) {
unsubscribe(); unsubscribe();
unsubscribe = null; unsubscribe = null;
@@ -149,20 +167,58 @@ defineExpose({
getInfo: () => ({ getInfo: () => ({
color: props.color, color: props.color,
isOn: isOn.value, isOn: isOn.value,
constraint: props.constraint, constraint: ledConstraint.value,
componentId: props.componentId,
direction: 'input', direction: 'input',
type: 'digital' type: 'digital',
pins: props.pins
}), }),
getPinPosition: (componentId: string) => { getPinPosition: (pinId: string) => {
if (pinRef.value && pinRef.value.getPinPosition) { // 如果是自定义的引脚ID
return pinRef.value.getPinPosition(componentId); if (props.pins && props.pins.length > 0) {
console.log('SMT_LED查找Pin ID:', pinId);
console.log('SMT_LED组件尺寸:', props.size, '宽高:', width.value, 'x', height.value);
const customPin = props.pins.find(p => p.pinId === pinId);
console.log('找到的引脚配置:', customPin);
if (customPin) {
// 考虑组件尺寸的缩放
const scaledX = customPin.x * props.size;
const scaledY = customPin.y * props.size;
console.log('使用Pin缩放后的坐标:', scaledX, scaledY);
return {
x: scaledX,
y: scaledY
};
} }
console.log('未找到匹配的引脚');
return null;
}
console.log('没有引脚配置');
return null; return null;
} }
}); });
</script> </script>
<script lang="ts">
// 添加一个静态方法来获取默认props
export function getDefaultProps() {
return {
size: 1,
color: 'red',
brightness: 80,
pins: [
{
pinId: 'LED',
constraint: '',
x: 50,
y: 30
}
]
};
}
</script>
<style scoped> <style scoped>
.led-container { .led-container {
display: flex; display: flex;

View File

@@ -0,0 +1,256 @@
<template>
<div class="seven-segment-display" :style="{ width: width + 'px', height: height + 'px', position: 'relative' }"> <svg
xmlns="http://www.w3.org/2000/svg"
:width="width"
:height="height"
viewBox="0 0 120 220"
class="display"
>
<!-- 数码管基座 -->
<rect width="120" height="180" x="0" y="0" fill="#222" rx="10" ry="10" />
<rect width="110" height="170" x="5" y="5" fill="#333" rx="5" ry="5" />
<!-- 7 + 小数点每个段由多边形表示重新设计点位置使其更接近实际数码管 -->
<!-- a段 (顶部横线) -->
<polygon
:points="'30,20 90,20 98,28 82,36 38,36 22,28'"
:fill="isSegmentActive('a') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('a') ? 1 : 0.15 }"
class="segment"
/>
<!-- b段 (右上竖线) -->
<polygon
:points="'100,30 108,38 108,82 100,90 92,82 92,38'"
:fill="isSegmentActive('b') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('b') ? 1 : 0.15 }"
class="segment"
/>
<!-- c段 (右下竖线) -->
<polygon
:points="'100,90 108,98 108,142 100,150 92,142 92,98'"
:fill="isSegmentActive('c') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('c') ? 1 : 0.15 }"
class="segment"
/>
<!-- d段 (底部横线) -->
<polygon
:points="'30,160 90,160 98,152 82,144 38,144 22,152'"
:fill="isSegmentActive('d') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('d') ? 1 : 0.15 }"
class="segment"
/>
<!-- e段 (左下竖线) -->
<polygon
:points="'20,90 28,98 28,142 20,150 12,142 12,98'"
:fill="isSegmentActive('e') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('e') ? 1 : 0.15 }"
class="segment"
/>
<!-- f段 (左上竖线) -->
<polygon
:points="'20,30 28,38 28,82 20,90 12,82 12,38'"
:fill="isSegmentActive('f') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('f') ? 1 : 0.15 }"
class="segment"
/>
<!-- g段 (中间横线) -->
<polygon
:points="'30,90 38,82 82,82 90,90 82,98 38,98'"
:fill="isSegmentActive('g') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('g') ? 1 : 0.15 }"
class="segment"
/> <!-- dp段 (小数点) -->
<circle
cx="108"
cy="154"
r="6"
:fill="isSegmentActive('dp') ? segmentColor : inactiveColor"
:style="{ opacity: isSegmentActive('dp') ? 1 : 0.15 }"
class="segment"
/>
</svg>
<!-- 引脚 -->
<div v-for="pin in pins" :key="pin.pinId"
:style="{
position: 'absolute',
left: `${pin.x * props.size}px`,
top: `${pin.y * props.size}px`,
transform: 'translate(-50%, -50%)'
}"
:data-pin-wrapper="`${pin.pinId}`"
:data-pin-x="`${pin.x * props.size}`"
:data-pin-y="`${pin.y * props.size}`">
<Pin
:ref="el => { if(el) pinRefs[pin.pinId] = el }"
:label="pin.pinId"
:constraint="pin.constraint"
:pinId="pin.pinId"
@pin-click="$emit('pin-click', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
import Pin from './Pin.vue';
// 存储Pin引用
const pinRefs = ref<Record<string, any>>({});
// 数码管属性
interface SevenSegmentDisplayProps {
size?: number;
color?: string;
pins?: {
pinId: string;
constraint: string;
x: number;
y: number;
}[];
cathodeType?: 'common' | 'anode'; // 共阴极或共阳极
}
const props = withDefaults(defineProps<SevenSegmentDisplayProps>(), {
size: 1,
color: 'red',
cathodeType: 'common', // 默认为共阴极
pins: () => [
{ pinId: 'a', constraint: '', x: 10 , y: 170 }, // a段
{ pinId: 'b', constraint: '', x: 25-1 , y: 170 }, // b段
{ pinId: 'c', constraint: '', x: 40-2 , y: 170 }, // c段
{ pinId: 'd', constraint: '', x: 55-3 , y: 170 }, // d段
{ pinId: 'e', constraint: '', x: 70-4 , y: 170 }, // e段
{ pinId: 'f', constraint: '', x: 85-5 , y: 170 }, // f段
{ pinId: 'g', constraint: '', x: 100-6, y: 170 }, // g段
{ pinId: 'dp', constraint: '', x: 115-7, y: 170 }, // 小数点
{ pinId: 'COM', constraint: '', x: 60 , y: 10 } // 公共端,稍微低一点
]
});
const width = computed(() => 120 * props.size);
const height = computed(() => 220 * props.size);
// 计算段颜色和非激活状态颜色
const segmentColor = computed(() => props.color || 'red');
const inactiveColor = computed(() => '#FFFFFF');
// 监听props变化
watch(
() => props,
(newProps) => {
console.log('SevenSegmentDisplay props changed:', newProps);
updateSegmentStates();
},
{ deep: true }
);
// 段引脚状态
const segmentStates = ref({
a: false,
b: false,
c: false,
d: false,
e: false,
f: false,
g: false,
dp: false,
});
// 判断段是否激活
function isSegmentActive(segment: 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'dp'): boolean {
return segmentStates.value[segment];
}
// 更新引脚状态的函数
function updateSegmentStates() {
for (const pin of props.pins) {
if (['a', 'b', 'c', 'd', 'e', 'f', 'g', 'dp'].includes(pin.pinId)) {
// 如果constraint为空则默认为未激活状态
if (!pin.constraint) {
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = false;
continue;
}
const pinState = getConstraintState(pin.constraint);
// 根据阴极/阳极类型反转逻辑
if (props.cathodeType === 'common') {
// 共阴极: 高电平激活段
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = pinState === 'high';
} else {
// 共阳极: 低电平激活段
segmentStates.value[pin.pinId as keyof typeof segmentStates.value] = pinState === 'low';
}
}
}
}
// 监听约束状态变化
function onConstraintChange(constraint: string, level: string) {
const affectedPin = props.pins.find(pin => pin.constraint === constraint);
if (affectedPin && ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'dp'].includes(affectedPin.pinId)) {
updateSegmentStates();
}
}
// 生命周期钩子
onMounted(() => {
updateSegmentStates();
onConstraintStateChange(onConstraintChange);
});
onUnmounted(() => {
// 清理约束状态监听
});
// 暴露属性和方法
defineExpose({
updateSegmentStates
});
</script>
<style scoped>
.seven-segment-display {
display: inline-block;
position: relative;
}
.segment {
transition: opacity 0.2s, fill 0.2s;
}
/* 数码管发光效果 */
.segment[style*="opacity: 1"] {
filter: drop-shadow(0 0 4px v-bind(segmentColor)) drop-shadow(0 0 2px v-bind(segmentColor));
}
</style>
<!-- 导出默认属性函数供外部使用 -->
<script lang="ts">
export function getDefaultProps() {
return {
size: 1,
color: 'red',
cathodeType: 'common',
pins: [
{ pinId: 'a', constraint: '', x: 10 , y: 170 },
{ pinId: 'b', constraint: '', x: 25-1 , y: 170 },
{ pinId: 'c', constraint: '', x: 40-2 , y: 170 },
{ pinId: 'd', constraint: '', x: 55-3 , y: 170 },
{ pinId: 'e', constraint: '', x: 70-4 , y: 170 },
{ pinId: 'f', constraint: '', x: 85-5 , y: 170 },
{ pinId: 'g', constraint: '', x: 100-6, y: 170 },
{ pinId: 'dp', constraint: '', x: 115-7, y: 170 },
{ pinId: 'COM', constraint: '', x: 60 , y: 10 }
]
};
}
</script>

View File

@@ -33,16 +33,18 @@ interface Props {
strokeColor?: string; strokeColor?: string;
strokeWidth?: number; strokeWidth?: number;
isActive?: boolean; isActive?: boolean;
routingMode?: 'auto' | 'orthogonal' | 'direct'; routingMode?: 'auto' | 'orthogonal' | 'direct' | 'path';
// 针脚引用属性 // 针脚引用属性
startComponentId?: string; startComponentId?: string;
startPinLabel?: string; startPinId?: string;
endComponentId?: string; endComponentId?: string;
endPinLabel?: string; endPinId?: string;
// 添加约束属性 // 添加约束属性
constraint?: string; constraint?: string;
// 显示标签 // 显示标签
showLabel?: boolean; showLabel?: boolean;
// 路径命令 - 对应diagram.json中的线放置迷你语言
pathCommands?: string[];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@@ -81,9 +83,190 @@ const labelPosition = computed(() => {
}); });
const pathData = computed(() => { const pathData = computed(() => {
// 如果有路径命令,使用路径布线模式
if (props.routingMode === 'path' && props.pathCommands && props.pathCommands.length > 0) {
return calculatePathFromCommands(
props.startX,
props.startY,
props.endX,
props.endY,
props.pathCommands
);
}
// 否则使用正交路径
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY); return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
}); });
function calculatePathFromCommands(
startX: number,
startY: number,
endX: number,
endY: number,
commands: string[]
) {
// 找到分隔符索引,通常是 "*"
const splitterIndex = commands.indexOf('*');
if (splitterIndex === -1) {
// 如果没有分隔符,回退到正交路径
return calculateOrthogonalPath(startX, startY, endX, endY);
}
// 分割命令为起点和终点两部分
const startCommands = commands.slice(0, splitterIndex);
const endCommands = commands.slice(splitterIndex + 1);
// 从起点开始生成路径
let currentX = startX;
let currentY = startY;
// 处理起点路径命令
const pathPoints: [number, number][] = [[currentX, currentY]];
// 解析并执行起点命令
for (const cmd of startCommands) {
const { newX, newY } = executePathCommand(currentX, currentY, cmd);
currentX = newX;
currentY = newY;
pathPoints.push([currentX, currentY]);
}
// 从终点开始反向处理
let endCurrentX = endX;
let endCurrentY = endY;
// 保存终点路径点,最后会反转
const endPathPoints: [number, number][] = [[endCurrentX, endCurrentY]];
// 解析并执行终点命令(需要从后向前执行反向命令)
for (let i = endCommands.length - 1; i >= 0; i--) {
const cmd = reversePathCommand(endCommands[i]);
const { newX, newY } = executePathCommand(endCurrentX, endCurrentY, cmd);
endCurrentX = newX;
endCurrentY = newY;
endPathPoints.push([endCurrentX, endCurrentY]);
} // 反转终点路径点,保留所有点
const reversedEndPoints = [...endPathPoints].reverse();
// 将两部分路径连接起来
// 如果终点路径的第一个点与起点路径的最后一个点相同,则去掉重复点
let combinedPoints;
if (pathPoints.length > 0 && reversedEndPoints.length > 0 &&
pathPoints[pathPoints.length - 1][0] === reversedEndPoints[0][0] &&
pathPoints[pathPoints.length - 1][1] === reversedEndPoints[0][1]) {
combinedPoints = [...pathPoints, ...reversedEndPoints.slice(1)];
} else {
combinedPoints = [...pathPoints, ...reversedEndPoints];
}
const allPoints = combinedPoints;
// 检查是否需要添加中间连接点
if (allPoints.length >= 2) {
const startPathEndPoint = pathPoints.length > 0 ? pathPoints[pathPoints.length - 1] : null;
const endPathStartPoint = reversedEndPoints.length > 0 ? reversedEndPoints[0] : null;
// 只有当起点路径和终点路径不相连时才添加连接线
if (startPathEndPoint && endPathStartPoint &&
(startPathEndPoint[0] !== endPathStartPoint[0] || startPathEndPoint[1] !== endPathStartPoint[1])) {
// 使用正交连接或直接连接
let middlePoints: [number, number][] = [];
// 正交连接,添加额外点以确保路径是正交的
middlePoints = generateOrthogonalConnection(
startPathEndPoint[0], startPathEndPoint[1],
endPathStartPoint[0], endPathStartPoint[1]
);
// 在起点路径和终点路径之间插入中间点
allPoints.splice(pathPoints.length, 0, ...middlePoints);
}
} // 生成SVG路径
if (allPoints.length < 2) {
// 如果没有足够的点,直接从起点到终点画一条线
return `M ${startX} ${startY} L ${endX} ${endY}`;
}
// 使用所有点生成路径字符串
let pathStr = `M ${allPoints[0][0]} ${allPoints[0][1]}`;
for (let i = 1; i < allPoints.length; i++) {
pathStr += ` L ${allPoints[i][0]} ${allPoints[i][1]}`;
}
return pathStr;
}
// 执行单个路径命令
function executePathCommand(x: number, y: number, command: string): { newX: number; newY: number } {
// 解析命令,例如 "down10", "right20", "downright5" 等
let newX = x;
let newY = y;
if (command.startsWith('right')) {
const distance = parseInt(command.substring(5), 10) || 10;
newX = x + distance;
newY = y;
} else if (command.startsWith('left')) {
const distance = parseInt(command.substring(4), 10) || 10;
newX = x - distance;
newY = y;
} else if (command.startsWith('down')) {
if (command.startsWith('downright')) {
const distance = parseInt(command.substring(9), 10) || 10;
newX = x + distance;
newY = y + distance;
} else if (command.startsWith('downleft')) {
const distance = parseInt(command.substring(8), 10) || 10;
newX = x - distance;
newY = y + distance;
} else {
const distance = parseInt(command.substring(4), 10) || 10;
newX = x;
newY = y + distance;
}
} else if (command.startsWith('up')) {
if (command.startsWith('upright')) {
const distance = parseInt(command.substring(7), 10) || 10;
newX = x + distance;
newY = y - distance;
} else if (command.startsWith('upleft')) {
const distance = parseInt(command.substring(6), 10) || 10;
newX = x - distance;
newY = y - distance;
} else {
const distance = parseInt(command.substring(2), 10) || 10;
newX = x;
newY = y - distance;
}
}
return { newX, newY };
}
// 生成两点之间的正交连接点
function generateOrthogonalConnection(x1: number, y1: number, x2: number, y2: number): [number, number][] {
const dx = x2 - x1;
const dy = y2 - y1;
if (dx === 0 || dy === 0) {
// 如果在同一水平或垂直线上,不需要额外点
return [];
}
// 选择先水平移动还是先垂直移动
const middlePoints: [number, number][] = [];
if (Math.abs(dx) > Math.abs(dy)) {
// 先水平后垂直
middlePoints.push([x1 + dx / 2, y1]);
middlePoints.push([x1 + dx / 2, y2]);
} else {
// 先垂直后水平
middlePoints.push([x1, y1 + dy / 2]);
middlePoints.push([x2, y1 + dy / 2]);
}
return middlePoints;
}
// 计算正交路径
function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) { function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) {
// 计算两点之间的水平和垂直距离 // 计算两点之间的水平和垂直距离
const dx = endX - startX; const dx = endX - startX;
@@ -107,6 +290,48 @@ function calculateOrthogonalPath(startX: number, startY: number, endX: number, e
} }
} }
// 添加反转命令函数 - 将方向命令反转
function reversePathCommand(command: string): string {
// 提取距离部分
const distanceMatch = command.match(/\d+$/);
const distance = distanceMatch ? distanceMatch[0] : "10"; // 默认距离是10
// 根据命令类型返回反向命令
// 水平方向反转
if (command.startsWith('right')) {
return `left${distance}`;
}
else if (command.startsWith('left')) {
return `right${distance}`;
}
// 垂直和斜向反转
else if (command.startsWith('down')) {
if (command.startsWith('downright')) {
return `upleft${distance}`;
}
else if (command.startsWith('downleft')) {
return `upright${distance}`;
}
else {
return `up${distance}`;
}
}
else if (command.startsWith('up')) {
if (command.startsWith('upright')) {
return `downleft${distance}`;
}
else if (command.startsWith('upleft')) {
return `downright${distance}`;
}
else {
return `down${distance}`;
}
}
// 默认情况下,无法反转就返回原命令
return command;
}
// 监听约束状态变化 // 监听约束状态变化
let unsubscribe: (() => void) | null = null; let unsubscribe: (() => void) | null = null;
@@ -145,13 +370,12 @@ watch(() => props.constraint, (newConstraint, oldConstraint) => {
}); });
// 暴露方法,用于获取这条连线的信息 // 暴露方法,用于获取这条连线的信息
defineExpose({ id: props.id, defineExpose({ id: props.id, getInfo: () => ({
getInfo: () => ({
id: props.id, id: props.id,
startComponentId: props.startComponentId, startComponentId: props.startComponentId,
startPinLabel: props.startPinLabel, startPinId: props.startPinId,
endComponentId: props.endComponentId, endComponentId: props.endComponentId,
endPinLabel: props.endPinLabel, endPinId: props.endPinId,
constraint: props.constraint constraint: props.constraint
}), }),
// 更新连线位置 // 更新连线位置

View File

@@ -1,363 +1,186 @@
// 组件配置声明 // componentConfig.ts 提供通用的组件配置功能
export type PropType = 'string' | 'number' | 'boolean' | 'select'; import type { DiagramPart } from '../diagramManager';
// 定义选择类型选项 // 属性配置接口
export interface PropOption { export interface PropertyConfig {
value: string | number | boolean;
label: string;
}
export interface PropConfig {
name: string; name: string;
type: string; type: string;
label: string; label: string;
default: any; default: any;
min?: number; options?: Array<{ value: any; label: string }>; // 添加 options 字段用于 select 类型
max?: number; isDirectProp?: boolean; // 标记是否为直接属性
step?: number; isReadOnly?: boolean; // 标记是否为只读属性
options?: PropOption[]; min?: number; // 用于数值类型的最小值
description?: string; max?: number; // 用于数值类型的最大值
category?: string; // 用于在UI中分组属性 step?: number; // 用于数值类型的步长
isArrayType?: boolean; // 标记数组类型属性
} }
export interface ComponentConfig { // 所有基础属性的标签映射
props: PropConfig[]; const basePropertyLabels: Record<number, string> = {
} 0: 'ID',
1: '组件类型',
// 存储所有组件的配置 2: '水平坐标',
const componentConfigs: Record<string, ComponentConfig> = { 3: '垂直坐标',
MechanicalButton: { 4: '角度',
props: [ 5: '分组',
{ 6: '锁定位置',
name: 'bindKey', 7: '隐藏引脚',
type: 'string', 8: '激活状态',
label: '绑定按键', 9: '层级'
default: '',
description: '触发按钮按下的键盘按键'
},
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '按钮的相对大小1代表标准大小'
},
{
name: 'buttonText',
type: 'string',
label: '按钮文本',
default: '',
description: '按钮上显示的自定义文本,优先级高于绑定按键'
},
{
name: 'label',
type: 'string',
label: '引脚标签',
default: 'BTN',
description: '引脚的标签文本'
},
{
name: 'constraint',
type: 'string',
label: '引脚约束',
default: '',
description: '相同约束字符串的引脚将被视为有电气连接'
}
]
},
Switch: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '开关的相对大小1代表标准大小'
},
{
name: 'switchCount',
type: 'number',
label: '开关数量',
default: 6,
min: 1,
max: 12,
step: 1,
description: '可翻转开关的数量'
},
{
name: 'showLabels',
type: 'boolean',
label: '显示标签',
default: true,
description: '是否显示开关编号标签'
},
{
name: 'initialValues',
type: 'string',
label: '初始状态',
default: '',
description: '开关的初始状态格式为逗号分隔的0/1如"1,0,1"表示第1、3个开关打开'
}
]
},
Pin: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '引脚的相对大小1代表标准大小'
},
{
name: 'label',
type: 'string',
label: '引脚标签',
default: 'PIN',
description: '用于标识引脚的名称'
},
{
name: 'constraint',
type: 'string',
label: '引脚约束',
default: '',
description: '相同约束字符串的引脚将被视为有电气连接'
},
{
name: 'direction',
type: 'select',
label: '输入/输出特性',
default: 'input',
options: [
{ value: 'input', label: '输入' },
{ value: 'output', label: '输出' },
{ value: 'inout', label: '双向' }
],
description: '引脚的输入/输出特性'
},
{
name: 'type',
type: 'select',
label: '模数特性',
default: 'digital',
options: [
{ value: 'digital', label: 'digital' },
{ value: 'analog', label: 'analog' }
],
description: '引脚的模数特性,数字或模拟'
}
]
},
HDMI: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'HDMI接口的相对大小1代表标准大小'
}
]
},
DDR: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'DDR内存的相对大小1代表标准大小'
}
]
},
ETH: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '以太网接口的相对大小1代表标准大小'
}
]
},
SD: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'SD卡插槽的相对大小1代表标准大小'
}
]
},
SFP: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'SFP光纤模块的相对大小1代表标准大小'
}
]
},
SMA: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'SMA连接器的相对大小1代表标准大小'
}
]
}, MotherBoard: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 2,
step: 0.1,
description: '主板的相对大小1代表标准大小'
}
]
}, SMT_LED: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: 'LED的相对大小1代表标准大小'
},
{
name: 'color',
type: 'select',
label: '颜色',
default: 'red',
options: [
{ value: 'red', label: '红色' },
{ value: 'green', label: '绿色' },
{ value: 'blue', label: '蓝色' },
{ value: 'yellow', label: '黄色' },
{ value: 'orange', label: '橙色' },
{ value: 'white', label: '白色' },
{ value: 'purple', label: '紫色' }
],
description: 'LED的颜色'
},
{
name: 'initialOn',
type: 'boolean',
label: '初始状态',
default: false,
description: 'LED的初始开关状态'
},
{
name: 'brightness',
type: 'number',
label: '亮度(%)',
default: 80,
min: 0,
max: 100,
step: 5,
description: 'LED的亮度百分比范围0-100'
},
{
name: 'constraint',
type: 'string',
label: '引脚约束',
default: '',
description: '相同约束字符串的引脚将被视为有电气连接'
}
]
},
// 线缆配置
Wire: {
props: [
{
name: 'routingMode',
type: 'select',
label: '路由方式',
default: 'orthogonal',
options: [
{ value: 'orthogonal', label: '直角' },
{ value: 'direct', label: '直线' },
{ value: 'auto', label: '自动' }
],
description: '线路连接方式'
},
{
name: 'strokeColor',
type: 'string',
label: '线条颜色',
default: '#4a5568',
description: '线条颜色使用CSS颜色值'
},
{
name: 'strokeWidth',
type: 'number',
label: '线条宽度',
default: 2,
min: 1,
max: 10,
step: 0.5,
description: '线条宽度'
},
{
name: 'constraint',
type: 'string',
label: '约束名称',
default: '',
description: '线路约束名称,用于标识连接关系'
},
{
name: 'showLabel',
type: 'boolean',
label: '显示标签',
default: false,
description: '是否显示连线上的约束标签'
}
]
},
}; };
// 获取组件配置的函数 // 只读属性索引列表
export function getComponentConfig(type: string): ComponentConfig | null { const readOnlyPropertyIndexes = [0, 1]; // 移除了isOn对应的索引8
return componentConfigs[type] || null;
/**
* 从组件数据中自动生成属性配置
* @param componentData 组件数据
* @returns 属性配置数组
*/
export function generatePropertyConfigs(componentData: DiagramPart): PropertyConfig[] {
const configs: PropertyConfig[] = [];
// 获取基础属性(排除attrs)
const directPropKeys = Object.keys(componentData).filter(key => key !== 'attrs');
// 为每个直接属性创建配置
directPropKeys.forEach((propName, index) => {
let propValue: any = (componentData as any)[propName];
let propType = typeof propValue;
// 对于undefined的属性提供默认值
if (propValue === undefined) {
if (propType === 'boolean') propValue = false;
else if (propType === 'number') propValue = 0;
else propValue = '';
}
// 创建配置对象
const propConfig: PropertyConfig = {
name: propName,
label: basePropertyLabels[index] || propName,
type: propType as 'string' | 'number' | 'boolean' | 'select',
default: propValue,
isDirectProp: true,
isReadOnly: readOnlyPropertyIndexes.includes(index)
};
// 数值类型的特殊设置
if (propType === 'number') {
if (index === 9) { // 层级
propConfig.min = 0;
propConfig.max = 100;
} else if (index === 4) { // 角度
propConfig.min = 0;
propConfig.max = 360;
}
propConfig.step = 1;
}
configs.push(propConfig);
});
return configs;
}
/**
* 从组件模块的getDefaultProps方法生成属性配置
* @param defaultProps 默认属性对象
* @returns 属性配置数组
*/
export function generatePropsFromDefault(defaultProps: Record<string, any>): PropertyConfig[] {
const configs: PropertyConfig[] = [];
for (const [propName, propValue] of Object.entries(defaultProps)) {
// 特殊处理pins属性
if (propName === 'pins') {
const propConfig: PropertyConfig = {
name: propName,
label: 'Pins',
default: propValue,
type: 'array',
isArrayType: true
};
configs.push(propConfig);
continue;
}
// 根据属性类型创建配置
let propType = typeof propValue;
let propConfig: PropertyConfig = {
name: propName,
label: propName.charAt(0).toUpperCase() + propName.slice(1), // 首字母大写作为标签
default: propValue,
type: propType as 'string' | 'number' | 'boolean' | 'select'
};
// 根据值类型设置表单控件类型
if (propType === 'string') {
propConfig.type = 'string';
} else if (propType === 'number') {
propConfig.type = 'number';
propConfig.min = 0;
propConfig.max = 100;
propConfig.step = 0.1;
} else if (propType === 'boolean') {
propConfig.type = 'boolean';
} else if (propType === 'object' && propValue !== null && propValue.hasOwnProperty('options')) {
// 如果是含有options的对象认为它是select类型
propConfig.type = 'select';
propConfig.options = (propValue as any).options;
}
configs.push(propConfig);
}
return configs;
}
/**
* 从属性对象生成配置
* @param attrs 属性对象
* @returns 属性配置数组
*/
export function generatePropsFromAttrs(attrs: Record<string, any>): PropertyConfig[] {
const configs: PropertyConfig[] = [];
for (const [propName, propValue] of Object.entries(attrs)) {
// 特殊处理pins属性
if (propName === 'pins') {
const propConfig: PropertyConfig = {
name: propName,
label: 'Pins',
default: propValue,
type: 'array',
isArrayType: true
};
configs.push(propConfig);
continue;
}
// 根据属性值类型创建配置
let propType = typeof propValue;
let propConfig: PropertyConfig = {
name: propName,
label: propName.charAt(0).toUpperCase() + propName.slice(1), // 首字母大写作为标签
default: propValue || '',
type: propType as 'string' | 'number' | 'boolean' | 'select'
};
configs.push(propConfig);
}
return configs;
}
/**
* 安全地从组件对象获取属性值
* @param component 组件对象
* @param propName 属性名
* @returns 属性值
*/
export function getPropValue(component: DiagramPart, propName: string): any {
if (!component) return undefined;
return (component as any)[propName];
} }

View File

@@ -7,20 +7,60 @@
viewBox="0 0 189.71826 110.06672" viewBox="0 0 189.71826 110.06672"
version="1.1" version="1.1"
id="svg1" id="svg1"
inkscape:version="1.4 (86a8ad7, 2024-10-11)"
sodipodi:docname="pin.svg"
xml:space="preserve" xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.70710678"
inkscape:cx="769.33218"
inkscape:cy="744.58344"
inkscape:window-width="2560"
inkscape:window-height="1017"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g4028-2"
inkscape:export-bgcolor="#ffffff00"
showguides="false"><inkscape:page
x="0"
y="-1.2139753e-13"
width="189.71826"
height="110.06673"
id="page2"
margin="0"
bleed="0" /></sodipodi:namedview><defs
id="defs1" /><g id="defs1" /><g
inkscape:label="图层 1"
inkscape:groupmode="layer"
id="layer1" id="layer1"
transform="translate(-14.20163,-182.98715)"><g transform="translate(-14.20163,-182.98715)"><g
id="g4491-5" id="g4491-5"
transform="translate(-2.671235,131.40328)"><g transform="translate(-2.671235,131.40328)"><g
id="g4489-4" id="g4489-4"
transform="translate(0.71912207,-18.325919)"><g transform="translate(0.71912207,-18.325919)"
inkscape:export-filename="motherboard.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><g
id="g4028-2"><path id="g4028-2"><path
style="fill:#222222;fill-opacity:1;fill-rule:nonzero;stroke-width:0.264999;stroke-dasharray:none" style="fill:#222222;fill-opacity:1;fill-rule:nonzero;stroke-width:0.264999;stroke-dasharray:none"
d="M 16.153743,72.454427 V 173.22414 c 0.209579,1.02406 0.641226,1.91539 2.158085,2.15808 h 13.173985 v -7.90761 h 18.279908 v 7.90115 h 7.88052 v -6.44562 c 0,-2.21895 3.644172,-2.24344 3.644172,0 v 11.02362 h 11.023621 v -7.47055 c 0,-1.06741 1.935964,-1.06294 1.935964,0 v 7.49332 H 95.197651 V 167.4585 H 203.13414 c 1.48786,0.0739 2.45207,-0.52544 2.73787,-1.99703 V 71.729696 c -0.0202,-1.117201 -0.51125,-1.824404 -1.81988,-1.819878 H 18.666141 c -1.750373,0.260441 -2.319686,1.28129 -2.512398,2.544609 z" d="M 16.153743,72.454427 V 173.22414 c 0.209579,1.02406 0.641226,1.91539 2.158085,2.15808 h 13.173985 v -7.90761 h 18.279908 v 7.90115 h 7.88052 v -6.44562 c 0,-2.21895 3.644172,-2.24344 3.644172,0 v 11.02362 h 11.023621 v -7.47055 c 0,-1.06741 1.935964,-1.06294 1.935964,0 v 7.49332 H 95.197651 V 167.4585 H 203.13414 c 1.48786,0.0739 2.45207,-0.52544 2.73787,-1.99703 V 71.729696 c -0.0202,-1.117201 -0.51125,-1.824404 -1.81988,-1.819878 H 18.666141 c -1.750373,0.260441 -2.319686,1.28129 -2.512398,2.544609 z"
id="path4026-5" /><rect id="path4026-5"
sodipodi:nodetypes="ccccccccccccccccccccccc"
inkscape:export-filename="path4026-5.svg"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" /><rect
style="fill:#353535;fill-opacity:1;fill-rule:nonzero;stroke:#aa8800;stroke-width:0.46;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" style="fill:#353535;fill-opacity:1;fill-rule:nonzero;stroke:#aa8800;stroke-width:0.46;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect4026-3" id="rect4026-3"
width="78.259003" width="78.259003"
@@ -37,22 +77,11 @@
y="74.100662" y="74.100662"
id="text4027-6" id="text4027-6"
transform="scale(0.93589337,1.0684978)"><tspan transform="scale(0.93589337,1.0684978)"><tspan
sodipodi:role="line"
id="tspan4027-8" id="tspan4027-8"
x="34.243221" x="34.243221"
y="74.100662" y="74.100662"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.45312px;font-family:'Microsoft Sans Serif';-inkscape-font-specification:'Microsoft Sans Serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.20475">MES2L676-BASE-V1.2</tspan></text><rect style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.45312px;font-family:'Microsoft Sans Serif';-inkscape-font-specification:'Microsoft Sans Serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.20475">MES2L676-BASE-V1.2</tspan></text></g><g
style="fill:#002b11;fill-opacity:0.772973;fill-rule:nonzero;stroke:none;stroke-width:0.459999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect4027-2"
width="30"
height="30"
x="84.577484"
y="103.45676" /><rect
style="fill:#1a1a1a;fill-opacity:0.772973;fill-rule:nonzero;stroke:none;stroke-width:0.459999;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect4028-6"
width="25"
height="25"
x="87.246216"
y="106.04675" /></g><g
id="g4488-6"><rect id="g4488-6"><rect
style="font-variation-settings:'wght' 700;opacity:1;fill:none;fill-opacity:0.968317;fill-rule:nonzero;stroke:#898989;stroke-width:0.347;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal" style="font-variation-settings:'wght' 700;opacity:1;fill:none;fill-opacity:0.968317;fill-rule:nonzero;stroke:#898989;stroke-width:0.347;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
id="rect4479-3" id="rect4479-3"

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,837 @@
{
"version": 1,
"author": "template",
"editor": "system",
"parts": [
{ "id": "board","type": "MotherBoard","x": 0,"y": 0,
"attrs": {
"size": 1
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": true,"index": 0
},
{
"id": "FPGA_chip", "type": "PG2L100H_FBG676","x": 250,"y": 200,
"attrs": {
"size": 1,
"pins": [
{"pinId": "pin_0","constraint": ""},
{"pinId": "pin_1","constraint": ""},
{"pinId": "pin_2","constraint": ""},
{"pinId": "pin_3","constraint": ""},
{"pinId": "pin_4","constraint": ""},
{"pinId": "pin_5","constraint": ""},
{"pinId": "pin_6","constraint": ""},
{"pinId": "pin_7","constraint": ""},
{"pinId": "pin_8","constraint": ""},
{"pinId": "pin_9","constraint": ""},
{"pinId": "pin_10","constraint": ""},
{"pinId": "pin_11","constraint": ""},
{"pinId": "pin_12","constraint": ""},
{"pinId": "pin_13","constraint": ""},
{"pinId": "pin_14","constraint": ""},
{"pinId": "pin_15","constraint": ""},
{"pinId": "pin_16","constraint": ""},
{"pinId": "pin_17","constraint": ""},
{"pinId": "pin_18","constraint": ""},
{"pinId": "pin_19","constraint": ""},
{"pinId": "pin_20","constraint": ""},
{"pinId": "pin_21","constraint": ""},
{"pinId": "pin_22","constraint": ""},
{"pinId": "pin_23","constraint": ""},
{"pinId": "pin_24","constraint": ""},
{"pinId": "pin_25","constraint": ""},
{"pinId": "pin_26","constraint": ""},
{"pinId": "pin_27","constraint": ""},
{"pinId": "pin_28","constraint": ""},
{"pinId": "pin_29","constraint": ""},
{"pinId": "pin_30","constraint": ""},
{"pinId": "pin_31","constraint": ""},
{"pinId": "pin_32","constraint": ""},
{"pinId": "pin_33","constraint": ""},
{"pinId": "pin_34","constraint": ""},
{"pinId": "pin_35","constraint": ""},
{"pinId": "pin_36","constraint": ""},
{"pinId": "pin_37","constraint": ""},
{"pinId": "pin_38","constraint": ""},
{"pinId": "pin_39","constraint": ""},
{"pinId": "pin_40","constraint": ""},
{"pinId": "pin_41","constraint": ""},
{"pinId": "pin_42","constraint": ""},
{"pinId": "pin_43","constraint": ""},
{"pinId": "pin_44","constraint": ""},
{"pinId": "pin_45","constraint": ""},
{"pinId": "pin_46","constraint": ""},
{"pinId": "pin_47","constraint": ""},
{"pinId": "pin_48","constraint": ""},
{"pinId": "pin_49","constraint": ""},
{"pinId": "pin_50","constraint": ""},
{"pinId": "pin_51","constraint": ""},
{"pinId": "pin_52","constraint": ""},
{"pinId": "pin_53","constraint": ""},
{"pinId": "pin_54","constraint": ""},
{"pinId": "pin_55","constraint": ""},
{"pinId": "pin_56","constraint": ""},
{"pinId": "pin_57","constraint": ""},
{"pinId": "pin_58","constraint": ""},
{"pinId": "pin_59","constraint": ""},
{"pinId": "pin_60","constraint": ""},
{"pinId": "pin_61","constraint": ""},
{"pinId": "pin_62","constraint": ""},
{"pinId": "pin_63","constraint": ""},
{"pinId": "pin_64","constraint": ""},
{"pinId": "pin_65","constraint": ""},
{"pinId": "pin_66","constraint": ""},
{"pinId": "pin_67","constraint": ""},
{"pinId": "pin_68","constraint": ""},
{"pinId": "pin_69","constraint": ""},
{"pinId": "pin_70","constraint": ""},
{"pinId": "pin_71","constraint": ""},
{"pinId": "pin_72","constraint": ""},
{"pinId": "pin_73","constraint": ""},
{"pinId": "pin_74","constraint": ""},
{"pinId": "pin_75","constraint": ""},
{"pinId": "pin_76","constraint": ""},
{"pinId": "pin_77","constraint": ""},
{"pinId": "pin_78","constraint": ""},
{"pinId": "pin_79","constraint": ""},
{"pinId": "pin_80","constraint": ""},
{"pinId": "pin_81","constraint": ""},
{"pinId": "pin_82","constraint": ""},
{"pinId": "pin_83","constraint": ""},
{"pinId": "pin_84","constraint": ""},
{"pinId": "pin_85","constraint": ""},
{"pinId": "pin_86","constraint": ""},
{"pinId": "pin_87","constraint": ""},
{"pinId": "pin_88","constraint": ""},
{"pinId": "pin_89","constraint": ""},
{"pinId": "pin_90","constraint": ""},
{"pinId": "pin_91","constraint": ""},
{"pinId": "pin_92","constraint": ""},
{"pinId": "pin_93","constraint": ""},
{"pinId": "pin_94","constraint": ""},
{"pinId": "pin_95","constraint": ""},
{"pinId": "pin_96","constraint": ""},
{"pinId": "pin_97","constraint": ""},
{"pinId": "pin_98","constraint": ""},
{"pinId": "pin_99","constraint": ""},
{"pinId": "pin_100","constraint": ""},
{"pinId": "pin_101","constraint": ""},
{"pinId": "pin_102","constraint": ""},
{"pinId": "pin_103","constraint": ""},
{"pinId": "pin_104","constraint": ""},
{"pinId": "pin_105","constraint": ""},
{"pinId": "pin_106","constraint": ""},
{"pinId": "pin_107","constraint": ""},
{"pinId": "pin_108","constraint": ""},
{"pinId": "pin_109","constraint": ""},
{"pinId": "pin_110","constraint": ""},
{"pinId": "pin_111","constraint": ""},
{"pinId": "pin_112","constraint": ""},
{"pinId": "pin_113","constraint": ""},
{"pinId": "pin_114","constraint": ""},
{"pinId": "pin_115","constraint": ""},
{"pinId": "pin_116","constraint": ""},
{"pinId": "pin_117","constraint": ""},
{"pinId": "pin_118","constraint": ""},
{"pinId": "pin_119","constraint": ""},
{"pinId": "pin_120","constraint": ""},
{"pinId": "pin_121","constraint": ""},
{"pinId": "pin_122","constraint": ""},
{"pinId": "pin_123","constraint": ""},
{"pinId": "pin_124","constraint": ""},
{"pinId": "pin_125","constraint": ""},
{"pinId": "pin_126","constraint": ""},
{"pinId": "pin_127","constraint": ""},
{"pinId": "pin_128","constraint": ""},
{"pinId": "pin_129","constraint": ""},
{"pinId": "pin_130","constraint": ""},
{"pinId": "pin_131","constraint": ""},
{"pinId": "pin_132","constraint": ""},
{"pinId": "pin_133","constraint": ""},
{"pinId": "pin_134","constraint": ""},
{"pinId": "pin_135","constraint": ""},
{"pinId": "pin_136","constraint": ""},
{"pinId": "pin_137","constraint": ""},
{"pinId": "pin_138","constraint": ""},
{"pinId": "pin_139","constraint": ""},
{"pinId": "pin_140","constraint": ""},
{"pinId": "pin_141","constraint": ""},
{"pinId": "pin_142","constraint": ""},
{"pinId": "pin_143","constraint": ""},
{"pinId": "pin_144","constraint": ""},
{"pinId": "pin_145","constraint": ""},
{"pinId": "pin_146","constraint": ""},
{"pinId": "pin_147","constraint": ""},
{"pinId": "pin_148","constraint": ""},
{"pinId": "pin_149","constraint": ""},
{"pinId": "pin_150","constraint": ""},
{"pinId": "pin_151","constraint": ""},
{"pinId": "pin_152","constraint": ""},
{"pinId": "pin_153","constraint": ""},
{"pinId": "pin_154","constraint": ""},
{"pinId": "pin_155","constraint": ""},
{"pinId": "pin_156","constraint": ""},
{"pinId": "pin_157","constraint": ""},
{"pinId": "pin_158","constraint": ""},
{"pinId": "pin_159","constraint": ""},
{"pinId": "pin_160","constraint": ""},
{"pinId": "pin_161","constraint": ""},
{"pinId": "pin_162","constraint": ""},
{"pinId": "pin_163","constraint": ""},
{"pinId": "pin_164","constraint": ""},
{"pinId": "pin_165","constraint": ""},
{"pinId": "pin_166","constraint": ""},
{"pinId": "pin_167","constraint": ""},
{"pinId": "pin_168","constraint": ""},
{"pinId": "pin_169","constraint": ""},
{"pinId": "pin_170","constraint": ""},
{"pinId": "pin_171","constraint": ""},
{"pinId": "pin_172","constraint": ""},
{"pinId": "pin_173","constraint": ""},
{"pinId": "pin_174","constraint": ""},
{"pinId": "pin_175","constraint": ""},
{"pinId": "pin_176","constraint": ""},
{"pinId": "pin_177","constraint": ""},
{"pinId": "pin_178","constraint": ""},
{"pinId": "pin_179","constraint": ""},
{"pinId": "pin_180","constraint": ""},
{"pinId": "pin_181","constraint": ""},
{"pinId": "pin_182","constraint": ""},
{"pinId": "pin_183","constraint": ""},
{"pinId": "pin_184","constraint": ""},
{"pinId": "pin_185","constraint": ""},
{"pinId": "pin_186","constraint": ""},
{"pinId": "pin_187","constraint": ""},
{"pinId": "pin_188","constraint": ""},
{"pinId": "pin_189","constraint": ""},
{"pinId": "pin_190","constraint": ""},
{"pinId": "pin_191","constraint": ""},
{"pinId": "pin_192","constraint": ""},
{"pinId": "pin_193","constraint": ""},
{"pinId": "pin_194","constraint": ""},
{"pinId": "pin_195","constraint": ""},
{"pinId": "pin_196","constraint": ""},
{"pinId": "pin_197","constraint": ""},
{"pinId": "pin_198","constraint": ""},
{"pinId": "pin_199","constraint": ""},
{"pinId": "pin_200","constraint": ""},
{"pinId": "pin_201","constraint": ""},
{"pinId": "pin_202","constraint": ""},
{"pinId": "pin_203","constraint": ""},
{"pinId": "pin_204","constraint": ""},
{"pinId": "pin_205","constraint": ""},
{"pinId": "pin_206","constraint": ""},
{"pinId": "pin_207","constraint": ""},
{"pinId": "pin_208","constraint": ""},
{"pinId": "pin_209","constraint": ""},
{"pinId": "pin_210","constraint": ""},
{"pinId": "pin_211","constraint": ""},
{"pinId": "pin_212","constraint": ""},
{"pinId": "pin_213","constraint": ""},
{"pinId": "pin_214","constraint": ""},
{"pinId": "pin_215","constraint": ""},
{"pinId": "pin_216","constraint": ""},
{"pinId": "pin_217","constraint": ""},
{"pinId": "pin_218","constraint": ""},
{"pinId": "pin_219","constraint": ""},
{"pinId": "pin_220","constraint": ""},
{"pinId": "pin_221","constraint": ""},
{"pinId": "pin_222","constraint": ""},
{"pinId": "pin_223","constraint": ""},
{"pinId": "pin_224","constraint": ""},
{"pinId": "pin_225","constraint": ""},
{"pinId": "pin_226","constraint": ""},
{"pinId": "pin_227","constraint": ""},
{"pinId": "pin_228","constraint": ""},
{"pinId": "pin_229","constraint": ""},
{"pinId": "pin_230","constraint": ""},
{"pinId": "pin_231","constraint": ""},
{"pinId": "pin_232","constraint": ""},
{"pinId": "pin_233","constraint": ""},
{"pinId": "pin_234","constraint": ""},
{"pinId": "pin_235","constraint": ""},
{"pinId": "pin_236","constraint": ""},
{"pinId": "pin_237","constraint": ""},
{"pinId": "pin_238","constraint": ""},
{"pinId": "pin_239","constraint": ""},
{"pinId": "pin_240","constraint": ""},
{"pinId": "pin_241","constraint": ""},
{"pinId": "pin_242","constraint": ""},
{"pinId": "pin_243","constraint": ""},
{"pinId": "pin_244","constraint": ""},
{"pinId": "pin_245","constraint": ""},
{"pinId": "pin_246","constraint": ""},
{"pinId": "pin_247","constraint": ""},
{"pinId": "pin_248","constraint": ""},
{"pinId": "pin_249","constraint": ""},
{"pinId": "pin_250","constraint": ""},
{"pinId": "pin_251","constraint": ""},
{"pinId": "pin_252","constraint": ""},
{"pinId": "pin_253","constraint": ""},
{"pinId": "pin_254","constraint": ""},
{"pinId": "pin_255","constraint": ""},
{"pinId": "pin_256","constraint": ""},
{"pinId": "pin_257","constraint": ""},
{"pinId": "pin_258","constraint": ""},
{"pinId": "pin_259","constraint": ""},
{"pinId": "pin_260","constraint": ""},
{"pinId": "pin_261","constraint": ""},
{"pinId": "pin_262","constraint": ""},
{"pinId": "pin_263","constraint": ""},
{"pinId": "pin_264","constraint": ""},
{"pinId": "pin_265","constraint": ""},
{"pinId": "pin_266","constraint": ""},
{"pinId": "pin_267","constraint": ""},
{"pinId": "pin_268","constraint": ""},
{"pinId": "pin_269","constraint": ""},
{"pinId": "pin_270","constraint": ""},
{"pinId": "pin_271","constraint": ""},
{"pinId": "pin_272","constraint": ""},
{"pinId": "pin_273","constraint": ""},
{"pinId": "pin_274","constraint": ""},
{"pinId": "pin_275","constraint": ""},
{"pinId": "pin_276","constraint": ""},
{"pinId": "pin_277","constraint": ""},
{"pinId": "pin_278","constraint": ""},
{"pinId": "pin_279","constraint": ""},
{"pinId": "pin_280","constraint": ""},
{"pinId": "pin_281","constraint": ""},
{"pinId": "pin_282","constraint": ""},
{"pinId": "pin_283","constraint": ""},
{"pinId": "pin_284","constraint": ""},
{"pinId": "pin_285","constraint": ""},
{"pinId": "pin_286","constraint": ""},
{"pinId": "pin_287","constraint": ""},
{"pinId": "pin_288","constraint": ""},
{"pinId": "pin_289","constraint": ""},
{"pinId": "pin_290","constraint": ""},
{"pinId": "pin_291","constraint": ""},
{"pinId": "pin_292","constraint": ""},
{"pinId": "pin_293","constraint": ""},
{"pinId": "pin_294","constraint": ""},
{"pinId": "pin_295","constraint": ""},
{"pinId": "pin_296","constraint": ""},
{"pinId": "pin_297","constraint": ""},
{"pinId": "pin_298","constraint": ""},
{"pinId": "pin_299","constraint": ""},
{"pinId": "pin_300","constraint": ""},
{"pinId": "pin_301","constraint": ""},
{"pinId": "pin_302","constraint": ""},
{"pinId": "pin_303","constraint": ""},
{"pinId": "pin_304","constraint": ""},
{"pinId": "pin_305","constraint": ""},
{"pinId": "pin_306","constraint": ""},
{"pinId": "pin_307","constraint": ""},
{"pinId": "pin_308","constraint": ""},
{"pinId": "pin_309","constraint": ""},
{"pinId": "pin_310","constraint": ""},
{"pinId": "pin_311","constraint": ""},
{"pinId": "pin_312","constraint": ""},
{"pinId": "pin_313","constraint": ""},
{"pinId": "pin_314","constraint": ""},
{"pinId": "pin_315","constraint": ""},
{"pinId": "pin_316","constraint": ""},
{"pinId": "pin_317","constraint": ""},
{"pinId": "pin_318","constraint": ""},
{"pinId": "pin_319","constraint": ""},
{"pinId": "pin_320","constraint": ""},
{"pinId": "pin_321","constraint": ""},
{"pinId": "pin_322","constraint": ""},
{"pinId": "pin_323","constraint": ""},
{"pinId": "pin_324","constraint": ""},
{"pinId": "pin_325","constraint": ""},
{"pinId": "pin_326","constraint": ""},
{"pinId": "pin_327","constraint": ""},
{"pinId": "pin_328","constraint": ""},
{"pinId": "pin_329","constraint": ""},
{"pinId": "pin_330","constraint": ""},
{"pinId": "pin_331","constraint": ""},
{"pinId": "pin_332","constraint": ""},
{"pinId": "pin_333","constraint": ""},
{"pinId": "pin_334","constraint": ""},
{"pinId": "pin_335","constraint": ""},
{"pinId": "pin_336","constraint": ""},
{"pinId": "pin_337","constraint": ""},
{"pinId": "pin_338","constraint": ""},
{"pinId": "pin_339","constraint": ""},
{"pinId": "pin_340","constraint": ""},
{"pinId": "pin_341","constraint": ""},
{"pinId": "pin_342","constraint": ""},
{"pinId": "pin_343","constraint": ""},
{"pinId": "pin_344","constraint": ""},
{"pinId": "pin_345","constraint": ""},
{"pinId": "pin_346","constraint": ""},
{"pinId": "pin_347","constraint": ""},
{"pinId": "pin_348","constraint": ""},
{"pinId": "pin_349","constraint": ""},
{"pinId": "pin_350","constraint": ""},
{"pinId": "pin_351","constraint": ""},
{"pinId": "pin_352","constraint": ""},
{"pinId": "pin_353","constraint": ""},
{"pinId": "pin_354","constraint": ""},
{"pinId": "pin_355","constraint": ""},
{"pinId": "pin_356","constraint": ""},
{"pinId": "pin_357","constraint": ""},
{"pinId": "pin_358","constraint": ""},
{"pinId": "pin_359","constraint": ""},
{"pinId": "pin_360","constraint": ""},
{"pinId": "pin_361","constraint": ""},
{"pinId": "pin_362","constraint": ""},
{"pinId": "pin_363","constraint": ""},
{"pinId": "pin_364","constraint": ""},
{"pinId": "pin_365","constraint": ""},
{"pinId": "pin_366","constraint": ""},
{"pinId": "pin_367","constraint": ""},
{"pinId": "pin_368","constraint": ""},
{"pinId": "pin_369","constraint": ""},
{"pinId": "pin_370","constraint": ""},
{"pinId": "pin_371","constraint": ""},
{"pinId": "pin_372","constraint": ""},
{"pinId": "pin_373","constraint": ""},
{"pinId": "pin_374","constraint": ""},
{"pinId": "pin_375","constraint": ""},
{"pinId": "pin_376","constraint": ""},
{"pinId": "pin_377","constraint": ""},
{"pinId": "pin_378","constraint": ""},
{"pinId": "pin_379","constraint": ""},
{"pinId": "pin_380","constraint": ""},
{"pinId": "pin_381","constraint": ""},
{"pinId": "pin_382","constraint": ""},
{"pinId": "pin_383","constraint": ""},
{"pinId": "pin_384","constraint": ""},
{"pinId": "pin_385","constraint": ""},
{"pinId": "pin_386","constraint": ""},
{"pinId": "pin_387","constraint": ""},
{"pinId": "pin_388","constraint": ""},
{"pinId": "pin_389","constraint": ""},
{"pinId": "pin_390","constraint": ""},
{"pinId": "pin_391","constraint": ""},
{"pinId": "pin_392","constraint": ""},
{"pinId": "pin_393","constraint": ""},
{"pinId": "pin_394","constraint": ""},
{"pinId": "pin_395","constraint": ""},
{"pinId": "pin_396","constraint": ""},
{"pinId": "pin_397","constraint": ""},
{"pinId": "pin_398","constraint": ""},
{"pinId": "pin_399","constraint": ""},
{"pinId": "pin_400","constraint": ""},
{"pinId": "pin_401","constraint": ""},
{"pinId": "pin_402","constraint": ""},
{"pinId": "pin_403","constraint": ""},
{"pinId": "pin_404","constraint": ""},
{"pinId": "pin_405","constraint": ""},
{"pinId": "pin_406","constraint": ""},
{"pinId": "pin_407","constraint": ""},
{"pinId": "pin_408","constraint": ""},
{"pinId": "pin_409","constraint": ""},
{"pinId": "pin_410","constraint": ""},
{"pinId": "pin_411","constraint": ""},
{"pinId": "pin_412","constraint": ""},
{"pinId": "pin_413","constraint": ""},
{"pinId": "pin_414","constraint": ""},
{"pinId": "pin_415","constraint": ""},
{"pinId": "pin_416","constraint": ""},
{"pinId": "pin_417","constraint": ""},
{"pinId": "pin_418","constraint": ""},
{"pinId": "pin_419","constraint": ""},
{"pinId": "pin_420","constraint": ""},
{"pinId": "pin_421","constraint": ""},
{"pinId": "pin_422","constraint": ""},
{"pinId": "pin_423","constraint": ""},
{"pinId": "pin_424","constraint": ""},
{"pinId": "pin_425","constraint": ""},
{"pinId": "pin_426","constraint": ""},
{"pinId": "pin_427","constraint": ""},
{"pinId": "pin_428","constraint": ""},
{"pinId": "pin_429","constraint": ""},
{"pinId": "pin_430","constraint": ""},
{"pinId": "pin_431","constraint": ""},
{"pinId": "pin_432","constraint": ""},
{"pinId": "pin_433","constraint": ""},
{"pinId": "pin_434","constraint": ""},
{"pinId": "pin_435","constraint": ""},
{"pinId": "pin_436","constraint": ""},
{"pinId": "pin_437","constraint": ""},
{"pinId": "pin_438","constraint": ""},
{"pinId": "pin_439","constraint": ""},
{"pinId": "pin_440","constraint": ""},
{"pinId": "pin_441","constraint": ""},
{"pinId": "pin_442","constraint": ""},
{"pinId": "pin_443","constraint": ""},
{"pinId": "pin_444","constraint": ""},
{"pinId": "pin_445","constraint": ""},
{"pinId": "pin_446","constraint": ""},
{"pinId": "pin_447","constraint": ""},
{"pinId": "pin_448","constraint": ""},
{"pinId": "pin_449","constraint": ""},
{"pinId": "pin_450","constraint": ""},
{"pinId": "pin_451","constraint": ""},
{"pinId": "pin_452","constraint": ""},
{"pinId": "pin_453","constraint": ""},
{"pinId": "pin_454","constraint": ""},
{"pinId": "pin_455","constraint": ""},
{"pinId": "pin_456","constraint": ""},
{"pinId": "pin_457","constraint": ""},
{"pinId": "pin_458","constraint": ""},
{"pinId": "pin_459","constraint": ""},
{"pinId": "pin_460","constraint": ""},
{"pinId": "pin_461","constraint": ""},
{"pinId": "pin_462","constraint": ""},
{"pinId": "pin_463","constraint": ""},
{"pinId": "pin_464","constraint": ""},
{"pinId": "pin_465","constraint": ""},
{"pinId": "pin_466","constraint": ""},
{"pinId": "pin_467","constraint": ""},
{"pinId": "pin_468","constraint": ""},
{"pinId": "pin_469","constraint": ""},
{"pinId": "pin_470","constraint": ""},
{"pinId": "pin_471","constraint": ""},
{"pinId": "pin_472","constraint": ""},
{"pinId": "pin_473","constraint": ""},
{"pinId": "pin_474","constraint": ""},
{"pinId": "pin_475","constraint": ""},
{"pinId": "pin_476","constraint": ""},
{"pinId": "pin_477","constraint": ""},
{"pinId": "pin_478","constraint": ""},
{"pinId": "pin_479","constraint": ""},
{"pinId": "pin_480","constraint": ""},
{"pinId": "pin_481","constraint": ""},
{"pinId": "pin_482","constraint": ""},
{"pinId": "pin_483","constraint": ""},
{"pinId": "pin_484","constraint": ""},
{"pinId": "pin_485","constraint": ""},
{"pinId": "pin_486","constraint": ""},
{"pinId": "pin_487","constraint": ""},
{"pinId": "pin_488","constraint": ""},
{"pinId": "pin_489","constraint": ""},
{"pinId": "pin_490","constraint": ""},
{"pinId": "pin_491","constraint": ""},
{"pinId": "pin_492","constraint": ""},
{"pinId": "pin_493","constraint": ""},
{"pinId": "pin_494","constraint": ""},
{"pinId": "pin_495","constraint": ""},
{"pinId": "pin_496","constraint": ""},
{"pinId": "pin_497","constraint": ""},
{"pinId": "pin_498","constraint": ""},
{"pinId": "pin_499","constraint": ""},
{"pinId": "pin_500","constraint": ""},
{"pinId": "pin_501","constraint": ""},
{"pinId": "pin_502","constraint": ""},
{"pinId": "pin_503","constraint": ""},
{"pinId": "pin_504","constraint": ""},
{"pinId": "pin_505","constraint": ""},
{"pinId": "pin_506","constraint": ""},
{"pinId": "pin_507","constraint": ""},
{"pinId": "pin_508","constraint": ""},
{"pinId": "pin_509","constraint": ""},
{"pinId": "pin_510","constraint": ""},
{"pinId": "pin_511","constraint": ""},
{"pinId": "pin_512","constraint": ""},
{"pinId": "pin_513","constraint": ""},
{"pinId": "pin_514","constraint": ""},
{"pinId": "pin_515","constraint": ""},
{"pinId": "pin_516","constraint": ""},
{"pinId": "pin_517","constraint": ""},
{"pinId": "pin_518","constraint": ""},
{"pinId": "pin_519","constraint": ""},
{"pinId": "pin_520","constraint": ""},
{"pinId": "pin_521","constraint": ""},
{"pinId": "pin_522","constraint": ""},
{"pinId": "pin_523","constraint": ""},
{"pinId": "pin_524","constraint": ""},
{"pinId": "pin_525","constraint": ""},
{"pinId": "pin_526","constraint": ""},
{"pinId": "pin_527","constraint": ""},
{"pinId": "pin_528","constraint": ""},
{"pinId": "pin_529","constraint": ""},
{"pinId": "pin_530","constraint": ""},
{"pinId": "pin_531","constraint": ""},
{"pinId": "pin_532","constraint": ""},
{"pinId": "pin_533","constraint": ""},
{"pinId": "pin_534","constraint": ""},
{"pinId": "pin_535","constraint": ""},
{"pinId": "pin_536","constraint": ""},
{"pinId": "pin_537","constraint": ""},
{"pinId": "pin_538","constraint": ""},
{"pinId": "pin_539","constraint": ""},
{"pinId": "pin_540","constraint": ""},
{"pinId": "pin_541","constraint": ""},
{"pinId": "pin_542","constraint": ""},
{"pinId": "pin_543","constraint": ""},
{"pinId": "pin_544","constraint": ""},
{"pinId": "pin_545","constraint": ""},
{"pinId": "pin_546","constraint": ""},
{"pinId": "pin_547","constraint": ""},
{"pinId": "pin_548","constraint": ""},
{"pinId": "pin_549","constraint": ""},
{"pinId": "pin_550","constraint": ""},
{"pinId": "pin_551","constraint": ""},
{"pinId": "pin_552","constraint": ""},
{"pinId": "pin_553","constraint": ""},
{"pinId": "pin_554","constraint": ""},
{"pinId": "pin_555","constraint": ""},
{"pinId": "pin_556","constraint": ""},
{"pinId": "pin_557","constraint": ""},
{"pinId": "pin_558","constraint": ""},
{"pinId": "pin_559","constraint": ""},
{"pinId": "pin_560","constraint": ""},
{"pinId": "pin_561","constraint": ""},
{"pinId": "pin_562","constraint": ""},
{"pinId": "pin_563","constraint": ""},
{"pinId": "pin_564","constraint": ""},
{"pinId": "pin_565","constraint": ""},
{"pinId": "pin_566","constraint": ""},
{"pinId": "pin_567","constraint": ""},
{"pinId": "pin_568","constraint": ""},
{"pinId": "pin_569","constraint": ""},
{"pinId": "pin_570","constraint": ""},
{"pinId": "pin_571","constraint": ""},
{"pinId": "pin_572","constraint": ""},
{"pinId": "pin_573","constraint": ""},
{"pinId": "pin_574","constraint": ""},
{"pinId": "pin_575","constraint": ""},
{"pinId": "pin_576","constraint": ""},
{"pinId": "pin_577","constraint": ""},
{"pinId": "pin_578","constraint": ""},
{"pinId": "pin_579","constraint": ""},
{"pinId": "pin_580","constraint": ""},
{"pinId": "pin_581","constraint": ""},
{"pinId": "pin_582","constraint": ""},
{"pinId": "pin_583","constraint": ""},
{"pinId": "pin_584","constraint": ""},
{"pinId": "pin_585","constraint": ""},
{"pinId": "pin_586","constraint": ""},
{"pinId": "pin_587","constraint": ""},
{"pinId": "pin_588","constraint": ""},
{"pinId": "pin_589","constraint": ""},
{"pinId": "pin_590","constraint": ""},
{"pinId": "pin_591","constraint": ""},
{"pinId": "pin_592","constraint": ""},
{"pinId": "pin_593","constraint": ""},
{"pinId": "pin_594","constraint": ""},
{"pinId": "pin_595","constraint": ""},
{"pinId": "pin_596","constraint": ""},
{"pinId": "pin_597","constraint": ""},
{"pinId": "pin_598","constraint": ""},
{"pinId": "pin_599","constraint": ""},
{"pinId": "pin_600","constraint": ""},
{"pinId": "pin_601","constraint": ""},
{"pinId": "pin_602","constraint": ""},
{"pinId": "pin_603","constraint": ""},
{"pinId": "pin_604","constraint": ""},
{"pinId": "pin_605","constraint": ""},
{"pinId": "pin_606","constraint": ""},
{"pinId": "pin_607","constraint": ""},
{"pinId": "pin_608","constraint": ""},
{"pinId": "pin_609","constraint": ""},
{"pinId": "pin_610","constraint": ""},
{"pinId": "pin_611","constraint": ""},
{"pinId": "pin_612","constraint": ""},
{"pinId": "pin_613","constraint": ""},
{"pinId": "pin_614","constraint": ""},
{"pinId": "pin_615","constraint": ""},
{"pinId": "pin_616","constraint": ""},
{"pinId": "pin_617","constraint": ""},
{"pinId": "pin_618","constraint": ""},
{"pinId": "pin_619","constraint": ""},
{"pinId": "pin_620","constraint": ""},
{"pinId": "pin_621","constraint": ""},
{"pinId": "pin_622","constraint": ""},
{"pinId": "pin_623","constraint": ""},
{"pinId": "pin_624","constraint": ""},
{"pinId": "pin_625","constraint": ""},
{"pinId": "pin_626","constraint": ""},
{"pinId": "pin_627","constraint": ""},
{"pinId": "pin_628","constraint": ""},
{"pinId": "pin_629","constraint": ""},
{"pinId": "pin_630","constraint": ""},
{"pinId": "pin_631","constraint": ""},
{"pinId": "pin_632","constraint": ""},
{"pinId": "pin_633","constraint": ""},
{"pinId": "pin_634","constraint": ""},
{"pinId": "pin_635","constraint": ""},
{"pinId": "pin_636","constraint": ""},
{"pinId": "pin_637","constraint": ""},
{"pinId": "pin_638","constraint": ""},
{"pinId": "pin_639","constraint": ""},
{"pinId": "pin_640","constraint": ""},
{"pinId": "pin_641","constraint": ""},
{"pinId": "pin_642","constraint": ""},
{"pinId": "pin_643","constraint": ""},
{"pinId": "pin_644","constraint": ""},
{"pinId": "pin_645","constraint": ""},
{"pinId": "pin_646","constraint": ""},
{"pinId": "pin_647","constraint": ""},
{"pinId": "pin_648","constraint": ""},
{"pinId": "pin_649","constraint": ""},
{"pinId": "pin_650","constraint": ""},
{"pinId": "pin_651","constraint": ""},
{"pinId": "pin_652","constraint": ""},
{"pinId": "pin_653","constraint": ""},
{"pinId": "pin_654","constraint": ""},
{"pinId": "pin_655","constraint": ""},
{"pinId": "pin_656","constraint": ""},
{"pinId": "pin_657","constraint": ""},
{"pinId": "pin_658","constraint": ""},
{"pinId": "pin_659","constraint": ""},
{"pinId": "pin_660","constraint": ""},
{"pinId": "pin_661","constraint": ""},
{"pinId": "pin_662","constraint": ""},
{"pinId": "pin_663","constraint": ""},
{"pinId": "pin_664","constraint": ""},
{"pinId": "pin_665","constraint": ""},
{"pinId": "pin_666","constraint": ""},
{"pinId": "pin_667","constraint": ""},
{"pinId": "pin_668","constraint": ""},
{"pinId": "pin_669","constraint": ""},
{"pinId": "pin_670","constraint": ""},
{"pinId": "pin_671","constraint": ""},
{"pinId": "pin_672","constraint": ""},
{"pinId": "pin_673","constraint": ""},
{"pinId": "pin_674","constraint": ""},
{"pinId": "pin_675","constraint": ""}
]
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": true,"index": 0
},
{ "id": "hdmi_out","type": "HDMI","x": -40,"y": 135,
"attrs": {
"size": 1.25
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "hdmi_in","type": "HDMI","x": -40,"y": 230,
"attrs": {
"size": 1.25
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "eth0","type": "ETH","x": 0,"y": 350,
"attrs": {
"size": 1.25
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "ddr3","type": "DDR","x": 405,"y": 225,
"attrs": {
"size": 1.25
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "sfp0","type": "SFP","x": 650,"y": 260,
"attrs": {
"size": 1.6
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "sfp1","type": "SFP","x": 650,"y": 340,
"attrs": {
"size": 1.6
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "sma_RXN","type": "SMA","x": 575,"y": 125,
"attrs": {
"size": 0.7
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "sma_RXP","type": "SMA","x": 575,"y": 175,
"attrs": {
"size": 0.7
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "sma_TXN","type": "SMA","x": 575,"y": 260,
"attrs": {
"size": 0.7
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "sma_TXP","type": "SMA","x": 575,"y": 310,
"attrs": {
"size": 0.7
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{
"id": "sd0","type": "SD","x": 540,"y": 410,
"attrs": {
"size": 1
},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": false,"isOn": false,"index": 0
},
{"id": "key0","type": "MechanicalButton","x": 335,"y": 435,
"attrs": {"size": 0.17,"bindKey": "","pins": [{"pinId": "BTN","constraint": "","x": 80,"y": 140}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "key1","type": "MechanicalButton","x": 370,"y": 435,
"attrs": {"size": 0.17,"bindKey": "","pins": [{"pinId": "BTN","constraint": "","x": 80,"y": 140}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "key2","type": "MechanicalButton","x": 405,"y": 435,
"attrs": {"size": 0.17,"bindKey": "","pins": [{"pinId": "BTN","constraint": "","x": 80,"y": 140}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "key3","type": "MechanicalButton","x": 440,"y": 435,
"attrs": {"size": 0.17,"bindKey": "","pins": [{"pinId": "BTN","constraint": "","x": 80,"y": 140}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "key4","type": "MechanicalButton","x": 475,"y": 435,
"attrs": {"size": 0.17,"bindKey": "","pins": [{"pinId": "BTN","constraint": "","x": 80,"y": 140}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "key_rest","type": "MechanicalButton","x": 510,"y": 435,
"attrs": {"size": 0.17,"bindKey": "","pins": [{"pinId": "BTN","constraint": "","x": 80,"y": 140}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led0","type": "SMT_LED","x": 340,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led1","type": "SMT_LED","x": 360,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led2","type": "SMT_LED","x": 380,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led3","type": "SMT_LED","x": 400,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led4","type": "SMT_LED","x": 420,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led5","type": "SMT_LED","x": 440,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led6","type": "SMT_LED","x": 460,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led7","type": "SMT_LED","x": 480,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led_power","type": "SMT_LED","x": 515,"y": 405,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 90,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": true,"index": 0},
{"id": "led_init","type": "SMT_LED","x": 215,"y": 340,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": true,"index": 0},
{"id": "led_done","type": "SMT_LED","x": 215,"y": 330,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": true,"index": 0},
{"id": "led_center_power","type": "SMT_LED","x": 215,"y": 320,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": true,"index": 0},
{"id": "led8","type": "SMT_LED","x": 215,"y": 310,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0},
{"id": "led9","type": "SMT_LED","x": 215,"y": 300,
"attrs": {"size": 0.17,"color": "green","brightness": 80,"pins": [{"pinId": "LED","constraint": "","x": 50,"y": 30}]},
"rotate": 0,"group": "PG2L00H_MotherBoard","positionlock": false,"hidepins": true,"isOn": false,"index": 0}
],
"connections": [
["led_done:LED","FPGA_chip:pin_644",0.5,["right10","downright1","*","upright3"]]
]
}

View File

@@ -2,23 +2,19 @@
<div class="h-screen flex flex-col overflow-hidden"> <div class="h-screen flex flex-col overflow-hidden">
<div class="flex flex-1 overflow-hidden relative"> <div class="flex flex-1 overflow-hidden relative">
<!-- 左侧图形化区域 --> <!-- 左侧图形化区域 -->
<div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }"> <DiagramCanvas <div class="relative bg-base-200 overflow-hidden h-full" :style="{ width: leftPanelWidth + '%' }">
ref="diagramCanvas" :components="components" <DiagramCanvas
:componentModules="componentModules" @component-selected="handleComponentSelected" ref="diagramCanvas"
:componentModules="componentModules"
@component-selected="handleComponentSelected"
@component-moved="handleComponentMoved" @component-moved="handleComponentMoved"
@update-component-prop="updateComponentProp"
@component-delete="handleComponentDelete" @component-delete="handleComponentDelete"
@wire-created="handleWireCreated" @wire-created="handleWireCreated"
@wire-deleted="handleWireDeleted" @wire-deleted="handleWireDeleted"
@diagram-updated="handleDiagramUpdated"
@open-components="openComponentsMenu"
@load-component-module="handleLoadComponentModule"
/> />
<!-- 添加元器件按钮 -->
<button class="btn btn-circle btn-primary absolute top-8 right-8 shadow-lg z-10" @click="openComponentsMenu">
<!-- SVG icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div> </div>
<!-- 拖拽分割线 --> <!-- 拖拽分割线 -->
@@ -28,81 +24,22 @@
></div> ></div>
<!-- 右侧编辑区域 --> <!-- 右侧编辑区域 -->
<div class="bg-base-100 flex flex-col p-4 overflow-auto" :style="{ width: (100 - leftPanelWidth) + '%' }"> <div class="bg-base-100 h-full overflow-hidden flex flex-col" :style="{ width: (100 - leftPanelWidth) + '%' }">
<h3 class="text-lg font-bold mb-4">属性编辑器</h3> <div class="overflow-y-auto p-4 flex-1">
<div v-if="!selectedComponentData" class="text-gray-400">选择元器件以编辑属性</div> <PropertyPanel
<div v-else> :componentData="selectedComponentData"
<div class="mb-4 pb-4 border-b border-base-300"> :componentConfig="selectedComponentConfig"
<h4 class="font-semibold text-lg mb-1">{{ selectedComponentData.name }}</h4> @updateProp="updateComponentProp"
<p class="text-xs text-gray-500">ID: {{ selectedComponentData.id }}</p> @updateDirectProp="updateComponentDirectProp"
<p class="text-xs text-gray-500">类型: {{ selectedComponentData.type }}</p>
</div>
<!-- 动态属性表单 -->
<div v-if="selectedComponentConfig && selectedComponentConfig.props" class="space-y-4">
<div v-for="prop in selectedComponentConfig.props" :key="prop.name" class="form-control">
<label class="label">
<span class="label-text">{{ prop.label || prop.name }}</span>
</label>
<!-- 根据 prop 类型选择输入控件 -->
<input
v-if="prop.type === 'string'"
type="text"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="selectedComponentData.props?.[prop.name]"
@input="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).value)"
/> />
<input
v-else-if="prop.type === 'number'"
type="number"
:placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full"
:value="selectedComponentData.props?.[prop.name]"
@input="updateComponentProp(selectedComponentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
/> <!-- 可以为 boolean 添加 checkbox color 添加 color picker -->
<div v-else-if="prop.type === 'boolean'" class="flex items-center">
<input
type="checkbox"
class="checkbox checkbox-sm mr-2"
:checked="selectedComponentData.props?.[prop.name]"
@change="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).checked)"
/>
<span>{{ prop.label || prop.name }}</span>
</div> <!-- 下拉选择框 -->
<select
v-else-if="prop.type === 'select' && prop.options"
class="select select-bordered select-sm w-full"
:value="selectedComponentData.props?.[prop.name]"
@change="(event) => {
const selectElement = event.target as HTMLSelectElement;
const value = selectElement.value;
console.log('选择的值:', value, '类型:', typeof value);
if (selectedComponentData) {updateComponentProp(selectedComponentData.id, prop.name, value);}
}"
>
<option v-for="option in prop.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<p v-else class="text-xs text-warning">不支持的属性类型: {{ prop.type }}</p>
</div> </div>
</div> </div>
<div v-else-if="selectedComponentData && !selectedComponentConfig" class="text-gray-500 text-sm"> </div> <!-- 元器件选择组件 -->
正在加载组件配置...
</div>
<div v-else-if="selectedComponentData && selectedComponentConfig && (!selectedComponentConfig.props || selectedComponentConfig.props.length === 0)" class="text-gray-500 text-sm">
此组件没有可配置的属性
</div>
</div>
</div> </div>
<!-- 元器件选择组件 -->
<ComponentSelector <ComponentSelector
:open="showComponentsMenu" :open="showComponentsMenu"
@update:open="showComponentsMenu = $event" @update:open="showComponentsMenu = $event"
@add-component="handleAddComponent" @add-component="handleAddComponent"
@add-template="handleAddTemplate"
@close="showComponentsMenu = false" @close="showComponentsMenu = false"
/> />
</div> </div>
@@ -114,42 +51,44 @@
import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // 引入 defineAsyncComponent 和 shallowRef 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 ComponentSelector from '@/components/ComponentSelector.vue';
import { getComponentConfig } from '@/components/equipments/componentConfig'; import PropertyPanel from '@/components/PropertyPanel.vue';
import type { ComponentConfig } from '@/components/equipments/componentConfig'; import type { DiagramData, DiagramPart, ConnectionArray } from '@/components/diagramManager';
import { validateDiagramData } from '@/components/diagramManager';
import {
type PropertyConfig,
generatePropertyConfigs,
generatePropsFromDefault,
generatePropsFromAttrs,
getPropValue
} from '@/components/equipments/componentConfig'; // 引入组件配置工具
// --- 元器件管理 --- // --- 元器件管理 ---
const showComponentsMenu = ref(false); const showComponentsMenu = ref(false);
interface ComponentItem { const diagramData = ref<DiagramData>({
id: string; version: 1,
type: string; // 现在是组件的文件名或标识符,例如 'MechanicalButton' author: 'admin',
name: string; editor: 'me',
x: number; parts: [],
y: number; connections: []
props?: Record<string, any>; // 添加 props 字段来存储组件实例的属性 });
}
const components = ref<ComponentItem[]>([]); const selectedComponentId = ref<string | null>(null);
const selectedComponentId = ref<string | null>(null); // 重命名为 selectedComponentId const selectedComponentData = computed(() => {
const selectedComponentData = computed(() => { // 改为计算属性 return diagramData.value.parts.find(p => p.id === selectedComponentId.value) || null;
return components.value.find(c => c.id === selectedComponentId.value) || null;
}); });
const diagramCanvas = ref(null); const diagramCanvas = ref(null);
// 存储动态导入的组件模块 // 存储动态导入的组件模块
interface ComponentModule { interface ComponentModule {
default: any; default: any;
getDefaultProps?: () => Record<string, any>;
config?: { config?: {
props?: Array<{ props?: Array<PropertyConfig>;
name: string;
type: string;
label?: string;
default: any;
options?: Array<{ value: any; label: string }>; // 添加 options 字段用于 select 类型
}>;
}; };
} }
const componentModules = shallowRef<Record<string, ComponentModule>>({}); const componentModules = shallowRef<Record<string, ComponentModule>>({});
const selectedComponentConfig = shallowRef<ComponentModule['config'] | null>(null); // 存储选中组件的配置 const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(null); // 存储选中组件的配置
// 动态加载组件定义 // 动态加载组件定义
async function loadComponentModule(type: string) { async function loadComponentModule(type: string) {
@@ -173,6 +112,12 @@ async function loadComponentModule(type: string) {
return componentModules.value[type]; return componentModules.value[type];
} }
// 处理组件模块加载请求
async function handleLoadComponentModule(type: string) {
console.log('Handling load component module request for:', type);
await loadComponentModule(type);
}
// --- 分割面板 --- // --- 分割面板 ---
const leftPanelWidth = ref(60); const leftPanelWidth = ref(60);
const isResizing = ref(false); const isResizing = ref(false);
@@ -224,110 +169,296 @@ async function handleAddComponent(componentData: { type: string; name: string; p
// 获取画布容器和位置信息 // 获取画布容器和位置信息
const canvasInstance = diagramCanvas.value as any; const canvasInstance = diagramCanvas.value as any;
// 默认位置(当无法获取画布信息时使用) // 获取当前画布的位置信息
let posX = 100; let position = { x: 100, y: 100 };
let posY = 100; let scale = 1;
try { try {
if (canvasInstance) { if (canvasInstance && canvasInstance.getCanvasPosition && canvasInstance.getScale) {
position = canvasInstance.getCanvasPosition();
scale = canvasInstance.getScale();
// 获取画布容器 // 获取画布容器
const canvasContainer = canvasInstance.$el as HTMLElement; const canvasContainer = canvasInstance.$el as HTMLElement;
if (canvasContainer) { if (canvasContainer) {
// 获取当前画布的位置和缩放信息
const canvasPosition = canvasInstance.getCanvasPosition ?
canvasInstance.getCanvasPosition() :
{ x: 0, y: 0 };
const scale = canvasInstance.getScale ?
canvasInstance.getScale() :
1;
// 计算可视区域中心点在画布坐标系中的位置 // 计算可视区域中心点在画布坐标系中的位置
const viewportWidth = canvasContainer.clientWidth; const viewportWidth = canvasContainer.clientWidth;
const viewportHeight = canvasContainer.clientHeight; const viewportHeight = canvasContainer.clientHeight;
// 计算画布中心点的坐标 // 计算画布中心点的坐标
posX = (viewportWidth / 2 - canvasPosition.x) / scale; position.x = (viewportWidth / 2 - position.x) / scale;
posY = (viewportHeight / 2 - canvasPosition.y) / scale; position.y = (viewportHeight / 2 - position.y) / scale;
} }
} }
} catch (error) { } catch (error) {
console.error('Error getting canvas position:', error); console.error('获取画布位置时出错:', error);
// 使用默认位置
} }
// 添加一些随机偏移,避免元器件重叠 // 添加一些随机偏移,避免元器件重叠
const offsetX = Math.floor(Math.random() * 100) - 50; const offsetX = Math.floor(Math.random() * 100) - 50;
const offsetY = Math.floor(Math.random() * 100) - 50; const offsetY = Math.floor(Math.random() * 100) - 50;
const newComponent: ComponentItem = { // 创建新组件使用diagramManager接口定义
const newComponent: DiagramPart = {
id: `component-${Date.now()}`, id: `component-${Date.now()}`,
type: componentData.type, type: componentData.type,
name: componentData.name, x: Math.round(position.x + offsetX),
x: Math.round(posX + offsetX), y: Math.round(position.y + offsetY),
y: Math.round(posY + offsetY), attrs: componentData.props,
props: componentData.props, // 使用从 ComponentSelector 传递的默认属性 rotate: 0,
group: '',
positionlock: false,
hidepins: true,
isOn: true,
index: 0
}; };
components.value.push(newComponent); console.log('添加新组件:', newComponent);
// 通过画布实例添加组件
if (canvasInstance && canvasInstance.getDiagramData && canvasInstance.updateDiagramDataDirectly) {
const currentData = canvasInstance.getDiagramData();
currentData.parts.push(newComponent);
canvasInstance.updateDiagramDataDirectly(currentData);
}
}
// 处理模板添加事件
async function handleAddTemplate(templateData: { id: string; name: string; template: any }) {
console.log('添加模板:', templateData);
console.log('=== 模板组件数量:', templateData.template?.parts?.length || 0);
// 获取画布实例
const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) {
console.error('没有可用的画布实例添加模板');
return;
}
// 获取当前图表数据
const currentData = canvasInstance.getDiagramData();
console.log('=== 当前图表组件数量:', currentData.parts.length);
// 生成唯一ID前缀以确保添加的组件ID不重复
const idPrefix = `template-${Date.now()}-`;
// 处理模板组件并添加到图表
if (templateData.template && templateData.template.parts) {
// 获取当前视口中心位置
let viewportCenter = { x: 300, y: 200 }; // 默认值
try {
if (canvasInstance && canvasInstance.getCanvasPosition && canvasInstance.getScale) {
const position = canvasInstance.getCanvasPosition();
const scale = canvasInstance.getScale();
// 获取画布容器
const canvasContainer = canvasInstance.$el as HTMLElement;
if (canvasContainer) {
// 计算可视区域中心点在画布坐标系中的位置
const viewportWidth = canvasContainer.clientWidth;
const viewportHeight = canvasContainer.clientHeight;
// 计算视口中心点的坐标 (与handleAddComponent函数中的方法相同)
viewportCenter.x = (viewportWidth / 2 - position.x) / scale;
viewportCenter.y = (viewportHeight / 2 - position.y) / scale;
console.log(`=== 计算的视口中心: x=${viewportCenter.x}, y=${viewportCenter.y}, scale=${scale}`);
}
}
} catch (error) {
console.error('获取视口中心位置时出错:', error);
}
console.log('=== 使用视口中心添加模板组件:', viewportCenter);
// 找到模板中的主要组件(假设是第一个组件)
const mainPart = templateData.template.parts[0];
// 创建带有新位置的组件
const newParts = templateData.template.parts.map((part: any) => {
// 创建组件副本并分配新ID
const newPart = JSON.parse(JSON.stringify(part));
newPart.id = `${idPrefix}${part.id}`;
// 计算相对于主要组件的偏移量,保持模板内部组件的相对位置关系
if (typeof newPart.x === 'number' && typeof newPart.y === 'number') {
const oldX = newPart.x;
const oldY = newPart.y;
// 计算相对位置(相对于主要组件)
const relativeX = part.x - mainPart.x;
const relativeY = part.y - mainPart.y;
// 应用到视口中心位置
newPart.x = viewportCenter.x + relativeX;
newPart.y = viewportCenter.y + relativeY;
console.log(`=== 组件[${newPart.id}]位置调整: (${oldX},${oldY}) -> (${newPart.x},${newPart.y})`);
}
return newPart;
});
// 向图表添加新组件
currentData.parts.push(...newParts);
// 处理连接关系
if (templateData.template.connections) {
// 创建一个映射表用于转换旧组件ID到新组件ID
const idMap: Record<string, string> = {};
templateData.template.parts.forEach((part: any) => {
idMap[part.id] = `${idPrefix}${part.id}`;
});
// 添加连接更新组件ID引用
const newConnections = templateData.template.connections.map((conn: any) => {
// 处理连接数据 (格式为 [from, to, type, path])
if (Array.isArray(conn)) {
const [from, to, type, path] = conn;
// 从连接字符串中提取组件ID和引脚ID
const fromParts = from.split(':');
const toParts = to.split(':');
if (fromParts.length === 2 && toParts.length === 2) {
const fromComponentId = fromParts[0];
const fromPinId = fromParts[1];
const toComponentId = toParts[0];
const toPinId = toParts[1];
// 创建新的连接字符串使用新的组件ID
const newFrom = `${idMap[fromComponentId] || fromComponentId}:${fromPinId}`;
const newTo = `${idMap[toComponentId] || toComponentId}:${toPinId}`;
return [newFrom, newTo, type, path];
}
}
return conn; // 如果格式不匹配,保持原样
});
// 添加到当前连接列表
currentData.connections.push(...newConnections);
}
// 更新图表数据
canvasInstance.updateDiagramDataDirectly(currentData);
console.log('=== 更新图表数据完成,新组件数量:', currentData.parts.length);
// 显示成功消息
showToast(`已添加 ${templateData.name} 模板`, 'success');
} else {
console.error('模板格式错误缺少parts数组');
showToast('模板格式错误', 'error');
}
} }
// 处理组件选中事件 // 处理组件选中事件
async function handleComponentSelected(componentData: ComponentItem | null) { async function handleComponentSelected(componentData: DiagramPart | null) {
selectedComponentId.value = componentData ? componentData.id : null; selectedComponentId.value = componentData ? componentData.id : null;
selectedComponentConfig.value = null; // 重置配置 selectedComponentConfig.value = null; // 重置配置
if (componentData) { if (componentData) {
// 从配置文件中获取组件配置 // 先加载组件模块
const config = getComponentConfig(componentData.type); const moduleRef = await loadComponentModule(componentData.type);
if (config) {
selectedComponentConfig.value = config; if (moduleRef) {
console.log(`Config for ${componentData.type}:`, config); try {
// 使用组件配置工具创建配置项
const propConfigs: PropertyConfig[] = [];
// 1. 自动获取组件属性信息 - 利用DiagramPart接口结构
const directPropConfigs = generatePropertyConfigs(componentData);
propConfigs.push(...directPropConfigs);
// 2. 尝试使用组件导出的getDefaultProps方法获取配置
if (typeof moduleRef.getDefaultProps === 'function') {
// 从getDefaultProps方法构建配置
const defaultProps = moduleRef.getDefaultProps();
const defaultPropConfigs = generatePropsFromDefault(defaultProps);
propConfigs.push(...defaultPropConfigs);
selectedComponentConfig.value = { props: propConfigs };
console.log(`Built config for ${componentData.type} from getDefaultProps:`, selectedComponentConfig.value);
} else { } else {
console.warn(`No config found for component type ${componentData.type}`); console.warn(`Component ${componentData.type} does not export getDefaultProps method.`);
// 创建一个空配置,只显示组件提供的属性
const attrs = componentData.attrs || {};
const attrPropConfigs = generatePropsFromAttrs(attrs);
propConfigs.push(...attrPropConfigs);
selectedComponentConfig.value = { props: propConfigs };
}
} catch (error) {
console.error(`Error building config for ${componentData.type}:`, error);
selectedComponentConfig.value = { props: [] };
}
} else {
console.warn(`Module for component ${componentData.type} not found.`);
selectedComponentConfig.value = { props: [] };
}
}
} }
// 同时加载组件模块以备用 // 处理图表数据更新事件
await loadComponentModule(componentData.type); function handleDiagramUpdated(data: DiagramData) {
} diagramData.value = data;
console.log('Diagram data updated:', data);
} }
// 处理组件移动事件 // 处理组件移动事件
function handleComponentMoved(moveData: { id: string; x: number; y: number }) { function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
const component = components.value.find(c => c.id === moveData.id); const part = diagramData.value.parts.find(p => p.id === moveData.id);
if (component) { if (part) {
component.x = moveData.x; part.x = moveData.x;
component.y = moveData.y; part.y = moveData.y;
} }
} }
// 处理组件删除事件 // 处理组件删除事件
function handleComponentDelete(componentId: string) { function handleComponentDelete(componentId: string) {
// 查找要删除的组件索引 // 查找要删除的组件
const index = components.value.findIndex(c => c.id === componentId); const component = diagramData.value.parts.find(p => p.id === componentId);
if (index !== -1) { if (!component) return;
// 从数组中移除该组件
components.value.splice(index, 1); // 收集需要删除的组件ID列表包括当前组件和同组组件
const componentsToDelete: string[] = [componentId];
// 如果组件属于一个组,则找出所有同组的组件
if (component.group && component.group !== '') {
const groupMembers = diagramData.value.parts.filter(
p => p.group === component.group && p.id !== componentId
);
// 将同组组件ID添加到删除列表
componentsToDelete.push(...groupMembers.map(p => p.id));
console.log(`删除组件 ${componentId} 及其组 ${component.group} 中的 ${groupMembers.length} 个组件`);
}
// 删除所有标记的组件
diagramData.value.parts = diagramData.value.parts.filter(
p => !componentsToDelete.includes(p.id)
);
// 同时删除与这些组件相关的所有连接
diagramData.value.connections = diagramData.value.connections.filter(
connection => {
for (const id of componentsToDelete) {
if (connection[0].startsWith(`${id}:`) || connection[1].startsWith(`${id}:`)) {
return false;
}
}
return true;
}
);
// 如果删除的是当前选中的组件,清除选中状态 // 如果删除的是当前选中的组件,清除选中状态
if (selectedComponentId.value === componentId) { if (selectedComponentId.value && componentsToDelete.includes(selectedComponentId.value)) {
selectedComponentId.value = null; selectedComponentId.value = null;
selectedComponentConfig.value = null; selectedComponentConfig.value = null;
} }
} }
}
// 更新组件属性的方法,处理字符串类型的初始值特殊格式 // 更新组件属性的方法
function updateComponentProp(componentId: string | { id: string; propName: string; value: any }, propName?: string, value?: any) { function updateComponentProp(componentId: string, propName: string, value: any) {
// 处理来自 DiagramCanvas 的事件 const canvasInstance = diagramCanvas.value as any;
if (typeof componentId === 'object') { if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) {
const { id, propName: name, value: val } = componentId; console.error('没有可用的画布实例进行属性更新');
componentId = id; return;
propName = name;
value = val;
}
const component = components.value.find(c => c.id === componentId);
if (component && propName !== undefined) {
if (!component.props) {
component.props = {};
} }
// 检查值是否为对象如果是对象并有value属性则使用该属性值 // 检查值是否为对象如果是对象并有value属性则使用该属性值
@@ -335,29 +466,117 @@ function updateComponentProp(componentId: string | { id: string; propName: strin
value = value.value; value = value.value;
} }
// 直接更新属性值 const currentData = canvasInstance.getDiagramData();
component.props[propName] = value; const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
console.log(`Updated ${componentId} prop ${propName} to:`, value, typeof value); if (part) {
// 检查是否为基本属性
if (propName in part) {
(part as any)[propName] = value;
} else {
// 否则当作attrs中的属性处理
if (!part.attrs) {
part.attrs = {};
}
part.attrs[propName] = value;
}
canvasInstance.updateDiagramDataDirectly(currentData);
console.log(`更新组件${componentId}的属性${propName}为:`, value, typeof value);
} }
} }
// 更新组件的直接属性
function updateComponentDirectProp(componentId: string, propName: string, value: any) {
const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.updateDiagramDataDirectly) {
console.error('没有可用的画布实例进行属性更新');
return;
}
const currentData = canvasInstance.getDiagramData();
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
if (part) {
// @ts-ignore: 动态属性赋值
part[propName] = value;
canvasInstance.updateDiagramDataDirectly(currentData);
console.log(`更新组件${componentId}的直接属性${propName}为:`, value, typeof value);
}
}
// 专门用于更新组件的显示层级 - 这个方法可以删除直接使用updateComponentProp即可
// 处理连线创建事件 // 处理连线创建事件
function handleWireCreated(wireData: any) { function handleWireCreated(wireData: any) {
console.log('Wire created:', wireData); console.log('Wire created:', wireData);
// 连线已在DiagramCanvas.vue中完成约束处理
} }
// 处理连线删除事件 // 处理连线删除事件
function handleWireDeleted(wireId: string) { function handleWireDeleted(wireId: string) {
console.log('Wire deleted:', wireId); console.log('Wire deleted:', wireId);
// 可以在这里添加连线删除的相关逻辑
} }
// 导出当前diagram数据
function exportDiagram() {
// 直接使用DiagramCanvas组件提供的导出功能
const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.exportDiagram) {
canvasInstance.exportDiagram();
}
}
// --- 消息提示 ---
const showNotification = ref(false);
const notificationMessage = ref('');
const notificationType = ref<'success' | 'error' | 'info'>('info');
function showToast(message: string, type: 'success' | 'error' | 'info' = 'info', duration = 3000) { const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.showToast) {
canvasInstance.showToast(message, type, duration);
} else {
// 后备方案:使用原来的通知系统
notificationMessage.value = message;
notificationType.value = type;
showNotification.value = true;
// 设置自动消失
setTimeout(() => {
showNotification.value = false;
}, duration);
}
}
// 显示通知
// --- 组件属性处理辅助函数 ---
// 直接使用 componentConfig.ts 中导入的 getPropValue 函数
// --- 生命周期钩子 --- // --- 生命周期钩子 ---
onMounted(() => { onMounted(async () => {
// 初始化画布设置 // 初始化画布设置
console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value); console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value);
// 获取初始图表数据
const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.getDiagramData) {
diagramData.value = canvasInstance.getDiagramData();
// 预加载所有使用的组件模块,以确保它们在渲染时可用
const componentTypes = new Set<string>();
diagramData.value.parts.forEach(part => {
componentTypes.add(part.type);
});
console.log('Preloading component modules:', Array.from(componentTypes));
// 并行加载所有组件模块
await Promise.all(
Array.from(componentTypes).map(type => loadComponentModule(type))
);
console.log('All component modules loaded');
}
}); });
onUnmounted(() => { onUnmounted(() => {
@@ -374,7 +593,7 @@ onUnmounted(() => {
.resizer { .resizer {
width: 6px; width: 6px;
height: 100%; height: 100%;
background-color: var(--b3); background-color: hsl(var(--b3));
cursor: col-resize; cursor: col-resize;
transition: background-color 0.3s; transition: background-color 0.3s;
z-index: 10; z-index: 10;
@@ -404,4 +623,12 @@ onUnmounted(() => {
transform: translateX(0); transform: translateX(0);
} }
} }
/* 确保滚动行为仅在需要时出现 */
html, body {
overflow: hidden;
height: 100%;
margin: 0;
padding: 0;
}
</style> </style>