rm:去除konva
This commit is contained in:
		
							
								
								
									
										50
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										50
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -13,7 +13,6 @@
 | 
			
		||||
        "@vueuse/core": "^13.5.0",
 | 
			
		||||
        "async-mutex": "^0.5.0",
 | 
			
		||||
        "highlight.js": "^11.11.1",
 | 
			
		||||
        "konva": "^9.3.20",
 | 
			
		||||
        "lodash": "^4.17.21",
 | 
			
		||||
        "log-symbols": "^7.0.0",
 | 
			
		||||
        "lucide-vue-next": "^0.525.0",
 | 
			
		||||
@@ -24,7 +23,6 @@
 | 
			
		||||
        "ts-log": "^2.2.7",
 | 
			
		||||
        "ts-results-es": "^5.0.1",
 | 
			
		||||
        "vue": "^3.5.13",
 | 
			
		||||
        "vue-konva": "^3.2.1",
 | 
			
		||||
        "vue-router": "4",
 | 
			
		||||
        "yocto-queue": "^1.2.1",
 | 
			
		||||
        "zod": "^3.24.2"
 | 
			
		||||
@@ -3174,26 +3172,6 @@
 | 
			
		||||
      "dev": true,
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/konva": {
 | 
			
		||||
      "version": "9.3.20",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/konva/-/konva-9.3.20.tgz",
 | 
			
		||||
      "integrity": "sha512-7XPD/YtgfzC8b1c7z0hhY5TF1IO/pBYNa29zMTA2PeBaqI0n5YplUeo4JRuRcljeAF8lWtW65jePZZF7064c8w==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "patreon",
 | 
			
		||||
          "url": "https://www.patreon.com/lavrton"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "type": "opencollective",
 | 
			
		||||
          "url": "https://opencollective.com/konva"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "type": "github",
 | 
			
		||||
          "url": "https://github.com/sponsors/lavrton"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT"
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/lightningcss": {
 | 
			
		||||
      "version": "1.29.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.29.2.tgz",
 | 
			
		||||
@@ -4660,34 +4638,6 @@
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/vue-konva": {
 | 
			
		||||
      "version": "3.2.1",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/vue-konva/-/vue-konva-3.2.1.tgz",
 | 
			
		||||
      "integrity": "sha512-gLF+VYnlrBfwtaN3NkgzzEqlj9nyCll80VZv2DdvLUM3cisUsdcRJJuMwGTBJOTebcnn6MB22r33IFd2m+m/ig==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "patreon",
 | 
			
		||||
          "url": "https://www.patreon.com/lavrton"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "type": "opencollective",
 | 
			
		||||
          "url": "https://opencollective.com/konva"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "type": "github",
 | 
			
		||||
          "url": "https://github.com/sponsors/lavrton"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">= 4.0.0",
 | 
			
		||||
        "npm": ">= 3.0.0"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "konva": ">7",
 | 
			
		||||
        "vue": "^3"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/vue-router": {
 | 
			
		||||
      "version": "4.5.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,6 @@
 | 
			
		||||
    "@vueuse/core": "^13.5.0",
 | 
			
		||||
    "async-mutex": "^0.5.0",
 | 
			
		||||
    "highlight.js": "^11.11.1",
 | 
			
		||||
    "konva": "^9.3.20",
 | 
			
		||||
    "lodash": "^4.17.21",
 | 
			
		||||
    "log-symbols": "^7.0.0",
 | 
			
		||||
    "lucide-vue-next": "^0.525.0",
 | 
			
		||||
@@ -30,7 +29,6 @@
 | 
			
		||||
    "ts-log": "^2.2.7",
 | 
			
		||||
    "ts-results-es": "^5.0.1",
 | 
			
		||||
    "vue": "^3.5.13",
 | 
			
		||||
    "vue-konva": "^3.2.1",
 | 
			
		||||
    "vue-router": "4",
 | 
			
		||||
    "yocto-queue": "^1.2.1",
 | 
			
		||||
    "zod": "^3.24.2"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,536 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="w-full h-full relative" @contextmenu.prevent>
 | 
			
		||||
    <v-stage
 | 
			
		||||
      class="h-full w-full"
 | 
			
		||||
      ref="stageRef"
 | 
			
		||||
      :config="labCanvasStore?.stageConfig"
 | 
			
		||||
      @mousedown="handleMouseDown"
 | 
			
		||||
      @mousemove="handleMouseMove"
 | 
			
		||||
      @mouseup="handleMouseUp"
 | 
			
		||||
      @wheel="handleWheel"
 | 
			
		||||
      @click="handleStageClick"
 | 
			
		||||
      @contextmenu="handleContextMenu"
 | 
			
		||||
    >
 | 
			
		||||
      <v-layer ref="layerRef">
 | 
			
		||||
        <template
 | 
			
		||||
          ref="canvasObjectsRef"
 | 
			
		||||
          v-for="item in objMap.values()"
 | 
			
		||||
          :key="item.id"
 | 
			
		||||
        >
 | 
			
		||||
          <v-group
 | 
			
		||||
            :config="{
 | 
			
		||||
              x: item.x,
 | 
			
		||||
              y: item.y,
 | 
			
		||||
              draggable: true,
 | 
			
		||||
              id: `group-${item.id}`,
 | 
			
		||||
            }"
 | 
			
		||||
            @dragstart="handleDragStart"
 | 
			
		||||
            @dragend="handleDragEnd"
 | 
			
		||||
            @mouseover="handleCanvasObjectMouseOver"
 | 
			
		||||
            @mouseout="handleCanvasObjectMouseOut"
 | 
			
		||||
          >
 | 
			
		||||
            <!-- Hover Box -->
 | 
			
		||||
            <v-rect
 | 
			
		||||
              v-show="!isUndefined(item.hoverBox)"
 | 
			
		||||
              :config="{
 | 
			
		||||
                ...item.hoverBox,
 | 
			
		||||
                visible:
 | 
			
		||||
                  !isUndefined(item.hoverBox) &&
 | 
			
		||||
                  item.isHoverring &&
 | 
			
		||||
                  !isDragging &&
 | 
			
		||||
                  selectedIds.length == 0,
 | 
			
		||||
                stroke: 'rgb(125,193,239)',
 | 
			
		||||
                strokeWidth: 2.5,
 | 
			
		||||
                dash: [10, 5],
 | 
			
		||||
                cornerRadius: 10,
 | 
			
		||||
              }"
 | 
			
		||||
            >
 | 
			
		||||
            </v-rect>
 | 
			
		||||
            <v-shape :config="item.config">
 | 
			
		||||
              <component
 | 
			
		||||
                :is="item.component"
 | 
			
		||||
                @mouseover="handleCanvasObjectMouseOver"
 | 
			
		||||
                @mouseout="handleCanvasObjectMouseOut"
 | 
			
		||||
              ></component>
 | 
			
		||||
            </v-shape>
 | 
			
		||||
          </v-group>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <v-transformer
 | 
			
		||||
          ref="transformerRef"
 | 
			
		||||
          :config="{
 | 
			
		||||
            borderStroke: 'rgb(125,193,239)',
 | 
			
		||||
            borderStrokeWidth: 3,
 | 
			
		||||
          }"
 | 
			
		||||
        />
 | 
			
		||||
        <v-rect
 | 
			
		||||
          ref="selectRectRef"
 | 
			
		||||
          v-if="selectionRectangle.visible"
 | 
			
		||||
          :config="{
 | 
			
		||||
            x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
 | 
			
		||||
            y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
 | 
			
		||||
            width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
 | 
			
		||||
            height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
 | 
			
		||||
            fill: '#0069FF88',
 | 
			
		||||
          }"
 | 
			
		||||
        />
 | 
			
		||||
      </v-layer>
 | 
			
		||||
    </v-stage>
 | 
			
		||||
    <LabComponentsDrawer
 | 
			
		||||
      class="absolute top-10 right-20"
 | 
			
		||||
      v-model:open="isDrawerOpen"
 | 
			
		||||
      :add-component="labCanvasStore.addComponent"
 | 
			
		||||
    ></LabComponentsDrawer>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import LabComponentsDrawer from "./LabComponentsDrawer.vue";
 | 
			
		||||
