添加折叠面板

This commit is contained in:
yuding
2025-12-22 15:39:58 +08:00
parent 005535a26d
commit ed0414c75b
29 changed files with 2759 additions and 1416 deletions

View File

@@ -0,0 +1,4 @@
---
alwaysApply: true
---
你是一个资深的前端工程师我这个项目需要你每次都看下AI_COLLABORATION.md文件里面有项目的所有信息

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

View File

@@ -461,28 +461,19 @@ interface IBimComponent {
- **BimEngine**: 总控制器,通过 Manager 统一管理所有组件
#### 为什么必须通过 Manager
1. **统一管理**: Manager 负责组件的生命周期管理,确保资源正确释放
1. **强制统一管理**: SDK 入口不再导出组件类(如 `BimDialog`),物理上切断了直接实例化的可能。
2. **主题和语言**: Manager 统一应用主题和国际化,保证一致性
3. **事件总线**: Manager 可以监听和发送事件,实现组件间解耦通信
- 简单场景:直接调用 Manager 方法
- 复杂场景:通过事件总线进行发布订阅
4. **容器管理**: Manager 管理组件的挂载容器,避免冲突
5. **API 封装**: Manager 提供统一的公共 API隐藏组件实现细节
#### 使用示例
**❌ 错误方式 - 直接使用组件:**
**❌ 错误方式 - 尝试直接导入组件:**
```typescript
// 错误:直接创建和使用组件
import { BimDialog } from 'bim-engine-sdk';
const dialog = new BimDialog({
container: document.getElementById('container'),
title: '测试弹窗',
content: '这是内容'
});
dialog.init();
// 问题:没有通过 Manager 管理,无法统一应用主题、语言等
// 错误:BimDialog 类未导出,会导致编译错误
import { BimDialog } from 'bim-engine-sdk';
// Error: Module 'bim-engine-sdk' has no exported member 'BimDialog'.
```
**✅ 正确方式 - 通过 Manager 使用:**
@@ -500,41 +491,15 @@ const dialog = engine.dialog.create({
title: '测试弹窗',
content: '这是内容'
});
// 优势:
// 1. 自动应用当前主题
// 2. 自动应用当前语言
// 3. 统一管理弹窗实例
// 4. 可以监听事件总线
```
**✅ 另一个正确示例 - 工具栏按钮:**
```typescript
// 正确:通过 ToolbarManager 操作工具栏
import { BimEngine } from 'bim-engine-sdk';
const engine = new BimEngine('container');
// 通过 ToolbarManager 添加按钮
engine.toolbar.addButton({
id: 'my-button',
groupId: 'group-1',
type: 'button',
label: 'toolbar.myButton',
icon: '<svg>...</svg>',
onClick: (button) => {
console.log('按钮被点击');
}
});
// 通过 ToolbarManager 控制可见性
engine.toolbar.setButtonVisibility('my-button', false);
```
#### 组件导出说明
虽然 `src/index.ts` 中导出了 `BimButtonGroup` 和 `Toolbar` 组件,但这是为了:
- 高级用户需要完全自定义的场景
- 内部 Manager 的实现需要
- **不推荐** 外部用户直接使用,应该通过 Manager
`src/index.ts` **仅导出** `BimEngine` 主类和必要的类型定义(如 `DialogOptions`)。
所有具体的组件类(如 `BimDialog`、`Toolbar`)均视为**内部实现细节**,不对外暴露。这意味着:
- 用户不能继承这些组件类进行扩展。
- 用户必须依赖 SDK 提供的 Manager API。
- 这保证了 SDK 内部架构的封闭性和稳定性。
### 4.1 Manager 类清单
@@ -545,7 +510,8 @@ 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` |
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 (组合 Dialog 和 Tree),管理 Tree 实例 | `BimComponent` |
| `ModelTreeManager` | `src/managers/model-tree-manager.ts` | 模型树业务管理器 | `BimComponent` |
| `PropertyPanelManager` | `src/managers/property-panel-manager.ts` | 属性面板业务管理器 (演示 Collapse) | `BimComponent` |
### 4.2 组件类清单
@@ -559,7 +525,8 @@ engine.toolbar.setButtonVisibility('my-button', false);
| `BimRightKey` | `src/components/right-key/index.ts` | 右键浮层容器 | `IBimComponent` |
| `BimMenu` | `src/components/menu/index.ts` | 通用菜单列表 | `IBimComponent` |
| `BimTree` | `src/components/tree/index.ts` | 通用树形组件 | `IBimComponent` |
| `BimTab` | `src/components/tab/index.ts` | 固定标签页组件(无运行时增删,当前在 ConstructTreeManagerBtn 内直接使用) | `IBimComponent` |
| `BimTab` | `src/components/tab/index.ts` | 固定标签页组件 | `IBimComponent` |
| `BimCollapse` | `src/components/collapse/index.ts` | 折叠面板组件 | `IBimComponent` |
### 4.3 服务类清单

1
Untitled Normal file
View File

@@ -0,0 +1 @@
AI_COLLABORATION.md

View File

@@ -159,6 +159,14 @@
</div>
</div>
<!-- 6. 功能面板 -->
<div class="control-group">
<h2>📑 功能面板 (Panels)</h2>
<div class="btn-container">
<button onclick="openPropertyPanel()">属性面板</button>
</div>
</div>
<!-- 5. 3D 引擎 -->
<div class="control-group">
<h2>🎮 3D 引擎 (Engine3D)</h2>
@@ -386,6 +394,17 @@
}
}
/**
* 打开属性面板
*/
function openPropertyPanel() {
if (!engine || !engine.propertyPanel) {
console.error('Property panel not available');
return;
}
engine.propertyPanel.show();
}
/**
* 更新引擎状态显示
*/

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

362
dist/index.d.ts vendored
View File

