542 lines
12 KiB
Markdown
542 lines
12 KiB
Markdown
|
|
# Learnings: 构件详情右键菜单功能
|
|||
|
|
|
|||
|
|
## 2026-01-28 - Implementation Complete
|
|||
|
|
|
|||
|
|
### Architecture Patterns
|
|||
|
|
|
|||
|
|
#### 1. Event-Driven Component Selection
|
|||
|
|
**Pattern**: 底层引擎触发事件 → SDK 监听并维护状态
|
|||
|
|
```typescript
|
|||
|
|
// SDK 层监听底层事件
|
|||
|
|
this.engine.events.on('click', (hit: any) => {
|
|||
|
|
if (hit && hit.object) {
|
|||
|
|
this.selectedComponent = { url: hit.object.url, id: hit.object.name };
|
|||
|
|
} else {
|
|||
|
|
this.selectedComponent = null;
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Insight**: SDK 不需要主动查询状态,而是被动监听底层事件,降低耦合度。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 2. Dynamic Menu Generation
|
|||
|
|
**Pattern**: 注册处理器返回函数,运行时动态生成菜单
|
|||
|
|
```typescript
|
|||
|
|
// 注册时传入函数,而非静态配置
|
|||
|
|
rightKey.registerHandler((_e) => {
|
|||
|
|
const selected = this.getSelectedComponent();
|
|||
|
|
const items: MenuItemConfig[] = [];
|
|||
|
|
|
|||
|
|
// 根据当前状态动态构建菜单
|
|||
|
|
if (selected) {
|
|||
|
|
items.push({ /* 构件详情 */ });
|
|||
|
|
}
|
|||
|
|
items.push({ /* 显示全部 */ });
|
|||
|
|
|
|||
|
|
return items;
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Insight**: 右键菜单内容不是静态的,而是根据运行时状态(选中/未选中)动态生成,提供上下文感知的用户体验。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 3. Manager Registry Pattern
|
|||
|
|
**Pattern**: 单例注册表 + 代理方法
|
|||
|
|
```typescript
|
|||
|
|
// Registry 存储所有 Manager 实例
|
|||
|
|
registry.engine3d = this.engine;
|
|||
|
|
registry.componentDetail = this.componentDetail;
|
|||
|
|
|
|||
|
|
// 其他 Manager 通过 Registry 访问
|
|||
|
|
const selected = registry.engine3d?.getSelectedComponent();
|
|||
|
|
registry.componentDetail?.show(selected.url, selected.id);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Insight**:
|
|||
|
|
- 避免 Manager 之间直接引用,降低耦合
|
|||
|
|
- 统一访问入口,便于管理和调试
|
|||
|
|
- 支持延迟初始化(nullable 类型)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 4. Data Transformation Layer
|
|||
|
|
**Pattern**: 底层数据 → SDK 转换 → UI 展示
|
|||
|
|
```typescript
|
|||
|
|
// 底层返回格式
|
|||
|
|
{
|
|||
|
|
properties: [{
|
|||
|
|
name: "分类名",
|
|||
|
|
children: [{ name: "属性名", value: "值" }]
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// SDK 转换为 UI 组件格式
|
|||
|
|
const categories = data.properties.map(cat => ({
|
|||
|
|
categoryName: cat.name,
|
|||
|
|
items: cat.children.map(child => ({
|
|||
|
|
key: child.name,
|
|||
|
|
value: child.value
|
|||
|
|
}))
|
|||
|
|
}));
|
|||
|
|
|
|||
|
|
// UI 组件渲染
|
|||
|
|
BimCollapse({ items: categories })
|
|||
|
|
→ BimDescription({ items: category.items })
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Key Insight**: SDK 作为中间层,负责数据格式转换,使 UI 组件专注于展示逻辑。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Code Conventions
|
|||
|
|
|
|||
|
|
#### 1. Console Logging
|
|||
|
|
```typescript
|
|||
|
|
// 中文日志 + Manager 前缀
|
|||
|
|
console.log('[Engine] 构件选中:', this.selectedComponent);
|
|||
|
|
console.log('[ComponentDetailManager] 显示构件详情');
|
|||
|
|
console.error('[EngineManager] Engine 尚未初始化');
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Reason**:
|
|||
|
|
- 中文便于业务人员理解
|
|||
|
|
- 前缀便于定位代码位置
|
|||
|
|
- 区分 log/warn/error 级别
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 2. Section Markers
|
|||
|
|
```typescript
|
|||
|
|
// ==================== 选中状态管理 ====================
|
|||
|
|
|
|||
|
|
private selectedComponent: { url: string; id: string } | null = null;
|
|||
|
|
|
|||
|
|
public getSelectedComponent(): { url: string; id: string } | null {
|
|||
|
|
return this.selectedComponent;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Reason**: 大文件中用分隔符标记功能区域,提升可读性。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 3. JSDoc Comments
|
|||
|
|
```typescript
|
|||
|
|
/**
|
|||
|
|
* 获取当前选中的构件信息
|
|||
|
|
* @returns 选中的构件信息,包含 url 和 id;未选中时返回 null
|
|||
|
|
*/
|
|||
|
|
public getSelectedComponent(): { url: string; id: string } | null {
|
|||
|
|
return this.selectedComponent;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Reason**:
|
|||
|
|
- 只对 public 方法添加 JSDoc
|
|||
|
|
- 描述功能、参数、返回值
|
|||
|
|
- 符合现有代码风格
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### TypeScript Patterns
|
|||
|
|
|
|||
|
|
#### 1. Optional Chaining + Nullish Coalescing
|
|||
|
|
```typescript
|
|||
|
|
// 安全访问 + 默认值
|
|||
|
|
return this.engineInstance?.getSelectedComponent() ?? null;
|
|||
|
|
|
|||
|
|
// 条件调用
|
|||
|
|
registry.componentDetail?.show(url, id);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Reason**:
|
|||
|
|
- Manager 可能未初始化
|
|||
|
|
- 优雅处理 null/undefined
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 2. Type-Safe Event Callbacks
|
|||
|
|
```typescript
|
|||
|
|
// 明确回调参数类型
|
|||
|
|
public getComponentProperties(
|
|||
|
|
url: string,
|
|||
|
|
id: string,
|
|||
|
|
callback: (data: any) => void // 底层未定义类型,先用 any
|
|||
|
|
): void {
|
|||
|
|
this.engine.modelProperties.getModelProperties(url, id, callback);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Trade-off**: 底层 API 未提供 TypeScript 类型定义,暂用 `any`,后续可补充。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### i18n Patterns
|
|||
|
|
|
|||
|
|
#### 1. Structured Translation Keys
|
|||
|
|
```typescript
|
|||
|
|
// 按功能分组
|
|||
|
|
menu: {
|
|||
|
|
componentDetail: '构件详情',
|
|||
|
|
showAll: '显示全部'
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
panel: {
|
|||
|
|
componentDetail: {
|
|||
|
|
title: '构件详情'
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Reason**:
|
|||
|
|
- 层级结构便于管理
|
|||
|
|
- 避免命名冲突
|
|||
|
|
- 易于扩展
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 2. Type-Safe Translation
|
|||
|
|
```typescript
|
|||
|
|
// 先定义类型
|
|||
|
|
interface TranslationDictionary {
|
|||
|
|
menu: {
|
|||
|
|
componentDetail: string;
|
|||
|
|
showAll: string;
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 再实现翻译
|
|||
|
|
export const zhCN: TranslationDictionary = { ... };
|
|||
|
|
export const enUS: TranslationDictionary = { ... };
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Reason**: TypeScript 编译时检查,防止遗漏翻译。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Documentation Patterns
|
|||
|
|
|
|||
|
|
#### 1. Multi-Chapter Structure
|
|||
|
|
```
|
|||
|
|
第一章:工具栏 (Toolbar)
|
|||
|
|
第二章:右键菜单 (Context Menu)
|
|||
|
|
第三章:构件交互 (Component Interaction)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Reason**:
|
|||
|
|
- 从功能维度到文档维度的升级
|
|||
|
|
- 支持未来扩展(第四章、第五章...)
|
|||
|
|
- 保持现有内容不变,降低风险
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### 2. Call Chain Visualization
|
|||
|
|
```
|
|||
|
|
用户点击构件
|
|||
|
|
↓
|
|||
|
|
[底层] interactionModule.handleMouseClick()
|
|||
|
|
↓
|
|||
|
|
[SDK] Engine: click event listener
|
|||
|
|
↓
|
|||
|
|
[SDK] EngineManager: getSelectedComponent()
|
|||
|
|
↓
|
|||
|
|
[UI] RightKeyManager: dynamic menu
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Reason**:
|
|||
|
|
- 清晰展示调用层级
|
|||
|
|
- 区分底层/SDK/UI 边界
|
|||
|
|
- 便于新人理解架构
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Challenges & Solutions
|
|||
|
|
|
|||
|
|
#### Challenge 1: 右键菜单如何知道是否有选中构件?
|
|||
|
|
**Solution**:
|
|||
|
|
- Engine 组件监听底层 click 事件,维护 `selectedComponent` 状态
|
|||
|
|
- EngineManager 暴露 `getSelectedComponent()` 方法
|
|||
|
|
- RightKeyManager 的处理器函数每次运行时调用此方法
|
|||
|
|
|
|||
|
|
**Key Decision**: 状态存储在 Engine,而非 EngineManager,因为 Engine 直接监听底层事件。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Challenge 2: 如何避免循环依赖?
|
|||
|
|
**Problem**:
|
|||
|
|
- ComponentDetailManager 需要调用 EngineManager
|
|||
|
|
- EngineManager 的菜单需要调用 ComponentDetailManager
|
|||
|
|
|
|||
|
|
**Solution**:
|
|||
|
|
- 使用 ManagerRegistry 单例作为中介
|
|||
|
|
- 两者都不直接引用对方,而是通过 Registry 访问
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ComponentDetailManager
|
|||
|
|
const registry = ManagerRegistry.getInstance();
|
|||
|
|
registry.engine3d?.getComponentProperties(...);
|
|||
|
|
|
|||
|
|
// EngineManager
|
|||
|
|
const registry = ManagerRegistry.getInstance();
|
|||
|
|
registry.componentDetail?.show(...);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
#### Challenge 3: 底层 API 数据格式与 UI 组件格式不一致
|
|||
|
|
**Problem**:
|
|||
|
|
- 底层返回: `properties: [{ name, children: [{ name, value }] }]`
|
|||
|
|
- UI 需要: `items: [{ categoryName, items: [{ key, value }] }]`
|
|||
|
|
|
|||
|
|
**Solution**:
|
|||
|
|
- ComponentDetailManager 中进行数据转换
|
|||
|
|
- 职责明确:Manager 负责数据适配,Component 负责展示
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
const categories = data.properties.map(cat => ({
|
|||
|
|
categoryName: cat.name,
|
|||
|
|
items: cat.children.map(child => ({
|
|||
|
|
key: child.name,
|
|||
|
|
value: child.value
|
|||
|
|
}))
|
|||
|
|
}));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Build & Verification
|
|||
|
|
|
|||
|
|
#### Build Command
|
|||
|
|
```bash
|
|||
|
|
bun run build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Output**:
|
|||
|
|
```
|
|||
|
|
✓ 87 modules transformed
|
|||
|
|
dist/iflow-engine.es.js 2,025.42 kB
|
|||
|
|
dist/iflow-engine.umd.js 1,329.90 kB
|
|||
|
|
✓ built in 4.98s
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Verification Checklist**:
|
|||
|
|
- [x] TypeScript 编译无错误
|
|||
|
|
- [x] 项目级 LSP diagnostics 无错误
|
|||
|
|
- [x] 模块打包成功(ESM + UMD)
|
|||
|
|
- [x] 文件大小在合理范围
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Git Workflow
|
|||
|
|
|
|||
|
|
**Commit Strategy**: 每个任务独立提交
|
|||
|
|
1. `cf20389` - Engine 监听点击事件
|
|||
|
|
2. `e75886d` - EngineManager 动态菜单
|
|||
|
|
3. `33f1c72` - ComponentDetailManager 实现
|
|||
|
|
4. `89789e0` - Registry 注册
|
|||
|
|
5. `a61c7f4` - i18n 国际化
|
|||
|
|
|
|||
|
|
**Benefits**:
|
|||
|
|
- 每个 commit 职责单一
|
|||
|
|
- 便于 code review
|
|||
|
|
- 支持 cherry-pick / revert
|
|||
|
|
- 清晰的历史记录
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Reusable Patterns for Future Work
|
|||
|
|
|
|||
|
|
### Pattern: Dynamic UI Based on Runtime State
|
|||
|
|
**When to use**:
|
|||
|
|
- Toolbar button visibility
|
|||
|
|
- Menu item enable/disable
|
|||
|
|
- Panel content switching
|
|||
|
|
|
|||
|
|
**Template**:
|
|||
|
|
```typescript
|
|||
|
|
// 1. State storage
|
|||
|
|
private currentState: State | null = null;
|
|||
|
|
|
|||
|
|
// 2. State getter
|
|||
|
|
public getState(): State | null {
|
|||
|
|
return this.currentState;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 3. Dynamic generator
|
|||
|
|
const handler = () => {
|
|||
|
|
const state = this.getState();
|
|||
|
|
const items = [];
|
|||
|
|
|
|||
|
|
if (state?.condition) {
|
|||
|
|
items.push({ /* conditional item */ });
|
|||
|
|
}
|
|||
|
|
items.push({ /* always visible item */ });
|
|||
|
|
|
|||
|
|
return items;
|
|||
|
|
};
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Pattern: Manager Communication via Registry
|
|||
|
|
**When to use**:
|
|||
|
|
- Manager A needs to call Manager B
|
|||
|
|
- Avoid circular dependencies
|
|||
|
|
|
|||
|
|
**Template**:
|
|||
|
|
```typescript
|
|||
|
|
// In Manager A
|
|||
|
|
const registry = ManagerRegistry.getInstance();
|
|||
|
|
registry.managerB?.doSomething();
|
|||
|
|
|
|||
|
|
// In Manager B
|
|||
|
|
const registry = ManagerRegistry.getInstance();
|
|||
|
|
registry.managerA?.getSomeData();
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Checklist**:
|
|||
|
|
- [ ] Update ManagerRegistry type definition
|
|||
|
|
- [ ] Register manager instance in BimEngine.init()
|
|||
|
|
- [ ] Use optional chaining (`?.`) for all registry accesses
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Pattern: Bottom-Up Data Flow (Event-Driven)
|
|||
|
|
**When to use**:
|
|||
|
|
- 底层引擎有原生事件系统
|
|||
|
|
- SDK 需要响应底层变化
|
|||
|
|
|
|||
|
|
**Template**:
|
|||
|
|
```typescript
|
|||
|
|
// 1. SDK 监听底层事件
|
|||
|
|
this.engine.events.on('eventName', (payload) => {
|
|||
|
|
// 2. 更新 SDK 状态
|
|||
|
|
this.updateState(payload);
|
|||
|
|
|
|||
|
|
// 3. 可选:触发 SDK 层事件
|
|||
|
|
registry.emit('sdk:eventName', transformedPayload);
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Gotchas
|
|||
|
|
|
|||
|
|
### Gotcha 1: Registry 访问时机
|
|||
|
|
**Problem**: 在 Manager constructor 中访问 registry 可能为空
|
|||
|
|
**Solution**: 在 `init()` 方法或延迟访问点使用 registry
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ❌ Bad
|
|||
|
|
constructor() {
|
|||
|
|
const registry = ManagerRegistry.getInstance();
|
|||
|
|
this.engine = registry.engine3d; // 可能为 null
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ Good
|
|||
|
|
public show() {
|
|||
|
|
const registry = ManagerRegistry.getInstance();
|
|||
|
|
registry.engine3d?.doSomething(); // 使用时访问
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Gotcha 2: 国际化类型定义先行
|
|||
|
|
**Problem**: 直接在 zh-CN.ts 添加翻译会导致 TypeScript 错误
|
|||
|
|
**Solution**: 先更新 types.ts,再更新 zh-CN.ts 和 en-US.ts
|
|||
|
|
|
|||
|
|
**Order**:
|
|||
|
|
1. `src/locales/types.ts` - 添加类型定义
|
|||
|
|
2. `src/locales/zh-CN.ts` - 添加中文翻译
|
|||
|
|
3. `src/locales/en-US.ts` - 添加英文翻译
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### Gotcha 3: 文档 Read-Before-Write
|
|||
|
|
**Problem**: Edit_tool 要求先 Read 文件才能编辑
|
|||
|
|
**Solution**: 即使之前读过,每次 Edit 前也要 Read 一次
|
|||
|
|
|
|||
|
|
```typescript
|
|||
|
|
// ✅ Correct workflow
|
|||
|
|
1. Read_tool(file)
|
|||
|
|
2. Edit_tool(file, oldString, newString)
|
|||
|
|
3. Read_tool(file) // Next edit
|
|||
|
|
4. Edit_tool(file, ...)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Metrics
|
|||
|
|
|
|||
|
|
- **Files Modified**: 9
|
|||
|
|
- **New File Created**: 1 (component-detail-manager.ts)
|
|||
|
|
- **Lines Added**: ~500
|
|||
|
|
- **Commits**: 5
|
|||
|
|
- **Build Time**: ~5s
|
|||
|
|
- **Implementation Time**: ~2 hours
|
|||
|
|
- **Documentation Lines**: +498 (API_CALLCHAIN.md)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Future Improvements
|
|||
|
|
|
|||
|
|
### 1. Type Definitions for Bottom-Layer API
|
|||
|
|
**Current**: `callback: (data: any) => void`
|
|||
|
|
**Future**:
|
|||
|
|
```typescript
|
|||
|
|
interface ComponentProperty {
|
|||
|
|
name: string;
|
|||
|
|
children: Array<{ name: string; value: string | number }>;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
interface PropertyData {
|
|||
|
|
properties: ComponentProperty[];
|
|||
|
|
materials: any[];
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
callback: (data: PropertyData) => void
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2. Loading State Optimization
|
|||
|
|
**Current**: Simple "加载中..." text
|
|||
|
|
**Future**:
|
|||
|
|
- Skeleton loading UI
|
|||
|
|
- Progress indicator
|
|||
|
|
- Error handling with retry
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 3. "显示全部" Implementation
|
|||
|
|
**Current**: `console.log('[EngineManager] 显示全部')`
|
|||
|
|
**Future**: 调用底层 API 显示所有隐藏构件
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 4. Property Panel Cache
|
|||
|
|
**Current**: 每次打开都重新请求
|
|||
|
|
**Future**:
|
|||
|
|
- Cache recently viewed properties
|
|||
|
|
- Invalidate on model change
|
|||
|
|
- Reduce API calls
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Success Criteria - All Met ✅
|
|||
|
|
|
|||
|
|
- [x] 点击构件后,控制台输出选中信息
|
|||
|
|
- [x] 有选中构件时,右键显示"构件详情"+"显示全部"
|
|||
|
|
- [x] 无选中构件时,右键只显示"显示全部"
|
|||
|
|
- [x] 点击"构件详情"弹出属性弹窗
|
|||
|
|
- [x] 弹窗正确展示底层 API 返回的属性数据
|
|||
|
|
- [x] 点击"显示全部"控制台输出提示
|
|||
|
|
- [x] 构建成功
|
|||
|
|
- [x] 调用链文档已重构为全局文档
|
|||
|
|
- [x] 新增右键菜单章节
|
|||
|
|
- [x] 新增构件交互章节
|