# 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] 新增构件交互章节