Files
bim_engine/src/managers/component-detail-manager.ts
yuding 73edf0b3b8 refactor(managers): accept registry parameter via constructor injection
All 16 managers now receive ManagerRegistry instance through constructor
instead of calling ManagerRegistry.getInstance(). This enables each
BimEngine instance to have its own isolated set of managers.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-02-28 10:08:36 +08:00

284 lines
9.1 KiB
TypeScript

import { BaseManager } from '../core/base-manager';
import { ManagerRegistry } from '../core/manager-registry';
import { BimCollapse } from '../components/collapse/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(registry: ManagerRegistry) {
super(registry);
}
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.createDialog();
}
if (this.currentSelection) {
this.loadAndRenderContent();
} else {
this.renderNoSelection();
}
}
private createDialog(): void {
const width = 300;
const x = document.body.clientWidth - width - 40;
this.dialog = this.registry.dialog?.create({
id: this.dialogId,
title: 'panel.componentDetail.title',
content: '',
width: `${width}px`,
height: '500px',
position: { x, y: 20 },
resizable: true,
onClose: () => this.hide()
} 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.cssText = 'height:100%;display:flex;align-items:center;justify-content:center;';
container.textContent = '加载中...';
this.dialog?.setContent(container);
}
private renderNoSelection(): void {
if (!this.dialog) return;
const container = document.createElement('div');
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);
}
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;color:var(--bim-text-secondary,#999);">无属性数据</div>';
return container;
}
const collapseItems = properties.map((category: any, index: number) => ({
id: `category-${index}`,
title: category.name || `分类 ${index + 1}`,
content: this.createCategoryContent(category.children || [])
}));
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 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');
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));
`;
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;
}
public isOpen(): boolean {
return this.dialog !== null;
}
public hide(): void {
if (this.tabInstance) {
this.tabInstance.destroy();
this.tabInstance = null;
}
if (this.dialog) {
this.dialog.destroy();
this.dialog = null;
}
}
public destroy(): void {
if (this.unsubscribeSelected) {
this.unsubscribeSelected();
this.unsubscribeSelected = null;
}
if (this.unsubscribeDeselected) {
this.unsubscribeDeselected();
this.unsubscribeDeselected = null;
}
this.hide();
super.destroy();
}
}