feat: 添加全局alert,并替换原先是toast

This commit is contained in:
SikongJueluo 2025-07-09 19:06:41 +08:00
parent 53027470fe
commit cbb3543c4a
No known key found for this signature in database
5 changed files with 363 additions and 128 deletions

1
components.d.ts vendored
View File

@ -9,6 +9,7 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
Alert: typeof import('./src/components/Alert/Alert.vue')['default'] Alert: typeof import('./src/components/Alert/Alert.vue')['default']
AlertDemo: typeof import('./src/components/AlertDemo.vue')['default']
BaseBoard: typeof import('./src/components/equipments/BaseBoard.vue')['default'] BaseBoard: typeof import('./src/components/equipments/BaseBoard.vue')['default']
BaseInputField: typeof import('./src/components/InputField/BaseInputField.vue')['default'] BaseInputField: typeof import('./src/components/InputField/BaseInputField.vue')['default']
Canvas: typeof import('./src/components/Canvas.vue')['default'] Canvas: typeof import('./src/components/Canvas.vue')['default']

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import Navbar from "./components/Navbar.vue"; import Navbar from "./components/Navbar.vue";
import Dialog from "./components/Dialog.vue"; import Dialog from "./components/Dialog.vue";
import { Alert, useAlertProvider } from "./components/Alert";
import { ref, provide, computed, onMounted } from "vue"; import { ref, provide, computed, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@ -49,19 +50,26 @@ provide("theme", {
const currentRoutePath = computed(() => { const currentRoutePath = computed(() => {
return router.currentRoute.value.path; return router.currentRoute.value.path;
}); });
useAlertProvider();
</script> </script>
<template> <template>
<div> <div>
<header class="relative"> <header class="relative">
<Navbar></Navbar> <Navbar />
<Dialog></Dialog> <Dialog />
<Alert />
</header> </header>
<main> <main>
<RouterView /> <RouterView />
</main> </main>
<footer v-if="currentRoutePath != '/project'" class="footer footer-center p-4 bg-base-300 text-base-content">
<footer
v-if="currentRoutePath != '/project'"
class="footer footer-center p-4 bg-base-300 text-base-content"
>
<div> <div>
<p>Copyright © 2023 - All right reserved by OurEDA</p> <p>Copyright © 2023 - All right reserved by OurEDA</p>
</div> </div>

View File

@ -0,0 +1,102 @@
<template>
<div class="fixed left-1/2 top-30 z-50 -translate-x-1/2">
<transition
name="alert"
enter-active-class="alert-enter-active"
leave-active-class="alert-leave-active"
enter-from-class="alert-enter-from"
enter-to-class="alert-enter-to"
leave-from-class="alert-leave-from"
leave-to-class="alert-leave-to"
>
<div
v-if="alertStore?.alertState.value.visible"
:class="alertClasses"
class="alert"
>
<div class="flex items-center gap-2">
<!-- Icons for different alert types -->
<CheckCircle
v-if="alertStore?.alertState.value.type === 'success'"
class="h-6 w-6 shrink-0 stroke-current"
/>
<XCircle
v-else-if="alertStore?.alertState.value.type === 'error'"
class="h-6 w-6 shrink-0 stroke-current"
/>
<AlertTriangle
v-else-if="alertStore?.alertState.value.type === 'warning'"
class="h-6 w-6 shrink-0 stroke-current"
/>
<Info v-else class="h-6 w-6 shrink-0 stroke-current" />
<span>{{ alertStore?.alertState.value.message }}</span>
</div>
<div class="flex-none">
<button
class="btn btn-sm btn-circle btn-ghost"
@click="alertStore?.hide"
>
<X class="h-4 w-4" />
</button>
</div>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { CheckCircle, XCircle, AlertTriangle, Info, X } from "lucide-vue-next";
import { useAlertStore } from ".";
const alertStore = useAlertStore();
// Computed classes for different alert types
const alertClasses = computed(() => {
const baseClasses = "shadow-lg max-w-sm";
switch (alertStore?.alertState.value.type) {
case "success":
return `${baseClasses} alert-success`;
case "error":
return `${baseClasses} alert-error`;
case "warning":
return `${baseClasses} alert-warning`;
case "info":
default:
return `${baseClasses} alert-info`;
}
});
</script>
<style scoped>
/* 进入和离开的过渡动画持续时间 */
.alert-enter-active,
.alert-leave-active {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
/* 进入的起始状态 */
.alert-enter-from {
opacity: 0;
transform: translateY(-20px) scale(0.95);
}
/* 进入的结束状态 */
.alert-enter-to {
opacity: 1;
transform: translateY(0) scale(1);
}
/* 离开的起始状态 */
.alert-leave-from {
opacity: 1;
transform: translateY(0) scale(1);
}
/* 离开的结束状态 */
.alert-leave-to {
opacity: 0;
transform: translateY(-10px) scale(0.98);
}
</style>

View File

@ -0,0 +1,61 @@
import { ref, computed } from "vue";
import Alert from "./Alert.vue";
import { createInjectionState } from "@vueuse/core";
export interface AlertState {
visible: boolean;
message: string;
type: "success" | "error" | "warning" | "info";
}
// create injectivon state using vueuse
const [useAlertProvider, useAlertStore] = createInjectionState(() => {
const alertState = ref<AlertState>({
visible: false,
message: "",
type: "info",
});
let timeoutId: number | null = null;
function show(
message: string,
type: AlertState["type"] = "info",
duration = 2000,
) {
// Clear existing timeout
if (timeoutId) {
window.clearTimeout(timeoutId);
}
alertState.value = {
visible: true,
message,
type,
};
// Auto hide after duration
if (duration > 0) {
timeoutId = window.setTimeout(() => {
hide();
}, duration);
}
}
function hide() {
alertState.value.visible = false;
if (timeoutId) {
window.clearTimeout(timeoutId);
timeoutId = null;
}
}
return {
alertState,
show,
hide,
};
});
export { Alert, useAlertProvider, useAlertStore };

View File

@ -1,70 +1,148 @@
<template> <template>
<div class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container" ref="canvasContainer" <div
@mousedown="handleCanvasMouseDown" @mousedown.middle.prevent="startMiddleDrag" @wheel.prevent="onZoom" class="flex-1 h-full w-full bg-base-200 relative overflow-hidden diagram-container"
@contextmenu.prevent="handleContextMenu"> 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"> <div class="absolute top-2 right-2 flex gap-2 z-30">
<button class="btn btn-sm btn-primary" @click="openDiagramFileSelector"> <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" <svg
stroke="currentColor"> xmlns="http://www.w3.org/2000/svg"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="h-4 w-4 mr-1"
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" /> 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> </svg>
导入 导入
</button> </button>
<button class="btn btn-sm btn-primary" @click="exportDiagram"> <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" <svg
stroke="currentColor"> xmlns="http://www.w3.org/2000/svg"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="h-4 w-4 mr-1"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> 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> </svg>
导出 导出
</button> </button>
<button class="btn btn-sm btn-primary" @click="emit('open-components')"> <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" <svg
stroke="currentColor"> xmlns="http://www.w3.org/2000/svg"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /> 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> </svg>
添加组件 添加组件
</button> </button>
<button class="btn btn-sm btn-primary" @click="emit('toggle-doc-panel')"> <button class="btn btn-sm btn-primary" @click="emit('toggle-doc-panel')">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" <svg
stroke="currentColor"> xmlns="http://www.w3.org/2000/svg"
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" class="h-4 w-4 mr-1"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg> </svg>
{{ props.showDocPanel ? "属性面板" : "文档" }} {{ props.showDocPanel ? "属性面板" : "文档" }}
</button> </button>
</div> </div>
<!-- 隐藏的文件输入 --> <!-- 隐藏的文件输入 -->
<input type="file" ref="fileInput" class="hidden" accept=".json" @change="handleFileSelected" /> <input
type="file"
ref="fileInput"
class="hidden"
accept=".json"
@change="handleFileSelected"
/>
<div ref="canvas" class="diagram-canvas" :style="{ <div
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`, ref="canvas"
}"> class="diagram-canvas"
:style="{
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
}"
>
<!-- 渲染连线 --> <!-- 渲染连线 -->
<svg class="wires-layer" width="10000" height="10000"> <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" <WireComponent
:start-y="wire.startY" :end-x="wire.endX" :end-y="wire.endY" :stroke-color="wire.color || '#4a5568'" v-for="(wire, index) in wireItems"
:stroke-width="wire.strokeWidth" :is-active="false" :start-component-id="wire.startComponentId" :key="wire.id"
:start-pin-id="wire.startPinId" :end-component-id="wire.endComponentId" :end-pin-id="wire.endPinId" :id="wire.id"
:routing-mode="wire.routingMode" :path-commands="wire.pathCommands" /> :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" <WireComponent
:start-y="creatingWireStart.y" :end-x="mousePosition.x" :end-y="mousePosition.y" stroke-color="#3182ce" v-if="isCreatingWire"
:stroke-width="2" :is-active="true" /> 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> </svg>
<!-- 渲染画布上的组件 --> <!-- 渲染画布上的组件 -->
<div v-for="component in diagramParts" :key="component.id" class="component-wrapper" :class="{ <div
'component-hover': hoveredComponent === component.id, v-for="component in diagramParts"
'component-selected': selectedComponentId === component.id, :key="component.id"
'component-disabled': !component.isOn, class="component-wrapper"
'component-hidepins': component.hidepins, :class="{
}" :style="{ 'component-hover': hoveredComponent === component.id,
'component-selected': selectedComponentId === component.id,
'component-disabled': !component.isOn,
'component-hidepins': component.hidepins,
}"
:style="{
top: component.y + 'px', top: component.y + 'px',
left: component.x + 'px', left: component.x + 'px',
zIndex: component.index ?? 0, zIndex: component.index ?? 0,
@ -73,56 +151,74 @@
: 'none', : 'none',
opacity: component.isOn ? 1 : 0.6, opacity: component.isOn ? 1 : 0.6,
display: 'block', display: 'block',
}" @mousedown.left.stop="startComponentDrag($event, component)" @mouseover=" }"
@mousedown.left.stop="startComponentDrag($event, component)"
@mouseover="
(event) => { (event) => {
hoveredComponent = component.id; hoveredComponent = component.id;
} }
" @mouseleave=" "
@mouseleave="
(event) => { (event) => {
hoveredComponent = null; hoveredComponent = null;
} }
"> "
>
<!-- 动态渲染组件 --> <!-- 动态渲染组件 -->
<component :is="componentManager.getComponentDefinition(component.type)" <component
v-if="componentManager.componentModules.value[component.type] && componentManager.getComponentDefinition(component.type)" :is="componentManager.getComponentDefinition(component.type)"
v-bind="componentManager.prepareComponentProps(component.attrs || {}, component.id)" @update:bindKey=" v-if="
componentManager.componentModules.value[component.type] &&
componentManager.getComponentDefinition(component.type)
"
v-bind="
componentManager.prepareComponentProps(
component.attrs || {},
component.id,
)
"
@update:bindKey="
(value: string) => (value: string) =>
updateComponentProp(component.id, 'bindKey', value) updateComponentProp(component.id, 'bindKey', value)
" @pin-click=" "
@pin-click="
(pinInfo: any) => (pinInfo: any) =>
handlePinClick(component.id, pinInfo, pinInfo.originalEvent) handlePinClick(component.id, pinInfo, pinInfo.originalEvent)
" :ref="(el: any) => componentManager.setComponentRef(component.id, el)" /> "
:ref="(el: any) => componentManager.setComponentRef(component.id, el)"
/>
<!-- Fallback if component module not loaded yet --> <!-- Fallback if component module not loaded yet -->
<div v-else <div
class="p-2 text-xs text-gray-400 border border-dashed border-gray-400 flex items-center justify-center"> 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="flex flex-col items-center">
<div class="loading loading-spinner loading-xs mb-1"></div> <div class="loading loading-spinner loading-xs mb-1"></div>
<span>Loading {{ component.type }}...</span> <span>Loading {{ component.type }}...</span>
<small class="mt-1 text-xs">{{ componentManager.componentModules.value[component.type] ? 'Module loaded but invalid' : 'Module not found' }}</small> <small class="mt-1 text-xs">{{
componentManager.componentModules.value[component.type]
? "Module loaded but invalid"
: "Module not found"
}}</small>
</div> </div>
</div> </div>
</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
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 class="loading loading-spinner loading-lg text-primary"></div>
</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"> <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> <span class="text-sm font-medium">{{ Math.round(scale * 100) }}%</span>
</div> </div>
</div> </div>
@ -140,6 +236,7 @@ import {
} from "vue"; } from "vue";
import { useEventListener } from "@vueuse/core"; import { useEventListener } from "@vueuse/core";
import WireComponent from "@/components/equipments/Wire.vue"; import WireComponent from "@/components/equipments/Wire.vue";
import { useAlertStore } from "@/components/Alert";
// diagram // diagram
import { import {
@ -186,9 +283,14 @@ const props = defineProps<{
// componentManager // componentManager
const componentManager = useComponentManager(); const componentManager = useComponentManager();
if (!componentManager) { if (!componentManager) {
throw new Error("DiagramCanvas must be used within a component manager provider"); throw new Error(
"DiagramCanvas must be used within a component manager provider",
);
} }
// Alert store
const alertStore = useAlertStore();
// --- --- // --- ---
const canvasContainer = ref<HTMLElement | null>(null); const canvasContainer = ref<HTMLElement | null>(null);
const canvas = ref<HTMLElement | null>(null); const canvas = ref<HTMLElement | null>(null);
@ -215,7 +317,9 @@ const diagramData = ref<DiagramData>({
}); });
// 便 // 便
const componentRefs = computed(() => componentManager?.componentRefs.value || {}); const componentRefs = computed(
() => componentManager?.componentRefs.value || {},
);
// diagramData index // diagramData index
const diagramParts = computed<DiagramPart[]>(() => { const diagramParts = computed<DiagramPart[]>(() => {
@ -320,13 +424,6 @@ const mousePosition = reactive({ x: 0, y: 0 });
// //
const isLoading = ref(false); const isLoading = ref(false);
//
const showNotification = ref(false);
const notificationMessage = ref("");
const notificationType = ref<"success" | "error" | "info">("info");
// toastID
const toastTimers: number[] = [];
// //
const fileInput = ref<HTMLInputElement | null>(null); const fileInput = ref<HTMLInputElement | null>(null);
@ -337,7 +434,7 @@ const isWireCreationEventActive = ref(false);
// 使VueUse // 使VueUse
// //
useEventListener(document, 'mousemove', (e: MouseEvent) => { useEventListener(document, "mousemove", (e: MouseEvent) => {
if (isDragEventActive.value) { if (isDragEventActive.value) {
onDrag(e); onDrag(e);
} }
@ -349,7 +446,7 @@ useEventListener(document, 'mousemove', (e: MouseEvent) => {
} }
}); });
useEventListener(document, 'mouseup', () => { useEventListener(document, "mouseup", () => {
if (isDragEventActive.value) { if (isDragEventActive.value) {
stopDrag(); stopDrag();
} }
@ -359,7 +456,7 @@ useEventListener(document, 'mouseup', () => {
}); });
// //
useEventListener(window, 'keydown', handleKeyDown); useEventListener(window, "keydown", handleKeyDown);
// --- --- // --- ---
const MIN_SCALE = 0.2; const MIN_SCALE = 0.2;
@ -893,7 +990,7 @@ function handleFileSelected(event: Event) {
const validation = validateDiagramData(parsed); const validation = validateDiagramData(parsed);
if (!validation.isValid) { if (!validation.isValid) {
showToast( alertStore?.show(
`不是有效的diagram.json格式: ${validation.errors.join("; ")}`, `不是有效的diagram.json格式: ${validation.errors.join("; ")}`,
"error", "error",
); );
@ -910,11 +1007,11 @@ function handleFileSelected(event: Event) {
// //
emit("diagram-updated", diagramData.value); emit("diagram-updated", diagramData.value);
showToast(`成功导入diagram文件`, "success"); alertStore?.show(`成功导入diagram文件`, "success");
} catch (error) { } catch (error) {
console.error("解析JSON文件出错:", error); console.error("解析JSON文件出错:", error);
if (document.body.contains(canvasContainer.value)) { if (document.body.contains(canvasContainer.value)) {
showToast("解析文件出错请确认是有效的JSON格式", "error"); alertStore?.show("解析文件出错请确认是有效的JSON格式", "error");
} }
} finally { } finally {
// //
@ -930,7 +1027,7 @@ function handleFileSelected(event: Event) {
reader.onerror = () => { reader.onerror = () => {
// //
if (document.body.contains(canvasContainer.value)) { if (document.body.contains(canvasContainer.value)) {
showToast("读取文件时出错", "error"); alertStore?.show("读取文件时出错", "error");
isLoading.value = false; isLoading.value = false;
} }
// //
@ -956,46 +1053,21 @@ function exportDiagram() {
a.download = "diagram.json"; a.download = "diagram.json";
a.click(); a.click();
// URL // URL
const timerId = setTimeout(() => { setTimeout(() => {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
// //
if (document.body.contains(canvasContainer.value)) { if (document.body.contains(canvasContainer.value)) {
isLoading.value = false; isLoading.value = false;
showToast("成功导出diagram文件", "success"); alertStore?.show("成功导出diagram文件", "success");
} }
}, 100); }, 100);
// ID便
toastTimers.push(timerId);
} catch (error) { } catch (error) {
console.error("导出diagram文件时出错:", error); console.error("导出diagram文件时出错:", error);
showToast("导出diagram文件时出错", "error"); alertStore?.show("导出diagram文件时出错", "error");
isLoading.value = false; 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 () => { onMounted(async () => {
// componentManager // componentManager
@ -1011,7 +1083,6 @@ onMounted(async () => {
getCanvasPosition: () => ({ x: position.x, y: position.y }), getCanvasPosition: () => ({ x: position.x, y: position.y }),
getScale: () => scale.value, getScale: () => scale.value,
$el: canvasContainer.value, $el: canvasContainer.value,
showToast
}; };
componentManager.setCanvasRef(canvasAPI); componentManager.setCanvasRef(canvasAPI);
} }
@ -1033,7 +1104,9 @@ onMounted(async () => {
// componentManager // componentManager
if (componentManager) { if (componentManager) {
await componentManager.preloadComponentModules(Array.from(componentTypes)); await componentManager.preloadComponentModules(
Array.from(componentTypes),
);
} }
} catch (error) { } catch (error) {
console.error("加载图表数据失败:", error); console.error("加载图表数据失败:", error);
@ -1061,16 +1134,6 @@ function handleKeyDown(e: KeyboardEvent) {
} }
} }
onUnmounted(() => {
// toast
toastTimers.forEach((timerId) => clearTimeout(timerId));
//
isDragEventActive.value = false;
isComponentDragEventActive.value = false;
isWireCreationEventActive.value = false;
});
// //
function updateDiagramDataDirectly(data: DiagramData) { function updateDiagramDataDirectly(data: DiagramData) {
// //
@ -1085,7 +1148,7 @@ function updateDiagramDataDirectly(data: DiagramData) {
emit("diagram-updated", data); emit("diagram-updated", data);
} }
// - 访 //
defineExpose({ defineExpose({
// //
getDiagramData: () => diagramData.value, getDiagramData: () => diagramData.value,
@ -1112,24 +1175,18 @@ defineExpose({
emit("diagram-updated", data); emit("diagram-updated", data);
// 便UI // 便UI
const timerId = setTimeout(() => { setTimeout(() => {
// //
if (document.body.contains(canvasContainer.value)) { if (document.body.contains(canvasContainer.value)) {
isLoading.value = false; isLoading.value = false;
} }
}, 200); }, 200);
// ID便
toastTimers.push(timerId);
}); });
}, },
// //
getCanvasPosition: () => ({ x: position.x, y: position.y }), getCanvasPosition: () => ({ x: position.x, y: position.y }),
getScale: () => scale.value, getScale: () => scale.value,
//
showToast,
}); });
// - // -
@ -1254,7 +1311,13 @@ watch(
-ms-user-select: none; -ms-user-select: none;
} }
.component-wrapper :deep(svg *:not([class*="interactive"]):not(rect.glow):not(circle[fill-opacity]):not([fill-opacity])) { .component-wrapper
:deep(
svg
*:not([class*="interactive"]):not(rect.glow):not(
circle[fill-opacity]
):not([fill-opacity])
) {
pointer-events: none; pointer-events: none;
/* 非交互元素不接收鼠标事件 */ /* 非交互元素不接收鼠标事件 */
} }