feat: 添加全局alert,并替换原先是toast
This commit is contained in:
parent
53027470fe
commit
cbb3543c4a
|
@ -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']
|
||||||
|
|
14
src/App.vue
14
src/App.vue
|
@ -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>
|
||||||
|
|
|
@ -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>
|
|
@ -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 };
|
|
@ -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
|
||||||
|
ref="canvas"
|
||||||
|
class="diagram-canvas"
|
||||||
|
:style="{
|
||||||
transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
|
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
|
||||||
|
v-for="component in diagramParts"
|
||||||
|
:key="component.id"
|
||||||
|
class="component-wrapper"
|
||||||
|
:class="{
|
||||||
'component-hover': hoveredComponent === component.id,
|
'component-hover': hoveredComponent === component.id,
|
||||||
'component-selected': selectedComponentId === component.id,
|
'component-selected': selectedComponentId === component.id,
|
||||||
'component-disabled': !component.isOn,
|
'component-disabled': !component.isOn,
|
||||||
'component-hidepins': component.hidepins,
|
'component-hidepins': component.hidepins,
|
||||||
}" :style="{
|
}"
|
||||||
|
: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");
|
|
||||||
// 保存toast定时器ID
|
|
||||||
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;
|
||||||
/* 非交互元素不接收鼠标事件 */
|
/* 非交互元素不接收鼠标事件 */
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue