Compare commits
2 Commits
Author | SHA1 | Date |
---|---|---|
|
d4b34bd6d4 | |
|
47cfe17d16 |
|
@ -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
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"version": 1,
|
||||
"author": "admin",
|
||||
"editor": "me",
|
||||
"parts": [
|
||||
], "connections": [
|
||||
]
|
||||
}
|
|
@ -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
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
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;
|
||||
});
|
||||
|
||||
// 获取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);
|
||||
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;
|
||||
} 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;
|
||||
|
|
|
@ -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
|
||||
}),
|
||||
// 更新连线位置
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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')) {
|
||||
// 如果是含有options的对象,认为它是select类型
|
||||
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(() => {
|
||||
|
|
Loading…
Reference in New Issue