509 lines
13 KiB
Markdown
509 lines
13 KiB
Markdown
|
# 场景编辑器组件详细分析
|
|||
|
|
|||
|
## 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>
|