1308 lines
39 KiB
Vue
1308 lines
39 KiB
Vue
<template>
|
||
<div class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container" 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">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
|
||
</svg>
|
||
导入
|
||
</button>
|
||
<button class="btn btn-sm btn-primary" @click="exportDiagram">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||
</svg>
|
||
导出
|
||
</button>
|
||
<button class="btn btn-sm btn-primary" @click="emit('open-components')">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24"
|
||
stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||
</svg>
|
||
添加组件
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 隐藏的文件输入 -->
|
||
<input type="file" ref="fileInput" class="hidden" accept=".json" @change="handleFileSelected" />
|
||
|
||
<div ref="canvas" class="diagram-canvas" :style="{
|
||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
||
}">
|
||
<!-- 渲染连线 -->
|
||
<svg class="wires-layer" 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" :class="{
|
||
'component-hover': hoveredComponent === component.id,
|
||
'component-selected': selectedComponentId === component.id,
|
||
'component-disabled': !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="getComponentDefinition(component.type)" v-if="props.componentModules[component.type]"
|
||
v-bind="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) => 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>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 通知组件 -->
|
||
<div v-if="showNotification" class="toast toast-top toast-center z-50 w-fit-content">
|
||
<div :class="`alert ${notificationType === 'success'
|
||
? 'alert-success'
|
||
: notificationType === 'error'
|
||
? 'alert-error'
|
||
: 'alert-info'
|
||
}`">
|
||
<span>{{ notificationMessage }}</span>
|
||
</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(scale * 100) }}%</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted, onUnmounted, computed, watch } from "vue";
|
||
import WireComponent from "./equipments/Wire.vue";
|
||
|
||
// 导入 diagram 管理器
|
||
import {
|
||
loadDiagramData,
|
||
saveDiagramData,
|
||
updatePartPosition,
|
||
updatePartAttribute,
|
||
deletePart,
|
||
addConnection,
|
||
deleteConnection,
|
||
findConnectionsByPart,
|
||
moveGroupComponents,
|
||
parseConnectionPin,
|
||
connectionArrayToWireItem,
|
||
validateDiagramData,
|
||
} from "./diagramManager";
|
||
|
||
import type {
|
||
DiagramData,
|
||
DiagramPart,
|
||
ConnectionArray,
|
||
WireItem,
|
||
} from "./diagramManager";
|
||
import { toString } from "lodash";
|
||
|
||
// 右键菜单处理函数
|
||
function handleContextMenu(e: MouseEvent) {
|
||
e.preventDefault();
|
||
}
|
||
|
||
// 定义组件发出的事件
|
||
const emit = defineEmits([
|
||
"diagram-updated",
|
||
"component-selected",
|
||
"component-moved",
|
||
"component-delete",
|
||
"wire-created",
|
||
"wire-deleted",
|
||
"load-component-module",
|
||
"open-components",
|
||
]);
|
||
|
||
// 定义组件接受的属性
|
||
const props = defineProps<{
|
||
componentModules: Record<string, any>;
|
||
}>();
|
||
|
||
// --- 画布状态 ---
|
||
const canvasContainer = ref<HTMLElement | null>(null);
|
||
const canvas = ref<HTMLElement | null>(null);
|
||
const position = reactive({ x: 0, y: 0 });
|
||
const scale = ref(1);
|
||
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 });
|
||
|
||
// Diagram 数据
|
||
const diagramData = ref<DiagramData>({
|
||
version: 1,
|
||
author: "admin",
|
||
editor: "me",
|
||
parts: [],
|
||
connections: [],
|
||
});
|
||
|
||
// 组件引用跟踪
|
||
const componentRefs = ref<Record<string, any>>({});
|
||
|
||
// 计算属性:从 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 = componentRefs.value?.[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 = componentRefs.value?.[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 showNotification = ref(false);
|
||
const notificationMessage = ref("");
|
||
const notificationType = ref<"success" | "error" | "info">("info");
|
||
// 保存toast定时器ID
|
||
const toastTimers: number[] = [];
|
||
|
||
// 文件选择引用
|
||
const fileInput = ref<HTMLInputElement | null>(null);
|
||
|
||
// --- 缩放功能 ---
|
||
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 mouseXCanvas = (mouseX - position.x) / scale.value;
|
||
const mouseYCanvas = (mouseY - position.y) / scale.value;
|
||
|
||
// 计算缩放值
|
||
const zoomFactor = 1.1; // 每次放大/缩小10%
|
||
const direction = e.deltaY > 0 ? -1 : 1;
|
||
|
||
// 计算新的缩放值
|
||
let newScale =
|
||
direction > 0 ? scale.value * zoomFactor : scale.value / zoomFactor;
|
||
newScale = Math.max(MIN_SCALE, Math.min(newScale, MAX_SCALE));
|
||
|
||
// 计算新的位置,使鼠标指针位置在缩放前后保持不变
|
||
position.x = mouseX - mouseXCanvas * newScale;
|
||
position.y = mouseY - mouseYCanvas * newScale;
|
||
|
||
// 更新缩放值
|
||
scale.value = newScale;
|
||
}
|
||
|
||
// --- 动态组件渲染 ---
|
||
const getComponentDefinition = (type: string) => {
|
||
const module = props.componentModules[type];
|
||
if (!module) return null;
|
||
|
||
// 确保我们返回一个有效的组件定义
|
||
if (module.default) {
|
||
return module.default;
|
||
} else if (module.__esModule && module.default) {
|
||
// 有时 Vue 的动态导入会将默认导出包装在 __esModule 属性下
|
||
return module.default;
|
||
} else {
|
||
console.warn(
|
||
`Module for ${type} found but default export is missing`,
|
||
module,
|
||
);
|
||
return null;
|
||
}
|
||
};
|
||
|
||
function prepareComponentProps(
|
||
attrs: Record<string, any>,
|
||
componentId?: string,
|
||
): Record<string, any> {
|
||
const result: Record<string, any> = { ...attrs };
|
||
if (componentId) {
|
||
result.componentId = componentId;
|
||
}
|
||
// console.log(`组件属性 ID: ${componentId}`, result);
|
||
return result;
|
||
}
|
||
|
||
// 设置组件引用
|
||
function setComponentRef(componentId: string, el: any) {
|
||
if (componentRefs.value) {
|
||
if (el) {
|
||
componentRefs.value[componentId] = el;
|
||
} else {
|
||
delete componentRefs.value[componentId];
|
||
}
|
||
}
|
||
}
|
||
|
||
// 重置组件引用缓存
|
||
function resetComponentRefs() {
|
||
componentRefs.value = {};
|
||
}
|
||
|
||
// 加载组件模块
|
||
async function loadComponentModule(type: string) {
|
||
if (!props.componentModules[type]) {
|
||
try {
|
||
// 通知父组件需要加载此类型的组件
|
||
emit("load-component-module", type);
|
||
} catch (error) {
|
||
console.error(`Failed to request component module ${type}:`, error);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 画布交互逻辑 ---
|
||
function handleCanvasMouseDown(e: MouseEvent) {
|
||
// 如果是直接点击画布(而不是元器件),清除选中状态
|
||
if (e.target === canvasContainer.value || e.target === canvas.value) {
|
||
if (selectedComponentId.value !== null) {
|
||
selectedComponentId.value = null;
|
||
emit("component-selected", 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;
|
||
dragStart.x = e.clientX - position.x;
|
||
dragStart.y = e.clientY - position.y;
|
||
|
||
document.addEventListener("mousemove", onDrag);
|
||
document.addEventListener("mouseup", stopDrag);
|
||
e.preventDefault();
|
||
}
|
||
|
||
// 中键拖拽画布
|
||
function startMiddleDrag(e: MouseEvent) {
|
||
if (e.button !== 1) return;
|
||
|
||
isMiddleDragging.value = true;
|
||
isDragging.value = false;
|
||
draggingComponentId.value = null;
|
||
|
||
dragStart.x = e.clientX - position.x;
|
||
dragStart.y = e.clientY - position.y;
|
||
|
||
document.addEventListener("mousemove", onDrag);
|
||
document.addEventListener("mouseup", stopDrag);
|
||
e.preventDefault();
|
||
}
|
||
|
||
// 拖拽画布过程
|
||
function onDrag(e: MouseEvent) {
|
||
if (!isDragging.value && !isMiddleDragging.value) return;
|
||
|
||
position.x = e.clientX - dragStart.x;
|
||
position.y = e.clientY - dragStart.y;
|
||
}
|
||
|
||
// 停止拖拽画布
|
||
function stopDrag() {
|
||
isDragging.value = false;
|
||
isMiddleDragging.value = false;
|
||
|
||
document.removeEventListener("mousemove", onDrag);
|
||
document.removeEventListener("mouseup", stopDrag);
|
||
}
|
||
|
||
// --- 组件拖拽交互 ---
|
||
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;
|
||
emit("component-selected", 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();
|
||
|
||
// 计算鼠标在画布坐标系中的位置
|
||
const mouseX_canvas =
|
||
(e.clientX - containerRect.left - position.x) / scale.value;
|
||
const mouseY_canvas =
|
||
(e.clientY - containerRect.top - position.y) / scale.value;
|
||
|
||
// 计算鼠标相对于组件左上角的偏移量
|
||
componentDragOffset.x = mouseX_canvas - component.x;
|
||
componentDragOffset.y = mouseY_canvas - component.y;
|
||
|
||
// 添加全局监听器
|
||
document.addEventListener("mousemove", onComponentDrag);
|
||
document.addEventListener("mouseup", stopComponentDrag);
|
||
}
|
||
|
||
// 拖拽组件过程
|
||
function onComponentDrag(e: MouseEvent) {
|
||
if (!draggingComponentId.value || !canvasContainer.value) return;
|
||
|
||
// 防止触发组件内部的事件
|
||
e.stopPropagation();
|
||
e.preventDefault();
|
||
|
||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||
|
||
// 计算鼠标在画布坐标系中的位置
|
||
const mouseX_canvas =
|
||
(e.clientX - containerRect.left - position.x) / scale.value;
|
||
const mouseY_canvas =
|
||
(e.clientY - containerRect.top - position.y) / scale.value;
|
||
|
||
// 计算组件新位置
|
||
const newX = mouseX_canvas - componentDragOffset.x;
|
||
const newY = mouseY_canvas - componentDragOffset.y;
|
||
|
||
// 获取当前拖动的组件
|
||
const draggedComponent = diagramParts.value.find(
|
||
(p) => p.id === draggingComponentId.value,
|
||
);
|
||
if (!draggedComponent) return;
|
||
|
||
// 更新组件位置
|
||
// diagramData.value = updatePartPosition(
|
||
// diagramData.value,
|
||
// draggingComponentId.value,
|
||
// Math.round(newX),
|
||
// Math.round(newY),
|
||
// );
|
||
|
||
// 如果组件属于组,移动组内所有其他组件
|
||
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 &&
|
||
!p.positionlock,
|
||
);
|
||
|
||
// 更新这些组件的位置
|
||
for (const groupComp of groupComponents) {
|
||
diagramData.value = updatePartPosition(
|
||
diagramData.value,
|
||
groupComp.id,
|
||
groupComp.x + deltaX,
|
||
groupComp.y + deltaY,
|
||
);
|
||
}
|
||
}
|
||
|
||
// 通知父组件位置已更新
|
||
emit("component-moved", {
|
||
id: draggingComponentId.value,
|
||
x: Math.round(newX),
|
||
y: Math.round(newY),
|
||
});
|
||
|
||
// 通知图表已更新
|
||
emit("diagram-updated", diagramData.value);
|
||
}
|
||
|
||
// 停止拖拽组件
|
||
function stopComponentDrag() {
|
||
// 如果有组件被拖拽,保存当前状态
|
||
if (draggingComponentId.value) {
|
||
console.log(`组件拖拽结束: ${draggingComponentId.value}`);
|
||
|
||
// 保存图表数据
|
||
saveDiagramData(diagramData.value);
|
||
|
||
// 清除拖动状态
|
||
draggingComponentId.value = null;
|
||
}
|
||
|
||
document.removeEventListener("mousemove", onComponentDrag);
|
||
document.removeEventListener("mouseup", stopComponentDrag);
|
||
}
|
||
|
||
// 更新组件属性
|
||
function updateComponentProp(
|
||
componentId: string,
|
||
propName: string,
|
||
value: any,
|
||
) {
|
||
diagramData.value = updatePartAttribute(
|
||
diagramData.value,
|
||
componentId,
|
||
propName,
|
||
value,
|
||
);
|
||
emit("diagram-updated", diagramData.value);
|
||
saveDiagramData(diagramData.value);
|
||
}
|
||
|
||
// --- 连线操作 ---
|
||
// 更新鼠标位置
|
||
function updateMousePosition(e: MouseEvent) {
|
||
if (!canvasContainer.value) return;
|
||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||
mousePosition.x = (e.clientX - containerRect.left - position.x) / scale.value;
|
||
mousePosition.y = (e.clientY - containerRect.top - position.y) / scale.value;
|
||
} // 处理引脚点击
|
||
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) {
|
||
// 开始创建连线
|
||
const containerRect = canvasContainer.value.getBoundingClientRect();
|
||
// 获取初始位置信息
|
||
let pinPosition = pinInfo.position;
|
||
console.log("Pin信息:", pinInfo);
|
||
console.log("Pin初始位置:", pinPosition);
|
||
console.log("组件ID:", componentId);
|
||
console.log("引脚ID:", pinId);
|
||
|
||
// 从组件引用中获取组件实例
|
||
const component = componentRefs.value[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,
|
||
);
|
||
document.addEventListener("mousemove", onCreatingWireMouseMove);
|
||
} 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 = componentRefs.value[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],
|
||
};
|
||
|
||
// 通知连线创建
|
||
emit("wire-created", newConnection);
|
||
emit("diagram-updated", diagramData.value);
|
||
|
||
// 保存图表数据
|
||
saveDiagramData(diagramData.value);
|
||
|
||
// 重置连线创建状态
|
||
resetWireCreation();
|
||
document.removeEventListener("mousemove", onCreatingWireMouseMove);
|
||
}
|
||
}
|
||
|
||
// 开始创建连线
|
||
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();
|
||
document.removeEventListener("mousemove", onCreatingWireMouseMove);
|
||
}
|
||
|
||
// 连线创建过程中的鼠标移动
|
||
function onCreatingWireMouseMove(e: MouseEvent) {
|
||
updateMousePosition(e);
|
||
}
|
||
|
||
// 删除连线
|
||
function deleteWire(wireIndex: number) {
|
||
diagramData.value = deleteConnection(diagramData.value, wireIndex);
|
||
emit("wire-deleted", wireIndex);
|
||
emit("diagram-updated", diagramData.value);
|
||
saveDiagramData(diagramData.value);
|
||
}
|
||
|
||
// 删除组件
|
||
function deleteComponent(componentId: string) {
|
||
diagramData.value = deletePart(diagramData.value, componentId);
|
||
emit("component-delete", componentId);
|
||
emit("diagram-updated", diagramData.value);
|
||
saveDiagramData(diagramData.value);
|
||
|
||
// 清除选中状态
|
||
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) {
|
||
showToast(
|
||
`不是有效的diagram.json格式: ${validation.errors.join("; ")}`,
|
||
"error",
|
||
);
|
||
isLoading.value = false;
|
||
return;
|
||
}
|
||
|
||
// 更新画布数据
|
||
diagramData.value = parsed as DiagramData;
|
||
|
||
// 保存到本地文件
|
||
saveDiagramData(diagramData.value);
|
||
|
||
// 发出更新事件
|
||
emit("diagram-updated", diagramData.value);
|
||
|
||
showToast(`成功导入diagram文件`, "success");
|
||
} catch (error) {
|
||
console.error("解析JSON文件出错:", error);
|
||
if (document.body.contains(canvasContainer.value)) {
|
||
showToast("解析文件出错,请确认是有效的JSON格式", "error");
|
||
}
|
||
} finally {
|
||
// 检查组件是否仍然挂载
|
||
if (document.body.contains(canvasContainer.value)) {
|
||
// 结束加载状态
|
||
isLoading.value = false;
|
||
}
|
||
|
||
// 清除文件输入,以便同一文件可以再次导入
|
||
target.value = "";
|
||
}
|
||
};
|
||
reader.onerror = () => {
|
||
// 检查组件是否仍然挂载
|
||
if (document.body.contains(canvasContainer.value)) {
|
||
showToast("读取文件时出错", "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对象
|
||
const timerId = setTimeout(() => {
|
||
URL.revokeObjectURL(url);
|
||
// 检查组件是否仍然挂载
|
||
if (document.body.contains(canvasContainer.value)) {
|
||
isLoading.value = false;
|
||
showToast("成功导出diagram文件", "success");
|
||
}
|
||
}, 100);
|
||
|
||
// 将定时器ID保存起来,以便在组件卸载时清除
|
||
toastTimers.push(timerId);
|
||
} catch (error) {
|
||
console.error("导出diagram文件时出错:", error);
|
||
showToast("导出diagram文件时出错", "error");
|
||
isLoading.value = false;
|
||
}
|
||
}
|
||
|
||
// 显示通知
|
||
function showToast(
|
||
message: string,
|
||
type: "success" | "error" | "info" = "info",
|
||
duration = 3000,
|
||
) {
|
||
notificationMessage.value = message;
|
||
notificationType.value = type;
|
||
showNotification.value = true;
|
||
|
||
// 保存定时器ID以便清除
|
||
const timerId = setTimeout(() => {
|
||
// 检查组件是否仍然挂载
|
||
if (document.body.contains(canvasContainer.value)) {
|
||
showNotification.value = false;
|
||
}
|
||
}, duration);
|
||
|
||
// 将定时器ID保存起来,以便在组件卸载时清除
|
||
toastTimers.push(timerId);
|
||
}
|
||
|
||
// --- 生命周期钩子 ---
|
||
onMounted(async () => {
|
||
// 重置组件引用
|
||
resetComponentRefs();
|
||
|
||
// 加载图表数据
|
||
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),
|
||
);
|
||
|
||
// 通知父组件需要加载组件模块
|
||
componentTypes.forEach((type) => {
|
||
if (!props.componentModules[type]) {
|
||
emit("load-component-module", type);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.error("加载图表数据失败:", error);
|
||
}
|
||
// 初始化中心位置
|
||
if (canvasContainer.value) {
|
||
// 修改为将画布中心点放在容器中心点
|
||
position.x = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
|
||
position.y = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
|
||
}
|
||
|
||
// 添加键盘事件监听器
|
||
window.addEventListener("keydown", handleKeyDown);
|
||
});
|
||
|
||
// 处理键盘事件
|
||
function handleKeyDown(e: KeyboardEvent) {
|
||
// 如果当前有选中的组件,并且按下了Delete键
|
||
if (selectedComponentId.value && e.key === "Delete") {
|
||
// 触发删除组件事件
|
||
deleteComponent(selectedComponentId.value);
|
||
}
|
||
|
||
// 如果当前正在创建连线,并且按下了ESC键
|
||
if (isCreatingWire.value && e.key === "Escape") {
|
||
// 取消连线创建
|
||
cancelWireCreation();
|
||
}
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
// 清理事件监听器
|
||
document.removeEventListener("mousemove", onComponentDrag);
|
||
document.removeEventListener("mouseup", stopComponentDrag);
|
||
document.removeEventListener("mousemove", onDrag);
|
||
document.removeEventListener("mouseup", stopDrag);
|
||
document.removeEventListener("mousemove", onCreatingWireMouseMove);
|
||
|
||
// 移除键盘事件监听器
|
||
window.removeEventListener("keydown", handleKeyDown);
|
||
|
||
// 清除所有toast定时器
|
||
toastTimers.forEach((timerId) => clearTimeout(timerId));
|
||
});
|
||
|
||
// --- 对外API ---
|
||
// 获取当前图表数据
|
||
function getDiagramData() {
|
||
return diagramData.value;
|
||
}
|
||
|
||
// 设置图表数据
|
||
function setDiagramData(data: DiagramData) {
|
||
diagramData.value = data;
|
||
emit("diagram-updated", data);
|
||
}
|
||
|
||
// 无加载动画的数据更新方法
|
||
function updateDiagramDataDirectly(data: DiagramData) {
|
||
// 检查组件是否仍然挂载
|
||
if (!document.body.contains(canvasContainer.value)) {
|
||
return; // 如果组件已经卸载,不执行后续操作
|
||
}
|
||
|
||
diagramData.value = data;
|
||
saveDiagramData(data);
|
||
|
||
// 发出diagram-updated事件
|
||
emit("diagram-updated", data);
|
||
}
|
||
|
||
// 暴露方法给父组件
|
||
defineExpose({
|
||
// 基本数据操作
|
||
getDiagramData: () => diagramData.value,
|
||
updateDiagramDataDirectly,
|
||
setDiagramData: (data: DiagramData) => {
|
||
// 检查组件是否仍然挂载
|
||
if (!document.body.contains(canvasContainer.value)) {
|
||
return; // 如果组件已经卸载,不执行后续操作
|
||
}
|
||
|
||
isLoading.value = true;
|
||
|
||
// 使用requestAnimationFrame确保UI更新
|
||
window.requestAnimationFrame(() => {
|
||
// 再次检查组件是否仍然挂载
|
||
if (!document.body.contains(canvasContainer.value)) {
|
||
return; // 如果组件已经卸载,不执行后续操作
|
||
}
|
||
|
||
diagramData.value = data;
|
||
saveDiagramData(data);
|
||
|
||
// 发出diagram-updated事件
|
||
emit("diagram-updated", data);
|
||
|
||
// 短暂延迟后结束加载状态,以便UI能更新
|
||
const timerId = setTimeout(() => {
|
||
// 检查组件是否仍然挂载
|
||
if (document.body.contains(canvasContainer.value)) {
|
||
isLoading.value = false;
|
||
}
|
||
}, 200);
|
||
|
||
// 将定时器ID保存起来,以便在组件卸载时清除
|
||
toastTimers.push(timerId);
|
||
});
|
||
},
|
||
|
||
// 文件操作
|
||
openDiagramFileSelector,
|
||
exportDiagram,
|
||
|
||
// 组件操作
|
||
getSelectedComponent: () => {
|
||
if (!selectedComponentId.value) return null;
|
||
return (
|
||
diagramParts.value.find((p) => p.id === selectedComponentId.value) || null
|
||
);
|
||
},
|
||
deleteSelectedComponent: () => {
|
||
if (selectedComponentId.value) {
|
||
deleteComponent(selectedComponentId.value);
|
||
}
|
||
},
|
||
|
||
// 画布状态
|
||
getCanvasPosition: () => ({ x: position.x, y: position.y }),
|
||
getScale: () => scale.value,
|
||
|
||
// 通知系统
|
||
showToast,
|
||
});
|
||
|
||
// 监视器 - 当图表数据更改时保存
|
||
watch(
|
||
diagramData,
|
||
(newData) => {
|
||
saveDiagramData(newData);
|
||
},
|
||
{ deep: true },
|
||
);
|
||
|
||
// 当组件模块加载完成后,确保组件引用正确建立
|
||
watch(
|
||
() => props.componentModules,
|
||
() => {
|
||
// 这里不需要特别处理,Vue 会自动通过 setComponentRef 函数更新引用
|
||
},
|
||
{ deep: true },
|
||
);
|
||
</script>
|
||
|
||
<style scoped>
|
||
.diagram-container {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
overflow: hidden;
|
||
background-size:
|
||
20px 20px,
|
||
20px 20px,
|
||
100px 100px,
|
||
100px 100px;
|
||
background-position: 0 0;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-moz-user-select: none;
|
||
-ms-user-select: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.diagram-canvas {
|
||
position: relative;
|
||
width: 10000px;
|
||
height: 10000px;
|
||
transform-origin: 0 0;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-moz-user-select: none;
|
||
-ms-user-select: none;
|
||
}
|
||
|
||
/* 连线层样式 */
|
||
.wires-layer {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: auto;
|
||
/* 修复:允许线被点击 */
|
||
z-index: 50;
|
||
overflow: visible;
|
||
/* 确保超出SVG范围的内容也能显示 */
|
||
}
|
||
|
||
.wires-layer path {
|
||
pointer-events: stroke;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* 元器件容器样式 */
|
||
.component-wrapper {
|
||
position: absolute;
|
||
padding: 0;
|
||
/* 移除内边距,确保元素大小与内容完全匹配 */
|
||
box-sizing: content-box;
|
||
/* 使用content-box确保内容尺寸不受padding影响 */
|
||
display: inline-block;
|
||
overflow: visible;
|
||
/* 允许内容溢出(用于显示边框) */
|
||
cursor: move;
|
||
/* 显示移动光标 */
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-moz-user-select: none;
|
||
-ms-user-select: none;
|
||
}
|
||
|
||
/* 悬停状态 - 使用outline而非伪元素 */
|
||
.component-hover {
|
||
outline: 2px dashed #3498db;
|
||
outline-offset: 2px;
|
||
z-index: 2;
|
||
}
|
||
|
||
/* 选中状态 - 使用outline而非伪元素 */
|
||
.component-selected {
|
||
outline: 3px dashed;
|
||
outline-color: #e74c3c #f39c12 #3498db #2ecc71;
|
||
outline-offset: 3px;
|
||
}
|
||
|
||
/* 禁用状态 */
|
||
.component-disabled {
|
||
cursor: not-allowed;
|
||
filter: grayscale(70%);
|
||
}
|
||
|
||
/* 隐藏引脚状态 */
|
||
.component-hidepins :deep([data-pin-wrapper]) {
|
||
display: none;
|
||
}
|
||
|
||
/* 为黑暗模式设置不同的网格线颜色 */
|
||
/* :root[data-theme="dark"] .diagram-container {
|
||
background-image:
|
||
linear-gradient(to right, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||
linear-gradient(to bottom, rgba(200, 200, 200, 0.1) 1px, transparent 1px),
|
||
linear-gradient(to right, rgba(180, 180, 180, 0.15) 100px, transparent 100px),
|
||
linear-gradient(to bottom, rgba(180, 180, 180, 0.15) 100px, transparent 100px);
|
||
} */
|
||
|
||
/* 深度选择器 - 默认阻止SVG内部元素的鼠标事件,但允许SVG本身和特定交互元素 */
|
||
.component-wrapper :deep(svg) {
|
||
pointer-events: auto;
|
||
/* 确保SVG本身可以接收鼠标事件用于拖拽 */
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
-moz-user-select: none;
|
||
-ms-user-select: none;
|
||
}
|
||
|
||
.component-wrapper :deep(svg *:not([class*="interactive"]):not(rect.glow):not(circle[fill-opacity]):not([fill-opacity])) {
|
||
pointer-events: none;
|
||
/* 非交互元素不接收鼠标事件 */
|
||
}
|
||
|
||
/* 允许特定SVG元素接收鼠标事件,用于交互 */
|
||
.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>
|