This repository has been archived on 2025-10-29. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
FPGA_WebLab/src/components/LabCanvas/DiagramCanvas.vue

1123 lines
32 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div
class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container flex flex-col select-none"
ref="canvasContainer"
@mousedown="handleCanvasMouseDown"
@mousedown.middle.prevent="startMiddleDrag"
@wheel.prevent="onZoom"
@contextmenu.prevent="handleContextMenu"
>
<!-- 工具栏 -->
<div class="absolute top-2 right-2 flex gap-2 z-30">
<button class="btn btn-sm btn-primary" @click="openDiagramFileSelector">
<FolderOpen class="h-4 w-4 mr-1" />
导入
</button>
<button class="btn btn-sm btn-primary" @click="exportDiagram">
<Download class="h-4 w-4 mr-1" />
导出
</button>
<button class="btn btn-sm btn-primary" @click="emit('open-components')">
<Plus class="h-4 w-4 mr-1" />
添加组件
</button>
<button class="btn btn-sm btn-primary" @click="emit('toggle-doc-panel')">
<FileText class="h-4 w-4 mr-1" />
{{ props.showDocPanel ? "属性面板" : "文档" }}
</button>
</div>
<!-- 隐藏的文件输入 -->
<input
type="file"
ref="fileInput"
class="hidden"
accept=".json"
@change="handleFileSelected"
/>
<div
ref="canvas"
class="diagram-canvas relative select-none"
:style="{
transform: `translate(${componentManager.canvasPosition.x}px, ${componentManager.canvasPosition.y}px) scale(${componentManager.canvasScale.value})`,
}"
>
<!-- 渲染连线 -->
<svg
class="wires-layer absolute top-0 left-0 w-full h-full z-50"
width="10000"
height="10000"
>
<!-- 已完成的连线 -->
<WireComponent
v-for="(wire, index) in wireItems"
:key="wire.id"
:id="wire.id"
:start-x="wire.startX"
:start-y="wire.startY"
:end-x="wire.endX"
:end-y="wire.endY"
:stroke-color="wire.color || '#4a5568'"
:stroke-width="wire.strokeWidth"
:is-active="false"
:start-component-id="wire.startComponentId"
:start-pin-id="wire.startPinId"
:end-component-id="wire.endComponentId"
:end-pin-id="wire.endPinId"
:routing-mode="wire.routingMode"
:path-commands="wire.pathCommands"
/>
<!-- 正在创建的连线 -->
<WireComponent
v-if="isCreatingWire"
id="temp-wire"
:start-x="creatingWireStart.x"
:start-y="creatingWireStart.y"
:end-x="mousePosition.x"
:end-y="mousePosition.y"
stroke-color="#3182ce"
:stroke-width="2"
:is-active="true"
/>
</svg>
<!-- 渲染画布上的组件 -->
<div
v-for="component in diagramParts"
:key="component.id"
class="component-wrapper absolute p-0 inline-block overflow-visible select-none"
:class="{
'component-hover': hoveredComponent === component.id,
'component-selected': selectedComponentId === component.id,
'cursor-not-allowed grayscale-70 opacity-60': !component.isOn,
'component-hidepins': component.hidepins,
}"
:style="{
top: component.y + 'px',
left: component.x + 'px',
zIndex: component.index ?? 0,
transform: component.rotate
? `rotate(${component.rotate}deg)`
: 'none',
opacity: component.isOn ? 1 : 0.6,
display: 'block',
}"
@mousedown.left.stop="startComponentDrag($event, component)"
@mouseover="hoveredComponent = component.id"
@mouseleave="hoveredComponent = null"
>
<!-- 动态渲染组件 -->
<component
:is="componentManager.getComponentDefinition(component.type)"
v-if="
componentManager.componentModules.value[component.type] &&
componentManager.getComponentDefinition(component.type)
"
v-bind="
componentManager.prepareComponentProps(
component.attrs || {},
component.id,
)
"
@update:bindKey="
(value: string) =>
updateComponentProp(component.id, 'bindKey', value)
"
@pin-click="
(pinInfo: any) =>
handlePinClick(component.id, pinInfo, pinInfo.originalEvent)
"
:ref="(el: any) => componentManager.setComponentRef(component.id, el)"
/>
<!-- Fallback if component module not loaded yet -->
<div
v-else
class="p-2 text-xs text-gray-400 border border-dashed border-gray-400 flex items-center justify-center"
>
<div class="flex flex-col items-center">
<div class="loading loading-spinner loading-xs mb-1"></div>
<span>Loading {{ component.type }}...</span>
<small class="mt-1 text-xs">{{
componentManager.componentModules.value[component.type]
? "Module loaded but invalid"
: "Module not found"
}}</small>
</div>
</div>
</div>
</div>
<!-- 加载指示器 -->
<div
v-if="isLoading"
class="absolute inset-0 bg-black bg-opacity-30 flex items-center justify-center z-50"
>
<div class="loading loading-spinner loading-lg text-primary"></div>
</div>
<!-- 缩放指示器 -->
<div
class="absolute bottom-4 right-4 bg-base-100 px-3 py-1.5 rounded-md shadow-md z-20"
style="opacity: 0.9"
>
<span class="text-sm font-medium"
>{{ Math.round(componentManager?.canvasScale.value * 100) }}%</span
>
</div>
</div>
</template>
<script setup lang="ts">
import {
ref,
reactive,
onMounted,
onUnmounted,
computed,
watch,
provide,
} from "vue";
import { useEventListener } from "@vueuse/core";
import { FolderOpen, Download, Plus, FileText } from "lucide-vue-next";
import WireComponent from "@/components/equipments/Wire.vue";
import { useAlertStore } from "@/components/Alert";
// 导入 diagram 管理器
import {
loadDiagramData,
saveDiagramData,
updatePartPosition,
updatePartAttribute,
parseConnectionPin,
connectionArrayToWireItem,
validateDiagramData,
} from "./composable/diagramManager";
import type {
DiagramData,
DiagramPart,
ConnectionArray,
WireItem,
} from "./composable/diagramManager";
import { CanvasCurrentSelectedComponentID } from "../InjectKeys";
import { useComponentManager } from "./composable/componentManager";
// 右键菜单处理函数
function handleContextMenu(e: MouseEvent) {
e.preventDefault();
}
// 定义组件发出的事件
const emit = defineEmits(["toggle-doc-panel", "open-components"]);
// 定义组件接受的属性
const props = defineProps<{
showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
}>();
// 获取componentManager实例
const componentManager = useComponentManager();
if (!componentManager) {
throw new Error(
"DiagramCanvas must be used within a component manager provider",
);
}
// 获取Alert store实例
const alertStore = useAlertStore();
// --- 画布状态 ---
const canvasContainer = ref<HTMLElement | null>(null);
const canvas = ref<HTMLElement | null>(null);
const isDragging = ref(false);
const isMiddleDragging = ref(false);
const dragStart = reactive({ x: 0, y: 0 });
const selectedComponentId = ref<string | null>(null);
const hoveredComponent = ref<string | null>(null);
const draggingComponentId = ref<string | null>(null);
const componentDragOffset = reactive({ x: 0, y: 0 });
// Provide and Inject
provide(CanvasCurrentSelectedComponentID, selectedComponentId);
// Diagram 数据
const diagramData = ref<DiagramData>({
version: 1,
author: "admin",
editor: "me",
parts: [],
connections: [],
});
// 计算属性:从 diagramData 中提取组件列表并按index属性排序
const diagramParts = computed<DiagramPart[]>(() => {
// 克隆原始数组以避免直接修改原始数据
const parts = [...diagramData.value.parts];
// 按照index属性进行排序index值大的排在后面显示在上层
// 如果没有定义index则默认为0
return parts.sort((a, b) => {
const indexA = a.index ?? 0;
const indexB = b.index ?? 0;
return indexA - indexB;
});
});
// 计算属性:转换连接为 WireItem 列表以供渲染
const wireItems = computed<WireItem[]>(() => {
// 检查组件是否仍然挂载
if (!document.body.contains(canvasContainer.value)) {
return []; // 如果组件已经卸载,返回空数组
}
return diagramData.value.connections.map((conn, index) => {
const [startPin, endPin] = conn;
const { componentId: startCompId, pinId: startPinId } =
parseConnectionPin(startPin);
const { componentId: endCompId, pinId: endPinId } =
parseConnectionPin(endPin);
// 查找对应的组件位置
const startComp = diagramParts.value.find((p) => p.id === startCompId);
const endComp = diagramParts.value.find((p) => p.id === endCompId);
// 默认位置(如果找不到组件)
const startPos = { x: 0, y: 0 };
const endPos = { x: 0, y: 0 };
// 如果找到组件,设置连线端点位置
if (startComp) {
startPos.x = startComp.x;
startPos.y = startComp.y;
// 尝试获取引脚精确位置(如果有实现)
const startCompRef = componentManager?.getComponentRef(startCompId);
if (startCompRef && typeof startCompRef.getPinPosition === "function") {
try {
const pinPos = startCompRef.getPinPosition(startPinId);
console.log(
`线路${index} - 起点引脚位置(来自${startCompId}):`,
pinPos,
);
if (pinPos) {
// 正确合并组件位置与引脚相对位置
startPos.x = startComp.x + pinPos.x;
startPos.y = startComp.y + pinPos.y;
console.log(`线路${index} - 计算后的起点位置:`, startPos);
}
} catch (error) {
// console.error(`获取引脚位置出错:`, error);
}
}
}
if (endComp) {
endPos.x = endComp.x;
endPos.y = endComp.y;
// 尝试获取引脚精确位置
const endCompRef = componentManager?.getComponentRef(endCompId);
if (endCompRef && typeof endCompRef.getPinPosition === "function") {
try {
const pinPos = endCompRef.getPinPosition(endPinId);
console.log(`线路${index} - 终点引脚位置(来自${endCompId}):`, pinPos);
if (pinPos) {
// 正确合并组件位置与引脚相对位置
endPos.x = endComp.x + pinPos.x;
endPos.y = endComp.y + pinPos.y;
console.log(`线路${index} - 计算后的终点位置:`, endPos);
}
} catch (error) {
console.error(`获取引脚位置出错:`, error);
}
}
}
return connectionArrayToWireItem(conn, index, startPos, endPos);
});
});
// 连线创建状态
const isCreatingWire = ref(false);
const creatingWireStart = reactive({ x: 0, y: 0 });
const creatingWireStartInfo = reactive({
componentId: "",
pinId: "",
constraint: "",
});
const mousePosition = reactive({ x: 0, y: 0 });
// 加载状态
const isLoading = ref(false);
// 文件选择引用
const fileInput = ref<HTMLInputElement | null>(null);
// VueUse事件监听器状态管理
const isDragEventActive = ref(false);
const isComponentDragEventActive = ref(false);
const isWireCreationEventActive = ref(false);
// 使用VueUse设置事件监听器
// 画布拖拽事件
useEventListener(document, "mousemove", (e: MouseEvent) => {
if (isDragEventActive.value) {
onCanvasDrag(e);
}
if (isComponentDragEventActive.value) {
onComponentDrag(e);
}
if (isWireCreationEventActive.value) {
onCreatingWireMouseMove(e);
}
});
useEventListener(document, "mouseup", () => {
if (isDragEventActive.value) {
stopDrag();
}
if (isComponentDragEventActive.value) {
stopComponentDrag();
}
});
// 键盘事件
useEventListener(window, "keydown", handleKeyDown);
// --- 缩放功能 ---
const MIN_SCALE = 0.2;
const MAX_SCALE = 10.0;
function onZoom(e: WheelEvent) {
e.preventDefault();
if (!canvasContainer.value) return;
// 获取容器的位置
const containerRect = canvasContainer.value.getBoundingClientRect();
// 计算鼠标在容器内的相对位置
const mouseX = e.clientX - containerRect.left;
const mouseY = e.clientY - containerRect.top;
// 计算缩放值
const zoomFactor = 1.1; // 每次放大/缩小10%
const direction = e.deltaY > 0 ? -1 : 1;
const finalZoomFactor = direction > 0 ? zoomFactor : 1 / zoomFactor;
// 使用componentManager的缩放方法
componentManager?.zoomAtPosition(mouseX, mouseY, finalZoomFactor);
}
// --- 画布交互逻辑 ---
function handleCanvasMouseDown(e: MouseEvent) {
// 如果是直接点击画布(而不是元器件),清除选中状态
if (e.target === canvasContainer.value || e.target === canvas.value) {
if (selectedComponentId.value !== null) {
selectedComponentId.value = null;
// 直接通过componentManager选择组件
if (componentManager) {
componentManager.selectComponent(null);
}
}
}
// 左键拖拽画布逻辑
if (
e.button === 0 &&
(e.target === canvasContainer.value || e.target === canvas.value)
) {
startDrag(e);
}
}
// 左键拖拽画布
function startDrag(e: MouseEvent) {
if (e.button !== 0 || draggingComponentId.value) return;
isDragging.value = true;
isMiddleDragging.value = false;
const currentPosition = componentManager?.getCanvasPosition();
if (!currentPosition) return;
dragStart.x = e.clientX - currentPosition.x;
dragStart.y = e.clientY - currentPosition.y;
isDragEventActive.value = true;
e.preventDefault();
}
// 中键拖拽画布
function startMiddleDrag(e: MouseEvent) {
if (e.button !== 1) return;
isMiddleDragging.value = true;
isDragging.value = false;
draggingComponentId.value = null;
const currentPosition = componentManager?.getCanvasPosition();
if (!currentPosition) return;
dragStart.x = e.clientX - currentPosition.x;
dragStart.y = e.clientY - currentPosition.y;
isDragEventActive.value = true;
e.preventDefault();
}
// 拖拽画布过程
function onCanvasDrag(e: MouseEvent) {
if (!isDragging.value && !isMiddleDragging.value) return;
const newX = e.clientX - dragStart.x;
const newY = e.clientY - dragStart.y;
// 使用componentManager设置画布位置
componentManager?.setCanvasPosition(newX, newY);
}
// 停止拖拽画布
function stopDrag() {
isDragging.value = false;
isMiddleDragging.value = false;
isDragEventActive.value = false;
}
// --- 组件拖拽交互 ---
function startComponentDrag(e: MouseEvent, component: DiagramPart) {
const target = e.target as HTMLElement;
// 检查点击的是否为交互元素 (如按钮、开关等)
const isInteractiveElement =
target.tagName === "rect" ||
target.tagName === "circle" ||
target.tagName === "path" ||
target.hasAttribute("fill-opacity") ||
(typeof target.className === "string" &&
(target.className.includes("glow") ||
target.className.includes("interactive")));
// 仍然选中组件,无论是否为交互元素
if (selectedComponentId.value !== component.id) {
selectedComponentId.value = component.id;
// 直接通过componentManager选择组件
if (componentManager) {
componentManager.selectComponent(component);
}
}
// 如果组件锁定位置或是交互元素,则不启动拖拽
if (component.positionlock || isInteractiveElement || e.button !== 0) {
return;
}
// 阻止事件冒泡
e.stopPropagation();
console.debug(`Start Drag Component: ${component.type}:${component.id}`);
// 设置拖拽状态
draggingComponentId.value = component.id;
isDragging.value = false;
isMiddleDragging.value = false;
// 获取容器位置
if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect();
// 使用componentManager的屏幕坐标转画布坐标方法
const mouseCanvasPos = componentManager?.screenToCanvas(
e.clientX - containerRect.left,
e.clientY - containerRect.top,
);
if (!mouseCanvasPos) return;
// 计算鼠标相对于组件左上角的偏移量
componentDragOffset.x = mouseCanvasPos.x - component.x;
componentDragOffset.y = mouseCanvasPos.y - component.y;
// 激活组件拖拽事件监听
isComponentDragEventActive.value = true;
}
// 拖拽组件过程
function onComponentDrag(e: MouseEvent) {
if (!draggingComponentId.value || !canvasContainer.value) return;
// 防止触发组件内部的事件
e.stopPropagation();
e.preventDefault();
const containerRect = canvasContainer.value.getBoundingClientRect();
// 使用componentManager的屏幕坐标转画布坐标方法
const mouseCanvasPos = componentManager?.screenToCanvas(
e.clientX - containerRect.left,
e.clientY - containerRect.top,
);
if (!mouseCanvasPos) return;
// 计算组件新位置
const newX = mouseCanvasPos.x - componentDragOffset.x;
const newY = mouseCanvasPos.y - componentDragOffset.y;
// 获取当前拖动的组件
const draggedComponent = diagramParts.value.find(
(p) => p.id === draggingComponentId.value,
);
if (!draggedComponent) return;
// 如果组件属于组,移动组内所有其他组件
if (draggedComponent.group) {
const deltaX = Math.round(newX) - draggedComponent.x;
const deltaY = Math.round(newY) - draggedComponent.y;
// 找出属于同一组但不是当前拖动组件的其他组件
const groupComponents = diagramParts.value.filter(
(p) =>
p.group === draggedComponent.group &&
p.id !== draggingComponentId.value,
);
// 更新这些组件的位置
for (const groupComp of groupComponents) {
diagramData.value = updatePartPosition(
diagramData.value,
groupComp.id,
groupComp.x + deltaX,
groupComp.y + deltaY,
);
}
}
// 通知componentManager位置已更新
if (componentManager) {
componentManager.moveComponent({
id: draggingComponentId.value,
x: Math.round(newX),
y: Math.round(newY),
});
}
}
// 停止拖拽组件
function stopComponentDrag() {
// 如果有组件被拖拽,保存当前状态
if (draggingComponentId.value) {
draggingComponentId.value = null;
}
isComponentDragEventActive.value = false;
saveDiagramData(diagramData.value);
}
// 更新组件属性
function updateComponentProp(
componentId: string,
propName: string,
value: any,
) {
// 直接通过componentManager更新组件属性
if (componentManager) {
componentManager.updateComponentProp(componentId, propName, value);
} else {
// 后备方案:直接更新数据
diagramData.value = updatePartAttribute(
diagramData.value,
componentId,
propName,
value,
);
}
}
// --- 连线操作 ---
// 更新鼠标位置
function updateMousePosition(e: MouseEvent) {
if (!canvasContainer.value) return;
const containerRect = canvasContainer.value.getBoundingClientRect();
// 使用componentManager的屏幕坐标转画布坐标方法
const canvasPos = componentManager?.screenToCanvas(
e.clientX - containerRect.left,
e.clientY - containerRect.top,
);
if (!canvasPos) return;
mousePosition.x = canvasPos.x;
mousePosition.y = canvasPos.y;
}
// 处理引脚点击
function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
if (!canvasContainer.value) return;
updateMousePosition(event);
if (!pinInfo || !pinInfo.label) {
console.error("无效的针脚信息:", pinInfo);
return;
}
// 获取引脚ID
const pinId = pinInfo.label;
console.log("----引脚点击详情开始----");
console.log("组件ID:", componentId);
console.log("引脚ID:", pinId);
console.log("引脚原始信息:", pinInfo);
console.log("鼠标位置:", mousePosition);
if (!isCreatingWire.value) {
// 获取初始位置信息
let pinPosition = pinInfo.position;
console.log("Pin信息:", pinInfo);
console.log("Pin初始位置:", pinPosition);
console.log("组件ID:", componentId);
console.log("引脚ID:", pinId);
// 从组件引用中获取组件实例
const component = componentManager?.getComponentRef(componentId);
console.log("组件引用:", component);
// 查找组件部件对象以获取组件位置
const componentPart = diagramParts.value.find((p) => p.id === componentId);
if (!componentPart) {
console.error("找不到组件部件对象:", componentId);
}
// 重新设置引脚位置(初始化)
pinPosition = { x: 0, y: 0 };
// 如果组件实例存在且有 getPinPosition 方法
if (
component &&
typeof component.getPinPosition === "function" &&
componentPart
) {
try {
console.log("尝试从组件获取引脚位置");
console.log("组件部件位置:", componentPart.x, componentPart.y);
const pinRelativePos = component.getPinPosition(pinId);
console.log("组件返回的引脚相对位置:", pinRelativePos);
if (pinRelativePos) {
// 计算引脚的绝对位置 = 组件位置 + 引脚相对位置
pinPosition = {
x: componentPart.x + pinRelativePos.x,
y: componentPart.y + pinRelativePos.y,
};
console.log("计算的引脚绝对位置:", pinPosition);
} else {
// 如果没有找到引脚位置,使用组件位置
pinPosition = {
x: componentPart.x,
y: componentPart.y,
};
console.log("使用组件位置:", pinPosition);
}
} catch (error) {
console.error(`获取引脚位置出错:`, error);
}
} else if (componentPart) {
// 如果组件不存在或没有 getPinPosition 方法,使用组件的位置
pinPosition = {
x: componentPart.x,
y: componentPart.y,
};
console.log("使用组件位置:", pinPosition);
}
console.log("最终使用的引脚位置:", pinPosition);
// 使用最终的引脚位置作为连线起点
setWireCreationStart(
pinPosition.x,
pinPosition.y,
componentId,
pinId,
pinInfo.constraint,
);
isWireCreationEventActive.value = true;
} else {
// 完成连线创建
if (
componentId === creatingWireStartInfo.componentId &&
pinId === creatingWireStartInfo.pinId
) {
// 如果点击的是同一个引脚,取消连线创建
cancelWireCreation();
return;
}
// 获取终点引脚位置
let endPosition = { x: 0, y: 0 };
const componentPart = diagramParts.value.find((p) => p.id === componentId);
const endComponent = componentManager?.getComponentRef(componentId);
console.log("终点组件部件:", componentPart);
console.log("终点组件引用:", endComponent);
// 如果找到组件,设置终点位置
if (componentPart) {
endPosition.x = componentPart.x;
endPosition.y = componentPart.y;
// 如果组件实现了getPinPosition方法使用它
if (endComponent && typeof endComponent.getPinPosition === "function") {
try {
const pinPos = endComponent.getPinPosition(pinId);
console.log("终点组件返回的引脚位置:", pinPos);
if (pinPos) {
// 正确合并组件位置与引脚相对位置
endPosition = {
x: componentPart.x + pinPos.x,
y: componentPart.y + pinPos.x,
};
console.log("终点引脚位置(来自组件方法):", endPosition);
}
} catch (error) {
console.error(`获取终点引脚位置出错:`, error);
}
} else {
// 对于没有提供引脚精确位置的组件,使用组件位置
console.log("终点组件没有提供引脚位置方法,使用组件位置");
}
} else {
console.error("找不到终点组件部件对象:", componentId);
}
console.log("最终使用的终点位置:", endPosition);
console.log("线路从", creatingWireStartInfo, "到", { componentId, pinId });
console.log("----引脚点击详情结束----");
// 创建新的连线
const newConnection: ConnectionArray = [
`${creatingWireStartInfo.componentId}:${creatingWireStartInfo.pinId}`,
`${componentId}:${pinId}`,
2, // 线宽
["right10", "*", "left10"], // 默认路径
];
// 更新图表数据
diagramData.value = {
...diagramData.value,
connections: [...diagramData.value.connections, newConnection],
};
// 重置连线创建状态
resetWireCreation();
isWireCreationEventActive.value = false;
}
}
// 开始创建连线
function setWireCreationStart(
x: number,
y: number,
componentId: string,
pinId: string,
constraint?: string,
) {
isCreatingWire.value = true;
creatingWireStart.x = x;
creatingWireStart.y = y;
creatingWireStartInfo.componentId = componentId;
creatingWireStartInfo.pinId = pinId;
creatingWireStartInfo.constraint = constraint || "";
}
// 重置连线创建状态
function resetWireCreation() {
isCreatingWire.value = false;
creatingWireStart.x = 0;
creatingWireStart.y = 0;
creatingWireStartInfo.componentId = "";
creatingWireStartInfo.pinId = "";
creatingWireStartInfo.constraint = "";
}
// 取消连线创建
function cancelWireCreation() {
resetWireCreation();
isWireCreationEventActive.value = false;
}
// 连线创建过程中的鼠标移动
function onCreatingWireMouseMove(e: MouseEvent) {
updateMousePosition(e);
}
// 删除组件
function deleteComponent(componentId: string) {
// 直接通过componentManager删除组件
if (componentManager) {
componentManager.deleteComponent(componentId);
}
// 清除选中状态
if (selectedComponentId.value === componentId) {
selectedComponentId.value = null;
}
}
// --- 文件操作功能 ---
// 打开文件选择器
function openDiagramFileSelector() {
if (fileInput.value) {
fileInput.value.click();
}
}
// 处理文件选择
function handleFileSelected(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) {
return;
}
// 设置加载状态
isLoading.value = true;
const reader = new FileReader();
reader.onload = (e) => {
try {
// 检查组件是否仍然挂载
if (!document.body.contains(canvasContainer.value)) {
return; // 如果组件已经卸载,不执行后续操作
}
const content = e.target?.result as string;
const parsed = JSON.parse(content);
// 使用验证函数检查文件格式
const validation = validateDiagramData(parsed);
if (!validation.isValid) {
alertStore?.show(
`不是有效的diagram.json格式: ${validation.errors.join("; ")}`,
"error",
);
isLoading.value = false;
return;
}
// 更新画布数据
diagramData.value = parsed as DiagramData;
alertStore?.show(`成功导入diagram文件`, "success");
} catch (error) {
console.error("解析JSON文件出错:", error);
if (document.body.contains(canvasContainer.value)) {
alertStore?.show("解析文件出错请确认是有效的JSON格式", "error");
}
} finally {
// 检查组件是否仍然挂载
if (document.body.contains(canvasContainer.value)) {
// 结束加载状态
isLoading.value = false;
}
// 清除文件输入,以便同一文件可以再次导入
target.value = "";
}
};
reader.onerror = () => {
// 检查组件是否仍然挂载
if (document.body.contains(canvasContainer.value)) {
alertStore?.show("读取文件时出错", "error");
isLoading.value = false;
}
// 清除文件输入
target.value = "";
};
reader.readAsText(file);
}
// 导出当前diagram数据
function exportDiagram() {
try {
isLoading.value = true;
// 创建一个Blob对象
const jsonString = JSON.stringify(diagramData.value, null, 2);
const blob = new Blob([jsonString], { type: "application/json" });
// 创建一个下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "diagram.json";
a.click();
// 释放URL对象
setTimeout(() => {
URL.revokeObjectURL(url);
// 检查组件是否仍然挂载
if (document.body.contains(canvasContainer.value)) {
isLoading.value = false;
alertStore?.show("成功导出diagram文件", "success");
}
}, 100);
} catch (error) {
console.error("导出diagram文件时出错:", error);
alertStore?.show("导出diagram文件时出错", "error");
isLoading.value = false;
}
}
// --- 生命周期钩子 ---
onMounted(async () => {
// 加载图表数据
try {
diagramData.value = await loadDiagramData();
// 预加载所有组件模块
const componentTypes = new Set<string>();
diagramData.value.parts.forEach((part) => {
componentTypes.add(part.type);
});
console.log(
"DiagramCanvas: Requesting component modules:",
Array.from(componentTypes),
);
// 直接通过componentManager预加载组件模块
if (componentManager) {
await componentManager.preloadComponentModules(
Array.from(componentTypes),
);
}
} catch (error) {
console.error("加载图表数据失败:", error);
}
// 初始化中心位置 - 使用componentManager设置
if (canvasContainer.value && componentManager) {
// 修改为将画布中心点放在容器中心点
const centerX = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
const centerY = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
componentManager.setCanvasPosition(centerX, centerY);
}
});
// 处理键盘事件
function handleKeyDown(e: KeyboardEvent) {
// 如果当前有选中的组件并且按下了Delete键
if (selectedComponentId.value && e.key === "Delete") {
// 触发删除组件事件
deleteComponent(selectedComponentId.value);
}
// 如果当前正在创建连线并且按下了ESC键
if (isCreatingWire.value && e.key === "Escape") {
// 取消连线创建
cancelWireCreation();
}
}
// 无加载动画的数据更新方法
function updateDiagramDataDirectly(data: DiagramData) {
// 检查组件是否仍然挂载
if (!document.body.contains(canvasContainer.value)) {
return; // 如果组件已经卸载,不执行后续操作
}
diagramData.value = data;
}
// 暴露方法给父组件
defineExpose({
// 基本数据操作
getDiagramData: () => diagramData.value,
updateDiagramDataDirectly,
});
</script>
<style scoped>
/* 基础容器样式 - 使用 Tailwind 类替代 */
.diagram-container {
background-size:
20px 20px,
20px 20px,
100px 100px,
100px 100px;
background-position: 0 0;
}
/* 画布样式 - 部分保留自定义属性 */
.diagram-canvas {
width: 10000px;
height: 10000px;
transform-origin: 0 0;
}
/* 连线层样式 */
.wires-layer {
pointer-events: auto;
overflow: visible;
}
.wires-layer path {
pointer-events: stroke;
cursor: pointer;
}
/* 组件容器样式 */
.component-wrapper {
box-sizing: content-box;
cursor: move;
}
/* 悬停状态 */
.component-hover {
outline: 2px dashed #3498db;
outline-offset: 2px;
}
/* 选中状态 */
.component-selected {
outline: 3px dashed;
outline-color: #e74c3c #f39c12 #3498db #2ecc71;
outline-offset: 3px;
}
/* SVG 交互样式 */
.component-wrapper :deep(svg) {
pointer-events: auto;
}
.component-wrapper
:deep(
svg
*:not([class*="interactive"]):not(rect.glow):not(
circle[fill-opacity]
):not([fill-opacity])
) {
pointer-events: none;
}
.component-wrapper :deep(svg circle[fill-opacity]),
.component-wrapper :deep(svg rect[fill-opacity]),
.component-wrapper :deep(svg rect[class*="glow"]),
.component-wrapper :deep(svg rect.glow),
.component-wrapper :deep(svg [class*="interactive"]),
.component-wrapper :deep(button),
.component-wrapper :deep(input) {
pointer-events: auto !important;
}
.wire-active {
stroke: #0099ff !important;
filter: drop-shadow(0 0 4px #0099ffcc);
}
</style>