添加折叠面板
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
1
src/components/button-group/toolbar/buttons/info/icon.ts
Normal file
1
src/components/button-group/toolbar/buttons/info/icon.ts
Normal 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>';
|
||||
@@ -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'));
|
||||
}
|
||||
};
|
||||
|
||||
121
src/components/collapse/index.css
Normal file
121
src/components/collapse/index.css
Normal 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;
|
||||
}
|
||||
244
src/components/collapse/index.ts
Normal file
244
src/components/collapse/index.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
49
src/components/collapse/types.ts
Normal file
49
src/components/collapse/types.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 引擎实例
|
||||
|
||||
108
src/components/tab/index.css
Normal file
108
src/components/tab/index.css
Normal 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
254
src/components/tab/index.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
37
src/components/tab/index.type.ts
Normal file
37
src/components/tab/index.type.ts
Normal 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;
|
||||
}
|
||||
|
||||
31
src/index.ts
31
src/index.ts
@@ -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.
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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: '高级设置'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
92
src/managers/property-panel-manager.ts
Normal file
92
src/managers/property-panel-manager.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user