fix(menu): refactor menu system to use pure config objects and fix submenu click events
This commit is contained in:
0
.idea/.gitignore
generated
vendored
Normal file
0
.idea/.gitignore
generated
vendored
Normal file
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
|
||||
<option name="progress" value="1.0" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/engine.iml
generated
Normal file
9
.idea/engine.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/engine.iml" filepath="$PROJECT_DIR$/.idea/engine.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
104
.idea/workspace.xml
generated
Normal file
104
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="728b1ce9-7308-4507-bebd-62399c54bf21" name="更改" comment="添加测试信息" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 7
|
||||
}</component>
|
||||
<component name="ProjectId" id="36NEeHE9A1qKqNSOk1qNAyy95Qz" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.go.formatter.settings.were.checked": "true",
|
||||
"RunOnceActivity.go.migrated.go.modules.settings": "true",
|
||||
"git-widget-placeholder": "main",
|
||||
"go.import.settings.migrated": "true",
|
||||
"kotlin-language-version-configured": "true",
|
||||
"last_opened_file_path": "/Users/yuding/WORK/LYZ/project/bimEngine/engine",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"settings.editor.selected.configurable": "preferences.pluginManager",
|
||||
"ts.external.directory.path": "/Users/yuding/WORK/LYZ/project/bimEngine/engine/node_modules/typescript/lib",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-jdk-9823dce3aa75-bf35d07a577b-intellij.indexing.shared.core-IU-252.27397.103" />
|
||||
<option value="bundled-js-predefined-d6986cc7102b-3aa1da707db6-JavaScript-IU-252.27397.103" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="默认任务">
|
||||
<changelist id="728b1ce9-7308-4507-bebd-62399c54bf21" name="更改" comment="" />
|
||||
<created>1764838723949</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1764838723949</updated>
|
||||
<workItem from="1764838724952" duration="1396000" />
|
||||
<workItem from="1764840544897" duration="4283000" />
|
||||
<workItem from="1764919374022" duration="3896000" />
|
||||
<workItem from="1764923867307" duration="53000" />
|
||||
<workItem from="1764923944573" duration="598000" />
|
||||
<workItem from="1765159215556" duration="215000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="添加测试信息">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764844749103</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764844749103</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="添加测试信息">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764844873205</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764844873205</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="添加测试信息">
|
||||
<option name="closed" value="true" />
|
||||
<created>1765159346641</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1765159346641</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="VcsManagerConfiguration">
|
||||
<MESSAGE value="添加测试信息" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="添加测试信息" />
|
||||
</component>
|
||||
<component name="VgoProject">
|
||||
<settings-migrated>true</settings-migrated>
|
||||
</component>
|
||||
</project>
|
||||
@@ -70,6 +70,7 @@ npm run build
|
||||
**运行 HTML Demo (纯 JS):**
|
||||
```bash
|
||||
npm run dev:demo
|
||||
# 自动执行:构建 SDK -> 复制 SDK 到 demo/lib -> 启动服务器
|
||||
# 开发服务器运行在 http://localhost:3000
|
||||
# 自动打开 /demo/index.html
|
||||
```
|
||||
@@ -77,6 +78,7 @@ npm run dev:demo
|
||||
**运行 Vue Demo:**
|
||||
```bash
|
||||
npm run dev:demo-vue
|
||||
# 自动执行:构建 SDK -> 复制 SDK 到 demo-vue/public/lib -> 启动服务器
|
||||
# 开发服务器运行在 http://localhost:3000
|
||||
# 自动打开 Vue 示例页面
|
||||
```
|
||||
@@ -88,8 +90,8 @@ npm run dev:all
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- 运行 Demo 前,建议先执行 `npm run build` 构建 SDK,确保 Demo 使用的是最新构建的 SDK
|
||||
- Demo 会自动从 `dist/` 目录或通过开发服务器加载 SDK
|
||||
- 现在的 `npm run dev:demo` 和 `npm run dev:demo-vue` 命令已包含自动化构建流程,无需手动运行 `npm run build`。
|
||||
- Demo 会自动从本地复制的 SDK 副本加载。
|
||||
|
||||
### 1.5 发布配置
|
||||
|
||||
@@ -534,6 +536,7 @@ engine.toolbar.setButtonVisibility('my-button', false);
|
||||
| `ToolbarManager` | `src/managers/toolbar-manager.ts` | 管理底部工具栏 | `BimComponent` |
|
||||
| `ButtonGroupManager` | `src/managers/button-group-manager.ts` | 管理通用按钮组 | `BimComponent` |
|
||||
| `EngineManager` | `src/managers/engine-manager.ts` | 管理 3D 引擎 | `BimComponent` |
|
||||
| `RightKeyManager` | `src/managers/right-key-manager.ts` | 管理右键菜单 (Context Menu)。直接使用 `MenuItemConfig` 接口配置 | `BimComponent` |
|
||||
|
||||
### 4.2 组件类清单
|
||||
|
||||
@@ -544,6 +547,8 @@ engine.toolbar.setButtonVisibility('my-button', false);
|
||||
| `BimButtonGroup` | `src/components/button-group/index.ts` | 通用按钮组组件 | `IBimComponent` |
|
||||
| `Toolbar` | `src/components/button-group/toolbar/index.ts` | 底部工具栏组件 | 继承 `BimButtonGroup` |
|
||||
| `Engine` | `src/components/engine/index.ts` | 3D 引擎组件 | `IBimComponent` |
|
||||
| `BimRightKey` | `src/components/right-key/index.ts` | 右键浮层容器 | `IBimComponent` |
|
||||
| `BimMenu` | `src/components/menu/index.ts` | 通用菜单列表 | `IBimComponent` |
|
||||
|
||||
### 4.3 服务类清单
|
||||
|
||||
@@ -960,7 +965,7 @@ export class MyDialog implements IBimComponent {
|
||||
|
||||
#### 语言要求(强制)
|
||||
- **所有输出必须使用中文**,包括:
|
||||
- 代码注释
|
||||
- 代码注释 (**强制:所有代码注释必须使用中文,解释清晰详细**)
|
||||
- 文档说明
|
||||
- 与用户交流
|
||||
- 错误信息
|
||||
|
||||
@@ -89,6 +89,30 @@ onMounted(() => {
|
||||
engine.value = new Engine(appContainer.value, { locale: 'zh-CN' });
|
||||
initEngine();
|
||||
console.log('Engine initialized:', engine.value);
|
||||
|
||||
// --- 新增:注册右键菜单 ---
|
||||
engine.value.rightKey.registerHandler((event: any) => {
|
||||
const { x, y } = event;
|
||||
return [
|
||||
{
|
||||
id: 'home',
|
||||
label: 'toolbar.home', // 使用国际化 Key
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path><polyline points="9 22 9 12 15 12 15 22"></polyline></svg>',
|
||||
onClick: () => {
|
||||
console.log('Go Home');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'log-pos',
|
||||
label: '打印坐标',
|
||||
icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>',
|
||||
onClick: () => {
|
||||
console.log(`Clicked at: ${x}, ${y}`);
|
||||
alert(`Right clicked at: ${x}, ${y}`);
|
||||
}
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Init failed:', err);
|
||||
|
||||
4937
dist/bim-engine-sdk.es.js
vendored
4937
dist/bim-engine-sdk.es.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bim-engine-sdk.es.js.map
vendored
2
dist/bim-engine-sdk.es.js.map
vendored
File diff suppressed because one or more lines are too long
374
dist/bim-engine-sdk.umd.js
vendored
374
dist/bim-engine-sdk.umd.js
vendored
File diff suppressed because one or more lines are too long
2
dist/bim-engine-sdk.umd.js.map
vendored
2
dist/bim-engine-sdk.umd.js.map
vendored
File diff suppressed because one or more lines are too long
201
dist/index.d.ts
vendored
201
dist/index.d.ts
vendored
@@ -146,6 +146,7 @@ export declare class BimEngine extends EventEmitter {
|
||||
buttonGroup: ButtonGroupManager | null;
|
||||
dialog: DialogManager | null;
|
||||
engine: EngineManager | null;
|
||||
rightKey: RightKeyManager | null;
|
||||
get localeManager(): LocaleManager;
|
||||
get themeManager(): ThemeManager;
|
||||
constructor(container: HTMLElement | string, options?: {
|
||||
@@ -169,6 +170,111 @@ export declare class BimEngine extends EventEmitter {
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用菜单列表组件
|
||||
* 负责渲染一组菜单项,支持分组、排序、图标、快捷键提示和递归多级子菜单。
|
||||
* 它不包含定位逻辑,仅负责内容渲染。
|
||||
*/
|
||||
export declare class BimMenu implements IBimComponent {
|
||||
element: HTMLElement;
|
||||
private options;
|
||||
private unsubscribeLocale;
|
||||
private unsubscribeTheme;
|
||||
private activeSubMenu;
|
||||
constructor(options: MenuOptions);
|
||||
/**
|
||||
* 初始化组件
|
||||
* 渲染 DOM 结构并订阅语言变更
|
||||
*/
|
||||
init(): void;
|
||||
/**
|
||||
* 设置主题
|
||||
* @param theme 全局主题配置
|
||||
*/
|
||||
setTheme(theme: ThemeConfig): void;
|
||||
/**
|
||||
* 响应语言变更
|
||||
* 重新渲染整个菜单以更新文本
|
||||
*/
|
||||
setLocales(): void;
|
||||
/**
|
||||
* 销毁组件
|
||||
* 清理事件监听、子菜单和 DOM 元素
|
||||
*/
|
||||
destroy(): void;
|
||||
/**
|
||||
* 获取组件根元素
|
||||
* 实现 IRightKeyContent 接口,允许被 RightKey 容器挂载
|
||||
*/
|
||||
getElement(): HTMLElement;
|
||||
/**
|
||||
* 核心渲染逻辑
|
||||
* 处理分组、排序和 DOM 生成
|
||||
*/
|
||||
private render;
|
||||
/**
|
||||
* 创建单个菜单项的 DOM 元素
|
||||
*/
|
||||
private createItemElement;
|
||||
/**
|
||||
* 打开子菜单
|
||||
* @param item 当前菜单项
|
||||
* @param parentLi 触发的 DOM 元素(用于定位)
|
||||
*/
|
||||
private openSubMenu;
|
||||
/**
|
||||
* 关闭当前激活的子菜单
|
||||
*/
|
||||
private closeSubMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* 右键浮层容器组件 (RightKey)
|
||||
* 这是一个纯粹的定位容器,负责在屏幕指定位置显示内容。
|
||||
* 它不关心具体内容是什么,只处理定位、边界检测和关闭逻辑。
|
||||
*/
|
||||
export declare class BimRightKey implements IBimComponent {
|
||||
private element;
|
||||
private content;
|
||||
private isVisible;
|
||||
private onCloseCallback?;
|
||||
constructor(options?: RightKeyOptions);
|
||||
init(): void;
|
||||
setTheme(_theme: ThemeConfig): void;
|
||||
setLocales(): void;
|
||||
destroy(): void;
|
||||
/**
|
||||
* 设置关闭时的回调函数
|
||||
* 通常用于通知 Manager 状态变更
|
||||
*/
|
||||
setOnClose(callback: () => void): void;
|
||||
/**
|
||||
* 挂载内容组件
|
||||
* @param content 实现了 IRightKeyContent 接口的组件实例
|
||||
*/
|
||||
mount(content: IRightKeyContent): void;
|
||||
/**
|
||||
* 卸载当前内容
|
||||
*/
|
||||
unmountContent(): void;
|
||||
/**
|
||||
* 在指定位置显示容器
|
||||
* 包含智能边界检测逻辑,防止溢出屏幕
|
||||
* @param x 目标 X 坐标 (通常是鼠标点击位置)
|
||||
* @param y 目标 Y 坐标
|
||||
*/
|
||||
show(x: number, y: number): void;
|
||||
/**
|
||||
* 隐藏容器
|
||||
*/
|
||||
hide(): void;
|
||||
/**
|
||||
* 处理全局点击事件
|
||||
* 用于检测是否点击了容器外部
|
||||
*/
|
||||
private handleGlobalClick;
|
||||
}
|
||||
|
||||
/** 按钮内部文字图标排列 */
|
||||
declare type ButtonAlign = 'vertical' | 'horizontal';
|
||||
|
||||
@@ -249,11 +355,11 @@ export declare function createEngine(r) {
|
||||
const e = r.version || "v1";
|
||||
switch (e) {
|
||||
case "v2":
|
||||
return new Nc(r);
|
||||
return new Bc(r);
|
||||
case "v1":
|
||||
return new I_(r);
|
||||
return new N_(r);
|
||||
:
|
||||
return console.warn(`Version '${e}' not found. Falling back to v2.`), new Nc(r);
|
||||
return console.warn(`Version '${e}' not found. Falling back to v2.`), new Bc(r);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +589,17 @@ declare interface IBimComponent {
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
declare interface IRightKeyContent {
|
||||
/**
|
||||
* 获取组件的根 DOM 元素
|
||||
*/
|
||||
getElement(): HTMLElement;
|
||||
/**
|
||||
* 销毁组件
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
declare type Listener<T = any> = (payload: T) => void;
|
||||
|
||||
declare type LocaleChangeListener = (locale: LocaleType) => void;
|
||||
@@ -519,6 +636,39 @@ declare class LocaleManager {
|
||||
*/
|
||||
declare type LocaleType = 'zh-CN' | 'en-US';
|
||||
|
||||
/**
|
||||
* 菜单项配置接口 (用于简化的对象配置)
|
||||
*/
|
||||
export declare interface MenuItemConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
icon?: string;
|
||||
group?: string;
|
||||
order?: number;
|
||||
children?: MenuItemConfig[];
|
||||
disabled?: boolean;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* <20><><EFBFBD>单组件配置选项
|
||||
*/
|
||||
declare interface MenuOptions {
|
||||
/**
|
||||
* 菜单项列表
|
||||
* 可以是扁平数组,组件会根据 group 字段自动分组
|
||||
*/
|
||||
items: MenuItemConfig[];
|
||||
/**
|
||||
* 分组显示顺序
|
||||
* 包含组 ID 的字符串数组。
|
||||
* 例如: ['view', 'edit', 'tools']
|
||||
* 未在此数组中定义的组将按默认顺序排在最后。
|
||||
*/
|
||||
groupOrder?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 模型加载选项
|
||||
* 用于配置模型的位置、旋转和缩放
|
||||
@@ -538,6 +688,51 @@ export declare interface OptButton extends ButtonConfig {
|
||||
children?: OptButton[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 右键菜单管理器 (RightKeyManager)
|
||||
* 负责协调右键交互流程:
|
||||
* 1. 监听 Canvas/容器的 contextmenu 事件
|
||||
* 2. 通过注册的处理器 (Handler) 获取需要显示的菜单项
|
||||
* 3. 实例化 Menu 组件并装载到 RightKey 容器中显示
|
||||
*/
|
||||
declare class RightKeyManager extends BimComponent {
|
||||
private container;
|
||||
private rightKeyPanel;
|
||||
private contextHandlers;
|
||||
constructor(engine: BimEngine, container: HTMLElement);
|
||||
private initEventListeners;
|
||||
destroy(): void;
|
||||
/**
|
||||
* 注册上下文菜单处理器
|
||||
* @param handler 处理函数,接收鼠标事件,返回菜单项数组
|
||||
*/
|
||||
registerHandler(handler: (e: MouseEvent) => MenuItemConfig[] | null): void;
|
||||
/**
|
||||
* 手动显示菜单
|
||||
* 允许外部直接调用以显示特定的菜单,不一定依赖右键事件
|
||||
* @param x 屏幕 X 坐标
|
||||
* @param y 屏幕 Y 坐标
|
||||
* @param items 菜单项列表
|
||||
* @param groupOrder 可<><E58FAF>的分组顺序
|
||||
*/
|
||||
showMenu(x: number, y: number, items: MenuItemConfig[], groupOrder?: string[]): void;
|
||||
/**
|
||||
* 隐藏右键菜单
|
||||
*/
|
||||
hide(): void;
|
||||
/**
|
||||
* 处理右键点击事件
|
||||
*/
|
||||
private handleContextMenu;
|
||||
}
|
||||
|
||||
declare interface RightKeyOptions {
|
||||
/** 自定义 CSS 类名 */
|
||||
className?: string;
|
||||
/** 层级 (z-index) */
|
||||
zIndex?: number;
|
||||
}
|
||||
|
||||
declare type ThemeChangeListener = (theme: ThemeConfig) => void;
|
||||
|
||||
/**
|
||||
|
||||
1183
docs/components/button-group.md
Normal file
1183
docs/components/button-group.md
Normal file
File diff suppressed because it is too large
Load Diff
882
docs/components/dialog.md
Normal file
882
docs/components/dialog.md
Normal file
@@ -0,0 +1,882 @@
|
||||
# Dialog 组件详细文档
|
||||
|
||||
> 本文档详细描述 Dialog 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
|
||||
|
||||
---
|
||||
|
||||
## 1. 组件概述
|
||||
|
||||
### 1.1 基本信息
|
||||
- **组件名称**: `BimDialog`
|
||||
- **文件路径**: `src/components/dialog/index.ts`
|
||||
- **类型定义**: `src/components/dialog/index.type.ts`
|
||||
- **样式文件**: `src/components/dialog/index.css`
|
||||
- **实现接口**: `IBimComponent`
|
||||
- **用途**: 提供可拖拽、可缩放的通用弹窗组件,支持自定义内容和样式
|
||||
|
||||
### 1.2 在 SDK 中的位置
|
||||
- Dialog 组件是独立的 UI 组件
|
||||
- 必须通过 `DialogManager` 使用,不允许直接使用
|
||||
- `DialogManager` 位于 `src/managers/dialog-manager.ts`
|
||||
|
||||
---
|
||||
|
||||
## 2. 组件类 API 文档
|
||||
|
||||
### 2.1 构造函数
|
||||
|
||||
```typescript
|
||||
constructor(options: DialogOptions)
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `options`: `DialogOptions` - 弹窗配置选项(详见类型定义)
|
||||
|
||||
**默认配置**:
|
||||
```typescript
|
||||
{
|
||||
title: 'Dialog',
|
||||
width: 300,
|
||||
height: 'auto',
|
||||
position: 'center',
|
||||
draggable: true,
|
||||
resizable: false,
|
||||
minWidth: 200,
|
||||
minHeight: 100
|
||||
}
|
||||
```
|
||||
|
||||
**行为**:
|
||||
- 合并用户配置和默认配置
|
||||
- 创建 DOM 结构
|
||||
- 自动调用 `init()` 方法
|
||||
|
||||
### 2.2 公共方法
|
||||
|
||||
#### `init(): void`
|
||||
初始化组件功能(实现 `IBimComponent` 接口)
|
||||
|
||||
**功能**:
|
||||
- 将弹窗元素添加到容器
|
||||
- 初始化位置
|
||||
- 如果 `draggable` 为 true,初始化拖拽功能
|
||||
- 如果 `resizable` 为 true,初始化缩放功能
|
||||
- 订阅主题和语言变更
|
||||
- 调用 `onOpen` 回调
|
||||
|
||||
**调用时机**: 构造函数中自动调用,也可手动调用
|
||||
|
||||
#### `setTheme(theme: ThemeConfig): void`
|
||||
设置主题(实现 `IBimComponent` 接口)
|
||||
|
||||
**参数**:
|
||||
- `theme`: `ThemeConfig` - 全局主题配置
|
||||
|
||||
**功能**:
|
||||
- 将主题颜色映射到 CSS 变量
|
||||
- 如果用户未自定义颜色,使用主题颜色
|
||||
- 如果用户已自定义颜色,保持用户自定义
|
||||
|
||||
**CSS 变量映射**:
|
||||
- `--bim-dialog-bg` ← `theme.panelBackground` (如果未自定义 `backgroundColor`)
|
||||
- `--bim-dialog-header-bg` ← `theme.componentHover` (如果未自定义 `headerBackgroundColor`)
|
||||
- `--bim-dialog-title-color` ← `theme.textPrimary` (如果未自定义 `titleColor`)
|
||||
- `--bim-dialog-text-color` ← `theme.textPrimary` (如果未自定义 `textColor`)
|
||||
- `--bim-dialog-border-color` ← `theme.border` (如果未自定义 `borderColor`)
|
||||
|
||||
#### `setLocales(): void`
|
||||
设置语言(实现 `IBimComponent` 接口)
|
||||
|
||||
**功能**:
|
||||
- 更新标题文本(如果标题是翻译键)
|
||||
- 使用 `t()` 函数获取翻译后的文本
|
||||
|
||||
**行为**:
|
||||
- 查找 `.bim-dialog-title` 元素
|
||||
- 如果 `options.title` 存在,使用 `t(options.title)` 更新文本
|
||||
|
||||
#### `setContent(content: HTMLElement | string): void`
|
||||
动态设置弹窗内容
|
||||
|
||||
**参数**:
|
||||
- `content`: `HTMLElement | string` - 内容元素或 HTML 字符串
|
||||
|
||||
**功能**:
|
||||
- 清空当前内容
|
||||
- 设置新内容(支持 HTML 字符串或 DOM 元素)
|
||||
|
||||
#### `close(): void`
|
||||
关闭弹窗并销毁
|
||||
|
||||
**功能**:
|
||||
- 清理 `requestAnimationFrame`(如果存在)
|
||||
- 取消主题和语言订阅
|
||||
- 从 DOM 中移除元素
|
||||
- 标记为已销毁
|
||||
- 调用 `onClose` 回调
|
||||
|
||||
#### `destroy(): void`
|
||||
销毁组件(实现 `IBimComponent` 接口)
|
||||
|
||||
**功能**:
|
||||
- 调用 `close()` 方法
|
||||
|
||||
### 2.3 私有方法(供理解实现细节)
|
||||
|
||||
#### `createDom(): HTMLElement`
|
||||
创建弹窗的 DOM 结构
|
||||
|
||||
**返回**: 创建的弹窗根元素
|
||||
|
||||
**DOM 结构**:
|
||||
```html
|
||||
<div class="bim-dialog" [id="..."]>
|
||||
<div class="bim-dialog-header" [class="draggable"]>
|
||||
<span class="bim-dialog-title">标题文本</span>
|
||||
<span class="bim-dialog-close">×</span>
|
||||
</div>
|
||||
<div class="bim-dialog-content">
|
||||
<!-- 用户内容 -->
|
||||
</div>
|
||||
[<div class="bim-dialog-resize-handle"></div>] <!-- 如果 resizable -->
|
||||
</div>
|
||||
```
|
||||
|
||||
**关键实现**:
|
||||
1. 创建根元素,应用 CSS 变量和尺寸
|
||||
2. 创建标题栏,包含标题和关闭按钮
|
||||
3. 创建内容区域,支持 HTML 字符串或 DOM 元素
|
||||
4. 如果 `resizable` 为 true,创建缩放手柄
|
||||
5. **事件拦截**: 绑定所有鼠标/触摸事件,使用 `stopPropagation()` 阻止冒泡,防止传递给 3D 引擎
|
||||
|
||||
**事件拦截列表**:
|
||||
```typescript
|
||||
['click', 'dblclick', 'contextmenu', 'wheel',
|
||||
'mousedown', 'mouseup', 'mousemove',
|
||||
'touchstart', 'touchend', 'touchmove',
|
||||
'pointerdown', 'pointerup', 'pointermove',
|
||||
'pointerenter', 'pointerleave', 'pointerover', 'pointerout']
|
||||
```
|
||||
|
||||
#### `initPosition(): void`
|
||||
初始化弹窗位置
|
||||
|
||||
**功能**:
|
||||
- 根据 `position` 选项计算弹窗位置
|
||||
- 支持预设位置(如 'center', 'top-left' 等)或坐标对象 `{ x, y }`
|
||||
- 确保弹窗不超出容器边界
|
||||
|
||||
**位置计算逻辑**:
|
||||
- `center`: `left = (containerWidth - dialogWidth) / 2`, `top = (containerHeight - dialogHeight) / 2`
|
||||
- `top-left`: `left = 0`, `top = 0`
|
||||
- `top-center`: `left = (containerWidth - dialogWidth) / 2`, `top = 0`
|
||||
- `top-right`: `left = containerWidth - dialogWidth`, `top = 0`
|
||||
- `left-center`: `left = 0`, `top = (containerHeight - dialogHeight) / 2`
|
||||
- `right-center`: `left = containerWidth - dialogWidth`, `top = (containerHeight - dialogHeight) / 2`
|
||||
- `bottom-left`: `left = 0`, `top = containerHeight - dialogHeight`
|
||||
- `bottom-center`: `left = (containerWidth - dialogWidth) / 2`, `top = containerHeight - dialogHeight`
|
||||
- `bottom-right`: `left = containerWidth - dialogWidth`, `top = containerHeight - dialogHeight`
|
||||
- `{ x, y }`: 直接使用坐标值
|
||||
|
||||
**边界限制**:
|
||||
```typescript
|
||||
left = Math.max(0, Math.min(left, containerWidth - dialogWidth));
|
||||
top = Math.max(0, Math.min(top, containerHeight - dialogHeight));
|
||||
```
|
||||
|
||||
#### `initDrag(): void`
|
||||
初始化拖拽功能
|
||||
|
||||
**功能**:
|
||||
- 在标题栏上绑定 `mousedown` 事件
|
||||
- 实现拖拽移动功能
|
||||
- 使用 `requestAnimationFrame` 优化性能
|
||||
- 使用 `capture: true` 确保事件在捕获阶段处理
|
||||
|
||||
**拖拽流程**:
|
||||
1. `mousedown`: 记录起始位置和弹窗当前位置,缓存容器和弹窗尺寸
|
||||
2. `mousemove`: 计算偏移量,更新弹窗位置,限制在容器边界内
|
||||
3. `mouseup`: 清理事件监听和动画帧
|
||||
|
||||
**性能优化**:
|
||||
- 使用 `requestAnimationFrame` 节流更新
|
||||
- 缓存容器和弹窗尺寸,减少 reflow
|
||||
- 使用 `capture: true` 确保事件处理
|
||||
|
||||
#### `initResize(): void`
|
||||
初始化缩放功能
|
||||
|
||||
**功能**:
|
||||
- 在缩放手柄上绑定 `mousedown` 事件
|
||||
- 实现右下角拖拽缩放功能
|
||||
- 使用 `requestAnimationFrame` 优化性能
|
||||
- 限制最小尺寸
|
||||
|
||||
**缩放流程**:
|
||||
1. `mousedown`: 记录起始位置和弹窗当前尺寸
|
||||
2. `mousemove`: 计算偏移量,更新弹窗尺寸,限制最小尺寸
|
||||
3. `mouseup`: 清理事件监听和动画帧
|
||||
|
||||
**尺寸限制**:
|
||||
```typescript
|
||||
newWidth = Math.max(minWidth || 100, startWidth + deltaX);
|
||||
newHeight = Math.max(minHeight || 50, startHeight + deltaY);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 分化组件说明
|
||||
|
||||
### 3.1 BimInfoDialog
|
||||
|
||||
**文件路径**: `src/components/dialog/bimInfoDialog/index.ts`
|
||||
|
||||
**继承关系**: `BimInfoDialog extends BimDialog`
|
||||
|
||||
**特殊功能**:
|
||||
- 预定义了信息展示的内容结构
|
||||
- 包含标题、信息列表和操作按钮
|
||||
- 使用固定的配置(宽度 320px,可缩放,可拖拽)
|
||||
|
||||
**实现方式**:
|
||||
- 在构造函数中创建内容 DOM
|
||||
- 调用父类构造函数,传入预定义的配置
|
||||
- 不需要重写其他方法,直接继承父类功能
|
||||
|
||||
**内容结构**:
|
||||
```html
|
||||
<div class="bim-info-dialog-content">
|
||||
<h3>Model Information</h3>
|
||||
<ul>
|
||||
<li><strong>Name:</strong> Sample Project</li>
|
||||
<li><strong>Version:</strong> 1.0.0</li>
|
||||
<li><strong>Date:</strong> 当前日期</li>
|
||||
<li><strong>Status:</strong> <span style="color: green;">Active</span></li>
|
||||
</ul>
|
||||
<button>Update Status</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**与父类的差异**:
|
||||
- 固定了内容结构
|
||||
- 固定了部分配置选项
|
||||
- 其他功能完全继承自 `BimDialog`
|
||||
|
||||
---
|
||||
|
||||
## 4. Manager API 文档
|
||||
|
||||
### 4.1 DialogManager 类
|
||||
|
||||
**文件路径**: `src/managers/dialog-manager.ts`
|
||||
|
||||
**继承关系**: `DialogManager extends BimComponent`
|
||||
|
||||
### 4.2 构造函数
|
||||
|
||||
```typescript
|
||||
constructor(engine: BimEngine, container: HTMLElement)
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `engine`: `BimEngine` - 引擎实例
|
||||
- `container`: `HTMLElement` - 弹窗挂载的目标容器
|
||||
|
||||
**行为**:
|
||||
- 保存容器引用
|
||||
- 监听 `ui:open-dialog` 事件
|
||||
- 如果事件 payload.id 是 'info',自动打开信息弹窗
|
||||
|
||||
### 4.3 公共方法
|
||||
|
||||
#### `create(options: Omit<DialogOptions, 'container'>): BimDialog`
|
||||
创建一个通用弹窗
|
||||
|
||||
**参数**:
|
||||
- `options`: `Omit<DialogOptions, 'container'>` - 弹窗配置(不需要传 container)
|
||||
|
||||
**返回**: `BimDialog` 实例
|
||||
|
||||
**功能**:
|
||||
- 自动使用 Manager 绑定的容器
|
||||
- 创建 `BimDialog` 实例
|
||||
- 应用当前主题
|
||||
- 将弹窗添加到活跃列表
|
||||
- 在 `onClose` 回调中自动从列表移除
|
||||
|
||||
**使用示例**:
|
||||
```typescript
|
||||
const dialog = engine.dialog.create({
|
||||
title: '测试弹窗',
|
||||
content: '这是内容',
|
||||
width: 400,
|
||||
height: 300
|
||||
});
|
||||
```
|
||||
|
||||
#### `showInfoDialog(): void`
|
||||
显示二次封装的模型信息弹窗
|
||||
|
||||
**功能**:
|
||||
- 创建 `BimInfoDialog` 实例
|
||||
- 直接显示信息弹窗
|
||||
|
||||
**注意**: 此方法创建的弹窗不会自动加入管理列表(遗留逻辑)
|
||||
|
||||
#### `updateTheme(theme: ThemeConfig): void`
|
||||
响应全局主题变更
|
||||
|
||||
**参数**:
|
||||
- `theme`: `ThemeConfig` - 全局主题配置
|
||||
|
||||
**功能**:
|
||||
- 遍历所有活跃弹窗
|
||||
- 调用每个弹窗的 `setTheme()` 方法
|
||||
|
||||
#### `destroy(): void`
|
||||
销毁管理器
|
||||
|
||||
**功能**:
|
||||
- 销毁所有活跃弹窗
|
||||
- 清空活跃列表
|
||||
|
||||
---
|
||||
|
||||
## 5. UI 详细描述
|
||||
|
||||
### 5.1 DOM 结构
|
||||
|
||||
**完整 HTML 结构**:
|
||||
```html
|
||||
<div class="bim-dialog" id="[可选ID]" style="[CSS变量和尺寸]">
|
||||
<!-- 标题栏 -->
|
||||
<div class="bim-dialog-header [draggable]">
|
||||
<span class="bim-dialog-title">标题文本</span>
|
||||
<span class="bim-dialog-close">×</span>
|
||||
</div>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<div class="bim-dialog-content">
|
||||
<!-- 用户内容(HTML字符串或DOM元素) -->
|
||||
</div>
|
||||
|
||||
<!-- 缩放手柄(如果 resizable) -->
|
||||
<div class="bim-dialog-resize-handle"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
### 5.2 CSS 类名和样式
|
||||
|
||||
#### `.bim-dialog` (根元素)
|
||||
- `position: absolute` - 绝对定位
|
||||
- `background-color: var(--bim-dialog-bg)` - 背景色(CSS 变量)
|
||||
- `border: 1px solid var(--bim-dialog-border-color)` - 边框
|
||||
- `border-radius: 6px` - 圆角
|
||||
- `box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3)` - 阴影
|
||||
- `display: flex` + `flex-direction: column` - 垂直布局
|
||||
- `z-index: 10001` - 层级(确保在 3D 引擎之上)
|
||||
- `min-width: 200px` - 最小宽度
|
||||
- `min-height: 100px` - 最小高度
|
||||
- `pointer-events: auto` - 确保可接收事件
|
||||
|
||||
#### `.bim-dialog-header` (标题栏)
|
||||
- `height: 32px` - 固定高度
|
||||
- `background-color: var(--bim-dialog-header-bg)` - 背景色
|
||||
- `display: flex` + `align-items: center` + `justify-content: space-between` - 水平布局
|
||||
- `padding: 0 10px` - 内边距
|
||||
- `cursor: default` - 默认光标
|
||||
- `user-select: none` - 禁止选中
|
||||
- `border-bottom: 1px solid var(--bim-dialog-border-color)` - 底边框
|
||||
|
||||
#### `.bim-dialog-header.draggable` (可拖拽标题栏)
|
||||
- `cursor: move` - 移动光标
|
||||
|
||||
#### `.bim-dialog-title` (标题文本)
|
||||
- `font-size: 14px` - 字体大小
|
||||
- `font-weight: 500` - 字体粗细
|
||||
- `white-space: nowrap` - 不换行
|
||||
- `overflow: hidden` - 隐藏溢出
|
||||
- `text-overflow: ellipsis` - 文本省略
|
||||
- `color: var(--bim-dialog-title-color)` - 文字颜色
|
||||
|
||||
#### `.bim-dialog-close` (关闭按钮)
|
||||
- `cursor: pointer` - 指针光标
|
||||
- `font-size: 18px` - 字体大小
|
||||
- `color: #999` - 默认颜色
|
||||
- `line-height: 1` - 行高
|
||||
- `margin-left: 8px` - 左边距
|
||||
|
||||
#### `.bim-dialog-close:hover` (关闭按钮悬停)
|
||||
- `color: #fff` - 悬停颜色
|
||||
|
||||
#### `.bim-dialog-content` (内容区域)
|
||||
- `flex: 1` - 占据剩余空间
|
||||
- `padding: 10px` - 内边距
|
||||
- `overflow: auto` - 内容溢出时滚动
|
||||
- `font-size: 14px` - 字体大小
|
||||
- `color: var(--bim-dialog-text-color)` - 文字颜色
|
||||
|
||||
#### `.bim-dialog-resize-handle` (缩放手柄)
|
||||
- `position: absolute` - 绝对定位
|
||||
- `width: 10px` + `height: 10px` - 尺寸
|
||||
- `bottom: 0` + `right: 0` - 右下角位置
|
||||
- `cursor: se-resize` - 缩放光标
|
||||
- `z-index: 10` - 层级
|
||||
|
||||
#### `.bim-dialog-resize-handle::after` (缩放装饰)
|
||||
- 使用伪元素创建右下角斜线装饰
|
||||
- `border-right` + `border-bottom` - 创建斜线效果
|
||||
|
||||
### 5.3 CSS 变量
|
||||
|
||||
**定义位置**: `:root` 或元素内联样式
|
||||
|
||||
**变量列表**:
|
||||
- `--bim-dialog-bg`: 窗体背景颜色(默认 `rgba(17, 17, 17, 0.95)`)
|
||||
- `--bim-dialog-header-bg`: 标题栏背景颜色(默认 `#2a2a2a`)
|
||||
- `--bim-dialog-title-color`: 标题文字颜色(默认 `#fff`)
|
||||
- `--bim-dialog-text-color`: 内容文字颜色(默认 `#ccc`)
|
||||
- `--bim-dialog-border-color`: 边框颜色(默认 `#444`)
|
||||
|
||||
### 5.4 交互行为
|
||||
|
||||
#### 拖拽
|
||||
- **触发区域**: 标题栏(`.bim-dialog-header`)
|
||||
- **触发条件**: `draggable: true`
|
||||
- **行为**:
|
||||
- 鼠标按下标题栏时开始拖拽
|
||||
- 鼠标移动时弹窗跟随移动
|
||||
- 鼠标释放时结束拖拽
|
||||
- 弹窗位置限制在容器边界内
|
||||
|
||||
#### 缩放
|
||||
- **触发区域**: 缩放手柄(`.bim-dialog-resize-handle`)
|
||||
- **触发条件**: `resizable: true`
|
||||
- **行为**:
|
||||
- 鼠标按下缩放手柄时开始缩放
|
||||
- 鼠标移动时弹窗尺寸跟随变化
|
||||
- 鼠标释放时结束缩放
|
||||
- 尺寸限制在最小尺寸以上
|
||||
|
||||
#### 关闭
|
||||
- **触发方式**:
|
||||
- 点击关闭按钮(`.bim-dialog-close`)
|
||||
- 调用 `close()` 方法
|
||||
- **行为**: 弹窗从 DOM 移除,调用 `onClose` 回调
|
||||
|
||||
### 5.5 响应式行为
|
||||
|
||||
- 弹窗位置会根据容器尺寸自动调整,确保不超出边界
|
||||
- 内容区域支持滚动(`overflow: auto`)
|
||||
- 标题文本过长时显示省略号(`text-overflow: ellipsis`)
|
||||
|
||||
---
|
||||
|
||||
## 6. 逻辑流程详细描述
|
||||
|
||||
### 6.1 初始化流程
|
||||
|
||||
1. **构造函数调用**:
|
||||
- 合并配置选项(用户配置 + 默认配置)
|
||||
- 调用 `createDom()` 创建 DOM 结构
|
||||
- 保存 header 和 contentArea 引用
|
||||
- 自动调用 `init()`
|
||||
|
||||
2. **init() 方法执行**:
|
||||
- 检查是否已初始化,如果是则返回
|
||||
- 将弹窗元素添加到容器
|
||||
- 调用 `initPosition()` 计算并设置位置
|
||||
- 如果 `draggable` 为 true,调用 `initDrag()`
|
||||
- 如果 `resizable` 为 true,调用 `initResize()`
|
||||
- 标记为已初始化
|
||||
- 调用 `onOpen` 回调(如果存在)
|
||||
- 订阅主题变更:`themeManager.subscribe()`
|
||||
- 订阅语言变更:`localeManager.subscribe()`
|
||||
|
||||
### 6.2 生命周期
|
||||
|
||||
#### 创建阶段
|
||||
- 构造函数 → `createDom()` → `init()`
|
||||
|
||||
#### 运行阶段
|
||||
- 响应主题变更:`setTheme()` 被调用
|
||||
- 响应语言变更:`setLocales()` 被调用
|
||||
- 用户交互:拖拽、缩放、关闭
|
||||
|
||||
#### 销毁阶段
|
||||
- `close()` 或 `destroy()` 被调用
|
||||
- 清理 `requestAnimationFrame`
|
||||
- 取消主题和语言订阅
|
||||
- 从 DOM 移除元素
|
||||
- 调用 `onClose` 回调
|
||||
|
||||
### 6.3 事件处理流程
|
||||
|
||||
#### 拖拽事件流程
|
||||
1. 用户在标题栏按下鼠标 → `mousedown` 事件
|
||||
2. 记录起始位置和弹窗当前位置
|
||||
3. 缓存容器和弹窗尺寸
|
||||
4. 在 document 上绑定 `mousemove` 和 `mouseup`(捕获阶段)
|
||||
5. 鼠标移动时 → `mousemove` 事件
|
||||
- 计算偏移量
|
||||
- 使用 `requestAnimationFrame` 更新位置
|
||||
- 限制在容器边界内
|
||||
6. 鼠标释放时 → `mouseup` 事件
|
||||
- 清理事件监听
|
||||
- 清理动画帧
|
||||
|
||||
#### 缩放事件流程
|
||||
1. 用户在缩放手柄按下鼠标 → `mousedown` 事件
|
||||
2. 记录起始位置和弹窗当前尺寸
|
||||
3. 在 document 上绑定 `mousemove` 和 `mouseup`(捕获阶段)
|
||||
4. 鼠标移动时 → `mousemove` 事件
|
||||
- 计算偏移量
|
||||
- 使用 `requestAnimationFrame` 更新尺寸
|
||||
- 限制在最小尺寸以上
|
||||
5. 鼠标释放时 → `mouseup` 事件
|
||||
- 清理事件监听
|
||||
- 清理动画帧
|
||||
|
||||
#### 事件拦截流程
|
||||
- 弹窗根元素上绑定所有鼠标/触摸事件
|
||||
- 使用 `stopPropagation()` 阻止事件冒泡
|
||||
- 防止事件传递给 3D 引擎
|
||||
- 使用 `passive: false` 允许阻止默认行为
|
||||
|
||||
### 6.4 状态管理
|
||||
|
||||
#### 内部状态
|
||||
- `_isInitialized`: 是否已初始化
|
||||
- `_isDestroyed`: 是否已销毁
|
||||
- `rafId`: `requestAnimationFrame` 的 ID(用于节流)
|
||||
|
||||
#### 订阅管理
|
||||
- `unsubscribeTheme`: 主题订阅取消函数
|
||||
- `unsubscribeLocale`: 语言订阅取消函数
|
||||
|
||||
### 6.5 与其他组件的交互
|
||||
|
||||
- **与 DialogManager**: 通过 Manager 创建和管理
|
||||
- **与 ThemeManager**: 订阅主题变更
|
||||
- **与 LocaleManager**: 订阅语言变更
|
||||
- **与 3D 引擎**: 通过事件拦截防止交互冲突
|
||||
|
||||
---
|
||||
|
||||
## 7. 国际化支持
|
||||
|
||||
### 7.1 使用的翻译键
|
||||
|
||||
- `options.title`: 弹窗标题(如果提供,会通过 `t()` 函数翻译)
|
||||
|
||||
### 7.2 语言变更处理
|
||||
|
||||
- 组件订阅 `localeManager.subscribe()`
|
||||
- 语言变更时,`setLocales()` 方法被调用
|
||||
- 更新标题文本:`titleEl.textContent = t(this.options.title)`
|
||||
|
||||
### 7.3 实现细节
|
||||
|
||||
```typescript
|
||||
public setLocales(): void {
|
||||
if (this.options.title) {
|
||||
const titleEl = this.header.querySelector('.bim-dialog-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = t(this.options.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 主题支持
|
||||
|
||||
### 8.1 使用的主题变量
|
||||
|
||||
- `theme.panelBackground` → `--bim-dialog-bg`
|
||||
- `theme.componentHover` → `--bim-dialog-header-bg`
|
||||
- `theme.textPrimary` → `--bim-dialog-title-color` 和 `--bim-dialog-text-color`
|
||||
- `theme.border` → `--bim-dialog-border-color`
|
||||
|
||||
### 8.2 主题变更处理
|
||||
|
||||
- 组件订阅 `themeManager.subscribe()`
|
||||
- 主题变更时,`setTheme()` 方法被调用
|
||||
- 如果用户未自定义颜色,使用主题颜色
|
||||
- 如果用户已自定义颜色,保持用户自定义
|
||||
|
||||
### 8.3 实现细节
|
||||
|
||||
```typescript
|
||||
public setTheme(theme: ThemeConfig) {
|
||||
const style = this.element.style;
|
||||
if (!this.options.backgroundColor)
|
||||
style.setProperty('--bim-dialog-bg', theme.panelBackground);
|
||||
if (!this.options.headerBackgroundColor)
|
||||
style.setProperty('--bim-dialog-header-bg', theme.componentHover);
|
||||
// ... 其他颜色映射
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 使用示例
|
||||
|
||||
### 9.1 基本使用(通过 Manager)
|
||||
|
||||
```typescript
|
||||
import { BimEngine } from 'bim-engine-sdk';
|
||||
|
||||
const engine = new BimEngine('container');
|
||||
|
||||
// 创建简单弹窗
|
||||
const dialog = engine.dialog.create({
|
||||
title: 'dialog.testTitle', // 使用翻译键
|
||||
content: '这是内容',
|
||||
width: 400,
|
||||
height: 300
|
||||
});
|
||||
|
||||
// 创建可缩放弹窗
|
||||
const resizableDialog = engine.dialog.create({
|
||||
title: '可缩放弹窗',
|
||||
content: '<div>内容</div>',
|
||||
width: 500,
|
||||
height: 400,
|
||||
resizable: true,
|
||||
draggable: true
|
||||
});
|
||||
|
||||
// 创建自定义位置弹窗
|
||||
const positionedDialog = engine.dialog.create({
|
||||
title: '自定义位置',
|
||||
content: '内容',
|
||||
position: { x: 100, y: 100 },
|
||||
width: 300,
|
||||
height: 200
|
||||
});
|
||||
```
|
||||
|
||||
### 9.2 高级使用
|
||||
|
||||
```typescript
|
||||
// 创建带自定义样式的弹窗
|
||||
const styledDialog = engine.dialog.create({
|
||||
title: '自定义样式',
|
||||
content: '<div>内容</div>',
|
||||
backgroundColor: 'rgba(100, 0, 0, 0.95)',
|
||||
headerBackgroundColor: '#cc0000',
|
||||
titleColor: '#ffffff',
|
||||
textColor: '#ffcccc',
|
||||
borderColor: '#ff6666',
|
||||
width: 300,
|
||||
height: 200
|
||||
});
|
||||
|
||||
// 创建带回调的弹窗
|
||||
const callbackDialog = engine.dialog.create({
|
||||
title: '带回调',
|
||||
content: '内容',
|
||||
onOpen: () => {
|
||||
console.log('弹窗已打开');
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('弹窗已关闭');
|
||||
}
|
||||
});
|
||||
|
||||
// 动态更新内容
|
||||
const dynamicDialog = engine.dialog.create({
|
||||
title: '动态内容',
|
||||
content: '初始内容'
|
||||
});
|
||||
|
||||
// 稍后更新内容
|
||||
dynamicDialog.setContent('<div>新内容</div>');
|
||||
```
|
||||
|
||||
### 9.3 使用信息弹窗
|
||||
|
||||
```typescript
|
||||
// 通过 Manager 显示信息弹窗
|
||||
engine.dialog.showInfoDialog();
|
||||
|
||||
// 通过事件总线打开(如果配置了监听)
|
||||
engine.emit('ui:open-dialog', { id: 'info' });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 实现细节(供 AI 重现)
|
||||
|
||||
### 10.1 关键算法
|
||||
|
||||
#### 位置计算算法
|
||||
```typescript
|
||||
// 计算居中位置
|
||||
function calculateCenterPosition(containerWidth, containerHeight, dialogWidth, dialogHeight) {
|
||||
return {
|
||||
left: (containerWidth - dialogWidth) / 2,
|
||||
top: (containerHeight - dialogHeight) / 2
|
||||
};
|
||||
}
|
||||
|
||||
// 边界限制算法
|
||||
function clampPosition(left, top, containerWidth, containerHeight, dialogWidth, dialogHeight) {
|
||||
return {
|
||||
left: Math.max(0, Math.min(left, containerWidth - dialogWidth)),
|
||||
top: Math.max(0, Math.min(top, containerHeight - dialogHeight))
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 拖拽算法
|
||||
```typescript
|
||||
// 拖拽位置更新
|
||||
function updateDragPosition(startX, startY, currentX, currentY, startLeft, startTop) {
|
||||
const deltaX = currentX - startX;
|
||||
const deltaY = currentY - startY;
|
||||
return {
|
||||
left: startLeft + deltaX,
|
||||
top: startTop + deltaY
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### 缩放算法
|
||||
```typescript
|
||||
// 缩放尺寸更新
|
||||
function updateResizeSize(startX, startY, currentX, currentY, startWidth, startHeight, minWidth, minHeight) {
|
||||
const deltaX = currentX - startX;
|
||||
const deltaY = currentY - startY;
|
||||
return {
|
||||
width: Math.max(minWidth, startWidth + deltaX),
|
||||
height: Math.max(minHeight, startHeight + deltaY)
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 性能优化点
|
||||
|
||||
1. **requestAnimationFrame 节流**:
|
||||
- 拖拽和缩放使用 `requestAnimationFrame` 节流更新
|
||||
- 避免频繁的 DOM 操作
|
||||
|
||||
2. **尺寸缓存**:
|
||||
- 拖拽开始时缓存容器和弹窗尺寸
|
||||
- 减少 `getBoundingClientRect()` 调用
|
||||
|
||||
3. **事件捕获**:
|
||||
- 使用 `capture: true` 确保事件在捕获阶段处理
|
||||
- 即使内部元素阻止冒泡,也能捕获事件
|
||||
|
||||
4. **CSS 变量**:
|
||||
- 使用 CSS 变量应用主题
|
||||
- 主题变更时无需重新渲染 DOM
|
||||
|
||||
### 10.3 注意事项和边界情况
|
||||
|
||||
1. **容器尺寸为 0**:
|
||||
- 位置计算时可能出现除零或负数
|
||||
- 使用 `Math.max()` 确保位置不为负
|
||||
|
||||
2. **弹窗尺寸大于容器**:
|
||||
- 位置计算时确保弹窗不超出容器
|
||||
- 使用边界限制算法
|
||||
|
||||
3. **快速连续操作**:
|
||||
- 使用 `rafId` 防止多个动画帧同时执行
|
||||
- 确保动画帧正确清理
|
||||
|
||||
4. **事件清理**:
|
||||
- 组件销毁时必须清理所有事件监听
|
||||
- 必须取消主题和语言订阅
|
||||
|
||||
5. **事件拦截**:
|
||||
- 必须拦截所有鼠标/触摸事件
|
||||
- 防止事件传递给 3D 引擎
|
||||
|
||||
6. **翻译键处理**:
|
||||
- 标题可能是翻译键或普通文本
|
||||
- 只有翻译键才需要通过 `t()` 函数处理
|
||||
|
||||
---
|
||||
|
||||
## 11. 类型定义
|
||||
|
||||
### 11.1 DialogOptions
|
||||
|
||||
```typescript
|
||||
interface DialogOptions extends DialogColors {
|
||||
container: HTMLElement; // 弹窗挂载的父容器(必需)
|
||||
title?: string; // 弹窗标题(可选,支持翻译键)
|
||||
content?: HTMLElement | string; // 弹窗内容(可选)
|
||||
width?: number | string; // 宽度(可选,默认 300)
|
||||
height?: number | string; // 高度(可选,默认 'auto')
|
||||
position?: DialogPosition; // 位置(可选,默认 'center')
|
||||
draggable?: boolean; // 是否可拖拽(可选,默认 true)
|
||||
resizable?: boolean; // 是否可缩放(可选,默认 false)
|
||||
minWidth?: number; // 最小宽度(可选,默认 200)
|
||||
minHeight?: number; // 最小高度(可选,默认 100)
|
||||
onClose?: () => void; // 关闭回调(可选)
|
||||
onOpen?: () => void; // 打开回调(可选)
|
||||
id?: string; // 弹窗 ID(可选)
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 DialogPosition
|
||||
|
||||
```typescript
|
||||
type DialogPosition =
|
||||
| 'center'
|
||||
| 'top-left' | 'top-center' | 'top-right'
|
||||
| 'left-center' | 'right-center'
|
||||
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||
| { x: number; y: number };
|
||||
```
|
||||
|
||||
### 11.3 DialogColors
|
||||
|
||||
```typescript
|
||||
interface DialogColors {
|
||||
backgroundColor?: string; // 窗体背景颜色
|
||||
headerBackgroundColor?: string; // 标题栏背景颜色
|
||||
titleColor?: string; // 标题文字颜色
|
||||
textColor?: string; // 内容文字颜色
|
||||
borderColor?: string; // 边框颜色
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 文件清单
|
||||
|
||||
### 12.1 相关文件
|
||||
|
||||
- `src/components/dialog/index.ts` - 主组件类
|
||||
- `src/components/dialog/index.type.ts` - 类型定义
|
||||
- `src/components/dialog/index.css` - 样式文件
|
||||
- `src/components/dialog/bimInfoDialog/index.ts` - 信息弹窗(分化组件)
|
||||
- `src/components/dialog/bimInfoDialog/index.css` - 信息弹窗样式
|
||||
- `src/managers/dialog-manager.ts` - 管理器类
|
||||
|
||||
### 12.2 依赖文件
|
||||
|
||||
- `src/types/component.ts` - IBimComponent 接口
|
||||
- `src/themes/types.ts` - ThemeConfig 类型
|
||||
- `src/services/theme.ts` - ThemeManager
|
||||
- `src/services/locale.ts` - LocaleManager
|
||||
|
||||
---
|
||||
|
||||
## 13. 更新记录
|
||||
|
||||
| 日期 | 修改内容 | 修改人 |
|
||||
|------|---------|--------|
|
||||
| 2024-XX-XX | 初始创建 | AI Assistant |
|
||||
|
||||
---
|
||||
|
||||
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!
|
||||
|
||||
601
docs/components/engine.md
Normal file
601
docs/components/engine.md
Normal file
@@ -0,0 +1,601 @@
|
||||
# Engine 组件详细文档
|
||||
|
||||
> 本文档详细描述 Engine 组件的实现细节,包括 API、UI 结构、逻辑流程等,供 AI 根据文档重现组件。
|
||||
|
||||
---
|
||||
|
||||
## 1. 组件概述
|
||||
|
||||
### 1.1 基本信息
|
||||
- **组件名称**: `Engine`
|
||||
- **文件路径**: `src/components/engine/index.ts`
|
||||
- **类型定义**: `src/components/engine/types.ts`
|
||||
- **实现接口**: `IBimComponent`
|
||||
- **用途**: 封装第三方 3D 引擎 SDK,提供统一的 3D 引擎管理接口
|
||||
|
||||
### 1.2 在 SDK 中的位置
|
||||
- Engine 组件是独立的 3D 引擎组件
|
||||
- 必须通过 `EngineManager` 使用,不允许直接使用
|
||||
- `EngineManager` 位于 `src/managers/engine-manager.ts`
|
||||
- 采用延迟初始化模式,需要用户主动调用 `init()` 方法
|
||||
|
||||
### 1.3 第三方 SDK 依赖
|
||||
- 依赖 `src/bim-engine-sdk.es.js` 中的 `createEngine` 函数
|
||||
- 通过依赖注入方式使用,不直接导入
|
||||
|
||||
---
|
||||
|
||||
## 2. 组件类 API 文档
|
||||
|
||||
### 2.1 构造函数
|
||||
|
||||
```typescript
|
||||
constructor(options: EngineOptions)
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `options`: `EngineOptions` - 3D 引擎配置选项
|
||||
|
||||
**默认配置**:
|
||||
```typescript
|
||||
{
|
||||
backgroundColor: 0x1a1a1a, // 默认深色背景
|
||||
version: 'v1', // 默认使用 v1 版本
|
||||
showStats: false, // 默认不显示统计
|
||||
showViewCube: true // 默认显示视图立方体
|
||||
}
|
||||
```
|
||||
|
||||
**行为**:
|
||||
- 解析容器元素
|
||||
- 如果容器没有 id,生成一个唯一的 id
|
||||
- 保存配置选项(设置默认值)
|
||||
|
||||
### 2.2 公共方法
|
||||
|
||||
#### `init(): void`
|
||||
初始化组件(实现 `IBimComponent` 接口)
|
||||
|
||||
**功能**:
|
||||
- 检查是否已初始化或已销毁
|
||||
- 创建引擎配置对象
|
||||
- 调用 `createEngineSDK()` 创建引擎实例
|
||||
- 标记为已初始化
|
||||
- 订阅主题变化
|
||||
- 应用当前主题
|
||||
|
||||
**错误处理**:
|
||||
- 如果创建失败,抛出错误并标记为未初始化
|
||||
|
||||
#### `setTheme(theme: ThemeConfig): void`
|
||||
设置主题(实现 `IBimComponent` 接口)
|
||||
|
||||
**参数**:
|
||||
- `theme`: `ThemeConfig` - 全局主题配置
|
||||
|
||||
**功能**:
|
||||
- 根据主题名称选择背景色:
|
||||
- `dark`: 使用深色背景 `0x1a1a1a`
|
||||
- `light`: 使用浅色背景 `0xf5f5f5`
|
||||
- `custom`: 使用配置中的 `backgroundColor` 或默认值
|
||||
- 尝试更新引擎背景色:
|
||||
- 如果引擎有 `setBackgroundColor()` 方法,调用它
|
||||
- 否则,如果引擎有 `scene.background`,设置其颜色
|
||||
|
||||
#### `setLocales(): void`
|
||||
设置语言(实现 `IBimComponent` 接口)
|
||||
|
||||
**功能**:
|
||||
- 3D 引擎组件暂时不需要本地化
|
||||
- 方法为空实现
|
||||
|
||||
#### `isInitialized(): boolean`
|
||||
检查是否已初始化
|
||||
|
||||
**返回**: `boolean` - 是否已初始化
|
||||
|
||||
#### `loadModel(url: string, options?: ModelLoadOptions): void`
|
||||
加载 3D 模型
|
||||
|
||||
**参数**:
|
||||
- `url`: `string` - 模型文件 URL(必需)
|
||||
- `options`: `ModelLoadOptions` (可选) - 加载选项
|
||||
|
||||
**功能**:
|
||||
- 检查引擎是否已初始化
|
||||
- 检查 URL 是否提供
|
||||
- 调用引擎的 `loader.loadModel()` 方法加载模型
|
||||
|
||||
**加载选项**:
|
||||
- `position`: `[number, number, number]` - 模型位置 [x, y, z]
|
||||
- `rotation`: `[number, number, number]` - 模型旋转(弧度)[x, y, z]
|
||||
- `scale`: `[number, number, number]` - 模型缩放 [x, y, z]
|
||||
- `id`: `string` - 模型 ID(可选)
|
||||
|
||||
#### `getEngine(): any`
|
||||
获取原始 3D 引擎实例
|
||||
|
||||
**返回**: 第三方 3D 引擎实例或 `null`
|
||||
|
||||
**功能**:
|
||||
- 返回底层 3D 引擎实例
|
||||
- 用于直接调用第三方引擎的其他 API
|
||||
|
||||
#### `destroy(): void`
|
||||
销毁组件(实现 `IBimComponent` 接口)
|
||||
|
||||
**功能**:
|
||||
- 取消主题订阅
|
||||
- 清空容器内容
|
||||
- 标记为已销毁和未初始化
|
||||
|
||||
**注意**: 不销毁引擎实例本身(由第三方 SDK 管理)
|
||||
|
||||
---
|
||||
|
||||
## 3. 分化组件说明
|
||||
|
||||
**Engine 组件没有分化组件**。
|
||||
|
||||
---
|
||||
|
||||
## 4. Manager API 文档
|
||||
|
||||
### 4.1 EngineManager 类
|
||||
|
||||
**文件路径**: `src/managers/engine-manager.ts`
|
||||
|
||||
**继承关系**: `EngineManager extends BimComponent`
|
||||
|
||||
### 4.2 构造函数
|
||||
|
||||
```typescript
|
||||
constructor(engine: BimEngine, container: HTMLElement)
|
||||
```
|
||||
|
||||
**参数**:
|
||||
- `engine`: `BimEngine` - 引擎实例
|
||||
- `container`: `HTMLElement` - 3D 引擎挂载的目标容器
|
||||
|
||||
**行为**:
|
||||
- 保存容器引用
|
||||
- **不自动初始化引擎**(延迟初始化模式)
|
||||
|
||||
### 4.3 公共方法
|
||||
|
||||
#### `initialize(options?: Omit<EngineOptions, 'container'>): boolean`
|
||||
初始化 3D 引擎
|
||||
|
||||
**参数**:
|
||||
- `options`: `Omit<EngineOptions, 'container'>` (可选) - 引擎配置选项
|
||||
|
||||
**返回**: `boolean` - 是否初始化成功
|
||||
|
||||
**功能**:
|
||||
- 如果已经初始化,先销毁旧的实例
|
||||
- 创建 `Engine` 组件实例
|
||||
- 调用组件的 `init()` 方法初始化引擎
|
||||
- 返回初始化结果
|
||||
|
||||
**错误处理**:
|
||||
- 如果初始化失败,返回 `false` 并输出错误信息
|
||||
|
||||
#### `isInitialized(): boolean`
|
||||
检查 3D 引擎是否已初始化
|
||||
|
||||
**返回**: `boolean` - 是否已初始化
|
||||
|
||||
#### `loadModel(url: string, options?: ModelLoadOptions): void`
|
||||
加载 3D 模型
|
||||
|
||||
**参数**:
|
||||
- `url`: `string` - 模型文件 URL
|
||||
- `options`: `ModelLoadOptions` (可选) - 加载选项
|
||||
|
||||
**功能**:
|
||||
- 检查引擎是否已初始化
|
||||
- 如果未初始化,输出错误信息
|
||||
- 调用引擎组件的 `loadModel()` 方法
|
||||
|
||||
#### `getEngine(): any`
|
||||
获取原始 3D 引擎实例
|
||||
|
||||
**返回**: 第三方 3D 引擎实例或 `null`
|
||||
|
||||
**功能**:
|
||||
- 用于直接调用第三方引擎的其他 API
|
||||
- 如果引擎未初始化,返回 `null` 并输出警告
|
||||
|
||||
#### `destroy(): void`
|
||||
销毁 3D 引擎实例
|
||||
|
||||
**功能**:
|
||||
- 调用引擎组件的 `destroy()` 方法
|
||||
- 清空引擎实例引用
|
||||
|
||||
---
|
||||
|
||||
## 5. UI 详细描述
|
||||
|
||||
### 5.1 DOM 结构
|
||||
|
||||
**Engine 组件不直接创建 UI 元素**,而是:
|
||||
|
||||
1. **使用容器元素**:
|
||||
- 容器元素由外部提供(通过 `EngineOptions.container`)
|
||||
- 如果容器没有 id,组件会生成一个唯一的 id
|
||||
|
||||
2. **第三方 SDK 创建 UI**:
|
||||
- 第三方 SDK 的 `createEngine()` 函数会在容器内创建 3D 场景
|
||||
- 可能包括:
|
||||
- Canvas 元素(WebGL 渲染)
|
||||
- 统计面板(如果 `showStats: true`)
|
||||
- 视图立方体(如果 `showViewCube: true`)
|
||||
|
||||
### 5.2 容器 ID 生成
|
||||
|
||||
**算法**:
|
||||
```typescript
|
||||
containerId = `engine-container-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
```
|
||||
|
||||
**格式**: `engine-container-[时间戳]-[随机字符串]`
|
||||
|
||||
**示例**: `engine-container-1704067200000-k3j9x2m1p`
|
||||
|
||||
### 5.3 第三方 SDK 配置
|
||||
|
||||
**传递给 `createEngine()` 的配置对象**:
|
||||
```typescript
|
||||
{
|
||||
containerId: string, // 容器 ID
|
||||
backgroundColor: number, // 背景色(十六进制数字)
|
||||
version: 'v1' | 'v2', // WebGL 版本
|
||||
showStats: boolean, // 是否显示统计
|
||||
showViewCube: boolean // 是否显示视图立方体
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 逻辑流程详细描述
|
||||
|
||||
### 6.1 初始化流程
|
||||
|
||||
1. **构造函数调用**:
|
||||
- 解析容器元素
|
||||
- 生成或使用容器 ID
|
||||
- 保存配置选项(设置默认值)
|
||||
|
||||
2. **init() 方法执行**(用户主动调用):
|
||||
- 检查是否已初始化,如果是则返回
|
||||
- 检查是否已销毁,如果是则报错
|
||||
- 创建引擎配置对象
|
||||
- 调用 `createEngineSDK()` 创建引擎实例
|
||||
- 检查引擎实例是否创建成功
|
||||
- 标记为已初始化
|
||||
- 订阅主题变化:`themeManager.subscribe()`
|
||||
- 应用当前主题
|
||||
|
||||
### 6.2 生命周期
|
||||
|
||||
#### 创建阶段
|
||||
- 构造函数 → 保存配置
|
||||
|
||||
#### 初始化阶段(延迟)
|
||||
- 用户调用 `init()` → 创建引擎实例 → 订阅主题
|
||||
|
||||
#### 运行阶段
|
||||
- 响应主题变更:`setTheme()` 被调用
|
||||
- 加载模型:`loadModel()` 被调用
|
||||
- 用户交互:3D 场景交互(由第三方 SDK 处理)
|
||||
|
||||
#### 销毁阶段
|
||||
- `destroy()` 被调用
|
||||
- 取消主题订阅
|
||||
- 清空容器
|
||||
- 更新状态
|
||||
|
||||
### 6.3 主题变更流程
|
||||
|
||||
1. 主题变更 → `themeManager` 通知订阅者
|
||||
2. `setTheme()` 方法被调用
|
||||
3. 根据主题名称选择背景色:
|
||||
- `dark` → `0x1a1a1a`
|
||||
- `light` → `0xf5f5f5`
|
||||
- `custom` → 使用配置值或默认值
|
||||
4. 尝试更新引擎背景色:
|
||||
- 方法 1: 调用 `engine.setBackgroundColor(backgroundColor)`
|
||||
- 方法 2: 设置 `engine.scene.background.setHex(backgroundColor)`
|
||||
|
||||
### 6.4 模型加载流程
|
||||
|
||||
1. 用户调用 `loadModel(url, options)`
|
||||
2. 检查引擎是否已初始化
|
||||
3. 检查 URL 是否提供
|
||||
4. 调用 `engine.loader.loadModel(url, options)`
|
||||
5. 第三方 SDK 处理模型加载
|
||||
|
||||
### 6.5 状态管理
|
||||
|
||||
#### 内部状态
|
||||
- `_isInitialized`: 是否已初始化
|
||||
- `_isDestroyed`: 是否已销毁
|
||||
- `engine`: 第三方 3D 引擎实例
|
||||
- `containerId`: 容器 ID
|
||||
- `options`: 引擎配置选项
|
||||
|
||||
#### 订阅管理
|
||||
- `unsubscribeTheme`: 主题订阅取消函数
|
||||
|
||||
### 6.6 与其他组件的交互
|
||||
|
||||
- **与 EngineManager**: 通过 Manager 创建和管理
|
||||
- **与 ThemeManager**: 订阅主题变更
|
||||
- **与第三方 SDK**: 通过 `createEngine()` 函数创建引擎实例
|
||||
|
||||
---
|
||||
|
||||
## 7. 国际化支持
|
||||
|
||||
### 7.1 使用的翻译键
|
||||
|
||||
**Engine 组件不使用翻译键**(3D 引擎不需要国际化)。
|
||||
|
||||
### 7.2 语言变更处理
|
||||
|
||||
- `setLocales()` 方法为空实现
|
||||
- 不订阅语言变更
|
||||
|
||||
---
|
||||
|
||||
## 8. 主题支持
|
||||
|
||||
### 8.1 使用的主题变量
|
||||
|
||||
- 根据主题名称选择背景色:
|
||||
- `dark` 主题 → `0x1a1a1a`(深色背景)
|
||||
- `light` 主题 → `0xf5f5f5`(浅色背景)
|
||||
- `custom` 主题 → 使用配置值或默认值
|
||||
|
||||
### 8.2 主题变更处理
|
||||
|
||||
- 组件订阅 `themeManager.subscribe()`
|
||||
- 主题变更时,`setTheme()` 方法被调用
|
||||
- 根据主题名称选择背景色
|
||||
- 尝试更新引擎背景色(通过引擎 API)
|
||||
|
||||
### 8.3 实现细节
|
||||
|
||||
```typescript
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
let backgroundColor: number;
|
||||
if (theme.name === 'dark') {
|
||||
backgroundColor = 0x1a1a1a;
|
||||
} else if (theme.name === 'light') {
|
||||
backgroundColor = 0xf5f5f5;
|
||||
} else {
|
||||
backgroundColor = this.options.backgroundColor ?? 0x1a1a1a;
|
||||
}
|
||||
|
||||
// 尝试更新引擎背景色
|
||||
if (this.engine && typeof this.engine.setBackgroundColor === 'function') {
|
||||
this.engine.setBackgroundColor(backgroundColor);
|
||||
} else if (this.engine && this.engine.scene) {
|
||||
if (this.engine.scene.background) {
|
||||
this.engine.scene.background.setHex(backgroundColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 使用示例
|
||||
|
||||
### 9.1 基本使用(通过 EngineManager)
|
||||
|
||||
```typescript
|
||||
import { BimEngine } from 'bim-engine-sdk';
|
||||
|
||||
const engine = new BimEngine('container');
|
||||
|
||||
// 初始化 3D 引擎(延迟初始化)
|
||||
const success = engine.initEngine({
|
||||
backgroundColor: 0x333333,
|
||||
version: 'v1',
|
||||
showStats: true,
|
||||
showViewCube: true
|
||||
});
|
||||
|
||||
if (success) {
|
||||
console.log('3D 引擎初始化成功');
|
||||
}
|
||||
|
||||
// 加载模型
|
||||
engine.engine.loadModel('/model/building.glb', {
|
||||
position: [0, 0, 0],
|
||||
rotation: [0, 0, 0],
|
||||
scale: [1, 1, 1]
|
||||
});
|
||||
```
|
||||
|
||||
### 9.2 高级使用
|
||||
|
||||
```typescript
|
||||
// 检查引擎是否已初始化
|
||||
if (engine.engine.isInitialized()) {
|
||||
// 加载模型
|
||||
engine.engine.loadModel('/model/model.glb');
|
||||
}
|
||||
|
||||
// 获取原始引擎实例,调用第三方 API
|
||||
const rawEngine = engine.engine.getEngine();
|
||||
if (rawEngine) {
|
||||
// 调用第三方 SDK 的其他方法
|
||||
rawEngine.someOtherMethod();
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 主题切换
|
||||
|
||||
```typescript
|
||||
// 切换主题会自动更新 3D 场景背景色
|
||||
engine.setTheme('dark'); // 深色背景
|
||||
engine.setTheme('light'); // 浅色背景
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 实现细节(供 AI 重现)
|
||||
|
||||
### 10.1 关键算法
|
||||
|
||||
#### 容器 ID 生成算法
|
||||
```typescript
|
||||
function generateContainerId(): string {
|
||||
const timestamp = Date.now();
|
||||
const random = Math.random().toString(36).substr(2, 9);
|
||||
return `engine-container-${timestamp}-${random}`;
|
||||
}
|
||||
```
|
||||
|
||||
#### 主题背景色选择算法
|
||||
```typescript
|
||||
function selectBackgroundColor(theme: ThemeConfig, defaultColor: number): number {
|
||||
if (theme.name === 'dark') {
|
||||
return 0x1a1a1a; // 深色背景
|
||||
} else if (theme.name === 'light') {
|
||||
return 0xf5f5f5; // 浅色背景
|
||||
} else {
|
||||
return defaultColor; // 自定义主题,使用配置值
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 引擎背景色更新算法
|
||||
```typescript
|
||||
function updateEngineBackground(engine: any, color: number): void {
|
||||
// 方法 1: 如果引擎有 setBackgroundColor 方法
|
||||
if (engine && typeof engine.setBackgroundColor === 'function') {
|
||||
engine.setBackgroundColor(color);
|
||||
return;
|
||||
}
|
||||
|
||||
// 方法 2: 如果引擎有 scene.background
|
||||
if (engine && engine.scene && engine.scene.background) {
|
||||
engine.scene.background.setHex(color);
|
||||
return;
|
||||
}
|
||||
|
||||
// 方法 3: 无法更新(引擎可能不支持)
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 性能优化点
|
||||
|
||||
1. **延迟初始化**:
|
||||
- 引擎不自动初始化,由用户主动调用
|
||||
- 减少不必要的资源消耗
|
||||
|
||||
2. **状态检查**:
|
||||
- 初始化前检查状态,避免重复初始化
|
||||
- 销毁后禁止初始化,避免错误
|
||||
|
||||
3. **错误处理**:
|
||||
- 所有操作都有错误检查
|
||||
- 提供有意义的错误信息
|
||||
|
||||
### 10.3 注意事项和边界情况
|
||||
|
||||
1. **容器 ID 唯一性**:
|
||||
- 容器 ID 必须唯一
|
||||
- 如果容器已有 id,使用现有 id
|
||||
- 如果容器没有 id,生成唯一 id
|
||||
|
||||
2. **延迟初始化**:
|
||||
- 引擎不会自动初始化
|
||||
- 用户必须主动调用 `init()` 或 `initialize()`
|
||||
- 初始化前调用其他方法会报错
|
||||
|
||||
3. **引擎实例管理**:
|
||||
- 引擎实例由第三方 SDK 创建
|
||||
- 组件不负责销毁引擎实例本身
|
||||
- 只负责清理订阅和容器
|
||||
|
||||
4. **主题更新兼容性**:
|
||||
- 不同第三方 SDK 的 API 可能不同
|
||||
- 需要尝试多种方式更新背景色
|
||||
- 如果都不支持,静默失败
|
||||
|
||||
5. **模型加载**:
|
||||
- 模型 URL 必须是可访问的
|
||||
- 模型格式由第三方 SDK 支持
|
||||
- 加载选项由第三方 SDK 处理
|
||||
|
||||
6. **错误处理**:
|
||||
- 初始化失败时,标记为未初始化
|
||||
- 提供有意义的错误信息
|
||||
- 不抛出未捕获的异常
|
||||
|
||||
7. **第三方 SDK 依赖**:
|
||||
- 依赖 `bim-engine-sdk.es.js` 文件
|
||||
- 通过动态导入或全局变量访问
|
||||
- 需要确保 SDK 文件已加载
|
||||
|
||||
---
|
||||
|
||||
## 11. 类型定义
|
||||
|
||||
### 11.1 EngineOptions
|
||||
|
||||
```typescript
|
||||
interface EngineOptions {
|
||||
container: HTMLElement; // 容器元素(必需)
|
||||
backgroundColor?: number; // 背景颜色(可选,默认 0x1a1a1a)
|
||||
version?: 'v1' | 'v2'; // WebGL 版本(可选,默认 'v1')
|
||||
showStats?: boolean; // 是否显示性能统计(可选,默认 false)
|
||||
showViewCube?: boolean; // 是否显示视图立方体(可选,默认 true)
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 ModelLoadOptions
|
||||
|
||||
```typescript
|
||||
interface ModelLoadOptions {
|
||||
position?: [number, number, number]; // 模型位置 [x, y, z](可选)
|
||||
rotation?: [number, number, number]; // 模型旋转(弧度)[x, y, z](可选)
|
||||
scale?: [number, number, number]; // 模型缩放 [x, y, z](可选)
|
||||
id?: string; // 模型 ID(可选)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 文件清单
|
||||
|
||||
### 12.1 相关文件
|
||||
|
||||
- `src/components/engine/index.ts` - 主组件类
|
||||
- `src/components/engine/types.ts` - 类型定义
|
||||
- `src/managers/engine-manager.ts` - 管理器类
|
||||
- `src/bim-engine-sdk.es.js` - 第三方 SDK(依赖)
|
||||
|
||||
### 12.2 依赖文件
|
||||
|
||||
- `src/types/component.ts` - IBimComponent 接口
|
||||
- `src/themes/types.ts` - ThemeConfig 类型
|
||||
- `src/services/theme.ts` - ThemeManager
|
||||
|
||||
---
|
||||
|
||||
## 13. 更新记录
|
||||
|
||||
| 日期 | 修改内容 | 修改人 |
|
||||
|------|---------|--------|
|
||||
| 2024-XX-XX | 初始创建 | AI Assistant |
|
||||
|
||||
---
|
||||
|
||||
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!
|
||||
|
||||
66
docs/components/menu.md
Normal file
66
docs/components/menu.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Menu 组件文档
|
||||
|
||||
## 1. 组件概述
|
||||
|
||||
`BimMenu` 是一个通用的菜单列表组件。
|
||||
- **位置**: `src/components/menu/index.ts`
|
||||
- **功能**: 渲染一组 `BimMenuItem`,支持分组、排序、图标、快捷键提示和多级子菜单。
|
||||
- **特点**: 数据驱动(通过 Item 类实例),支持国际化。
|
||||
|
||||
## 2. 组件类 API
|
||||
|
||||
### `BimMenu`
|
||||
|
||||
#### 构造函数
|
||||
`new BimMenu(options: MenuOptions)`
|
||||
|
||||
#### 方法
|
||||
|
||||
| 方法名 | 参数 | 返回值 | 描述 |
|
||||
|:----|:---|:---|:---|
|
||||
| `init` | `()` | `void` | 初始化,渲染 DOM |
|
||||
| `destroy` | `()` | `void` | 销毁,清理事件和子菜单 |
|
||||
| `getElement` | `()` | `HTMLElement` | 获取根 DOM 元素 (实现 IRightKeyContent) |
|
||||
|
||||
### `BimMenuItem` (基类)
|
||||
|
||||
所有菜单项必须继承此抽象类。
|
||||
|
||||
| 方法/属性 | 类型 | 描述 |
|
||||
|:----|:---|:---|
|
||||
| `id` | `string` | 唯一标识 |
|
||||
| `group` | `string` | 分组 ID (默认 'default') |
|
||||
| `order` | `number` | 排序权重 |
|
||||
| `getLabel()` | `string` | 获取显示文本 |
|
||||
| `onClick()` | `void` | 点击回调 |
|
||||
| `getIcon()` | `string?` | 获取图标 HTML |
|
||||
| `getChildren()` | `BimMenuItem[]?` | 获取子菜单项 |
|
||||
|
||||
## 3. UI 详细描述
|
||||
|
||||
- **根元素**: `<ul class="bim-menu">`
|
||||
- **分组分割线**: `<li class="bim-menu-divider">`
|
||||
- **菜单项**: `<li class="bim-menu-item">`
|
||||
- 图标槽: `.bim-menu-item-icon`
|
||||
- 文本槽: `.bim-menu-item-label`
|
||||
- 箭头槽: `.bim-menu-item-arrow` (仅当有子菜单时)
|
||||
|
||||
## 4. 逻辑流程
|
||||
|
||||
1. **分组与排序**:
|
||||
- 根据 `item.group` 将项归类。
|
||||
- 根据 `options.groupOrder` 对组进行排序。
|
||||
- 组内根据 `item.order` 排序。
|
||||
2. **多级菜单**:
|
||||
- 鼠标进入 (`mouseenter`) 带子菜单的项时,创建新的 `BimMenu` 实例。
|
||||
- 将新实例挂载到临时的 fixed 容器中。
|
||||
- 自动计算位置(通常在父项右侧,空间不足时翻转)。
|
||||
|
||||
## 5. 类型定义
|
||||
|
||||
```typescript
|
||||
export interface MenuOptions {
|
||||
items: BimMenuItem[];
|
||||
groupOrder?: string[];
|
||||
}
|
||||
```
|
||||
58
docs/components/right-key.md
Normal file
58
docs/components/right-key.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# RightKey 组件文档
|
||||
|
||||
## 1. 组件概述
|
||||
|
||||
`BimRightKey` 是一个通用的右键浮层容器组件。
|
||||
- **位置**: `src/components/right-key/index.ts`
|
||||
- **功能**: 负责在指定坐标 (x, y) 显示内容,处理边界检测防止溢出屏幕,以及点击外部自动关闭。
|
||||
- **特点**: 它不关心内容是什么,通过 `mount()` 方法挂载任意实现 `IRightKeyContent` 接口的内容组件。
|
||||
|
||||
## 2. 组件类 API
|
||||
|
||||
### `BimRightKey`
|
||||
|
||||
#### 方法
|
||||
|
||||
| 方法名 | 参数 | 返回值 | 描述 |
|
||||
|:----|:---|:---|:---|
|
||||
| `init` | `()` | `void` | 初始化,绑定全局点击事件 |
|
||||
| `mount` | `(content: IRightKeyContent)` | `void` | 挂载内容组件 |
|
||||
| `show` | `(x: number, y: number)` | `void` | <20><><EFBFBD>指定位置显示,会自动调整位置防止溢出 |
|
||||
| `hide` | `()` | `void` | 隐藏容器 |
|
||||
| `destroy` | `()` | `void` | 销毁容器,解绑事件 |
|
||||
| `setOnClose` | `(callback: () => void)` | `void` | 设置关闭时的回调 |
|
||||
|
||||
## 3. Manager API
|
||||
|
||||
### `RightKeyManager`
|
||||
|
||||
- **位置**: `src/managers/right-key-manager.ts`
|
||||
- **功能**: 监听容器的 `contextmenu` 事件,决定显示什么内容。
|
||||
|
||||
#### 方法
|
||||
|
||||
| 方法名 | 参数 | 返回值 | 描述 |
|
||||
|:----|:---|:---|:---|
|
||||
| `registerHandler` | `(handler: (e) => BimMenuItem[])` | `void` | 注册上下文处理器 |
|
||||
| `showMenu` | `(x, y, items, groupOrder?)` | `void` | 手动显示菜单 |
|
||||
| `hide` | `()` | `void` | 隐藏 |
|
||||
|
||||
## 4. UI 详细描述
|
||||
|
||||
- **容器**: `<div class="bim-right-key"></div>`
|
||||
- **定位**: `position: fixed`
|
||||
- **层级**: `z-index: 10000` (默认)
|
||||
|
||||
## 5. 实现细节
|
||||
|
||||
- **边界检测**: 在 `show(x, y)` 时,会计算容器宽高和视口宽高。如果 `x + width > viewportWidth`,则 `x = x - width`(向左展开)。同理处理垂直方向。
|
||||
- **点击外部关闭**: 监听 `document` 的 `mousedown` 事件,如果点击目标不在容器内,则调用 `hide()`。
|
||||
|
||||
## 6. 类型定义
|
||||
|
||||
```typescript
|
||||
export interface IRightKeyContent {
|
||||
getElement(): HTMLElement;
|
||||
destroy(): void;
|
||||
}
|
||||
```
|
||||
@@ -18,8 +18,10 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"dev:demo": "cd demo && npm run dev",
|
||||
"dev:demo-vue": "cd demo-vue && npm run dev",
|
||||
"copy:demo": "mkdir -p demo/lib && cp dist/bim-engine-sdk.es.js dist/bim-engine-sdk.umd.js dist/bim-engine-sdk.umd.js.map demo/lib/",
|
||||
"copy:demo-vue": "mkdir -p demo-vue/public/lib && cp dist/bim-engine-sdk.es.js dist/bim-engine-sdk.umd.js dist/bim-engine-sdk.umd.js.map demo-vue/public/lib/",
|
||||
"dev:demo": "npm run build && npm run copy:demo && cd demo && npm run dev",
|
||||
"dev:demo-vue": "npm run build && npm run copy:demo-vue && cd demo-vue && npm run dev",
|
||||
"dev:all": "npm run dev:demo & npm run dev:demo-vue"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ToolbarManager } from './managers/toolbar-manager';
|
||||
import { ButtonGroupManager } from './managers/button-group-manager';
|
||||
import { DialogManager } from './managers/dialog-manager';
|
||||
import { EngineManager } from './managers/engine-manager';
|
||||
import { RightKeyManager } from './managers/right-key-manager';
|
||||
import type { EngineOptions, ModelLoadOptions } from './components/engine';
|
||||
import { localeManager } from './services/locale';
|
||||
import { themeManager } from './services/theme';
|
||||
@@ -22,6 +23,7 @@ export class BimEngine extends EventEmitter {
|
||||
public buttonGroup: ButtonGroupManager | null = null; // 通用
|
||||
public dialog: DialogManager | null = null;
|
||||
public engine: EngineManager | null = null; // 3D 引擎管理器
|
||||
public rightKey: RightKeyManager | null = null; // 右键菜单管理器
|
||||
|
||||
public get localeManager() { return localeManager; }
|
||||
public get themeManager() { return themeManager; }
|
||||
@@ -77,6 +79,7 @@ export class BimEngine extends EventEmitter {
|
||||
this.dialog = new DialogManager(this, this.wrapper);
|
||||
this.toolbar = new ToolbarManager(this, this.wrapper);
|
||||
this.buttonGroup = new ButtonGroupManager(this, this.wrapper);
|
||||
this.rightKey = new RightKeyManager(this, this.wrapper);
|
||||
|
||||
|
||||
// 初始主题
|
||||
@@ -120,6 +123,7 @@ export class BimEngine extends EventEmitter {
|
||||
this.toolbar?.destroy();
|
||||
this.buttonGroup?.destroy();
|
||||
this.engine?.destroy();
|
||||
this.rightKey?.destroy();
|
||||
this.dialog = null;
|
||||
this.container.innerHTML = '';
|
||||
this.clear(); // Clear all events
|
||||
|
||||
15
src/components/menu/buttons/four.ts
Normal file
15
src/components/menu/buttons/four.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { BimEngine } from "../../../bim-engine";
|
||||
import { MenuItemConfig } from "../item";
|
||||
|
||||
export const fourMenuButton = (engine: BimEngine): MenuItemConfig => {
|
||||
return {
|
||||
id: "fourMenu",
|
||||
label: "menu.info",
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
|
||||
onClick: () => {
|
||||
console.log('dianjile')
|
||||
engine.dialog?.showInfoDialog()
|
||||
engine.rightKey?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/components/menu/buttons/home.ts
Normal file
18
src/components/menu/buttons/home.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { BimEngine } from "../../../bim-engine";
|
||||
import { MenuItemConfig } from "../item";
|
||||
import { fourMenuButton } from "./four";
|
||||
import { secondMenuButton } from "./second";
|
||||
|
||||
export const homeMenuButton = (engine: BimEngine): MenuItemConfig => {
|
||||
return {
|
||||
id: "homeMenu",
|
||||
label: "menu.home",
|
||||
group: 'home',
|
||||
children: [secondMenuButton(engine), fourMenuButton(engine)],
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
|
||||
onClick: () => {
|
||||
engine.dialog?.showInfoDialog()
|
||||
engine.rightKey?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/components/menu/buttons/info.ts
Normal file
16
src/components/menu/buttons/info.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { BimEngine } from "../../../bim-engine";
|
||||
import { MenuItemConfig } from "../item";
|
||||
|
||||
export const infoMenuButton = (engine: BimEngine): MenuItemConfig => {
|
||||
return {
|
||||
id: "infoMenu",
|
||||
label: "menu.info",
|
||||
group: 'info',
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
|
||||
onClick: () => {
|
||||
console.log('dianjile')
|
||||
engine.dialog?.showInfoDialog()
|
||||
engine.rightKey?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/components/menu/buttons/second.ts
Normal file
15
src/components/menu/buttons/second.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { BimEngine } from "../../../bim-engine";
|
||||
import { MenuItemConfig } from "../item";
|
||||
|
||||
export const secondMenuButton = (engine: BimEngine): MenuItemConfig => {
|
||||
return {
|
||||
id: "infoMenu",
|
||||
label: "menu.info",
|
||||
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
|
||||
onClick: () => {
|
||||
console.log('dianjile')
|
||||
engine.dialog?.showInfoDialog()
|
||||
engine.rightKey?.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/components/menu/index.css
Normal file
83
src/components/menu/index.css
Normal file
@@ -0,0 +1,83 @@
|
||||
.bim-menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bim-ui_bg_color, #2b2d30);
|
||||
border-radius: 4px;
|
||||
padding: 4px 0;
|
||||
min-width: 160px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
user-select: none;
|
||||
color: var(--bim-ui_text_primary, #ffffff);
|
||||
}
|
||||
|
||||
.bim-menu-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.bim-menu-divider {
|
||||
height: 1px;
|
||||
background-color: var(--bim-ui_border_color, #3e4145);
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.bim-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
color: var(--bim-ui_text_primary, #ffffff);
|
||||
}
|
||||
|
||||
.bim-menu-item:hover {
|
||||
background-color: var(--bim-ui_bg_hover, #3e4145);
|
||||
}
|
||||
|
||||
.bim-menu-item.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.bim-menu-item-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bim-menu-item-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.bim-menu-item-label {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bim-menu-item-arrow {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.bim-menu-item-arrow svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
274
src/components/menu/index.ts
Normal file
274
src/components/menu/index.ts
Normal file
@@ -0,0 +1,274 @@
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { ThemeConfig } from '../../themes/types';
|
||||
import { localeManager, t } from '../../services/locale';
|
||||
import { MenuItemConfig } from './item';
|
||||
import { MenuOptions } from './types';
|
||||
import './index.css';
|
||||
import { themeManager } from '../../services/theme';
|
||||
|
||||
/**
|
||||
* 通用菜单列表组件
|
||||
* 负责渲染一组菜单项,支持分组、排序、图标、快捷键提示和递归多级子菜单。
|
||||
* 它不包含定位逻辑,仅负责内容渲染。
|
||||
*/
|
||||
export class BimMenu implements IBimComponent {
|
||||
public element: HTMLElement;
|
||||
private options: MenuOptions;
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
private unsubscribeTheme: (() => void) | null = null;
|
||||
|
||||
// 当前激活的子菜单引用,用于自动关闭
|
||||
private activeSubMenu: { menu: BimMenu; container: HTMLElement } | null = null;
|
||||
|
||||
constructor(options: MenuOptions) {
|
||||
this.options = options;
|
||||
this.element = document.createElement('ul');
|
||||
this.element.className = 'bim-menu';
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件
|
||||
* 渲染 DOM 结构并订阅语言变更
|
||||
*/
|
||||
public init(): void {
|
||||
this.render();
|
||||
|
||||
// 订阅语言变更事件,实现国际化自动更新
|
||||
this.unsubscribeLocale = localeManager.subscribe(() => {
|
||||
this.setLocales();
|
||||
});
|
||||
// 自动订阅主题变更
|
||||
this.unsubscribeTheme = themeManager.subscribe((theme) => {
|
||||
this.setTheme(theme);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
* @param theme 全局主题配置
|
||||
*/
|
||||
public setTheme(theme: ThemeConfig) {
|
||||
const style = this.element.style;
|
||||
style.setProperty('--bim-ui_bg_color', theme.panelBackground);
|
||||
style.setProperty('--bim-ui_text_primary', theme.textPrimary);
|
||||
style.setProperty('--bim-ui_border_color', theme.border);
|
||||
style.setProperty('--bim-ui_bg_hover', theme.componentHover);
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应语言变更
|
||||
* 重新渲染整个菜单以更新文本
|
||||
*/
|
||||
public setLocales(): void {
|
||||
this.element.innerHTML = '';
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁组件
|
||||
* 清理事件监听、子菜单和 DOM 元素
|
||||
*/
|
||||
public destroy(): void {
|
||||
// 取消语言订阅
|
||||
if (this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale();
|
||||
this.unsubscribeLocale = null;
|
||||
}
|
||||
if (this.unsubscribeTheme) {
|
||||
this.unsubscribeTheme();
|
||||
this.unsubscribeTheme = null;
|
||||
}
|
||||
// 关闭并销毁所有打开的子菜单
|
||||
this.closeSubMenu();
|
||||
// 移除自身 DOM
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取组件根元素
|
||||
* 实现 IRightKeyContent 接口,允许被 RightKey 容器挂载
|
||||
*/
|
||||
public getElement(): HTMLElement {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心渲染逻辑
|
||||
* 处理分组、排序和 DOM 生成
|
||||
*/
|
||||
private render(): void {
|
||||
const { items, groupOrder } = this.options;
|
||||
|
||||
// 1. 数据分桶:按 group 字段将菜单项分组
|
||||
const groups = new Map<string, MenuItemConfig[]>();
|
||||
const defaultGroup = 'default';
|
||||
|
||||
items.forEach(item => {
|
||||
const groupName = item.group || defaultGroup;
|
||||
if (!groups.has(groupName)) {
|
||||
groups.set(groupName, []);
|
||||
}
|
||||
groups.get(groupName)!.push(item);
|
||||
});
|
||||
|
||||
// 2. 确定分组顺序
|
||||
let sortedGroupKeys: string[] = [];
|
||||
if (groupOrder) {
|
||||
// 优先按照 groupOrder 指定的顺序排序
|
||||
sortedGroupKeys = groupOrder.filter(g => groups.has(g));
|
||||
// 将未在 groupOrder 中定义的组追加到最后
|
||||
for (const key of groups.keys()) {
|
||||
if (!sortedGroupKeys.includes(key)) {
|
||||
sortedGroupKeys.push(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果未指定顺序,则按默认遍历顺序
|
||||
sortedGroupKeys = Array.from(groups.keys());
|
||||
}
|
||||
|
||||
// 3. 渲染分组和组内项
|
||||
sortedGroupKeys.forEach((groupName, index) => {
|
||||
// 除了第一组外,每组之前插入分割线
|
||||
if (index > 0) {
|
||||
const divider = document.createElement('li');
|
||||
divider.className = 'bim-menu-divider';
|
||||
this.element.appendChild(divider);
|
||||
}
|
||||
|
||||
const groupItems = groups.get(groupName)!;
|
||||
// 组内排序:根据 item.order 升序排列
|
||||
groupItems.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
|
||||
groupItems.forEach(item => {
|
||||
// 仅渲染可见的项
|
||||
if (item.visible !== false) {
|
||||
this.element.appendChild(this.createItemElement(item));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单个菜单项的 DOM 元素
|
||||
*/
|
||||
private createItemElement(item: MenuItemConfig): HTMLElement {
|
||||
const li = document.createElement('li');
|
||||
// 根据状态设置样式类
|
||||
const isEnabled = !item.disabled;
|
||||
li.className = `bim-menu-item ${isEnabled ? '' : 'disabled'}`;
|
||||
|
||||
// 1. 图标区域 (Icon Slot)
|
||||
const iconDiv = document.createElement('div');
|
||||
iconDiv.className = 'bim-menu-item-icon';
|
||||
if (item.icon) {
|
||||
iconDiv.innerHTML = item.icon;
|
||||
}
|
||||
li.appendChild(iconDiv);
|
||||
|
||||
// 2. 文本区域 (Label Slot)
|
||||
const labelDiv = document.createElement('div');
|
||||
labelDiv.className = 'bim-menu-item-label';
|
||||
// 获取翻译后的文本
|
||||
labelDiv.textContent = t(item.label);
|
||||
li.appendChild(labelDiv);
|
||||
|
||||
// 3. 子菜单指示器 (Arrow Slot)
|
||||
const children = item.children;
|
||||
const hasChildren = children && children.length > 0;
|
||||
|
||||
if (hasChildren) {
|
||||
const arrowDiv = document.createElement('div');
|
||||
arrowDiv.className = 'bim-menu-item-arrow';
|
||||
// 简单的右箭头 SVG
|
||||
arrowDiv.innerHTML = '<svg viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>';
|
||||
li.appendChild(arrowDiv);
|
||||
|
||||
// 绑定子菜单交互事件
|
||||
// 鼠标移入:打开子菜单
|
||||
li.addEventListener('mouseenter', () => this.openSubMenu(item, li));
|
||||
} else {
|
||||
// 鼠标移入普通项:关闭当前已打开的子菜单
|
||||
li.addEventListener('mouseenter', () => this.closeSubMenu());
|
||||
}
|
||||
|
||||
// 4. 绑定点击事件
|
||||
if (isEnabled) {
|
||||
// Debug Log: 检查是否绑定了事件
|
||||
// console.log(`[BimMenu] Binding click for ${item.id}, hasChildren: ${hasChildren}, hasOnClick: ${!!item.onClick}`);
|
||||
|
||||
li.addEventListener('click', (e) => {
|
||||
e.stopPropagation(); // 防止冒泡
|
||||
console.log(`[BimMenu] Clicked item: ${item.id}`);
|
||||
|
||||
// 如果是叶子节点(没有子菜单),则触发点击动作
|
||||
if (!hasChildren) {
|
||||
if (item.onClick) {
|
||||
console.log(`[BimMenu] Executing onClick for ${item.id}`);
|
||||
item.onClick();
|
||||
} else {
|
||||
console.warn(`[BimMenu] No onClick handler for ${item.id}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开子菜单
|
||||
* @param item 当前菜单项
|
||||
* @param parentLi 触发的 DOM 元素(用于定位)
|
||||
*/
|
||||
private openSubMenu(item: MenuItemConfig, parentLi: HTMLElement): void {
|
||||
const children = item.children;
|
||||
if (!children || children.length === 0) return;
|
||||
|
||||
// 如果当前已经打开了子菜单,先关闭它
|
||||
this.closeSubMenu();
|
||||
|
||||
// 创建子菜单容器 (模拟一个临时的悬浮层)
|
||||
const container = document.createElement('div');
|
||||
container.style.position = 'fixed';
|
||||
container.style.zIndex = '10001'; // 确保比父菜单层级高
|
||||
|
||||
// 初步计算位置:位于父项右侧
|
||||
const rect = parentLi.getBoundingClientRect();
|
||||
container.style.top = `${rect.top}px`;
|
||||
container.style.left = `${rect.right}px`;
|
||||
|
||||
// 关键修复:阻止 mousedown 冒泡
|
||||
// 防止点击子菜单时触发 BimRightKey 的全局关闭逻辑(因为它认为点击发生在主菜单外部)
|
||||
container.addEventListener('mousedown', (e) => e.stopPropagation());
|
||||
|
||||
// 递归创建新的 BimMenu 实例
|
||||
const subMenu = new BimMenu({ items: children });
|
||||
subMenu.init();
|
||||
container.appendChild(subMenu.element);
|
||||
document.body.appendChild(container);
|
||||
|
||||
// 保存引用以便后续清理
|
||||
this.activeSubMenu = { menu: subMenu, container };
|
||||
|
||||
// 边界检测:如果超出屏幕右侧,则向左展开
|
||||
const subRect = container.getBoundingClientRect();
|
||||
if (subRect.right > window.innerWidth) {
|
||||
container.style.left = `${rect.left - subRect.width}px`;
|
||||
}
|
||||
// TODO: 垂直方向边界检测
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭当前激活的子菜单
|
||||
*/
|
||||
private closeSubMenu(): void {
|
||||
if (this.activeSubMenu) {
|
||||
this.activeSubMenu.menu.destroy();
|
||||
this.activeSubMenu.container.remove();
|
||||
this.activeSubMenu = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
14
src/components/menu/item.ts
Normal file
14
src/components/menu/item.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 菜单项配置接口 (用于简化的对象配置)
|
||||
*/
|
||||
export interface MenuItemConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
icon?: string;
|
||||
group?: string;
|
||||
order?: number;
|
||||
children?: MenuItemConfig[];
|
||||
disabled?: boolean;
|
||||
visible?: boolean;
|
||||
}
|
||||
38
src/components/menu/types.ts
Normal file
38
src/components/menu/types.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { MenuItemConfig } from './item';
|
||||
|
||||
/**
|
||||
* <20><><EFBFBD>单组件配置选项
|
||||
*/
|
||||
export interface MenuOptions {
|
||||
/**
|
||||
* 菜单项列表
|
||||
* 可以是扁平数组,组件会根据 group 字段自动分组
|
||||
*/
|
||||
items: MenuItemConfig[];
|
||||
|
||||
/**
|
||||
* 分组显示顺序
|
||||
* 包含组 ID 的字符串数组。
|
||||
* 例如: ['view', 'edit', 'tools']
|
||||
* 未在此数组中定义的组将按默认顺序排在最后。
|
||||
*/
|
||||
groupOrder?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 右键容器内容接口
|
||||
* 任何想要挂载到 RightKey 容器中的组件都必须实现此接口
|
||||
*/
|
||||
export interface IRightKeyContent {
|
||||
/**
|
||||
* 获取组件的根 DOM 元素
|
||||
* 容器将把此元素 append 到自身内部
|
||||
*/
|
||||
getElement(): HTMLElement;
|
||||
|
||||
/**
|
||||
* 销毁组件
|
||||
* 容器在关闭或切换内容时会调用此方法,组件应在此清理资源
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
11
src/components/right-key/index.css
Normal file
11
src/components/right-key/index.css
Normal file
@@ -0,0 +1,11 @@
|
||||
.bim-right-key {
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
display: none;
|
||||
background: transparent;
|
||||
/* Container styles if needed, but usually the content provides the visual box */
|
||||
}
|
||||
|
||||
.bim-right-key.visible {
|
||||
display: block;
|
||||
}
|
||||
156
src/components/right-key/index.ts
Normal file
156
src/components/right-key/index.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { ThemeConfig } from '../../themes/types';
|
||||
import { IRightKeyContent, RightKeyOptions } from './types';
|
||||
import './index.css';
|
||||
|
||||
/**
|
||||
* 右键浮层容器组件 (RightKey)
|
||||
* 这是一个纯粹的定位容器,负责在屏幕指定位置显示内容。
|
||||
* 它不关心具体内容是什么,只处理定位、边界检测和关闭逻辑。
|
||||
*/
|
||||
export class BimRightKey implements IBimComponent {
|
||||
private element: HTMLElement;
|
||||
private content: IRightKeyContent | null = null;
|
||||
private isVisible: boolean = false;
|
||||
private onCloseCallback?: () => void;
|
||||
|
||||
constructor(options?: RightKeyOptions) {
|
||||
this.element = document.createElement('div');
|
||||
this.element.className = `bim-right-key ${options?.className || ''}`;
|
||||
|
||||
// 设置层级,默认很高以覆盖其他 UI
|
||||
if (options?.zIndex) {
|
||||
this.element.style.zIndex = options.zIndex.toString();
|
||||
}
|
||||
|
||||
// 挂载到 body 以便进行固定定位
|
||||
document.body.appendChild(this.element);
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
// 绑定全局点击事件,用于实现"点击外部关闭"
|
||||
document.addEventListener('mousedown', this.handleGlobalClick);
|
||||
|
||||
// 阻止在容器自身上触发系统默认右键菜单
|
||||
this.element.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
public setTheme(_theme: ThemeConfig): void {
|
||||
// 容器本身通常是透明的,主题样式主要由内容组件处理
|
||||
// 如果容器需要背景色,可以在这里设置
|
||||
// 使用 _theme 前缀避免 TS 未使用变量报错
|
||||
}
|
||||
|
||||
public setLocales(): void {
|
||||
// 容器不包含文本,无需处理国际化
|
||||
// 内容组件的国际化由内容组件自身处理
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
document.removeEventListener('mousedown', this.handleGlobalClick);
|
||||
this.unmountContent();
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置关闭时的回调函数
|
||||
* 通常用于通知 Manager 状态变更
|
||||
*/
|
||||
public setOnClose(callback: () => void): void {
|
||||
this.onCloseCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 挂载内容组件
|
||||
* @param content 实现了 IRightKeyContent 接口的组件实例
|
||||
*/
|
||||
public mount(content: IRightKeyContent): void {
|
||||
// 先卸载旧内容,防止内存泄漏
|
||||
this.unmountContent();
|
||||
|
||||
this.content = content;
|
||||
this.element.appendChild(content.getElement());
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载当前内容
|
||||
*/
|
||||
public unmountContent(): void {
|
||||
if (this.content) {
|
||||
this.content.destroy(); // 重要:调用组件销毁方法清理资源
|
||||
this.element.innerHTML = '';
|
||||
this.content = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定位置显示容器
|
||||
* 包含智能边界检测逻辑,防止溢出屏幕
|
||||
* @param x 目标 X 坐标 (通常是鼠标点击位置)
|
||||
* @param y 目标 Y 坐标
|
||||
*/
|
||||
public show(x: number, y: number): void {
|
||||
this.element.classList.add('visible');
|
||||
this.isVisible = true;
|
||||
|
||||
// 1. 先定位到目标位置,以便测量尺寸
|
||||
this.element.style.left = `${x}px`;
|
||||
this.element.style.top = `${y}px`;
|
||||
|
||||
// 2. 获取容器<E5AEB9><E599A8><EFBFBD>寸和视口尺寸
|
||||
const rect = this.element.getBoundingClientRect();
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
let newX = x;
|
||||
let newY = y;
|
||||
|
||||
// 3. 水平方向边界检测:如果溢出右边界,则向左对齐
|
||||
if (x + rect.width > viewportWidth) {
|
||||
newX = x - rect.width;
|
||||
}
|
||||
|
||||
// 4. 垂直方向边界检测:如果溢出下边界,则向上对齐
|
||||
if (y + rect.height > viewportHeight) {
|
||||
newY = y - rect.height;
|
||||
}
|
||||
|
||||
// 5. 应用修正后的坐标
|
||||
this.element.style.left = `${newX}px`;
|
||||
this.element.style.top = `${newY}px`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏容器
|
||||
*/
|
||||
public hide(): void {
|
||||
this.element.classList.remove('visible');
|
||||
this.isVisible = false;
|
||||
|
||||
// 为了状态重置,通常隐藏时也卸载内容
|
||||
this.unmountContent();
|
||||
|
||||
if (this.onCloseCallback) {
|
||||
this.onCloseCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理全局点击事件
|
||||
* 用于检测是否点击了容器外部
|
||||
*/
|
||||
private handleGlobalClick = (e: MouseEvent): void => {
|
||||
if (!this.isVisible) return;
|
||||
|
||||
// 如果点击的是容器内部,不做处理
|
||||
if (this.element.contains(e.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 点击外部,关闭容器
|
||||
this.hide();
|
||||
};
|
||||
}
|
||||
18
src/components/right-key/types.ts
Normal file
18
src/components/right-key/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface IRightKeyContent {
|
||||
/**
|
||||
* 获取组件的根 DOM 元素
|
||||
*/
|
||||
getElement(): HTMLElement;
|
||||
|
||||
/**
|
||||
* 销毁组件
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export interface RightKeyOptions {
|
||||
/** 自定义 CSS 类名 */
|
||||
className?: string;
|
||||
/** 层级 (z-index) */
|
||||
zIndex?: number;
|
||||
}
|
||||
34
src/index.ts
34
src/index.ts
@@ -7,39 +7,15 @@ export { Toolbar } from './components/button-group/toolbar';
|
||||
// 导出相关类型定义
|
||||
export type { OptButton, ButtonGroup, ButtonGroupOptions, ClickPayload } from './components/button-group/index.type';
|
||||
|
||||
// 导出 RightKey/Menu 组件
|
||||
export type { MenuItemConfig } from './components/menu/item';
|
||||
export { BimMenu } from './components/menu';
|
||||
export { BimRightKey } from './components/right-key';
|
||||
|
||||
// 导出主引擎类
|
||||
export { BimEngine };
|
||||
|
||||
// 导出 3D 引擎相关类型
|
||||
export type { EngineOptions, ModelLoadOptions } from './components/engine';
|
||||
|
||||
// 导出 createEngine 函数(从第三方 SDK 重新导出)
|
||||
// 注意:createEngine 的实际实现来自 bim-engine-sdk.es.js
|
||||
//
|
||||
// 使用方式:
|
||||
// 1. 直接从 SDK 文件导入(推荐,如 Vue 示例):
|
||||
// import { createEngine } from '/engine/bim-engine-sdk.es.js';
|
||||
//
|
||||
// 2. 从主入口导入(如果构建配置支持):
|
||||
// import { createEngine } from 'bim-engine-sdk';
|
||||
//
|
||||
// 示例:
|
||||
// ```javascript
|
||||
// const engine = createEngine({
|
||||
// containerId: 'vue2-container',
|
||||
// backgroundColor: 0x333333,
|
||||
// version: 'v1',
|
||||
// showStats: true,
|
||||
// showViewCube: true
|
||||
// });
|
||||
//
|
||||
// engine.loader.loadModel(url, {
|
||||
// position: [10, -5, 0],
|
||||
// rotation: [0, 0, 0],
|
||||
// scale: [1, 1, 1]
|
||||
// });
|
||||
// ```
|
||||
|
||||
// 重新导出 createEngine(从 SDK 文件)
|
||||
// 注意:如果直接导入失败,用户应该直接从 bim-engine-sdk.es.js 文件导入
|
||||
export { createEngine } from './bim-engine-sdk.es.js';
|
||||
@@ -21,4 +21,8 @@ export const enUS: TranslationDictionary = {
|
||||
testTitle: 'Test Dialog',
|
||||
testContent: '<div style="padding: 10px;">This is a <b>draggable</b> and <b>resizable</b> dialog.<br><br>Try dragging the title bar or resizing from the bottom-right corner.</div>',
|
||||
},
|
||||
menu: {
|
||||
info: "info",
|
||||
home: "home",
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,6 +23,10 @@ export interface TranslationDictionary {
|
||||
testTitle: string;
|
||||
testContent: string;
|
||||
};
|
||||
menu: {
|
||||
info: string;
|
||||
home: string;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,4 +21,8 @@ export const zhCN: TranslationDictionary = {
|
||||
testTitle: '测试弹窗',
|
||||
testContent: '<div style="padding: 10px;">这是一个 <b>可拖拽</b> 且 <b>可缩放</b> 的弹窗。<br><br>你可以尝试拖动标题栏,或者拖动右下角改变大小。</div>',
|
||||
},
|
||||
menu: {
|
||||
info: "信息",
|
||||
home: "首页"
|
||||
}
|
||||
};
|
||||
|
||||
99
src/managers/right-key-manager.ts
Normal file
99
src/managers/right-key-manager.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { BimComponent } from '../core/component';
|
||||
import { BimEngine } from '../bim-engine';
|
||||
import { BimRightKey } from '../components/right-key';
|
||||
import { BimMenu } from '../components/menu';
|
||||
import { MenuItemConfig } from '../components/menu/item';
|
||||
import { infoMenuButton } from '../components/menu/buttons/info';
|
||||
import { homeMenuButton } from '../components/menu/buttons/home';
|
||||
|
||||
/**
|
||||
* 右键菜单管理器 (RightKeyManager)
|
||||
* 负责协调右键交互流程:
|
||||
* 1. 监听 Canvas/容器的 contextmenu 事件
|
||||
* 2. 通过注册的处理器 (Handler) 获取需要显示的菜单项
|
||||
* 3. 实例化 Menu 组件并装载到 RightKey 容器中显示
|
||||
*/
|
||||
export class RightKeyManager extends BimComponent {
|
||||
private container: HTMLElement;
|
||||
private rightKeyPanel: BimRightKey;
|
||||
|
||||
// 存储注册的上下文处理器
|
||||
// 每个处理<E5A484><E79086><EFBFBD>接收鼠标事件,返回一组菜单项(如果没有对应菜单则返回 null)
|
||||
private contextHandlers: Array<(e: MouseEvent) => MenuItemConfig[] | null> = [];
|
||||
|
||||
constructor(engine: BimEngine, container: HTMLElement) {
|
||||
super(engine);
|
||||
this.container = container;
|
||||
|
||||
// 初始化右键容器,设置极高的层级以覆盖所有 UI
|
||||
this.rightKeyPanel = new BimRightKey({ zIndex: 9000 });
|
||||
this.rightKeyPanel.init();
|
||||
|
||||
this.initEventListeners();
|
||||
}
|
||||
|
||||
private initEventListeners(): void {
|
||||
this.container.addEventListener('contextmenu', this.handleContextMenu);
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.container.removeEventListener('contextmenu', this.handleContextMenu);
|
||||
this.rightKeyPanel.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册上下文菜单处理器
|
||||
* @param handler 处理函数,接收鼠标事件,返回菜单项数组
|
||||
*/
|
||||
public registerHandler(handler: (e: MouseEvent) => MenuItemConfig[] | null): void {
|
||||
this.contextHandlers.push(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动显示菜单
|
||||
* 允许外部直接调用以显示特定的菜单,不一定依赖右键事件
|
||||
* @param x 屏幕 X 坐标
|
||||
* @param y 屏幕 Y 坐标
|
||||
* @param items 菜单项列表
|
||||
* @param groupOrder 可<><E58FAF>的分组顺序
|
||||
*/
|
||||
public showMenu(x: number, y: number, items: MenuItemConfig[], groupOrder?: string[]): void {
|
||||
if (!items || items.length === 0) return;
|
||||
|
||||
// 1. 创建菜单内容组件
|
||||
const menu = new BimMenu({ items, groupOrder });
|
||||
menu.init(); // 必须初始化以生成 DOM
|
||||
|
||||
// 2. 将菜单挂载到右键容器
|
||||
this.rightKeyPanel.mount(menu);
|
||||
|
||||
// 3. 显示容器
|
||||
this.rightKeyPanel.show(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏右键菜单
|
||||
*/
|
||||
public hide(): void {
|
||||
this.rightKeyPanel.hide();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理右键点击事件
|
||||
*/
|
||||
private handleContextMenu = (e: MouseEvent): void => {
|
||||
// 阻止浏览器默认的右键菜单
|
||||
e.preventDefault();
|
||||
let items: MenuItemConfig[] = [];
|
||||
items.push(infoMenuButton(this.engine))
|
||||
items.push(infoMenuButton(this.engine))
|
||||
items.push(infoMenuButton(this.engine))
|
||||
items.push(homeMenuButton(this.engine))
|
||||
if (items && items.length > 0) {
|
||||
this.showMenu(e.clientX, e.clientY, items);
|
||||
} else {
|
||||
// 如果没有任何内容,则关闭可能存在的菜单
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export const darkTheme: ThemeConfig = {
|
||||
iconActive: '#ffffff',
|
||||
|
||||
componentBackground: 'transparent',
|
||||
componentHover: '#333333',
|
||||
componentHover: '#4e4d4dff',
|
||||
componentActive: 'rgba(255, 255, 255, 0.1)'
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user