refactor:持续解耦合
This commit is contained in:
		@@ -83,15 +83,15 @@
 | 
			
		||||
          }
 | 
			
		||||
        ">
 | 
			
		||||
        <!-- 动态渲染组件 -->
 | 
			
		||||
        <component :is="getComponentDefinition(component.type)" 
 | 
			
		||||
          v-if="props.componentModules[component.type] && getComponentDefinition(component.type)"
 | 
			
		||||
          v-bind="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="
 | 
			
		||||
            (pinInfo: any) =>
 | 
			
		||||
              handlePinClick(component.id, pinInfo, pinInfo.originalEvent)
 | 
			
		||||
          " :ref="(el: any) => setComponentRef(component.id, el)" />
 | 
			
		||||
          " :ref="(el: any) => componentManager.setComponentRef(component.id, el)" />
 | 
			
		||||
 | 
			
		||||
        <!-- Fallback if component module not loaded yet -->
 | 
			
		||||
        <div v-else
 | 
			
		||||
@@ -99,7 +99,7 @@
 | 
			
		||||
          <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">{{ props.componentModules[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>
 | 
			
		||||
@@ -138,6 +138,7 @@ import {
 | 
			
		||||
  watch,
 | 
			
		||||
  provide,
 | 
			
		||||
} from "vue";
 | 
			
		||||
import { useEventListener } from "@vueuse/core";
 | 
			
		||||
import WireComponent from "@/components/equipments/Wire.vue";
 | 
			
		||||
 | 
			
		||||
// 导入 diagram 管理器
 | 
			
		||||
@@ -151,17 +152,17 @@ import {
 | 
			
		||||
  parseConnectionPin,
 | 
			
		||||
  connectionArrayToWireItem,
 | 
			
		||||
  validateDiagramData,
 | 
			
		||||
} from "./diagramManager";
 | 
			
		||||
} from "./composable/diagramManager";
 | 
			
		||||
 | 
			
		||||
import type {
 | 
			
		||||
  DiagramData,
 | 
			
		||||
  DiagramPart,
 | 
			
		||||
  ConnectionArray,
 | 
			
		||||
  WireItem,
 | 
			
		||||
} from "./diagramManager";
 | 
			
		||||
} from "./composable/diagramManager";
 | 
			
		||||
 | 
			
		||||
import { CanvasCurrentSelectedComponentID } from "../InjectKeys";
 | 
			
		||||
import { useComponentManager } from "./componentManager";
 | 
			
		||||
import { useComponentManager } from "./composable/componentManager";
 | 
			
		||||
 | 
			
		||||
// 右键菜单处理函数
 | 
			
		||||
