diff --git a/src/components/LabCanvas/LabCanvas.vue b/src/components/LabCanvas/LabCanvas.vue index 1e48f5d..0f13d59 100644 --- a/src/components/LabCanvas/LabCanvas.vue +++ b/src/components/LabCanvas/LabCanvas.vue @@ -3,7 +3,7 @@ + - + + + @@ -72,6 +79,7 @@ @@ -80,38 +88,13 @@ import LabComponentsDrawer from "./LabComponentsDrawer.vue"; import Konva from "konva"; import { isNull, isUndefined } from "lodash"; -import type { - VGroup, - VLayer, - VNode, - VStage, - VTransformer, -} from "@/utils/VueKonvaType"; -import { ref, reactive, watch, onMounted, useTemplateRef } from "vue"; -import type { IRect } from "konva/lib/types"; +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 stageSize = { - width: window.innerWidth, - height: window.innerHeight, -}; - -type CanvasObjectBox = { - x: number; - y: number; - width: number; - height: number; -}; - -type CanvasObject = { - type: "Rect"; - config: Konva.RectConfig; - id: string; - x: number; - y: number; - isHoverring: boolean; - box?: CanvasObjectBox; -}; +const labCanvasStore = useLabCanvasStore(); function calculateRectBounding( width: number, @@ -164,31 +147,8 @@ function calculateRectBounding( } } -const objMap = reactive>(new Map()); -onMounted(() => { - for (let n = 0; n < 100; n++) { - const id = Math.round(Math.random() * 10000).toString(); - const x = Math.random() * stageSize.width; - const y = Math.random() * stageSize.height; - const width = 30 + Math.random() * 30; - const height = 30 + Math.random() * 30; - const rotation = Math.random() * 180; - - objMap.set(id, { - type: "Rect", - config: { - width: width, - height: height, - rotation: rotation, - fill: "grey", - id: id, - }, - id: id, - x: x, - y: y, - isHoverring: false, - }); - } +const objMap = computed(() => { + return new Map(labCanvasStore?.components.value.map((item) => [item.id, item])); }); const layerRef = useTemplateRef("layerRef"); @@ -319,7 +279,7 @@ function handleMouseDown(e: Event) { // 获取鼠标事件信息 const mouseEvent = (e as any).evt as MouseEvent; - + // 如果是右键按下,开始右键拖拽 if (mouseEvent.button === 2) { isRightDragging.value = true; @@ -364,13 +324,13 @@ function handleMouseMove(e: Event) { 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; @@ -424,19 +384,19 @@ function handleMouseUp(e: Event) { // console.log(`Stage Scale: ${stageScale.value}`); let currentSelectedIds = []; - for (let [key, shape] of objMap) { - const shapeConfig = objMap.get(shape.id); + for (let [key, shape] of objMap.value) { + const shapeConfig = objMap.value.get(shape.id); if (isUndefined(shapeConfig)) { return; } - if (isUndefined(shapeConfig.box)) { + if (isUndefined(shapeConfig.hoverBox)) { if ( shapeConfig.config.width && shapeConfig.config.height && shapeConfig.config.rotation ) { - shapeConfig.box = calculateRectBounding( + shapeConfig.hoverBox = calculateRectBounding( shapeConfig.config.width, shapeConfig.config.height, shapeConfig.config.rotation, @@ -451,10 +411,10 @@ function handleMouseUp(e: Event) { if ( Konva.Util.haveIntersection(selBox, { - x: shapeConfig.box.x + shapeConfig.x, - y: shapeConfig.box.y + shapeConfig.y, - width: shapeConfig.box.width, - height: shapeConfig.box.height, + x: shapeConfig.hoverBox.x + shapeConfig.x, + y: shapeConfig.hoverBox.y + shapeConfig.y, + width: shapeConfig.hoverBox.width, + height: shapeConfig.hoverBox.height, }) ) currentSelectedIds.push(shapeConfig.id); @@ -521,19 +481,19 @@ function handleCanvasObjectMouseOver(evt: Event) { } // Get client rect - const objectConfig = objMap.get(object.id()); + const objectConfig = objMap.value.get(object.id()); if (isUndefined(objectConfig)) { return; } // Get clientBox for first time - if (isUndefined(objectConfig.box)) { + if (isUndefined(objectConfig.hoverBox)) { if ( objectConfig.config.width && objectConfig.config.height && objectConfig.config.rotation ) { - objectConfig.box = calculateRectBounding( + objectConfig.hoverBox = calculateRectBounding( objectConfig.config.width, objectConfig.config.height, objectConfig.config.rotation, @@ -562,7 +522,7 @@ function handleCanvasObjectMouseOut(evt: Event) { } // Get client rect - const objectConfig = objMap.get(object.id()); + const objectConfig = objMap.value.get(object.id()); if (isUndefined(objectConfig)) { return; } diff --git a/src/components/LabCanvas/LabCanvasNew.vue b/src/components/LabCanvas/LabCanvasNew.vue deleted file mode 100644 index 610200f..0000000 --- a/src/components/LabCanvas/LabCanvasNew.vue +++ /dev/null @@ -1,609 +0,0 @@ - - - - - diff --git a/src/components/LabCanvas/LabCanvasType.ts b/src/components/LabCanvas/LabCanvasType.ts new file mode 100644 index 0000000..50c2359 --- /dev/null +++ b/src/components/LabCanvas/LabCanvasType.ts @@ -0,0 +1,20 @@ +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; \ No newline at end of file diff --git a/src/components/LabCanvas/LabComponentsDrawerNew.vue b/src/components/LabCanvas/LabComponentsDrawerNew.vue deleted file mode 100644 index b336a49..0000000 --- a/src/components/LabCanvas/LabComponentsDrawerNew.vue +++ /dev/null @@ -1,675 +0,0 @@ - - - - - diff --git a/src/components/LabCanvas/QUICKSTART.md b/src/components/LabCanvas/QUICKSTART.md deleted file mode 100644 index c1d2338..0000000 --- a/src/components/LabCanvas/QUICKSTART.md +++ /dev/null @@ -1,236 +0,0 @@ -# 快速开始指南 - -这个文档将帮助你快速开始使用重构后的LabCanvas组件管理系统。 - -## 基本集成 - -### 1. 简单使用 - -最简单的方式是直接使用 `LabCanvasNew` 组件: - -```vue - - - -``` - -### 2. 自定义集成 - -如果你需要更多控制,可以分别使用各个管理器: - -```vue - - - -``` - -## 主要功能 - -### 组件管理 - -```javascript -// 添加组件 -const component = componentManager.addComponent('MechanicalButton', { x: 100, y: 100 }) - -// 删除组件 -componentManager.deleteComponent(componentId) - -// 选择组件 -componentManager.selectComponent(componentId) - -// 更新组件属性 -componentManager.updateComponentProps(componentId, { color: 'red' }) - -// 撤销/重做 -componentManager.undo() -componentManager.redo() -``` - -### 模板管理 - -```javascript -// 从URL加载模板 -const template = await templateManager.loadTemplateFromUrl('/templates/button-panel.json') - -// 创建模板 -const template = templateManager.createTemplateFromComponents( - 'Button Panel', - 'A panel with multiple buttons', - 'custom', - selectedComponents -) - -// 实例化模板 -const instance = templateManager.instantiateTemplate(templateId, { x: 200, y: 200 }) -``` - -### 渲染管理 - -```javascript -// 渲染单个组件 -renderer.renderComponent(component) - -// 批量渲染 -renderer.renderComponents(components) - -// 清空画布 -renderer.clearComponents() - -// 查找组件 -const componentId = renderer.getComponentAtPosition(x, y) -``` - -## 事件处理 - -组件管理器支持各种事件回调: - -```javascript -const componentManager = useKonvaComponentManager({ - events: { - onAdd: (component) => { - console.log('组件添加:', component) - }, - onDelete: (componentId) => { - console.log('组件删除:', componentId) - }, - onSelect: (componentIds) => { - console.log('组件选择:', componentIds) - }, - onMove: (componentId, x, y) => { - console.log('组件移动:', componentId, x, y) - } - } -}) -``` - -## 键盘快捷键 - -系统内置了常用的键盘快捷键: - -- `Ctrl/Cmd + Z`: 撤销 -- `Ctrl/Cmd + Shift + Z`: 重做 -- `Ctrl/Cmd + A`: 全选 -- `Ctrl/Cmd + D`: 复制 -- `Delete`: 删除选中组件 -- `Escape`: 清除选择 - -## 自定义组件类型 - -要添加新的组件类型: - -1. 创建组件模板: - -```javascript -const customTemplate = { - type: 'MyCustomComponent', - name: '我的自定义组件', - category: 'custom', - defaultProps: { - customProp: 'default value' - } -} - -componentManager.registerTemplate(customTemplate) -``` - -2. 创建对应的Vue组件文件 `MyCustomComponent.vue` - -3. 在组件抽屉中添加该类型 - -## 项目保存和加载 - -```javascript -// 保存项目 -const projectData = componentManager.getProjectData() -localStorage.setItem('my-project', JSON.stringify(projectData)) - -// 加载项目 -const projectData = JSON.parse(localStorage.getItem('my-project')) -componentManager.loadProjectData(projectData) -``` - -## 注意事项 - -1. 确保已安装 `konva` 和 `vue-konva` 依赖 -2. 确保已安装 `@vueuse/core` -3. 组件ID必须唯一 -4. 模板JSON格式需要符合规范 -5. 大量组件时注意性能优化 - -## 故障排除 - -### 常见问题 - -1. **组件不显示**: 检查组件是否正确注册,位置是否在可视区域内 -2. **选择不工作**: 确保事件处理正确设置,检查z-index -3. **性能问题**: 考虑使用虚拟化或分页加载 -4. **保存失败**: 检查localStorage可用性和数据大小限制 - -### 调试技巧 - -```javascript -// 启用调试模式 -window.labCanvasDebug = true - -// 查看当前状态 -console.log('Components:', componentManager.components.value) -console.log('Selected:', componentManager.selectedComponents.value) -console.log('History:', componentManager.history.value) -``` - -更多详细信息请参考 [README.md](./README.md)。 diff --git a/src/components/LabCanvas/README.md b/src/components/LabCanvas/README.md deleted file mode 100644 index f8572c7..0000000 --- a/src/components/LabCanvas/README.md +++ /dev/null @@ -1,281 +0,0 @@ -# LabCanvas 组件管理系统 - -这是一个基于 VueUse 和 Konva 的重构版本的组件管理系统,具有以下特性: - -## 功能特性 - -### 1. 组件管理 (`useKonvaComponentManager`) -- # 创建模板 -function createTemplate() { - const selectedComponents = getSelectedComponents() // 获取选中的组件 - const template = templateManager.createTemplateFromComponents( - 'My Template', - 'A custom template', - 'custom', - selectedComponents - ) - console.log('Template created:', template) -}复制 -- ✅ 组件选择(单选、多选、框选) -- ✅ 组件拖拽和位置更新 -- ✅ 撤销/重做功能 -- ✅ 自动保存到本地存储 -- ✅ 键盘快捷键支持 - -### 2. 模板管理 (`useKonvaTemplateManager`) -- ✅ 模板的创建、保存、加载 -- ✅ 从JSON文件导入模板 -- ✅ 模板实例化(支持批量组件添加) -- ✅ 模板分类和搜索 -- ✅ 本地存储模板 - -### 3. Konva渲染 (`useKonvaRenderer`) -- ✅ 自动渲染组件到Konva画布 -- ✅ 选择框和悬停效果 -- ✅ 组件事件处理 -- ✅ 层级管理 -- ✅ 边界框计算 - -### 4. 组件抽屉 (`LabComponentsDrawerNew`) -- ✅ 分类显示组件(基础、高级、虚拟外设、模板) -- ✅ 搜索功能 -- ✅ 组件预览 -- ✅ 模板支持 - -## 使用方法 - -### 基本用法 - -```vue - - - -``` - -### 高级用法 - 自定义组件管理 - -```vue - - - -``` - -### 模板管理 - -```vue - -``` - -## 组件类型定义 - -### KonvaComponent -```typescript -interface KonvaComponent { - id: string // 唯一标识符 - type: string // 组件类型 - name: string // 显示名称 - x: number // X坐标 - y: number // Y坐标 - rotation: number // 旋转角度 - scaleX: number // X缩放 - scaleY: number // Y缩放 - visible: boolean // 是否可见 - draggable: boolean // 是否可拖拽 - isSelected: boolean // 是否选中 - isHovered: boolean // 是否悬停 - zIndex: number // 层级 - groupId?: string // 组ID - locked: boolean // 是否锁定 - props: ComponentProps // 组件属性 - boundingBox?: IRect // 边界框 -} -``` - -### ComponentTemplate -```typescript -interface ComponentTemplate { - type: string // 组件类型 - name: string // 显示名称 - category: 'basic' | 'advanced' | 'virtual' | 'template' // 分类 - defaultProps: ComponentProps // 默认属性 - previewSize?: number // 预览大小 - icon?: string // 图标 -} -``` - -## 键盘快捷键 - -- `Ctrl/Cmd + Z`: 撤销 -- `Ctrl/Cmd + Shift + Z`: 重做 -- `Ctrl/Cmd + A`: 全选 -- `Ctrl/Cmd + D`: 复制选中组件 -- `Delete/Backspace`: 删除选中组件 -- `Escape`: 清除选择 - -## 鼠标操作 - -- **左键单击**: 选择组件 -- **左键拖拽**: 拖动组件 -- **右键拖拽**: 平移画布 -- **滚轮**: 缩放画布 -- **Shift/Ctrl + 左键**: 多选组件 -- **左键拖拽空白区域**: 框选组件 - -## 模板格式 - -模板JSON文件格式: -```json -{ - "id": "template-id", - "name": "模板名称", - "description": "模板描述", - "category": "template", - "components": [ - { - "id": "comp-1", - "type": "MechanicalButton", - "name": "按钮1", - "x": 0, - "y": 0, - "rotation": 0, - "scaleX": 1, - "scaleY": 1, - "visible": true, - "draggable": true, - "isSelected": false, - "isHovered": false, - "zIndex": 0, - "locked": false, - "props": { - "color": "blue", - "size": "medium" - } - } - ], - "groups": [] -} -``` - -## 扩展开发 - -### 添加自定义组件类型 - -1. 创建组件Vue文件 -2. 注册组件模板 -3. 添加到组件抽屉中 - -```typescript -// 注册新组件类型 -componentManager.registerTemplate({ - type: 'MyCustomComponent', - name: '我的自定义组件', - category: 'custom', - defaultProps: { - customProp: 'default value' - } -}) -``` - -### 自定义渲染 - -```typescript -// 自定义组件渲染逻辑 -renderer.registerComponentModule('MyCustomComponent', { - render: (component) => { - // 自定义渲染逻辑 - } -}) -``` - -这个重构版本提供了更好的类型安全、更清晰的架构分离,以及更强大的扩展性。 diff --git a/src/components/LabCanvas/composable/LabCanvasManager.ts b/src/components/LabCanvas/composable/LabCanvasManager.ts new file mode 100644 index 0000000..4976c67 --- /dev/null +++ b/src/components/LabCanvas/composable/LabCanvasManager.ts @@ -0,0 +1,123 @@ +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(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) { + 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) { + 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 }; diff --git a/src/components/LabCanvas/composables/useKonvaComponentManager.ts b/src/components/LabCanvas/composables/useKonvaComponentManager.ts deleted file mode 100644 index 2b7c6d9..0000000 --- a/src/components/LabCanvas/composables/useKonvaComponentManager.ts +++ /dev/null @@ -1,574 +0,0 @@ -import { ref, reactive, computed, watch, nextTick } from 'vue' -import { useLocalStorage, useDebounceFn, useEventListener } from '@vueuse/core' -import type { - KonvaComponent, - ComponentTemplate, - ComponentProps, - SelectionState, - HistoryRecord, - ComponentEvents, - CanvasConfig, - ProjectData -} from '../types/KonvaComponent' -import type Konva from 'konva' -import type { IRect } from 'konva/lib/types' - -export interface UseKonvaComponentManagerOptions { - // 画布配置 - canvasConfig?: Partial - // 事件回调 - events?: ComponentEvents - // 是否启用历史记录 - enableHistory?: boolean - // 历史记录最大条数 - maxHistorySize?: number - // 是否自动保存到本地存储 - autoSave?: boolean - // 自动保存的键名 - saveKey?: string -} - -export function useKonvaComponentManager(options: UseKonvaComponentManagerOptions = {}) { - // 默认配置 - const defaultCanvas: CanvasConfig = { - width: window.innerWidth, - height: window.innerHeight, - scale: 1, - offsetX: 0, - offsetY: 0, - gridSize: 20, - showGrid: true, - snapToGrid: false - } - - // 组件存储 - const components = ref>(new Map()) - const componentTemplates = ref>(new Map()) - - // 画布配置 - const canvasConfig = reactive({ - ...defaultCanvas, - ...options.canvasConfig - }) - - // 选择状态 - const selectionState = reactive({ - selectedIds: [], - hoveredId: null, - isDragging: false, - dragTargetId: null, - isSelecting: false, - selectionBox: { - visible: false, - x1: 0, - y1: 0, - x2: 0, - y2: 0 - } - }) - - // 历史记录 - const history = ref([]) - const historyIndex = ref(-1) - const maxHistorySize = options.maxHistorySize || 50 - - // 自动保存 - const saveKey = options.saveKey || 'konva-component-manager' - const savedData = useLocalStorage(saveKey, null) - - // 计算属性 - const selectedComponents = computed(() => { - return selectionState.selectedIds - .map(id => components.value.get(id)) - .filter(Boolean) as KonvaComponent[] - }) - - const visibleComponents = computed(() => { - return Array.from(components.value.values()).filter(c => c.visible) - }) - - const canUndo = computed(() => historyIndex.value > 0) - const canRedo = computed(() => historyIndex.value < history.value.length - 1) - - // 生成唯一ID - function generateId(): string { - return `component-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` - } - - // 添加历史记录 - function addHistoryRecord(record: Omit) { - if (!options.enableHistory) return - - // 移除当前索引之后的记录 - if (historyIndex.value < history.value.length - 1) { - history.value.splice(historyIndex.value + 1) - } - - // 添加新记录 - const newRecord: HistoryRecord = { - ...record, - id: generateId(), - timestamp: Date.now() - } - - history.value.push(newRecord) - historyIndex.value = history.value.length - 1 - - // 限制历史记录数量 - if (history.value.length > maxHistorySize) { - history.value.shift() - historyIndex.value-- - } - } - - // 注册组件模板 - function registerTemplate(template: ComponentTemplate) { - componentTemplates.value.set(template.type, template) - } - - // 批量注册组件模板 - function registerTemplates(templates: ComponentTemplate[]) { - templates.forEach(registerTemplate) - } - - // 创建组件实例 - function createComponent( - template: ComponentTemplate, - position: { x: number; y: number }, - customProps?: Partial - ): KonvaComponent { - const component: KonvaComponent = { - id: generateId(), - type: template.type, - name: template.name, - x: position.x, - y: position.y, - rotation: 0, - scaleX: 1, - scaleY: 1, - visible: true, - draggable: true, - isSelected: false, - isHovered: false, - zIndex: components.value.size, - locked: false, - props: { - ...template.defaultProps, - ...customProps - } - } - - return component - } - - // 添加组件 - function addComponent( - type: string, - position: { x: number; y: number }, - customProps?: Partial - ): KonvaComponent | null { - const template = componentTemplates.value.get(type) - if (!template) { - console.error(`Component template not found: ${type}`) - return null - } - - const component = createComponent(template, position, customProps) - components.value.set(component.id, component) - - // 添加历史记录 - addHistoryRecord({ - type: 'add', - components: [component] - }) - - // 触发事件 - options.events?.onAdd?.(component) - - return component - } - - // 删除组件 - function deleteComponent(componentId: string): boolean { - const component = components.value.get(componentId) - if (!component) return false - - // 添加历史记录 - addHistoryRecord({ - type: 'delete', - components: [component] - }) - - components.value.delete(componentId) - - // 从选择状态中移除 - const index = selectionState.selectedIds.indexOf(componentId) - if (index > -1) { - selectionState.selectedIds.splice(index, 1) - } - - // 触发事件 - options.events?.onDelete?.(componentId) - - return true - } - - // 批量删除组件 - function deleteComponents(componentIds: string[]): boolean { - const deletedComponents = componentIds - .map(id => components.value.get(id)) - .filter(Boolean) as KonvaComponent[] - - if (deletedComponents.length === 0) return false - - // 添加历史记录 - addHistoryRecord({ - type: 'delete', - components: deletedComponents - }) - - // 删除组件 - componentIds.forEach(id => { - components.value.delete(id) - const index = selectionState.selectedIds.indexOf(id) - if (index > -1) { - selectionState.selectedIds.splice(index, 1) - } - }) - - // 触发事件 - componentIds.forEach(id => options.events?.onDelete?.(id)) - - return true - } - - // 更新组件位置 - function updateComponentPosition(componentId: string, x: number, y: number) { - const component = components.value.get(componentId) - if (!component || component.locked) return - - const oldState = { x: component.x, y: component.y } - component.x = x - component.y = y - - // 触发事件 - options.events?.onMove?.(componentId, x, y) - } - - // 更新组件属性 - function updateComponentProps(componentId: string, props: Partial) { - const component = components.value.get(componentId) - if (!component) return - - const oldProps = { ...component.props } - Object.assign(component.props, props) - - // 触发事件 - options.events?.onModify?.(componentId, component.props) - } - - // 选择组件 - function selectComponent(componentId: string, multi = false) { - if (!multi) { - // 清除之前的选择 - selectionState.selectedIds.forEach(id => { - const comp = components.value.get(id) - if (comp) comp.isSelected = false - }) - selectionState.selectedIds = [] - } - - const component = components.value.get(componentId) - if (component) { - if (!selectionState.selectedIds.includes(componentId)) { - selectionState.selectedIds.push(componentId) - component.isSelected = true - } - } - - options.events?.onSelect?.(selectionState.selectedIds) - } - - // 取消选择组件 - function deselectComponent(componentId: string) { - const index = selectionState.selectedIds.indexOf(componentId) - if (index > -1) { - selectionState.selectedIds.splice(index, 1) - const component = components.value.get(componentId) - if (component) component.isSelected = false - } - - options.events?.onSelect?.(selectionState.selectedIds) - } - - // 清除所有选择 - function clearSelection() { - selectionState.selectedIds.forEach(id => { - const comp = components.value.get(id) - if (comp) comp.isSelected = false - }) - selectionState.selectedIds = [] - options.events?.onSelect?.([]) - } - - // 框选组件 - function selectComponentsInArea(area: IRect) { - const selectedIds: string[] = [] - - components.value.forEach(component => { - if (!component.visible) return - - // 计算组件边界框 - const bbox = component.boundingBox || { - x: component.x, - y: component.y, - width: 100, // 默认宽度 - height: 100 // 默认高度 - } - - // 检查是否与选择区域相交 - if ( - bbox.x < area.x + area.width && - bbox.x + bbox.width > area.x && - bbox.y < area.y + area.height && - bbox.y + bbox.height > area.y - ) { - selectedIds.push(component.id) - component.isSelected = true - } - }) - - selectionState.selectedIds = selectedIds - options.events?.onSelect?.(selectedIds) - } - - // 设置组件悬停状态 - function setComponentHover(componentId: string | null) { - // 清除之前的悬停状态 - if (selectionState.hoveredId) { - const prevHovered = components.value.get(selectionState.hoveredId) - if (prevHovered) prevHovered.isHovered = false - } - - selectionState.hoveredId = componentId - - // 设置新的悬停状态 - if (componentId) { - const component = components.value.get(componentId) - if (component) component.isHovered = true - } - } - - // 复制组件 - function duplicateComponent(componentId: string): KonvaComponent | null { - const original = components.value.get(componentId) - if (!original) return null - - const template = componentTemplates.value.get(original.type) - if (!template) return null - - const duplicate = createComponent(template, { - x: original.x + 20, - y: original.y + 20 - }, original.props) - - components.value.set(duplicate.id, duplicate) - - // 添加历史记录 - addHistoryRecord({ - type: 'add', - components: [duplicate] - }) - - return duplicate - } - - // 撤销 - function undo() { - if (!canUndo.value) return - - const record = history.value[historyIndex.value - 1] - if (!record) return - - // 根据操作类型执行撤销 - switch (record.type) { - case 'add': - // 撤销添加 = 删除 - record.components?.forEach(comp => { - components.value.delete(comp.id) - }) - break - case 'delete': - // 撤销删除 = 添加 - record.components?.forEach(comp => { - components.value.set(comp.id, comp) - }) - break - // TODO: 实现其他操作类型的撤销 - } - - historyIndex.value-- - } - - // 重做 - function redo() { - if (!canRedo.value) return - - const record = history.value[historyIndex.value + 1] - if (!record) return - - // 根据操作类型执行重做 - switch (record.type) { - case 'add': - // 重做添加 - record.components?.forEach(comp => { - components.value.set(comp.id, comp) - }) - break - case 'delete': - // 重做删除 - record.components?.forEach(comp => { - components.value.delete(comp.id) - }) - break - // TODO: 实现其他操作类型的重做 - } - - historyIndex.value++ - } - - // 获取项目数据 - function getProjectData(): ProjectData { - return { - version: '1.0.0', - name: 'Untitled Project', - canvas: canvasConfig, - components: Array.from(components.value.values()), - groups: [], // TODO: 实现组功能 - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString() - } - } - - // 加载项目数据 - function loadProjectData(data: ProjectData) { - // 清除现有数据 - components.value.clear() - clearSelection() - - // 加载画布配置 - Object.assign(canvasConfig, data.canvas) - - // 加载组件 - data.components.forEach(comp => { - components.value.set(comp.id, comp) - }) - - // 清除历史记录 - history.value = [] - historyIndex.value = -1 - } - - // 自动保存 - const debouncedSave = useDebounceFn(() => { - if (options.autoSave) { - savedData.value = getProjectData() - } - }, 1000) - - // 监听组件变化并自动保存 - watch( - () => components.value.size, - () => { - debouncedSave() - }, - { deep: true } - ) - - // 键盘事件处理 - useEventListener('keydown', (e: KeyboardEvent) => { - if (e.ctrlKey || e.metaKey) { - switch (e.key) { - case 'z': - e.preventDefault() - if (e.shiftKey) { - redo() - } else { - undo() - } - break - case 'a': - e.preventDefault() - // 全选 - const allIds = Array.from(components.value.keys()) - selectionState.selectedIds = allIds - allIds.forEach(id => { - const comp = components.value.get(id) - if (comp) comp.isSelected = true - }) - options.events?.onSelect?.(allIds) - break - case 'd': - e.preventDefault() - // 复制选中的组件 - selectedComponents.value.forEach(comp => { - duplicateComponent(comp.id) - }) - break - } - } else if (e.key === 'Delete' || e.key === 'Backspace') { - e.preventDefault() - // 删除选中的组件 - if (selectionState.selectedIds.length > 0) { - deleteComponents(selectionState.selectedIds) - } - } else if (e.key === 'Escape') { - e.preventDefault() - clearSelection() - } - }) - - // 初始化时加载保存的数据 - if (options.autoSave && savedData.value) { - loadProjectData(savedData.value) - } - - return { - // 状态 - components: computed(() => components.value), - selectedComponents, - visibleComponents, - selectionState, - canvasConfig, - canUndo, - canRedo, - - // 组件管理 - registerTemplate, - registerTemplates, - addComponent, - deleteComponent, - deleteComponents, - updateComponentPosition, - updateComponentProps, - duplicateComponent, - - // 选择管理 - selectComponent, - deselectComponent, - clearSelection, - selectComponentsInArea, - setComponentHover, - - // 历史管理 - undo, - redo, - - // 项目管理 - getProjectData, - loadProjectData, - - // 工具方法 - generateId - } -} diff --git a/src/components/LabCanvas/composables/useKonvaRenderer.ts b/src/components/LabCanvas/composables/useKonvaRenderer.ts deleted file mode 100644 index 95d2258..0000000 --- a/src/components/LabCanvas/composables/useKonvaRenderer.ts +++ /dev/null @@ -1,406 +0,0 @@ -import { ref, computed, onMounted, onUnmounted, watch, type Ref } from 'vue' -import Konva from 'konva' -import type { Stage } from 'konva/lib/Stage' -import type { Layer } from 'konva/lib/Layer' -import type { Group } from 'konva/lib/Group' -import type { Node } from 'konva/lib/Node' -import type { KonvaComponent } from '../types/KonvaComponent' - -export interface UseKonvaRendererOptions { - // 是否启用动画 - enableAnimations?: boolean - // 选择框样式 - selectionStyle?: { - stroke?: string - strokeWidth?: number - dash?: number[] - } - // 悬停样式 - hoverStyle?: { - stroke?: string - strokeWidth?: number - opacity?: number - } -} - -export function useKonvaRenderer( - stageRef: Ref, - layerRef: Ref, - options: UseKonvaRendererOptions = {} -) { - // 默认样式配置 - const defaultSelectionStyle = { - stroke: 'rgb(125,193,239)', - strokeWidth: 2, - dash: [5, 5] - } - - const defaultHoverStyle = { - stroke: 'rgb(125,193,239)', - strokeWidth: 1, - opacity: 0.8 - } - - const selectionStyle = { ...defaultSelectionStyle, ...options.selectionStyle } - const hoverStyle = { ...defaultHoverStyle, ...options.hoverStyle } - - // Konva节点缓存 - const nodeCache = ref>(new Map()) - const componentModules = ref>(new Map()) - - // 获取Konva舞台 - const getStage = (): Stage | null => { - return stageRef.value?.getNode?.() || null - } - - // 获取Konva图层 - const getLayer = (): Layer | null => { - return layerRef.value?.getNode?.() || null - } - - // 注册组件模块 - function registerComponentModule(type: string, module: any) { - componentModules.value.set(type, module) - } - - // 批量注册组件模块 - function registerComponentModules(modules: Map) { - modules.forEach((module, type) => { - componentModules.value.set(type, module) - }) - } - - // 动态加载组件模块 - async function loadComponentModule(type: string): Promise { - if (componentModules.value.has(type)) { - return componentModules.value.get(type) - } - - try { - // 尝试动态导入组件 - const module = await import(`../../equipments/${type}.vue`) - componentModules.value.set(type, module) - return module - } catch (error) { - console.error(`Failed to load component module: ${type}`, error) - return null - } - } - - // 创建组件对应的Konva节点 - function createKonvaNode(component: KonvaComponent): Konva.Group | null { - const module = componentModules.value.get(component.type) - if (!module) { - console.warn(`Component module not found: ${component.type}`) - return null - } - - // 创建组节点 - const group = new Konva.Group({ - id: `group-${component.id}`, - x: component.x, - y: component.y, - rotation: component.rotation, - scaleX: component.scaleX, - scaleY: component.scaleY, - visible: component.visible, - draggable: component.draggable && !component.locked - }) - - // 创建基础矩形(作为组件的占位符) - const rect = new Konva.Rect({ - width: component.boundingBox?.width || 100, - height: component.boundingBox?.height || 100, - fill: 'transparent', - stroke: 'transparent', - strokeWidth: 0 - }) - - group.add(rect) - - // 添加选择框 - const selectionRect = new Konva.Rect({ - width: component.boundingBox?.width || 100, - height: component.boundingBox?.height || 100, - fill: 'transparent', - stroke: selectionStyle.stroke, - strokeWidth: selectionStyle.strokeWidth, - dash: selectionStyle.dash, - visible: component.isSelected - }) - - group.add(selectionRect) - - // 添加悬停框 - const hoverRect = new Konva.Rect({ - width: component.boundingBox?.width || 100, - height: component.boundingBox?.height || 100, - fill: 'transparent', - stroke: hoverStyle.stroke, - strokeWidth: hoverStyle.strokeWidth, - opacity: hoverStyle.opacity, - visible: component.isHovered && !component.isSelected - }) - - group.add(hoverRect) - - // 缓存节点 - nodeCache.value.set(component.id, group) - - return group - } - - // 更新Konva节点 - function updateKonvaNode(component: KonvaComponent) { - const group = nodeCache.value.get(component.id) - if (!group) return - - // 更新基础属性 - group.setAttrs({ - x: component.x, - y: component.y, - rotation: component.rotation, - scaleX: component.scaleX, - scaleY: component.scaleY, - visible: component.visible, - draggable: component.draggable && !component.locked - }) - - // 更新选择框和悬停框 - const children = group.getChildren() - const selectionRect = children[1] as Konva.Rect - const hoverRect = children[2] as Konva.Rect - - if (selectionRect) { - selectionRect.visible(component.isSelected) - } - - if (hoverRect) { - hoverRect.visible(component.isHovered && !component.isSelected) - } - } - - // 删除Konva节点 - function removeKonvaNode(componentId: string) { - const group = nodeCache.value.get(componentId) - if (group) { - group.destroy() - nodeCache.value.delete(componentId) - } - } - - // 渲染组件到Konva舞台 - function renderComponent(component: KonvaComponent) { - const layer = getLayer() - if (!layer) return - - // 如果节点已存在,更新它 - if (nodeCache.value.has(component.id)) { - updateKonvaNode(component) - return - } - - // 创建新节点 - const group = createKonvaNode(component) - if (group) { - layer.add(group) - } - } - - // 批量渲染组件 - function renderComponents(components: KonvaComponent[]) { - const layer = getLayer() - if (!layer) return - - components.forEach(component => { - renderComponent(component) - }) - - layer.batchDraw() - } - - // 清除所有渲染的组件 - function clearComponents() { - const layer = getLayer() - if (!layer) return - - nodeCache.value.forEach(group => { - group.destroy() - }) - nodeCache.value.clear() - layer.removeChildren() - layer.batchDraw() - } - - // 获取组件的边界框 - function getComponentBounds(componentId: string): { x: number; y: number; width: number; height: number } | null { - const group = nodeCache.value.get(componentId) - if (!group) return null - - const bounds = group.getClientRect() - return bounds - } - - // 查找指定位置的组件 - function getComponentAtPosition(x: number, y: number): string | null { - const stage = getStage() - if (!stage) return null - - const shape = stage.getIntersection({ x, y }) - if (!shape) return null - - // 找到对应的组 - let current: Node | null = shape - while (current && current.nodeType !== 'Group') { - current = current.getParent() - } - - if (current && current.id().startsWith('group-')) { - return current.id().replace('group-', '') - } - - return null - } - - // 获取选择区域内的组件 - function getComponentsInArea(area: { x: number; y: number; width: number; height: number }): string[] { - const componentIds: string[] = [] - - nodeCache.value.forEach((group, componentId) => { - const bounds = group.getClientRect() - - // 检查边界框是否与选择区域相交 - if ( - bounds.x < area.x + area.width && - bounds.x + bounds.width > area.x && - bounds.y < area.y + area.height && - bounds.y + bounds.height > area.y - ) { - componentIds.push(componentId) - } - }) - - return componentIds - } - - // 设置组件层级 - function setComponentZIndex(componentId: string, zIndex: number) { - const group = nodeCache.value.get(componentId) - if (!group) return - - group.zIndex(zIndex) - group.getLayer()?.batchDraw() - } - - // 将组件移动到顶层 - function moveComponentToTop(componentId: string) { - const group = nodeCache.value.get(componentId) - if (!group) return - - group.moveToTop() - group.getLayer()?.batchDraw() - } - - // 将组件移动到底层 - function moveComponentToBottom(componentId: string) { - const group = nodeCache.value.get(componentId) - if (!group) return - - group.moveToBottom() - group.getLayer()?.batchDraw() - } - - // 添加组件事件监听器 - function addComponentEventListeners( - component: KonvaComponent, - events: { - onSelect?: (componentId: string) => void - onDeselect?: (componentId: string) => void - onHover?: (componentId: string) => void - onUnhover?: (componentId: string) => void - onDragStart?: (componentId: string) => void - onDragEnd?: (componentId: string, x: number, y: number) => void - onDoubleClick?: (componentId: string) => void - } - ) { - const group = nodeCache.value.get(component.id) - if (!group) return - - // 点击事件 - group.on('click', () => { - events.onSelect?.(component.id) - }) - - // 双击事件 - group.on('dblclick', () => { - events.onDoubleClick?.(component.id) - }) - - // 鼠标进入事件 - group.on('mouseenter', () => { - events.onHover?.(component.id) - document.body.style.cursor = 'pointer' - }) - - // 鼠标离开事件 - group.on('mouseleave', () => { - events.onUnhover?.(component.id) - document.body.style.cursor = 'default' - }) - - // 拖拽开始事件 - group.on('dragstart', () => { - events.onDragStart?.(component.id) - }) - - // 拖拽结束事件 - group.on('dragend', () => { - const pos = group.position() - events.onDragEnd?.(component.id, pos.x, pos.y) - }) - } - - // 刷新图层 - function redraw() { - const layer = getLayer() - layer?.batchDraw() - } - - return { - // 状态 - nodeCache: computed(() => nodeCache.value), - - // 模块管理 - registerComponentModule, - registerComponentModules, - loadComponentModule, - - // 渲染管理 - renderComponent, - renderComponents, - clearComponents, - redraw, - - // 节点操作 - createKonvaNode, - updateKonvaNode, - removeKonvaNode, - - // 查询方法 - getComponentBounds, - getComponentAtPosition, - getComponentsInArea, - - // 层级管理 - setComponentZIndex, - moveComponentToTop, - moveComponentToBottom, - - // 事件管理 - addComponentEventListeners, - - // 工具方法 - getStage, - getLayer - } -} diff --git a/src/components/LabCanvas/composables/useKonvaTemplateManager.ts b/src/components/LabCanvas/composables/useKonvaTemplateManager.ts deleted file mode 100644 index 589f598..0000000 --- a/src/components/LabCanvas/composables/useKonvaTemplateManager.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { ref, computed } from 'vue' -import { useLocalStorage } from '@vueuse/core' -import type { - ComponentGroup, - KonvaComponent, - ComponentTemplate, - ProjectData -} from '../types/KonvaComponent' - -export interface TemplateData { - id: string - name: string - description?: string - thumbnailUrl?: string - category: string - components: KonvaComponent[] - groups?: ComponentGroup[] -} - -export interface UseKonvaTemplateManagerOptions { - // 模板存储键名 - storageKey?: string - // 是否启用本地存储 - enableStorage?: boolean -} - -export function useKonvaTemplateManager(options: UseKonvaTemplateManagerOptions = {}) { - const storageKey = options.storageKey || 'konva-templates' - - // 模板存储 - const templates = ref>(new Map()) - - // 本地存储 - const savedTemplates = useLocalStorage(storageKey, []) - - // 计算属性 - const templateList = computed(() => Array.from(templates.value.values())) - - const templatesByCategory = computed(() => { - const result: Record = {} - templateList.value.forEach(template => { - if (!result[template.category]) { - result[template.category] = [] - } - result[template.category].push(template) - }) - return result - }) - - // 生成唯一ID - function generateTemplateId(): string { - return `template-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` - } - - // 注册模板 - function registerTemplate(template: TemplateData) { - templates.value.set(template.id, template) - saveToStorage() - } - - // 批量注册模板 - function registerTemplates(templateList: TemplateData[]) { - templateList.forEach(template => { - templates.value.set(template.id, template) - }) - saveToStorage() - } - - // 获取模板 - function getTemplate(templateId: string): TemplateData | undefined { - return templates.value.get(templateId) - } - - // 删除模板 - function deleteTemplate(templateId: string): boolean { - const success = templates.value.delete(templateId) - if (success) { - saveToStorage() - } - return success - } - - // 根据类别过滤模板 - function getTemplatesByCategory(category: string): TemplateData[] { - return templateList.value.filter(template => template.category === category) - } - - // 搜索模板 - function searchTemplates(query: string): TemplateData[] { - const lowercaseQuery = query.toLowerCase() - return templateList.value.filter(template => - template.name.toLowerCase().includes(lowercaseQuery) || - (template.description && template.description.toLowerCase().includes(lowercaseQuery)) || - template.category.toLowerCase().includes(lowercaseQuery) - ) - } - - // 从组件创建模板 - function createTemplateFromComponents( - name: string, - description: string, - category: string, - components: KonvaComponent[], - groups?: ComponentGroup[] - ): TemplateData { - // 计算所有组件的边界框 - let minX = Infinity - let minY = Infinity - let maxX = -Infinity - let maxY = -Infinity - - components.forEach(comp => { - minX = Math.min(minX, comp.x) - minY = Math.min(minY, comp.y) - maxX = Math.max(maxX, comp.x + (comp.boundingBox?.width || 100)) - maxY = Math.max(maxY, comp.y + (comp.boundingBox?.height || 100)) - }) - - // 标准化组件位置(相对于左上角) - const normalizedComponents = components.map(comp => ({ - ...comp, - id: generateTemplateId(), // 给模板中的组件新的ID - x: comp.x - minX, - y: comp.y - minY, - isSelected: false, - isHovered: false - })) - - const template: TemplateData = { - id: generateTemplateId(), - name, - description, - category, - components: normalizedComponents, - groups: groups?.map(group => ({ - ...group, - id: generateTemplateId(), - x: group.x - minX, - y: group.y - minY - })) - } - - registerTemplate(template) - return template - } - - // 实例化模板组件 - function instantiateTemplate( - templateId: string, - position: { x: number; y: number }, - generateNewIds = true - ): { components: KonvaComponent[]; groups?: ComponentGroup[] } | null { - const template = getTemplate(templateId) - if (!template) return null - - // 生成新的ID映射表 - const idMapping = new Map() - - if (generateNewIds) { - template.components.forEach(comp => { - idMapping.set(comp.id, generateTemplateId()) - }) - template.groups?.forEach(group => { - idMapping.set(group.id, generateTemplateId()) - }) - } - - // 克隆组件并更新位置 - const components: KonvaComponent[] = template.components.map(comp => ({ - ...comp, - id: generateNewIds ? idMapping.get(comp.id)! : comp.id, - x: comp.x + position.x, - y: comp.y + position.y, - isSelected: false, - isHovered: false, - groupId: comp.groupId && generateNewIds ? idMapping.get(comp.groupId) : comp.groupId - })) - - // 克隆组 - const groups: ComponentGroup[] | undefined = template.groups?.map(group => ({ - ...group, - id: generateNewIds ? idMapping.get(group.id)! : group.id, - x: group.x + position.x, - y: group.y + position.y, - components: group.components.map(comp => ({ - ...comp, - id: generateNewIds ? idMapping.get(comp.id)! : comp.id, - x: comp.x + position.x, - y: comp.y + position.y, - groupId: generateNewIds ? idMapping.get(group.id)! : group.id - })) - })) - - return { components, groups } - } - - // 导出模板为JSON - function exportTemplate(templateId: string): string | null { - const template = getTemplate(templateId) - if (!template) return null - - return JSON.stringify(template, null, 2) - } - - // 从JSON导入模板 - function importTemplate(jsonData: string): TemplateData | null { - try { - const data = JSON.parse(jsonData) as TemplateData - - // 验证数据结构 - if (!data.id || !data.name || !data.components || !Array.isArray(data.components)) { - throw new Error('Invalid template format') - } - - // 生成新的ID以避免冲突 - const newTemplate: TemplateData = { - ...data, - id: generateTemplateId() - } - - registerTemplate(newTemplate) - return newTemplate - } catch (error) { - console.error('Failed to import template:', error) - return null - } - } - - // 保存到本地存储 - function saveToStorage() { - if (options.enableStorage !== false) { - savedTemplates.value = Array.from(templates.value.values()) - } - } - - // 从本地存储加载 - function loadFromStorage() { - if (options.enableStorage !== false) { - savedTemplates.value.forEach(template => { - templates.value.set(template.id, template) - }) - } - } - - // 从远程URL加载模板 - async function loadTemplateFromUrl(url: string): Promise { - try { - const response = await fetch(url) - if (!response.ok) { - throw new Error(`Failed to load template: ${response.statusText}`) - } - - const data = await response.json() - - // 验证和转换数据格式 - const template: TemplateData = { - id: data.id || generateTemplateId(), - name: data.name || 'Unnamed Template', - description: data.description, - category: data.category || 'imported', - components: data.parts?.map((part: any) => ({ - id: part.id || generateTemplateId(), - type: part.type, - name: part.type, - x: part.x || 0, - y: part.y || 0, - rotation: part.rotate || 0, - scaleX: 1, - scaleY: 1, - visible: part.isOn !== false, - draggable: !part.positionlock, - isSelected: false, - isHovered: false, - zIndex: part.index || 0, - groupId: part.group || undefined, - locked: part.positionlock || false, - props: part.attrs || {} - })) || [], - thumbnailUrl: data.thumbnailUrl - } - - registerTemplate(template) - return template - } catch (error) { - console.error('Failed to load template from URL:', error) - return null - } - } - - // 初始化时从本地存储加载 - loadFromStorage() - - return { - // 状态 - templates: computed(() => templates.value), - templateList, - templatesByCategory, - - // 模板管理 - registerTemplate, - registerTemplates, - getTemplate, - deleteTemplate, - getTemplatesByCategory, - searchTemplates, - - // 模板创建和实例化 - createTemplateFromComponents, - instantiateTemplate, - - // 导入导出 - exportTemplate, - importTemplate, - loadTemplateFromUrl, - - // 存储管理 - saveToStorage, - loadFromStorage, - - // 工具方法 - generateTemplateId - } -} diff --git a/src/components/LabCanvas/index.ts b/src/components/LabCanvas/index.ts index 90dbf08..cf6f248 100644 --- a/src/components/LabCanvas/index.ts +++ b/src/components/LabCanvas/index.ts @@ -1,316 +1,5 @@ -// LabCanvas组件管理系统 - 统一导出 -// 基于VueUse重构的Konva组件管理系统 +import LabCanvas from './LabCanvas.vue'; +import LabComponentsDrawer from './LabComponentsDrawer.vue'; +import { useProvideLabCanvasStore, useLabCanvasStore } from './composable/LabCanvasManager'; -// 类型定义 -export type { - KonvaComponent, - KonvaComponentBase, - ComponentTemplate, - ComponentProps, - ComponentGroup, - SelectionState, - HistoryRecord, - ComponentEvents, - CanvasConfig, - ProjectData -} from './types/KonvaComponent' - -// 核心管理器 -export { useKonvaComponentManager } from './composables/useKonvaComponentManager' -export type { UseKonvaComponentManagerOptions } from './composables/useKonvaComponentManager' - -// 模板管理器 -export { useKonvaTemplateManager } from './composables/useKonvaTemplateManager' -export type { - TemplateData, - UseKonvaTemplateManagerOptions -} from './composables/useKonvaTemplateManager' - -// 渲染器 -export { useKonvaRenderer } from './composables/useKonvaRenderer' -export type { UseKonvaRendererOptions } from './composables/useKonvaRenderer' - -// 组件 -export { default as LabCanvasNew } from './LabCanvasNew.vue' -export { default as LabComponentsDrawerNew } from './LabComponentsDrawerNew.vue' -export { default as ExampleUsage } from '../../views/TestView.vue' - -// 导入需要的类型 -import type { ProjectData, ComponentTemplate, CanvasConfig } from './types/KonvaComponent' - -// 工具函数 -export const KonvaLabUtils = { - /** - * 生成唯一ID - */ - generateId: (): string => { - return `component-${Date.now()}-${Math.random().toString(36).substr(2, 9)}` - }, - - /** - * 计算矩形的边界框(考虑旋转和缩放) - */ - calculateBoundingBox: ( - width: number, - height: number, - rotation: number = 0, - scaleX: number = 1, - scaleY: number = 1, - padding: number = 0 - ) => { - const scaledWidth = width * scaleX - const scaledHeight = height * scaleY - - const radians = (rotation * Math.PI) / 180 - const cos = Math.cos(radians) - const sin = Math.sin(radians) - - 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, - })) - - 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)) - - return { - x: minX - padding, - y: minY - padding, - width: maxX - minX + padding * 2, - height: maxY - minY + padding * 2, - } - }, - - /** - * 检查两个矩形是否相交 - */ - rectsIntersect: ( - rect1: { x: number; y: number; width: number; height: number }, - rect2: { x: number; y: number; width: number; height: number } - ): boolean => { - return ( - rect1.x < rect2.x + rect2.width && - rect1.x + rect1.width > rect2.x && - rect1.y < rect2.y + rect2.height && - rect1.y + rect1.height > rect2.y - ) - }, - - /** - * 计算两点之间的距离 - */ - distance: ( - point1: { x: number; y: number }, - point2: { x: number; y: number } - ): number => { - const dx = point2.x - point1.x - const dy = point2.y - point1.y - return Math.sqrt(dx * dx + dy * dy) - }, - - /** - * 格式化项目数据用于导出 - */ - formatProjectForExport: (projectData: ProjectData) => { - return { - ...projectData, - exportedAt: new Date().toISOString(), - components: projectData.components.map((comp: any) => ({ - ...comp, - // 移除运行时状态 - isSelected: false, - isHovered: false, - konvaNodeId: undefined - })) - } - }, - - /** - * 验证项目数据格式 - */ - validateProjectData: (data: any): { isValid: boolean; errors: string[] } => { - const errors: string[] = [] - - if (!data.version) errors.push('缺少版本信息') - if (!data.canvas) errors.push('缺少画布配置') - if (!Array.isArray(data.components)) errors.push('组件数据格式错误') - - // 验证组件 - if (Array.isArray(data.components)) { - data.components.forEach((comp: any, index: number) => { - if (!comp.id) errors.push(`组件 ${index} 缺少ID`) - if (!comp.type) errors.push(`组件 ${index} 缺少类型`) - if (typeof comp.x !== 'number') errors.push(`组件 ${index} X坐标无效`) - if (typeof comp.y !== 'number') errors.push(`组件 ${index} Y坐标无效`) - }) - } - - return { - isValid: errors.length === 0, - errors - } - } -} - -// 预定义的组件模板 -export const PredefinedTemplates: ComponentTemplate[] = [ - { - type: "MechanicalButton", - name: "机械按钮", - category: "basic", - defaultProps: { - size: "medium", - color: "blue", - pressed: false - }, - previewSize: 0.4 - }, - { - type: "Switch", - name: "开关", - category: "basic", - defaultProps: { - state: false, - style: "toggle" - }, - previewSize: 0.35 - }, - { - type: "Pin", - name: "引脚", - category: "basic", - defaultProps: { - type: "input", - label: "Pin" - }, - previewSize: 0.8 - }, - { - type: "SMT_LED", - name: "贴片LED", - category: "basic", - defaultProps: { - color: "red", - state: false, - brightness: 1 - }, - previewSize: 0.7 - }, - { - type: "SevenSegmentDisplay", - name: "数码管", - category: "advanced", - defaultProps: { - digits: 4, - value: "0000", - color: "red" - }, - previewSize: 0.4 - }, - { - type: "HDMI", - name: "HDMI接口", - category: "advanced", - defaultProps: { - version: "2.0", - connected: false - }, - previewSize: 0.5 - }, - { - type: "DDR", - name: "DDR内存", - category: "advanced", - defaultProps: { - type: "DDR4", - capacity: "8GB", - speed: "3200MHz" - }, - previewSize: 0.5 - }, - { - type: "ETH", - name: "以太网接口", - category: "advanced", - defaultProps: { - speed: "1000Mbps", - connected: false - }, - previewSize: 0.5 - }, - { - type: "SD", - name: "SD卡插槽", - category: "basic", - defaultProps: { - cardInserted: false, - type: "microSD" - }, - previewSize: 0.6 - }, - { - type: "SFP", - name: "SFP光纤模块", - category: "advanced", - defaultProps: { - type: "SFP+", - speed: "10Gbps", - connected: false - }, - previewSize: 0.4 - }, - { - type: "SMA", - name: "SMA连接器", - category: "basic", - defaultProps: { - type: "female", - impedance: "50ohm" - }, - previewSize: 0.7 - }, - { - type: "MotherBoard", - name: "主板", - category: "template", - defaultProps: { - model: "Generic", - size: "ATX" - }, - previewSize: 0.13 - }, - { - type: "DDS", - name: "信号发生器", - category: "virtual", - defaultProps: { - frequency: 1000, - amplitude: 1.0, - waveform: "sine", - enabled: false - }, - previewSize: 0.3 - } -] - -// 默认配置 -export const DefaultCanvasConfig: CanvasConfig = { - width: window.innerWidth, - height: window.innerHeight, - scale: 1, - offsetX: 0, - offsetY: 0, - gridSize: 20, - showGrid: true, - snapToGrid: false -} - -// 版本信息 -export const VERSION = '1.0.0' +export {LabCanvas, LabComponentsDrawer}; \ No newline at end of file diff --git a/src/components/LabCanvas/types/KonvaComponent.ts b/src/components/LabCanvas/types/KonvaComponent.ts deleted file mode 100644 index 160175b..0000000 --- a/src/components/LabCanvas/types/KonvaComponent.ts +++ /dev/null @@ -1,118 +0,0 @@ -import type { IRect } from 'konva/lib/types' - -// Konva组件的基础类型定义 -export interface KonvaComponentBase { - id: string - type: string - name: string - x: number - y: number - rotation: number - scaleX: number - scaleY: number - visible: boolean - draggable: boolean - isSelected: boolean - isHovered: boolean - zIndex: number - groupId?: string // 如果属于某个组 - locked: boolean // 是否锁定位置 -} - -// 组件属性配置 -export interface ComponentProps { - [key: string]: any -} - -// 具体的Konva组件实例 -export interface KonvaComponent extends KonvaComponentBase { - props: ComponentProps - konvaNodeId?: string // 对应的Konva节点ID - boundingBox?: IRect // 缓存的边界框 -} - -// 组件模板定义 -export interface ComponentTemplate { - type: string - name: string - description?: string - defaultProps: ComponentProps - category: 'basic' | 'advanced' | 'virtual' | 'template' - previewSize?: number - icon?: string -} - -// 组件组/模板定义 -export interface ComponentGroup { - id: string - name: string - description?: string - components: KonvaComponent[] - x: number - y: number - rotation: number - scaleX: number - scaleY: number - locked: boolean -} - -// 选择状态 -export interface SelectionState { - selectedIds: string[] - hoveredId: string | null - isDragging: boolean - dragTargetId: string | null - isSelecting: boolean - selectionBox: { - visible: boolean - x1: number - y1: number - x2: number - y2: number - } -} - -// 操作历史记录 -export interface HistoryRecord { - id: string - type: 'add' | 'delete' | 'move' | 'modify' | 'group' | 'ungroup' - timestamp: number - components?: KonvaComponent[] - oldState?: any - newState?: any -} - -// 组件事件类型 -export interface ComponentEvents { - onAdd?: (component: KonvaComponent) => void - onDelete?: (componentId: string) => void - onSelect?: (componentIds: string[]) => void - onMove?: (componentId: string, x: number, y: number) => void - onModify?: (componentId: string, props: ComponentProps) => void - onGroup?: (groupId: string, componentIds: string[]) => void - onUngroup?: (groupId: string) => void -} - -// 画布配置 -export interface CanvasConfig { - width: number - height: number - scale: number - offsetX: number - offsetY: number - gridSize: number - showGrid: boolean - snapToGrid: boolean -} - -// 导出/导入格式 -export interface ProjectData { - version: string - name: string - description?: string - canvas: CanvasConfig - components: KonvaComponent[] - groups: ComponentGroup[] - createdAt: string - updatedAt: string -} diff --git a/src/views/ProjectView.vue b/src/views/ProjectView.vue index 1a792c5..55982f6 100644 --- a/src/views/ProjectView.vue +++ b/src/views/ProjectView.vue @@ -26,9 +26,11 @@ + \ No newline at end of file