28 KiB
28 KiB
路径漫游功能开发
概述
目标: 实现路径漫游功能,支持单条路径的漫游点管理和播放
交付物:
- 漫游点管理(添加、删除、跳转)
- 路径设置(漫游时间、循环播放)
- 漫游播放(播放时高亮当前点)
预估工作量: 中等 执行方式: 顺序执行
原型交互图
┌─────────────────────────────────────┐
│ 路径漫游 [×] │
├─────────────────────────────────────┤
│ │
│ ─────────── 路径设置 ─────────── │
│ │
│ 漫游时间 │
│ ┌───────────────────────┐ │
│ │ 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.tssrc/locales/zh-CN.tssrc/locales/en-US.ts
types.ts 修改内容 (替换 walkControl.path 部分):
path: {
/** 对话框标题 */
dialogTitle: string;
/** 漫游时间标签 */
duration: string;
/** 时间单位 */
durationUnit: string;
/** 循环播放 */
loop: string;
/** 添加漫游点按钮 */
addPoint: string;
/** 删除全部按钮 */
deleteAll: string;
/** 漫游点前缀 */
point: string;
/** 播放按钮 */
play: string;
/** 无漫游点提示 */
noPoints: string;
};
zh-CN.ts 修改内容:
path: {
dialogTitle: '路径漫游',
duration: '漫游时间',
durationUnit: '秒',
loop: '循环播放',
addPoint: '添加漫游点',
deleteAll: '删除全部',
point: '漫游点',
play: '播放漫游',
noPoints: '暂无漫游点,请添加'
}
en-US.ts 修改内容:
path: {
dialogTitle: 'Path Roaming',
duration: 'Duration',
durationUnit: 's',
loop: 'Loop',
addPoint: 'Add Point',
deleteAll: 'Delete All',
point: 'Point',
play: 'Play',
noPoints: 'No points yet'
}
验收标准:
- 三个文件都已修改
- 构建无错误
任务 2: 创建类型定义
新建文件: src/components/walk-path-panel/types.ts
/**
* 漫游点接口
* 表示路径中的一个漫游点
*/
export interface RoamingPoint {
/** 漫游点索引 */
index: number;
}
/**
* 播放选项接口
* 配置漫游播放的参数
*/
export interface PlayOptions {
/** 总播放时长(毫秒),不包括停留时间 */
duration?: number;
/** 是否循环播放 */
loop?: boolean;
/** 播放完成的回调 */
onComplete?: () => void;
/** 每个点播放完成的回调,用于高亮当前点 */
onPointComplete?: (pointIndex: number) => void;
}
验收标准:
- 文件已创建
- 包含所有接口定义和注释
任务 3: Engine 添加 pathRoaming 方法
修改文件: src/components/engine/index.ts
添加位置: 在 getEngineInfo() 方法后面添加以下代码
// ==================== 路径漫游 ====================
/**
* 添加漫游点
* 将当前相机位置添加为一个漫游点
*/
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);
}
// ==================== 结束:路径漫游 ====================
验收标准:
- 6个方法已添加
- 每个方法都有 JSDoc 注释
- 构建无错误
任务 4: EngineManager 添加代理方法
修改文件: src/managers/engine-manager.ts
添加位置: 在 getEngineInfo() 方法后面添加以下代码
// ==================== 路径漫游 ====================
/**
* 添加漫游点
* 将当前相机位置添加为一个漫游点
*/
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);
}
// ==================== 结束:路径漫游 ====================
验收标准:
- 6个代理方法已添加
- 每个方法都有 JSDoc 注释
- 构建无错误
任务 5: 实现 WalkPathPanel 组件
修改文件: src/components/walk-path-panel/index.ts
完整替换为以下代码:
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);
}
}
}
验收标准:
- 面板显示正确
- 打开时加载已有漫游点
- 添加/删除漫游点功能正常
- 播放时高亮当前点
- 每个方法都有注释
任务 6: 添加样式文件
新建文件: src/components/walk-path-panel/index.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;
}
验收标准:
- 样式文件已创建
- 使用 CSS 变量适配主题
- 播放中的点有高亮样式
- 悬浮效果正常
- 每个样式块都有注释
任务 7: 构建验证
执行命令:
npm run build
验收标准:
- 构建成功
- 无 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 |