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"> <script setup lang="ts">
import { ref, computed, shallowRef, onMounted } from 'vue'; import { ref, computed, shallowRef, onMounted } from 'vue';
import { getComponentConfig } from '@/components/equipments/componentConfig';
// Props // Props
interface Props { interface Props {
@ -208,18 +207,22 @@ function closeMenu() {
// //
async function addComponent(componentTemplate: { type: string; name: string }) { async function addComponent(componentTemplate: { type: string; name: string }) {
// //
const config = getComponentConfig(componentTemplate.type); const moduleRef = await loadComponentModule(componentTemplate.type);
const defaultProps: Record<string, any> = {}; let defaultProps: Record<string, any> = {};
if (config && config.props) { // getDefaultProps
config.props.forEach(prop => { if(moduleRef){
defaultProps[prop.name] = prop.default; if (typeof moduleRef.getDefaultProps === 'function') {
}); defaultProps = moduleRef.getDefaultProps();
console.log(`Got default props from ${componentTemplate.type}:`, defaultProps);
} else {
// 退
console.log(`No getDefaultProps found for ${componentTemplate.type}`);
}
} else{
console.log(`Failed to load module for ${componentTemplate.type}`);
} }
// 便使
await loadComponentModule(componentTemplate.type);
// //
emit('add-component', { emit('add-component', {

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

View File

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

View File

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

View File

@ -33,16 +33,18 @@ interface Props {
strokeColor?: string; strokeColor?: string;
strokeWidth?: number; strokeWidth?: number;
isActive?: boolean; isActive?: boolean;
routingMode?: 'auto' | 'orthogonal' | 'direct'; routingMode?: 'auto' | 'orthogonal' | 'direct' | 'path';
// //
startComponentId?: string; startComponentId?: string;
startPinLabel?: string; startPinId?: string;
endComponentId?: string; endComponentId?: string;
endPinLabel?: string; endPinId?: string;
// //
constraint?: string; constraint?: string;
// //
showLabel?: boolean; showLabel?: boolean;
// - diagram.json线
pathCommands?: string[];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -81,9 +83,168 @@ const labelPosition = computed(() => {
}); });
const pathData = 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) { function calculateOrthogonalPath(startX: number, startY: number, endX: number, endY: number) {
// //
const dx = endX - startX; const dx = endX - startX;
@ -145,13 +306,12 @@ watch(() => props.constraint, (newConstraint, oldConstraint) => {
}); });
// 线 // 线
defineExpose({ id: props.id, defineExpose({ id: props.id, getInfo: () => ({
getInfo: () => ({
id: props.id, id: props.id,
startComponentId: props.startComponentId, startComponentId: props.startComponentId,
startPinLabel: props.startPinLabel, startPinId: props.startPinId,
endComponentId: props.endComponentId, endComponentId: props.endComponentId,
endPinLabel: props.endPinLabel, endPinId: props.endPinId,
constraint: props.constraint constraint: props.constraint
}), }),
// 线 // 线

View File

@ -1,363 +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

@ -1,24 +1,19 @@
<template> <template>
<div class="h-screen flex flex-col overflow-hidden"> <div class="h-screen flex flex-col overflow-hidden">
<div class="flex flex-1 overflow-hidden relative"> <div class="flex flex-1 overflow-hidden relative">
<!-- 左侧图形化区域 --> <!-- 左侧图形化区域 -->
<div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }"> <DiagramCanvas <div class="relative bg-base-200 overflow-hidden" :style="{ width: leftPanelWidth + '%' }"> <DiagramCanvas
ref="diagramCanvas" :components="components" ref="diagramCanvas"
:componentModules="componentModules" @component-selected="handleComponentSelected" :componentModules="componentModules"
@component-selected="handleComponentSelected"
@component-moved="handleComponentMoved" @component-moved="handleComponentMoved"
@update-component-prop="updateComponentProp"
@component-delete="handleComponentDelete" @component-delete="handleComponentDelete"
@wire-created="handleWireCreated" @wire-created="handleWireCreated"
@wire-deleted="handleWireDeleted" @wire-deleted="handleWireDeleted"
@diagram-updated="handleDiagramUpdated"
@open-components="openComponentsMenu"
@load-component-module="handleLoadComponentModule"
/> />
<!-- 添加元器件按钮 -->
<button class="btn btn-circle btn-primary absolute top-8 right-8 shadow-lg z-10" @click="openComponentsMenu">
<!-- SVG icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div> </div>
<!-- 拖拽分割线 --> <!-- 拖拽分割线 -->
@ -31,11 +26,11 @@
<div class="bg-base-100 flex flex-col p-4 overflow-auto" :style="{ width: (100 - leftPanelWidth) + '%' }"> <div class="bg-base-100 flex flex-col p-4 overflow-auto" :style="{ width: (100 - leftPanelWidth) + '%' }">
<h3 class="text-lg font-bold mb-4">属性编辑器</h3> <h3 class="text-lg font-bold mb-4">属性编辑器</h3>
<div v-if="!selectedComponentData" class="text-gray-400">选择元器件以编辑属性</div> <div v-if="!selectedComponentData" class="text-gray-400">选择元器件以编辑属性</div>
<div v-else> <div v-else>
<div class="mb-4 pb-4 border-b border-base-300"> <div class="mb-4 pb-4 border-b border-base-300">
<h4 class="font-semibold text-lg mb-1">{{ selectedComponentData.name }}</h4> <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">ID: {{ selectedComponentData?.id }}</p>
<p class="text-xs text-gray-500">类型: {{ selectedComponentData.type }}</p> <p class="text-xs text-gray-500">类型: {{ selectedComponentData?.type }}</p>
</div> </div>
<!-- 动态属性表单 --> <!-- 动态属性表单 -->
@ -50,30 +45,32 @@
type="text" type="text"
:placeholder="prop.label || prop.name" :placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full" class="input input-bordered input-sm w-full"
:value="selectedComponentData.props?.[prop.name]" :value="selectedComponentData?.attrs?.[prop.name]"
@input="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).value)" @input="updateComponentProp(selectedComponentData!.id, prop.name, ($event.target as HTMLInputElement).value)"
/> />
<input <input
v-else-if="prop.type === 'number'" v-else-if="prop.type === 'number'"
type="number" type="number"
:placeholder="prop.label || prop.name" :placeholder="prop.label || prop.name"
class="input input-bordered input-sm w-full" class="input input-bordered input-sm w-full"
:value="selectedComponentData.props?.[prop.name]" :value="selectedComponentData?.attrs?.[prop.name]"
@input="updateComponentProp(selectedComponentData.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)" @input="updateComponentProp(selectedComponentData!.id, prop.name, parseFloat(($event.target as HTMLInputElement).value) || prop.default)"
/> <!-- boolean checkbox color color picker --> />
<!-- 可以为 boolean 添加 checkbox color 添加 color picker -->
<div v-else-if="prop.type === 'boolean'" class="flex items-center"> <div v-else-if="prop.type === 'boolean'" class="flex items-center">
<input <input
type="checkbox" type="checkbox"
class="checkbox checkbox-sm mr-2" class="checkbox checkbox-sm mr-2"
:checked="selectedComponentData.props?.[prop.name]" :checked="selectedComponentData?.attrs?.[prop.name]"
@change="updateComponentProp(selectedComponentData.id, prop.name, ($event.target as HTMLInputElement).checked)" @change="updateComponentProp(selectedComponentData!.id, prop.name, ($event.target as HTMLInputElement).checked)"
/> />
<span>{{ prop.label || prop.name }}</span> <span>{{ prop.label || prop.name }}</span>
</div> <!-- 下拉选择框 --> </div>
<!-- 下拉选择框 -->
<select <select
v-else-if="prop.type === 'select' && prop.options" v-else-if="prop.type === 'select' && prop.options"
class="select select-bordered select-sm w-full" class="select select-bordered select-sm w-full"
:value="selectedComponentData.props?.[prop.name]" :value="selectedComponentData?.attrs?.[prop.name]"
@change="(event) => { @change="(event) => {
const selectElement = event.target as HTMLSelectElement; const selectElement = event.target as HTMLSelectElement;
const value = selectElement.value; const value = selectElement.value;
@ -96,7 +93,8 @@
此组件没有可配置的属性 此组件没有可配置的属性
</div> </div>
</div> </div>
</div> </div> </div>
</div>
<!-- 元器件选择组件 --> <!-- 元器件选择组件 -->
<ComponentSelector <ComponentSelector
@ -114,29 +112,29 @@
import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // defineAsyncComponent shallowRef import { ref, reactive, computed, onMounted, onUnmounted, defineAsyncComponent, shallowRef } from 'vue'; // defineAsyncComponent shallowRef
import DiagramCanvas from '@/components/DiagramCanvas.vue'; import DiagramCanvas from '@/components/DiagramCanvas.vue';
import ComponentSelector from '@/components/ComponentSelector.vue'; import ComponentSelector from '@/components/ComponentSelector.vue';
import { getComponentConfig } from '@/components/equipments/componentConfig'; import type { DiagramData, DiagramPart, ConnectionArray } from '@/components/diagramManager';
import type { ComponentConfig } from '@/components/equipments/componentConfig'; import { validateDiagramData } from '@/components/diagramManager';
// --- --- // --- ---
const showComponentsMenu = ref(false); const showComponentsMenu = ref(false);
interface ComponentItem { const diagramData = ref<DiagramData>({
id: string; version: 1,
type: string; // 'MechanicalButton' author: 'admin',
name: string; editor: 'me',
x: number; parts: [],
y: number; connections: []
props?: Record<string, any>; // props });
}
const components = ref<ComponentItem[]>([]); const selectedComponentId = ref<string | null>(null);
const selectedComponentId = ref<string | null>(null); // selectedComponentId const selectedComponentData = computed(() => {
const selectedComponentData = computed(() => { // return diagramData.value.parts.find(p => p.id === selectedComponentId.value) || null;
return components.value.find(c => c.id === selectedComponentId.value) || null;
}); });
const diagramCanvas = ref(null); const diagramCanvas = ref(null);
// //
interface ComponentModule { interface ComponentModule {
default: any; default: any;
getDefaultProps?: () => Record<string, any>;
config?: { config?: {
props?: Array<{ props?: Array<{
name: string; name: string;
@ -173,6 +171,12 @@ async function loadComponentModule(type: string) {
return componentModules.value[type]; return componentModules.value[type];
} }
//
async function handleLoadComponentModule(type: string) {
console.log('Handling load component module request for:', type);
await loadComponentModule(type);
}
// --- --- // --- ---
const leftPanelWidth = ref(60); const leftPanelWidth = ref(60);
const isResizing = ref(false); const isResizing = ref(false);
@ -259,54 +263,140 @@ async function handleAddComponent(componentData: { type: string; name: string; p
const offsetX = Math.floor(Math.random() * 100) - 50; const offsetX = Math.floor(Math.random() * 100) - 50;
const offsetY = Math.floor(Math.random() * 100) - 50; const offsetY = Math.floor(Math.random() * 100) - 50;
const newComponent: ComponentItem = { // ComponentItem DiagramPart
const newComponent: DiagramPart = {
id: `component-${Date.now()}`, id: `component-${Date.now()}`,
type: componentData.type, type: componentData.type,
name: componentData.name,
x: Math.round(posX + offsetX), x: Math.round(posX + offsetX),
y: Math.round(posY + offsetY), y: Math.round(posY + offsetY),
props: componentData.props, // 使 ComponentSelector attrs: componentData.props, // 使 ComponentSelector
rotate: 0,
group: '',
positionlock: false,
hide: false,
isOn: false
}; };
console.log('Adding new component:', newComponent);
components.value.push(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; selectedComponentId.value = componentData ? componentData.id : null;
selectedComponentConfig.value = null; // selectedComponentConfig.value = null; //
if (componentData) { if (componentData) {
// //
const config = getComponentConfig(componentData.type); const moduleRef = await loadComponentModule(componentData.type);
if (config) {
selectedComponentConfig.value = config;
console.log(`Config for ${componentData.type}:`, config);
} else {
console.warn(`No config found for component type ${componentData.type}`);
}
// if (moduleRef) {
await loadComponentModule(componentData.type); 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;
}
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 }) { function handleComponentMoved(moveData: { id: string; x: number; y: number }) {
const component = components.value.find(c => c.id === moveData.id); const part = diagramData.value.parts.find(p => p.id === moveData.id);
if (component) { if (part) {
component.x = moveData.x; part.x = moveData.x;
component.y = moveData.y; part.y = moveData.y;
} }
} }
// //
function handleComponentDelete(componentId: string) { function handleComponentDelete(componentId: string) {
// //
const index = components.value.findIndex(c => c.id === componentId); const index = diagramData.value.parts.findIndex(p => p.id === componentId);
if (index !== -1) { 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) { if (selectedComponentId.value === componentId) {
selectedComponentId.value = null; 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) { function updateComponentProp(componentId: string, propName: string, value: any) {
// DiagramCanvas const canvasInstance = diagramCanvas.value as any;
if (typeof componentId === 'object') { if (!canvasInstance || !canvasInstance.getDiagramData || !canvasInstance.setDiagramData) {
const { id, propName: name, value: val } = componentId; console.error('Canvas instance not available for property update');
componentId = id; return;
propName = name;
value = val;
} }
const component = components.value.find(c => c.id === componentId);
if (component && propName !== undefined) { // value使
if (!component.props) { if (value !== null && typeof value === 'object' && 'value' in value) {
component.props = {}; value = value.value;
}
const currentData = canvasInstance.getDiagramData();
const part = currentData.parts.find((p: DiagramPart) => p.id === componentId);
if (part) {
if (!part.attrs) {
part.attrs = {};
} }
// value使 part.attrs[propName] = value;
if (value !== null && typeof value === 'object' && 'value' in value) {
value = value.value;
}
//
component.props[propName] = value;
canvasInstance.setDiagramData(currentData);
console.log(`Updated ${componentId} prop ${propName} to:`, value, typeof value); 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) { function handleWireCreated(wireData: any) {
console.log('Wire created:', wireData); console.log('Wire created:', wireData);
// 线DiagramCanvas.vue
} }
// 线 // 线
function handleWireDeleted(wireId: string) { function handleWireDeleted(wireId: string) {
console.log('Wire deleted:', wireId); console.log('Wire deleted:', wireId);
// 线
} }
// diagram
function exportDiagram() {
// 使DiagramCanvas
const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.exportDiagram) {
canvasInstance.exportDiagram();
}
}
// --- ---
const showNotification = ref(false);
const notificationMessage = ref('');
const notificationType = ref<'success' | 'error' | 'info'>('info');
function showToast(message: string, type: 'success' | 'error' | 'info' = 'info', duration = 3000) { const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.showToast) {
canvasInstance.showToast(message, type, duration);
} else {
// 使
notificationMessage.value = message;
notificationType.value = type;
showNotification.value = true;
//
setTimeout(() => {
showNotification.value = false;
}, duration);
}
}
//
// --- --- // --- ---
onMounted(() => { onMounted(async () => {
// //
console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value); console.log('ProjectView mounted, diagram canvas ref:', diagramCanvas.value);
//
const canvasInstance = diagramCanvas.value as any;
if (canvasInstance && canvasInstance.getDiagramData) {
diagramData.value = canvasInstance.getDiagramData();
// 使
const componentTypes = new Set<string>();
diagramData.value.parts.forEach(part => {
componentTypes.add(part.type);
});
console.log('Preloading component modules:', Array.from(componentTypes));
//
await Promise.all(
Array.from(componentTypes).map(type => loadComponentModule(type))
);
console.log('All component modules loaded');
}
}); });
onUnmounted(() => { onUnmounted(() => {