@@ -1,7 +1,7 @@
/**
* 通用按钮组组件 (BimButtonGroup)
*/
export declare class BimButtonGroup implements IBimComponent {
declare class BimButtonGroup implements IBimComponent {
private container;
private options;
private groups;
@@ -156,6 +156,7 @@ export declare class BimEngine extends EventEmitter {
dialog: DialogManager | null;
engine: EngineManager | null;
rightKey: RightKeyManager | null;
propertyPanel: PropertyPanelManager | null;
constructor(container: HTMLElement | string, options?: {
locale?: LocaleType;
theme?: ThemeType;
@@ -172,121 +173,75 @@ export declare class BimEngine extends EventEmitter {
}
/**
* 通用菜单列表组件
* 负责渲染一组菜单项,支持分组、排序、图标、快捷键提示和递归多级子菜单。
* 它不包含定位逻辑,仅负责内容渲染。
* 树节点类
* 负责渲染单个节点、处理交互和递归
*/
export declare class BimMenu implements IBimComponent {
declare class BimTreeNode {
config: TreeNodeConfig;
element: HTMLElement;
private options;
private unsubscribeLocale;
private unsubscribeTheme;
private activeSubMenu;
constructor(options: MenuOptions);
children: BimTreeNode[];
parent: BimTreeNode | null;
checkState: TreeNodeCheckState;
private contentEl;
private switcherEl;
private checkboxEl;
private titleEl;
private actionsEl;
private childrenContainer;
private onExpandChange;
private onCheckChange;
private onNodeClick;
private renderActions?;
constructor(config: TreeNodeConfig, options: TreeOptions, callbacks: {
onExpand: (n: BimTreeNode) => void;
onCheck: (n: BimTreeNode) => void;
onClick: (n: BimTreeNode) => void;
});
/**
* 初始化组件
* 渲染 DOM 结构并订阅语言变更
* 创建节点 DOM
*/
init(): void;
private createDom;
/**
* 设置主题
* @param theme 全局主题配置
* 设置高亮选中状态 (Select 模式下)
*/
setTheme(theme: ThemeConfig): void;
setSelected(selected: boolean): void;
/**
* 响应语言变
* 重新渲染整个菜单以更新文本
* 更新显示文本 (国际化支持) -> 移除国际化,直接显示
*/
setLocales(): void;
updateLabel(): void;
/**
* 销毁组件
* 清理事件监听、子菜单和 DOM 元素
* 切换展开状态
*/
toggleExpand(force?: boolean): void;
/**
* 切换选中状态 (用户点击)
*/
toggleCheck(): void;
/**
* 设置选中状态 (API调用或联动)
* @param state 新状态
* @param fireEvent 是否触发事件
*/
setChecked(state: TreeNodeCheckState, fireEvent?: boolean): void;
/**
* 更新复选框 UI 样式
*/
updateCheckboxUI(): void;
/**
* 添加子节点实例
*/
appendChild(childNode: BimTreeNode): void;
/**
* 销毁
*/
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?;
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 状态变更
*/
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';
/** 按钮配置 */
declare interface ButtonConfig {
export declare interface ButtonConfig {
id: string;
type: ButtonType;
label: string;
@@ -305,11 +260,6 @@ declare interface ButtonConfig {
minWidth?: number;
}
export declare interface ButtonGroup {
id: string;
buttons: OptButton[];
}
declare interface ButtonGroupColors {
backgroundColor?: string;
btnBackgroundColor?: string;
@@ -352,10 +302,40 @@ export declare interface ButtonGroupOptions extends ButtonGroupColors {
declare type ButtonType = 'button' | 'menu';
export declare interface ClickPayload {
button: OptButton;
action: 'activate' | 'deactivate' | 'trigger';
isActive?: boolean;
export declare interface CollapseItemConfig {
/** 唯一标识符 */
id: string;
/** 标题文本的翻译键 (例如 'panel.attributes') */
title: string;
/** 内容: HTML字符串 或 HTMLElement */
content: string | HTMLElement;
/** 标题栏左侧图标 (SVG 字符串, 可选) */
icon?: string;
/** 标题栏右侧额外内容 (可选) */
extra?: string | HTMLElement;
/** 是否禁用 */
disabled?: boolean;
/** 自定义类名 */
className?: string;
}
export declare interface CollapseOptions {
/** 挂载容器 */
container: HTMLElement | string;
/** 面板项列表 */
items: CollapseItemConfig[];
/** 是否开启手风琴模式 (默认 false) */
accordion?: boolean;
/** 初始展开的面板 ID 列表 */
activeIds?: string[];
/** 是否显示边框 (默认 true) */
bordered?: boolean;
/** 是否幽灵模式 (默认 false) */
ghost?: boolean;
/** 自定义类名 */
className?: string;
/** 切换面板时的回调 */
onChange?: (activeIds: string[]) => void;
}
/**
@@ -381,18 +361,6 @@ declare class ConstructTreeManagerBtn extends BimComponent {
setColors(colors: ButtonGroupColors): void;
}
export declare function createEngine(s) {
const e = s.version || "v1";
switch (e) {
case "v2":
return new Fc(s);
case "v1":
return new O_(s);
:
return console.warn(`Version '${e}' not found. Falling back to v2.`), new Fc(s);
}
}
/**
* 弹窗颜色配置
*/
@@ -446,7 +414,7 @@ declare class DialogManager extends BimComponent {
/**
* 弹窗配置选项接口
*/
declare interface DialogOptions extends DialogColors {
export declare interface DialogOptions extends DialogColors {
/** 弹窗挂载的父容器 */
container: HTMLElement;
/** 弹窗标题 */
@@ -480,12 +448,12 @@ declare interface DialogOptions extends DialogColors {
* 可以是预设的字符串位置(如 'center', 'top-left' 等),
* 也可以是具体的坐标对象 { x, y }
*/
declare type DialogPosition = 'center' | 'top-left' | 'top-center' | 'top-right' | 'left-center' | 'right-center' | 'bottom-left' | 'bottom-center' | 'bottom-right' | {
export declare type DialogPosition = 'center' | 'top-left' | 'top-center' | 'top-right' | 'left-center' | 'right-center' | 'bottom-left' | 'bottom-center' | 'bottom-right' | {
x: number;
y: number;
};
declare interface EngineEvents {
export declare interface EngineEvents {
'ui:open-dialog': {
id: string;
data?: any;
@@ -518,6 +486,9 @@ declare interface EngineEvents {
id: string;
expanded: boolean;
};
'ui:collapse-change': {
activeIds: string[];
};
'sys:theme-changed': {
theme: string;
};
@@ -611,7 +582,7 @@ declare type GroupPosition = 'center' | 'top-left' | 'top-center' | 'top-right'
* BIM 引擎组件通用接口
* 所有受引擎管理的 UI 组件都必须实现此接口
*/
declare interface IBimComponent {
export declare interface IBimComponent {
/**
* 初始化组件
* 用于创建 DOM、绑定事件、加载资源等
@@ -634,17 +605,6 @@ declare interface IBimComponent {
destroy(): void;
}
declare interface IRightKeyContent {
/**
* 获取组件的根 DOM 元素
*/
getElement(): HTMLElement;
/**
* 销毁组件
*/
destroy(): void;
}
declare type Listener<T = any> = (payload: T) => void;
/**
@@ -655,7 +615,7 @@ declare type LocaleType = 'zh-CN' | 'en-US';
/**
* 菜单项配置接口 (用于简化的对象配置)
*/
export declare interface MenuItemConfig {
declare interface MenuItemConfig {
id: string;
label: string;
onClick?: () => void;
@@ -667,24 +627,6 @@ export declare interface MenuItemConfig {
visible?: boolean;
}
/**
* <20><><EFBFBD>单组件配置选项
*/
declare interface MenuOptions {
/**
* 菜单项列表
* 可以是扁平数组,组件会根据 group 字段自动分组
*/
items: MenuItemConfig[];
/**
* 分组显示顺序
* 包含组 ID 的字符串数组。
* 例如: ['view', 'edit', 'tools']
* 未在此数组中定义的组将按默认顺序排在最后。
*/
groupOrder?: string[];
}
/**
* 模型加载选项
* 用于配置模型的位置、旋转和缩放
@@ -700,10 +642,24 @@ export declare interface ModelLoadOptions {
id?: string;
}
export declare interface OptButton extends ButtonConfig {
/**
* 节点点击行为类型
*/
export declare type NodeClickAction = 'select' | 'expand';
declare interface OptButton extends ButtonConfig {
children?: OptButton[];
}
declare class PropertyPanelManager extends BimComponent {
constructor(engine: BimEngine);
init(): void;
show(): void;
private createBaseInfoContent;
private createMaterialContent;
destroy(): void;
}
/**
* 右键菜单管理器 (RightKeyManager)
* 负责协调右键交互流程:
@@ -742,22 +698,11 @@ declare class RightKeyManager extends BimComponent {
private handleContextMenu;
}
declare interface RightKeyOptions {
/** 自定义 CSS 类名 */
className?: string;
/** 层级 (z-index) */
zIndex?: number;
/** 监听事件的容器 */
container?: HTMLElement;
/** 有效右键点击时的回调 */
onContext?: (e: MouseEvent) => void;
}
/**
* 全局主题配置接口
* 定义系统通用的语义化颜色
*/
declare interface ThemeConfig {
export declare interface ThemeConfig {
/** 主题名称 */
name: string;
/** 品牌色/主色 */
@@ -789,18 +734,7 @@ declare interface ThemeConfig {
/**
* 主题类型定义
*/
declare type ThemeType = 'dark' | 'light' | 'custom';
/**
* 底部工具栏 (Toolbar)
* BimButtonGroup 的子类,专门用于加载工具栏默认按钮。
*/
export declare class Toolbar extends BimButtonGroup {
/**
* 重写初始化,加载默认按钮
*/
init(): Promise<void>;
}
export declare type ThemeType = 'dark' | 'light' | 'custom';
/**
* 底部工具栏管理器 (ToolbarManager)
@@ -824,4 +758,74 @@ declare class ToolbarManager extends BimComponent {
setColors(colors: ButtonGroupColors): void;
}
/**
* 节点勾选状态枚举
*/
export declare enum TreeNodeCheckState {
Unchecked = 0,
Checked = 1,
Indeterminate = 2
}
/**
* 树节点配置接口
*/
export declare interface TreeNodeConfig {
/** 唯一标识符 */
id: string;
/** 显示文本的翻译键 */
label: string;
/** 节点图标 (SVG string 或 URL) */
icon?: string;
/** 子节点列表 */
children?: TreeNodeConfig[];
/** 初始展开状态 (默认 false) */
expanded?: boolean;
/** 初始选中状态 (默认 false) */
checked?: boolean;
/** 是否禁用 (默认 false) */
disabled?: boolean;
/** 自定义业务数据 */
data?: any;
/** 是否是叶子节点 (用于异步加载场景,暂留接口) */
isLeaf?: boolean;
/** 点击整行的行为 (默认 'select') */
clickAction?: NodeClickAction;
}
/**
* 树组件配置选项
*/
export declare interface TreeOptions {
/** 树的数据源 */
data: TreeNodeConfig[];
/** 是否显示复选框 (默认 true) */
checkable?: boolean;
/**
* 父子节点选中状态是否关联 (默认 true)
* true: 选中父选子,子全选自动选父
* false: 独立选中
*/
checkStrictly?: boolean;
/** 默认展开所有节点 (默认 false) */
defaultExpandAll?: boolean;
/** 缩进宽度 (像素,默认 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;
}
export { }

View File

@@ -0,0 +1,69 @@
# Collapse 组件文档
> **注意**: 本组件为 UI 组件,必须通过 Manager如 `PropertyPanelManager`)封装后使用,不可直接在业务代码中实例化。
## 1. 组件概述
`BimCollapse` 是一个通用的折叠面板组件,支持手风琴模式、自定义内容和标题。常用于属性面板、设置菜单等场景。
## 2. API 参考
### 2.1 配置项 `CollapseOptions`
```typescript
interface CollapseOptions {
container: HTMLElement | string; // 挂载容器
items: CollapseItemConfig[]; // 面板项列表
accordion?: boolean; // 是否开启手风琴模式 (默认 false)
activeIds?: string[]; // 初始展开的 ID 列表
bordered?: boolean; // 是否显示边框 (默认 true)
ghost?: boolean; // 是否幽灵模式 (默认 false)
className?: string; // 自定义类名
onChange?: (activeIds: string[]) => void; // 切换回调
}
```
### 2.2 面板项配置 `CollapseItemConfig`
```typescript
interface CollapseItemConfig {
id: string; // 唯一标识
title: string; // 标题翻译键 (例如 'panel.base')
content: string | HTMLElement; // 内容
icon?: string; // 标题图标 (SVG)
extra?: string | HTMLElement; // 标题右侧额外内容
disabled?: boolean; // 是否禁用
className?: string; // 自定义类名
}
```
### 2.3 方法
* `toggleItem(id: string)`: 切换指定面板的展开/折叠状态。
* `setLocales()`: 更新组件文本(通常自动调用)。
* `destroy()`: 销毁组件。
## 3. 使用示例 (在 Manager 中)
```typescript
import { BimCollapse } from '../components/collapse/index';
// 在 Manager 的方法中
const collapse = new BimCollapse({
container: this.containerElement,
accordion: true,
items: [
{
id: 'item1',
title: 'my.title.key', // 翻译键
content: '<div>Content Here</div>'
}
]
});
```
## 4. 国际化
组件会自动订阅 `localeManager`
* **标题**: `config.title` 必须是翻译键。
* **内容**: 如果内容包含文本,请确保内容生成时已翻译,或内容本身具有响应国际化的能力。

92
docs/components/tab.md Normal file
View File

@@ -0,0 +1,92 @@
## 1. 组件概述
- **名称**BimTab
- **位置**`src/components/tab/`
- **功能**:渲染固定标签页(不支持运行期增删),支持点击切换、禁用、可选内容托管。主题由全局 `ThemeManager` 提供,文案通过 `t()` 翻译。
- **使用方式**:直接通过构造函数 + `init()` 创建;本项目内目前仅在 `ConstructTreeManagerBtn` 弹窗中使用(无专门 Manager
## 2. 组件类 APIBimTab
- `constructor(options: TabOptions)`:创建实例并挂载到 `options.container`
- `init(): void`:渲染头部与内容,订阅主题/语言。
- `activateTab(tabId: string): void`:切换激活标签,触发 `onChange`
- `setTheme(theme: ThemeConfig): void`:应用主题变量。
- `setLocales(): void`:刷新标题文案。
- `destroy(): void`:解绑事件、取消订阅、移除 DOM。
## 3. 分化组件
- 当前无子类或变体,后续可扩展滚动、溢出折叠等能力。
## 4. 使用场景说明
- **ConstructTreeManagerBtn 弹窗**:顶部三段标签(构件/系统/空间),构件标签承载原有 Tree系统/空间暂为空容器。
- 适用于固定分区切换,内容简单或由外部回调控制。
## 5. UI 结构
```
div.bim-tab
div.bim-tab__nav [role=tablist]
button.bim-tab__item[role=tab][aria-selected][aria-disabled?]
span.bim-tab__icon? (可选)
span.bim-tab__title
div.bim-tab__content
div.bim-tab__panel[role=tabpanel][aria-labelledby=tab-xxx]
...
```
- 状态类:`.is-active``.is-disabled`
- 内容区:仅切换显示,不强制填充(可为空)。
## 6. 逻辑流程
1) `constructor`:创建根节点、头部、内容容器;缓存 tab 数据;挂载到传入容器。
2) `init`:渲染头部按钮与内容面板,设置初始激活;应用当前主题/语言;订阅主题、语言变更。
3) 交互:点击非禁用按钮 → `activateTab` 更新头部/面板状态 → 回调 `onChange`
4) 销毁:解绑点击监听、取消订阅、清空映射并移除 DOM。
## 7. 国际化
- 标题:`t(tab.title)`,若翻译键不存在则回退原文。
- 新增翻译键:`tab.component` / `tab.system` / `tab.space`(已在 `zh-CN.ts``en-US.ts` 注册)。
- `setLocales()`:遍历头部按钮刷新标题文案;订阅 `localeManager` 自动响应语言切换。
## 8. 主题支持
- 来自 `themeManager`,不从配置传入。
- 使用的变量(设置在根节点上):
- `--bim-tab-bg``--bim-tab-nav-bg`
- `--bim-tab-text``--bim-tab-text-secondary``--bim-tab-text-active`
- `--bim-tab-border``--bim-tab-hover-bg``--bim-tab-active-bg`
- `--bim-tab-icon`
- `setTheme(theme)`:根据 `ThemeConfig` 写入上述 CSS 变量。
## 9. 使用示例
```ts
const tabMount = document.createElement('div');
const tab = new BimTab({
container: tabMount,
tabs: [
{ id: 'component', title: 'tab.component', content: componentEl },
{ id: 'system', title: 'tab.system', content: systemEl },
{ id: 'space', title: 'tab.space', content: spaceEl },
],
activeId: 'component',
onChange: (id) => {
// 根据 id 做额外逻辑(如埋点、动态加载)
},
});
tab.init();
```
## 10. 实现细节
- 仅支持固定 tabs`TabOptions.tabs` 为初始化列表,不提供新增/删除接口。
- 内容托管两种方式:`content: HTMLElement`append`content: string`innerHTML。未提供内容时面板为空。
- 事件绑定:头部使用事件委托绑定 click销毁时移除。
- 访问性:头部 `role=tab``aria-selected``aria-disabled`;面板 `role=tabpanel``aria-labelledby` 对应头部 id。
- 主题/语言订阅:`localeManager.subscribe``themeManager.subscribe`,销毁时必须取消。
## 11. 类型定义
- 位置:`src/components/tab/index.type.ts`
- 主要类型:
- `TabItem`: `{ id; title; disabled?; icon?; content?; meta? }`
- `TabOptions`: `{ container; tabs; activeId?; onChange? }`
## 12. 文件清单
- `src/components/tab/index.ts`:组件实现
- `src/components/tab/index.type.ts`:类型定义
- `src/components/tab/index.css`:样式
- (无 Manager当前仅在 `ConstructTreeManagerBtn` 中直接使用

View File

@@ -5,6 +5,7 @@ import { DialogManager } from './managers/dialog-manager';
import { EngineManager } from './managers/engine-manager';
import { RightKeyManager } from './managers/right-key-manager';
import { ConstructTreeManagerBtn } from './managers/construct-tree-manager-btn';
import { PropertyPanelManager } from './managers/property-panel-manager';
import type { EngineOptions, ModelLoadOptions } from './components/engine';
import { localeManager } from './services/locale';
import { themeManager } from './services/theme';
@@ -25,6 +26,7 @@ export class BimEngine extends EventEmitter {
public dialog: DialogManager | null = null;
public engine: EngineManager | null = null; // 3D 引擎管理器
public rightKey: RightKeyManager | null = null; // 右键菜单管理器
public propertyPanel: PropertyPanelManager | null = null; // 属性面板 (演示 Collapse)
constructor(
@@ -78,6 +80,7 @@ export class BimEngine extends EventEmitter {
this.buttonGroup = new ButtonGroupManager(this, this.wrapper);
this.rightKey = new RightKeyManager(this, this.wrapper);
this.constructTreeBtn = new ConstructTreeManagerBtn(this, this.wrapper);
this.propertyPanel = new PropertyPanelManager(this);
// 初始主题
this.updateTheme(themeManager.getTheme());
@@ -101,6 +104,7 @@ export class BimEngine extends EventEmitter {
this.engine?.destroy();
this.dialog?.destroy();
this.rightKey?.destroy();
this.propertyPanel?.destroy();
this.container.innerHTML = '';
this.clear();
}

View File

@@ -0,0 +1 @@
export const infoIcon = '<svg viewBox="0 0 1024 1024"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z" fill="currentColor"/><path d="M464 336a48 48 0 1 0 96 0 48 48 0 1 0-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z" fill="currentColor"/></svg>';

View File

@@ -1,16 +1,13 @@
import type { ButtonConfig } from '../../../index.type';
import { ButtonConfig } from '../../../index.type';
import { infoIcon } from './icon';
/**
* 定位按钮配置
*/
export const infoButton: ButtonConfig = {
id: 'info',
groupId: 'group-2',
id: 'toolbar-info',
type: 'button',
label: 'toolbar.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>',
keepActive: false,
onClick: (button) => {
console.log('信息按钮被点击:', button.id);
icon: infoIcon,
onClick: () => {
// WORKAROUND: Dispatch a standard custom event on document
document.dispatchEvent(new CustomEvent('bim-demo:open-property-panel'));
}
};

View File

@@ -0,0 +1,121 @@
/* Root Container */
.bim-collapse {
background-color: var(--bim-bg-color, #ffffff);
border: 1px solid var(--bim-border-color, #d9d9d9);
border-radius: 4px;
font-size: 14px;
color: var(--bim-text-color, rgba(0, 0, 0, 0.88));
}
.bim-collapse.is-ghost {
background-color: transparent;
border: none;
}
.bim-collapse.is-ghost .bim-collapse-item {
border-bottom: none;
}
.bim-collapse.is-ghost .bim-collapse-header {
background-color: transparent;
padding-left: 0;
padding-right: 0;
}
.bim-collapse.is-ghost .bim-collapse-content {
background-color: transparent;
border-top: none;
}
/* Item */
.bim-collapse-item {
border-bottom: 1px solid var(--bim-border-color, #d9d9d9);
}
.bim-collapse-item:last-child {
border-bottom: none;
}
.bim-collapse-item.is-disabled .bim-collapse-header {
color: var(--bim-disabled-color, rgba(0, 0, 0, 0.25));
cursor: not-allowed;
}
/* Header */
.bim-collapse-header {
display: flex;
align-items: center;
padding: 12px 16px;
background-color: var(--bim-header-bg-color, rgba(0, 0, 0, 0.02));
cursor: pointer;
transition: all 0.3s;
position: relative;
}
.bim-collapse-header:hover {
background-color: var(--bim-header-hover-bg-color, rgba(0, 0, 0, 0.05));
}
/* Arrow Icon */
.bim-collapse-arrow {
margin-right: 12px;
font-size: 12px;
width: 12px;
height: 12px;
transition: transform 0.24s;
display: inline-flex;
align-items: center;
justify-content: center;
}
.bim-collapse-arrow svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.bim-collapse-item.is-active .bim-collapse-arrow {
transform: rotate(90deg);
}
/* Icon (User provided) */
.bim-collapse-icon {
margin-right: 8px;
display: inline-flex;
align-items: center;
}
.bim-collapse-icon svg {
width: 16px;
height: 16px;
fill: currentColor;
}
/* Title */
.bim-collapse-title {
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Extra */
.bim-collapse-extra {
margin-left: auto;
}
/* Content */
.bim-collapse-content {
overflow: hidden;
background-color: var(--bim-content-bg-color, #ffffff);
border-top: 1px solid var(--bim-border-color, #d9d9d9);
transition: height 0.2s ease-in-out, opacity 0.2s ease-in-out;
}
.bim-collapse-content.is-hidden {
display: none;
}
.bim-collapse-content-box {
padding: 16px;
}

View File

@@ -0,0 +1,244 @@
import './index.css';
import { CollapseOptions, CollapseItemConfig } from './types';
import { IBimComponent } from '../../types/component';
import { t, localeManager } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { ThemeConfig } from '../../themes/types';
/**
* 单个折叠面板项
*/
class BimCollapseItem {
public element: HTMLElement;
public headerEl!: HTMLElement;
public contentEl!: HTMLElement;
public contentBoxEl!: HTMLElement;
public arrowEl!: HTMLElement;
public titleEl!: HTMLElement;
private config: CollapseItemConfig;
private parent: BimCollapse;
constructor(config: CollapseItemConfig, parent: BimCollapse) {
this.config = config;
this.parent = parent;
this.element = this.createDom();
}
private createDom(): HTMLElement {
const itemEl = document.createElement('div');
itemEl.className = `bim-collapse-item ${this.config.className || ''}`;
if (this.config.disabled) itemEl.classList.add('is-disabled');
itemEl.dataset.id = this.config.id;
// <20><>部区域
this.headerEl = document.createElement('div');
this.headerEl.className = 'bim-collapse-header';
// 箭头图标
this.arrowEl = document.createElement('span');
this.arrowEl.className = 'bim-collapse-arrow';
this.arrowEl.innerHTML = `<svg viewBox="0 0 1024 1024"><path d="M288 192l448 320-448 320z"></path></svg>`;
this.headerEl.appendChild(this.arrowEl);
// 自定义图标 (可选)
if (this.config.icon) {
const iconEl = document.createElement('span');
iconEl.className = 'bim-collapse-icon';
iconEl.innerHTML = this.config.icon;
this.headerEl.appendChild(iconEl);
}
// 标题文本
this.titleEl = document.createElement('span');
this.titleEl.className = 'bim-collapse-title';
this.titleEl.textContent = t(this.config.title); // 初始翻译
this.headerEl.appendChild(this.titleEl);
// 额外内容 (可选,如右侧标签)
if (this.config.extra) {
const extraEl = document.createElement('div');
extraEl.className = 'bim-collapse-extra';
if (typeof this.config.extra === 'string') {
extraEl.innerHTML = this.config.extra;
} else {
extraEl.appendChild(this.config.extra);
}
this.headerEl.appendChild(extraEl);
}
// 点击事件
this.headerEl.addEventListener('click', () => {
if (this.config.disabled) return;
this.parent.toggleItem(this.config.id);
});
itemEl.appendChild(this.headerEl);
// 内容区域
this.contentEl = document.createElement('div');
this.contentEl.className = 'bim-collapse-content is-hidden';
this.contentBoxEl = document.createElement('div');
this.contentBoxEl.className = 'bim-collapse-content-box';
if (typeof this.config.content === 'string') {
this.contentBoxEl.innerHTML = this.config.content;
} else {
this.contentBoxEl.appendChild(this.config.content);
}
this.contentEl.appendChild(this.contentBoxEl);
itemEl.appendChild(this.contentEl);
return itemEl;
}
public updateLocale() {
if (this.titleEl) {
this.titleEl.textContent = t(this.config.title);
}
}
public setActive(isActive: boolean) {
if (isActive) {
this.element.classList.add('is-active');
this.contentEl.classList.remove('is-hidden');
// 简单的动画处理:设置 height
// 实际生产中可能需要更复杂的 JS 动画库或 transitionend 事件处理
// 这里依赖 CSS transition
} else {
this.element.classList.remove('is-active');
this.contentEl.classList.add('is-hidden');
}
}
}
/**
* 折叠面板组件
*/
export class BimCollapse implements IBimComponent {
private element: HTMLElement;
private options: CollapseOptions;
private items: Map<string, BimCollapseItem> = new Map();
private activeIds: Set<string> = new Set();
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
constructor(options: CollapseOptions) {
this.options = {
bordered: true,
accordion: false,
...options
};
this.element = document.createElement('div');
this.element.className = `bim-collapse ${this.options.className || ''}`;
if (!this.options.bordered) this.element.style.border = 'none';
if (this.options.ghost) this.element.classList.add('is-ghost');
const container = typeof this.options.container === 'string'
? document.getElementById(this.options.container)
: this.options.container;
if (container) {
container.appendChild(this.element);
}
// 初始化激活的 ID
if (this.options.activeIds) {
this.options.activeIds.forEach(id => this.activeIds.add(id));
}
this.init();
}
public init() {
// 创建子项
this.options.items.forEach(itemConfig => {
const item = new BimCollapseItem(itemConfig, this);
this.items.set(itemConfig.id, item);
this.element.appendChild(item.element);
// 设置初始状态
if (this.activeIds.has(itemConfig.id)) {
item.setActive(true);
}
});
// 订阅语言变更
this.unsubscribeLocale = localeManager.subscribe(() => {
this.setLocales();
});
// 订阅主题变更
this.unsubscribeTheme = themeManager.subscribe((theme) => {
this.setTheme(theme);
});
// 初始应用主题
this.setTheme(themeManager.getTheme());
}
public toggleItem(id: string) {
const isActive = this.activeIds.has(id);
if (this.options.accordion) {
// 手风琴模式:关闭其他所有,只展开目标
this.activeIds.clear();
if (!isActive) {
this.activeIds.add(id);
}
} else {
// 普通模式:切换目标状态
if (isActive) {
this.activeIds.delete(id);
} else {
this.activeIds.add(id);
}
}
this.refreshState();
if (this.options.onChange) {
this.options.onChange(Array.from(this.activeIds));
}
}
private refreshState() {
this.items.forEach((item, id) => {
item.setActive(this.activeIds.has(id));
});
}
public setTheme(theme: ThemeConfig): void {
const style = this.element.style;
style.setProperty('--bim-bg-color', theme.panelBackground);
style.setProperty('--bim-border-color', theme.border);
style.setProperty('--bim-text-color', theme.textPrimary);
// 头部默认背景色使用 componentBackground
style.setProperty('--bim-header-bg-color', theme.componentHover);
style.setProperty('--bim-header-hover-bg-color', theme.componentHover);
style.setProperty('--bim-content-bg-color', theme.panelBackground);
style.setProperty('--bim-disabled-color', theme.textSecondary);
}
public setLocales(): void {
this.items.forEach(item => item.updateLocale());
}
public destroy(): void {
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
this.element.remove();
this.items.clear();
}
}

View File

@@ -0,0 +1,49 @@
export interface CollapseItemConfig {
/** 唯一标识符 */
id: string;
/** 标题文本的翻译键 (例如 'panel.attributes') */
title: string;
/** 内容: HTML字符串 或 HTMLElement */
content: string | HTMLElement;
/** 标题栏左侧图标 (SVG 字符串, 可选) */
icon?: string;
/** 标题栏右侧额外内容 (可选) */
extra?: string | HTMLElement;
/** 是否禁用 */
disabled?: boolean;
/** 自定义类名 */
className?: string;
}
export interface CollapseOptions {
/** 挂载容器 */
container: HTMLElement | string;
/** 面板项列表 */
items: CollapseItemConfig[];
/** 是否开启手风琴模式 (默认 false) */
accordion?: boolean;
/** 初始展开的面板 ID 列表 */
activeIds?: string[];
/** 是否显示边框 (默认 true) */
bordered?: boolean;
/** 是否幽灵模式 (默认 false) */
ghost?: boolean;
/** 自定义类名 */
className?: string;
/** 切换面板时的回调 */
onChange?: (activeIds: string[]) => void;
}

View File

@@ -62,7 +62,6 @@
.bim-dialog-content {
flex: 1;
padding: 10px;
overflow: auto;
font-size: 14px;
color: var(--bim-dialog-text-color);
@@ -93,4 +92,4 @@
.bim-dialog-resize-handle:hover::after {
border-color: #fff;
}
}

View File

@@ -8,6 +8,14 @@ import { createEngine as createEngineSDK } from '../../bim-engine-sdk.es.js';
// 重新导出类型,方便外部引用
export type { EngineOptions, ModelLoadOptions };
/**
* 创建 Engine 实例的工厂函数
* 兼容旧代码直接 import { createEngine } 的方式
*/
export const createEngine = (options: EngineOptions) => {
return new Engine(options);
};
/**
* 3D 引擎组件
* 负责创建和管理第三方 3D 引擎实例

View File

@@ -0,0 +1,108 @@
.bim-tab {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: transparent;
color: var(--bim-tab-text, #e6e6e6);
}
.bim-tab__nav {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 4px 0;
background: transparent;
}
.bim-tab__item {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 4px 0;
border: none;
border-radius: 0;
background: transparent;
color: var(--bim-tab-text, #e6e6e6);
cursor: pointer;
transition: color 0.2s ease, border-color 0.2s ease;
font-size: 14px;
border-bottom: 2px solid transparent;
}
.bim-tab__item:hover:not(.is-disabled):not(.is-active) {
color: var(--bim-tab-text, #e6e6e6);
border-bottom-color: var(--bim-tab-border, rgba(255, 255, 255, 0.15));
}
.bim-tab__item.is-active {
color: var(--bim-tab-text-active, #4da3ff);
border-bottom-color: var(--bim-tab-text-active, #4da3ff);
}
.bim-tab__item.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.bim-tab__icon {
width: 16px;
height: 16px;
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--bim-tab-icon, currentColor);
}
.bim-tab__icon svg {
width: 100%;
height: 100%;
fill: currentColor;
}
.bim-tab__title {
white-space: nowrap;
}
.bim-tab__content {
flex: 1;
display: flex;
position: relative;
min-height: 0; /* 防止撑开导致外层滚动 */
overflow: hidden; /* 限制滚动只在内部面板 */
}
.bim-tab__panel {
display: none;
width: 100%;
height: 100%;
flex: 1;
}
.bim-tab__panel.is-active {
display: flex;
flex-direction: column;
height: 100%;
}
/* 构件树弹窗内的内容布局tab+搜索固定,树区域滚动 */
.construct-tab__container {
height: 100%;
display: flex;
flex-direction: column;
}
.construct-tab__panel-content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; /* 允许内部滚动 */
overflow: hidden;
}
.construct-tab__panel-content .bim-tree {
flex: 1;
}

254
src/components/tab/index.ts Normal file
View File

@@ -0,0 +1,254 @@
import { IBimComponent } from '../../types/component';
import { localeManager, t } from '../../services/locale';
import { themeManager } from '../../services/theme';
import type { ThemeConfig } from '../../themes/types';
import type { TabItem, TabOptions } from './index.type';
import './index.css';
/**
* 简单标签页组件(固定标签,不支持运行时增删)
* - 仅处理标签头部与内容切换
* - 主题从 ThemeManager 获取,不在配置中传入
* - 文案通过 t() 翻译,支持传原文直接展示
*/
export class BimTab implements IBimComponent {
/** 组件根节点 */
public element: HTMLElement;
/** 头部容器 */
private navElement: HTMLElement;
/** 内容容器 */
private contentElement: HTMLElement;
/** 业务配置 */
private options: TabOptions;
/** 当前激活的标签 id */
private activeId: string | null;
/** id -> TabItem */
private tabMap: Map<string, TabItem> = new Map();
/** id -> 内容容器 */
private panelMap: Map<string, HTMLElement> = new Map();
/** 主题/语言订阅解除函数 */
private unsubscribeLocale: (() => void) | null = null;
private unsubscribeTheme: (() => void) | null = null;
/** 头部点击事件处理引用(便于销毁时解绑) */
private navClickHandler: ((e: MouseEvent) => void) | null = null;
constructor(options: TabOptions) {
this.options = options;
this.activeId = options.activeId || (options.tabs[0]?.id ?? null);
// 预置 tabMap方便后续查找
options.tabs.forEach((tab) => this.tabMap.set(tab.id, tab));
// 构建基础 DOM 结构
this.element = document.createElement('div');
this.element.className = 'bim-tab';
this.navElement = document.createElement('div');
this.navElement.className = 'bim-tab__nav';
this.navElement.setAttribute('role', 'tablist');
this.element.appendChild(this.navElement);
this.contentElement = document.createElement('div');
this.contentElement.className = 'bim-tab__content';
this.element.appendChild(this.contentElement);
// 挂载到容器
this.options.container.appendChild(this.element);
}
/**
* 初始化组件
*/
public init(): void {
this.renderNav();
this.renderPanels();
// 初始化文案与主题
this.setLocales();
this.setTheme(themeManager.getTheme());
// 订阅语言、主题变化
this.unsubscribeLocale = localeManager.subscribe(() => this.setLocales());
this.unsubscribeTheme = themeManager.subscribe((theme) => this.setTheme(theme));
}
/**
* 渲染头部标签
*/
private renderNav(): void {
this.navElement.innerHTML = '';
this.navClickHandler = (event: MouseEvent) => {
const target = (event.target as HTMLElement).closest<HTMLButtonElement>('.bim-tab__item');
if (!target) return;
const tabId = target.dataset.id;
if (!tabId) return;
const tab = this.tabMap.get(tabId);
if (tab?.disabled) return;
this.activateTab(tabId);
};
this.navElement.addEventListener('click', this.navClickHandler);
this.options.tabs.forEach((tab) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'bim-tab__item';
btn.dataset.id = tab.id;
btn.setAttribute('role', 'tab');
btn.id = `tab-${tab.id}`;
btn.setAttribute('aria-selected', `${tab.id === this.activeId}`);
if (tab.disabled) {
btn.disabled = true;
btn.setAttribute('aria-disabled', 'true');
btn.classList.add('is-disabled');
}
// 图标
if (tab.icon) {
const iconEl = document.createElement('span');
iconEl.className = 'bim-tab__icon';
iconEl.innerHTML = tab.icon;
btn.appendChild(iconEl);
}
const titleEl = document.createElement('span');
titleEl.className = 'bim-tab__title';
titleEl.textContent = this.resolveTitle(tab.title);
btn.appendChild(titleEl);
if (tab.id === this.activeId) {
btn.classList.add('is-active');
}
this.navElement.appendChild(btn);
});
}
/**
* 渲染内容面板
*/
private renderPanels(): void {
this.contentElement.innerHTML = '';
this.panelMap.clear();
this.options.tabs.forEach((tab) => {
const panel = document.createElement('div');
panel.className = 'bim-tab__panel';
panel.dataset.id = tab.id;
panel.setAttribute('role', 'tabpanel');
panel.setAttribute('aria-labelledby', `tab-${tab.id}`);
if (tab.content instanceof HTMLElement) {
panel.appendChild(tab.content);
} else if (typeof tab.content === 'string') {
panel.innerHTML = tab.content;
}
if (tab.id === this.activeId) {
panel.classList.add('is-active');
} else {
panel.style.display = 'none';
}
this.panelMap.set(tab.id, panel);
this.contentElement.appendChild(panel);
});
}
/**
* 激活指定标签
* @param tabId 目标标签 id
*/
public activateTab(tabId: string): void {
if (this.activeId === tabId) return;
const targetTab = this.tabMap.get(tabId);
if (!targetTab || targetTab.disabled) return;
this.activeId = tabId;
// 更新头部状态
const buttons = this.navElement.querySelectorAll<HTMLButtonElement>('.bim-tab__item');
buttons.forEach((btn) => {
const isActive = btn.dataset.id === tabId;
btn.classList.toggle('is-active', isActive);
btn.setAttribute('aria-selected', `${isActive}`);
});
// 更新面板显示
this.panelMap.forEach((panel, id) => {
const isActive = id === tabId;
panel.classList.toggle('is-active', isActive);
panel.style.display = isActive ? 'block' : 'none';
});
if (this.options.onChange) {
this.options.onChange(tabId, targetTab);
}
}
/**
* 应用主题
*/
public setTheme(theme: ThemeConfig): void {
const style = this.element.style;
style.setProperty('--bim-tab-bg', theme.panelBackground);
style.setProperty('--bim-tab-nav-bg', theme.panelBackground);
style.setProperty('--bim-tab-text', theme.textPrimary);
style.setProperty('--bim-tab-text-secondary', theme.textSecondary);
style.setProperty('--bim-tab-text-active', theme.primary);
style.setProperty('--bim-tab-border', theme.border);
style.setProperty('--bim-tab-hover-bg', theme.componentHover);
style.setProperty('--bim-tab-active-bg', theme.componentActive);
style.setProperty('--bim-tab-icon', theme.icon);
}
/**
* 应用当前语言文案
*/
public setLocales(): void {
const buttons = this.navElement.querySelectorAll<HTMLButtonElement>('.bim-tab__item');
buttons.forEach((btn) => {
const id = btn.dataset.id;
if (!id) return;
const tab = this.tabMap.get(id);
if (!tab) return;
const titleEl = btn.querySelector<HTMLElement>('.bim-tab__title');
if (titleEl) {
titleEl.textContent = this.resolveTitle(tab.title);
}
});
}
/**
* 清理资源
*/
public destroy(): void {
if (this.navClickHandler) {
this.navElement.removeEventListener('click', this.navClickHandler);
this.navClickHandler = null;
}
if (this.unsubscribeLocale) {
this.unsubscribeLocale();
this.unsubscribeLocale = null;
}
if (this.unsubscribeTheme) {
this.unsubscribeTheme();
this.unsubscribeTheme = null;
}
this.panelMap.clear();
this.tabMap.clear();
this.element.remove();
}
/**
* 工具:解析标题(优先翻译,不存在则回退原值)
*/
private resolveTitle(title: string): string {
try {
const translated = t(title);
return translated || title;
} catch (err) {
// 翻译失败时使用原值
return title;
}
}
}

View File

@@ -0,0 +1,37 @@
/**
* 标签项定义TabItem
* 用于描述单个标签页的基础信息和内容。
*/
export interface TabItem {
/** 唯一标识 */
id: string;
/** 标题文案或翻译键,渲染时统一走 t() */
title: string;
/** 是否禁用 */
disabled?: boolean;
/** 可选图标,支持内联 SVG / HTML 字符串 */
icon?: string;
/** 可选的内容区域,支持 HTMLElement 或 HTML 字符串 */
content?: HTMLElement | string;
/** 业务侧自定义附加数据 */
meta?: Record<string, unknown>;
}
/**
* Tab 组件初始化参数
*/
export interface TabOptions {
/** 挂载容器 */
container: HTMLElement;
/** 预设的固定标签列表(不支持运行期增删) */
tabs: TabItem[];
/** 初始激活的标签 id默认使用首个标签 */
activeId?: string;
/**
* 切换回调
* @param tabId 当前激活的标签 id
* @param tab 当前激活的标签对象
*/
onChange?: (tabId: string, tab: TabItem | undefined) => void;
}

View File

@@ -1,21 +1,16 @@
import { BimEngine } from './bim-engine';
// Main Entry
export * from './bim-engine';
// 导出通用组件
export { BimButtonGroup } from './components/button-group';
export { Toolbar } from './components/button-group/toolbar';
// Types - Core
export * from './types/component';
export * from './types/events';
export type { ThemeConfig, ThemeType } from './themes/types';
// 导出相关类型定义
export type { OptButton, ButtonGroup, ButtonGroupOptions, ClickPayload } from './components/button-group/index.type';
// Types - Components
export type { EngineOptions, ModelLoadOptions } from './components/engine/types';
export type { DialogOptions, DialogPosition } from './components/dialog/index.type';
export type { ButtonConfig, ButtonGroupOptions } from './components/button-group/index.type';
export type { TreeOptions, TreeNodeConfig, TreeNodeCheckState, NodeClickAction } from './components/tree/types';
export type { CollapseOptions, CollapseItemConfig } from './components/collapse/types';
// 导出 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';
export { createEngine } from './bim-engine-sdk.es.js';
// Note: Component classes are intentionally NOT exported to enforce Manager pattern usage.

View File

@@ -16,6 +16,7 @@ export const enUS: TranslationDictionary = {
walkPerson: 'Person',
walkBird: 'Bird Eye',
walkMenu: 'Menu',
tree: 'Tree',
},
dialog: {
testTitle: 'Test Dialog',
@@ -35,5 +36,14 @@ export const enUS: TranslationDictionary = {
component: 'Component',
system: 'System',
space: 'Space',
}
};
},
panel: {
property: {
title: 'Property Panel',
base: 'Basic Info',
material: 'Material',
advanced: 'Advanced'
}
}
};

View File

@@ -9,35 +9,44 @@ export interface TranslationDictionary {
openTestDialog: string;
openInfoDialog: string;
};
toolbar: {
home: string;
info: string;
location: string;
setting: string;
walk: string;
walkPerson: string;
walkBird: string;
walkMenu: string;
};
dialog: {
testTitle: string;
testContent: string;
};
menu: {
info: string;
home: string;
};
toolbar: {
home: string;
info: string;
location: string;
setting: string;
walk: string;
walkMenu: string;
walkPerson: string;
walkBird: string;
tree: string;
};
panel: {
property: {
title: string;
base: string;
material: string;
advanced: string;
}
};
dialog: {
testTitle: string;
testContent: string;
};
menu: {
info: string;
home: string;
};
tree: {
searchPlaceholder: string;
};
constructTree: {
title: string;
}
};
tab: {
component: string;
system: string;
space: string;
}
};
}
/**

View File

@@ -13,9 +13,10 @@ export const zhCN: TranslationDictionary = {
location: '定位',
setting: '设置',
walk: '漫游',
walkPerson: '人视',
walkBird: '鸟瞰',
walkMenu: '菜单',
walkMenu: '漫游菜单',
walkPerson: '第一人称',
walkBird: '第三人称',
tree: '模型树'
},
dialog: {
testTitle: '测试弹窗',
@@ -35,5 +36,13 @@ export const zhCN: TranslationDictionary = {
component: '构件',
system: '系统',
space: '空间',
},
panel: {
property: {
title: '属性面板',
base: '基本属性',
material: '材质信息',
advanced: '高级设置'
}
}
};

View File

@@ -0,0 +1,92 @@
import { BimComponent } from '../core/component';
import { BimEngine } from '../bim-engine';
import { BimCollapse } from '../components/collapse/index';
export class PropertyPanelManager extends BimComponent {
constructor(engine: BimEngine) {
super(engine);
}
public init(): void {
document.addEventListener('bim-demo:open-property-panel', () => {
this.show();
});
}
public show() {
if (!this.engine.dialog) {
console.warn('Dialog manager is not initialized');
return;
}
const dialog = this.engine.dialog.create({
title: 'panel.property.title', // '属性面板'
minWidth: 320,
height: 420,
position: 'top-right',
resizable: false
});
// 2. Create Content Container
const contentContainer = document.createElement('div');
contentContainer.style.height = '100%';
contentContainer.style.overflowY = 'auto';
// Use public API to set content
dialog.setContent(contentContainer);
// 3. Create Collapse inside the Dialog
new BimCollapse({
container: contentContainer,
accordion: true,
activeIds: ['base'],
items: [
{
id: 'base',
title: 'panel.property.base', // '基本属性'
content: this.createBaseInfoContent(),
icon: '<svg viewBox="0 0 1024 1024"><path d="M512 64q190.4 0 326.4 136T974.4 526.4 838.4 852.8 512 988.8 185.6 852.8 49.6 526.4 185.6 200 512 64m0-64C229.6 0 0 229.6 0 512s229.6 512 512 512 512-229.6 512-512S794.4 0 512 0z" fill="currentColor"/></svg>'
},
{
id: 'material',
title: 'panel.property.material', // '材质信息'
content: this.createMaterialContent(),
icon: '<svg viewBox="0 0 1024 1024"><path d="M128 128h768v768H128z" fill="none" stroke="currentColor" stroke-width="64"/></svg>'
},
{
id: 'advanced',
title: 'panel.property.advanced', // '高级设置'
content: '<div>Loading...</div>', // Placeholder
disabled: true
}
]
});
}
private createBaseInfoContent(): HTMLElement {
const div = document.createElement('div');
div.style.padding = '8px 0';
div.innerHTML = `
<div style="margin-bottom:8px"><b>ID:</b> <span style="color:#666">E-2023001</span></div>
<div style="margin-bottom:8px"><b>Name:</b> <span style="color:#666">Wall-01</span></div>
<div style="margin-bottom:8px"><b>Level:</b> <span style="color:#666">F1</span></div>
`;
return div;
}
private createMaterialContent(): HTMLElement {
const div = document.createElement('div');
div.innerHTML = `
<div style="display:flex;align-items:center;margin-bottom:8px">
<div style="width:20px;height:20px;background:#ccc;margin-right:8px;border:1px solid #999"></div>
<span>Concrete</span>
</div>
<div>Density: 2400 kg/m³</div>
`;
return div;
}
public destroy(): void {
// Cleanup if needed
}
}

View File

@@ -7,12 +7,28 @@ export 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 };
}
// 树组件事件
'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 };
// 折叠面板事件
'ui:collapse-change': { activeIds: string[] };
// 系统事件
'sys:theme-changed': { theme: string };
'sys:locale-changed': { locale: string };
}