添加折叠面板
This commit is contained in:
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user