添加折叠面板
This commit is contained in:
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