Compare commits

..

2 Commits
master ... dpp

Author SHA1 Message Date
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
10 changed files with 1457 additions and 890 deletions

View File

@ -101,7 +101,6 @@
<script setup lang="ts">
import { ref, computed, shallowRef, onMounted } from 'vue';
import { getComponentConfig } from '@/components/equipments/componentConfig';
// Props
interface Props {
@ -208,18 +207,22 @@ function closeMenu() {
//
async function addComponent(componentTemplate: { type: string; name: string }) {
//
const config = getComponentConfig(componentTemplate.type);
const defaultProps: Record<string, any> = {};
//
const moduleRef = await loadComponentModule(componentTemplate.type);
let defaultProps: Record<string, any> = {};
if (config && config.props) {
config.props.forEach(prop => {
defaultProps[prop.name] = prop.default;
});
// getDefaultProps
if(moduleRef){
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', {

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,309 @@
// 定义 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;
hide: boolean;
isOn: boolean;
}
// 连接类型定义 - 使用元组类型表示四元素数组
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 {
return {
...data,
parts: data.parts.filter(part => part.id !== partId),
// 同时删除与此组件相关的所有连接
connections: data.connections.filter(conn => {
const [startPin, endPin] = conn;
const startCompId = startPin.split(':')[0];
const endCompId = endPin.split(':')[0];
return startCompId !== partId && endCompId !== partId;
})
};
}
// 添加连接
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

@ -79,7 +79,6 @@
ref="pinRef"
direction="output"
type="digital"
:label="props.label"
:constraint="props.constraint"
:size="0.8"
:componentId="props.componentId"
@ -99,7 +98,6 @@ const pinRef = ref<any>(null);
// Pin
interface PinProps {
label?: string;
constraint?: string;
componentId?: string; // componentId
// 便
@ -121,7 +119,6 @@ const props = withDefaults(defineProps<Props>(), {
size: 1,
bindKey: '',
buttonText: '',
label: 'BTN',
constraint: '',
componentId: 'button-default', // componentId
//
@ -142,7 +139,6 @@ const displayText = computed(() => {
//
const emit = defineEmits([
'update:bindKey',
'update:label',
'update:constraint',
'press',
'release',
@ -212,8 +208,6 @@ defineExpose({
//
bindKey: props.bindKey,
buttonText: props.buttonText,
// Pin
label: props.label,
constraint: props.constraint,
componentId: props.componentId, // componentId
// Pin
@ -230,6 +224,25 @@ defineExpose({
});
</script>
<script lang="ts">
// props
export function getDefaultProps() {
return {
size: 1,
bindKey: '',
buttonText: '',
pins: [
{
pinId: 'BTN',
constraint: '',
x: 80,
y: 140
}
]
};
}
</script>
<style scoped>
.button-container {
display: flex;

View File

@ -4,20 +4,17 @@
:height="height"
:viewBox="'0 0 ' + viewBoxWidth + ' ' + viewBoxHeight"
class="pin-component"
:data-component-id="props.componentId"
:data-pin-label="props.label"
>
<g :transform="`translate(${viewBoxWidth/2}, ${viewBoxHeight/2})`">
<g>
<g transform="translate(-12.5, -12.5)">
<circle
<g transform="translate(-12.5, -12.5)"> <circle
:style="{ fill: pinColor }"
cx="12.5"
cy="12.5"
r="3.75"
class="interactive"
@click.stop="handlePinClick"
:data-pin-element="`${props.componentId}`" />
:data-pin-element="`${props.pinId}`" />
</g>
</g>
</g>
@ -28,16 +25,13 @@
import { ref, computed, reactive, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintColor, getConstraintState, onConstraintStateChange, notifyConstraintChange } from '../../stores/constraints';
// ID
const uniqueId = `pin-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
interface Props {
size?: number;
label?: string;
constraint?: string;
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
componentId?: string; // ID
pinId?: string; // ID
}
const props = withDefaults(defineProps<Props>(), {
@ -46,7 +40,7 @@ const props = withDefaults(defineProps<Props>(), {
constraint: '',
direction: 'input',
type: 'digital',
componentId: 'pin-default' // ID
pinId: 'pin-default', // ID
});
const emit = defineEmits([
@ -136,49 +130,51 @@ function updateAnalogValue(value: number) {
defineExpose({
setAnalogValue: updateAnalogValue,
getAnalogValue: () => analogValue.value,
getInfo: () => ({
getAnalogValue: () => analogValue.value, getInfo: () => ({
label: props.label,
constraint: props.constraint,
direction: props.direction,
type: props.type,
componentId: props.componentId
pinId: props.pinId
}),
getPinPosition: (componentId: string) => {
if (componentId !== props.componentId) return null;
console.log('getPinPosition', componentId, props.componentId);
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();
getPinPosition: () => {
// Pin
const circle = document.querySelector(`circle[data-pin-element="${props.pinId}"]`);
if (circle) {
const rect = circle.getBoundingClientRect();
return {
x: rect.left + rect.width / 2,
y: rect.top + rect.height / 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
};
return null;
}
});
</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>
.pin-component {
display: block;

View File

@ -39,13 +39,19 @@
ry="18"
filter="blur(5px)"
class="glow"
/>
</svg>
<!-- 新增数字输入引脚Pin放在LED左侧居中 -->
<div style="position:absolute;left:-18px;top:50%;transform:translateY(-50%);">
<Pin
ref="pinRef"
v-bind="props"
/> </svg>
<!-- 渲染自定义引脚数组 -->
<div v-for="pin in props.pins" :key="pin.pinId"
:style="{
position: 'absolute',
left: `${pin.x}px`,
top: `${pin.y}px`,
transform: 'translate(-50%, -50%)'
}"> <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>
@ -57,34 +63,36 @@ import { ref, computed, watch, onMounted, onUnmounted } from 'vue';
import { getConstraintState, onConstraintStateChange } from '../../stores/constraints';
import Pin from './Pin.vue';
// --- getPinPositionPin ---
const pinRef = ref<any>(null);
// Pin
interface PinProps {
label?: string;
constraint?: string;
componentId?: string; // componentId
// 便
direction?: 'input' | 'output' | 'inout';
type?: 'digital' | 'analog';
}
// Pin
const pinRefs = ref<Record<string, any>>({});
// LED
interface LEDProps {
size?: number;
color?: string;
initialOn?: boolean;
brightness?: number;
pins?: {
pinId: string;
constraint: string;
x: number;
y: number;
}[];
}
//
interface Props extends PinProps, LEDProps {}
const props = withDefaults(defineProps<Props>(), {
const props = withDefaults(defineProps<LEDProps>(), {
size: 1,
color: 'red',
initialOn: false,
brightness: 80,
pins: () => [
{
pinId: 'LED',
constraint: '',
label: 'LED',
componentId: ''
x: 50,
y: 30
}
]
});
const width = computed(() => 100 * props.size);
@ -106,18 +114,26 @@ const ledColor = computed(() => {
return colorMap[props.color.toLowerCase()] || props.color;
});
// LEDconstraint
const ledConstraint = computed(() => {
if (props.pins && props.pins.length > 0) {
return props.pins[0].constraint;
}
return '';
});
//
let unsubscribe: (() => void) | null = null;
onMounted(() => {
if (props.constraint) {
if (ledConstraint.value) {
unsubscribe = onConstraintStateChange((constraint, level) => {
if (constraint === props.constraint) {
if (constraint === ledConstraint.value) {
isOn.value = (level === 'high');
}
});
// LED
const currentState = getConstraintState(props.constraint);
const currentState = getConstraintState(ledConstraint.value);
isOn.value = (currentState === 'high');
}
});
@ -128,7 +144,7 @@ onUnmounted(() => {
}
});
watch(() => props.constraint, (newConstraint) => {
watch(() => ledConstraint.value, (newConstraint) => {
if (unsubscribe) {
unsubscribe();
unsubscribe = null;
@ -149,20 +165,49 @@ defineExpose({
getInfo: () => ({
color: props.color,
isOn: isOn.value,
constraint: props.constraint,
componentId: props.componentId,
constraint: ledConstraint.value,
direction: 'input',
type: 'digital'
}),
getPinPosition: (componentId: string) => {
if (pinRef.value && pinRef.value.getPinPosition) {
return pinRef.value.getPinPosition(componentId);
type: 'digital',
pins: props.pins
}), getPinPosition: (pinId: string) => {
// ID
if (props.pins && props.pins.length > 0) {
console.log('Pin ID:', pinId);
const customPin = props.pins.find(p => p.pinId === pinId);
console.log('Custom Pin:', customPin);
console.log('Pin Refs:', pinRefs.value[pinId]);
if (customPin) {
// PingetPinPosition
return {
x: customPin.x,
y: customPin.y
}
return null;
} return null;
} return null;
}
});
</script>
<script lang="ts">
// props
export function getDefaultProps() {
return {
size: 1,
color: 'red',
initialOn: false,
brightness: 80,
pins: [
{
pinId: 'LED',
constraint: '',
x: 50,
y: 30
}
]
};
}
</script>
<style scoped>
.led-container {
display: flex;

View File

@ -33,16 +33,18 @@ interface Props {
strokeColor?: string;
strokeWidth?: number;
isActive?: boolean;
routingMode?: 'auto' | 'orthogonal' | 'direct';
routingMode?: 'auto' | 'orthogonal' | 'direct' | 'path';
//
startComponentId?: string;
startPinLabel?: string;
startPinId?: string;
endComponentId?: string;
endPinLabel?: string;
endPinId?: string;
//
constraint?: string;
//
showLabel?: boolean;
// - diagram.json线
pathCommands?: string[];
}
const props = withDefaults(defineProps<Props>(), {
@ -81,9 +83,168 @@ const labelPosition = 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);
});
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).reverse();
//
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 (const cmd of endCommands) {
const { newX, newY } = executePathCommand(endCurrentX, endCurrentY, cmd);
endCurrentX = newX;
endCurrentY = newY;
endPathPoints.push([endCurrentX, endCurrentY]);
}
//
const reversedEndPoints = endPathPoints.slice(1).reverse();
//
const allPoints = [...pathPoints, ...reversedEndPoints];
// 线
if (allPoints.length >= 2) {
const startFinalPoint = allPoints[pathPoints.length - 1];
const endFirstPoint = allPoints[pathPoints.length];
if (startFinalPoint && endFirstPoint &&
(startFinalPoint[0] !== endFirstPoint[0] || startFinalPoint[1] !== endFirstPoint[1])) {
// - 使
const middlePoints = generateOrthogonalConnection(
startFinalPoint[0], startFinalPoint[1],
endFirstPoint[0], endFirstPoint[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"
if (command.startsWith('right')) {
const distance = parseInt(command.substring(5), 10) || 10;
return { newX: x + distance, newY: y };
} else if (command.startsWith('left')) {
const distance = parseInt(command.substring(4), 10) || 10;
return { newX: x - distance, newY: y };
} else if (command.startsWith('down')) {
if (command.startsWith('downright')) {
const distance = parseInt(command.substring(9), 10) || 10;
return { newX: x + distance, newY: y + distance };
} else if (command.startsWith('downleft')) {
const distance = parseInt(command.substring(8), 10) || 10;
return { newX: x - distance, newY: y + distance };
} else {
const distance = parseInt(command.substring(4), 10) || 10;
return { newX: x, newY: y + distance };
}
} else if (command.startsWith('up')) {
if (command.startsWith('upright')) {
const distance = parseInt(command.substring(7), 10) || 10;
return { newX: x + distance, newY: y - distance };
} else if (command.startsWith('upleft')) {
const distance = parseInt(command.substring(6), 10) || 10;
return { newX: x - distance, newY: y - distance };
} else {
const distance = parseInt(command.substring(2), 10) || 10;
return { newX: x, newY: y - distance };
}
}
//
return { newX: x, newY: y };
}
//
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) {
//
const dx = endX - startX;
@ -145,13 +306,12 @@ watch(() => props.constraint, (newConstraint, oldConstraint) => {
});
// 线
defineExpose({ id: props.id,
getInfo: () => ({
defineExpose({ id: props.id, getInfo: () => ({
id: props.id,
startComponentId: props.startComponentId,
startPinLabel: props.startPinLabel,
startPinId: props.startPinId,
endComponentId: props.endComponentId,
endPinLabel: props.endPinLabel,
endPinId: props.endPinId,
constraint: props.constraint
}),
// 线

View File

@ -1,363 +0,0 @@
// 组件配置声明
export type PropType = 'string' | 'number' | 'boolean' | 'select';
// 定义选择类型选项
export interface PropOption {
value: string | number | boolean;
label: string;
}
export interface PropConfig {
name: string;
type: string;
label: string;
default: any;
min?: number;
max?: number;
step?: number;
options?: PropOption[];
description?: string;
category?: string; // 用于在UI中分组属性
}
export interface ComponentConfig {
props: PropConfig[];
}
// 存储所有组件的配置
const componentConfigs: Record<string, ComponentConfig> = {
MechanicalButton: {
props: [
{
name: 'bindKey',
type: 'string',
label: '绑定按键',
default: '',
description: '触发按钮按下的键盘按键'
},
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '按钮的相对大小1代表标准大小'
},
{
name: 'buttonText',
type: 'string',
label: '按钮文本',
default: '',
description: '按钮上显示的自定义文本,优先级高于绑定按键'
},
{
name: 'label',
type: 'string',
label: '引脚标签',
default: 'BTN',
description: '引脚的标签文本'
},
{
name: 'constraint',
type: 'string',
label: '引脚约束',
default: '',
description: '相同约束字符串的引脚将被视为有电气连接'
}
]
},
Switch: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '开关的相对大小1代表标准大小'
},
{
name: 'switchCount',
type: 'number',
label: '开关数量',
default: 6,
min: 1,
max: 12,
step: 1,
description: '可翻转开关的数量'
},
{
name: 'showLabels',
type: 'boolean',
label: '显示标签',
default: true,
description: '是否显示开关编号标签'
},
{
name: 'initialValues',
type: 'string',
label: '初始状态',
default: '',
description: '开关的初始状态格式为逗号分隔的0/1如"1,0,1"表示第1、3个开关打开'
}
]
},
Pin: {
props: [
{
name: 'size',
type: 'number',
label: '大小',
default: 1,
min: 0.5,
max: 3,
step: 0.1,
description: '引脚的相对大小1代表标准大小'
},
{
name: 'label',
type: 'string',
label: '引脚标签',
default: 'PIN',
description: '用于标识引脚的名称'
},
{
name: 'constraint',
type: 'string',
label: '引脚约束',
default: '',
description: '相同约束字符串的引脚将被视为有电气连接'
},
{
name: 'direction',
type: 'select',
label: '输入/输出特性',
default: 'input',
options: [
{ value: 'input', label: '输入' },
{ value: 'output', label: '输出' },
{ value: 'inout', label: '双向' }
],
description: '引脚的输入/输出特性'
},
{
name: 'type',
type: 'select',
label: '模数特性',
default: 'digital',
options: [
{ value: 'digital', label: 'digital' },
{ value: 'analog', label: 'analog' }
],
description: '引脚的模数特性,数字或模拟'
}
]
},
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 {
return componentConfigs[type] || null;
}

View File

@ -3,22 +3,17 @@
<div class="flex flex-1 overflow-hidden relative">
<!-- 左侧图形化区域 -->
<div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }"> <DiagramCanvas
ref="diagramCanvas" :components="components"
:componentModules="componentModules" @component-selected="handleComponentSelected"
ref="diagramCanvas"
:componentModules="componentModules"
@component-selected="handleComponentSelected"
@component-moved="handleComponentMoved"
@update-component-prop="updateComponentProp"
@component-delete="handleComponentDelete"
@wire-created="handleWireCreated"
@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>
<!-- 拖拽分割线 -->
@ -33,9 +28,9 @@
<div v-if="!selectedComponentData" 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">{{ selectedComponentData.name }}</h4>
<p class="text-xs text-gray-500">ID: {{ selectedComponentData.id }}</p>
<p class="text-xs text-gray-500">类型: {{ selectedComponentData.type }}</p>
<h4 class="font-semibold text-lg mb-1">{{ selectedComponentData?.type }}</h4>
<p class="text-xs text-gray-500">ID: {{ selectedComponentData?.id }}</p>
<p class="text-xs text-gray-500">类型: {{ selectedComponentData?.type }}</p>
</div>
<!-- 动态属性表单 -->
@ -50,30 +45,32 @@
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)"
:value="selectedComponentData?.attrs?.[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 -->
:value="selectedComponentData?.attrs?.[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)"
:checked="selectedComponentData?.attrs?.[prop.name]"
@change="updateComponentProp(selectedComponentData!.id, prop.name, ($event.target as HTMLInputElement).checked)"
/>
<span>{{ prop.label || prop.name }}</span>
</div> <!-- 下拉选择框 -->
</div>
<!-- 下拉选择框 -->
<select
v-else-if="prop.type === 'select' && prop.options"
class="select select-bordered select-sm w-full"
:value="selectedComponentData.props?.[prop.name]"
:value="selectedComponentData?.attrs?.[prop.name]"
@change="(event) => {
const selectElement = event.target as HTMLSelectElement;
const value = selectElement.value;
@ -96,7 +93,8 @@
此组件没有可配置的属性
</div>
</div>
</div> </div>
</div>
</div>
<!-- 元器件选择组件 -->
<ComponentSelector
@ -114,29 +112,29 @@
import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // defineAsyncComponent shallowRef
import DiagramCanvas from '@/components/DiagramCanvas.vue';
import ComponentSelector from '@/components/ComponentSelector.vue';
import { getComponentConfig } from '@/components/equipments/componentConfig';
import type { ComponentConfig } from '@/components/equipments/componentConfig';
import type { DiagramData, DiagramPart, ConnectionArray } from '@/components/diagramManager';
import { validateDiagramData } from '@/components/diagramManager';
// --- ---
const showComponentsMenu = ref(false);
interface ComponentItem {
id: string;
type: string; // 'MechanicalButton'
name: string;
x: number;
y: number;
props?: Record<string, any>; // props
}
const components = ref<ComponentItem[]>([]);
const selectedComponentId = ref<string | null>(null); // selectedComponentId
const selectedComponentData = computed(() => { //
return components.value.find(c => c.id === selectedComponentId.value) || null;
const diagramData = ref<DiagramData>({
version: 1,
author: 'admin',
editor: 'me',
parts: [],
connections: []
});
const selectedComponentId = ref<string | null>(null);
const selectedComponentData = computed(() => {
return diagramData.value.parts.find(p => p.id === selectedComponentId.value) || null;
});
const diagramCanvas = ref(null);
//
interface ComponentModule {
default: any;
getDefaultProps?: () => Record<string, any>;
config?: {
props?: Array<{
name: string;
@ -173,6 +171,12 @@ async function loadComponentModule(type: string) {
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 isResizing = ref(false);
@ -259,54 +263,140 @@ async function handleAddComponent(componentData: { type: string; name: string; p
const offsetX = Math.floor(Math.random() * 100) - 50;
const offsetY = Math.floor(Math.random() * 100) - 50;
const newComponent: ComponentItem = {
// ComponentItem DiagramPart
const newComponent: DiagramPart = {
id: `component-${Date.now()}`,
type: componentData.type,
name: componentData.name,
x: Math.round(posX + offsetX),
y: Math.round(posY + offsetY),
props: componentData.props, // 使 ComponentSelector
attrs: componentData.props, // 使 ComponentSelector
rotate: 0,
group: '',
positionlock: false,
hide: false,
isOn: false
};
components.value.push(newComponent);
console.log('Adding new component:', newComponent);
// diagramCanvas
if (canvasInstance && canvasInstance.getDiagramData && canvasInstance.setDiagramData) {
const currentData = canvasInstance.getDiagramData();
currentData.parts.push(newComponent);
canvasInstance.setDiagramData(currentData);
}
}
//
async function handleComponentSelected(componentData: ComponentItem | null) {
async function handleComponentSelected(componentData: DiagramPart | null) {
selectedComponentId.value = componentData ? componentData.id : null;
selectedComponentConfig.value = null; //
if (componentData) {
//
const config = getComponentConfig(componentData.type);
if (config) {
selectedComponentConfig.value = config;
console.log(`Config for ${componentData.type}:`, config);
} else {
console.warn(`No config found for component type ${componentData.type}`);
//
const moduleRef = await loadComponentModule(componentData.type);
if (moduleRef) {
try {
// 使getDefaultProps
if (typeof moduleRef.getDefaultProps === 'function') {
// getDefaultProps
const defaultProps = moduleRef.getDefaultProps();
const propConfigs = [];
for (const [propName, propValue] of Object.entries(defaultProps)) {
// pins
if (propName === 'pins') continue;
//
let propType = typeof propValue;
let propConfig: any = {
name: propName,
label: propName.charAt(0).toUpperCase() + propName.slice(1), //
default: propValue
};
//
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')) {
// optionsselect
propConfig.type = 'select';
propConfig.options = propValue.options;
}
//
await loadComponentModule(componentData.type);
propConfigs.push(propConfig);
}
selectedComponentConfig.value = { props: propConfigs };
console.log(`Built config for ${componentData.type} from getDefaultProps:`, selectedComponentConfig.value);
} else {
console.warn(`Component ${componentData.type} does not export getDefaultProps method.`);
//
const attrs = componentData.attrs || {};
const propConfigs = [];
for (const [propName, propValue] of Object.entries(attrs)) {
// pins
if (propName === 'pins') continue;
//
let propType = typeof propValue;
let propConfig: any = {
name: propName,
label: propName.charAt(0).toUpperCase() + propName.slice(1), //
default: propValue || '',
type: propType
};
propConfigs.push(propConfig);
}
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: [] };
}
}
}
//
function handleDiagramUpdated(data: DiagramData) {
diagramData.value = data;
console.log('Diagram data updated:', data);
}
//
function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
const component = components.value.find(c => c.id === moveData.id);
if (component) {
component.x = moveData.x;
component.y = moveData.y;
const part = diagramData.value.parts.find(p => p.id === moveData.id);
if (part) {
part.x = moveData.x;
part.y = moveData.y;
}
}
//
function handleComponentDelete(componentId: string) {
//
const index = components.value.findIndex(c => c.id === componentId);
const index = diagramData.value.parts.findIndex(p => p.id === componentId);
if (index !== -1) {
//
components.value.splice(index, 1);
diagramData.value.parts.splice(index, 1);
//
diagramData.value.connections = diagramData.value.connections.filter(
connection => !connection[0].startsWith(`${componentId}:`) && !connection[1].startsWith(`${componentId}:`)
);
//
if (selectedComponentId.value === componentId) {
selectedComponentId.value = null;
@ -315,29 +405,29 @@ function handleComponentDelete(componentId: string) {
}
}
//
function updateComponentProp(componentId: string | { id: string; propName: string; value: any }, propName?: string, value?: any) {
// DiagramCanvas
if (typeof componentId === 'object') {
const { id, propName: name, value: val } = componentId;
componentId = id;
propName = name;
value = val;
}
const component = components.value.find(c => c.id === componentId);
if (component && propName !== undefined) {
if (!component.props) {
component.props = {};
//
function updateComponentProp(componentId: string, propName: string, value: any) {
const canvasInstance = diagramCanvas.value as any;
if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.setDiagramData) {
console.error('Canvas instance not available for property update');
return;
}
// value使
if (value !== null && typeof value === 'object' && 'value' in value) {
value = value.value;
}
const currentData = canvasInstance.getDiagramData();
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
//
component.props[propName] = value;
if (part) {
if (!part.attrs) {
part.attrs = {};
}
part.attrs[propName] = value;
canvasInstance.setDiagramData(currentData);
console.log(`Updated ${componentId} prop ${propName} to:`, value, typeof value);
}
}
@ -345,19 +435,69 @@ function updateComponentProp(componentId: string | { id: string; propName: strin
// 线
function handleWireCreated(wireData: any) {
console.log('Wire created:', wireData);
// 线DiagramCanvas.vue
}
// 线
function handleWireDeleted(wireId: string) {
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);
}
}
//
// --- ---
onMounted(() => {
onMounted(async () => {
//
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(() => {