Files
bim_engine/src/components/collapse/index.ts
yuding ff0ca20e96 style(collapse): 为 ghost 模式表头添加下边框分隔
- 添加 border-bottom: 1px solid var(--bim-border-subtle)
- 修复 setTheme 中的 CSS 变量命名,使用标准名称
- 折叠的多个面板现在有明显的视觉分隔
2026-01-28 16:03:07 +08:00

243 lines
7.9 KiB
TypeScript
Raw Blame History

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-elevated', theme.bgElevated);
style.setProperty('--bim-border-default', theme.borderDefault);
style.setProperty('--bim-border-subtle', theme.borderSubtle);
style.setProperty('--bim-text-primary', theme.textPrimary);
style.setProperty('--bim-text-disabled', theme.textDisabled);
style.setProperty('--bim-component-bg', theme.componentBg);
style.setProperty('--bim-component-bg-hover', theme.componentBgHover);
style.setProperty('--bim-component-bg-active', theme.componentBgActive);
}
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();
}
}