1038 lines
28 KiB
Markdown
1038 lines
28 KiB
Markdown
# 路径漫游功能开发
|
||
|
||
## 概述
|
||
|
||
> **目标**: 实现路径漫游功能,支持单条路径的漫游点管理和播放
|
||
>
|
||
> **交付物**:
|
||
> - 漫游点管理(添加、删除、跳转)
|
||
> - 路径设置(漫游时间、循环播放)
|
||
> - 漫游播放(播放时高亮当前点)
|
||
>
|
||
> **预估工作量**: 中等
|
||
> **执行方式**: 顺序执行
|
||
|
||
---
|
||
|
||
## 原型交互图
|
||
|
||
```
|
||
┌─────────────────────────────────────┐
|
||
│ 路径漫游 [×] │
|
||
├─────────────────────────────────────┤
|
||
│ │
|
||
│ ─────────── 路径设置 ─────────── │
|
||
│ │
|
||
│ 漫游时间 │
|
||
│ ┌───────────────────────┐ │
|
||
│ │ 10 │ 秒 │
|
||
│ └───────────────────────┘ │
|
||
│ │
|
||
│ ☑ 循环播放 │
|
||
│ │
|
||
│ ─────────── 漫游点 ──────────── │
|
||
│ │
|
||
│ ┌────────────┐ ┌────────────┐ │
|
||
│ │ +添加漫游点│ │ 删除全部 │ │
|
||
│ └────────────┘ └────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────┐ │
|
||
│ │ 漫游点0 [▶][×]│ │ ← 播放时高亮
|
||
│ └─────────────────────────────┘ │
|
||
│ ┌─────────────────────────────┐ │
|
||
│ │ 漫游点1 [▶][×]│ │
|
||
│ └─────────────────────────────┘ │
|
||
│ ┌─────────────────────────────┐ │
|
||
│ │ 漫游点2 [▶][×]│ │
|
||
│ └─────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────┐ │
|
||
│ │ ▶ 播放漫游 │ │
|
||
│ └─────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────┘
|
||
```
|
||
|
||
**关键交互**:
|
||
- 打开弹窗时:调用 `getPoints()` 获取已有漫游点
|
||
- 添加漫游点:调用 `addPoint()` 添加当前位置
|
||
- 删除漫游点:调用 `removePoint(index)`
|
||
- 跳转到点:调用 `jumpToPoint(index)`
|
||
- 播放漫游:调用 `play(options)`,通过 `onPointComplete` 高亮当前点
|
||
|
||
---
|
||
|
||
## 调用链路
|
||
|
||
```
|
||
用户操作 (WalkPathPanel)
|
||
↓
|
||
registry.engine3d.pathRoamingXxx()
|
||
↓
|
||
EngineManager (managers/engine-manager.ts)
|
||
↓
|
||
this.engineInstance.pathRoamingXxx()
|
||
↓
|
||
Engine (components/engine/index.ts)
|
||
↓
|
||
this.engine.pathRoaming.xxx()
|
||
↓
|
||
底层引擎 SDK
|
||
```
|
||
|
||
---
|
||
|
||
## 任务列表
|
||
|
||
### 任务 1: 添加国际化文本
|
||
|
||
**修改文件**:
|
||
- `src/locales/types.ts`
|
||
- `src/locales/zh-CN.ts`
|
||
- `src/locales/en-US.ts`
|
||
|
||
**types.ts 修改内容** (替换 `walkControl.path` 部分):
|
||
|
||
```typescript
|
||
path: {
|
||
/** 对话框标题 */
|
||
dialogTitle: string;
|
||
/** 漫游时间标签 */
|
||
duration: string;
|
||
/** 时间单位 */
|
||
durationUnit: string;
|
||
/** 循环播放 */
|
||
loop: string;
|
||
/** 添加漫游点按钮 */
|
||
addPoint: string;
|
||
/** 删除全部按钮 */
|
||
deleteAll: string;
|
||
/** 漫游点前缀 */
|
||
point: string;
|
||
/** 播放按钮 */
|
||
play: string;
|
||
/** 无漫游点提示 */
|
||
noPoints: string;
|
||
};
|
||
```
|
||
|
||
**zh-CN.ts 修改内容**:
|
||
|
||
```typescript
|
||
path: {
|
||
dialogTitle: '路径漫游',
|
||
duration: '漫游时间',
|
||
durationUnit: '秒',
|
||
loop: '循环播放',
|
||
addPoint: '添加漫游点',
|
||
deleteAll: '删除全部',
|
||
point: '漫游点',
|
||
play: '播放漫游',
|
||
noPoints: '暂无漫游点,请添加'
|
||
}
|
||
```
|
||
|
||
**en-US.ts 修改内容**:
|
||
|
||
```typescript
|
||
path: {
|
||
dialogTitle: 'Path Roaming',
|
||
duration: 'Duration',
|
||
durationUnit: 's',
|
||
loop: 'Loop',
|
||
addPoint: 'Add Point',
|
||
deleteAll: 'Delete All',
|
||
point: 'Point',
|
||
play: 'Play',
|
||
noPoints: 'No points yet'
|
||
}
|
||
```
|
||
|
||
**验收标准**:
|
||
- [x] 三个文件都已修改
|
||
- [x] 构建无错误
|
||
|
||
---
|
||
|
||
### 任务 2: 创建类型定义
|
||
|
||
**新建文件**: `src/components/walk-path-panel/types.ts`
|
||
|
||
```typescript
|
||
/**
|
||
* 漫游点接口
|
||
* 表示路径中的一个漫游点
|
||
*/
|
||
export interface RoamingPoint {
|
||
/** 漫游点索引 */
|
||
index: number;
|
||
}
|
||
|
||
/**
|
||
* 播放选项接口
|
||
* 配置漫游播放的参数
|
||
*/
|
||
export interface PlayOptions {
|
||
/** 总播放时长(毫秒),不包括停留时间 */
|
||
duration?: number;
|
||
/** 是否循环播放 */
|
||
loop?: boolean;
|
||
/** 播放完成的回调 */
|
||
onComplete?: () => void;
|
||
/** 每个点播放完成的回调,用于高亮当前点 */
|
||
onPointComplete?: (pointIndex: number) => void;
|
||
}
|
||
```
|
||
|
||
**验收标准**:
|
||
- [x] 文件已创建
|
||
- [x] 包含所有接口定义和注释
|
||
|
||
---
|
||
|
||
### 任务 3: Engine 添加 pathRoaming 方法
|
||
|
||
**修改文件**: `src/components/engine/index.ts`
|
||
|
||
**添加位置**: 在 `getEngineInfo()` 方法后面添加以下代码
|
||
|
||
```typescript
|
||
// ==================== 路径漫游 ====================
|
||
|
||
/**
|
||
* 添加漫游点
|
||
* 将当前相机位置添加为一个漫游点
|
||
*/
|
||
public pathRoamingAddPoint(): void {
|
||
// 检查引擎是否已初始化
|
||
if (!this._isInitialized || !this.engine?.pathRoaming) {
|
||
console.warn('[Engine] pathRoaming not available');
|
||
return;
|
||
}
|
||
// 调用底层 API 添加点位
|
||
this.engine.pathRoaming.addPoint(0);
|
||
}
|
||
|
||
/**
|
||
* 删除指定索引的漫游点
|
||
* @param index 要删除的漫游点索引
|
||
*/
|
||
public pathRoamingRemovePoint(index: number): void {
|
||
if (!this._isInitialized || !this.engine?.pathRoaming) {
|
||
console.warn('[Engine] pathRoaming not available');
|
||
return;
|
||
}
|
||
this.engine.pathRoaming.removePoint(index);
|
||
}
|
||
|
||
/**
|
||
* 清除所有漫游点
|
||
*/
|
||
public pathRoamingClearPoints(): void {
|
||
if (!this._isInitialized || !this.engine?.pathRoaming) {
|
||
console.warn('[Engine] pathRoaming not available');
|
||
return;
|
||
}
|
||
this.engine.pathRoaming.clearPoints();
|
||
}
|
||
|
||
/**
|
||
* 获取所有漫游点
|
||
* @returns 漫游点数组
|
||
*/
|
||
public pathRoamingGetPoints(): any[] {
|
||
if (!this._isInitialized || !this.engine?.pathRoaming) {
|
||
console.warn('[Engine] pathRoaming not available');
|
||
return [];
|
||
}
|
||
return this.engine.pathRoaming.getPoints() ?? [];
|
||
}
|
||
|
||
/**
|
||
* 跳转到指定漫游点
|
||
* @param index 目标漫游点索引
|
||
*/
|
||
public pathRoamingJumpToPoint(index: number): void {
|
||
if (!this._isInitialized || !this.engine?.pathRoaming) {
|
||
console.warn('[Engine] pathRoaming not available');
|
||
return;
|
||
}
|
||
this.engine.pathRoaming.jumpToPoint(index);
|
||
}
|
||
|
||
/**
|
||
* 播放漫游
|
||
* @param options 播放选项,包含时长、循环、回调等配置
|
||
*/
|
||
public pathRoamingPlay(options?: {
|
||
duration?: number;
|
||
loop?: boolean;
|
||
onComplete?: () => void;
|
||
onPointComplete?: (pointIndex: number) => void;
|
||
}): void {
|
||
if (!this._isInitialized || !this.engine?.pathRoaming) {
|
||
console.warn('[Engine] pathRoaming not available');
|
||
return;
|
||
}
|
||
this.engine.pathRoaming.play(options);
|
||
}
|
||
|
||
// ==================== 结束:路径漫游 ====================
|
||
```
|
||
|
||
**验收标准**:
|
||
- [x] 6个方法已添加
|
||
- [x] 每个方法都有 JSDoc 注释
|
||
- [x] 构建无错误
|
||
|
||
---
|
||
|
||
### 任务 4: EngineManager 添加代理方法
|
||
|
||
**修改文件**: `src/managers/engine-manager.ts`
|
||
|
||
**添加位置**: 在 `getEngineInfo()` 方法后面添加以下代码
|
||
|
||
```typescript
|
||
// ==================== 路径漫游 ====================
|
||
|
||
/**
|
||
* 添加漫游点
|
||
* 将当前相机位置添加为一个漫游点
|
||
*/
|
||
public pathRoamingAddPoint(): void {
|
||
this.engineInstance?.pathRoamingAddPoint();
|
||
}
|
||
|
||
/**
|
||
* 删除指定索引的漫游点
|
||
* @param index 要删除的漫游点索引
|
||
*/
|
||
public pathRoamingRemovePoint(index: number): void {
|
||
this.engineInstance?.pathRoamingRemovePoint(index);
|
||
}
|
||
|
||
/**
|
||
* 清除所有漫游点
|
||
*/
|
||
public pathRoamingClearPoints(): void {
|
||
this.engineInstance?.pathRoamingClearPoints();
|
||
}
|
||
|
||
/**
|
||
* 获取所有漫游点
|
||
* @returns 漫游点数组
|
||
*/
|
||
public pathRoamingGetPoints(): any[] {
|
||
return this.engineInstance?.pathRoamingGetPoints() ?? [];
|
||
}
|
||
|
||
/**
|
||
* 跳转到指定漫游点
|
||
* @param index 目标漫游点索引
|
||
*/
|
||
public pathRoamingJumpToPoint(index: number): void {
|
||
this.engineInstance?.pathRoamingJumpToPoint(index);
|
||
}
|
||
|
||
/**
|
||
* 播放漫游
|
||
* @param options 播放选项,包含时长、循环、回调等配置
|
||
*/
|
||
public pathRoamingPlay(options?: {
|
||
duration?: number;
|
||
loop?: boolean;
|
||
onComplete?: () => void;
|
||
onPointComplete?: (pointIndex: number) => void;
|
||
}): void {
|
||
this.engineInstance?.pathRoamingPlay(options);
|
||
}
|
||
|
||
// ==================== 结束:路径漫游 ====================
|
||
```
|
||
|
||
**验收标准**:
|
||
- [x] 6个代理方法已添加
|
||
- [x] 每个方法都有 JSDoc 注释
|
||
- [x] 构建无错误
|
||
|
||
---
|
||
|
||
### 任务 5: 实现 WalkPathPanel 组件
|
||
|
||
**修改文件**: `src/components/walk-path-panel/index.ts`
|
||
|
||
**完整替换为以下代码**:
|
||
|
||
```typescript
|
||
import './index.css';
|
||
import type { ThemeConfig } from '../../themes/types';
|
||
import { IBimComponent } from '../../types/component';
|
||
import { localeManager, t } from '../../services/locale';
|
||
import { themeManager } from '../../services/theme';
|
||
import { ManagerRegistry } from '../../core/manager-registry';
|
||
import type { RoamingPoint } from './types';
|
||
|
||
/**
|
||
* 路径漫游面板组件
|
||
* 提供漫游点的添加、删除、跳转和播放功能
|
||
*/
|
||
export class WalkPathPanel implements IBimComponent {
|
||
public element!: HTMLElement;
|
||
|
||
/** 管理器注册表实例 */
|
||
private registry = ManagerRegistry.getInstance();
|
||
/** 国际化订阅取消函数 */
|
||
private unsubscribeLocale: (() => void) | null = null;
|
||
/** 主题订阅取消函数 */
|
||
private unsubscribeTheme: (() => void) | null = null;
|
||
|
||
/** 漫游点列表 */
|
||
private points: RoamingPoint[] = [];
|
||
/** 漫游时间(毫秒) */
|
||
private duration: number = 10000;
|
||
/** 是否循环播放 */
|
||
private loop: boolean = false;
|
||
/** 当前播放中的点索引,-1 表示未播放 */
|
||
private playingPointIndex: number = -1;
|
||
/** 是否正在播放 */
|
||
private isPlaying: boolean = false;
|
||
|
||
/**
|
||
* 初始化组件
|
||
* 创建 DOM 元素,订阅国际化和主题变化,加载已有漫游点
|
||
*/
|
||
public init(): void {
|
||
// 创建根元素
|
||
this.element = document.createElement('div');
|
||
this.element.className = 'walk-path-panel';
|
||
|
||
// 订阅国际化变化
|
||
this.unsubscribeLocale = localeManager.subscribe(() => this.render());
|
||
// 订阅主题变化
|
||
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
|
||
|
||
// 从引擎加载已有的漫游点
|
||
this.loadPointsFromEngine();
|
||
|
||
// 渲染界面
|
||
this.render();
|
||
// 应用当前主题
|
||
this.setTheme(themeManager.getTheme());
|
||
}
|
||
|
||
/**
|
||
* 从引擎加载已有的漫游点
|
||
* 在面板打开时调用,获取底层引擎中已存在的漫游点
|
||
*/
|
||
private loadPointsFromEngine(): void {
|
||
const enginePoints = this.registry.engine3d?.pathRoamingGetPoints() ?? [];
|
||
// 将引擎返回的点转换为本地格式
|
||
this.points = enginePoints.map((_: any, index: number) => ({
|
||
index: index
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 渲染整个面板
|
||
* 清空现有内容并重新构建界面
|
||
*/
|
||
private render(): void {
|
||
this.element.innerHTML = '';
|
||
|
||
// 渲染路径设置区域
|
||
const settings = this.createSettingsSection();
|
||
this.element.appendChild(settings);
|
||
|
||
// 渲染漫游点区域
|
||
const pointsSection = this.createPointsSection();
|
||
this.element.appendChild(pointsSection);
|
||
|
||
// 渲染播放按钮
|
||
const playBtn = this.createPlayButton();
|
||
this.element.appendChild(playBtn);
|
||
}
|
||
|
||
/**
|
||
* 创建路径设置区域
|
||
* 包含漫游时间和循环播放设置
|
||
*/
|
||
private createSettingsSection(): HTMLElement {
|
||
const section = document.createElement('div');
|
||
section.className = 'walk-path-settings';
|
||
|
||
// ===== 漫游时间 =====
|
||
const durationGroup = document.createElement('div');
|
||
durationGroup.className = 'walk-path-form-group';
|
||
|
||
const durationLabel = document.createElement('label');
|
||
durationLabel.textContent = t('walkControl.path.duration');
|
||
|
||
const durationWrapper = document.createElement('div');
|
||
durationWrapper.className = 'walk-path-input-wrapper';
|
||
|
||
const durationInput = document.createElement('input');
|
||
durationInput.type = 'number';
|
||
durationInput.className = 'walk-path-input';
|
||
durationInput.value = String(this.duration / 1000);
|
||
durationInput.min = '1';
|
||
durationInput.oninput = (e) => {
|
||
// 更新漫游时间,转换为毫秒
|
||
const val = parseInt((e.target as HTMLInputElement).value) || 1;
|
||
this.duration = val * 1000;
|
||
};
|
||
|
||
const durationUnit = document.createElement('span');
|
||
durationUnit.className = 'walk-path-unit';
|
||
durationUnit.textContent = t('walkControl.path.durationUnit');
|
||
|
||
durationWrapper.appendChild(durationInput);
|
||
durationWrapper.appendChild(durationUnit);
|
||
durationGroup.appendChild(durationLabel);
|
||
durationGroup.appendChild(durationWrapper);
|
||
|
||
// ===== 循环播放 =====
|
||
const loopGroup = document.createElement('div');
|
||
loopGroup.className = 'walk-path-form-group walk-path-form-group-inline';
|
||
|
||
const loopCheckbox = document.createElement('input');
|
||
loopCheckbox.type = 'checkbox';
|
||
loopCheckbox.id = 'walk-path-loop-checkbox';
|
||
loopCheckbox.className = 'walk-path-checkbox';
|
||
loopCheckbox.checked = this.loop;
|
||
loopCheckbox.onchange = (e) => {
|
||
// 更新循环播放状态
|
||
this.loop = (e.target as HTMLInputElement).checked;
|
||
};
|
||
|
||
const loopLabel = document.createElement('label');
|
||
loopLabel.htmlFor = 'walk-path-loop-checkbox';
|
||
loopLabel.textContent = t('walkControl.path.loop');
|
||
|
||
loopGroup.appendChild(loopCheckbox);
|
||
loopGroup.appendChild(loopLabel);
|
||
|
||
section.appendChild(durationGroup);
|
||
section.appendChild(loopGroup);
|
||
|
||
return section;
|
||
}
|
||
|
||
/**
|
||
* 创建漫游点区域
|
||
* 包含操作按钮和漫游点列表
|
||
*/
|
||
private createPointsSection(): HTMLElement {
|
||
const section = document.createElement('div');
|
||
section.className = 'walk-path-points-section';
|
||
|
||
// ===== 操作栏 =====
|
||
const toolbar = document.createElement('div');
|
||
toolbar.className = 'walk-path-points-toolbar';
|
||
|
||
// 添加漫游点按钮
|
||
const addBtn = document.createElement('button');
|
||
addBtn.className = 'walk-path-btn walk-path-btn-small';
|
||
addBtn.textContent = `+ ${t('walkControl.path.addPoint')}`;
|
||
addBtn.onclick = () => this.addPoint();
|
||
|
||
// 删除全部按钮
|
||
const deleteAllBtn = document.createElement('button');
|
||
deleteAllBtn.className = 'walk-path-btn walk-path-btn-small walk-path-btn-danger';
|
||
deleteAllBtn.textContent = t('walkControl.path.deleteAll');
|
||
deleteAllBtn.onclick = () => this.deleteAllPoints();
|
||
|
||
toolbar.appendChild(addBtn);
|
||
toolbar.appendChild(deleteAllBtn);
|
||
section.appendChild(toolbar);
|
||
|
||
// ===== 漫游点列表 =====
|
||
const list = document.createElement('div');
|
||
list.className = 'walk-path-points-list';
|
||
|
||
if (this.points.length === 0) {
|
||
// 空状态提示
|
||
const empty = document.createElement('div');
|
||
empty.className = 'walk-path-empty';
|
||
empty.textContent = t('walkControl.path.noPoints');
|
||
list.appendChild(empty);
|
||
} else {
|
||
// 渲染每个漫游点
|
||
this.points.forEach((_, index) => {
|
||
const item = this.createPointItem(index);
|
||
list.appendChild(item);
|
||
});
|
||
}
|
||
|
||
section.appendChild(list);
|
||
return section;
|
||
}
|
||
|
||
/**
|
||
* 创建单个漫游点项
|
||
* @param index 漫游点索引
|
||
*/
|
||
private createPointItem(index: number): HTMLElement {
|
||
const item = document.createElement('div');
|
||
item.className = 'walk-path-point-item';
|
||
// 如果是当前播放的点,添加高亮样式
|
||
if (this.playingPointIndex === index) {
|
||
item.classList.add('walk-path-point-item-active');
|
||
}
|
||
|
||
// 漫游点名称
|
||
const name = document.createElement('span');
|
||
name.className = 'walk-path-point-name';
|
||
name.textContent = `${t('walkControl.path.point')}${index}`;
|
||
|
||
// 操作按钮容器
|
||
const actions = document.createElement('div');
|
||
actions.className = 'walk-path-point-actions';
|
||
|
||
// 跳转按钮
|
||
const jumpBtn = document.createElement('button');
|
||
jumpBtn.className = 'walk-path-btn-icon';
|
||
jumpBtn.innerHTML = '▶';
|
||
jumpBtn.title = t('walkControl.path.play');
|
||
jumpBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
this.jumpToPoint(index);
|
||
};
|
||
|
||
// 删除按钮
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'walk-path-btn-icon walk-path-btn-icon-danger';
|
||
deleteBtn.innerHTML = '×';
|
||
deleteBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
this.deletePoint(index);
|
||
};
|
||
|
||
actions.appendChild(jumpBtn);
|
||
actions.appendChild(deleteBtn);
|
||
|
||
item.appendChild(name);
|
||
item.appendChild(actions);
|
||
return item;
|
||
}
|
||
|
||
/**
|
||
* 创建播放按钮
|
||
*/
|
||
private createPlayButton(): HTMLElement {
|
||
const playBtn = document.createElement('button');
|
||
playBtn.className = 'walk-path-btn walk-path-btn-play';
|
||
playBtn.textContent = `▶ ${t('walkControl.path.play')}`;
|
||
// 如果没有漫游点,禁用按钮
|
||
playBtn.disabled = this.points.length === 0;
|
||
playBtn.onclick = () => this.playPath();
|
||
return playBtn;
|
||
}
|
||
|
||
// ==================== 操作方法 ====================
|
||
|
||
/**
|
||
* 添加漫游点
|
||
* 将当前相机位置添加为新的漫游点
|
||
*/
|
||
private addPoint(): void {
|
||
// 调用引擎添加漫游点
|
||
this.registry.engine3d?.pathRoamingAddPoint();
|
||
// 更新本地状态
|
||
const newIndex = this.points.length;
|
||
this.points.push({ index: newIndex });
|
||
// 重新渲染
|
||
this.render();
|
||
}
|
||
|
||
/**
|
||
* 删除指定漫游点
|
||
* @param index 要删除的漫游点索引
|
||
*/
|
||
private deletePoint(index: number): void {
|
||
// 调用引擎删除漫游点
|
||
this.registry.engine3d?.pathRoamingRemovePoint(index);
|
||
// 从本地列表中移除
|
||
this.points.splice(index, 1);
|
||
// 重新索引
|
||
this.points.forEach((p, i) => p.index = i);
|
||
// 重新渲染
|
||
this.render();
|
||
}
|
||
|
||
/**
|
||
* 删除所有漫游点
|
||
*/
|
||
private deleteAllPoints(): void {
|
||
// 调用引擎清除所有漫游点
|
||
this.registry.engine3d?.pathRoamingClearPoints();
|
||
// 清空本地列表
|
||
this.points = [];
|
||
// 重新渲染
|
||
this.render();
|
||
}
|
||
|
||
/**
|
||
* 跳转到指定漫游点
|
||
* @param index 目标漫游点索引
|
||
*/
|
||
private jumpToPoint(index: number): void {
|
||
this.registry.engine3d?.pathRoamingJumpToPoint(index);
|
||
}
|
||
|
||
/**
|
||
* 播放漫游
|
||
* 按顺序播放所有漫游点
|
||
*/
|
||
private playPath(): void {
|
||
// 检查是否有漫游点
|
||
if (this.points.length === 0) return;
|
||
|
||
// 标记开始播放
|
||
this.isPlaying = true;
|
||
|
||
// 调用引擎播放
|
||
this.registry.engine3d?.pathRoamingPlay({
|
||
duration: this.duration,
|
||
loop: this.loop,
|
||
onPointComplete: (pointIndex: number) => {
|
||
// 更新当前播放点索引
|
||
this.playingPointIndex = pointIndex;
|
||
// 重新渲染以更新高亮状态
|
||
this.render();
|
||
},
|
||
onComplete: () => {
|
||
// 播放完成,重置状态
|
||
this.isPlaying = false;
|
||
this.playingPointIndex = -1;
|
||
// 重新渲染以移除高亮
|
||
this.render();
|
||
}
|
||
});
|
||
}
|
||
|
||
// ==================== 生命周期 ====================
|
||
|
||
/**
|
||
* 应用主题
|
||
* @param theme 主题配置
|
||
*/
|
||
public setTheme(theme: ThemeConfig): void {
|
||
if (!this.element) return;
|
||
// 设置 CSS 变量
|
||
this.element.style.setProperty('--bim-text-primary', theme.textPrimary ?? '#fff');
|
||
this.element.style.setProperty('--bim-text-secondary', theme.textSecondary ?? '#94a3b8');
|
||
this.element.style.setProperty('--bim-bg-elevated', theme.bgElevated ?? '#1f2d3e');
|
||
this.element.style.setProperty('--bim-border-default', theme.borderDefault ?? '#334155');
|
||
this.element.style.setProperty('--bim-primary', theme.primary ?? '#3b82f6');
|
||
}
|
||
|
||
/**
|
||
* 销毁组件
|
||
* 清理订阅和 DOM 元素
|
||
*/
|
||
public destroy(): void {
|
||
// 取消订阅
|
||
this.unsubscribeLocale?.();
|
||
this.unsubscribeTheme?.();
|
||
// 移除 DOM 元素
|
||
if (this.element?.parentElement) {
|
||
this.element.parentElement.removeChild(this.element);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**验收标准**:
|
||
- [x] 面板显示正确
|
||
- [x] 打开时加载已有漫游点
|
||
- [x] 添加/删除漫游点功能正常
|
||
- [x] 播放时高亮当前点
|
||
- [x] 每个方法都有注释
|
||
|
||
---
|
||
|
||
### 任务 6: 添加样式文件
|
||
|
||
**新建文件**: `src/components/walk-path-panel/index.css`
|
||
|
||
```css
|
||
/* 路径漫游面板根容器 */
|
||
.walk-path-panel {
|
||
padding: 16px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
height: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
/* ==================== 按钮样式 ==================== */
|
||
|
||
/* 按钮通用样式 */
|
||
.walk-path-btn {
|
||
padding: 10px 16px;
|
||
border: none;
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
/* 播放按钮 */
|
||
.walk-path-btn-play {
|
||
background: var(--bim-primary, #3b82f6);
|
||
color: #fff;
|
||
width: 100%;
|
||
}
|
||
|
||
.walk-path-btn-play:hover:not(:disabled) {
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.walk-path-btn-play:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
/* 小按钮 */
|
||
.walk-path-btn-small {
|
||
padding: 6px 12px;
|
||
font-size: 12px;
|
||
background: var(--bim-bg-elevated, #1f2d3e);
|
||
color: var(--bim-text-primary, #fff);
|
||
border: 1px solid var(--bim-border-default, #334155);
|
||
}
|
||
|
||
.walk-path-btn-small:hover {
|
||
background: var(--bim-border-default, #334155);
|
||
}
|
||
|
||
/* 危险按钮(删除) */
|
||
.walk-path-btn-danger {
|
||
color: #ef4444;
|
||
}
|
||
|
||
/* ==================== 表单样式 ==================== */
|
||
|
||
/* 设置区域 */
|
||
.walk-path-settings {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
/* 表单组 */
|
||
.walk-path-form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6px;
|
||
}
|
||
|
||
.walk-path-form-group label {
|
||
font-size: 12px;
|
||
color: var(--bim-text-secondary, #94a3b8);
|
||
}
|
||
|
||
/* 行内表单组(复选框) */
|
||
.walk-path-form-group-inline {
|
||
flex-direction: row;
|
||
align-items: center;
|
||
}
|
||
|
||
/* 输入框 */
|
||
.walk-path-input {
|
||
padding: 8px 12px;
|
||
border: 1px solid var(--bim-border-default, #334155);
|
||
border-radius: 4px;
|
||
background: var(--bim-bg-elevated, #1f2d3e);
|
||
color: var(--bim-text-primary, #fff);
|
||
font-size: 14px;
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.walk-path-input:focus {
|
||
outline: none;
|
||
border-color: var(--bim-primary, #3b82f6);
|
||
}
|
||
|
||
/* 输入框包装器(带单位) */
|
||
.walk-path-input-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.walk-path-input-wrapper .walk-path-input {
|
||
flex: 1;
|
||
}
|
||
|
||
/* 单位文本 */
|
||
.walk-path-unit {
|
||
color: var(--bim-text-secondary, #94a3b8);
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* 复选框 */
|
||
.walk-path-checkbox {
|
||
width: 16px;
|
||
height: 16px;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
/* ==================== 漫游点区域 ==================== */
|
||
|
||
/* 漫游点区域容器 */
|
||
.walk-path-points-section {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
min-height: 0;
|
||
}
|
||
|
||
/* 操作栏 */
|
||
.walk-path-points-toolbar {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 漫游点列表 */
|
||
.walk-path-points-list {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 空状态提示 */
|
||
.walk-path-empty {
|
||
text-align: center;
|
||
padding: 32px 16px;
|
||
color: var(--bim-text-secondary, #94a3b8);
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* ==================== 漫游点项 ==================== */
|
||
|
||
/* 漫游点项容器 */
|
||
.walk-path-point-item {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 10px 12px;
|
||
background: var(--bim-bg-elevated, #1f2d3e);
|
||
border-radius: 4px;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.walk-path-point-item:hover {
|
||
background: var(--bim-border-default, #334155);
|
||
}
|
||
|
||
/* 悬浮时显示操作按钮 */
|
||
.walk-path-point-item:hover .walk-path-point-actions {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 播放中的点高亮样式 */
|
||
.walk-path-point-item-active {
|
||
background: var(--bim-primary, #3b82f6);
|
||
}
|
||
|
||
.walk-path-point-item-active .walk-path-point-name {
|
||
color: #fff;
|
||
}
|
||
|
||
.walk-path-point-item-active .walk-path-point-actions {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 漫游点名称 */
|
||
.walk-path-point-name {
|
||
font-size: 14px;
|
||
color: var(--bim-text-primary, #fff);
|
||
}
|
||
|
||
/* 操作按钮容器 */
|
||
.walk-path-point-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
/* 图标按钮 */
|
||
.walk-path-btn-icon {
|
||
width: 28px;
|
||
height: 28px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
background: transparent;
|
||
color: var(--bim-text-primary, #fff);
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.walk-path-btn-icon:hover {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
/* 危险图标按钮 */
|
||
.walk-path-btn-icon-danger:hover {
|
||
background: rgba(239, 68, 68, 0.2);
|
||
color: #ef4444;
|
||
}
|
||
```
|
||
|
||
**验收标准**:
|
||
- [x] 样式文件已创建
|
||
- [x] 使用 CSS 变量适配主题
|
||
- [x] 播放中的点有高亮样式
|
||
- [x] 悬浮效果正常
|
||
- [x] 每个样式块都有注释
|
||
|
||
---
|
||
|
||
### 任务 7: 构建验证
|
||
|
||
**执行命令**:
|
||
```bash
|
||
npm run build
|
||
```
|
||
|
||
**验收标准**:
|
||
- [x] 构建成功
|
||
- [x] 无 TypeScript 错误
|
||
|
||
---
|
||
|
||
## 文件修改清单
|
||
|
||
| 文件 | 操作 | 说明 |
|
||
|------|------|------|
|
||
| `src/locales/types.ts` | 修改 | 添加 path 字段类型 |
|
||
| `src/locales/zh-CN.ts` | 修改 | 添加中文翻译 |
|
||
| `src/locales/en-US.ts` | 修改 | 添加英文翻译 |
|
||
| `src/components/walk-path-panel/types.ts` | 新建 | 类型定义 |
|
||
| `src/components/engine/index.ts` | 修改 | 添加 6 个 pathRoaming 方法 |
|
||
| `src/managers/engine-manager.ts` | 修改 | 添加 6 个代理方法 |
|
||
| `src/components/walk-path-panel/index.ts` | 重写 | 完整面板实现 |
|
||
| `src/components/walk-path-panel/index.css` | 新建 | 样式文件 |
|
||
|
||
---
|
||
|
||
## 提交策略
|
||
|
||
| 任务 | 提交信息 |
|
||
|------|----------|
|
||
| 1 | `feat(walk-path): add locale strings` |
|
||
| 2 | `feat(walk-path): add types` |
|
||
| 3-4 | `feat(engine): add pathRoaming methods` |
|
||
| 5-6 | `feat(walk-path): implement panel with highlight` |
|