function handleContextMenu(e: MouseEvent) {
 | 
			
		||||
@@ -179,7 +180,6 @@ const emit = defineEmits([
 | 
			
		||||
 | 
			
		||||
// 定义组件接受的属性
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
  componentModules: Record<string, any>;
 | 
			
		||||
  showDocPanel?: boolean; // 添加属性接收文档面板的显示状态
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
@@ -214,8 +214,8 @@ const diagramData = ref<DiagramData>({
 | 
			
		||||
  connections: [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 组件引用跟踪
 | 
			
		||||
const componentRefs = ref<Record<string, any>>({});
 | 
			
		||||
// 组件引用跟踪(保留以便向后兼容)
 | 
			
		||||
const componentRefs = computed(() => componentManager?.componentRefs.value || {});
 | 
			
		||||
 | 
			
		||||
// 计算属性:从 diagramData 中提取组件列表,并按index属性排序
 | 
			
		||||
const diagramParts = computed<DiagramPart[]>(() => {
 | 
			
		||||
@@ -259,7 +259,7 @@ const wireItems = computed<WireItem[]>(() => {
 | 
			
		||||
      startPos.y = startComp.y;
 | 
			
		||||
 | 
			
		||||
      // 尝试获取引脚精确位置(如果有实现)
 | 
			
		||||
      const startCompRef = componentRefs.value?.[startCompId];
 | 
			
		||||
      const startCompRef = componentManager?.getComponentRef(startCompId);
 | 
			
		||||
      if (startCompRef && typeof startCompRef.getPinPosition === "function") {
 | 
			
		||||
        try {
 | 
			
		||||
          const pinPos = startCompRef.getPinPosition(startPinId);
 | 
			
		||||
@@ -285,7 +285,7 @@ const wireItems = computed<WireItem[]>(() => {
 | 
			
		||||
      endPos.y = endComp.y;
 | 
			
		||||
 | 
			
		||||
      // 尝试获取引脚精确位置
 | 
			
		||||
      const endCompRef = componentRefs.value?.[endCompId];
 | 
			
		||||
      const endCompRef = componentManager?.getComponentRef(endCompId);
 | 
			
		||||
      if (endCompRef && typeof endCompRef.getPinPosition === "function") {
 | 
			
		||||
        try {
 | 
			
		||||
          const pinPos = endCompRef.getPinPosition(endPinId);
 | 
			
		||||
@@ -330,6 +330,37 @@ const toastTimers: number[] = [];
 | 
			
		||||
// 文件选择引用
 | 
			
		||||
const fileInput = ref<HTMLInputElement | null>(null);
 | 
			
		||||
 | 
			
		||||
// VueUse事件监听器状态管理
 | 
			
		||||
const isDragEventActive = ref(false);
 | 
			
		||||
const isComponentDragEventActive = ref(false);
 | 
			
		||||
const isWireCreationEventActive = ref(false);
 | 
			
		||||
 | 
			
		||||
// 使用VueUse设置事件监听器
 | 
			
		||||
// 画布拖拽事件
 | 
			
		||||
useEventListener(document, 'mousemove', (e: MouseEvent) => {
 | 
			
		||||
  if (isDragEventActive.value) {
 | 
			
		||||
    onDrag(e);
 | 
			
		||||
  }
 | 
			
		||||
  if (isComponentDragEventActive.value) {
 | 
			
		||||
    onComponentDrag(e);
 | 
			
		||||
  }
 | 
			
		||||
  if (isWireCreationEventActive.value) {
 | 
			
		||||
    onCreatingWireMouseMove(e);
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
useEventListener(document, 'mouseup', () => {
 | 
			
		||||
  if (isDragEventActive.value) {
 | 
			
		||||
    stopDrag();
 | 
			
		||||
  }
 | 
			
		||||
  if (isComponentDragEventActive.value) {
 | 
			
		||||
    stopComponentDrag();
 | 
			
		||||
  }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 键盘事件
 | 
			
		||||
useEventListener(window, 'keydown', handleKeyDown);
 | 
			
		||||
 | 
			
		||||
// --- 缩放功能 ---
 | 
			
		||||
const MIN_SCALE = 0.2;
 | 
			
		||||
const MAX_SCALE = 10.0;
 | 
			
		||||
@@ -367,76 +398,6 @@ function onZoom(e: WheelEvent) {
 | 
			
		||||
  scale.value = newScale;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 动态组件渲染 ---
 | 
			
		||||
const getComponentDefinition = (type: string) => {
 | 
			
		||||
  const module = props.componentModules[type];
 | 
			
		||||
  
 | 
			
		||||
  if (!module) {
 | 
			
		||||
    console.warn(`No module found for component type: ${type}`);
 | 
			
		||||
    // 尝试异步加载组件模块
 | 
			
		||||
    loadComponentModule(type);
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 确保我们返回一个有效的组件定义
 | 
			
		||||
  if (module.default) {
 | 
			
		||||
    return module.default;
 | 
			
		||||
  } else if (module.__esModule && module.default) {
 | 
			
		||||
    // 有时 Vue 的动态导入会将默认导出包装在 __esModule 属性下
 | 
			
		||||
    return module.default;
 | 
			
		||||
  } else {
 | 
			
		||||
    console.warn(
 | 
			
		||||
      `Module for ${type} found but default export is missing`,
 | 
			
		||||
      module,
 | 
			
		||||
    );
 | 
			
		||||
    return null;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function prepareComponentProps(
 | 
			
		||||
  attrs: Record<string, any>,
 | 
			
		||||
  componentId?: string,
 | 
			
		||||
): Record<string, any> {
 | 
			
		||||
  const result: Record<string, any> = { ...attrs };
 | 
			
		||||
  if (componentId) {
 | 
			
		||||
    result.componentId = componentId;
 | 
			
		||||
  }
 | 
			
		||||
  // console.log(`组件属性 ID: ${componentId}`, result);
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 设置组件引用
 | 
			
		||||
function setComponentRef(componentId: string, el: any) {
 | 
			
		||||
  if (componentRefs.value) {
 | 
			
		||||
    if (el) {
 | 
			
		||||
      componentRefs.value[componentId] = el;
 | 
			
		||||
    } else {
 | 
			
		||||
      delete componentRefs.value[componentId];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重置组件引用缓存
 | 
			
		||||
function resetComponentRefs() {
 | 
			
		||||
  componentRefs.value = {};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 加载组件模块
 | 
			
		||||
async function loadComponentModule(type: string) {
 | 
			
		||||
  if (!props.componentModules[type]) {
 | 
			
		||||
    try {
 | 
			
		||||
      // 直接通过componentManager加载组件模块
 | 
			
		||||
      if (componentManager) {
 | 
			
		||||
        await componentManager.loadComponentModule(type);
 | 
			
		||||
        // 强制更新当前组件,确保新加载的模块能被识别
 | 
			
		||||
        console.log(`Component module ${type} loaded successfully`);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error(`Failed to request component module ${type}:`, error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 画布交互逻辑 ---
 | 
			
		||||
function handleCanvasMouseDown(e: MouseEvent) {
 | 
			
		||||
  // 如果是直接点击画布(而不是元器件),清除选中状态
 | 
			
		||||
@@ -468,8 +429,7 @@ function startDrag(e: MouseEvent) {
 | 
			
		||||
  dragStart.x = e.clientX - position.x;
 | 
			
		||||
  dragStart.y = e.clientY - position.y;
 | 
			
		||||
 | 
			
		||||
  document.addEventListener("mousemove", onDrag);
 | 
			
		||||
  document.addEventListener("mouseup", stopDrag);
 | 
			
		||||
  isDragEventActive.value = true;
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -484,8 +444,7 @@ function startMiddleDrag(e: MouseEvent) {
 | 
			
		||||
  dragStart.x = e.clientX - position.x;
 | 
			
		||||
  dragStart.y = e.clientY - position.y;
 | 
			
		||||
 | 
			
		||||
  document.addEventListener("mousemove", onDrag);
 | 
			
		||||
  document.addEventListener("mouseup", stopDrag);
 | 
			
		||||
  isDragEventActive.value = true;
 | 
			
		||||
  e.preventDefault();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -501,9 +460,7 @@ function onDrag(e: MouseEvent) {
 | 
			
		||||
function stopDrag() {
 | 
			
		||||
  isDragging.value = false;
 | 
			
		||||
  isMiddleDragging.value = false;
 | 
			
		||||
 | 
			
		||||
  document.removeEventListener("mousemove", onDrag);
 | 
			
		||||
  document.removeEventListener("mouseup", stopDrag);
 | 
			
		||||
  isDragEventActive.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// --- 组件拖拽交互 ---
 | 
			
		||||
@@ -558,9 +515,8 @@ function startComponentDrag(e: MouseEvent, component: DiagramPart) {
 | 
			
		||||
  componentDragOffset.x = mouseX_canvas - component.x;
 | 
			
		||||
  componentDragOffset.y = mouseY_canvas - component.y;
 | 
			
		||||
 | 
			
		||||
  // 添加全局监听器
 | 
			
		||||
  document.addEventListener("mousemove", onComponentDrag);
 | 
			
		||||
  document.addEventListener("mouseup", stopComponentDrag);
 | 
			
		||||
  // 激活组件拖拽事件监听
 | 
			
		||||
  isComponentDragEventActive.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 拖拽组件过程
 | 
			
		||||
@@ -589,14 +545,6 @@ function onComponentDrag(e: MouseEvent) {
 | 
			
		||||
  );
 | 
			
		||||
  if (!draggedComponent) return;
 | 
			
		||||
 | 
			
		||||
  // 更新组件位置
 | 
			
		||||
  // diagramData.value = updatePartPosition(
 | 
			
		||||
  //   diagramData.value,
 | 
			
		||||
  //   draggingComponentId.value,
 | 
			
		||||
  //   Math.round(newX),
 | 
			
		||||
  //   Math.round(newY),
 | 
			
		||||
  // );
 | 
			
		||||
 | 
			
		||||
  // 如果组件属于组,移动组内所有其他组件
 | 
			
		||||
  if (draggedComponent.group) {
 | 
			
		||||
    const deltaX = Math.round(newX) - draggedComponent.x;
 | 
			
		||||
@@ -647,8 +595,7 @@ function stopComponentDrag() {
 | 
			
		||||
    draggingComponentId.value = null;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  document.removeEventListener("mousemove", onComponentDrag);
 | 
			
		||||
  document.removeEventListener("mouseup", stopComponentDrag);
 | 
			
		||||
  isComponentDragEventActive.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 更新组件属性
 | 
			
		||||
@@ -700,8 +647,6 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
 | 
			
		||||
  console.log("鼠标位置:", mousePosition);
 | 
			
		||||
 | 
			
		||||
  if (!isCreatingWire.value) {
 | 
			
		||||
    // 开始创建连线
 | 
			
		||||
    const containerRect = canvasContainer.value.getBoundingClientRect();
 | 
			
		||||
    // 获取初始位置信息
 | 
			
		||||
    let pinPosition = pinInfo.position;
 | 
			
		||||
    console.log("Pin信息:", pinInfo);
 | 
			
		||||
@@ -710,7 +655,7 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
 | 
			
		||||
    console.log("引脚ID:", pinId);
 | 
			
		||||
 | 
			
		||||
    // 从组件引用中获取组件实例
 | 
			
		||||
    const component = componentRefs.value[componentId];
 | 
			
		||||
    const component = componentManager?.getComponentRef(componentId);
 | 
			
		||||
    console.log("组件引用:", component);
 | 
			
		||||
 | 
			
		||||
    // 查找组件部件对象以获取组件位置
 | 
			
		||||
@@ -771,7 +716,7 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
 | 
			
		||||
      pinId,
 | 
			
		||||
      pinInfo.constraint,
 | 
			
		||||
    );
 | 
			
		||||
    document.addEventListener("mousemove", onCreatingWireMouseMove);
 | 
			
		||||
    isWireCreationEventActive.value = true;
 | 
			
		||||
  } else {
 | 
			
		||||
    // 完成连线创建
 | 
			
		||||
    if (
 | 
			
		||||
@@ -786,7 +731,7 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
 | 
			
		||||
    // 获取终点引脚位置
 | 
			
		||||
    let endPosition = { x: 0, y: 0 };
 | 
			
		||||
    const componentPart = diagramParts.value.find((p) => p.id === componentId);
 | 
			
		||||
    const endComponent = componentRefs.value[componentId];
 | 
			
		||||
    const endComponent = componentManager?.getComponentRef(componentId);
 | 
			
		||||
 | 
			
		||||
    console.log("终点组件部件:", componentPart);
 | 
			
		||||
    console.log("终点组件引用:", endComponent);
 | 
			
		||||
@@ -848,7 +793,7 @@ function handlePinClick(componentId: string, pinInfo: any, event: MouseEvent) {
 | 
			
		||||
 | 
			
		||||
    // 重置连线创建状态
 | 
			
		||||
    resetWireCreation();
 | 
			
		||||
    document.removeEventListener("mousemove", onCreatingWireMouseMove);
 | 
			
		||||
    isWireCreationEventActive.value = false;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -881,7 +826,7 @@ function resetWireCreation() {
 | 
			
		||||
// 取消连线创建
 | 
			
		||||
function cancelWireCreation() {
 | 
			
		||||
  resetWireCreation();
 | 
			
		||||
  document.removeEventListener("mousemove", onCreatingWireMouseMove);
 | 
			
		||||
  isWireCreationEventActive.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 连线创建过程中的鼠标移动
 | 
			
		||||
@@ -1053,9 +998,6 @@ function showToast(
 | 
			
		||||
 | 
			
		||||
// --- 生命周期钩子 ---
 | 
			
		||||
onMounted(async () => {
 | 
			
		||||
  // 重置组件引用
 | 
			
		||||
  resetComponentRefs();
 | 
			
		||||
 | 
			
		||||
  // 设置componentManager的画布引用
 | 
			
		||||
  if (componentManager) {
 | 
			
		||||
    // 创建一个包含必要方法的画布API对象
 | 
			
		||||
@@ -1102,9 +1044,6 @@ onMounted(async () => {
 | 
			
		||||
    position.x = canvasContainer.value.clientWidth / 2 - 5000; // 画布宽度的一半
 | 
			
		||||
    position.y = canvasContainer.value.clientHeight / 2 - 5000; // 画布高度的一半
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 添加键盘事件监听器
 | 
			
		||||
  window.addEventListener("keydown", handleKeyDown);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 处理键盘事件
 | 
			
		||||
@@ -1123,32 +1062,15 @@ function handleKeyDown(e: KeyboardEvent) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
  // 清理事件监听器
 | 
			
		||||
  document.removeEventListener("mousemove", onComponentDrag);
 | 
			
		||||
  document.removeEventListener("mouseup", stopComponentDrag);
 | 
			
		||||
  document.removeEventListener("mousemove", onDrag);
 | 
			
		||||
  document.removeEventListener("mouseup", stopDrag);
 | 
			
		||||
  document.removeEventListener("mousemove", onCreatingWireMouseMove);
 | 
			
		||||
 | 
			
		||||
  // 移除键盘事件监听器
 | 
			
		||||
  window.removeEventListener("keydown", handleKeyDown);
 | 
			
		||||
 | 
			
		||||
  // 清除所有toast定时器
 | 
			
		||||
  toastTimers.forEach((timerId) => clearTimeout(timerId));
 | 
			
		||||
  
 | 
			
		||||
  // 重置事件状态
 | 
			
		||||
  isDragEventActive.value = false;
 | 
			
		||||
  isComponentDragEventActive.value = false;
 | 
			
		||||
  isWireCreationEventActive.value = false;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// --- 对外API ---
 | 
			
		||||
// 获取当前图表数据
 | 
			
		||||
function getDiagramData() {
 | 
			
		||||
  return diagramData.value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 设置图表数据
 | 
			
		||||
function setDiagramData(data: DiagramData) {
 | 
			
		||||
  diagramData.value = data;
 | 
			
		||||
  emit("diagram-updated", data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 无加载动画的数据更新方法
 | 
			
		||||
function updateDiagramDataDirectly(data: DiagramData) {
 | 
			
		||||
  // 检查组件是否仍然挂载
 | 
			
		||||
@@ -1218,15 +1140,6 @@ watch(
 | 
			
		||||
  },
 | 
			
		||||
  { deep: true },
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
// 当组件模块加载完成后,确保组件引用正确建立
 | 
			
		||||
watch(
 | 
			
		||||
  () => props.componentModules,
 | 
			
		||||
  () => {
 | 
			
		||||
    // 这里不需要特别处理,Vue 会自动通过 setComponentRef 函数更新引用
 | 
			
		||||
  },
 | 
			
		||||
  { deep: true },
 | 
			
		||||
);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,75 +0,0 @@
 | 
			
		||||
# 组件管理器重构
 | 
			
		||||
 | 
			
		||||
这次重构将 ProjectView 中关于组件和模板的增删查改逻辑抽取到了独立的服务中,使代码更加模块化和可维护。
 | 
			
		||||
 | 
			
		||||
## 新增文件
 | 
			
		||||
 | 
			
		||||
### `/src/components/LabCanvas/componentManager.ts`
 | 
			
		||||
使用 VueUse 的 `createInjectionState` 创建的组件管理服务,包含以下功能:
 | 
			
		||||
 | 
			
		||||
#### 状态管理
 | 
			
		||||
- `componentModules`: 存储动态导入的组件模块
 | 
			
		||||
- `selectedComponentId`: 当前选中的组件ID
 | 
			
		||||
- `selectedComponentData`: 当前选中的组件数据(计算属性)
 | 
			
		||||
- `selectedComponentConfig`: 当前选中组件的配置信息
 | 
			
		||||
 | 
			
		||||
#### 核心方法
 | 
			
		||||
- `loadComponentModule(type: string)`: 动态加载组件模块
 | 
			
		||||
- `preloadComponentModules(types: string[])`: 预加载多个组件模块
 | 
			
		||||
- `addComponent(componentData)`: 添加新组件到画布
 | 
			
		||||
- `addTemplate(templateData)`: 添加模板到画布
 | 
			
		||||
- `deleteComponent(componentId)`: 删除组件及其相关连接
 | 
			
		||||
- `selectComponent(componentData)`: 选中组件并生成配置
 | 
			
		||||
- `updateComponentProp()`: 更新组件属性
 | 
			
		||||
- `updateComponentDirectProp()`: 更新组件直接属性
 | 
			
		||||
- `moveComponent()`: 移动组件位置
 | 
			
		||||
 | 
			
		||||
### `/src/components/LabCanvas/index.ts`
 | 
			
		||||
统一导出文件,提供清晰的模块接口:
 | 
			
		||||
- 导出组件管理器的 hooks
 | 
			
		||||
- 导出类型定义
 | 
			
		||||
 | 
			
		||||
## 重构后的 ProjectView
 | 
			
		||||
 | 
			
		||||
现在 ProjectView 仅负责:
 | 
			
		||||
 | 
			
		||||
### 页面UI状态管理
 | 
			
		||||
- 文档面板的显示/隐藏
 | 
			
		||||
- 组件选择器的显示/隐藏
 | 
			
		||||
- 通知消息的显示
 | 
			
		||||
 | 
			
		||||
### 文档相关功能
 | 
			
		||||
- 加载和显示教程文档
 | 
			
		||||
- 文档面板的切换动画
 | 
			
		||||
 | 
			
		||||
### 事件委托
 | 
			
		||||
- 将所有组件操作事件委托给组件管理器处理
 | 
			
		||||
- 保持简洁的事件处理逻辑
 | 
			
		||||
 | 
			
		||||
## 使用方式
 | 
			
		||||
 | 
			
		||||
在需要使用组件管理功能的地方:
 | 
			
		||||
 | 
			
		||||
```typescript
 | 
			
		||||
// 提供服务(通常在父组件中)
 | 
			
		||||
import { useProvideComponentManager } from "@/components/LabCanvas";
 | 
			
		||||
const componentManager = useProvideComponentManager();
 | 
			
		||||
 | 
			
		||||
// 消费服务(在子组件中)
 | 
			
		||||
import { useComponentManager } from "@/components/LabCanvas";
 | 
			
		||||
const componentManager = useComponentManager(); // 如果未提供则会报错
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## 优势
 | 
			
		||||
 | 
			
		||||
1. **关注点分离**: ProjectView 专注于页面动画和UI状态,组件管理逻辑独立
 | 
			
		||||
2. **可重用性**: componentManager 可以在其他页面或组件中复用
 | 
			
		||||
3. **可测试性**: 组件管理逻辑可以独立进行单元测试
 | 
			
		||||
4. **类型安全**: 完整的 TypeScript 类型支持
 | 
			
		||||
5. **依赖注入**: 使用 VueUse 的注入机制,避免 prop drilling
 | 
			
		||||
 | 
			
		||||
## 注意事项
 | 
			
		||||
 | 
			
		||||
- 组件管理器需要在使用前通过 `useProvideComponentManager` 提供
 | 
			
		||||
- 画布引用需要通过 `setCanvasRef` 设置
 | 
			
		||||
- 初始化需要调用 `initialize` 方法
 | 
			
		||||
@@ -15,6 +15,7 @@ interface ComponentModule {
 | 
			
		||||
  config?: {
 | 
			
		||||
    props?: Array<PropertyConfig>;
 | 
			
		||||
  };
 | 
			
		||||
  __esModule?: boolean; // 添加 __esModule 属性
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定义组件管理器的状态和方法
 | 
			
		||||
@@ -24,7 +25,8 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
 | 
			
		||||
    const componentModules = ref<Record<string, ComponentModule>>({});
 | 
			
		||||
    const selectedComponentId = ref<string | null>(null);
 | 
			
		||||
    const selectedComponentConfig = shallowRef<{ props: PropertyConfig[] } | null>(null);
 | 
			
		||||
    const diagramCanvas = ref(null);
 | 
			
		||||
    const diagramCanvas = ref<any>(null);
 | 
			
		||||
    const componentRefs = ref<Record<string, any>>({});
 | 
			
		||||
 | 
			
		||||
    // 计算当前选中的组件数据
 | 
			
		||||
    const selectedComponentData = computed(() => {
 | 
			
		||||
@@ -486,6 +488,110 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
 | 
			
		||||
      diagramCanvas.value = canvasRef;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 设置组件DOM引用
 | 
			
		||||
     */
 | 
			
		||||
    function setComponentRef(componentId: string, el: any) {
 | 
			
		||||
      if (el) {
 | 
			
		||||
        componentRefs.value[componentId] = el;
 | 
			
		||||
      } else {
 | 
			
		||||
        delete componentRefs.value[componentId];
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取组件DOM引用
 | 
			
		||||
     */
 | 
			
		||||
    function getComponentRef(componentId: string) {
 | 
			
		||||
      return componentRefs.value[componentId];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取当前图表数据
 | 
			
		||||
     */
 | 
			
		||||
    function getDiagramData() {
 | 
			
		||||
      const canvasInstance = diagramCanvas.value;
 | 
			
		||||
      if (canvasInstance && canvasInstance.getDiagramData) {
 | 
			
		||||
        return canvasInstance.getDiagramData();
 | 
			
		||||
      }
 | 
			
		||||
      return { parts: [], connections: [], version: 1, author: "admin", editor: "me" };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 更新图表数据
 | 
			
		||||
     */
 | 
			
		||||
    function updateDiagramData(data: any) {
 | 
			
		||||
      const canvasInstance = diagramCanvas.value;
 | 
			
		||||
      if (canvasInstance && canvasInstance.updateDiagramDataDirectly) {
 | 
			
		||||
        canvasInstance.updateDiagramDataDirectly(data);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取画布位置和缩放信息
 | 
			
		||||
     */
 | 
			
		||||
    function getCanvasInfo() {
 | 
			
		||||
      const canvasInstance = diagramCanvas.value;
 | 
			
		||||
      if (!canvasInstance) return { position: { x: 0, y: 0 }, scale: 1 };
 | 
			
		||||
      
 | 
			
		||||
      const position = canvasInstance.getCanvasPosition ? canvasInstance.getCanvasPosition() : { x: 0, y: 0 };
 | 
			
		||||
      const scale = canvasInstance.getScale ? canvasInstance.getScale() : 1;
 | 
			
		||||
      
 | 
			
		||||
      return { position, scale };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 显示通知
 | 
			
		||||
     */
 | 
			
		||||
    function showToast(message: string, type: "success" | "error" | "info" = "info") {
 | 
			
		||||
      const canvasInstance = diagramCanvas.value;
 | 
			
		||||
      if (canvasInstance && canvasInstance.showToast) {
 | 
			
		||||
        canvasInstance.showToast(message, type);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取组件定义
 | 
			
		||||
     */
 | 
			
		||||
    function getComponentDefinition(type: string) {
 | 
			
		||||
      const module = componentModules.value[type];
 | 
			
		||||
      
 | 
			
		||||
      if (!module) {
 | 
			
		||||
        console.warn(`No module found for component type: ${type}`);
 | 
			
		||||
        // 尝试异步加载组件模块
 | 
			
		||||
        loadComponentModule(type);
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // 确保我们返回一个有效的组件定义
 | 
			
		||||
      if (module.default) {
 | 
			
		||||
        return module.default;
 | 
			
		||||
      } else if (module.__esModule && module.default) {
 | 
			
		||||
        // 有时 Vue 的动态导入会将默认导出包装在 __esModule 属性下
 | 
			
		||||
        return module.default;
 | 
			
		||||
      } else {
 | 
			
		||||
        console.warn(
 | 
			
		||||
          `Module for ${type} found but default export is missing`,
 | 
			
		||||
          module,
 | 
			
		||||
        );
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 准备组件属性
 | 
			
		||||
     */
 | 
			
		||||
    function prepareComponentProps(
 | 
			
		||||
      attrs: Record<string, any>,
 | 
			
		||||
      componentId?: string,
 | 
			
		||||
    ): Record<string, any> {
 | 
			
		||||
      const result: Record<string, any> = { ...attrs };
 | 
			
		||||
      if (componentId) {
 | 
			
		||||
        result.componentId = componentId;
 | 
			
		||||
      }
 | 
			
		||||
      return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 初始化组件管理器
 | 
			
		||||
     */
 | 
			
		||||
@@ -511,6 +617,7 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
 | 
			
		||||
      selectedComponentId,
 | 
			
		||||
      selectedComponentData,
 | 
			
		||||
      selectedComponentConfig,
 | 
			
		||||
      componentRefs,
 | 
			
		||||
      
 | 
			
		||||
      // 方法
 | 
			
		||||
      loadComponentModule,
 | 
			
		||||
@@ -523,6 +630,14 @@ const [useProvideComponentManager, useComponentManager] = createInjectionState(
 | 
			
		||||
      updateComponentDirectProp,
 | 
			
		||||
      moveComponent,
 | 
			
		||||
      setCanvasRef,
 | 
			
		||||
      setComponentRef,
 | 
			
		||||
      getComponentRef,
 | 
			
		||||
      getDiagramData,
 | 
			
		||||
      updateDiagramData,
 | 
			
		||||
      getCanvasInfo,
 | 
			
		||||
      showToast,
 | 
			
		||||
      getComponentDefinition,
 | 
			
		||||
      prepareComponentProps,
 | 
			
		||||
      initialize,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
@@ -83,4 +83,3 @@ export function setMousePosition(x: number, y: number) {
 | 
			
		||||
  mousePosition.y = y;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 其它Wire相关操作可继续扩展...
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
// 导出组件管理器服务
 | 
			
		||||
export { useProvideComponentManager, useComponentManager } from './componentManager';
 | 
			
		||||
export { useProvideComponentManager, useComponentManager } from './composable/componentManager';
 | 
			
		||||
 | 
			
		||||
// 导出图表管理器
 | 
			
		||||
export type { DiagramData, DiagramPart } from './diagramManager';
 | 
			
		||||
export type { DiagramData, DiagramPart } from './composable/diagramManager';
 | 
			
		||||
 | 
			
		||||
// 导出连线管理器
 | 
			
		||||
export type { WireItem } from './wireManager';
 | 
			
		||||
export type { WireItem } from './composable/wireManager';
 | 
			
		||||
 
 | 
			
		||||
@@ -128,7 +128,7 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import CollapsibleSection from "./CollapsibleSection.vue";
 | 
			
		||||
import { type DiagramPart } from "@/components/LabCanvas/diagramManager";
 | 
			
		||||
import { type DiagramPart } from "@/components/LabCanvas/composable/diagramManager";
 | 
			
		||||
import {
 | 
			
		||||
  type PropertyConfig,
 | 
			
		||||
  getPropValue,
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
// 导入所需的类型和组件
 | 
			
		||||
import { type DiagramPart } from "@/components/LabCanvas/diagramManager"; // 图表部件类型定义
 | 
			
		||||
import { type DiagramPart } from "@/components/LabCanvas/composable/diagramManager"; // 图表部件类型定义
 | 
			
		||||
import { type PropertyConfig } from "@/components/equipments/componentConfig"; // 属性配置类型定义
 | 
			
		||||
import CollapsibleSection from "./CollapsibleSection.vue"; // 可折叠区域组件
 | 
			
		||||
import PropertyEditor from "./PropertyEditor.vue"; // 属性编辑器组件
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
// componentConfig.ts 提供通用的组件配置功能
 | 
			
		||||
import type { DiagramPart } from '../LabCanvas/diagramManager';
 | 
			
		||||
import type { DiagramPart } from '../LabCanvas/composable/diagramManager';
 | 
			
		||||
 | 
			
		||||
// 属性配置接口
 | 
			
		||||
export interface PropertyConfig {
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@
 | 
			
		||||
          :min-size="30"
 | 
			
		||||
          class="relative bg-base-200 overflow-hidden h-full"
 | 
			
		||||
        >
 | 
			
		||||
          <DiagramCanvas ref="diagramCanvas" :componentModules="componentManager.componentModules.value" :showDocPanel="showDocPanel"
 | 
			
		||||
          <DiagramCanvas ref="diagramCanvas" :showDocPanel="showDocPanel"
 | 
			
		||||
            @diagram-updated="handleDiagramUpdated" @open-components="openComponentsMenu"
 | 
			
		||||
            @toggle-doc-panel="toggleDocPanel" />
 | 
			
		||||
        </SplitterPanel>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user