web-map/场景编辑器组件详细分析.md

509 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 场景编辑器组件详细分析
## 1. 组件概述
`scene-editor.vue` 是一个基于 Vue 3 的复杂场景编辑器组件,主要用于管理和编辑工业机器人的场景配置。该组件提供了完整的场景编辑功能,包括机器人管理、路径规划、区域设置等。
## 2. 核心功能分析
### 2.1 场景数据管理
- **场景读取**: 通过 `getSceneById` API 获取场景数据
- **场景推送**: 通过 `pushSceneById` API 将场景数据推送到数据库
- **场景保存**: 通过编辑器服务保存场景配置
- **文件导入/导出**: 支持 `.scene` 格式文件的导入导出
### 2.2 编辑器状态控制
- **编辑模式切换**: 通过 `editable` 状态控制编辑器的启用/禁用
- **权限管理**: 根据编辑状态显示不同的操作按钮和功能
- **实时状态同步**: 编辑状态变化时自动更新编辑器服务状态
### 2.3 三大管理区域
- **机器人管理**: 显示和管理场景中的机器人组和单个机器人
- **库区管理**: 管理各种类型的库区(仅显示库区类型的区域)
- **高级组管理**: 管理复杂的路径、点位、区域等元素
### 2.4 详情卡片系统
- **动态卡片显示**: 根据选中元素类型显示对应的详情卡片
- **编辑/查看模式**: 根据编辑状态显示编辑卡片或查看卡片
- **悬浮定位**: 卡片固定在右侧悬浮显示
## 3. 技术架构分析
### 3.1 核心依赖关系
```typescript
// 主要导入依赖
import { getSceneById, pushSceneById } from '@api/scene'; // 场景API
import { EditorService } from '@core/editor.service'; // 编辑器服务
import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils'; // 工具函数
```
### 3.2 组件架构设计
#### 3.2.1 状态管理
```typescript
// 核心状态定义
const title = ref<string>(''); // 场景标题
const editable = ref<boolean>(false); // 编辑状态
const show = ref<boolean>(true); // 卡片显示状态
const current = ref<{ type: string; id: string }>(); // 当前选中元素
const container = shallowRef<HTMLDivElement>(); // 编辑器容器
const editor = shallowRef<EditorService>(); // 编辑器服务实例
```
#### 3.2.2 依赖注入系统
```typescript
const EDITOR_KEY = Symbol('editor-key');
provide(EDITOR_KEY, editor);
```
使用 Vue 3 的依赖注入机制,将编辑器服务注入到子组件中。
### 3.3 EditorService 核心服务分析
#### 3.3.1 服务基础
```typescript
export class EditorService extends Meta2d {
// 继承自 Meta2d 图形引擎
// 提供场景编辑的核心功能
}
```
#### 3.3.2 核心方法
- **load()**: 加载场景数据到编辑器
- **save()**: 保存当前场景数据
- **setState()**: 设置编辑器状态(可编辑/只读)
- **updateRobots()**: 更新机器人数据
- **addArea()**: 添加区域
- **deleteById()**: 删除指定元素
### 3.4 API 接口设计
#### 3.4.1 场景相关API
```typescript
// 获取场景数据
export async function getSceneById(id: string): Promise<SceneDetail | null>;
// 推送场景到数据库
export async function pushSceneById(id: string): Promise<boolean>;
// 保存场景数据
export async function saveSceneById(id: string, json: string, png?: string): Promise<boolean>;
```
#### 3.4.2 文件操作工具
```typescript
// 文件选择
export async function selectFile(accept?: string, limit?: number): Promise<File | void>;
// 文件解码
export async function decodeTextFile(file: File): Promise<string | undefined>;
// 文本转二进制
export function textToBlob(text: string): Blob | undefined;
// 文件下载
export function downloadFile(url: string, name?: string): void;
```
## 4. 从零开发实现过程
### 4.1 第一步:创建基础组件结构
```vue
<script setup lang="ts">
// 1. 定义组件属性
type Props = {
id: string; // 场景ID
};
const props = defineProps<Props>();
// 2. 基础状态管理
const title = ref<string>('');
const editable = ref<boolean>(false);
</script>
<template>
<a-layout class="full">
<!-- 头部区域 -->
<a-layout-header class="p-16" style="height: 64px">
<!-- 标题和操作按钮 -->
</a-layout-header>
<!-- 主体区域 -->
<a-layout class="p-16">
<!-- 左侧面板 -->
<a-layout-sider :width="320">
<!-- 选项卡内容 -->
</a-layout-sider>
<!-- 编辑器区域 -->
<a-layout-content>
<div ref="container" class="editor-container full"></div>
</a-layout-content>
</a-layout>
</a-layout>
</template>
```
### 4.2 第二步:集成编辑器服务
```typescript
// 1. 导入编辑器服务
import { EditorService } from '@core/editor.service';
// 2. 创建编辑器实例
const container = shallowRef<HTMLDivElement>();
const editor = shallowRef<EditorService>();
// 3. 组件挂载时初始化编辑器
onMounted(() => {
editor.value = new EditorService(container.value!);
});
// 4. 设置依赖注入
const EDITOR_KEY = Symbol('editor-key');
provide(EDITOR_KEY, editor);
```
### 4.3 第三步:实现场景数据管理
```typescript
// 1. 导入API
import { getSceneById, pushSceneById } from '@api/scene';
// 2. 读取场景数据
const readScene = async () => {
const res = await getSceneById(props.id);
title.value = res?.label ?? '';
editor.value?.load(res?.json, editable.value);
};
// 3. 推送场景数据
const pushScene = async () => {
const res = await pushSceneById(props.id);
if (!res) return Promise.reject();
message.success(t('场景推送成功'));
return Promise.resolve();
};
// 4. 监听场景ID变化
watch(
() => props.id,
() => readScene(),
{ immediate: true, flush: 'post' },
);
```
### 4.4 第四步:实现文件导入导出
```typescript
// 1. 导入工具函数
import { decodeTextFile, downloadFile, selectFile, textToBlob } from '@core/utils';
// 2. 导入场景文件
const importScene = async () => {
const file = await selectFile('.scene');
if (!file?.size) return;
const json = await decodeTextFile(file);
editor.value?.load(json, editable.value);
};
// 3. 导出场景文件
const exportScene = () => {
const json = editor.value?.save();
if (!json) return;
const blob = textToBlob(json);
if (!blob?.size) return;
const url = URL.createObjectURL(blob);
downloadFile(url, `${title.value || 'unknown'}.scene`);
URL.revokeObjectURL(url);
};
```
### 4.5 第五步:集成管理组件
```vue
<template>
<a-layout-sider :width="320">
<a-tabs type="card">
<!-- 机器人管理 -->
<a-tab-pane key="1" :tab="$t('机器人')">
<RobotGroups
v-if="editor"
:token="EDITOR_KEY"
:sid="id"
:editable="editable"
:current="current?.id"
@change="selectRobot"
show-group-edit
/>
</a-tab-pane>
<!-- 库区管理 -->
<a-tab-pane key="2" :tab="$t('库区')">
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" only-area1 />
</a-tab-pane>
<!-- 高级组管理 -->
<a-tab-pane key="3" :tab="$t('高级组')">
<PenGroups v-if="editor" :token="EDITOR_KEY" :current="current?.id" />
</a-tab-pane>
</a-tabs>
</a-layout-sider>
</template>
```
### 4.6 第六步:实现选中元素监听
```typescript
// 1. 监听编辑器选中元素
watch(
() => editor.value?.selected.value[0],
(v) => {
const pen = editor.value?.getPenById(v);
if (pen?.id) {
current.value = { type: pen.name as 'point' | 'line' | 'area', id: pen.id };
return;
}
if (current.value?.type === 'robot') return;
current.value = undefined;
},
);
// 2. 计算选中元素类型
const isRobot = computed(() => current.value?.type === 'robot');
const isPoint = computed(() => current.value?.type === 'point');
const isRoute = computed(() => current.value?.type === 'line');
const isArea = computed(() => current.value?.type === 'area');
// 3. 机器人选择处理
const selectRobot = (id: string) => {
current.value = { type: 'robot', id };
editor.value?.inactive();
};
```
### 4.7 第七步:添加工具栏和详情卡片
```vue
<template>
<!-- 工具栏 -->
<div v-if="editable" class="toolbar-container">
<EditorToolbar v-if="editor" :token="EDITOR_KEY" :id="id" />
</div>
<!-- 详情卡片 -->
<template v-if="current?.id">
<a-float-button style="top: 80px; right: 16px" shape="square" @click="show = !show">
<template #icon><i class="icon detail" /></template>
</a-float-button>
<div v-if="show" class="card-container">
<RobotDetailCard v-if="isRobot" :token="EDITOR_KEY" :current="current.id" />
<template v-if="isPoint">
<PointEditCard v-if="editable" :token="EDITOR_KEY" :id="current.id" />
<PointDetailCard v-else :token="EDITOR_KEY" :current="current.id" />
</template>
<!-- 其他卡片类型... -->
</div>
</template>
</template>
```
## 5. 子组件详细分析
### 5.1 RobotGroups 组件
**功能**: 管理机器人组和单个机器人
**核心特性**:
- 机器人组的增删改查
- 机器人的添加、注册、移除
- 批量操作支持(全选、批量移除)
- 搜索过滤功能
**关键实现**:
```typescript
// 机器人列表获取
const robots = computed<RobotInfo[]>(() => editor.value.robots.filter(({ label }) => label.includes(keyword.value)));
// 批量选择管理
const selected = reactive<Set<RobotInfo['id']>>(new Set());
const selectAll = (checked: boolean) => {
if (checked) {
robots.value.forEach(({ id }) => selected.add(id));
} else {
selected.clear();
}
};
```
### 5.2 PenGroups 组件
**功能**: 管理点位、路线、区域等绘制元素
**核心特性**:
- 分类显示不同类型的元素(点位、路线、区域)
- 支持筛选特定类型(如仅显示库区)
- 搜索过滤功能
- 点击选中功能
**关键实现**:
```typescript
// 点位列表
const points = computed<MapPen[]>(() =>
editor.value.points.value.filter(({ label }) => label?.includes(keyword.value)),
);
// 区域列表(按类型分组)
const areas = computed<MapPen[]>(() => editor.value.areas.value.filter(({ label }) => label?.includes(keyword.value)));
```
### 5.3 EditorToolbar 组件
**功能**: 提供编辑工具栏
**核心特性**:
- 区域添加工具(库区、互斥区、非互斥区)
- 场景保存功能
- 撤销/重做操作
- 删除操作
**关键实现**:
```typescript
// 区域添加模式
const mode = ref<MapAreaType>();
watch(editor.value.mouseBrush, (v) => {
if (!mode.value) return;
const [p1, p2] = v ?? [];
if (isEmpty(p1) || isEmpty(p2)) return;
editor.value.addArea(p1, p2, mode.value);
mode.value = undefined;
});
```
## 6. 样式设计分析
### 6.1 布局结构
- **头部**: 固定高度64px包含标题和操作按钮
- **主体**: 左侧面板320px宽度右侧编辑器自适应
- **工具栏**: 固定在底部中央,悬浮显示
- **详情卡片**: 固定在右侧320px宽度悬浮显示
### 6.2 核心样式
```scss
.editor-container {
background-color: transparent !important;
}
.toolbar-container {
position: fixed;
bottom: 40px;
left: 50%;
z-index: 100;
transform: translateX(-50%);
}
.card-container {
position: fixed;
top: 80px;
right: 64px;
z-index: 100;
width: 320px;
height: calc(100% - 96px);
overflow: visible;
pointer-events: none;
& > * {
pointer-events: all;
}
}
```
## 7. 维护和调试指南
### 7.1 常见问题排查
#### 问题1: 场景数据加载失败
**排查步骤**:
1. 检查 `props.id` 是否正确传入
2. 检查 `getSceneById` API 是否正常响应
3. 检查编辑器服务是否正确初始化
#### 问题2: 编辑器功能异常
**排查步骤**:
1. 检查 `container` 元素是否正确获取
2. 检查 `EditorService` 是否正确实例化
3. 检查依赖注入是否正常工作
#### 问题3: 文件导入导出失败
**排查步骤**:
1. 检查工具函数是否正确导入
2. 检查文件格式是否正确
3. 检查浏览器兼容性
### 7.2 性能优化建议
1. **使用 shallowRef**: 对于大对象使用 `shallowRef` 避免深度响应式
2. **组件懒加载**: 使用 `v-if` 控制组件渲染时机
3. **事件防抖**: 对于频繁触发的事件(如搜索)使用防抖
4. **内存管理**: 及时清理事件监听器和定时器
### 7.3 扩展开发指南
#### 添加新的元素类型
1.`EditorService` 中添加对应的管理方法
2.`PenGroups` 组件中添加新的分组
3. 创建对应的详情卡片组件
4. 在主组件中添加类型判断逻辑
#### 添加新的工具
1.`EditorToolbar` 组件中添加工具按钮
2.`EditorService` 中实现对应功能
3. 处理工具状态管理和交互逻辑
## 8. 总结
这个场景编辑器组件是一个功能完整、架构清晰的复杂组件,主要特点:
1. **模块化设计**: 通过子组件分离不同功能模块
2. **服务化架构**: 核心逻辑封装在 EditorService 中
3. **响应式状态管理**: 使用 Vue 3 的响应式系统管理复杂状态
4. **依赖注入**: 通过 provide/inject 实现服务共享
5. **文件操作**: 完整的文件导入导出功能
6. **用户体验**: 良好的交互设计和视觉反馈
对于维护和扩展这个组件,需要重点关注:
- EditorService 的 API 设计和实现
- 各子组件之间的通信机制
- 状态管理的一致性
- 性能优化和内存管理
</rewritten_file>