feat: 添加全局alert,并替换原先是toast
This commit is contained in:
		
							
								
								
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -9,6 +9,7 @@ export {}
 | 
			
		||||
declare module 'vue' {
 | 
			
		||||
  export interface GlobalComponents {
 | 
			
		||||
    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']
 | 
			
		||||
    BaseInputField: typeof import('./src/components/InputField/BaseInputField.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">
 | 
			
		||||
import Navbar from "./components/Navbar.vue";
 | 
			
		||||
import Dialog from "./components/Dialog.vue";
 | 
			
		||||
import { Alert, useAlertProvider } from "./components/Alert";
 | 
			
		||||
import { ref, provide, computed, onMounted } from "vue";
 | 
			
		||||
import { useRouter } from "vue-router";
 | 
			
		||||
 | 
			
		||||
@@ -49,19 +50,26 @@ provide("theme", {
 | 
			
		||||
const currentRoutePath = computed(() => {
 | 
			
		||||
  return router.currentRoute.value.path;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
useAlertProvider();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <header class="relative">
 | 
			
		||||
      <Navbar></Navbar>
 | 
			
		||||
      <Dialog></Dialog>
 | 
			
		||||
      <Navbar />
 | 
			
		||||
      <Dialog />
 | 
			
		||||
      <Alert />
 | 
			
		||||
    </header>
 | 
			
		||||
 | 
			
		||||
    <main>
 | 
			
		||||
      <RouterView />
 | 
			
		||||
    </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>
 | 
			
		||||
        <p>Copyright © 2023 - All right reserved by OurEDA</p>
 | 
			
		||||
      </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>
 | 
			
		||||
  <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="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
 | 
			
		||||
          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
 | 
			
		||||
          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
 | 
			
		||||
          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>
 | 
			
		||||
      <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"
 | 
			
		||||
          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
 | 
			
		||||
          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="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>
 | 
			
		||||
        {{ props.showDocPanel ? "属性面板" : "文档" }}
 | 
			
		||||
      </button>
 | 
			
		||||
    </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="{
 | 
			
		||||
      transform: `translate(${position.x}px, ${position.y}px) scale(${scale})`,
 | 
			
		||||
    }">
 | 
			
		||||
    <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-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" />
 | 
			
		||||
        <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="{
 | 
			
		||||
      <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,
 | 
			
		||||
@@ -73,56 +151,74 @@
 | 
			
		||||
            : 'none',
 | 
			
		||||
          opacity: component.isOn ? 1 : 0.6,
 | 
			
		||||
          display: 'block',
 | 
			
		||||
        }" @mousedown.left.stop="startComponentDrag($event, component)" @mouseover="
 | 
			
		||||
        }"
 | 
			
		||||
        @mousedown.left.stop="startComponentDrag($event, component)"
 | 
			
		||||
        @mouseover="
 | 
			
		||||
          (event) => {
 | 
			
		||||
            hoveredComponent = component.id;
 | 
			
		||||
          }
 | 
			
		||||
        " @mouseleave="
 | 
			
		||||
        "
 | 
			
		||||
        @mouseleave="
 | 
			
		||||
          (event) => {
 | 
			
		||||
            hoveredComponent = null;
 | 
			
		||||
          }
 | 
			
		||||
        ">
 | 
			
		||||
        "
 | 
			
		||||
      >
 | 
			
		||||
        <!-- 动态渲染组件 -->
 | 
			
		||||
        <component :is="componentManager.getComponentDefinition(component.type)" 
 | 
			
		||||
          v-if="componentManager.componentModules.value[component.type] && componentManager.getComponentDefinition(component.type)"
 | 
			
		||||
          v-bind="componentManager.prepareComponentProps(component.attrs || {}, component.id)" @update:bindKey="
 | 
			
		||||
        <component
 | 
			
		||||
          :is="componentManager.getComponentDefinition(component.type)"
 | 
			
		||||
          v-if="
 | 
			
		||||
            componentManager.componentModules.value[component.type] &&
 | 
			
		||||
            componentManager.getComponentDefinition(component.type)
 | 
			
		||||
          "
 | 
			
		||||
          v-bind="
 | 
			
		||||
            componentManager.prepareComponentProps(
 | 
			
		||||
              component.attrs || {},
 | 
			
		||||
              component.id,
 | 
			
		||||
            )
 | 
			
		||||
          "
 | 
			
		||||
          @update:bindKey="
 | 
			
		||||
            (value: string) =>
 | 
			
		||||
              updateComponentProp(component.id, 'bindKey', value)
 | 
			
		||||
          " @pin-click="
 | 
			
		||||
          "
 | 
			
		||||
          @pin-click="
 | 
			
		||||
            (pinInfo: any) =>
 | 
			
		||||
              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 -->
 | 
			
		||||
        <div v-else
 | 
			
		||||
          class="p-2 text-xs text-gray-400 border border-dashed border-gray-400 flex items-center justify-center">
 | 
			
		||||
        <div
 | 
			
		||||
          v-else
 | 
			
		||||
          class="p-2 text-xs text-gray-400 border border-dashed border-gray-400 flex items-center justify-center"
 | 
			
		||||
        >
 | 
			
		||||
          <div class="flex flex-col items-center">
 | 
			
		||||
            <div class="loading loading-spinner loading-xs mb-1"></div>
 | 
			
		||||
            <span>Loading {{ component.type }}...</span>
 | 
			
		||||
            <small class="mt-1 text-xs">{{ componentManager.componentModules.value[component.type] ? 'Module loaded but invalid' : 'Module not found' }}</small>
 | 
			
		||||
            <small class="mt-1 text-xs">{{
 | 
			
		||||
              componentManager.componentModules.value[component.type]
 | 
			
		||||
                ? "Module loaded but invalid"
 | 
			
		||||
                : "Module not found"
 | 
			
		||||
            }}</small>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <!-- 通知组件 -->
 | 
			
		||||
    <div v-if="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>
 | 
			
		||||
 | 
			
		||||
    <!-- 缩放指示器 -->
 | 
			
		||||
    <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>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
@@ -140,6 +236,7 @@ import {
 | 
			
		||||
} from "vue";
 | 
			
		||||
import { useEventListener } from "@vueuse/core";
 | 
			
		||||
import WireComponent from "@/components/equipments/Wire.vue";
 | 
			
		||||
import { useAlertStore } from "@/components/Alert";
 | 
			
		||||
 | 
			
		||||
// 导入 diagram 管理器
 | 
			
		||||
import {
 | 
			
		||||
@@ -186,9 +283,14 @@ const props = defineProps<{
 | 
			
		||||
// 获取componentManager实例
 | 
			
		||||
const componentManager = useComponentManager();
 | 
			
		||||
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 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属性排序
 | 
			
		||||
const diagramParts = computed<DiagramPart[]>(() => {
 | 
			
		||||
@@ -320,13 +424,6 @@ 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);
 | 
			
		||||
 | 
			
		||||
@@ -337,7 +434,7 @@ const isWireCreationEventActive = ref(false);
 | 
			
		||||
 | 
			
		||||
// 使用VueUse设置事件监听器
 | 
			
		||||
// 画布拖拽事件
 | 
			
		||||
useEventListener(document, 'mousemove', (e: MouseEvent) => {
 | 
			
		||||
useEventListener(document, "mousemove", (e: MouseEvent) => {
 | 
			
		||||
  if (isDragEventActive.value) {
 | 
			
		||||
    onDrag(e);
 | 
			
		||||
  }
 | 
			
		||||
@@ -349,7 +446,7 @@ useEventListener(document, 'mousemove', (e: MouseEvent) => {
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
useEventListener(document, 'mouseup', () => {
 | 
			
		||||
useEventListener(document, "mouseup", () => {
 | 
			
		||||
  if (isDragEventActive.value) {
 | 
			
		||||
    stopDrag();
 | 
			
		||||
  }
 | 
			
		||||
@@ -359,7 +456,7 @@ useEventListener(document, 'mouseup', () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 键盘事件
 | 
			
		||||
useEventListener(window, 'keydown', handleKeyDown);
 | 
			
		||||
useEventListener(window, "keydown", handleKeyDown);
 | 
			
		||||
 | 
			
		||||
// --- 缩放功能 ---
 | 
			
		||||
const MIN_SCALE = 0.2;
 | 
			
		||||
@@ -893,7 +990,7 @@ function handleFileSelected(event: Event) {
 | 
			
		||||
      const validation = validateDiagramData(parsed);
 | 
			
		||||
 | 
			
		||||
      if (!validation.isValid) {
 | 
			
		||||
        showToast(
 | 
			
		||||
        alertStore?.show(
 | 
			
		||||
          `不是有效的diagram.json格式: ${validation.errors.join("; ")}`,
 | 
			
		||||
          "error",
 | 
			
		||||
        );
 | 
			
		||||
@@ -910,11 +1007,11 @@ function handleFileSelected(event: Event) {
 | 
			
		||||
      // 发出更新事件
 | 
			
		||||
      emit("diagram-updated", diagramData.value);
 | 
			
		||||
 | 
			
		||||
      showToast(`成功导入diagram文件`, "success");
 | 
			
		||||
      alertStore?.show(`成功导入diagram文件`, "success");
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error("解析JSON文件出错:", error);
 | 
			
		||||
      if (document.body.contains(canvasContainer.value)) {
 | 
			
		||||
        showToast("解析文件出错,请确认是有效的JSON格式", "error");
 | 
			
		||||
        alertStore?.show("解析文件出错,请确认是有效的JSON格式", "error");
 | 
			
		||||
      }
 | 
			
		||||
    } finally {
 | 
			
		||||
      // 检查组件是否仍然挂载
 | 
			
		||||
@@ -930,7 +1027,7 @@ function handleFileSelected(event: Event) {
 | 
			
		||||
  reader.onerror = () => {
 | 
			
		||||
    // 检查组件是否仍然挂载
 | 
			
		||||
    if (document.body.contains(canvasContainer.value)) {
 | 
			
		||||
      showToast("读取文件时出错", "error");
 | 
			
		||||
      alertStore?.show("读取文件时出错", "error");
 | 
			
		||||
      isLoading.value = false;
 | 
			
		||||
    }
 | 
			
		||||
    // 清除文件输入
 | 
			
		||||
@@ -956,46 +1053,21 @@ function exportDiagram() {
 | 
			
		||||
    a.download = "diagram.json";
 | 
			
		||||
    a.click();
 | 
			
		||||
    // 释放URL对象
 | 
			
		||||
    const timerId = setTimeout(() => {
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      URL.revokeObjectURL(url);
 | 
			
		||||
      // 检查组件是否仍然挂载
 | 
			
		||||
      if (document.body.contains(canvasContainer.value)) {
 | 
			
		||||
        isLoading.value = false;
 | 
			
		||||
        showToast("成功导出diagram文件", "success");
 | 
			
		||||
        alertStore?.show("成功导出diagram文件", "success");
 | 
			
		||||
      }
 | 
			
		||||
    }, 100);
 | 
			
		||||
 | 
			
		||||
    // 将定时器ID保存起来,以便在组件卸载时清除
 | 
			
		||||
    toastTimers.push(timerId);
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("导出diagram文件时出错:", error);
 | 
			
		||||
    showToast("导出diagram文件时出错", "error");
 | 
			
		||||
    alertStore?.show("导出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 () => {
 | 
			
		||||
  // 设置componentManager的画布引用
 | 
			
		||||
@@ -1011,7 +1083,6 @@ onMounted(async () => {
 | 
			
		||||
      getCanvasPosition: () => ({ x: position.x, y: position.y }),
 | 
			
		||||
      getScale: () => scale.value,
 | 
			
		||||
      $el: canvasContainer.value,
 | 
			
		||||
      showToast
 | 
			
		||||
    };
 | 
			
		||||
    componentManager.setCanvasRef(canvasAPI);
 | 
			
		||||
  }
 | 
			
		||||
@@ -1033,7 +1104,9 @@ onMounted(async () => {
 | 
			
		||||
 | 
			
		||||
    // 直接通过componentManager预加载组件模块
 | 
			
		||||
    if (componentManager) {
 | 
			
		||||
      await componentManager.preloadComponentModules(Array.from(componentTypes));
 | 
			
		||||
      await componentManager.preloadComponentModules(
 | 
			
		||||
        Array.from(componentTypes),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  } catch (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) {
 | 
			
		||||
  // 检查组件是否仍然挂载
 | 
			
		||||
@@ -1085,7 +1148,7 @@ function updateDiagramDataDirectly(data: DiagramData) {
 | 
			
		||||
  emit("diagram-updated", data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 暴露方法给父组件 - 简化版本,主要用于数据访问
 | 
			
		||||
// 暴露方法给父组件
 | 
			
		||||
defineExpose({
 | 
			
		||||
  // 基本数据操作
 | 
			
		||||
  getDiagramData: () => diagramData.value,
 | 
			
		||||
@@ -1112,24 +1175,18 @@ defineExpose({
 | 
			
		||||
      emit("diagram-updated", data);
 | 
			
		||||
 | 
			
		||||
      // 短暂延迟后结束加载状态,以便UI能更新
 | 
			
		||||
      const timerId = setTimeout(() => {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        // 检查组件是否仍然挂载
 | 
			
		||||
        if (document.body.contains(canvasContainer.value)) {
 | 
			
		||||
          isLoading.value = false;
 | 
			
		||||
        }
 | 
			
		||||
      }, 200);
 | 
			
		||||
 | 
			
		||||
      // 将定时器ID保存起来,以便在组件卸载时清除
 | 
			
		||||
      toastTimers.push(timerId);
 | 
			
		||||
    });
 | 
			
		||||
  },
 | 
			
		||||
 | 
			
		||||
  // 画布状态
 | 
			
		||||
  getCanvasPosition: () => ({ x: position.x, y: position.y }),
 | 
			
		||||
  getScale: () => scale.value,
 | 
			
		||||
 | 
			
		||||
  // 通知系统
 | 
			
		||||
  showToast,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 监视器 - 当图表数据更改时保存
 | 
			
		||||
@@ -1254,7 +1311,13 @@ watch(
 | 
			
		||||
  -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;
 | 
			
		||||
  /* 非交互元素不接收鼠标事件 */
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user