添加测试信息

This commit is contained in:
yuding
2025-12-16 11:57:44 +08:00
parent 2a2258cb9c
commit 9d1ebfd817
30 changed files with 6196 additions and 5211 deletions

View File

@@ -537,8 +537,7 @@ engine.toolbar.setButtonVisibility('my-button', false);
| `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) | `BimComponent` |
| `TreeManager` | `src/managers/tree-manager.ts` | 管理树组件实例 | `BimComponent` |
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 (组合 Dialog 和 Tree) | `BimComponent` |
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 (组合 Dialog 和 Tree),管理 Tree 实例 | `BimComponent` |
### 4.2 组件类清单

View File

@@ -185,63 +185,14 @@ 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);
// 使用 Manager 中的默认 Mock 数据
engine.value.modelTree.showStructTree();
};
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');
// 使用 Manager 中的默认 Mock 数据
engine.value.modelTree.showSimpleTree(undefined, 'Simple Tree');
};
// --- 工具栏操作 ---

View File

@@ -132,8 +132,6 @@
<button onclick="openTestDialog()">测试弹窗</button>
<button onclick="openInfoDialog()">信息弹窗</button>
<button onclick="openRedDialog()">警告弹窗</button>
<button onclick="openTreeDialog()">树组件测试</button>
<button onclick="openSimpleTreeDialog()">纯树测试</button>
</div>
</div>
@@ -241,69 +239,6 @@
});
}
// --- 树组件测试 ---
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;
@@ -471,4 +406,4 @@
</script>
</body>
</html>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

135
dist/index.d.ts vendored
View File

@@ -18,6 +18,10 @@ export declare class BimButtonGroup implements IBimComponent {
setEngine(engine: BimEngine): void;
protected emit<K extends keyof EngineEvents>(event: K, payload: EngineEvents[K]): void;
private initContainer;
/**
* 设置事件拦截防止事件<E4BA8B><E4BBB6>泡到下层元素如 3D 引擎)
*/
private setupEventInterception;
private updatePosition;
/**
* 应用样式到容器
@@ -111,6 +115,11 @@ declare class BimDialog implements IBimComponent {
* 设置元素尺寸
*/
private setSize;
/**
* 根据内容自动调整弹窗宽度
* @param recenter 是否重新计算定位(例如保持居中),默认 true
*/
fitWidth(recenter?: boolean): void;
/**
* 初始化弹窗位置
*/
@@ -142,11 +151,11 @@ export declare class BimEngine extends EventEmitter {
private container;
private wrapper;
toolbar: ToolbarManager | null;
constructTreeBtn: ConstructTreeManagerBtn | null;
buttonGroup: ButtonGroupManager | null;
dialog: DialogManager | null;
engine: EngineManager | null;
get localeManager(): LocaleManager;
get themeManager(): ThemeManager;
rightKey: RightKeyManager | null;
constructor(container: HTMLElement | string, options?: {
locale?: LocaleType;
theme?: ThemeType;
@@ -230,11 +239,17 @@ export declare class BimRightKey implements IBimComponent {
private content;
private isVisible;
private onCloseCallback?;
private options?;
private mouseDownTime;
private readonly CLICK_THRESHOLD;
constructor(options?: RightKeyOptions);
init(): void;
setTheme(_theme: ThemeConfig): void;
setLocales(): void;
destroy(): void;
private handleContainerMouseDown;
private handleContainerMouseUp;
private handleContainerContextMenu;
/**
* 设置关闭时的回调函数
* 通常用于通知 Manager 状态变更
@@ -343,15 +358,38 @@ export declare interface ClickPayload {
isActive?: boolean;
}
export declare function createEngine(r) {
const e = r.version || "v1";
/**
* 底部工具栏管理器 (ToolbarManager)
* 仅负责管理底部工具栏实例。
*/
declare class ConstructTreeManagerBtn extends BimComponent {
private toolbar;
private toolbarContainer;
private container;
private dialog;
constructor(engine: BimEngine, container: HTMLElement);
private init;
openConstructTreeDialog(): void;
refresh(): void;
destroy(): void;
addGroup(groupId: string, beforeGroupId?: string): void;
addButton(config: ButtonConfig): void;
setButtonVisibility(id: string, v: boolean): void;
setShowLabel(show: boolean): void;
setVisible(visible: boolean): void;
setBackgroundColor(color: string): void;
setColors(colors: ButtonGroupColors): void;
}
export declare function createEngine(s) {
const e = s.version || "v1";
switch (e) {
case "v2":
return new Oc(r);
return new Fc(s);
case "v1":
return new U_(r);
return new O_(s);
:
return console.warn(`Version '${e}' not found. Falling back to v2.`), new Oc(r);
return console.warn(`Version '${e}' not found. Falling back to v2.`), new Fc(s);
}
}
@@ -466,6 +504,20 @@ declare interface EngineEvents {
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;
};
@@ -595,35 +647,6 @@ declare interface IRightKeyContent {
declare type Listener<T = any> = (payload: T) => void;
declare type LocaleChangeListener = (locale: LocaleType) => void;
/**
* 语言管理器类
*/
declare class LocaleManager {
private currentLocale;
private messages;
private listeners;
constructor();
/**
* 获取当前语言
*/
getLocale(): LocaleType;
/**
* 切换语言
*/
setLocale(locale: LocaleType): void;
/**
* 翻译核心方法
*/
t(key: string): string;
/**
* 订阅变更
*/
subscribe(listener: LocaleChangeListener): () => void;
private notifyListeners;
}
/**
* 语言代码类型
*/
@@ -693,7 +716,6 @@ declare class RightKeyManager extends BimComponent {
private rightKeyPanel;
private contextHandlers;
constructor(engine: BimEngine, container: HTMLElement);
private initEventListeners;
destroy(): void;
/**
* 注册上下文菜单处理器
@@ -715,6 +737,7 @@ declare class RightKeyManager extends BimComponent {
hide(): void;
/**
* 处理右键点击事件
* 由 BimRightKey 组件在检测到有效右键点击时调用
*/
private handleContextMenu;
}
@@ -724,10 +747,12 @@ declare interface RightKeyOptions {
className?: string;
/** 层级 (z-index) */
zIndex?: number;
/** 监听事件的容器 */
container?: HTMLElement;
/** 有效右键点击时的回调 */
onContext?: (e: MouseEvent) => void;
}
declare type ThemeChangeListener = (theme: ThemeConfig) => void;
/**
* 全局主题配置接口
* 定义系统通用的语义化颜色
@@ -761,38 +786,6 @@ declare interface ThemeConfig {
componentActive: string;
}
/**
* 主题管理器 (单例)
*/
declare class ThemeManager {
private currentTheme;
private listeners;
constructor();
/**
* 获取当前主题配置
*/
getTheme(): ThemeConfig;
/**
* 切换预设主题
* @param themeName 'dark' | 'light'
*/
setTheme(themeName: 'dark' | 'light'): void;
/**
* 应用自定义主题配置
* @param theme 配置对象
*/
setCustomTheme(theme: ThemeConfig): void;
/**
* 内部应用主题逻辑
*/
private applyTheme;
/**
* 订阅主题变更
*/
subscribe(listener: ThemeChangeListener): () => void;
private notifyListeners;
}
/**
* 主题类型定义
*/

View File

@@ -1181,3 +1181,5 @@ type ExpandDirection = 'up' | 'down' | 'left' | 'right';
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!

View File

@@ -880,3 +880,5 @@ interface DialogColors {
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!

View File

@@ -599,3 +599,5 @@ interface ModelLoadOptions {
**重要提醒**: 本文档必须与组件代码保持同步。任何组件修改都必须更新本文档!

View File

@@ -313,6 +313,6 @@ interface MenuOptions {
## 13. 更新记录
| 日期 | 修改内容 | 修改人 |
|------|---------|--------|
| 日期 | 修改内容 | 修改人 |
| ---------- | ----------------------------------------- | ------------ |
| 2024-XX-XX | 重构为纯配置对象驱动,移除 BimMenuItem 类 | AI Assistant |

View File

@@ -2,7 +2,7 @@
## 1. 组件概述
**BimTree** 是一个通用的树形组件,支持多级嵌套、复选框、图标和自定义内容。它通常用于展示模型结构、文件目录或层级列表。
该组件设计为被包裹在容器(如 Dialog并通过 `TreeManager` 进行创建和管理。
该组件设计为被包裹在容器(如 Dialog并通过 `ModelTreeManager` 进行创建和管理。
## 2. 组件类 API (BimTree)
`src/components/tree/index.ts`
@@ -15,11 +15,29 @@
* `getCheckedNodes(includeHalfChecked?: boolean)`: 获取当前所有被勾选的节点配置列表。
* `getNode(id: string)`: 获取指定 ID 的节点实例。
## 3. Manager API (TreeManager)
`src/managers/tree-manager.ts`
## 3. Manager API (ModelTreeManager)
* `create(options: TreeOptions)`: 创建并返回一个新的 BimTree 实例。
* `options`: 参见 `TreeOptions` 类型定义。
`ModelTreeManager` 负责创建和管理 `BimTree` 实例。
### 方法
#### `createTree(options: TreeOptions): BimTree`
创建一个新的树组件实例。
- **参数**: `options` (TreeOptions) - 树组件配置
- **返回**: `BimTree` - 树组件实例
- **功能**:
- 实例化组件
- 自动绑定组件事件到全局事件总线 (`ui:tree-node-check`, `ui:tree-node-select` 等)
- 初始化组件
#### `showStructTree(data: TreeNodeConfig[], title: string): { tree: BimTree, dialog: BimDialog }`
显示带复选框的模型结构树弹窗。
#### `showSimpleTree(data: TreeNodeConfig[], title: string): { tree: BimTree, dialog: BimDialog }`
显示简单的树形弹窗 (无复选框)。
---
## 4. 类型定义
`src/components/tree/types.ts`
@@ -42,6 +60,11 @@ interface TreeOptions {
checkStrictly?: boolean; // 是否父子联动 (默认 true)
defaultExpandAll?: boolean;
indent?: number;
// 事件回调
onNodeCheck?: (node: BimTreeNode) => void;
onNodeSelect?: (node: BimTreeNode) => void;
onNodeExpand?: (node: BimTreeNode) => void;
}
```
@@ -73,7 +96,8 @@ interface TreeOptions {
## 8. 使用示例
```typescript
const tree = engine.tree.create({
// 通过 ModelTreeManager 创建
const tree = engine.modelTree.createTree({
data: [
{ id: 'root', label: 'tree.root', children: [...] }
],

18
package-lock.json generated
View File

@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@types/node": "^25.0.2",
"typescript": "^5.9.3",
"vite": "^7.2.6",
"vite-plugin-css-injected-by-js": "^3.5.2",
@@ -1055,6 +1056,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.0.2",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-25.0.2.tgz",
"integrity": "sha512-gWEkeiyYE4vqjON/+Obqcoeffmk0NF15WSBwSs7zwVA2bAbTaE0SJ7P0WNGoJn8uE7fiaV5a7dKYIJriEqOrmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@volar/language-core": {
"version": "2.4.26",
"resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.26.tgz",
@@ -1992,6 +2003,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/universalify/-/universalify-2.0.1.tgz",

View File

@@ -33,6 +33,7 @@
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^25.0.2",
"typescript": "^5.9.3",
"vite": "^7.2.6",
"vite-plugin-css-injected-by-js": "^3.5.2",

View File

@@ -20,4 +20,12 @@
left: 50%;
transform: translateX(-50%);
z-index: 100;
}
}
/* 构造树按钮 */
.bim-construct-tree-btn {
position: absolute;
top: 20px;
left: 20px !important;
z-index: 100;
}

View File

@@ -3,9 +3,8 @@ 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 { ConstructTreeManagerBtn } from './managers/construct-tree-manager-btn';
import type { EngineOptions, ModelLoadOptions } from './components/engine';
import { localeManager } from './services/locale';
import { themeManager } from './services/theme';
@@ -21,15 +20,12 @@ export class BimEngine extends EventEmitter {
private wrapper: HTMLElement | null = null;
public toolbar: ToolbarManager | null = null; // 底部专用
public constructTreeBtn: ConstructTreeManagerBtn | null = null; // 底部专用
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; }
constructor(
container: HTMLElement | string,
@@ -80,9 +76,8 @@ 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.constructTreeBtn = new ConstructTreeManagerBtn(this, this.wrapper);
// 初始主题
this.updateTheme(themeManager.getTheme());
@@ -105,10 +100,8 @@ 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();
}
}
}

View File

@@ -94,6 +94,29 @@ export class BimButtonGroup implements IBimComponent {
}
this.updatePosition();
// 添加事件拦截,防止点击穿透到 3D 引擎
this.setupEventInterception(this.container);
}
/**
* 设置事件拦截防止事件<E4BA8B><E4BBB6>泡到下层元素如 3D 引擎)
*/
private setupEventInterception(el: HTMLElement): void {
const stopPropagation = (e: Event) => {
e.stopPropagation();
};
const events = [
'click', 'dblclick', 'contextmenu', 'wheel',
'mousedown', 'mouseup', 'mousemove',
'touchstart', 'touchend', 'touchmove',
'pointerdown', 'pointerup', 'pointermove', 'pointerenter', 'pointerleave', 'pointerover', 'pointerout'
];
events.forEach(eventType => {
el.addEventListener(eventType, stopPropagation, { passive: false });
});
}
private updatePosition() {
@@ -405,6 +428,9 @@ export class BimButtonGroup implements IBimComponent {
// 先添加到 DOM 以便计算尺寸
document.body.appendChild(dropdown);
// 添加事件拦截
this.setupEventInterception(dropdown);
// 添加菜单项
button.children.forEach(subBtn => {

View File

@@ -129,6 +129,8 @@ export class BimDialog implements IBimComponent {
// 设置初始尺寸
this.setSize(el, this.options.width, this.options.height);
// 确保最小尺寸生效
if (this.options.minWidth) el.style.minWidth = `${this.options.minWidth}px`;
// 创建标题栏 (Header)
const header = document.createElement('div');
@@ -197,10 +199,32 @@ export class BimDialog implements IBimComponent {
*/
private setSize(el: HTMLElement, width?: number | string, height?: number | string) {
if (width !== undefined) {
el.style.width = typeof width === 'number' ? `${width}px` : width;
if (width === 'auto' || width === 'fit-content') {
el.style.width = width;
} else {
el.style.width = typeof width === 'number' ? `${width}px` : width;
}
}
if (height !== undefined) {
el.style.height = typeof height === 'number' ? `${height}px` : height;
if (height === 'auto' || height === 'fit-content') {
el.style.height = height;
} else {
el.style.height = typeof height === 'number' ? `${height}px` : height;
}
}
}
/**
* 根据内容自动调整弹窗宽度
* @param recenter 是否重新计算定位(例如保持居中),默认 true
*/
public fitWidth(recenter: boolean = false) {
// 1. 设置为 fit-content 以获取自然宽度,高度保持不变
this.element.style.width = 'fit-content';
// 2. 如果需要重新定位
if (recenter) {
this.initPosition();
}
}
@@ -427,4 +451,4 @@ export class BimDialog implements IBimComponent {
public destroy() {
this.close();
}
}
}

View File

@@ -1,10 +1,122 @@
/* 树容器 */
.bim-tree {
width: 100%;
overflow: auto;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden; /* 内部滚动 */
font-size: 14px;
color: var(--bim-ui_text_primary, #333);
user-select: none; /* 防止双击选中文字 */
position: relative; /* 为下拉框定位 */
}
/* 搜索区域 */
.bim-tree-search {
padding: 8px;
/* border-bottom: 1px solid var(--bim-ui_border_color, #d9d9d9); */
/* 背景透明,使用父布局背景 */
background-color: transparent;
flex-shrink: 0;
position: relative;
}
.bim-tree-search-wrapper {
position: relative;
width: 100%;
display: flex;
align-items: center;
}
.bim-tree-search-icon {
position: absolute;
left: 8px;
width: 16px;
height: 16px;
color: var(--bim-ui_text_secondary, #999);
pointer-events: none; /* 让点击穿透到 input */
display: flex;
align-items: center;
justify-content: center;
}
.bim-tree-search-icon svg {
width: 100%;
height: 100%;
}
.bim-tree-search-input {
width: 100%;
height: 32px;
/* 左侧留出图标位置: 8px left + 16px icon + 4px gap = 28px */
padding: 4px 8px 4px 30px;
border: 1px solid var(--bim-ui_border_color, #d9d9d9);
border-radius: 4px;
outline: none;
font-size: 13px;
color: inherit;
/* 输入框背景半透明或白色,看设计,通常输入框本身还是需要背景的,否则文字看不清 */
background-color: var(--bim-ui_bg_color, #fff);
transition: all 0.2s;
box-sizing: border-box;
}
.bim-tree-search-input:focus {
border-color: var(--bim-primary_color, #1890ff);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
/* 搜索结果下拉框 */
.bim-tree-search-results {
position: absolute;
top: 100%;
left: 8px;
right: 8px;
background-color: var(--bim-ui_bg_color, #fff);
border: 1px solid var(--bim-ui_border_color, #eee);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
z-index: 10;
display: none;
}
.bim-tree-search-results.is-visible {
display: block;
}
.bim-tree-search-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid rgba(0,0,0,0.03);
}
.bim-tree-search-item:last-child {
border-bottom: none;
}
.bim-tree-search-item:hover {
background-color: var(--bim-ui_bg_hover, #f5f5f5);
}
.bim-tree-search-item-title {
font-weight: 500;
display: block;
}
.bim-tree-search-item-path {
font-size: 12px;
color: var(--bim-ui_text_secondary, #999);
margin-top: 2px;
display: block;
}
/* 树内容区域 */
.bim-tree-content {
flex: 1;
overflow-y: auto;
padding: 4px 0;
}
/* 节点行容器 */
@@ -146,16 +258,66 @@
.bim-tree-title {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* 移除省略号样式,许文本撑开 */
/* overflow: hidden; */
/* text-overflow: ellipsis; */
}
/* 子节点容器 */
.bim-tree-children {
display: none;
overflow: hidden;
overflow: visible; /* 允许子节点撑开宽度 */
}
.bim-tree-children.is-visible {
display: block;
}
/* 选中高亮状态 */
.bim-tree-node-content.is-selected {
background-color: var(--bim-ui_bg_selected, rgba(24, 144, 255, 0.2));
color: var(--bim-primary_color, #1890ff);
}
/* 节点操作栏 */
.bim-tree-node-actions {
display: none; /* 默认隐藏 */
align-items: center;
margin-left: 8px; /* 与文字的间距 */
flex-shrink: 0;
}
.bim-tree-node-content.is-selected .bim-tree-node-actions {
display: flex; /* 选中时显示 */
}
/* 树内容区域 */
.bim-tree-content {
flex: 1;
overflow: auto; /* 支持横向和纵向滚动 */
padding: 4px 0;
}
/* 节点行容器 */
.bim-tree-node {
display: flex;
flex-direction: column;
width: fit-content; /* 关键:让节点容器跟随内容宽度 */
min-width: 100%;
}
/* 节点内容行 (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;
/* 关键:允许宽度根据内容撑开,且至少占满容器 */
width: fit-content;
min-width: 100%;
box-sizing: border-box;
}

View File

@@ -1,6 +1,6 @@
import { IBimComponent } from '../../types/component';
import { ThemeConfig } from '../../themes/types';
import { localeManager } from '../../services/locale';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import { TreeOptions, TreeNodeConfig, TreeNodeCheckState } from './types';
import { BimTreeNode } from './tree-node';
@@ -14,15 +14,21 @@ import './index.css';
export class BimTree implements IBimComponent {
public element: HTMLElement;
private contentElement: HTMLElement; // 树内容容器
private searchInput: HTMLInputElement | null = null;
private searchResults: HTMLElement | null = null;
private options: TreeOptions;
private nodeMap: Map<string, BimTreeNode> = new Map();
private rootNodes: BimTreeNode[] = [];
private selectedNode: BimTreeNode | null = null; // 当前选中的高亮节点
// 订阅清理函数
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
// 事<><E4BA8B><EFBFBD>回调 (由 Manager 注入)
private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;
// 事件回调 (由 Manager 注入)
public onNodeCheck?: (node: BimTreeNode) => void;
public onNodeSelect?: (node: BimTreeNode) => void;
public onNodeExpand?: (node: BimTreeNode) => void;
@@ -32,11 +38,75 @@ export class BimTree implements IBimComponent {
checkable: true,
checkStrictly: true,
indent: 24,
defaultExpandAll: false,
defaultExpandAll: true,
enableSearch: false,
searchPlaceholder: 'tree.searchPlaceholder',
...options
};
// 主容器
this.element = document.createElement('div');
this.element.className = 'bim-tree';
// 搜索区域
if (this.options.enableSearch) {
this.createSearchDOM();
}
// 内容容器
this.contentElement = document.createElement('div');
this.contentElement.className = 'bim-tree-content';
this.element.appendChild(this.contentElement);
// 初始化回调
if (options.onNodeCheck) this.onNodeCheck = options.onNodeCheck;
if (options.onNodeSelect) this.onNodeSelect = options.onNodeSelect;
if (options.onNodeExpand) this.onNodeExpand = options.onNodeExpand;
}
private createSearchDOM() {
const searchContainer = document.createElement('div');
searchContainer.className = 'bim-tree-search';
const wrapper = document.createElement('div');
wrapper.className = 'bim-tree-search-wrapper';
// 图标
const icon = document.createElement('span');
icon.className = 'bim-tree-search-icon';
icon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="m21 21l-4.343-4.343m0 0A8 8 0 1 0 5.343 5.343a8 8 0 0 0 11.314 11.314"/></svg>`;
wrapper.appendChild(icon);
this.searchInput = document.createElement('input');
this.searchInput.className = 'bim-tree-search-input';
this.searchInput.type = 'text';
this.searchInput.placeholder = t(this.options.searchPlaceholder || '搜索...');
// 绑定输入事件
this.searchInput.addEventListener('input', (e) => {
const query = (e.target as HTMLInputElement).value;
this.handleSearch(query);
});
wrapper.appendChild(this.searchInput);
searchContainer.appendChild(wrapper);
// 搜索结果容器
this.searchResults = document.createElement('div');
this.searchResults.className = 'bim-tree-search-results';
searchContainer.appendChild(this.searchResults);
this.element.appendChild(searchContainer);
// 点击外部关闭搜索结果
this.clickOutsideHandler = (e: MouseEvent) => {
if (this.searchResults &&
!this.searchResults.contains(e.target as Node) &&
!this.searchInput?.contains(e.target as Node)) {
this.searchResults.classList.remove('is-visible');
}
};
document.addEventListener('click', this.clickOutsideHandler);
}
public init(): void {
@@ -45,6 +115,119 @@ export class BimTree implements IBimComponent {
// 订阅系统事件
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
// 初始化主题
this.setTheme(themeManager.getTheme());
}
/**
* 处理搜索逻辑
*/
private handleSearch(query: string) {
if (!this.searchResults) return;
if (!query.trim()) {
this.searchResults.classList.remove('is-visible');
this.searchResults.innerHTML = '';
return;
}
const results: { node: BimTreeNode, label: string, path: string }[] = [];
const lowerQuery = query.toLowerCase();
this.nodeMap.forEach(node => {
// 直接使用 config.label不进行翻译
const label = node.config.label;
if (label.toLowerCase().includes(lowerQuery)) {
results.push({
node,
label,
path: this.getNodePath(node)
});
}
});
this.renderSearchResults(results);
}
/**
* 获取节点路径(面包屑)
*/
private getNodePath(node: BimTreeNode): string {
const path: string[] = [];
let current: BimTreeNode | null = node.parent; // 从父级开始
while (current) {
// 直接使用 label
path.unshift(current.config.label);
current = current.parent;
}
return path.join(' > ');
}
/**
* 渲染搜索结果列表
*/
private renderSearchResults(results: { node: BimTreeNode, label: string, path: string }[]) {
if (!this.searchResults) return;
this.searchResults.innerHTML = '';
if (results.length === 0) {
const noData = document.createElement('div');
noData.className = 'bim-tree-search-item';
noData.style.cursor = 'default';
noData.style.color = '#999';
noData.textContent = 'No results';
this.searchResults.appendChild(noData);
} else {
results.forEach(res => {
const item = document.createElement('div');
item.className = 'bim-tree-search-item';
const title = document.createElement('span');
title.className = 'bim-tree-search-item-title';
title.textContent = res.label;
const path = document.createElement('span');
path.className = 'bim-tree-search-item-path';
path.textContent = res.path;
item.appendChild(title);
if (res.path) item.appendChild(path);
item.addEventListener('click', () => {
this.revealNode(res.node);
});
this.searchResults!.appendChild(item);
});
}
this.searchResults.classList.add('is-visible');
}
/**
* 定位到指定节点
*/
public revealNode(node: BimTreeNode) {
// 1. 关闭搜索下拉
if (this.searchResults) {
this.searchResults.classList.remove('is-visible');
if (this.searchInput) this.searchInput.value = ''; // 可选:清空搜索框
}
// 2. 递归展开父节点
let current = node.parent;
while (current) {
current.toggleExpand(true); // 强制展开
current = current.parent;
}
// 3. 选中节点 (Select)
this.handleNodeSelect(node);
// 4. 滚动到可视区域
setTimeout(() => {
node.element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, 100);
}
/**
@@ -66,6 +249,9 @@ export class BimTree implements IBimComponent {
*/
public setLocales(): void {
this.nodeMap.forEach(node => node.updateLabel());
if (this.searchInput) {
this.searchInput.placeholder = t(this.options.searchPlaceholder || 'tree.searchPlaceholder');
}
}
public destroy(): void {
@@ -77,38 +263,45 @@ export class BimTree implements IBimComponent {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
if (this.clickOutsideHandler) {
document.removeEventListener('click', this.clickOutsideHandler);
this.clickOutsideHandler = null;
}
this.rootNodes.forEach(node => node.destroy());
this.rootNodes = [];
this.nodeMap.clear();
this.element.remove();
this.selectedNode = null;
}
/**
* 核心渲染逻辑
*/
private render(): void {
this.element.innerHTML = '';
this.contentElement.innerHTML = ''; // 清空内容区,而不是整个 element
this.nodeMap.clear();
this.rootNodes = [];
this.options.data.forEach(config => {
this.createNodeRecursively(config, null);
});
// 如果设置了 defaultExpandAll展开所有
if (this.options.defaultExpandAll) {
this.expandAll(true);
}
// 移除旧的 expandAll 调用,因为 expanded 状态已在 createNodeRecursively 中处理
}
/**
* 递归创建节点
*/
private createNodeRecursively(config: TreeNodeConfig, parent: BimTreeNode | null) {
// 处理展开状态优先级: config.expanded > defaultExpandAll
if (config.expanded === undefined) {
config.expanded = this.options.defaultExpandAll;
}
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); }
onClick: (n) => this.handleNodeSelect(n)
});
this.nodeMap.set(config.id, node);
@@ -117,7 +310,7 @@ export class BimTree implements IBimComponent {
parent.appendChild(node);
} else {
this.rootNodes.push(node);
this.element.appendChild(node.element);
this.contentElement.appendChild(node.element); // 挂载到 contentElement
}
if (config.children && config.children.length > 0) {
@@ -125,10 +318,27 @@ export class BimTree implements IBimComponent {
this.createNodeRecursively(childConfig, node);
});
}
// 如果是初始化渲染,需要处理 checkStrictly 的向上联动(因为数据里可能只给了子节点 checked父节点没给
// 这里做一个简单的后处理:如果 checkStrictly 开启,且当前节点 checked则触发一次联动
// 注意:这可能会导致性能问题,化做法是在所有节点创建完后统一计算一次状态
// 注意:这可能会导致性能问题,<EFBFBD><EFBFBD><EFBFBD>化做法是在所有节点创建完后统一计算一次状态
}
/**
* 处理节点选择 (高亮)
*/
private handleNodeSelect(node: BimTreeNode) {
// 如果之前有选中的,先取消选中
if (this.selectedNode && this.selectedNode !== node) {
this.selectedNode.setSelected(false);
}
// 设置当前为选中
node.setSelected(true);
this.selectedNode = node;
// 触发外部回调
if (this.onNodeSelect) this.onNodeSelect(node);
}
/**
@@ -152,7 +362,7 @@ export class BimTree implements IBimComponent {
updateChildren(child, state);
});
};
// 当前节点是 Checked 或 Unchecked子节点跟随
if (isChecked) {
updateChildren(node, TreeNodeCheckState.Checked);
@@ -179,7 +389,7 @@ export class BimTree implements IBimComponent {
} else {
current.setChecked(TreeNodeCheckState.Indeterminate, false);
}
current = current.parent;
}
}

View File

@@ -1,6 +1,4 @@
import { TreeNodeConfig, TreeNodeCheckState, TreeOptions } from './types';
import { t } from '../../services/locale';
/**
* 树节点类
* 负责渲染单个节点、处理交互和递归
@@ -11,39 +9,40 @@ export class BimTreeNode {
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
// UI Elements
private contentEl!: HTMLElement;
private switcherEl!: HTMLElement;
private checkboxEl: HTMLElement | null = null;
private titleEl!: HTMLElement;
private actionsEl!: HTMLElement; // 操作栏容器
private childrenContainer!: HTMLElement;
// 外部回调
private onExpandChange: (node: BimTreeNode) => void;
private onCheckChange: (node: BimTreeNode) => void;
private onNodeClick: (node: BimTreeNode) => void;
private renderActions?: (node: TreeNodeConfig) => HTMLElement | string;
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.renderActions = options.renderActions;
// 初始化状态
this.checkState = config.checked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked;
this.element = this.createDom(options);
}
) {
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
*/
@@ -55,33 +54,25 @@ export class BimTreeNode {
// 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 复选框 (可选)
@@ -89,7 +80,7 @@ export class BimTreeNode {
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;
@@ -102,8 +93,8 @@ export class BimTreeNode {
if (this.config.icon) {
const iconEl = document.createElement('span');
iconEl.className = 'bim-tree-icon';
iconEl.innerHTML = this.config.icon.includes('<svg')
? this.config.icon
iconEl.innerHTML = this.config.icon.includes('<svg')
? this.config.icon
: `<img src="${this.config.icon}" />`;
this.contentEl.appendChild(iconEl);
}
@@ -114,11 +105,26 @@ export class BimTreeNode {
this.updateLabel(); // 设置文本
this.contentEl.appendChild(this.titleEl);
// 1.5 操作栏 (Actions)
this.actionsEl = document.createElement('div');
this.actionsEl.className = 'bim-tree-node-actions';
this.actionsEl.addEventListener('click', (e) => {
e.stopPropagation(); // 防止点击操作栏触发选中/展开
});
this.contentEl.appendChild(this.actionsEl);
// 绑定整行点击
this.contentEl.addEventListener('click', (e) => {
e.stopPropagation();
if (this.config.disabled) return;
this.onNodeClick(this);
const action = this.config.clickAction || 'select';
if (action === 'expand') {
this.toggleExpand();
} else {
this.onNodeClick(this);
}
});
nodeEl.appendChild(this.contentEl);
@@ -128,8 +134,8 @@ export class BimTreeNode {
this.childrenContainer.className = 'bim-tree-children';
// 设置缩进
const indent = options.indent || 24;
this.childrenContainer.style.paddingLeft = `${indent}px`; // 每一级子<EFBFBD><EFBFBD><EFBFBD>器左移
this.childrenContainer.style.paddingLeft = `${indent}px`; // 每一级子器左移
if (this.config.expanded && hasChildren) {
this.childrenContainer.classList.add('is-visible');
}
@@ -139,11 +145,33 @@ export class BimTreeNode {
}
/**
* 更新显示文本 (国际化支持)
* 设置高亮选中状态 (Select 模式下)
*/
public setSelected(selected: boolean) {
if (selected) {
this.contentEl.classList.add('is-selected');
// 渲染自定义操作栏
if (this.renderActions) {
const content = this.renderActions(this.config);
this.actionsEl.innerHTML = '';
if (typeof content === 'string') {
this.actionsEl.innerHTML = content;
} else if (content instanceof HTMLElement) {
this.actionsEl.appendChild(content);
}
}
} else {
this.contentEl.classList.remove('is-selected');
this.actionsEl.innerHTML = ''; // 清空内容
}
}
/**
* 更新显示文本 (国际化支持) -> 移除国际化,直接显示
*/
public updateLabel() {
if (this.titleEl) {
this.titleEl.textContent = t(this.config.label);
this.titleEl.textContent = this.config.label;
}
}
@@ -151,7 +179,7 @@ export class BimTreeNode {
* 切换展开状态
*/
public toggleExpand(force?: boolean) {
if (this.config.children?.length === 0) return;
if (!this.config.children || this.config.children.length === 0) return;
const newState = force !== undefined ? force : !this.config.expanded;
this.config.expanded = newState;
@@ -174,7 +202,7 @@ export class BimTreeNode {
* 切换选中状态 (用户点击)
*/
public toggleCheck() {
// 如果当前是半选,点击变全选;如果全选,点击<EFBFBD><EFBFBD>未选;如果未选,点击变全选
// 如果当前是半选,点击变全选;如果全选,点击未选;如果未选,点击变全选
// 简化逻辑:只要不是 Checked点击都变 Checked如果是 Checked变 Unchecked
const newChecked = this.checkState !== TreeNodeCheckState.Checked;
this.setChecked(newChecked ? TreeNodeCheckState.Checked : TreeNodeCheckState.Unchecked, true);
@@ -203,9 +231,9 @@ export class BimTreeNode {
*/
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) {
@@ -220,13 +248,13 @@ export class BimTreeNode {
childNode.parent = this;
this.children.push(childNode);
this.childrenContainer.appendChild(childNode.element);
// 如果之前是隐藏的箭头,现在有了子节点,需要显示出来
if (this.children.length === 1) {
this.switcherEl.classList.remove('is-hidden');
}
}
/**
* 销毁
*/

View File

@@ -1,3 +1,5 @@
import type { BimTreeNode } from './tree-node';
/**
* 节点勾选状态枚举
*/
@@ -7,6 +9,11 @@ export enum TreeNodeCheckState {
Indeterminate = 2 // 半选
}
/**
* 节点点击行为类型
*/
export type NodeClickAction = 'select' | 'expand';
/**
* 树节点配置接口
*/
@@ -35,8 +42,11 @@ export interface TreeNodeConfig {
/** 自定义业务数据 */
data?: any;
/** 是否是叶子节点 (用于异步加载场景,暂<EFBFBD><EFBFBD><EFBFBD>接口) */
/** 是否是叶子节点 (用于异步加载场景,暂接口) */
isLeaf?: boolean;
/** 点击整行的行为 (默认 'select') */
clickAction?: NodeClickAction;
}
/**
@@ -61,4 +71,25 @@ export interface TreeOptions {
/** 缩进宽度 (像素,默认 24) */
indent?: number;
}
/** 是否启用搜索功能 (默认 false) */
enableSearch?: boolean;
/** 搜索框占位符 */
searchPlaceholder?: string;
/** 节点勾选回调 */
onNodeCheck?: (node: BimTreeNode) => void;
/** 节点选择回调 */
onNodeSelect?: (node: BimTreeNode) => void;
/** 节点展开/折叠回调 */
onNodeExpand?: (node: BimTreeNode) => void;
/**
* 选中时显示的自定义操作栏渲染函数
* 返回 HTML 字符串或 HTMLElement
*/
renderActions?: (node: TreeNodeConfig) => HTMLElement | string;
}

View File

@@ -26,12 +26,9 @@ export const enUS: TranslationDictionary = {
home: 'Home',
},
tree: {
modelStruct: 'Model Structure',
floor1: 'Level 1',
floor2: 'Level 2',
wall: 'Walls',
column: 'Columns',
window: 'Windows',
door: 'Doors',
}
searchPlaceholder: 'Please enter content to search',
},
constructTree: {
title: 'Construct Tree',
}
};

View File

@@ -27,15 +27,12 @@ export interface TranslationDictionary {
info: string;
home: string;
};
tree: {
modelStruct: string;
floor1: string;
floor2: string;
wall: string;
column: string;
window: string;
door: string;
}
tree: {
searchPlaceholder: string;
};
constructTree: {
title: string;
}
}
/**

View File

@@ -1,37 +1,34 @@
import { TranslationDictionary } from './types';
import {TranslationDictionary} from './types';
export const zhCN: TranslationDictionary = {
common: {
title: 'BimEngine',
description: '这是一个使用 BIM-ENGINE。',
openTestDialog: '打开测试弹窗',
openInfoDialog: '打开信息弹窗 (封装版)',
},
toolbar: {
home: '首页',
info: '信息',
location: '定位',
setting: '设置',
walk: '漫游',
walkPerson: '人视',
walkBird: '鸟瞰',
walkMenu: '菜单',
},
dialog: {
testTitle: '测试弹窗',
testContent: '<div style="padding: 10px;">这是一个 <b>可拖拽</b> 且 <b>可缩放</b> 的弹窗。<br><br>你可以尝试拖动标题栏,或者拖动右下角改变大小。</div>',
},
menu: {
info: '信息',
home: '首页',
},
tree: {
modelStruct: '模型结构',
floor1: '一楼',
floor2: '二楼',
wall: '墙体',
column: '柱子',
window: '窗户',
door: '门',
}
common: {
title: 'BimEngine',
description: '这是一个使用 BIM-ENGINE。',
openTestDialog: '打开测试弹窗',
openInfoDialog: '打开信息弹窗 (封装版)',
},
toolbar: {
home: '首页',
info: '信息',
location: '定位',
setting: '设置',
walk: '漫游',
walkPerson: '人视',
walkBird: '鸟瞰',
walkMenu: '菜单',
},
dialog: {
testTitle: '测试弹窗',
testContent: '<div style="padding: 10px;">这是一个 <b>可拖拽</b> 且 <b>可缩放</b> 的弹窗。<br><br>你可以尝试拖动标题栏,或者拖动右下角改变大小。</div>',
},
menu: {
info: '信息',
home: '首页',
},
tree: {
searchPlaceholder: '请输入要搜索的内容',
},
constructTree: {
title: '目录树',
}
};

View File

@@ -0,0 +1,200 @@
import type {ButtonGroupColors, ButtonConfig} from '../components/button-group/index.type';
import {Toolbar} from '../components/button-group/toolbar';
import {BimComponent} from '../core/component';
import type {BimEngine} from '../bim-engine';
import {BimButtonGroup} from "../components/button-group";
import {BimTree} from "../components/tree";
import {TreeNodeConfig} from "../components/tree/types.ts";
import {BimDialog} from "../components/dialog";
const MOCK_STRUCT_DATA: TreeNodeConfig[] =[
{
id: 'root',
label: '全部构件',
expanded: true,
clickAction: 'expand',
children: [
{
id: 'level-1',
label: '一层',
expanded: false,
icon:'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M20.73 16.52V7.59a.7.7 0 0 0-.08-.33a.74.74 0 0 0-.36-.36l-8-3.58a.75.75 0 0 0-.62 0l-8 3.58a.8.8 0 0 0-.44.69v8.82a.83.83 0 0 0 .44.69l8 3.58a.72.72 0 0 0 .62 0l8-3.58a.77.77 0 0 0 .44-.58m-16-7.78l6.5 2.92v7.18l-6.5-2.91Zm8 2.92l6.5-2.92v7.19l-6.5 2.91ZM12 4.82l6.17 2.77L12 10.35L5.83 7.59Z"/></svg>',
clickAction: 'expand',
children: [
{ id: 'l1-wall', label: '墙体128'},
{ id: 'l1-column', label: '柱46' },
{ id: 'l1-beam', label: '梁82' },
{ id: 'l1-slab', label: '楼板12' },
{ id: 'l1-door', label: '门24' },
{ id: 'l1-window', label: '窗36' }
]
},
{
id: 'level-2',
label: '二层',
expanded: false,
clickAction: 'expand',
children: [
{ id: 'l2-wall', label: '墙体141' },
{ id: 'l2-column', label: '柱52' },
{ id: 'l2-beam', label: '梁90' },
{ id: 'l2-slab', label: '楼板12' },
{ id: 'l2-door', label: '门18' },
{ id: 'l2-window', label: '窗40' }
]
},
{
id: 'level-3',
label: '三层',
expanded: false,
clickAction: 'expand',
children: [
{ id: 'l3-wall', label: '墙体136' },
{ id: 'l3-column', label: '柱48' },
{ id: 'l3-beam', label: '梁88' },
{ id: 'l3-slab', label: '楼板12' },
{ id: 'l3-door', label: '门16' },
{ id: 'l3-window', label: '窗38' }
]
},
{
id: 'level-roof',
label: '屋面层',
expanded: false,
clickAction: 'expand',
children: [
{ id: 'rf-slab', label: '屋面板6' },
{ id: 'rf-beam', label: '屋面梁24' },
{ id: 'rf-parapet', label: '女儿墙18' }
]
}
]
}
];
/**
* 底部工具栏管理器 (ToolbarManager)
* 仅负责管理底部工具栏实例。
*/
export class ConstructTreeManagerBtn extends BimComponent {
private toolbar: Toolbar | null = null;
private toolbarContainer: HTMLElement | null = null;
private container: HTMLElement;
private dialog: BimDialog | null = null;
constructor(engine: BimEngine, container: HTMLElement) {
super(engine);
this.container = container;
this.init();
}
private init() {
// 创建底部工具栏专用容器
this.toolbarContainer = document.createElement('div');
this.toolbarContainer.id = 'bim-construct-tree';
this.container.appendChild(this.toolbarContainer);
this.toolbar = new BimButtonGroup({
container: this.toolbarContainer,
showLabel: false,
direction: 'column',
position: 'top-left', // 底部居中
align: 'vertical', // 图标在上
expand: 'up' // 向上展开
});
this.toolbar.init();
this.toolbar.setEngine(this.engine);
this.toolbar.addGroup('construct-tree');
this.toolbar.addButton({
id: 'construct-tree-btn',
groupId: 'construct-tree',
type: 'button',
label: 'construct-tree',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M15 21v-3h-4V8H9v3H2V3h7v3h6V3h7v8h-7V8h-2v8h2v-3h7v8zM4 5v4zm13 10v4zm0-10v4zm0 4h3V5h-3zm0 10h3v-4h-3zM4 9h3V5H4z"/></svg>',
onClick: () => {
this.openConstructTreeDialog()
}
});
this.toolbar.render();
}
public openConstructTreeDialog() {
this.setVisible(false)
const tree = new BimTree({
data: MOCK_STRUCT_DATA,
checkable: true,
indent: 0,
enableSearch: true,
checkStrictly: true,
defaultExpandAll: true,
renderActions:(_node)=>{
return '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-miterlimit="10" stroke-width="1.5" d="m14.828 6.343l2.829 2.829m1.414-1.415L8.464 18.364l-3.535.707l.707-3.536L16.243 4.93a2 2 0 0 1 2.828 2.828Z"/></svg>'
},
onNodeCheck: (node) => {
console.log('onNodeCheck', node);
},
onNodeSelect: (node) => {
console.log('onNodeSelect', node);
},
onNodeExpand: (node) => {
console.log('onNodeExpand', node);
this.dialog?.fitWidth()
},
});
tree.init();
this.dialog = this.engine.dialog!.create({
title: 'constructTree.title',
minWidth: 300,
height: 400,
content: tree.element,
position: {x: 20, y: 20},
resizable: false,
// 关键:绑定生命周期
onClose: () => {
this.setVisible(true)
}
});
this.dialog?.fitWidth()
}
public refresh() {
this.toolbar?.render();
}
public destroy() {
this.toolbar?.destroy();
this.toolbar = null;
}
// --- 转发 API ---
public addGroup(groupId: string, beforeGroupId?: string) {
this.toolbar?.addGroup(groupId, beforeGroupId);
this.toolbar?.render();
}
public addButton(config: ButtonConfig) {
this.toolbar?.addButton(config);
this.toolbar?.render();
}
public setButtonVisibility(id: string, v: boolean) {
this.toolbar?.updateButtonVisibility(id, v);
}
public setShowLabel(show: boolean) {
this.toolbar?.setShowLabel(show);
}
public setVisible(visible: boolean) {
if (this.toolbarContainer) {
this.toolbarContainer.style.visibility = visible ? 'visible' : 'hidden';
}
}
public setBackgroundColor(color: string) {
this.toolbar?.setBackgroundColor(color);
}
public setColors(colors: ButtonGroupColors) {
this.toolbar?.setColors(colors);
}
}

View File

@@ -1,76 +0,0 @@
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 实例由它们各自的生命周期管理
// 这里不需要销毁它们,除非我们持有了全局引用
}
}

View File

@@ -1,57 +0,0 @@
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持有并销毁
// 这里可以做一些全局清理工作
}
}

View File

@@ -3,14 +3,14 @@ import dts from 'vite-plugin-dts';
import { resolve } from 'path';
import cssInjectedByJs from 'vite-plugin-css-injected-by-js';
export default defineConfig(({ command }) => {
export default defineConfig(() => {
return {
plugins: [
// 移除 Vue 插件
dts({
include: ['src'],
exclude: [
'src/**/*.es.js',
'src/**/*.es.js',
'src/bim-engine-sdk.es.js',
'**/*.es.js'
], // 排除第三方 SDK 文件,避免类型分析错误
@@ -40,4 +40,4 @@ export default defineConfig(({ command }) => {
emptyOutDir: true,
},
};
});
});