import Konva from "konva";
 | 
			
		||||
import { isNull, isUndefined } from "lodash";
 | 
			
		||||
import type { VLayer, VNode, VStage, VTransformer } from "@/utils/VueKonvaType";
 | 
			
		||||
import { ref, reactive, watch, onMounted, useTemplateRef, computed } from "vue";
 | 
			
		||||
import type { Stage } from "konva/lib/Stage";
 | 
			
		||||
import type { LabCanvasComponentConfig } from "./LabCanvasType";
 | 
			
		||||
import { useLabCanvasStore } from "./composable/LabCanvasManager";
 | 
			
		||||
 | 
			
		||||
const labCanvasStore = useLabCanvasStore();
 | 
			
		||||
 | 
			
		||||
function calculateRectBounding(
 | 
			
		||||
  width: number,
 | 
			
		||||
  height: number,
 | 
			
		||||
  rotation: number,
 | 
			
		||||
  scale: number = 1,
 | 
			
		||||
  padding?: number,
 | 
			
		||||
) {
 | 
			
		||||
  // Apply scale to dimensions first
 | 
			
		||||
  const scaledWidth = width * scale;
 | 
			
		||||
  const scaledHeight = height * scale;
 | 
			
		||||
 | 
			
		||||
  // calculate bounding box for rotated rectangle
 | 
			
		||||
  const radians = (rotation * Math.PI) / 180;
 | 
			
		||||
  const cos = Math.cos(radians);
 | 
			
		||||
  const sin = Math.sin(radians);
 | 
			
		||||
 | 
			
		||||
  // calculate corners of the scaled rectangle
 | 
			
		||||
  const corners = [
 | 
			
		||||
    { x: 0, y: 0 },
 | 
			
		||||
    { x: scaledWidth, y: 0 },
 | 
			
		||||
    { x: scaledWidth, y: scaledHeight },
 | 
			
		||||
    { x: 0, y: scaledHeight },
 | 
			
		||||
  ].map((point) => ({
 | 
			
		||||
    x: point.x * cos - point.y * sin,
 | 
			
		||||
    y: point.x * sin + point.y * cos,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  // find bounding box dimensions
 | 
			
		||||
  const minX = Math.min(...corners.map((p) => p.x));
 | 
			
		||||
  const maxX = Math.max(...corners.map((p) => p.x));
 | 
			
		||||
  const minY = Math.min(...corners.map((p) => p.y));
 | 
			
		||||
  const maxY = Math.max(...corners.map((p) => p.y));
 | 
			
		||||
 | 
			
		||||
  if (padding) {
 | 
			
		||||
    const scaledPadding = padding * scale;
 | 
			
		||||
    return {
 | 
			
		||||
      x: minX - scaledPadding,
 | 
			
		||||
      y: minY - scaledPadding,
 | 
			
		||||
      width: maxX - minX + scaledPadding * 2,
 | 
			
		||||
      height: maxY - minY + scaledPadding * 2,
 | 
			
		||||
    };
 | 
			
		||||
  } else {
 | 
			
		||||
    return {
 | 
			
		||||
      x: minX,
 | 
			
		||||
      y: minY,
 | 
			
		||||
      width: maxX - minX,
 | 
			
		||||
      height: maxY - minY,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const objMap = computed(() => {
 | 
			
		||||
  return new Map(labCanvasStore?.components.value.map((item) => [item.id, item]));
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const layerRef = useTemplateRef<VLayer>("layerRef");
 | 
			
		||||
const canvasObjectsRef =
 | 
			
		||||
  useTemplateRef<HTMLTemplateElement[]>("canvasObjectsRef");
 | 
			
		||||
const transformerRef = useTemplateRef<VTransformer>("transformerRef");
 | 
			
		||||
const selectRectRef = useTemplateRef<VNode>("selectRectRef");
 | 
			
		||||
const stageRef = useTemplateRef<VStage>("StageRef");
 | 
			
		||||
 | 
			
		||||
const isDragging = ref(false);
 | 
			
		||||
const dragItemId = ref<string | null>(null);
 | 
			
		||||
 | 
			
		||||
const isRightDragging = ref(false);
 | 
			
		||||
const lastPointerPosition = ref<{ x: number; y: number } | null>(null);
 | 
			
		||||
 | 
			
		||||
const isSelecting = ref(false);
 | 
			
		||||
const selectedIds = ref<string[]>([]);
 | 
			
		||||
const selectionRectangle = reactive({
 | 
			
		||||
  visible: false,
 | 
			
		||||
  x1: 0,
 | 
			
		||||
  y1: 0,
 | 
			
		||||
  x2: 0,
 | 
			
		||||
  y2: 0,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const stageScale = ref(1);
 | 
			
		||||
const isDrawerOpen = ref(false);
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  if (isNull(transformerRef.value)) return;
 | 
			
		||||
 | 
			
		||||
  const selectedBox = transformerRef.value.getNode();
 | 
			
		||||
  selectedBox.resizeEnabled(false);
 | 
			
		||||
  selectedBox.rotateEnabled(false);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Update transformer nodes when selection changes
 | 
			
		||||
watch(selectedIds, () => {
 | 
			
		||||
  if (isNull(transformerRef.value)) return;
 | 
			
		||||
 | 
			
		||||
  const nodes = selectedIds.value.map((id) => {
 | 
			
		||||
    if (isNull(layerRef.value)) {
 | 
			
		||||
      console.error("layer is null");
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (let node of layerRef.value.getNode().children) {
 | 
			
		||||
      if (node instanceof Konva.Group && node.id() === `group-${id}`)
 | 
			
		||||
        return node;
 | 
			
		||||
    }
 | 
			
		||||
  }) as Konva.Node[];
 | 
			
		||||
 | 
			
		||||
  // console.log(nodes);
 | 
			
		||||
  if (!isUndefined(nodes)) transformerRef.value.getNode().nodes(nodes);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function handleCacheChange(e: Event) {
 | 
			
		||||
  const target = e.target as HTMLInputElement;
 | 
			
		||||
  const shouldCache = isNull(target) ? false : target.checked;
 | 
			
		||||
 | 
			
		||||
  if (isNull(layerRef.value)) return;
 | 
			
		||||
 | 
			
		||||
  if (shouldCache) {
 | 
			
		||||
    layerRef.value.getNode().cache();
 | 
			
		||||
  } else {
 | 
			
		||||
    layerRef.value.getNode().clearCache();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Drag event handlers
 | 
			
		||||
function handleDragStart(e: Event) {
 | 
			
		||||
  isDragging.value = true;
 | 
			
		||||
 | 
			
		||||
  // save drag element:
 | 
			
		||||
  const target = e.target as unknown as Konva.Node;
 | 
			
		||||
  dragItemId.value = target.id();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleDragEnd() {
 | 
			
		||||
  isDragging.value = false;
 | 
			
		||||
  dragItemId.value = null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Mouse event handlers
 | 
			
		||||
function handleStageClick(e: { target: any; evt: MouseEvent }) {
 | 
			
		||||
  if (isNull(e.target)) return;
 | 
			
		||||
  const target = e.target as unknown as Konva.Shape | Konva.Group;
 | 
			
		||||
  // if we are selecting with rect, do nothing
 | 
			
		||||
  if (selectionRectangle.visible) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // if click on empty area - remove all selections
 | 
			
		||||
  if (target === target.getStage()) {
 | 
			
		||||
    selectedIds.value = [];
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const clickedId = target.attrs.id;
 | 
			
		||||
 | 
			
		||||
  // do we pressed shift or ctrl?
 | 
			
		||||
  const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
 | 
			
		||||
  const isSelected = selectedIds.value.includes(clickedId);
 | 
			
		||||
 | 
			
		||||
  if (!metaPressed && !isSelected) {
 | 
			
		||||
    // if no key pressed and the node is not selected
 | 
			
		||||
    // select just one
 | 
			
		||||
    selectedIds.value = [clickedId];
 | 
			
		||||
  } else if (metaPressed && isSelected) {
 | 
			
		||||
    // if we pressed keys and node was selected
 | 
			
		||||
    // we need to remove it from selection:
 | 
			
		||||
    selectedIds.value = selectedIds.value.filter((id) => id !== clickedId);
 | 
			
		||||
  } else if (metaPressed && !isSelected) {
 | 
			
		||||
    // add the node into selection
 | 
			
		||||
    selectedIds.value = [...selectedIds.value, clickedId];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleContextMenu(e: Event) {
 | 
			
		||||
  // 防止默认右键菜单弹出
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleMouseDown(e: Event) {
 | 
			
		||||
  if (isNull(e.target)) return;
 | 
			
		||||
  const target = e.target as unknown as Konva.Container;
 | 
			
		||||
  const stage = target.getStage();
 | 
			
		||||
 | 
			
		||||
  // 获取鼠标事件信息
 | 
			
		||||
  const mouseEvent = (e as any).evt as MouseEvent;
 | 
			
		||||
 | 
			
		||||
  // 如果是右键按下,开始右键拖拽
 | 
			
		||||
  if (mouseEvent.button === 2) {
 | 
			
		||||
    isRightDragging.value = true;
 | 
			
		||||
    const pos = stage?.getPointerPosition();
 | 
			
		||||
    if (pos) {
 | 
			
		||||
      lastPointerPosition.value = { x: pos.x, y: pos.y };
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // do nothing if we mousedown on any shape
 | 
			
		||||
  if ((e.target as unknown) !== target.getStage()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // start selection rectangle
 | 
			
		||||
  isSelecting.value = true;
 | 
			
		||||
  const pos = stage?.getPointerPosition();
 | 
			
		||||
  if (!isNull(pos) && !isUndefined(pos) && !isNull(stage)) {
 | 
			
		||||
    // Convert pointer position to relative coordinates considering scale and position
 | 
			
		||||
    const relativePos = {
 | 
			
		||||
      x: (pos.x - stage.x()) / stage.scaleX(),
 | 
			
		||||
      y: (pos.y - stage.y()) / stage.scaleY(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    selectionRectangle.visible = true;
 | 
			
		||||
    selectionRectangle.x1 = relativePos.x;
 | 
			
		||||
    selectionRectangle.y1 = relativePos.y;
 | 
			
		||||
    selectionRectangle.x2 = relativePos.x;
 | 
			
		||||
    selectionRectangle.y2 = relativePos.y;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleMouseMove(e: Event) {
 | 
			
		||||
  if (isNull(e.target)) return;
 | 
			
		||||
  const target = e.target as unknown as Konva.Container;
 | 
			
		||||
  const stage = target.getStage();
 | 
			
		||||
 | 
			
		||||
  // 如果正在右键拖拽,移动画布
 | 
			
		||||
  if (isRightDragging.value && lastPointerPosition.value && stage) {
 | 
			
		||||
    const pos = stage.getPointerPosition();
 | 
			
		||||
    if (pos) {
 | 
			
		||||
      const dx = pos.x - lastPointerPosition.value.x;
 | 
			
		||||
      const dy = pos.y - lastPointerPosition.value.y;
 | 
			
		||||
 | 
			
		||||
      const currentPos = stage.position();
 | 
			
		||||
      stage.position({
 | 
			
		||||
        x: currentPos.x + dx,
 | 
			
		||||
        y: currentPos.y + dy,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      lastPointerPosition.value = { x: pos.x, y: pos.y };
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // do nothing if we didn't start selection
 | 
			
		||||
  if (!isSelecting.value) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const pos = stage?.getPointerPosition();
 | 
			
		||||
  if (!isNull(pos) && !isUndefined(pos) && !isNull(stage)) {
 | 
			
		||||
    // Convert pointer position to relative coordinates considering scale and position
 | 
			
		||||
    const relativePos = {
 | 
			
		||||
      x: (pos.x - stage.x()) / stage.scaleX(),
 | 
			
		||||
      y: (pos.y - stage.y()) / stage.scaleY(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    selectionRectangle.x2 = relativePos.x;
 | 
			
		||||
    selectionRectangle.y2 = relativePos.y;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleMouseUp(e: Event) {
 | 
			
		||||
  // 如果是右键释放,结束右键拖拽
 | 
			
		||||
  const mouseEvent = (e as any).evt as MouseEvent;
 | 
			
		||||
  if (mouseEvent.button === 2 && isRightDragging.value) {
 | 
			
		||||
    isRightDragging.value = false;
 | 
			
		||||
    lastPointerPosition.value = null;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // do nothing if we didn't start selection
 | 
			
		||||
  if (!isSelecting.value) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isSelecting.value = false;
 | 
			
		||||
 | 
			
		||||
  // update visibility in timeout, so we can check it in click event
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    selectionRectangle.visible = false;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const selBox = {
 | 
			
		||||
    x: Math.min(selectionRectangle.x1, selectionRectangle.x2),
 | 
			
		||||
    y: Math.min(selectionRectangle.y1, selectionRectangle.y2),
 | 
			
		||||
    width: Math.abs(selectionRectangle.x2 - selectionRectangle.x1),
 | 
			
		||||
    height: Math.abs(selectionRectangle.y2 - selectionRectangle.y1),
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // console.log(`Stage Scale: ${stageScale.value}`);
 | 
			
		||||
  let currentSelectedIds = [];
 | 
			
		||||
  for (let [key, shape] of objMap.value) {
 | 
			
		||||
    const shapeConfig = objMap.value.get(shape.id);
 | 
			
		||||
    if (isUndefined(shapeConfig)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (isUndefined(shapeConfig.hoverBox)) {
 | 
			
		||||
      if (
 | 
			
		||||
        shapeConfig.config.width &&
 | 
			
		||||
        shapeConfig.config.height &&
 | 
			
		||||
        shapeConfig.config.rotation
 | 
			
		||||
      ) {
 | 
			
		||||
        shapeConfig.hoverBox = calculateRectBounding(
 | 
			
		||||
          shapeConfig.config.width,
 | 
			
		||||
          shapeConfig.config.height,
 | 
			
		||||
          shapeConfig.config.rotation,
 | 
			
		||||
          stageScale.value,
 | 
			
		||||
          5,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        console.error("Could not calculate rect bounding");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
      Konva.Util.haveIntersection(selBox, {
 | 
			
		||||
        x: shapeConfig.hoverBox.x + shapeConfig.x,
 | 
			
		||||
        y: shapeConfig.hoverBox.y + shapeConfig.y,
 | 
			
		||||
        width: shapeConfig.hoverBox.width,
 | 
			
		||||
        height: shapeConfig.hoverBox.height,
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
      currentSelectedIds.push(shapeConfig.id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  selectedIds.value = currentSelectedIds;
 | 
			
		||||
}
 | 
			
		||||
function handleWheel(e: { target: any; evt: MouseEvent & { deltaY: number } }) {
 | 
			
		||||
  e.evt.preventDefault();
 | 
			
		||||
 | 
			
		||||
  const stage = e.target as Stage;
 | 
			
		||||
  if (stage === null || stage === undefined) {
 | 
			
		||||
    console.error("Stage is not defined");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const oldScale = stage.scaleX();
 | 
			
		||||
  const pointer = stage.getPointerPosition();
 | 
			
		||||
  if (pointer === null || pointer === undefined) {
 | 
			
		||||
    console.error("Pointer position is not defined");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const mousePointTo = {
 | 
			
		||||
    x: (pointer.x - stage.x()) / oldScale,
 | 
			
		||||
    y: (pointer.y - stage.y()) / oldScale,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // how to scale? Zoom in? Or zoom out?
 | 
			
		||||
  let direction = e.evt.deltaY < 0 ? 1 : -1;
 | 
			
		||||
 | 
			
		||||
  // when we zoom on trackpad, e.evt.ctrlKey is true
 | 
			
		||||
  // in that case lets revert direction
 | 
			
		||||
  if (e.evt.ctrlKey) {
 | 
			
		||||
    direction = -direction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const scaleBy = 1.05;
 | 
			
		||||
  const newScale = direction > 0 ? oldScale * scaleBy : oldScale / scaleBy;
 | 
			
		||||
 | 
			
		||||
  stage.scale({ x: newScale, y: newScale });
 | 
			
		||||
  stageScale.value = newScale;
 | 
			
		||||
 | 
			
		||||
  const newPos = {
 | 
			
		||||
    x: pointer.x - mousePointTo.x * newScale,
 | 
			
		||||
    y: pointer.y - mousePointTo.y * newScale,
 | 
			
		||||
  };
 | 
			
		||||
  stage.position(newPos);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleCanvasObjectMouseOver(evt: Event) {
 | 
			
		||||
  if (isNull(evt.target)) return;
 | 
			
		||||
  const target = evt.target;
 | 
			
		||||
 | 
			
		||||
  let object = null;
 | 
			
		||||
  if (target instanceof Konva.Group) {
 | 
			
		||||
    if (!target.hasChildren()) return;
 | 
			
		||||
    object = target.children[0];
 | 
			
		||||
  } else if (target instanceof Konva.Rect) {
 | 
			
		||||
    object = target;
 | 
			
		||||
  } else {
 | 
			
		||||
    console.trace(`Not Konva class: ${target}`);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get client rect
 | 
			
		||||
  const objectConfig = objMap.value.get(object.id());
 | 
			
		||||
  if (isUndefined(objectConfig)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get clientBox for first time
 | 
			
		||||
  if (isUndefined(objectConfig.hoverBox)) {
 | 
			
		||||
    if (
 | 
			
		||||
      objectConfig.config.width &&
 | 
			
		||||
      objectConfig.config.height &&
 | 
			
		||||
      objectConfig.config.rotation
 | 
			
		||||
    ) {
 | 
			
		||||
      objectConfig.hoverBox = calculateRectBounding(
 | 
			
		||||
        objectConfig.config.width,
 | 
			
		||||
        objectConfig.config.height,
 | 
			
		||||
        objectConfig.config.rotation,
 | 
			
		||||
        stageScale.value, // 传入当前缩放比例
 | 
			
		||||
        5,
 | 
			
		||||
      );
 | 
			
		||||
    } else console.error("Could not calculate rect bounding");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  objectConfig.isHoverring = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleCanvasObjectMouseOut(evt: Event) {
 | 
			
		||||
  if (isNull(evt.target)) return;
 | 
			
		||||
  const target = evt.target;
 | 
			
		||||
 | 
			
		||||
  let object = null;
 | 
			
		||||
  if (target instanceof Konva.Group) {
 | 
			
		||||
    if (!target.hasChildren()) return;
 | 
			
		||||
    object = target.children[0];
 | 
			
		||||
  } else if (target instanceof Konva.Rect) {
 | 
			
		||||
    object = target;
 | 
			
		||||
  } else {
 | 
			
		||||
    console.trace(`Not Konva class: ${target}`);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get client rect
 | 
			
		||||
  const objectConfig = objMap.value.get(object.id());
 | 
			
		||||
  if (isUndefined(objectConfig)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  objectConfig.isHoverring = false;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
@import "../../assets/main.css";
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
import type { VNode } from "@/utils/VueKonvaType";
 | 
			
		||||
import Konva from "konva";
 | 
			
		||||
import BaseBoard from "../equipments/BaseBoard.vue";
 | 
			
		||||
 | 
			
		||||
export type LabCanvasComponentConfig = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  component: VNode;
 | 
			
		||||
  x: number;
 | 
			
		||||
  y: number;
 | 
			
		||||
  config: Konva.ShapeConfig;
 | 
			
		||||
  isHoverring: boolean;
 | 
			
		||||
  hoverBox: {
 | 
			
		||||
    x: number;
 | 
			
		||||
    y: number;
 | 
			
		||||
    width: number;
 | 
			
		||||
    height: number;
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type LabCanvasComponent = typeof BaseBoard;
 | 
			
		||||
@@ -1,618 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div>
 | 
			
		||||
    <!-- 元器件选择菜单 (Drawer) -->
 | 
			
		||||
    <div class="drawer drawer-end z-50">
 | 
			
		||||
      <input
 | 
			
		||||
        id="Lab-drawer"
 | 
			
		||||
        type="checkbox"
 | 
			
		||||
        class="drawer-toggle"
 | 
			
		||||
        v-model="showComponentsMenu"
 | 
			
		||||
      />
 | 
			
		||||
      <div class="drawer-content">
 | 
			
		||||
        <!-- Page content here -->
 | 
			
		||||
        <label
 | 
			
		||||
          for="Lab-drawer"
 | 
			
		||||
          class="drawer-button btn btn-primary rounded-2xl"
 | 
			
		||||
          ><Plus></Plus
 | 
			
		||||
        ></label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="drawer-side">
 | 
			
		||||
        <label
 | 
			
		||||
          for="Lab-drawer"
 | 
			
		||||
          aria-label="close sidebar"
 | 
			
		||||
          class="drawer-overlay !bg-opacity-50"
 | 
			
		||||
        ></label>
 | 
			
		||||
        <div
 | 
			
		||||
          class="menu p-0 w-[460px] min-h-full bg-base-100 shadow-xl flex flex-col"
 | 
			
		||||
        >
 | 
			
		||||
          <!-- 菜单头部 -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="p-6 border-b border-base-300 flex justify-between items-center"
 | 
			
		||||
          >
 | 
			
		||||
            <h3 class="text-xl font-bold text-primary flex items-center gap-2">
 | 
			
		||||
              <Plus class="text-primary w-5 h-5"></Plus>
 | 
			
		||||
              添加实验元器件
 | 
			
		||||
            </h3>
 | 
			
		||||
            <label
 | 
			
		||||
              for="Lab-drawer"
 | 
			
		||||
              class="btn btn-ghost btn-sm btn-circle"
 | 
			
		||||
              @click="closeMenu"
 | 
			
		||||
            >
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                width="20"
 | 
			
		||||
                height="20"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                stroke-width="2"
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
              >
 | 
			
		||||
                <line x1="18" y1="6" x2="6" y2="18"></line>
 | 
			
		||||
                <line x1="6" y1="6" x2="18" y2="18"></line>
 | 
			
		||||
              </svg>
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 导航栏 -->
 | 
			
		||||
          <div class="tabs tabs-boxed bg-base-200 mx-6 mt-4 rounded-box">
 | 
			
		||||
            <a
 | 
			
		||||
              class="tab"
 | 
			
		||||
              :class="{ 'tab-active': activeTab === 'components' }"
 | 
			
		||||
              @click="activeTab = 'components'"
 | 
			
		||||
              >元器件</a
 | 
			
		||||
            >
 | 
			
		||||
            <a
 | 
			
		||||
              class="tab"
 | 
			
		||||
              :class="{ 'tab-active': activeTab === 'templates' }"
 | 
			
		||||
              @click="activeTab = 'templates'"
 | 
			
		||||
              >模板</a
 | 
			
		||||
            >
 | 
			
		||||
            <a
 | 
			
		||||
              class="tab"
 | 
			
		||||
              :class="{ 'tab-active': activeTab === 'virtual' }"
 | 
			
		||||
              @click="activeTab = 'virtual'"
 | 
			
		||||
              >虚拟外设</a
 | 
			
		||||
            >
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 搜索框 -->
 | 
			
		||||
          <div class="px-6 py-4 border-b border-base-300">
 | 
			
		||||
            <div class="join w-full">
 | 
			
		||||
              <div class="join-item flex-1 relative">
 | 
			
		||||
                <input
 | 
			
		||||
                  type="text"
 | 
			
		||||
                  placeholder="搜索..."
 | 
			
		||||
                  class="input input-bordered input-sm w-full pl-10"
 | 
			
		||||
                  v-model="searchQuery"
 | 
			
		||||
                  @keyup.enter="searchComponents"
 | 
			
		||||
                />
 | 
			
		||||
                <svg
 | 
			
		||||
                  xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                  width="16"
 | 
			
		||||
                  height="16"
 | 
			
		||||
                  viewBox="0 0 24 24"
 | 
			
		||||
                  fill="none"
 | 
			
		||||
                  stroke="currentColor"
 | 
			
		||||
                  stroke-width="2"
 | 
			
		||||
                  stroke-linecap="round"
 | 
			
		||||
                  stroke-linejoin="round"
 | 
			
		||||
                  class="absolute left-3 top-1/2 -translate-y-1/2 text-base-content opacity-60"
 | 
			
		||||
                >
 | 
			
		||||
                  <circle cx="11" cy="11" r="8"></circle>
 | 
			
		||||
                  <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
 | 
			
		||||
                </svg>
 | 
			
		||||
              </div>
 | 
			
		||||
              <button class="btn btn-sm join-item" @click="searchComponents">
 | 
			
		||||
                搜索
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 元器件列表 (组件选项卡) -->
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="activeTab === 'components'"
 | 
			
		||||
            class="px-6 py-4 overflow-auto flex-1"
 | 
			
		||||
          >
 | 
			
		||||
            <div
 | 
			
		||||
              v-if="filteredComponents.length > 0"
 | 
			
		||||
              class="grid grid-cols-2 gap-4"
 | 
			
		||||
            >
 | 
			
		||||
              <div
 | 
			
		||||
                v-for="(component, index) in filteredComponents"
 | 
			
		||||
                :key="index"
 | 
			
		||||
                class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
 | 
			
		||||
                @click="addComponent(component)"
 | 
			
		||||
              >
 | 
			
		||||
                <div class="card-body p-3 items-center text-center">
 | 
			
		||||
                  <div
 | 
			
		||||
                    class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"
 | 
			
		||||
                  >
 | 
			
		||||
                    <!-- 直接使用组件作为预览 -->
 | 
			
		||||
                    <component
 | 
			
		||||
                      v-if="componentModules[component.type]"
 | 
			
		||||
                      :is="componentModules[component.type].default"
 | 
			
		||||
                      class="component-preview"
 | 
			
		||||
                      :size="getPreviewSize(component.type)"
 | 
			
		||||
                    />
 | 
			
		||||
                    <!-- 加载中状态 -->
 | 
			
		||||
                    <span v-else class="text-xs text-gray-400">加载中...</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <h3 class="card-title text-sm mt-2">{{ component.name }}</h3>
 | 
			
		||||
                  <p class="text-xs opacity-70">{{ component.type }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- 无搜索结果 -->
 | 
			
		||||
            <div v-else class="py-16 text-center">
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                width="48"
 | 
			
		||||
                height="48"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                stroke-width="1.5"
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                class="mx-auto text-base-300 mb-3"
 | 
			
		||||
              >
 | 
			
		||||
                <circle cx="11" cy="11" r="8"></circle>
 | 
			
		||||
                <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
 | 
			
		||||
                <line x1="8" y1="11" x2="14" y2="11"></line>
 | 
			
		||||
              </svg>
 | 
			
		||||
              <p class="text-base-content opacity-70">没有找到匹配的元器件</p>
 | 
			
		||||
              <button
 | 
			
		||||
                class="btn btn-sm btn-ghost mt-3"
 | 
			
		||||
                @click="searchQuery = ''"
 | 
			
		||||
              >
 | 
			
		||||
                清除搜索
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 模板列表 (模板选项卡) -->
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="activeTab === 'templates'"
 | 
			
		||||
            class="px-6 py-4 overflow-auto flex-1"
 | 
			
		||||
          >
 | 
			
		||||
            <div
 | 
			
		||||
              v-if="filteredTemplates.length > 0"
 | 
			
		||||
              class="grid grid-cols-2 gap-4"
 | 
			
		||||
            >
 | 
			
		||||
              <div
 | 
			
		||||
                v-for="(template, index) in filteredTemplates"
 | 
			
		||||
                :key="index"
 | 
			
		||||
                class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
 | 
			
		||||
                @click="addTemplate(template)"
 | 
			
		||||
              >
 | 
			
		||||
                <div class="card-body p-3 items-center text-center">
 | 
			
		||||
                  <div
 | 
			
		||||
                    class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"
 | 
			
		||||
                  >
 | 
			
		||||
                    <img
 | 
			
		||||
                      :src="
 | 
			
		||||
                        template.thumbnailUrl || '/placeholder-template.png'
 | 
			
		||||
                      "
 | 
			
		||||
                      alt="Template thumbnail"
 | 
			
		||||
                      class="max-h-full max-w-full object-contain"
 | 
			
		||||
                    />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <h3 class="card-title text-sm mt-2">{{ template.name }}</h3>
 | 
			
		||||
                  <p class="text-xs opacity-70">
 | 
			
		||||
                    {{ template.description || "模板" }}
 | 
			
		||||
                  </p>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- 无搜索结果 -->
 | 
			
		||||
            <div v-else class="py-16 text-center">
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                width="48"
 | 
			
		||||
                height="48"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                stroke-width="1.5"
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                class="mx-auto text-base-300 mb-3"
 | 
			
		||||
              >
 | 
			
		||||
                <circle cx="11" cy="11" r="8"></circle>
 | 
			
		||||
                <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
 | 
			
		||||
                <line x1="8" y1="11" x2="14" y2="11"></line>
 | 
			
		||||
              </svg>
 | 
			
		||||
              <p class="text-base-content opacity-70">没有找到匹配的模板</p>
 | 
			
		||||
              <button
 | 
			
		||||
                class="btn btn-sm btn-ghost mt-3"
 | 
			
		||||
                @click="searchQuery = ''"
 | 
			
		||||
              >
 | 
			
		||||
                清除搜索
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 虚拟外设列表 (虚拟外设选项卡) -->
 | 
			
		||||
          <div
 | 
			
		||||
            v-if="activeTab === 'virtual'"
 | 
			
		||||
            class="px-6 py-4 overflow-auto flex-1"
 | 
			
		||||
          >
 | 
			
		||||
            <div
 | 
			
		||||
              v-if="filteredVirtualDevices.length > 0"
 | 
			
		||||
              class="grid grid-cols-2 gap-4"
 | 
			
		||||
            >
 | 
			
		||||
              <div
 | 
			
		||||
                v-for="(device, index) in filteredVirtualDevices"
 | 
			
		||||
                :key="index"
 | 
			
		||||
                class="card bg-base-200 hover:bg-base-300 transition-all duration-300 hover:shadow-md cursor-pointer"
 | 
			
		||||
                @click="addComponent(device)"
 | 
			
		||||
              >
 | 
			
		||||
                <div class="card-body p-3 items-center text-center">
 | 
			
		||||
                  <div
 | 
			
		||||
                    class="bg-base-100 rounded-lg w-full h-[90px] flex items-center justify-center overflow-hidden p-2"
 | 
			
		||||
                  >
 | 
			
		||||
                    <!-- 直接使用组件作为预览 -->
 | 
			
		||||
                    <component
 | 
			
		||||
                      v-if="componentModules[device.type]"
 | 
			
		||||
                      :is="componentModules[device.type].default"
 | 
			
		||||
                      class="component-preview"
 | 
			
		||||
                      :size="getPreviewSize(device.type)"
 | 
			
		||||
                    />
 | 
			
		||||
                    <!-- 加载中状态 -->
 | 
			
		||||
                    <span v-else class="text-xs text-gray-400">加载中...</span>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <h3 class="card-title text-sm mt-2">{{ device.name }}</h3>
 | 
			
		||||
                  <p class="text-xs opacity-70">{{ device.type }}</p>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <!-- 无搜索结果 -->
 | 
			
		||||
            <div v-else class="py-16 text-center">
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                width="48"
 | 
			
		||||
                height="48"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                stroke-width="1.5"
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                class="mx-auto text-base-300 mb-3"
 | 
			
		||||
              >
 | 
			
		||||
                <circle cx="11" cy="11" r="8"></circle>
 | 
			
		||||
                <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
 | 
			
		||||
                <line x1="8" y1="11" x2="14" y2="11"></line>
 | 
			
		||||
              </svg>
 | 
			
		||||
              <p class="text-base-content opacity-70">没有找到匹配的虚拟外设</p>
 | 
			
		||||
              <button
 | 
			
		||||
                class="btn btn-sm btn-ghost mt-3"
 | 
			
		||||
                @click="searchQuery = ''"
 | 
			
		||||
              >
 | 
			
		||||
                清除搜索
 | 
			
		||||
              </button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <!-- 底部操作区 -->
 | 
			
		||||
          <div
 | 
			
		||||
            class="p-4 border-t border-base-300 bg-base-200 flex justify-between"
 | 
			
		||||
          >
 | 
			
		||||
            <label
 | 
			
		||||
              for="Lab-drawer"
 | 
			
		||||
              class="btn btn-sm btn-ghost"
 | 
			
		||||
              @click="closeMenu"
 | 
			
		||||
            >
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                width="16"
 | 
			
		||||
                height="16"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                stroke-width="2"
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                class="mr-1"
 | 
			
		||||
              >
 | 
			
		||||
                <path d="M19 12H5M12 19l-7-7 7-7"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              返回
 | 
			
		||||
            </label>
 | 
			
		||||
            <label
 | 
			
		||||
              for="Lab-drawer"
 | 
			
		||||
              class="btn btn-sm btn-primary"
 | 
			
		||||
              @click="closeMenu"
 | 
			
		||||
            >
 | 
			
		||||
              完成
 | 
			
		||||
            </label>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { Plus } from "lucide-vue-next";
 | 
			
		||||
import { ref, computed, shallowRef, onMounted } from "vue";
 | 
			
		||||
import motherboardSvg from "../../components/equipments/svg/motherboard.svg";
 | 
			
		||||
import buttonSvg from "../../components/equipments/svg/button.svg";
 | 
			
		||||
 | 
			
		||||
// 显示/隐藏组件菜单
 | 
			
		||||
const isOpen = defineModel<boolean>("open", {
 | 
			
		||||
  default: false,
 | 
			
		||||
});
 | 
			
		||||
const showComponentsMenu = computed({
 | 
			
		||||
  get: () => isOpen.value,
 | 
			
		||||
  set: (value) => (isOpen.value = value),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 定义组件发出的事件
 | 
			
		||||
const emit = defineEmits([
 | 
			
		||||
  "close",
 | 
			
		||||
  "add-component",
 | 
			
		||||
  "add-template",
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 当前激活的选项卡
 | 
			
		||||
const activeTab = ref("components");
 | 
			
		||||
 | 
			
		||||
// --- 搜索功能 ---
 | 
			
		||||
const searchQuery = ref("");
 | 
			
		||||
 | 
			
		||||
// --- 可用元器件列表 ---
 | 
			
		||||
const availableComponents = [
 | 
			
		||||
  { type: "MechanicalButton", name: "机械按钮" },
 | 
			
		||||
  { type: "Switch", name: "开关" },
 | 
			
		||||
  { type: "Pin", name: "引脚" },
 | 
			
		||||
  { type: "SMT_LED", name: "贴片LED" },
 | 
			
		||||
  { type: "SevenSegmentDisplay", name: "数码管" },
 | 
			
		||||
  { type: "HDMI", name: "HDMI接口" },
 | 
			
		||||
  { type: "DDR", name: "DDR内存" },
 | 
			
		||||
  { type: "ETH", name: "以太网接口" },
 | 
			
		||||
  { type: "SD", name: "SD卡插槽" },
 | 
			
		||||
  { type: "SFP", name: "SFP光纤模块" },
 | 
			
		||||
  { type: "SMA", name: "SMA连接器" },
 | 
			
		||||
  { type: "MotherBoard", name: "主板" },
 | 
			
		||||
  { type: "PG2L100H_FBG676", name: "PG2L100H FBG676芯片" },
 | 
			
		||||
  { type: "BaseBoard", name: "通用底板" },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// --- 可用虚拟外设列表 ---
 | 
			
		||||
const availableVirtualDevices = [{ type: "DDS", name: "信号发生器" }];
 | 
			
		||||
 | 
			
		||||
// --- 可用模板列表 ---
 | 
			
		||||
const availableTemplates = ref([
 | 
			
		||||
  {
 | 
			
		||||
    name: "PG2L100H 基础开发板",
 | 
			
		||||
    id: "PG2L100H_Pango100pro",
 | 
			
		||||
    description: "包含主板和两个LED的基本设置",
 | 
			
		||||
    path: "/EquipmentTemplates/PG2L100H_Pango100pro.json",
 | 
			
		||||
    thumbnailUrl: motherboardSvg,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "矩阵键盘",
 | 
			
		||||
    id: "MatrixKey",
 | 
			
		||||
    description: "包含4x4,共16个按键的矩阵键盘",
 | 
			
		||||
    path: "/EquipmentTemplates/MatrixKey.json",
 | 
			
		||||
    thumbnailUrl: buttonSvg,
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 组件模块缓存
 | 
			
		||||
const componentModules = shallowRef<Record<string, any>>({});
 | 
			
		||||
 | 
			
		||||
// 动态加载组件定义
 | 
			
		||||
async function loadComponentModule(type: string) {
 | 
			
		||||
  if (!componentModules.value[type]) {
 | 
			
		||||
    try {
 | 
			
		||||
      // 假设组件都在 src/components/equipments/ 目录下,且文件名与 type 相同
 | 
			
		||||
      const module = await import(`@/components/equipments/${type}.vue`);
 | 
			
		||||
 | 
			
		||||
      // 将模块添加到缓存中
 | 
			
		||||
      componentModules.value = {
 | 
			
		||||
        ...componentModules.value,
 | 
			
		||||
        [type]: module,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      console.log(`Loaded module for ${type}:`, module);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`Failed to load component module ${type}:`, error);
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return componentModules.value[type];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 预加载组件模块
 | 
			
		||||
async function preloadComponentModules() {
 | 
			
		||||
  // 加载基础组件
 | 
			
		||||
  for (const component of availableComponents) {
 | 
			
		||||
    try {
 | 
			
		||||
      await loadComponentModule(component.type);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`Failed to preload component ${component.type}:`, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 加载虚拟外设组件
 | 
			
		||||
  for (const device of availableVirtualDevices) {
 | 
			
		||||
    try {
 | 
			
		||||
      await loadComponentModule(device.type);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`Failed to preload virtual device ${device.type}:`, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取组件预览时适合的尺寸
 | 
			
		||||
function getPreviewSize(componentType: string): number {
 | 
			
		||||
  // 根据组件类型返回适当的预览尺寸
 | 
			
		||||
  const previewSizes: Record<string, number> = {
 | 
			
		||||
    MechanicalButton: 0.4, // 按钮较大,需要更小尺寸
 | 
			
		||||
    Switch: 0.35, // 开关较大,需要更小尺寸
 | 
			
		||||
    Pin: 0.8, // 引脚较小,可以大一些
 | 
			
		||||
    SMT_LED: 0.7, // LED可以保持适中
 | 
			
		||||
    SevenSegmentDisplay: 0.4, // 数码管较大,需要较小尺寸
 | 
			
		||||
    HDMI: 0.5, // HDMI接口较大
 | 
			
		||||
    DDR: 0.5, // DDR内存较大
 | 
			
		||||
    ETH: 0.5, // 以太网接口较大
 | 
			
		||||
    SD: 0.6, // SD卡插槽适中
 | 
			
		||||
    SFP: 0.4, // SFP光纤模块较大
 | 
			
		||||
    SMA: 0.7, // SMA连接器可以适中
 | 
			
		||||
    MotherBoard: 0.13, // 主板最大,需要最小尺寸
 | 
			
		||||
    DDS: 0.3, // 信号发生器较大,需要较小尺寸
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // 返回对应尺寸,如果没有特定配置则返回默认值0.5
 | 
			
		||||
  return previewSizes[componentType] || 0.5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 搜索组件
 | 
			
		||||
function searchComponents() {
 | 
			
		||||
  // 根据用户输入过滤可用组件列表
 | 
			
		||||
  // 实际逻辑已经在 filteredComponents 计算属性中实现
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 关闭菜单
 | 
			
		||||
function closeMenu() {
 | 
			
		||||
  showComponentsMenu.value = false;
 | 
			
		||||
  emit("close");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加新元器件
 | 
			
		||||
async function addComponent(componentTemplate: { type: string; name: string }) {
 | 
			
		||||
  // 先加载组件模块
 | 
			
		||||
  const moduleRef = await loadComponentModule(componentTemplate.type);
 | 
			
		||||
  let defaultProps: Record<string, any> = {};
 | 
			
		||||
 | 
			
		||||
  // 尝试直接调用组件导出的getDefaultProps方法
 | 
			
		||||
  if (moduleRef) {
 | 
			
		||||
    if (typeof moduleRef.getDefaultProps === "function") {
 | 
			
		||||
      defaultProps = moduleRef.getDefaultProps();
 | 
			
		||||
      console.log(
 | 
			
		||||
        `Got default props from ${componentTemplate.type}:`,
 | 
			
		||||
        defaultProps,
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      // 回退到配置文件
 | 
			
		||||
      console.log(`No getDefaultProps found for ${componentTemplate.type}`);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    console.log(`Failed to load module for ${componentTemplate.type}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 发送添加组件事件给父组件
 | 
			
		||||
  emit("add-component", {
 | 
			
		||||
    type: componentTemplate.type,
 | 
			
		||||
    name: componentTemplate.name,
 | 
			
		||||
    props: defaultProps,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // 关闭菜单
 | 
			
		||||
  closeMenu();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 添加模板
 | 
			
		||||
async function addTemplate(template: any) {
 | 
			
		||||
  try {
 | 
			
		||||
    // 加载模板JSON文件
 | 
			
		||||
    const response = await fetch(template.path);
 | 
			
		||||
    if (!response.ok) {
 | 
			
		||||
      throw new Error(`Failed to load template: ${response.statusText}`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const templateData = await response.json();
 | 
			
		||||
    console.log("加载模板:", templateData);
 | 
			
		||||
 | 
			
		||||
    // 发出事件,将模板数据传递给父组件
 | 
			
		||||
    emit("add-template", {
 | 
			
		||||
      id: template.id,
 | 
			
		||||
      name: template.name,
 | 
			
		||||
      template: templateData,
 | 
			
		||||
      capsPage: template.capsPage,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // 关闭菜单
 | 
			
		||||
    closeMenu();
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error("加载模板出错:", error);
 | 
			
		||||
    alert("无法加载模板文件,请检查控制台错误信息");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 过滤后的元器件列表 (用于菜单)
 | 
			
		||||
const filteredComponents = computed(() => {
 | 
			
		||||
  if (!searchQuery.value || activeTab.value !== "components") {
 | 
			
		||||
    return availableComponents;
 | 
			
		||||
  }
 | 
			
		||||
  const query = searchQuery.value.toLowerCase();
 | 
			
		||||
  return availableComponents.filter(
 | 
			
		||||
    (component) =>
 | 
			
		||||
      component.name.toLowerCase().includes(query) ||
 | 
			
		||||
      component.type.toLowerCase().includes(query),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 过滤后的模板列表 (用于菜单)
 | 
			
		||||
const filteredTemplates = computed(() => {
 | 
			
		||||
  if (!searchQuery.value || activeTab.value !== "templates") {
 | 
			
		||||
    return availableTemplates.value;
 | 
			
		||||
  }
 | 
			
		||||
  const query = searchQuery.value.toLowerCase();
 | 
			
		||||
  return availableTemplates.value.filter(
 | 
			
		||||
    (template) =>
 | 
			
		||||
      template.name.toLowerCase().includes(query) ||
 | 
			
		||||
      (template.description &&
 | 
			
		||||
        template.description.toLowerCase().includes(query)),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 过滤后的虚拟外设列表 (用于菜单)
 | 
			
		||||
const filteredVirtualDevices = computed(() => {
 | 
			
		||||
  if (!searchQuery.value || activeTab.value !== "virtual") {
 | 
			
		||||
    return availableVirtualDevices;
 | 
			
		||||
  }
 | 
			
		||||
  const query = searchQuery.value.toLowerCase();
 | 
			
		||||
  return availableVirtualDevices.filter(
 | 
			
		||||
    (device) =>
 | 
			
		||||
      device.name.toLowerCase().includes(query) ||
 | 
			
		||||
      device.type.toLowerCase().includes(query),
 | 
			
		||||
  );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 生命周期钩子
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  // 预加载组件模块
 | 
			
		||||
  preloadComponentModules();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
/* 组件预览样式 */
 | 
			
		||||
.component-preview {
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
  max-height: 100%;
 | 
			
		||||
  object-fit: contain;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 动画效果 */
 | 
			
		||||
.animate-slideUp {
 | 
			
		||||
  animation: slideUp 0.3s ease-out forwards;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes slideUp {
 | 
			
		||||
  from {
 | 
			
		||||
    opacity: 0;
 | 
			
		||||
    transform: translateY(20px);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  to {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
    transform: translateY(0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,123 +0,0 @@
 | 
			
		||||
import { computed, shallowRef } from "vue";
 | 
			
		||||
import { createInjectionState, useStorage } from "@vueuse/core";
 | 
			
		||||
import { type LabCanvasComponentConfig } from "../LabCanvasType";
 | 
			
		||||
import type Konva from "konva";
 | 
			
		||||
 | 
			
		||||
const [useProvideLabCanvasStore, useLabCanvasStore] = createInjectionState(
 | 
			
		||||
  (initialStageConfig: Konva.StageConfig) => {
 | 
			
		||||
    const components = useStorage(
 | 
			
		||||
      "LabCanvasComponents",
 | 
			
		||||
      [] as LabCanvasComponentConfig[],
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // state
 | 
			
		||||
    const stageConfig = shallowRef<Konva.StageConfig>(initialStageConfig);
 | 
			
		||||
 | 
			
		||||
    // getters
 | 
			
		||||
    const getComponentById = computed(() => (id: string) => {
 | 
			
		||||
      return components.value.find(component => component.id === id);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const componentCount = computed(() => components.value.length);
 | 
			
		||||
 | 
			
		||||
    // actions
 | 
			
		||||
    function addComponent(componentData: {
 | 
			
		||||
      id: string;
 | 
			
		||||
      component: any;
 | 
			
		||||
      x?: number;
 | 
			
		||||
      y?: number;
 | 
			
		||||
      config: Konva.ShapeConfig;
 | 
			
		||||
    }) {
 | 
			
		||||
      const newComponent: LabCanvasComponentConfig = {
 | 
			
		||||
        id: componentData.id,
 | 
			
		||||
        component: componentData.component,
 | 
			
		||||
        x: componentData.x ?? 100,
 | 
			
		||||
        y: componentData.y ?? 100,
 | 
			
		||||
        config: componentData.config,
 | 
			
		||||
        isHoverring: false,
 | 
			
		||||
        hoverBox: {
 | 
			
		||||
          x: 0,
 | 
			
		||||
          y: 0,
 | 
			
		||||
          width: componentData.config.width || 100,
 | 
			
		||||
          height: componentData.config.height || 100,
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      components.value.push(newComponent);
 | 
			
		||||
      return newComponent;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function removeComponent(id: string) {
 | 
			
		||||
      const index = components.value.findIndex(component => component.id === id);
 | 
			
		||||
      if (index !== -1) {
 | 
			
		||||
        const removedComponent = components.value[index];
 | 
			
		||||
        components.value.splice(index, 1);
 | 
			
		||||
        return removedComponent;
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function removeComponents(ids: string[]) {
 | 
			
		||||
      const removedComponents: LabCanvasComponentConfig[] = [];
 | 
			
		||||
      
 | 
			
		||||
      ids.forEach(id => {
 | 
			
		||||
        const removed = removeComponent(id);
 | 
			
		||||
        if (removed) {
 | 
			
		||||
          removedComponents.push(removed);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      return removedComponents;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function updateComponent(id: string, updates: Partial<LabCanvasComponentConfig>) {
 | 
			
		||||
      const component = components.value.find(comp => comp.id === id);
 | 
			
		||||
      if (component) {
 | 
			
		||||
        Object.assign(component, updates);
 | 
			
		||||
        return component;
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function updateComponentPosition(id: string, x: number, y: number) {
 | 
			
		||||
      return updateComponent(id, { x, y });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function updateComponentConfig(id: string, config: Partial<Konva.ShapeConfig>) {
 | 
			
		||||
      const component = components.value.find(comp => comp.id === id);
 | 
			
		||||
      if (component) {
 | 
			
		||||
        component.config = { ...component.config, ...config };
 | 
			
		||||
        return component;
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function clearComponents() {
 | 
			
		||||
      const clearedComponents = [...components.value];
 | 
			
		||||
      components.value.splice(0);
 | 
			
		||||
      return clearedComponents;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function setComponents(newComponents: LabCanvasComponentConfig[]) {
 | 
			
		||||
      components.value.splice(0, components.value.length, ...newComponents);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return { 
 | 
			
		||||
      stageConfig, 
 | 
			
		||||
      components,
 | 
			
		||||
      // getters
 | 
			
		||||
      getComponentById,
 | 
			
		||||
      componentCount,
 | 
			
		||||
      // actions
 | 
			
		||||
      addComponent,
 | 
			
		||||
      removeComponent,
 | 
			
		||||
      removeComponents,
 | 
			
		||||
      updateComponent,
 | 
			
		||||
      updateComponentPosition,
 | 
			
		||||
      updateComponentConfig,
 | 
			
		||||
      clearComponents,
 | 
			
		||||
      setComponents,
 | 
			
		||||
    };
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export { useProvideLabCanvasStore, useLabCanvasStore };
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
import LabCanvas from './LabCanvas.vue';
 | 
			
		||||
import LabComponentsDrawer from './LabComponentsDrawer.vue';
 | 
			
		||||
import { useProvideLabCanvasStore, useLabCanvasStore } from './composable/LabCanvasManager';
 | 
			
		||||
 | 
			
		||||
export {LabCanvas, LabComponentsDrawer};
 | 
			
		||||
@@ -1,38 +0,0 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="h-screen w-screen">
 | 
			
		||||
    <SplitterGroup id="splitter-group" direction="horizontal">
 | 
			
		||||
      <SplitterPanel
 | 
			
		||||
        id="splitter-group-panel-canvas"
 | 
			
		||||
        :default-size="80"
 | 
			
		||||
        :min-size="30"
 | 
			
		||||
        class="bg-white border rounded-xl flex items-center justify-center"
 | 
			
		||||
      >
 | 
			
		||||
        <LabCanvas></LabCanvas>
 | 
			
		||||
      </SplitterPanel>
 | 
			
		||||
      <SplitterResizeHandle id="splitter-group-resize-handle" class="w-2" />
 | 
			
		||||
      <SplitterPanel
 | 
			
		||||
        id="splitter-group-panel-properties"
 | 
			
		||||
        :min-size="20"
 | 
			
		||||
        class="bg-white border rounded-xl flex items-center justify-center"
 | 
			
		||||
      >
 | 
			
		||||
        Panel A
 | 
			
		||||
      </SplitterPanel>
 | 
			
		||||
    </SplitterGroup>
 | 
			
		||||
    <!-- <div class="absolute top-20 left-10">
 | 
			
		||||
      <input type="checkbox" class="checkbox" @change="handleCacheChange" />
 | 
			
		||||
      cache shapes
 | 
			
		||||
    </div> -->
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { useProvideLabCanvasStore } from "@/components/LabCanvasNew/composable/LabCanvasManager";
 | 
			
		||||
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from "reka-ui";
 | 
			
		||||
import LabCanvas from "@/components/LabCanvasNew/LabCanvas.vue";
 | 
			
		||||
 | 
			
		||||
useProvideLabCanvasStore({width:window.innerWidth, height: window.innerHeight});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
@import "../assets/main.css";
 | 
			
		||||
</style>
 | 
			
		||||
		Reference in New Issue
	
	Block a user