feat(tree): implement tree component with checkbox support and manager pattern
- Added core Tree component (BimTree, BimTreeNode) - Added TreeManager for lifecycle management - Added ModelTreeManager for business logic encapsulation (Tree + Dialog) - Integrated into BimEngine and updated demos - Added internationalization support
This commit is contained in:
@@ -536,7 +536,9 @@ 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` |
|
||||
| `RightKeyManager` | `src/managers/right-key-manager.ts` | 管理右键菜单 (Context Menu) | `BimComponent` |
|
||||
| `TreeManager` | `src/managers/tree-manager.ts` | 管理树组件实例 | `BimComponent` |
|
||||
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 (组合 Dialog 和 Tree) | `BimComponent` |
|
||||
|
||||
### 4.2 组件类清单
|
||||
|
||||
@@ -549,6 +551,7 @@ engine.toolbar.setButtonVisibility('my-button', false);
|
||||
| `Engine` | `src/components/engine/index.ts` | 3D 引擎组件 | `IBimComponent` |
|
||||
| `BimRightKey` | `src/components/right-key/index.ts` | 右键浮层容器 | `IBimComponent` |
|
||||
| `BimMenu` | `src/components/menu/index.ts` | 通用菜单列表 | `IBimComponent` |
|
||||
| `BimTree` | `src/components/tree/index.ts` | 通用树形组件 | `IBimComponent` |
|
||||
|
||||
### 4.3 服务类清单
|
||||
|
||||
@@ -573,6 +576,11 @@ interface EngineEvents {
|
||||
'engine:model-loaded': { url: string };
|
||||
'engine:object-clicked': { objectId: string; position: { x: number, y: number, z: number } };
|
||||
|
||||
// 树组件事件
|
||||
'ui:tree-node-check': { id: string; checked: boolean; node: any };
|
||||
'ui:tree-node-select': { id: string; selected: boolean; node: any };
|
||||
'ui:tree-node-expand': { id: string; expanded: boolean };
|
||||
|
||||
// 系统事件
|
||||
'sys:theme-changed': { theme: string };
|
||||
'sys:locale-changed': { locale: string };
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
<button @click="openTestDialog">测试弹窗</button>
|
||||
<button @click="openInfoDialog">信息弹窗</button>
|
||||
<button @click="openRedDialog">警告弹窗</button>
|
||||
<button @click="openTreeDialog">树组件测试</button>
|
||||
<button @click="openSimpleTreeDialog">纯树测试</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -181,6 +183,67 @@ const openRedDialog = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const openTreeDialog = () => {
|
||||
if (!engine.value || !engine.value.modelTree) return;
|
||||
|
||||
// 1. 构造树数据
|
||||
const treeData = [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'tree.modelStruct', // 翻译键
|
||||
expanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'level-1',
|
||||
label: 'tree.floor1',
|
||||
expanded: true,
|
||||
children: [
|
||||
{ id: 'l1-wall', label: 'tree.wall', checked: true },
|
||||
{ id: 'l1-col', label: 'tree.column' },
|
||||
{ id: 'l1-win', label: 'tree.window' },
|
||||
{ id: 'l1-door', label: 'tree.door' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'level-2',
|
||||
label: 'tree.floor2',
|
||||
children: [
|
||||
{ id: 'l2-wall', label: 'tree.wall' },
|
||||
{ id: 'l2-col', label: 'tree.column' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 2. 使用高级 API
|
||||
engine.value.modelTree.showStructTree(treeData);
|
||||
};
|
||||
|
||||
const openSimpleTreeDialog = () => {
|
||||
if (!engine.value || !engine.value.modelTree) return;
|
||||
|
||||
const treeData = [
|
||||
{
|
||||
id: 'menu-root',
|
||||
label: 'menu.home',
|
||||
expanded: true,
|
||||
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></svg>',
|
||||
children: [
|
||||
{ id: 'm-info', label: 'menu.info' },
|
||||
{
|
||||
id: 'm-struct', label: 'tree.modelStruct', children: [
|
||||
{ id: 'm-w', label: 'tree.wall' },
|
||||
{ id: 'm-c', label: 'tree.column' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
engine.value.modelTree.showSimpleTree(treeData, 'Simple Tree');
|
||||
};
|
||||
|
||||
// --- 工具栏操作 ---
|
||||
const toggleToolbar = () => {
|
||||
if (!engine.value || !engine.value.toolbar) return;
|
||||
|
||||
@@ -132,6 +132,8 @@
|
||||
<button onclick="openTestDialog()">测试弹窗</button>
|
||||
<button onclick="openInfoDialog()">信息弹窗</button>
|
||||
<button onclick="openRedDialog()">警告弹窗</button>
|
||||
<button onclick="openTreeDialog()">树组件测试</button>
|
||||
<button onclick="openSimpleTreeDialog()">纯树测试</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -239,6 +241,69 @@
|
||||
});
|
||||
}
|
||||
|
||||
// --- 树组件测试 ---
|
||||
function openTreeDialog() {
|
||||
if (!engine || !engine.modelTree) return;
|
||||
|
||||
// 1. 构造树数据
|
||||
const treeData = [
|
||||
{
|
||||
id: 'root',
|
||||
label: 'tree.modelStruct',
|
||||
expanded: true,
|
||||
children: [
|
||||
{
|
||||
id: 'level-1',
|
||||
label: 'tree.floor1',
|
||||
expanded: true,
|
||||
children: [
|
||||
{ id: 'l1-wall', label: 'tree.wall', checked: true },
|
||||
{ id: 'l1-col', label: 'tree.column' },
|
||||
{ id: 'l1-win', label: 'tree.window' },
|
||||
{ id: 'l1-door', label: 'tree.door' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'level-2',
|
||||
label: 'tree.floor2',
|
||||
children: [
|
||||
{ id: 'l2-wall', label: 'tree.wall' },
|
||||
{ id: 'l2-col', label: 'tree.column' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 2. 使用高级 API 直接显示
|
||||
engine.modelTree.showStructTree(treeData);
|
||||
}
|
||||
|
||||
// --- 纯树测试 (无复选框) ---
|
||||
function openSimpleTreeDialog() {
|
||||
if (!engine || !engine.modelTree) return;
|
||||
|
||||
// 简单数据
|
||||
const treeData = [
|
||||
{
|
||||
id: 'menu-root',
|
||||
label: 'menu.home',
|
||||
expanded: true,
|
||||
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></svg>',
|
||||
children: [
|
||||
{ id: 'm-info', label: 'menu.info' },
|
||||
{ id: 'm-struct', label: 'tree.modelStruct', children: [
|
||||
{ id: 'm-w', label: 'tree.wall' },
|
||||
{ id: 'm-c', label: 'tree.column' }
|
||||
]}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// 使用高级 API
|
||||
engine.modelTree.showSimpleTree(treeData, 'Simple Tree');
|
||||
}
|
||||
|
||||
// --- 工具栏操作 ---
|
||||
function toggleToolbar() {
|
||||
if (!engine || !engine.toolbar) return;
|
||||
|
||||
84
docs/components/tree.md
Normal file
84
docs/components/tree.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Tree 组件文档
|
||||
|
||||
## 1. 组件概述
|
||||
**BimTree** 是一个通用的树形组件,支持多级嵌套、复选框、图标和自定义内容。它通常用于展示模型结构、文件目录或层级列表。
|
||||
该组件设计为被包裹在容器(如 Dialog)中,并通过 `TreeManager` 进行创建和管理。
|
||||
|
||||
## 2. 组件类 API (BimTree)
|
||||
`src/components/tree/index.ts`
|
||||
|
||||
### 2.1 核心方法
|
||||
* `checkNode(id: string, checked: boolean)`: 勾选/取消勾选指定节点。支持父子联动。
|
||||
* `checkAll(checked: boolean)`: 全选或全不选。
|
||||
* `expandNode(id: string, expanded: boolean)`: 展开/折叠指定节点。
|
||||
* `expandAll(expanded: boolean)`: 展开/折叠所有层级。
|
||||
* `getCheckedNodes(includeHalfChecked?: boolean)`: 获取当前所有被勾选的节点配置列表。
|
||||
* `getNode(id: string)`: 获取指定 ID 的节点实例。
|
||||
|
||||
## 3. Manager API (TreeManager)
|
||||
`src/managers/tree-manager.ts`
|
||||
|
||||
* `create(options: TreeOptions)`: 创建并返回一个新的 BimTree 实例。
|
||||
* `options`: 参见 `TreeOptions` 类型定义。
|
||||
|
||||
## 4. 类型定义
|
||||
`src/components/tree/types.ts`
|
||||
|
||||
```typescript
|
||||
interface TreeNodeConfig {
|
||||
id: string;
|
||||
label: string; // 翻译键
|
||||
children?: TreeNodeConfig[];
|
||||
checked?: boolean;
|
||||
expanded?: boolean;
|
||||
disabled?: boolean;
|
||||
icon?: string;
|
||||
data?: any; // 业务数据
|
||||
}
|
||||
|
||||
interface TreeOptions {
|
||||
data: TreeNodeConfig[];
|
||||
checkable?: boolean; // 是否显示复选框
|
||||
checkStrictly?: boolean; // 是否父子联动 (默认 true)
|
||||
defaultExpandAll?: boolean;
|
||||
indent?: number;
|
||||
}
|
||||
```
|
||||
|
||||
## 5. UI 结构与样式
|
||||
* **容器**: `.bim-tree` (Flex column)
|
||||
* **节点**: `.bim-tree-node`
|
||||
* **内容行**: `.bim-tree-node-content` (Flex row, align-center)
|
||||
* **箭头**: `.bim-tree-switcher` (SVG, rotate transform)
|
||||
* **复选框**: `.bim-tree-checkbox` (自定义样式, support indeterminate)
|
||||
* **图标**: `.bim-tree-icon`
|
||||
* **文本**: `.bim-tree-title`
|
||||
* **子容器**: `.bim-tree-children` (padding-left 缩进)
|
||||
|
||||
## 6. 逻辑流程
|
||||
1. **初始化**:
|
||||
* 根据 `data` 递归创建 `BimTreeNode` 实例。
|
||||
* 建立 `id -> Node` 的 Map 索引。
|
||||
* 订阅主题和语言变更。
|
||||
2. **联动逻辑 (Check Cascade)**:
|
||||
* **向下**: 父节点状态变更 -> 递归强制设置所有子节点。
|
||||
* **向上**: 子节点状态变更 -> 冒泡检查兄弟节点状态 -> 更新父节点 (Checked/Unchecked/Indeterminate)。
|
||||
3. **事件**:
|
||||
* 点击复选框 -> 更新状态 -> 触发联动 -> 发送 `ui:tree-node-check`。
|
||||
* 点击内容 -> 发送 `ui:tree-node-select`。
|
||||
|
||||
## 7. 国际化支持
|
||||
* 节点 `label` 必须是翻译键。
|
||||
* 组件订阅 `localeManager`,语言变更时自动刷新文本。
|
||||
|
||||
## 8. 使用示例
|
||||
```typescript
|
||||
const tree = engine.tree.create({
|
||||
data: [
|
||||
{ id: 'root', label: 'tree.root', children: [...] }
|
||||
],
|
||||
checkable: true
|
||||
});
|
||||
// 挂载到 DOM
|
||||
document.body.appendChild(tree.element);
|
||||
```
|
||||
@@ -3,6 +3,9 @@ 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 { TreeManager } from './managers/tree-manager';
|
||||
import { RightKeyManager } from './managers/right-key-manager';
|
||||
import { ModelTreeManager } from './managers/model-tree-manager';
|
||||
import type { EngineOptions, ModelLoadOptions } from './components/engine';
|
||||
import { localeManager } from './services/locale';
|
||||
import { themeManager } from './services/theme';
|
||||
@@ -21,6 +24,9 @@ export class BimEngine extends EventEmitter {
|
||||
public buttonGroup: ButtonGroupManager | null = null; // 通用
|
||||
public dialog: DialogManager | null = null;
|
||||
public engine: EngineManager | null = null; // 3D 引擎管理器
|
||||
public tree: TreeManager | null = null; // 树组件管理器
|
||||
public rightKey: RightKeyManager | null = null; // 右键菜单管理器
|
||||
public modelTree: ModelTreeManager | null = null; // 模型树业务管理器
|
||||
|
||||
public get localeManager() { return localeManager; }
|
||||
public get themeManager() { return themeManager; }
|
||||
@@ -74,6 +80,10 @@ 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.tree = new TreeManager(this, this.wrapper);
|
||||
this.rightKey = new RightKeyManager(this, this.wrapper);
|
||||
this.modelTree = new ModelTreeManager(this);
|
||||
|
||||
// 初始主题
|
||||
this.updateTheme(themeManager.getTheme());
|
||||
// 订阅主题变化
|
||||
@@ -95,6 +105,9 @@ export class BimEngine extends EventEmitter {
|
||||
this.buttonGroup?.destroy();
|
||||
this.engine?.destroy();
|
||||
this.dialog?.destroy();
|
||||
this.tree?.destroy();
|
||||
this.rightKey?.destroy();
|
||||
this.modelTree?.destroy();
|
||||
this.container.innerHTML = '';
|
||||
this.clear();
|
||||
}
|
||||
|
||||
161
src/components/tree/index.css
Normal file
161
src/components/tree/index.css
Normal file
@@ -0,0 +1,161 @@
|
||||
/* 树容器 */
|
||||
.bim-tree {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
font-size: 14px;
|
||||
color: var(--bim-ui_text_primary, #333);
|
||||
user-select: none; /* 防止双击选中文字 */
|
||||
}
|
||||
|
||||
/* 节点行容器 */
|
||||
.bim-tree-node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 节点内容行 (Flex 布局) */
|
||||
.bim-tree-node-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 32px; /* 标准行高 */
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
border-radius: 4px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.bim-tree-node-content:hover {
|
||||
background-color: var(--bim-ui_bg_hover, rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
/* 展开/折叠箭头 */
|
||||
.bim-tree-switcher {
|
||||
width: 24px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--bim-ui_text_secondary, #999);
|
||||
transition: transform 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bim-tree-switcher svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
fill: currentColor;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.bim-tree-switcher.is-expanded svg {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.bim-tree-switcher.is-hidden {
|
||||
visibility: hidden; /*叶子节点占位但不显示*/
|
||||
}
|
||||
|
||||
/* 复选框 */
|
||||
.bim-tree-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid var(--bim-ui_border_color, #d9d9d9);
|
||||
border-radius: 2px;
|
||||
margin-right: 8px;
|
||||
background-color: var(--bim-ui_bg_color, #fff);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.bim-tree-checkbox:hover {
|
||||
border-color: var(--bim-primary_color, #1890ff);
|
||||
}
|
||||
|
||||
/* 选中状态 */
|
||||
.bim-tree-checkbox.is-checked {
|
||||
background-color: var(--bim-primary_color, #1890ff);
|
||||
border-color: var(--bim-primary_color, #1890ff);
|
||||
}
|
||||
|
||||
.bim-tree-checkbox.is-checked::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1px;
|
||||
left: 4px;
|
||||
width: 5px;
|
||||
height: 9px;
|
||||
border: 2px solid #fff;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* 半选状态 */
|
||||
.bim-tree-checkbox.is-indeterminate {
|
||||
background-color: var(--bim-ui_bg_color, #fff);
|
||||
border-color: var(--bim-primary_color, #1890ff);
|
||||
}
|
||||
|
||||
.bim-tree-checkbox.is-indeterminate::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 3px;
|
||||
width: 8px;
|
||||
height: 2px;
|
||||
background-color: var(--bim-primary_color, #1890ff);
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
.bim-tree-node.is-disabled .bim-tree-checkbox {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.bim-tree-node.is-disabled .bim-tree-checkbox.is-checked {
|
||||
background-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.bim-tree-node.is-disabled .bim-tree-node-content {
|
||||
color: var(--bim-ui_text_disabled, #ccc);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 图标 */
|
||||
.bim-tree-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bim-tree-icon img,
|
||||
.bim-tree-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 文本 */
|
||||
.bim-tree-title {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* 子节点容器 */
|
||||
.bim-tree-children {
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bim-tree-children.is-visible {
|
||||
display: block;
|
||||
}
|
||||
218
src/components/tree/index.ts
Normal file
218
src/components/tree/index.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { ThemeConfig } from '../../themes/types';
|
||||
import { localeManager } from '../../services/locale';
|
||||
import { themeManager } from '../../services/theme';
|
||||
import { TreeOptions, TreeNodeConfig, TreeNodeCheckState } from './types';
|
||||
import { BimTreeNode } from './tree-node';
|
||||
import './index.css';
|
||||
|
||||
// 定义辅助事件发射器接口 (由于 BimTree 通常作为 Manager 的一部分或独立使用)
|
||||
// 为了方便,这里我们假设它会被 TreeManager 管理,TreeManager 会处理事件发射
|
||||
// 但 BimTree 本身也需要一个方式通知 Manager。
|
||||
// 我们可以通过构造函数传入 event bus 适配器,或者直接使用 CustomEvent。
|
||||
// 更好的方式:BimTree 提供 onEvent 回调。
|
||||
|
||||
export class BimTree implements IBimComponent {
|
||||
public element: HTMLElement;
|
||||
private options: TreeOptions;
|
||||
private nodeMap: Map<string, BimTreeNode> = new Map();
|
||||
private rootNodes: BimTreeNode[] = [];
|
||||
|
||||
// 订阅清理函数
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
private unsubscribeTheme: (() => void) | null = null;
|
||||
|
||||
// 事<><E4BA8B><EFBFBD>回调 (由 Manager 注入)
|
||||
public onNodeCheck?: (node: BimTreeNode) => void;
|
||||
public onNodeSelect?: (node: BimTreeNode) => void;
|
||||
public onNodeExpand?: (node: BimTreeNode) => void;
|
||||
|
||||
constructor(options: TreeOptions) {
|
||||
this.options = {
|
||||
checkable: true,
|
||||
checkStrictly: true,
|
||||
indent: 24,
|
||||
defaultExpandAll: false,
|
||||
...options
|
||||
};
|
||||
this.element = document.createElement('div');
|
||||
this.element.className = 'bim-tree';
|
||||
}
|
||||
|
||||
public init(): void {
|
||||
this.render();
|
||||
|
||||
// 订阅系统事件
|
||||
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
|
||||
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
*/
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
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_text_secondary', theme.textSecondary || '#999');
|
||||
style.setProperty('--bim-ui_border_color', theme.border);
|
||||
style.setProperty('--bim-ui_bg_hover', theme.componentHover);
|
||||
style.setProperty('--bim-primary_color', theme.primary);
|
||||
// style.setProperty('--bim-ui_text_disabled', theme.textDisabled); // 如果 ThemeConfig 有这个字段
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应语言变更
|
||||
*/
|
||||
public setLocales(): void {
|
||||
this.nodeMap.forEach(node => node.updateLabel());
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale();
|
||||
this.unsubscribeLocale = null;
|
||||
}
|
||||
if (this.unsubscribeTheme) {
|
||||
this.unsubscribeTheme();
|
||||
this.unsubscribeTheme = null;
|
||||
}
|
||||
this.rootNodes.forEach(node => node.destroy());
|
||||
this.rootNodes = [];
|
||||
this.nodeMap.clear();
|
||||
this.element.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心渲染逻辑
|
||||
*/
|
||||
private render(): void {
|
||||
this.element.innerHTML = '';
|
||||
this.nodeMap.clear();
|
||||
this.rootNodes = [];
|
||||
|
||||
this.options.data.forEach(config => {
|
||||
this.createNodeRecursively(config, null);
|
||||
});
|
||||
|
||||
// 如果设置了 defaultExpandAll,展开所有
|
||||
if (this.options.defaultExpandAll) {
|
||||
this.expandAll(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归创建节点
|
||||
*/
|
||||
private createNodeRecursively(config: TreeNodeConfig, parent: BimTreeNode | null) {
|
||||
const node = new BimTreeNode(config, this.options, {
|
||||
onExpand: (n) => { if (this.onNodeExpand) this.onNodeExpand(n); },
|
||||
onCheck: (n) => this.handleNodeCheck(n),
|
||||
onClick: (n) => { if (this.onNodeSelect) this.onNodeSelect(n); }
|
||||
});
|
||||
|
||||
this.nodeMap.set(config.id, node);
|
||||
|
||||
if (parent) {
|
||||
parent.appendChild(node);
|
||||
} else {
|
||||
this.rootNodes.push(node);
|
||||
this.element.appendChild(node.element);
|
||||
}
|
||||
|
||||
if (config.children && config.children.length > 0) {
|
||||
config.children.forEach(childConfig => {
|
||||
this.createNodeRecursively(childConfig, node);
|
||||
});
|
||||
}
|
||||
|
||||
// 如果是初始化渲染,需要处理 checkStrictly 的向上联动(因为数据里可能只给了子节点 checked,父节点没给)
|
||||
// 这里做一个简单的后处理:如果 checkStrictly 开启,且当前节点 checked,则触发一次联动
|
||||
// 注意:这可能会导致性能问题,优化做法是在所有节点创建完后统一计算一次状态
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理节点勾选逻辑 (核心算法)
|
||||
*/
|
||||
private handleNodeCheck(node: BimTreeNode) {
|
||||
const isChecked = node.checkState === TreeNodeCheckState.Checked;
|
||||
|
||||
// 1. 触发外部回调 (Event)
|
||||
if (this.onNodeCheck) this.onNodeCheck(node);
|
||||
|
||||
// 2. 如果不联动,直接返回
|
||||
if (this.options.checkStrictly === false) return;
|
||||
|
||||
// 3. 联动逻辑
|
||||
// 3.1 向下级联 (Cascade Down): 父变子全变
|
||||
const updateChildren = (n: BimTreeNode, state: TreeNodeCheckState) => {
|
||||
n.children.forEach(child => {
|
||||
if (child.config.disabled) return; // 跳过禁用节点
|
||||
child.setChecked(state, false); // 不再触发事件,只更新状态 UI
|
||||
updateChildren(child, state);
|
||||
});
|
||||
};
|
||||
|
||||
// 当前节点是 Checked 或 Unchecked,子节点跟随
|
||||
if (isChecked) {
|
||||
updateChildren(node, TreeNodeCheckState.Checked);
|
||||
} else {
|
||||
updateChildren(node, TreeNodeCheckState.Unchecked);
|
||||
}
|
||||
|
||||
// 3.2 向上冒泡 (Bubble Up): 子变父更新
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (current.config.disabled) {
|
||||
current = current.parent;
|
||||
continue;
|
||||
}
|
||||
|
||||
const children = current.children;
|
||||
const allChecked = children.every(c => c.checkState === TreeNodeCheckState.Checked);
|
||||
const allUnchecked = children.every(c => c.checkState === TreeNodeCheckState.Unchecked);
|
||||
|
||||
if (allChecked) {
|
||||
current.setChecked(TreeNodeCheckState.Checked, false);
|
||||
} else if (allUnchecked) {
|
||||
current.setChecked(TreeNodeCheckState.Unchecked, false);
|
||||
} else {
|
||||
current.setChecked(TreeNodeCheckState.Indeterminate, false);
|
||||
}
|
||||
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
|
||||
// ================== Public APIs ==================
|
||||
|
||||
public getNode(id: string): BimTreeNode | undefined {
|
||||
return this.nodeMap.get(id);
|
||||
}
|
||||
|
||||
public checkNode(id: string, checked: boolean) {
|
||||
const node = this.nodeMap.get(id);
|
||||
if (node) {
|
||||
node.setChecked(checked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked, true);
|
||||
// 手动调用联动处理,因为 setChecked 的 fireEvent 只触发回调,不包含内部逻辑调用?
|
||||
// 不,我们在 createNodeRecursively 里绑定的 onCheck 就是 handleNodeCheck
|
||||
// 所以只要 fireEvent=true,就会触发 handleNodeCheck,进而触发联动。
|
||||
}
|
||||
}
|
||||
|
||||
public expandAll(expanded: boolean) {
|
||||
this.nodeMap.forEach(node => node.toggleExpand(expanded));
|
||||
}
|
||||
|
||||
public getCheckedNodes(includeHalfChecked: boolean = false): TreeNodeConfig[] {
|
||||
const result: TreeNodeConfig[] = [];
|
||||
this.nodeMap.forEach(node => {
|
||||
if (node.checkState === TreeNodeCheckState.Checked) {
|
||||
result.push(node.config);
|
||||
} else if (includeHalfChecked && node.checkState === TreeNodeCheckState.Indeterminate) {
|
||||
result.push(node.config);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
239
src/components/tree/tree-node.ts
Normal file
239
src/components/tree/tree-node.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import { TreeNodeConfig, TreeNodeCheckState, TreeOptions } from './types';
|
||||
import { t } from '../../services/locale';
|
||||
|
||||
/**
|
||||
* 树节点类
|
||||
* 负责渲染单个节点、处理交互和递归
|
||||
*/
|
||||
export class BimTreeNode {
|
||||
public config: TreeNodeConfig;
|
||||
public element: HTMLElement;
|
||||
public children: BimTreeNode[] = [];
|
||||
public parent: BimTreeNode | null = null;
|
||||
public checkState: TreeNodeCheckState = TreeNodeCheckState.Unchecked;
|
||||
|
||||
// UI Elements
|
||||
private contentEl!: HTMLElement;
|
||||
private switcherEl!: HTMLElement;
|
||||
private checkboxEl: HTMLElement | null = null;
|
||||
private titleEl!: HTMLElement;
|
||||
private childrenContainer!: HTMLElement;
|
||||
|
||||
// 外部回调
|
||||
private onExpandChange: (node: BimTreeNode) => void;
|
||||
private onCheckChange: (node: BimTreeNode) => void;
|
||||
private onNodeClick: (node: BimTreeNode) => void;
|
||||
|
||||
constructor(
|
||||
config: TreeNodeConfig,
|
||||
options: TreeOptions,
|
||||
callbacks: {
|
||||
onExpand: (n: BimTreeNode) => void,
|
||||
onCheck: (n: BimTreeNode) => void,
|
||||
onClick: (n: BimTreeNode) => void
|
||||
}
|
||||
) {
|
||||
this.config = config;
|
||||
this.onExpandChange = callbacks.onExpand;
|
||||
this.onCheckChange = callbacks.onCheck;
|
||||
this.onNodeClick = callbacks.onClick;
|
||||
|
||||
// 初始化状态
|
||||
this.checkState = config.checked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked;
|
||||
|
||||
this.element = this.createDom(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建节点 DOM
|
||||
*/
|
||||
private createDom(options: TreeOptions): HTMLElement {
|
||||
const nodeEl = document.createElement('div');
|
||||
nodeEl.className = 'bim-tree-node';
|
||||
if (this.config.disabled) nodeEl.classList.add('is-disabled');
|
||||
|
||||
// 1. 内容行
|
||||
this.contentEl = document.createElement('div');
|
||||
this.contentEl.className = 'bim-tree-node-content';
|
||||
|
||||
// 计算缩进 (由父级传入或在 Tree 中统一处理样式,这里使用 padding-left 动态计算较复杂,
|
||||
// 我们改用 Flex + 占位符 或者直接在 Tree 递归时处理。
|
||||
// 实际上,递归渲染时只需把 children 放在一个有 padding-left 的容器里即可,CSS 中已经处理吗?
|
||||
// 检查 CSS: .bim-tree-children 没有 padding-left。
|
||||
// 我们采用在 CSS 中给 children 加 padding-left 的方式最简单,或者动态设置 style.
|
||||
// 为了灵活性,这里采用动态设置 contentEl 的 paddingLeft
|
||||
// 但这需要知道层级 depth。为了简单,我们在 renderChildren 时设置容器样式。
|
||||
|
||||
// 1.1 展开/折叠箭头
|
||||
this.switcherEl = document.createElement('span');
|
||||
this.switcherEl.className = 'bim-tree-switcher';
|
||||
// 默认右箭头 SVG
|
||||
this.switcherEl.innerHTML = `<svg viewBox="0 0 1024 1024"><path d="M288 192l448 320-448 320z"></path></svg>`;
|
||||
|
||||
const hasChildren = this.config.children && this.config.children.length > 0;
|
||||
if (!hasChildren) {
|
||||
this.switcherEl.classList.add('is-hidden');
|
||||
} else if (this.config.expanded) {
|
||||
this.switcherEl.classList.add('is-expanded');
|
||||
}
|
||||
|
||||
this.switcherEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.toggleExpand();
|
||||
});
|
||||
|
||||
this.contentEl.appendChild(this.switcherEl);
|
||||
|
||||
// 1.2 复选框 (可选)
|
||||
if (options.checkable !== false) {
|
||||
this.checkboxEl = document.createElement('span');
|
||||
this.checkboxEl.className = 'bim-tree-checkbox';
|
||||
this.updateCheckboxUI();
|
||||
|
||||
this.checkboxEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.config.disabled) return;
|
||||
this.toggleCheck();
|
||||
});
|
||||
this.contentEl.appendChild(this.checkboxEl);
|
||||
}
|
||||
|
||||
// 1.3 图标 (可选)
|
||||
if (this.config.icon) {
|
||||
const iconEl = document.createElement('span');
|
||||
iconEl.className = 'bim-tree-icon';
|
||||
iconEl.innerHTML = this.config.icon.includes('<svg')
|
||||
? this.config.icon
|
||||
: `<img src="${this.config.icon}" />`;
|
||||
this.contentEl.appendChild(iconEl);
|
||||
}
|
||||
|
||||
// 1.4 文本
|
||||
this.titleEl = document.createElement('span');
|
||||
this.titleEl.className = 'bim-tree-title';
|
||||
this.updateLabel(); // 设置文本
|
||||
this.contentEl.appendChild(this.titleEl);
|
||||
|
||||
// 绑定整行点击
|
||||
this.contentEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (this.config.disabled) return;
|
||||
this.onNodeClick(this);
|
||||
});
|
||||
|
||||
nodeEl.appendChild(this.contentEl);
|
||||
|
||||
// 2. 子节点容器
|
||||
this.childrenContainer = document.createElement('div');
|
||||
this.childrenContainer.className = 'bim-tree-children';
|
||||
// 设置缩进
|
||||
const indent = options.indent || 24;
|
||||
this.childrenContainer.style.paddingLeft = `${indent}px`; // 每一级子<E7BAA7><E5AD90><EFBFBD>器左移
|
||||
|
||||
if (this.config.expanded && hasChildren) {
|
||||
this.childrenContainer.classList.add('is-visible');
|
||||
}
|
||||
nodeEl.appendChild(this.childrenContainer);
|
||||
|
||||
return nodeEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新显示文本 (国际化支持)
|
||||
*/
|
||||
public updateLabel() {
|
||||
if (this.titleEl) {
|
||||
this.titleEl.textContent = t(this.config.label);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换展开状态
|
||||
*/
|
||||
public toggleExpand(force?: boolean) {
|
||||
if (this.config.children?.length === 0) return;
|
||||
|
||||
const newState = force !== undefined ? force : !this.config.expanded;
|
||||
this.config.expanded = newState;
|
||||
|
||||
if (newState) {
|
||||
this.switcherEl.classList.add('is-expanded');
|
||||
this.childrenContainer.classList.add('is-visible');
|
||||
} else {
|
||||
this.switcherEl.classList.remove('is-expanded');
|
||||
this.childrenContainer.classList.remove('is-visible');
|
||||
}
|
||||
|
||||
// 触发回调
|
||||
if (force === undefined) { // 只有用户交互才触发回调,防止初始化时无限循环
|
||||
this.onExpandChange(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换选中状态 (用户点击)
|
||||
*/
|
||||
public toggleCheck() {
|
||||
// 如果当前是半选,点击变全选;如果全选,点击<E782B9><E587BB>未选;如果未选,点击变全选
|
||||
// 简化逻辑:只要不是 Checked,点击都变 Checked;如果是 Checked,变 Unchecked
|
||||
const newChecked = this.checkState !== TreeNodeCheckState.Checked;
|
||||
this.setChecked(newChecked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选中状态 (API调用或联动)
|
||||
* @param state 新状态
|
||||
* @param fireEvent 是否触发事件
|
||||
*/
|
||||
public setChecked(state: TreeNodeCheckState, fireEvent: boolean = false) {
|
||||
if (this.checkState === state) return;
|
||||
|
||||
this.checkState = state;
|
||||
this.config.checked = (state === TreeNodeCheckState.Checked);
|
||||
|
||||
this.updateCheckboxUI();
|
||||
|
||||
if (fireEvent) {
|
||||
this.onCheckChange(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新复选框 UI 样式
|
||||
*/
|
||||
public updateCheckboxUI() {
|
||||
if (!this.checkboxEl) return;
|
||||
|
||||
this.checkboxEl.classList.remove('is-checked', 'is-indeterminate');
|
||||
|
||||
if (this.checkState === TreeNodeCheckState.Checked) {
|
||||
this.checkboxEl.classList.add('is-checked');
|
||||
} else if (this.checkState === TreeNodeCheckState.Indeterminate) {
|
||||
this.checkboxEl.classList.add('is-indeterminate');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加子节点实例
|
||||
*/
|
||||
public appendChild(childNode: BimTreeNode) {
|
||||
childNode.parent = this;
|
||||
this.children.push(childNode);
|
||||
this.childrenContainer.appendChild(childNode.element);
|
||||
|
||||
// 如果之前是隐藏的箭头,现在有了子节点,需要显示出来
|
||||
if (this.children.length === 1) {
|
||||
this.switcherEl.classList.remove('is-hidden');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
public destroy() {
|
||||
this.children.forEach(c => c.destroy());
|
||||
this.children = [];
|
||||
this.element.remove();
|
||||
this.parent = null;
|
||||
}
|
||||
}
|
||||
64
src/components/tree/types.ts
Normal file
64
src/components/tree/types.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 节点勾选状态枚举
|
||||
*/
|
||||
export enum TreeNodeCheckState {
|
||||
Unchecked = 0,
|
||||
Checked = 1,
|
||||
Indeterminate = 2 // 半选
|
||||
}
|
||||
|
||||
/**
|
||||
* 树节点配置接口
|
||||
*/
|
||||
export interface TreeNodeConfig {
|
||||
/** 唯一标识符 */
|
||||
id: string;
|
||||
|
||||
/** 显示文本的翻译键 */
|
||||
label: string;
|
||||
|
||||
/** 节点图标 (SVG string 或 URL) */
|
||||
icon?: string;
|
||||
|
||||
/** 子节点列表 */
|
||||
children?: TreeNodeConfig[];
|
||||
|
||||
/** 初始展开状态 (默认 false) */
|
||||
expanded?: boolean;
|
||||
|
||||
/** 初始选中状态 (默认 false) */
|
||||
checked?: boolean;
|
||||
|
||||
/** 是否禁用 (默认 false) */
|
||||
disabled?: boolean;
|
||||
|
||||
/** 自定义业务数据 */
|
||||
data?: any;
|
||||
|
||||
/** 是否是叶子节点 (用于异步加载场景,暂<EFBC8C><E69A82><EFBFBD>接口) */
|
||||
isLeaf?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 树组件配置选项
|
||||
*/
|
||||
export interface TreeOptions {
|
||||
/** 树的数据源 */
|
||||
data: TreeNodeConfig[];
|
||||
|
||||
/** 是否显示复选框 (默认 true) */
|
||||
checkable?: boolean;
|
||||
|
||||
/**
|
||||
* 父子节点选中状态是否关联 (默认 true)
|
||||
* true: 选中父选子,子全选自动选父
|
||||
* false: 独立选中
|
||||
*/
|
||||
checkStrictly?: boolean;
|
||||
|
||||
/** 默认展开所有节点 (默认 false) */
|
||||
defaultExpandAll?: boolean;
|
||||
|
||||
/** 缩进宽度 (像素,默认 24) */
|
||||
indent?: number;
|
||||
}
|
||||
@@ -22,7 +22,16 @@ export const enUS: TranslationDictionary = {
|
||||
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",
|
||||
info: 'Info',
|
||||
home: 'Home',
|
||||
},
|
||||
tree: {
|
||||
modelStruct: 'Model Structure',
|
||||
floor1: 'Level 1',
|
||||
floor2: 'Level 2',
|
||||
wall: 'Walls',
|
||||
column: 'Columns',
|
||||
window: 'Windows',
|
||||
door: 'Doors',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -26,6 +26,15 @@ export interface TranslationDictionary {
|
||||
menu: {
|
||||
info: string;
|
||||
home: string;
|
||||
};
|
||||
tree: {
|
||||
modelStruct: string;
|
||||
floor1: string;
|
||||
floor2: string;
|
||||
wall: string;
|
||||
column: string;
|
||||
window: string;
|
||||
door: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,16 @@ export const zhCN: TranslationDictionary = {
|
||||
testContent: '<div style="padding: 10px;">这是一个 <b>可拖拽</b> 且 <b>可缩放</b> 的弹窗。<br><br>你可以尝试拖动标题栏,或者拖动右下角改变大小。</div>',
|
||||
},
|
||||
menu: {
|
||||
info: "信息",
|
||||
home: "首页"
|
||||
info: '信息',
|
||||
home: '首页',
|
||||
},
|
||||
tree: {
|
||||
modelStruct: '模型结构',
|
||||
floor1: '一楼',
|
||||
floor2: '二楼',
|
||||
wall: '墙体',
|
||||
column: '柱子',
|
||||
window: '窗户',
|
||||
door: '门',
|
||||
}
|
||||
};
|
||||
|
||||
76
src/managers/model-tree-manager.ts
Normal file
76
src/managers/model-tree-manager.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { BimComponent } from '../core/component';
|
||||
import { BimTree } from '../components/tree/index';
|
||||
import { BimDialog } from '../components/dialog/index';
|
||||
import { TreeNodeConfig } from '../components/tree/types';
|
||||
|
||||
/**
|
||||
* 模型树业务管理器
|
||||
* 负责组装 Tree 和 Dialog 组件,提供开箱即用的业务弹窗
|
||||
*/
|
||||
export class ModelTreeManager extends BimComponent {
|
||||
|
||||
/**
|
||||
* 显示带复选框的模型结构树弹窗
|
||||
* @param data 树节点数据
|
||||
* @param title 弹窗标题 (翻译键)
|
||||
*/
|
||||
public showStructTree(data: TreeNodeConfig[], title: string = 'tree.modelStruct'): { tree: BimTree, dialog: BimDialog } {
|
||||
// 1. 创建 Tree 实例
|
||||
const tree = this.engine.tree!.create({
|
||||
data: data,
|
||||
checkable: true,
|
||||
checkStrictly: true,
|
||||
defaultExpandAll: true
|
||||
});
|
||||
|
||||
// 2. 创建 Dialog 实例
|
||||
const dialog = this.engine.dialog!.create({
|
||||
title: title,
|
||||
width: 300,
|
||||
height: 400,
|
||||
content: tree.element,
|
||||
position: 'left-center',
|
||||
resizable: true,
|
||||
// 关键:绑定生命周期
|
||||
onClose: () => {
|
||||
tree.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
return { tree, dialog };
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示简单的树形弹窗 (无复选框)
|
||||
* @param data 树节点数据
|
||||
* @param title 弹窗标题 (翻译键)
|
||||
*/
|
||||
public showSimpleTree(data: TreeNodeConfig[], title: string = 'menu.home'): { tree: BimTree, dialog: BimDialog } {
|
||||
// 1. 创建 Tree 实例
|
||||
const tree = this.engine.tree!.create({
|
||||
data: data,
|
||||
checkable: false,
|
||||
defaultExpandAll: true
|
||||
});
|
||||
|
||||
// 2. 创建 Dialog 实例
|
||||
const dialog = this.engine.dialog!.create({
|
||||
title: title,
|
||||
width: 250,
|
||||
height: 300,
|
||||
content: tree.element,
|
||||
position: 'center',
|
||||
resizable: true,
|
||||
onClose: () => {
|
||||
tree.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
return { tree, dialog };
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
// 具体的 Tree 和 Dialog 实例由它们各自的生命周期管理
|
||||
// 这里不需要销毁它们,除非我们持有了全局引用
|
||||
}
|
||||
}
|
||||
57
src/managers/tree-manager.ts
Normal file
57
src/managers/tree-manager.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { BimComponent } from '../core/component';
|
||||
import { BimTree } from '../components/tree/index';
|
||||
import { TreeOptions } from '../components/tree/types';
|
||||
import type { BimEngine } from '../bim-engine';
|
||||
|
||||
/**
|
||||
* 树组件管理器
|
||||
* 负责创建和管理 BimTree 实例
|
||||
*/
|
||||
export class TreeManager extends BimComponent {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
constructor(engine: BimEngine, _container: HTMLElement) {
|
||||
super(engine);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的树组件实例
|
||||
* @param options 配置选项
|
||||
*/
|
||||
public create(options: TreeOptions): BimTree {
|
||||
const tree = new BimTree(options);
|
||||
|
||||
// 绑定事件桥接
|
||||
tree.onNodeCheck = (node) => {
|
||||
this.emit('ui:tree-node-check', {
|
||||
id: node.config.id,
|
||||
checked: node.config.checked || false,
|
||||
node: node.config
|
||||
});
|
||||
};
|
||||
|
||||
tree.onNodeSelect = (node) => {
|
||||
this.emit('ui:tree-node-select', {
|
||||
id: node.config.id,
|
||||
selected: true,
|
||||
node: node.config
|
||||
});
|
||||
};
|
||||
|
||||
tree.onNodeExpand = (node) => {
|
||||
this.emit('ui:tree-node-expand', {
|
||||
id: node.config.id,
|
||||
expanded: node.config.expanded || false
|
||||
});
|
||||
};
|
||||
|
||||
tree.init();
|
||||
return tree;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
// TreeManager 本身不持有 Tree 实例的强引用列表
|
||||
// 实例通常由调用者(如 Dialog)持有并销毁
|
||||
// 这里可以做一些全局清理工作
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,14 @@ export interface EngineEvents {
|
||||
|
||||
// Engine Events
|
||||
'engine:model-loaded': { url: string };
|
||||
'engine:object-clicked': { objectId: string; position: { x: number, y: number, z: number } };
|
||||
|
||||
// System Events
|
||||
'engine:object-clicked': { objectId: string; position: { x: number, y: number, z: number } };
|
||||
|
||||
// 树组件事件
|
||||
'ui:tree-node-check': { id: string; checked: boolean; node: any };
|
||||
'ui:tree-node-select': { id: string; selected: boolean; node: any };
|
||||
'ui:tree-node-expand': { id: string; expanded: boolean };
|
||||
|
||||
// 系统事件
|
||||
'sys:theme-changed': { theme: string };
|
||||
'sys:locale-changed': { locale: string };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user