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

13 KiB
Raw Blame History

场景编辑器组件详细分析

1. 组件概述

scene-editor.vue 是一个基于 Vue 3 的复杂场景编辑器组件,主要用于管理和编辑工业机器人的场景配置。该组件提供了完整的场景编辑功能,包括机器人管理、路径规划、区域设置等。

2. 核心功能分析

2.1 场景数据管理

  • 场景读取: 通过 getSceneById API 获取场景数据
  • 场景推送: 通过 pushSceneById API 将场景数据推送到数据库
  • 场景保存: 通过编辑器服务保存场景配置
  • 文件导入/导出: 支持 .scene 格式文件的导入导出

2.2 编辑器状态控制

  • 编辑模式切换: 通过 editable 状态控制编辑器的启用/禁用
  • 权限管理: 根据编辑状态显示不同的操作按钮和功能
  • 实时状态同步: 编辑状态变化时自动更新编辑器服务状态

2.3 三大管理区域

  • 机器人管理: 显示和管理场景中的机器人组和单个机器人
  • 库区管理: 管理各种类型的库区(仅显示库区类型的区域)
  • 高级组管理: 管理复杂的路径、点位、区域等元素

2.4 详情卡片系统

  • 动态卡片显示: 根据选中元素类型显示对应的详情卡片
  • 编辑/查看模式: 根据编辑状态显示编辑卡片或查看卡片
  • 悬浮定位: 卡片固定在右侧悬浮显示

3. 技术架构分析

3.1 核心依赖关系

// 主要导入依赖
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 状态管理

// 核心状态定义
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 依赖注入系统

const EDITOR_KEY = Symbol('editor-key');
provide(EDITOR_KEY, editor);

使用 Vue 3 的依赖注入机制,将编辑器服务注入到子组件中。

3.3 EditorService 核心服务分析

3.3.1 服务基础

export class EditorService extends Meta2d {
  // 继承自 Meta2d 图形引擎
  // 提供场景编辑的核心功能
}

3.3.2 核心方法

  • load(): 加载场景数据到编辑器
  • save(): 保存当前场景数据
  • setState(): 设置编辑器状态(可编辑/只读)
  • updateRobots(): 更新机器人数据
  • addArea(): 添加区域
  • deleteById(): 删除指定元素

3.4 API 接口设计

3.4.1 场景相关API

// 获取场景数据
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 文件操作工具

// 文件选择
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 第一步:创建基础组件结构

<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 第二步:集成编辑器服务

// 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 第三步:实现场景数据管理

// 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 第四步:实现文件导入导出

// 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 第五步:集成管理组件

<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 第六步:实现选中元素监听

// 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 第七步:添加工具栏和详情卡片

<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 组件

功能: 管理机器人组和单个机器人 核心特性:

  • 机器人组的增删改查
  • 机器人的添加、注册、移除
  • 批量操作支持(全选、批量移除)
  • 搜索过滤功能

关键实现:

// 机器人列表获取
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 组件

功能: 管理点位、路线、区域等绘制元素 核心特性:

  • 分类显示不同类型的元素(点位、路线、区域)
  • 支持筛选特定类型(如仅显示库区)
  • 搜索过滤功能
  • 点击选中功能

关键实现:

// 点位列表
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 组件

功能: 提供编辑工具栏 核心特性:

  • 区域添加工具(库区、互斥区、非互斥区)
  • 场景保存功能
  • 撤销/重做操作
  • 删除操作

关键实现:

// 区域添加模式
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 核心样式

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