refactor:持续解耦合

This commit is contained in:
SikongJueluo 2025-07-09 16:32:17 +08:00
parent 91b00a977c
commit 3a292c0a98
No known key found for this signature in database
10 changed files with 183 additions and 231 deletions

View File

@ -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>

View File

@ -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` 方法

View File

@ -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,
};
}

View File

@ -83,4 +83,3 @@ export function setMousePosition(x: number, y: number) {
mousePosition.y = y;
}
// 其它Wire相关操作可继续扩展...

View File

@ -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';

View File

@ -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,

View File

@ -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"; //

View File

@ -1,5 +1,5 @@
// componentConfig.ts 提供通用的组件配置功能
import type { DiagramPart } from '../LabCanvas/diagramManager';
import type { DiagramPart } from '../LabCanvas/composable/diagramManager';
// 属性配置接口
export interface PropertyConfig {

View File

@ -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>