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.
This commit is contained in:
alivender
2025-05-07 15:42:35 +08:00
parent 1c75aa621a
commit 47cfe17d16
10 changed files with 1457 additions and 890 deletions

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

@@ -1,23 +1,20 @@
<template>
<svg
<svg
:width="width"
: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

@@ -17,11 +17,11 @@
<rect
width="70"
height="30"
x="15"
y="15"
x="15"
y="15"
:fill="ledColor"
:style="{ opacity: isOn ? 1 : 0.2 }"
rx="15"
rx="15"
ry="15"
class="interactive"
/>
@@ -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';
// --- 关键暴露getPinPosition代理到内部Pin ---
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',
constraint: '',
label: 'LED',
componentId: ''
initialOn: false,
brightness: 80,
pins: () => [
{
pinId: 'LED',
constraint: '',
x: 50,
y: 30
}
]
});
const width = computed(() => 100 * props.size);
@@ -106,18 +114,26 @@ const ledColor = computed(() => {
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;
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);
}
return null;
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) {
// 调用对应Pin组件的getPinPosition方法
return {
x: customPin.x,
y: customPin.y
}
} 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(() => {
return calculateOrthogonalPath(props.startX, props.startY, props.endX, props.endY);
// 如果有路径命令,使用路径布线模式
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;
}