feat(tree,menu,docs): 对接目录树三类数据并中文化文档
This commit is contained in:
@@ -19,7 +19,7 @@
|
||||
background-color: var(--bim-component-bg);
|
||||
padding-left: 12px;
|
||||
padding-right: 0;
|
||||
border-bottom: 1px solid var(--bim-border-strong);
|
||||
border-bottom: 1px solid var(--bim-border-default);
|
||||
}
|
||||
|
||||
.bim-collapse.is-ghost .bim-collapse-header:hover {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { themeManager } from '../../services/theme';
|
||||
import type { EngineOptions, ModelLoadOptions } from './types';
|
||||
import type { MeasureMode } from '../../types/measure';
|
||||
import type { SectionBoxRange } from '../section-box-panel/types';
|
||||
import { ManagerRegistry } from '../../core/manager-registry';
|
||||
// 导入第三方 SDK 的 createEngine 函数(从 npm 包引入)
|
||||
// import { createEngine as createEngineSDK } from 'iflow-engine-base';
|
||||
import { createEngine as createEngineSDK } from '../../../../bim_engine_base/dist/bim-engine-sdk.es';
|
||||
@@ -128,15 +129,18 @@ export class Engine implements IBimComponent {
|
||||
|
||||
// 监听构件点击事件
|
||||
this.engine.events.on('click', (hit: any) => {
|
||||
const registry = ManagerRegistry.getInstance();
|
||||
if (hit && hit.object) {
|
||||
this.selectedComponent = {
|
||||
url: hit.object.url,
|
||||
id: hit.object.name
|
||||
};
|
||||
console.log('[Engine] 构件选中:', this.selectedComponent);
|
||||
registry.emit('component:selected', this.selectedComponent);
|
||||
} else {
|
||||
this.selectedComponent = null;
|
||||
console.log('[Engine] 取消选中');
|
||||
registry.emit('component:deselected', {});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -735,6 +739,31 @@ export class Engine implements IBimComponent {
|
||||
|
||||
// ==================== 结束:构件选中 ====================
|
||||
|
||||
// ==================== 模型树 ====================
|
||||
|
||||
public getTypeTreeData(): any[] {
|
||||
if (!this._isInitialized || !this.engine?.modelTree) {
|
||||
return [];
|
||||
}
|
||||
return this.engine.modelTree.getTypeTreeData();
|
||||
}
|
||||
|
||||
public getLevelTreeData(): any[] {
|
||||
if (!this._isInitialized || !this.engine?.modelTree) {
|
||||
return [];
|
||||
}
|
||||
return this.engine.modelTree.getLevelTreeData();
|
||||
}
|
||||
|
||||
public getMajorTreeData(): any[] {
|
||||
if (!this._isInitialized || !this.engine?.modelTree) {
|
||||
return [];
|
||||
}
|
||||
return this.engine.modelTree.getMajorTreeData();
|
||||
}
|
||||
|
||||
// ==================== 结束:模型树 ====================
|
||||
|
||||
/** 激活框选放大功能 */
|
||||
public activateZoomBox(): void {
|
||||
if (!this._isInitialized || !this.engine) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
padding: 6px;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
min-width: 160px;
|
||||
box-shadow: var(--bim-shadow-lg);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
user-select: none;
|
||||
@@ -38,8 +37,7 @@
|
||||
}
|
||||
|
||||
.bim-menu-item:hover {
|
||||
background-color: var(--bim-floating-btn-bg);
|
||||
transform: translateX(2px);
|
||||
background-color: var(--bim-component-bg-hover);
|
||||
}
|
||||
|
||||
.bim-menu-item.disabled {
|
||||
@@ -49,6 +47,10 @@
|
||||
}
|
||||
|
||||
.bim-menu-item-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bim-menu.has-icons .bim-menu-item-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 8px;
|
||||
|
||||
@@ -93,14 +93,23 @@ export class BimMenu implements IBimComponent {
|
||||
return this.element;
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心渲染逻辑
|
||||
* 处理分组、排序和 DOM 生成
|
||||
*/
|
||||
private hasAnyIcon(items: MenuItemConfig[]): boolean {
|
||||
return items.some(item => {
|
||||
if (item.icon) return true;
|
||||
if (item.children) return this.hasAnyIcon(item.children);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private render(): void {
|
||||
const { items, groupOrder } = this.options;
|
||||
|
||||
// 1. 数据分桶:按 group 字段将菜单项分组
|
||||
if (this.hasAnyIcon(items)) {
|
||||
this.element.classList.add('has-icons');
|
||||
} else {
|
||||
this.element.classList.remove('has-icons');
|
||||
}
|
||||
|
||||
const groups = new Map<string, MenuItemConfig[]>();
|
||||
const defaultGroup = 'default';
|
||||
|
||||
@@ -112,25 +121,19 @@ export class BimMenu implements IBimComponent {
|
||||
groups.get(groupName)!.push(item);
|
||||
});
|
||||
|
||||
// 2. 确定分组顺序
|
||||
let sortedGroupKeys: string[] = [];
|
||||
if (groupOrder) {
|
||||
// 优先按照 groupOrder 指定的顺序排序
|
||||
sortedGroupKeys = groupOrder.filter(g => groups.has(g));
|
||||
// 将未在 groupOrder 中定义的组追加到最后
|
||||
for (const key of groups.keys()) {
|
||||
if (!sortedGroupKeys.includes(key)) {
|
||||
sortedGroupKeys.push(key);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果未指定顺序,则按默认遍历顺序
|
||||
sortedGroupKeys = Array.from(groups.keys());
|
||||
}
|
||||
|
||||
// 3. 渲染分组和组内项
|
||||
sortedGroupKeys.forEach((groupName, index) => {
|
||||
// 除了第一组外,每组之前插入分割线
|
||||
if (index > 0) {
|
||||
const divider = document.createElement('li');
|
||||
divider.className = 'bim-menu-divider';
|
||||
@@ -138,13 +141,16 @@ export class BimMenu implements IBimComponent {
|
||||
}
|
||||
|
||||
const groupItems = groups.get(groupName)!;
|
||||
// 组内排序:根据 item.order 升序排列
|
||||
groupItems.sort((a, b) => (a.order || 0) - (b.order || 0));
|
||||
|
||||
groupItems.forEach(item => {
|
||||
// 仅渲染可见的项
|
||||
if (item.visible !== false) {
|
||||
this.element.appendChild(this.createItemElement(item));
|
||||
if (item.divider) {
|
||||
const divider = document.createElement('li');
|
||||
divider.className = 'bim-menu-divider';
|
||||
this.element.appendChild(divider);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
/**
|
||||
* 菜单项配置接口 (用于简化的对象配置)
|
||||
*/
|
||||
export interface MenuItemConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
@@ -11,4 +8,5 @@ export interface MenuItemConfig {
|
||||
children?: MenuItemConfig[];
|
||||
disabled?: boolean;
|
||||
visible?: boolean;
|
||||
divider?: boolean;
|
||||
}
|
||||
@@ -35,6 +35,12 @@ export const enUS: TranslationDictionary = {
|
||||
info: 'Info',
|
||||
home: 'Home',
|
||||
componentDetail: 'Component Detail',
|
||||
hideSelected: 'Hide Selected',
|
||||
transparentSelected: 'Transparent Selected',
|
||||
isolateSelected: 'Isolate Selected',
|
||||
hideOthers: 'Hide Others',
|
||||
transparentOthers: 'Transparent Others',
|
||||
fitSectionBox: 'Fit Section Box',
|
||||
showAll: 'Show All'
|
||||
},
|
||||
tree: {
|
||||
@@ -47,6 +53,8 @@ export const enUS: TranslationDictionary = {
|
||||
component: 'Component',
|
||||
system: 'System',
|
||||
space: 'Space',
|
||||
type: 'Type',
|
||||
major: 'Major',
|
||||
},
|
||||
panel: {
|
||||
property: {
|
||||
@@ -60,7 +68,8 @@ export const enUS: TranslationDictionary = {
|
||||
}
|
||||
},
|
||||
componentDetail: {
|
||||
title: 'Component Detail'
|
||||
title: 'Component Detail',
|
||||
noSelection: 'Please select a component first'
|
||||
}
|
||||
},
|
||||
measure: {
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface TranslationDictionary {
|
||||
};
|
||||
componentDetail: {
|
||||
title: string;
|
||||
noSelection: string;
|
||||
};
|
||||
};
|
||||
dialog: {
|
||||
@@ -53,6 +54,12 @@ export interface TranslationDictionary {
|
||||
info: string;
|
||||
home: string;
|
||||
componentDetail: string;
|
||||
hideSelected: string;
|
||||
transparentSelected: string;
|
||||
isolateSelected: string;
|
||||
hideOthers: string;
|
||||
transparentOthers: string;
|
||||
fitSectionBox: string;
|
||||
showAll: string;
|
||||
};
|
||||
tree: {
|
||||
@@ -65,6 +72,8 @@ export interface TranslationDictionary {
|
||||
component: string;
|
||||
system: string;
|
||||
space: string;
|
||||
type: string;
|
||||
major: string;
|
||||
};
|
||||
measure: {
|
||||
btnName: string;
|
||||
|
||||
@@ -35,6 +35,12 @@ export const zhCN: TranslationDictionary = {
|
||||
info: '信息',
|
||||
home: '首页',
|
||||
componentDetail: '构件详情',
|
||||
hideSelected: '隐藏选中构件',
|
||||
transparentSelected: '半透明选中构件',
|
||||
isolateSelected: '隔离选中构件',
|
||||
hideOthers: '其他构件隐藏',
|
||||
transparentOthers: '其他构件半透明',
|
||||
fitSectionBox: '剖切盒适应',
|
||||
showAll: '显示全部'
|
||||
},
|
||||
tree: {
|
||||
@@ -47,6 +53,8 @@ export const zhCN: TranslationDictionary = {
|
||||
component: '构件',
|
||||
system: '系统',
|
||||
space: '空间',
|
||||
type: '类型',
|
||||
major: '专业',
|
||||
},
|
||||
panel: {
|
||||
property: {
|
||||
@@ -60,7 +68,8 @@ export const zhCN: TranslationDictionary = {
|
||||
}
|
||||
},
|
||||
componentDetail: {
|
||||
title: '构件详情'
|
||||
title: '构件详情',
|
||||
noSelection: '请先选中构件'
|
||||
}
|
||||
},
|
||||
measure: {
|
||||
|
||||
@@ -1,35 +1,56 @@
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
import { BimCollapse } from '../components/collapse/index';
|
||||
import { BimDescription } from '../components/description/index';
|
||||
import { BimTab } from '../components/tab';
|
||||
import { t } from '../services/locale';
|
||||
|
||||
export class ComponentDetailManager extends BaseManager {
|
||||
private dialogId = 'component-detail-dialog';
|
||||
private dialog: any = null;
|
||||
private currentSelection: { url: string; id: string } | null = null;
|
||||
private unsubscribeSelected: (() => void) | null = null;
|
||||
private unsubscribeDeselected: (() => void) | null = null;
|
||||
private tabInstance: BimTab | null = null;
|
||||
private propertiesData: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public show(modelUrl: string, componentId: string): void {
|
||||
public init(): void {
|
||||
this.unsubscribeSelected = this.registry.on('component:selected', (payload) => {
|
||||
this.currentSelection = payload;
|
||||
if (this.isOpen()) {
|
||||
this.loadAndRenderContent();
|
||||
}
|
||||
});
|
||||
|
||||
this.unsubscribeDeselected = this.registry.on('component:deselected', () => {
|
||||
this.currentSelection = null;
|
||||
if (this.isOpen()) {
|
||||
this.renderNoSelection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
if (!this.registry.dialog) {
|
||||
console.warn('[ComponentDetailManager] Dialog manager not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOpen()) {
|
||||
this.hide();
|
||||
if (!this.isOpen()) {
|
||||
this.createDialog();
|
||||
}
|
||||
|
||||
this.createDialog();
|
||||
this.showLoading();
|
||||
|
||||
this.registry.engine3d?.getComponentProperties(modelUrl, componentId, (data) => {
|
||||
this.renderProperties(data, componentId);
|
||||
});
|
||||
if (this.currentSelection) {
|
||||
this.loadAndRenderContent();
|
||||
} else {
|
||||
this.renderNoSelection();
|
||||
}
|
||||
}
|
||||
|
||||
private createDialog(): void {
|
||||
const width = 400;
|
||||
const width = 300;
|
||||
const x = document.body.clientWidth - width - 40;
|
||||
|
||||
this.dialog = this.registry.dialog?.create({
|
||||
@@ -44,27 +65,81 @@ export class ComponentDetailManager extends BaseManager {
|
||||
} as any);
|
||||
}
|
||||
|
||||
private loadAndRenderContent(): void {
|
||||
if (!this.dialog || !this.currentSelection) return;
|
||||
|
||||
this.showLoading();
|
||||
|
||||
this.registry.engine3d?.getComponentProperties(
|
||||
this.currentSelection.url,
|
||||
this.currentSelection.id,
|
||||
(data) => {
|
||||
this.propertiesData = data;
|
||||
this.renderTabbedContent();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private showLoading(): void {
|
||||
const container = document.createElement('div');
|
||||
container.style.padding = '20px';
|
||||
container.style.textAlign = 'center';
|
||||
container.style.cssText = 'height:100%;display:flex;align-items:center;justify-content:center;';
|
||||
container.textContent = '加载中...';
|
||||
this.dialog?.setContent(container);
|
||||
}
|
||||
|
||||
private renderProperties(data: any, _componentId: string): void {
|
||||
private renderNoSelection(): void {
|
||||
if (!this.dialog) return;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.style.height = '100%';
|
||||
container.style.overflowY = 'auto';
|
||||
container.style.cssText = 'height:100%;display:flex;align-items:center;justify-content:center;color:var(--bim-text-secondary,#999);';
|
||||
container.textContent = t('panel.componentDetail.noSelection') || '请先选中构件';
|
||||
this.dialog.setContent(container);
|
||||
}
|
||||
|
||||
const properties = data?.properties || [];
|
||||
private renderTabbedContent(): void {
|
||||
if (!this.dialog) return;
|
||||
|
||||
if (this.tabInstance) {
|
||||
this.tabInstance.destroy();
|
||||
this.tabInstance = null;
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'height:100%;display:flex;flex-direction:column;';
|
||||
|
||||
const propertiesPanel = this.createPropertiesPanel();
|
||||
const materialsPanel = this.createMaterialsPanel();
|
||||
|
||||
this.tabInstance = new BimTab({
|
||||
container,
|
||||
activeId: 'properties',
|
||||
tabs: [
|
||||
{
|
||||
id: 'properties',
|
||||
title: 'panel.property.tab.props',
|
||||
content: propertiesPanel
|
||||
},
|
||||
{
|
||||
id: 'materials',
|
||||
title: 'panel.property.tab.material',
|
||||
content: materialsPanel
|
||||
}
|
||||
]
|
||||
});
|
||||
this.tabInstance.init();
|
||||
|
||||
this.dialog.setContent(container);
|
||||
}
|
||||
|
||||
private createPropertiesPanel(): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'height:100%;overflow-y:auto;';
|
||||
|
||||
const properties = this.propertiesData?.properties || [];
|
||||
|
||||
if (properties.length === 0) {
|
||||
container.innerHTML = '<div style="padding:20px;text-align:center;">无属性数据</div>';
|
||||
this.dialog.setContent(container);
|
||||
return;
|
||||
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--bim-text-secondary,#999);">无属性数据</div>';
|
||||
return container;
|
||||
}
|
||||
|
||||
const collapseItems = properties.map((category: any, index: number) => ({
|
||||
@@ -76,26 +151,102 @@ export class ComponentDetailManager extends BaseManager {
|
||||
new BimCollapse({
|
||||
container,
|
||||
accordion: false,
|
||||
ghost: true,
|
||||
activeIds: collapseItems.length > 0 ? [collapseItems[0].id] : [],
|
||||
items: collapseItems
|
||||
});
|
||||
|
||||
this.dialog.setContent(container);
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#${this.dialogId} .bim-collapse-header {
|
||||
background-color: var(--bim-component-bg-hover) !important;
|
||||
}
|
||||
#${this.dialogId} .bim-collapse-header:hover {
|
||||
background-color: var(--bim-component-bg-active) !important;
|
||||
}
|
||||
`;
|
||||
container.appendChild(style);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private createMaterialsPanel(): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = 'height:100%;overflow-y:auto;';
|
||||
|
||||
const materials = this.propertiesData?.materials || [];
|
||||
|
||||
if (materials.length === 0) {
|
||||
container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--bim-text-secondary,#999);">无材质数据</div>';
|
||||
return container;
|
||||
}
|
||||
|
||||
const collapseItems = materials.map((material: any, index: number) => ({
|
||||
id: `material-${index}`,
|
||||
title: material.name || `材质 ${index + 1}`,
|
||||
content: this.createCategoryContent(material.children || material.properties || [])
|
||||
}));
|
||||
|
||||
new BimCollapse({
|
||||
container,
|
||||
accordion: false,
|
||||
ghost: true,
|
||||
activeIds: collapseItems.length > 0 ? [collapseItems[0].id] : [],
|
||||
items: collapseItems
|
||||
});
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
#${this.dialogId} .bim-collapse-header {
|
||||
background-color: var(--bim-component-bg-hover) !important;
|
||||
}
|
||||
#${this.dialogId} .bim-collapse-header:hover {
|
||||
background-color: var(--bim-component-bg-active) !important;
|
||||
}
|
||||
`;
|
||||
container.appendChild(style);
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
private createCategoryContent(items: any[]): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
|
||||
const descItems = items.map((item: any) => ({
|
||||
label: item.name || '-',
|
||||
value: String(item.value ?? '-')
|
||||
}));
|
||||
items.forEach((item: any, index: number) => {
|
||||
const row = document.createElement('div');
|
||||
row.style.cssText = `
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--bim-border-default, rgba(255,255,255,0.15));
|
||||
`;
|
||||
|
||||
new BimDescription({
|
||||
container,
|
||||
labelWidth: '120px',
|
||||
bordered: true,
|
||||
items: descItems
|
||||
if (index === items.length - 1) {
|
||||
row.style.borderBottom = 'none';
|
||||
}
|
||||
|
||||
const label = document.createElement('div');
|
||||
label.style.cssText = `
|
||||
width: 120px;
|
||||
flex-shrink: 0;
|
||||
color: var(--bim-text-secondary, #999);
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
border-right: 1px solid var(--bim-border-default, rgba(255,255,255,0.15));
|
||||
`;
|
||||
label.textContent = item.name || '-';
|
||||
|
||||
const value = document.createElement('div');
|
||||
value.style.cssText = `
|
||||
flex: 1;
|
||||
color: var(--bim-text-primary, #fff);
|
||||
font-size: 13px;
|
||||
padding: 8px 12px;
|
||||
word-break: break-all;
|
||||
`;
|
||||
value.textContent = String(item.value ?? '-');
|
||||
|
||||
row.appendChild(label);
|
||||
row.appendChild(value);
|
||||
container.appendChild(row);
|
||||
});
|
||||
|
||||
return container;
|
||||
@@ -106,6 +257,10 @@ export class ComponentDetailManager extends BaseManager {
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this.tabInstance) {
|
||||
this.tabInstance.destroy();
|
||||
this.tabInstance = null;
|
||||
}
|
||||
if (this.dialog) {
|
||||
this.dialog.destroy();
|
||||
this.dialog = null;
|
||||
@@ -113,6 +268,14 @@ export class ComponentDetailManager extends BaseManager {
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.unsubscribeSelected) {
|
||||
this.unsubscribeSelected();
|
||||
this.unsubscribeSelected = null;
|
||||
}
|
||||
if (this.unsubscribeDeselected) {
|
||||
this.unsubscribeDeselected();
|
||||
this.unsubscribeDeselected = null;
|
||||
}
|
||||
this.hide();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* 构件树管理器
|
||||
* 负责管理构件树按钮和构件树对话框
|
||||
*/
|
||||
import type { ButtonGroupColors, ButtonConfig } from '../components/button-group/index.type';
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
import { BimButtonGroup } from '../components/button-group';
|
||||
@@ -11,76 +7,37 @@ import { BimDialog } from '../components/dialog';
|
||||
import { BimTab } from '../components/tab';
|
||||
import { getIcon } from '../utils/icon-manager';
|
||||
|
||||
/** 模拟的构件树数据 */
|
||||
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)' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
function transformTreeData(apiData: any[]): TreeNodeConfig[] {
|
||||
if (!apiData || apiData.length === 0) return [];
|
||||
|
||||
return apiData.map((model, modelIndex) => {
|
||||
const transformNode = (node: any, index: number): TreeNodeConfig => {
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
return {
|
||||
id: node.id || `node-${modelIndex}-${index}`,
|
||||
label: node.name || node.label || '未命名',
|
||||
expanded: false,
|
||||
clickAction: hasChildren ? 'expand' : 'select',
|
||||
children: hasChildren
|
||||
? node.children.map((child: any, childIndex: number) => transformNode(child, childIndex))
|
||||
: undefined,
|
||||
data: node
|
||||
};
|
||||
};
|
||||
|
||||
const hasChildren = model.children && model.children.length > 0;
|
||||
return {
|
||||
id: `model-${modelIndex}`,
|
||||
label: model.name || '模型',
|
||||
expanded: true,
|
||||
clickAction: 'expand',
|
||||
children: hasChildren
|
||||
? model.children.map((child: any, childIndex: number) => transformNode(child, childIndex))
|
||||
: undefined
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构件树管理器
|
||||
* 管理左上角的构件树按钮和对话框
|
||||
*/
|
||||
export class ConstructTreeManagerBtn extends BaseManager {
|
||||
/** 按钮组实例 */
|
||||
private toolbar: BimButtonGroup | null = null;
|
||||
@@ -125,41 +82,54 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
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();
|
||||
const levelTreeData = this.registry.engine3d?.getLevelTreeData() ?? [];
|
||||
const typeTreeData = this.registry.engine3d?.getTypeTreeData() ?? [];
|
||||
const majorTreeData = this.registry.engine3d?.getMajorTreeData() ?? [];
|
||||
|
||||
const systemPlaceholder = document.createElement('div');
|
||||
systemPlaceholder.className = 'construct-tab__panel-content';
|
||||
const spacePlaceholder = document.createElement('div');
|
||||
spacePlaceholder.className = 'construct-tab__panel-content';
|
||||
console.log('[ConstructTree] 构件树数据 (Level):', levelTreeData);
|
||||
console.log('[ConstructTree] 类型树数据 (Type):', typeTreeData);
|
||||
console.log('[ConstructTree] 专业树数据 (Major):', majorTreeData);
|
||||
|
||||
const createTree = (data: any[]) => {
|
||||
const tree = new BimTree({
|
||||
data: transformTreeData(data),
|
||||
checkable: true,
|
||||
indent: 0,
|
||||
enableSearch: true,
|
||||
checkStrictly: true,
|
||||
defaultExpandAll: true,
|
||||
onNodeCheck: (node) => {
|
||||
console.log('onNodeCheck', node);
|
||||
},
|
||||
onNodeSelect: (node) => {
|
||||
console.log('onNodeSelect', node);
|
||||
},
|
||||
onNodeExpand: () => {
|
||||
this.dialog?.fitWidth();
|
||||
},
|
||||
});
|
||||
tree.init();
|
||||
return tree;
|
||||
};
|
||||
|
||||
const componentTree = createTree(levelTreeData);
|
||||
const typeTree = createTree(typeTreeData);
|
||||
const majorTree = createTree(majorTreeData);
|
||||
|
||||
const componentPanel = document.createElement('div');
|
||||
componentPanel.className = 'construct-tab__panel-content';
|
||||
componentPanel.appendChild(tree.element);
|
||||
componentPanel.appendChild(componentTree.element);
|
||||
|
||||
const typePanel = document.createElement('div');
|
||||
typePanel.className = 'construct-tab__panel-content';
|
||||
typePanel.appendChild(typeTree.element);
|
||||
|
||||
const majorPanel = document.createElement('div');
|
||||
majorPanel.className = 'construct-tab__panel-content';
|
||||
majorPanel.appendChild(majorTree.element);
|
||||
|
||||
const tabMount = document.createElement('div');
|
||||
tabMount.className = 'construct-tab__container';
|
||||
@@ -169,8 +139,8 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
container: tabMount,
|
||||
tabs: [
|
||||
{ id: 'component', title: 'tab.component', content: componentPanel },
|
||||
{ id: 'system', title: 'tab.system', content: systemPlaceholder },
|
||||
{ id: 'space', title: 'tab.space', content: spacePlaceholder },
|
||||
{ id: 'type', title: 'tab.type', content: typePanel },
|
||||
{ id: 'major', title: 'tab.major', content: majorPanel },
|
||||
],
|
||||
activeId: 'component',
|
||||
onChange: () => {
|
||||
@@ -188,7 +158,9 @@ export class ConstructTreeManagerBtn extends BaseManager {
|
||||
resizable: false,
|
||||
onClose: () => {
|
||||
tab.destroy();
|
||||
tree.destroy();
|
||||
componentTree.destroy();
|
||||
typeTree.destroy();
|
||||
majorTree.destroy();
|
||||
this.setVisible(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
import { Engine, type EngineOptions, type ModelLoadOptions } from '../components/engine';
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
import { RightKeyManager } from './right-key-manager';
|
||||
import { infoMenuButton } from '../components/menu/buttons/info';
|
||||
import { homeMenuButton } from '../components/menu/buttons/home';
|
||||
import type { MeasureMode } from '../types/measure';
|
||||
import type { SectionBoxRange } from '../components/section-box-panel/types';
|
||||
import type { MenuItemConfig } from '../components/menu/item';
|
||||
@@ -56,31 +54,96 @@ export class EngineManager extends BaseManager {
|
||||
const items: MenuItemConfig[] = [];
|
||||
|
||||
if (selected) {
|
||||
// 1. 构件详情
|
||||
items.push({
|
||||
id: 'componentDetail',
|
||||
label: 'menu.componentDetail',
|
||||
group: 'component',
|
||||
order: 1,
|
||||
divider: true,
|
||||
onClick: () => {
|
||||
const registry = ManagerRegistry.getInstance();
|
||||
registry.componentDetail?.show(selected.url, selected.id);
|
||||
registry.componentDetail?.show();
|
||||
this.rightKey?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 2. 隐藏选中构件
|
||||
items.push({
|
||||
id: 'hideSelected',
|
||||
label: 'menu.hideSelected',
|
||||
group: 'component',
|
||||
order: 2,
|
||||
onClick: () => {
|
||||
console.log('[Menu] 隐藏选中构件 - 暂未实现');
|
||||
this.rightKey?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 半透明选中构件
|
||||
items.push({
|
||||
id: 'transparentSelected',
|
||||
label: 'menu.transparentSelected',
|
||||
group: 'component',
|
||||
order: 3,
|
||||
onClick: () => {
|
||||
console.log('[Menu] 半透明选中构件 - 暂未实现');
|
||||
this.rightKey?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 4. 隔离选中构件(带子菜单)
|
||||
items.push({
|
||||
id: 'isolateSelected',
|
||||
label: 'menu.isolateSelected',
|
||||
group: 'component',
|
||||
order: 4,
|
||||
divider: true,
|
||||
children: [
|
||||
{
|
||||
id: 'hideOthers',
|
||||
label: 'menu.hideOthers',
|
||||
onClick: () => {
|
||||
console.log('[Menu] 隔离 - 其他构件隐藏 - 暂未实现');
|
||||
this.rightKey?.hide();
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'transparentOthers',
|
||||
label: 'menu.transparentOthers',
|
||||
onClick: () => {
|
||||
console.log('[Menu] 隔离 - 其他构件半透明 - 暂未实现');
|
||||
this.rightKey?.hide();
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 5. 剖切盒适应
|
||||
items.push({
|
||||
id: 'fitSectionBox',
|
||||
label: 'menu.fitSectionBox',
|
||||
group: 'component',
|
||||
order: 5,
|
||||
onClick: () => {
|
||||
console.log('[Menu] 剖切盒适应 - 暂未实现');
|
||||
this.rightKey?.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 显示全部(始终显示)
|
||||
items.push({
|
||||
id: 'showAll',
|
||||
label: 'menu.showAll',
|
||||
group: 'component',
|
||||
order: 6,
|
||||
onClick: () => {
|
||||
console.log('[Menu] 显示全部 - 功能开发中');
|
||||
console.log('[Menu] 显示全部 - 暂未实现');
|
||||
this.rightKey?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
items.push(infoMenuButton());
|
||||
items.push(homeMenuButton());
|
||||
|
||||
return items;
|
||||
});
|
||||
|
||||
@@ -370,6 +433,22 @@ export class EngineManager extends BaseManager {
|
||||
|
||||
// ==================== 结束:构件选中 ====================
|
||||
|
||||
// ==================== 模型树 ====================
|
||||
|
||||
public getTypeTreeData(): any[] {
|
||||
return this.engineInstance?.getTypeTreeData() ?? [];
|
||||
}
|
||||
|
||||
public getLevelTreeData(): any[] {
|
||||
return this.engineInstance?.getLevelTreeData() ?? [];
|
||||
}
|
||||
|
||||
public getMajorTreeData(): any[] {
|
||||
return this.engineInstance?.getMajorTreeData() ?? [];
|
||||
}
|
||||
|
||||
// ==================== 结束:模型树 ====================
|
||||
|
||||
/** 销毁引擎管理器 */
|
||||
public destroy(): void {
|
||||
if (this.engineInstance) {
|
||||
|
||||
@@ -1,222 +0,0 @@
|
||||
/**
|
||||
* 属性面板管理器
|
||||
* 负责管理构件属性面板的显示和内容
|
||||
*/
|
||||
import { BaseManager } from '../core/base-manager';
|
||||
import { BimCollapse } from '../components/collapse/index';
|
||||
import { BimDescription } from '../components/description/index';
|
||||
import { BimTab } from '../components/tab/index';
|
||||
|
||||
/**
|
||||
* 属性面板管理器
|
||||
* 显示选中构件的属性信息和材质信息
|
||||
*/
|
||||
export class PropertyPanelManager extends BaseManager {
|
||||
/** 对话框 ID */
|
||||
private dialogId = 'property-panel-dialog';
|
||||
/** 对话框实例 */
|
||||
private dialog: any = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
/** 初始化,监听打开事件 */
|
||||
public init(): void {
|
||||
document.addEventListener('bim-demo:open-property-panel', () => {
|
||||
this.show();
|
||||
});
|
||||
}
|
||||
|
||||
/** 显示属性面板 */
|
||||
public show() {
|
||||
if (!this.registry.dialog) {
|
||||
console.warn('Dialog manager is not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isOpen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = 360;
|
||||
const x = document.body.clientWidth - width - 40;
|
||||
|
||||
this.dialog = this.registry.dialog.create({
|
||||
id: this.dialogId,
|
||||
title: 'panel.property.title',
|
||||
content: '',
|
||||
width: `${width}px`,
|
||||
height: '500px',
|
||||
position: { x, y: 20 },
|
||||
showMask: false,
|
||||
resizable: true,
|
||||
onClose: () => {
|
||||
this.hide();
|
||||
}
|
||||
} as any);
|
||||
|
||||
const contentContainer = document.createElement('div');
|
||||
contentContainer.style.height = '100%';
|
||||
contentContainer.style.display = 'flex';
|
||||
contentContainer.style.flexDirection = 'column';
|
||||
|
||||
this.dialog.setContent(contentContainer);
|
||||
|
||||
const tab = new BimTab({
|
||||
container: contentContainer,
|
||||
tabs: [
|
||||
{
|
||||
id: 'props',
|
||||
title: 'panel.property.tab.props',
|
||||
content: this.createPropsTabContent()
|
||||
},
|
||||
{
|
||||
id: 'material',
|
||||
title: 'panel.property.tab.material',
|
||||
content: this.createMaterialTabContent()
|
||||
}
|
||||
]
|
||||
});
|
||||
tab.init();
|
||||
}
|
||||
|
||||
/** 创建属性标签页内容 */
|
||||
private createPropsTabContent(): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
container.style.height = '100%';
|
||||
container.style.overflowY = 'auto';
|
||||
|
||||
new BimCollapse({
|
||||
container: container,
|
||||
accordion: true,
|
||||
activeIds: ['base', 'location'],
|
||||
items: [
|
||||
{
|
||||
id: 'base',
|
||||
title: 'panel.property.base',
|
||||
content: this.createBaseInfoContent(),
|
||||
},
|
||||
{
|
||||
id: 'advanced',
|
||||
title: 'panel.property.advanced',
|
||||
content: this.createAdvancedInfoContent(),
|
||||
disabled: false
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/** 创建材质标签页内容 */
|
||||
private createMaterialTabContent(): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
container.style.height = '100%';
|
||||
container.style.overflowY = 'auto';
|
||||
|
||||
new BimCollapse({
|
||||
container: container,
|
||||
accordion: true,
|
||||
activeIds: ['material'],
|
||||
items: [
|
||||
{
|
||||
id: 'material',
|
||||
title: 'panel.property.material',
|
||||
content: this.createMaterialContent(),
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/** 创建基本信息内容 */
|
||||
private createBaseInfoContent(): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
|
||||
new BimDescription({
|
||||
container: container,
|
||||
labelWidth: '80px',
|
||||
bordered: true,
|
||||
items: [
|
||||
{ label: 'Guid', value: '<span style="color:#666">1f8d-4a2e-9c</span>' },
|
||||
{ label: 'Name', value: '<b>Basic Wall: Generic - 200mm</b>' },
|
||||
{ label: 'Type', value: 'Basic Wall' },
|
||||
{ label: 'Level', value: 'Trane - Centrifugal Water Chiller - CVHF 2 Stage direct drive TAG(BP-RHS-1100RT) 0202104531 1' }
|
||||
]
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/** 创建高级信息内容 */
|
||||
private createAdvancedInfoContent(): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
|
||||
new BimDescription({
|
||||
container: container,
|
||||
labelWidth: '100px',
|
||||
bordered: true,
|
||||
items: [
|
||||
{ label: 'Area', value: '32.5 m²' },
|
||||
{ label: 'Volume', value: '6.5 m³' },
|
||||
{ label: 'Length', value: '5000 mm' },
|
||||
{ label: 'Phase', value: 'New Construction' }
|
||||
]
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
/** 创建材质内容 */
|
||||
private createMaterialContent(): HTMLElement {
|
||||
const container = document.createElement('div');
|
||||
|
||||
const preview = document.createElement('div');
|
||||
preview.style.display = 'flex';
|
||||
preview.style.alignItems = 'center';
|
||||
preview.style.marginBottom = '4px';
|
||||
preview.innerHTML = `
|
||||
<div style="width:24px;height:24px;background:#9ca3af;margin-right:8px;border:1px solid #6b7280;border-radius:2px;"></div>
|
||||
<span>Concrete - Cast-in-Place Gray</span>
|
||||
`;
|
||||
|
||||
const descContainer = document.createElement('div');
|
||||
|
||||
new BimDescription({
|
||||
container: descContainer,
|
||||
items: [
|
||||
{ label: 'Preview', value: preview },
|
||||
{ label: 'Class', value: 'Concrete' },
|
||||
{ label: 'Density', value: '2400 kg/m³' },
|
||||
{ label: 'Thermal', value: '0.6 W/(m·K)' }
|
||||
]
|
||||
});
|
||||
|
||||
container.appendChild(descContainer);
|
||||
return container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查面板是否打开
|
||||
* @returns 是否打开
|
||||
*/
|
||||
public isOpen(): boolean {
|
||||
return this.dialog !== null;
|
||||
}
|
||||
|
||||
/** 隐藏面板 */
|
||||
public hide(): void {
|
||||
if (this.dialog) {
|
||||
this.dialog.destroy();
|
||||
this.dialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 销毁管理器 */
|
||||
public destroy(): void {
|
||||
this.hide();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,8 @@ export interface EngineEvents {
|
||||
// 地图事件
|
||||
'map:opened': {};
|
||||
'map:closed': {};
|
||||
|
||||
// 构件选中事件
|
||||
'component:selected': { url: string; id: string };
|
||||
'component:deselected': {};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user