refactor: reorganize project structure and implement self-managed i18n/theme for components
This commit is contained in:
@@ -1,113 +1,112 @@
|
||||
import './bim-engine.css';
|
||||
import { ToolbarManager } from './modules/toolbar-manager';
|
||||
import { DialogManager } from './modules/dialog-manager';
|
||||
import { ToolbarManager } from './managers/toolbar-manager';
|
||||
import { ButtonGroupManager } from './managers/button-group-manager';
|
||||
import { DialogManager } from './managers/dialog-manager';
|
||||
import { localeManager } from './services/locale';
|
||||
import { themeManager } from './services/theme';
|
||||
import type { LocaleType } from './locales/types';
|
||||
import type { ThemeType, ThemeConfig } from './themes/types';
|
||||
|
||||
/**
|
||||
* BimEngine 主类
|
||||
* 负责初始化整个应用界面,协调各个子模块(如工具栏、弹窗等)。
|
||||
*/
|
||||
export class BimEngine {
|
||||
/** 主容器元素 */
|
||||
private container: HTMLElement;
|
||||
/** 内部包装器元素,用于承载所有 UI 组件 */
|
||||
private wrapper: HTMLElement | null = null;
|
||||
|
||||
/** 工具栏管理器实例 */
|
||||
public toolbar: ToolbarManager | null = null;
|
||||
/** 弹窗管理器实例 */
|
||||
private topLeftGroup: any = null; // 保存左上角按钮组的引用
|
||||
|
||||
public toolbar: ToolbarManager | null = null; // 底部专用
|
||||
public buttonGroup: ButtonGroupManager | null = null; // 通用
|
||||
public dialog: DialogManager | null = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param container 容器元素或容器 ID
|
||||
*/
|
||||
constructor(container: HTMLElement | string) {
|
||||
public get localeManager() { return localeManager; }
|
||||
public get themeManager() { return themeManager; }
|
||||
|
||||
constructor(container: HTMLElement | string, options?: { locale?: LocaleType; theme?: ThemeType }) {
|
||||
const el = typeof container === 'string' ? document.getElementById(container) : container;
|
||||
if (!el) throw new Error('Container not found');
|
||||
this.container = el;
|
||||
|
||||
if (options?.locale) localeManager.setLocale(options.locale);
|
||||
if (options?.theme) {
|
||||
if (options.theme === 'custom') {
|
||||
console.warn('Custom theme should be set via setCustomTheme().');
|
||||
} else {
|
||||
themeManager.setTheme(options.theme);
|
||||
}
|
||||
}
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化方法
|
||||
* 创建 DOM 结构并初始化各子模块
|
||||
*/
|
||||
private init() {
|
||||
// 1. 清空容器可能存在的旧内容
|
||||
this.container.innerHTML = '';
|
||||
public setLocale(locale: LocaleType) { localeManager.setLocale(locale); }
|
||||
public getLocale(): LocaleType { return localeManager.getLocale(); }
|
||||
public setTheme(theme: 'dark' | 'light') { themeManager.setTheme(theme); }
|
||||
public setCustomTheme(theme: ThemeConfig) { themeManager.setCustomTheme(theme); }
|
||||
|
||||
// 2. 创建外层容器 div
|
||||
private init() {
|
||||
this.container.innerHTML = '';
|
||||
this.wrapper = document.createElement('div');
|
||||
this.wrapper.className = 'bim-engine-wrapper';
|
||||
this.container.appendChild(this.wrapper);
|
||||
|
||||
// 3. 创建标题 h1
|
||||
const title = document.createElement('h1');
|
||||
title.textContent = 'BimEngine';
|
||||
title.className = 'bim-engine-title';
|
||||
|
||||
// 4. 创建描述段落 p
|
||||
const desc = document.createElement('p');
|
||||
desc.textContent = '这是一个使用BIM-ENGINE。';
|
||||
desc.className = 'bim-engine-desc';
|
||||
|
||||
// 6. 创建操作按钮组容器
|
||||
const btnGroupContainer = document.createElement('div');
|
||||
btnGroupContainer.id = 'opt-btn-groups';
|
||||
btnGroupContainer.className = 'bim-engine-opt-btn-container';
|
||||
|
||||
// 7. 组装元素
|
||||
this.wrapper.appendChild(title);
|
||||
this.wrapper.appendChild(desc);
|
||||
|
||||
// 初始化管理器
|
||||
this.dialog = new DialogManager(this.wrapper);
|
||||
this.toolbar = new ToolbarManager(btnGroupContainer);
|
||||
this.toolbar = new ToolbarManager(this.wrapper);
|
||||
this.buttonGroup = new ButtonGroupManager(this.wrapper);
|
||||
|
||||
// 5. 测试按钮(更新为使用管理器)
|
||||
// 5.1 创建普通测试弹窗按钮
|
||||
const dialogBtn = document.createElement('button');
|
||||
dialogBtn.textContent = '打开测试弹窗';
|
||||
dialogBtn.className = 'bim-engine-btn';
|
||||
dialogBtn.onclick = () => {
|
||||
this.dialog?.create({
|
||||
title: '测试弹窗',
|
||||
content: '<div style="padding: 10px;">这是一个 <b>可拖拽</b> 且 <b>可缩放</b> 的弹窗。<br><br>你可以尝试拖动标题栏,或者拖动右下角改变大小。</div>',
|
||||
width: 300,
|
||||
height: 400,
|
||||
position: 'top-left',
|
||||
draggable: true,
|
||||
resizable: true
|
||||
// --- 创建左上角按钮组 (需求 1 & 2) ---
|
||||
this.createTopLeftGroup();
|
||||
|
||||
// 初始主题
|
||||
this.updateTheme(themeManager.getTheme());
|
||||
|
||||
// 在主题更新后,设置左上角按钮组的自定义颜色
|
||||
if (this.topLeftGroup) {
|
||||
this.topLeftGroup.setColors({
|
||||
backgroundColor: '#ff00ff'
|
||||
});
|
||||
};
|
||||
|
||||
// 5.2 创建二次封装信息弹窗按钮
|
||||
const infoDialogBtn = document.createElement('button');
|
||||
infoDialogBtn.textContent = '打开信息弹窗 (封装版)';
|
||||
infoDialogBtn.className = 'bim-engine-btn';
|
||||
infoDialogBtn.style.marginLeft = '10px';
|
||||
infoDialogBtn.onclick = () => {
|
||||
this.dialog?.showInfoDialog();
|
||||
};
|
||||
|
||||
// 将按钮和工具栏容器添加到包装器中
|
||||
this.wrapper.appendChild(dialogBtn);
|
||||
this.wrapper.appendChild(infoDialogBtn);
|
||||
this.wrapper.appendChild(btnGroupContainer);
|
||||
|
||||
// 8. 将包装器挂载到主容器
|
||||
this.container.appendChild(this.wrapper);
|
||||
}
|
||||
themeManager.subscribe((theme) => {
|
||||
this.updateTheme(theme);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁实例
|
||||
* 清理所有资源和 DOM 元素
|
||||
*/
|
||||
public destroy() {
|
||||
if (this.toolbar) {
|
||||
this.toolbar.destroy();
|
||||
this.toolbar = null;
|
||||
private createTopLeftGroup() {
|
||||
if (!this.buttonGroup) return;
|
||||
|
||||
this.topLeftGroup = this.buttonGroup.create({
|
||||
position: 'top-left',
|
||||
direction: 'column',
|
||||
align: 'vertical',
|
||||
backgroundColor: '#ff00ff', // 自定义背景色,不会被主题覆盖
|
||||
showLabel: false
|
||||
});
|
||||
|
||||
this.topLeftGroup.addGroup('main');
|
||||
this.topLeftGroup.addButton({
|
||||
id: 'menu-btn',
|
||||
groupId: 'main',
|
||||
type: 'button',
|
||||
label: 'Menu', // 应该用 translation key
|
||||
icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/></svg>',
|
||||
onClick: () => {
|
||||
alert("点击按钮")
|
||||
}
|
||||
});
|
||||
|
||||
// 手动 render 一次以显示
|
||||
this.topLeftGroup.render();
|
||||
}
|
||||
|
||||
private updateTheme(theme: ThemeConfig) {
|
||||
if (this.wrapper) {
|
||||
this.wrapper.style.backgroundColor = theme.background;
|
||||
this.wrapper.style.color = theme.textPrimary;
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.toolbar?.destroy();
|
||||
this.buttonGroup?.destroy();
|
||||
this.dialog = null;
|
||||
this.container.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
291
src/components/button-group/index.css
Normal file
291
src/components/button-group/index.css
Normal file
@@ -0,0 +1,291 @@
|
||||
/* 根容器 */
|
||||
.bim-btn-group-root {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bim-btn-group-root.static {
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
right: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.bim-btn-group-root.dir-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bim-btn-group-root.dir-column {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* 分组区域 */
|
||||
.bim-btn-group-section {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background-color: var(--bim-btn-group-section-bg, rgba(17, 17, 17, 0.88));
|
||||
border-radius: 6px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.bim-btn-group-root.dir-row .bim-btn-group-section {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bim-btn-group-root.dir-column .bim-btn-group-section {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 按钮外层 */
|
||||
.opt-btn-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 按钮本体 */
|
||||
.opt-btn {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
color: var(--bim-btn-text-color, #ccc);
|
||||
background-color: var(--bim-btn-bg, transparent);
|
||||
padding: 6px;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
/* 为绝对定位提供锚点 */
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.opt-btn:hover {
|
||||
background-color: var(--bim-btn-hover-bg, #444);
|
||||
}
|
||||
|
||||
.opt-btn.active {
|
||||
background-color: var(--bim-btn-active-bg, rgba(255, 255, 255, 0.15));
|
||||
color: var(--bim-btn-text-active-color, #fff);
|
||||
}
|
||||
|
||||
.opt-btn.active .opt-btn-icon {
|
||||
color: var(--bim-icon-active-color, #fff);
|
||||
}
|
||||
|
||||
.opt-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* --- 图标 --- */
|
||||
.opt-btn-icon {
|
||||
width: var(--bim-icon-size, 24px);
|
||||
height: var(--bim-icon-size, 24px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bim-icon-color, #ccc);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.opt-btn-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* --- 箭头 --- */
|
||||
.opt-btn-arrow {
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
transition: transform 0.2s;
|
||||
display: inline-block;
|
||||
/* 默认情况 (有Label) */
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.opt-btn-arrow.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* --- 文字容器 --- */
|
||||
.opt-btn-text-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* --- Label 显示控制 --- */
|
||||
.opt-btn-label {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.opt-btn.no-label .opt-btn-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- 场景 A: 有 Label (常规布局) --- */
|
||||
|
||||
.opt-btn.align-vertical:not(.no-label) {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.opt-btn.align-vertical:not(.no-label) .opt-btn-text-wrapper {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.opt-btn.align-vertical:not(.no-label) .opt-btn-label {
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.opt-btn.align-horizontal:not(.no-label) {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.opt-btn.align-horizontal:not(.no-label) .opt-btn-text-wrapper {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.opt-btn.align-horizontal:not(.no-label) .opt-btn-label {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* --- 场景 B: 无 Label (强制绝对定位) --- */
|
||||
|
||||
/* 当没有 label 时,text-wrapper 其实只包裹了 arrow */
|
||||
/* 我们需要让 wrapper 失去布局影响,直接定位内部的 arrow */
|
||||
|
||||
.opt-btn.no-label .opt-btn-text-wrapper {
|
||||
/* 让 wrapper 变为 0 尺寸,不影响 flex 布局 */
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
/* 关键:允许子元素溢出 */
|
||||
position: absolute;
|
||||
/* 脱离文档流,相对于 .opt-btn */
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.opt-btn.no-label .opt-btn-arrow {
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
margin: 0;
|
||||
/* 清除之前的 margin */
|
||||
font-size: 8px;
|
||||
/*
|
||||
如果父级 wrapper 已经是 top:0, right:0
|
||||
那么 arrow 相对 wrapper 定位即可
|
||||
*/
|
||||
}
|
||||
|
||||
/* --- 下拉菜单 & 动画 --- */
|
||||
.opt-btn-dropdown {
|
||||
position: absolute;
|
||||
background-color: var(--bim-toolbar-bg, rgba(17, 17, 17, 0.95));
|
||||
border-radius: 4px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1001;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
/* 动画起始状态 */
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
/* 默认向上偏移一点 */
|
||||
transition: opacity 0.2s ease, transform 0.2s cubic-bezier(0.2, 0, 0.2, 1), visibility 0.2s;
|
||||
}
|
||||
|
||||
/* 动画激活状态 (需要 JS 添加 .show 类,或者直接在 display:flex 时生效?) */
|
||||
/* 这里的实现有点 tricky,因为 JS 里直接 appendChild */
|
||||
/* 我们可以利用 CSS 动画关键帧,或者简单的 transition */
|
||||
/* 由于 DOM 是动态插入的,插入瞬间是 opacity: 0 -> requestAnimationFrame -> opacity: 1 */
|
||||
/* 简单的办法:添加动画 keyframes */
|
||||
|
||||
@keyframes dropdown-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px) scale(0.98);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.opt-btn-dropdown {
|
||||
/* 覆盖上面的 transition,直接用 animation */
|
||||
animation: dropdown-fade-in 0.2s cubic-bezier(0.2, 0, 0.2, 1) forwards;
|
||||
/* 初始可见性由 JS 控制(append 即显示) */
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
color: var(--bim-btn-text-color, #ccc);
|
||||
transition: background 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item:hover {
|
||||
background-color: var(--bim-btn-hover-bg, #444);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 下拉菜单项 - 横向布局(图标在左,默认) */
|
||||
.opt-btn-dropdown-item.align-horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item.align-horizontal .opt-btn-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 下拉菜单项 - 纵向布局(图标在上) */
|
||||
.opt-btn-dropdown-item.align-vertical {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item.align-vertical .opt-btn-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item.align-vertical .opt-btn-dropdown-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* --- 特定样式覆盖:底部工具栏 --- */
|
||||
.bim-btn-group-root.is-bottom-toolbar .opt-btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.bim-btn-group-root.is-bottom-toolbar .opt-btn {
|
||||
padding: 8px;
|
||||
}
|
||||
533
src/components/button-group/index.ts
Normal file
533
src/components/button-group/index.ts
Normal file
@@ -0,0 +1,533 @@
|
||||
import './index.css';
|
||||
import type {
|
||||
OptButton,
|
||||
ButtonGroup,
|
||||
ButtonGroupOptions,
|
||||
ButtonConfig,
|
||||
ButtonGroupColors
|
||||
} from './index.type';
|
||||
import { t, localeManager } from '../../services/locale';
|
||||
import { themeManager } from '../../services/theme';
|
||||
import type { ThemeConfig } from '../../themes/types';
|
||||
import { IBimComponent } from '../../types/component';
|
||||
|
||||
/**
|
||||
* 通用按钮组组件 (BimButtonGroup)
|
||||
*/
|
||||
export class BimButtonGroup implements IBimComponent {
|
||||
private container: HTMLElement;
|
||||
private options: ButtonGroupOptions;
|
||||
private groups: ButtonGroup[] = [];
|
||||
private activeBtnIds: Set<string> = new Set();
|
||||
private btnRefs: Map<string, HTMLElement> = new Map();
|
||||
private dropdownElement: HTMLElement | null = null;
|
||||
private hoverTimeout: number | null = null;
|
||||
private customColors: Set<keyof ButtonGroupColors> = new Set(); // 记录用户自定义的颜色属性
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
private unsubscribeTheme: (() => void) | null = null;
|
||||
|
||||
private readonly DEFAULT_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
|
||||
|
||||
constructor(options: ButtonGroupOptions) {
|
||||
const el = typeof options.container === 'string'
|
||||
? document.getElementById(options.container)
|
||||
: options.container;
|
||||
|
||||
if (!el) throw new Error('Container not found');
|
||||
|
||||
this.container = el;
|
||||
// 合并默认配置
|
||||
this.options = {
|
||||
showLabel: true,
|
||||
visibility: {},
|
||||
direction: 'row', // 默认横向
|
||||
position: 'static', // 默认静态定位
|
||||
align: 'vertical', // 默认图标在上
|
||||
expand: 'down', // 默认向下展开
|
||||
...options
|
||||
};
|
||||
|
||||
// 记录初始传入的自定义颜色
|
||||
const colorKeys: (keyof ButtonGroupColors)[] = [
|
||||
'backgroundColor', 'btnBackgroundColor', 'btnHoverColor',
|
||||
'btnActiveColor', 'iconColor', 'iconActiveColor',
|
||||
'textColor', 'textActiveColor'
|
||||
];
|
||||
colorKeys.forEach(key => {
|
||||
if (options[key]) {
|
||||
this.customColors.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
this.initContainer();
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
private initContainer(): void {
|
||||
this.container.innerHTML = '';
|
||||
this.container.classList.add('bim-btn-group-root');
|
||||
|
||||
if (this.options.direction === 'column') {
|
||||
this.container.classList.add('dir-column');
|
||||
} else {
|
||||
this.container.classList.add('dir-row');
|
||||
}
|
||||
|
||||
if (this.options.className) {
|
||||
this.container.classList.add(this.options.className);
|
||||
}
|
||||
|
||||
this.updatePosition();
|
||||
}
|
||||
|
||||
private updatePosition() {
|
||||
const pos = this.options.position;
|
||||
const style = this.container.style;
|
||||
|
||||
style.top = ''; style.bottom = ''; style.left = ''; style.right = ''; style.transform = '';
|
||||
|
||||
if (pos === 'static') {
|
||||
this.container.classList.add('static');
|
||||
return;
|
||||
}
|
||||
|
||||
this.container.classList.remove('static');
|
||||
this.container.style.position = 'absolute';
|
||||
|
||||
if (typeof pos === 'object' && 'x' in pos) {
|
||||
style.left = `${pos.x}px`;
|
||||
style.top = `${pos.y}px`;
|
||||
} else {
|
||||
const margin = '20px';
|
||||
switch (pos) {
|
||||
case 'top-left':
|
||||
style.top = margin; style.left = margin;
|
||||
break;
|
||||
case 'top-center':
|
||||
style.top = margin; style.left = '50%'; style.transform = 'translateX(-50%)';
|
||||
break;
|
||||
case 'top-right':
|
||||
style.top = margin; style.right = margin;
|
||||
break;
|
||||
case 'bottom-left':
|
||||
style.bottom = margin; style.left = margin;
|
||||
break;
|
||||
case 'bottom-center':
|
||||
style.bottom = margin; style.left = '50%'; style.transform = 'translateX(-50%)';
|
||||
break;
|
||||
case 'bottom-right':
|
||||
style.bottom = margin; style.right = margin;
|
||||
break;
|
||||
case 'left-center':
|
||||
style.left = margin; style.top = '50%'; style.transform = 'translateY(-50%)';
|
||||
break;
|
||||
case 'right-center':
|
||||
style.right = margin; style.top = '50%'; style.transform = 'translateY(-50%)';
|
||||
break;
|
||||
case 'center':
|
||||
style.top = '50%'; style.left = '50%'; style.transform = 'translate(-50%, -50%)';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用样式到容器
|
||||
*/
|
||||
private applyStyles(): void {
|
||||
const style = this.container.style;
|
||||
if (this.options.backgroundColor) style.setProperty('--bim-btn-group-section-bg', this.options.backgroundColor);
|
||||
if (this.options.btnBackgroundColor) style.setProperty('--bim-btn-bg', this.options.btnBackgroundColor);
|
||||
if (this.options.btnHoverColor) style.setProperty('--bim-btn-hover-bg', this.options.btnHoverColor);
|
||||
if (this.options.btnActiveColor) style.setProperty('--bim-btn-active-bg', this.options.btnActiveColor);
|
||||
if (this.options.iconColor) style.setProperty('--bim-icon-color', this.options.iconColor);
|
||||
if (this.options.iconActiveColor) style.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
|
||||
if (this.options.textColor) style.setProperty('--bim-btn-text-color', this.options.textColor);
|
||||
if (this.options.textActiveColor) style.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题颜色
|
||||
* 只会应用到没有被用户自定义的颜色属性上
|
||||
*/
|
||||
public setTheme(theme: ThemeConfig): void {
|
||||
const themeColors: ButtonGroupColors = {
|
||||
backgroundColor: theme.panelBackground,
|
||||
btnBackgroundColor: theme.componentBackground,
|
||||
btnHoverColor: theme.componentHover,
|
||||
btnActiveColor: theme.componentActive,
|
||||
iconColor: theme.icon,
|
||||
iconActiveColor: theme.iconActive,
|
||||
textColor: theme.textSecondary,
|
||||
textActiveColor: theme.textPrimary
|
||||
};
|
||||
|
||||
// 只应用没有被自定义的颜色
|
||||
Object.entries(themeColors).forEach(([key, value]) => {
|
||||
const colorKey = key as keyof ButtonGroupColors;
|
||||
if (!this.customColors.has(colorKey)) {
|
||||
this.options[colorKey] = value;
|
||||
}
|
||||
});
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接设置颜色(强制覆盖)
|
||||
* 设置的颜色会被标记为自定义,后续的 setTheme 不会覆盖它们
|
||||
*/
|
||||
public setColors(colors: ButtonGroupColors): void {
|
||||
// 更新 options
|
||||
this.options = { ...this.options, ...colors };
|
||||
|
||||
// 标记这些颜色为自定义
|
||||
Object.keys(colors).forEach(key => {
|
||||
this.customColors.add(key as keyof ButtonGroupColors);
|
||||
});
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
this.render();
|
||||
|
||||
// 自动订阅语言变更
|
||||
this.unsubscribeLocale = localeManager.subscribe(() => {
|
||||
this.setLocales();
|
||||
});
|
||||
|
||||
// 自动订阅主题变更
|
||||
this.unsubscribeTheme = themeManager.subscribe((theme) => {
|
||||
this.setTheme(theme);
|
||||
});
|
||||
}
|
||||
|
||||
public setLocales(): void {
|
||||
this.render();
|
||||
}
|
||||
|
||||
public addGroup(groupId: string, beforeGroupId?: string): void {
|
||||
if (this.groups.some(g => g.id === groupId)) return;
|
||||
const newGroup: ButtonGroup = { id: groupId, buttons: [] };
|
||||
if (beforeGroupId) {
|
||||
const index = this.groups.findIndex(g => g.id === beforeGroupId);
|
||||
index !== -1 ? this.groups.splice(index, 0, newGroup) : this.groups.push(newGroup);
|
||||
} else {
|
||||
this.groups.push(newGroup);
|
||||
}
|
||||
}
|
||||
|
||||
public addButton(config: ButtonConfig): void {
|
||||
const { groupId, parentId } = config;
|
||||
const group = this.groups.find(g => g.id === groupId);
|
||||
if (!group) return;
|
||||
|
||||
const button: OptButton = { ...config, children: config.children || [] };
|
||||
if (parentId) {
|
||||
const parentBtn = this.findButton(group.buttons, parentId);
|
||||
if (parentBtn) {
|
||||
if (!parentBtn.children) parentBtn.children = [];
|
||||
parentBtn.children.push(button);
|
||||
}
|
||||
} else {
|
||||
group.buttons.push(button);
|
||||
}
|
||||
}
|
||||
|
||||
private findButton(buttons: OptButton[], id: string): OptButton | undefined {
|
||||
for (const btn of buttons) {
|
||||
if (btn.id === id) return btn;
|
||||
if (btn.children) {
|
||||
const found = this.findButton(btn.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
this.container.innerHTML = '';
|
||||
this.btnRefs.clear();
|
||||
|
||||
this.groups.forEach((group, index) => {
|
||||
const groupElement = this.renderGroup(group, index, this.groups.length);
|
||||
this.container.appendChild(groupElement);
|
||||
});
|
||||
}
|
||||
|
||||
private renderGroup(group: ButtonGroup, index: number, total: number): HTMLElement {
|
||||
const groupEl = document.createElement('div');
|
||||
groupEl.className = 'bim-btn-group-section';
|
||||
|
||||
if (index < total - 1) {
|
||||
groupEl.classList.add('has-divider');
|
||||
}
|
||||
|
||||
group.buttons.forEach(button => {
|
||||
if (this.isVisible(button.id)) {
|
||||
const btnWrapper = this.renderButton(button);
|
||||
groupEl.appendChild(btnWrapper);
|
||||
}
|
||||
});
|
||||
return groupEl;
|
||||
}
|
||||
|
||||
private renderButton(button: OptButton): HTMLElement {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'opt-btn-wrapper';
|
||||
|
||||
const btnEl = document.createElement('div');
|
||||
btnEl.className = 'opt-btn';
|
||||
|
||||
// 按钮优先使用自己的 align,否则使用全局配置,默认为 vertical
|
||||
const align = button.align || this.options.align || 'vertical';
|
||||
if (align === 'horizontal') {
|
||||
btnEl.classList.add('align-horizontal');
|
||||
} else {
|
||||
btnEl.classList.add('align-vertical');
|
||||
}
|
||||
|
||||
if (this.activeBtnIds.has(button.id)) btnEl.classList.add('active');
|
||||
if (button.disabled) btnEl.classList.add('disabled');
|
||||
|
||||
// 判断是否显示 label
|
||||
const hasLabel = this.options.showLabel && button.label;
|
||||
if (!hasLabel) {
|
||||
btnEl.classList.add('no-label');
|
||||
}
|
||||
|
||||
// 应用按钮的自定义样式
|
||||
const iconSize = button.iconSize || 32;
|
||||
const minWidth = button.minWidth || 50;
|
||||
btnEl.style.minWidth = `${minWidth}px`;
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'opt-btn-icon';
|
||||
icon.style.width = `${iconSize}px`;
|
||||
icon.style.height = `${iconSize}px`;
|
||||
icon.innerHTML = this.getIcon(button.icon);
|
||||
btnEl.appendChild(icon);
|
||||
|
||||
// 创建文字和箭头的容器,确保它们始终在一起(无论主轴是横是竖)
|
||||
const textWrapper = document.createElement('div');
|
||||
textWrapper.className = 'opt-btn-text-wrapper';
|
||||
|
||||
if (this.options.showLabel && button.label) {
|
||||
const label = document.createElement('span');
|
||||
label.className = 'opt-btn-label';
|
||||
label.textContent = t(button.label);
|
||||
textWrapper.appendChild(label);
|
||||
}
|
||||
|
||||
if (button.children && button.children.length > 0) {
|
||||
const arrow = document.createElement('span');
|
||||
arrow.className = 'opt-btn-arrow';
|
||||
arrow.textContent = '▼';
|
||||
textWrapper.appendChild(arrow);
|
||||
}
|
||||
|
||||
// 只有当有内容时才添加 wrapper
|
||||
if (textWrapper.hasChildNodes()) {
|
||||
btnEl.appendChild(textWrapper);
|
||||
}
|
||||
|
||||
btnEl.addEventListener('click', () => this.handleClick(button));
|
||||
btnEl.addEventListener('mouseenter', () => this.handleMouseEnter(button, btnEl));
|
||||
btnEl.addEventListener('mouseleave', () => this.handleMouseLeave());
|
||||
|
||||
this.btnRefs.set(button.id, btnEl);
|
||||
wrapper.appendChild(btnEl);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
private handleClick(button: OptButton): void {
|
||||
if (button.disabled) return;
|
||||
if (!button.children || button.children.length === 0) {
|
||||
if (button.keepActive) {
|
||||
const wasActive = this.activeBtnIds.has(button.id);
|
||||
if (wasActive) this.activeBtnIds.delete(button.id);
|
||||
else this.activeBtnIds.add(button.id);
|
||||
this.updateButtonState(button.id);
|
||||
}
|
||||
this.closeDropdown();
|
||||
if (button.onClick) button.onClick(button);
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseEnter(button: OptButton, btnEl: HTMLElement): void {
|
||||
if (this.hoverTimeout) clearTimeout(this.hoverTimeout);
|
||||
if (button.children && button.children.length > 0) {
|
||||
this.showDropdown(button, btnEl);
|
||||
} else {
|
||||
this.closeDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
private handleMouseLeave(): void {
|
||||
this.hoverTimeout = window.setTimeout(() => this.closeDropdown(), 200);
|
||||
}
|
||||
|
||||
private showDropdown(button: OptButton, btnEl: HTMLElement): void {
|
||||
this.closeDropdown();
|
||||
if (!button.children) return;
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'opt-btn-dropdown';
|
||||
if (this.options.backgroundColor) dropdown.style.setProperty('--bim-toolbar-bg', this.options.backgroundColor);
|
||||
|
||||
// 获取按钮的位置信息
|
||||
const btnRect = btnEl.getBoundingClientRect();
|
||||
const expand = this.options.expand || 'down';
|
||||
|
||||
// 根据主按钮组的方向设置下拉菜单的布局方向
|
||||
if (this.options.direction === 'row') {
|
||||
dropdown.style.flexDirection = 'column'; // 横向按钮组,菜单纵向排列
|
||||
} else {
|
||||
dropdown.style.flexDirection = 'row'; // 纵向按钮组,菜单横向排列
|
||||
}
|
||||
|
||||
// 先添加到 DOM 以便计算尺寸
|
||||
document.body.appendChild(dropdown);
|
||||
|
||||
// 添加菜单项
|
||||
button.children.forEach(subBtn => {
|
||||
if (this.isVisible(subBtn.id)) {
|
||||
const item = this.renderDropdownItem(subBtn);
|
||||
dropdown.appendChild(item);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取下拉菜单的实际尺寸
|
||||
const dropdownRect = dropdown.getBoundingClientRect();
|
||||
|
||||
if (expand === 'up') {
|
||||
// 向上展开,与按钮水平居中对齐
|
||||
dropdown.style.bottom = (window.innerHeight - btnRect.top + 8) + 'px';
|
||||
dropdown.style.left = (btnRect.left + (btnRect.width - dropdownRect.width) / 2) + 'px';
|
||||
} else if (expand === 'down') {
|
||||
// 向下展开,与按钮水平居中对齐
|
||||
dropdown.style.top = (btnRect.bottom + 8) + 'px';
|
||||
dropdown.style.left = (btnRect.left + (btnRect.width - dropdownRect.width) / 2) + 'px';
|
||||
} else if (expand === 'right') {
|
||||
// 向右展开,与按钮垂直居中对齐
|
||||
dropdown.style.top = (btnRect.top + (btnRect.height - dropdownRect.height) / 2) + 'px';
|
||||
dropdown.style.left = (btnRect.right + 8) + 'px';
|
||||
} else if (expand === 'left') {
|
||||
// 向左展开,与按钮垂直居中对齐
|
||||
dropdown.style.top = (btnRect.top + (btnRect.height - dropdownRect.height) / 2) + 'px';
|
||||
dropdown.style.right = (window.innerWidth - btnRect.left + 8) + 'px';
|
||||
}
|
||||
|
||||
dropdown.addEventListener('mouseenter', () => { if (this.hoverTimeout) clearTimeout(this.hoverTimeout); });
|
||||
dropdown.addEventListener('mouseleave', () => this.handleMouseLeave());
|
||||
this.dropdownElement = dropdown;
|
||||
}
|
||||
|
||||
private renderDropdownItem(button: OptButton): HTMLElement {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'opt-btn-dropdown-item';
|
||||
|
||||
// 应用按钮的 align 设置,默认为 horizontal(图标在左)
|
||||
const align = button.align || 'horizontal';
|
||||
if (align === 'horizontal') {
|
||||
item.classList.add('align-horizontal');
|
||||
} else {
|
||||
item.classList.add('align-vertical');
|
||||
}
|
||||
|
||||
// 应用按钮的自定义样式
|
||||
const iconSize = button.iconSize || 32; // 二级菜单默认图标更小
|
||||
const minWidth = button.minWidth; // 不设置默认值,让下拉菜单项保持紧凑
|
||||
if (minWidth) {
|
||||
item.style.minWidth = `${minWidth}px`;
|
||||
}
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'opt-btn-icon';
|
||||
icon.style.width = `${iconSize}px`;
|
||||
icon.style.height = `${iconSize}px`;
|
||||
icon.innerHTML = this.getIcon(button.icon);
|
||||
item.appendChild(icon);
|
||||
|
||||
// 只有在 showLabel 为 true 时才显示 label
|
||||
if (this.options.showLabel && button.label) {
|
||||
const label = document.createElement('span');
|
||||
label.className = 'opt-btn-dropdown-label';
|
||||
label.textContent = t(button.label);
|
||||
item.appendChild(label);
|
||||
}
|
||||
|
||||
item.addEventListener('click', (e) => { e.stopPropagation(); this.handleClick(button); });
|
||||
return item;
|
||||
}
|
||||
|
||||
private closeDropdown(): void {
|
||||
if (this.dropdownElement) {
|
||||
this.dropdownElement.remove();
|
||||
this.dropdownElement = null;
|
||||
}
|
||||
this.btnRefs.forEach(btnEl => {
|
||||
const arrow = btnEl.querySelector('.opt-btn-arrow');
|
||||
if (arrow) arrow.classList.remove('rotated');
|
||||
});
|
||||
}
|
||||
|
||||
private updateButtonState(buttonId: string): void {
|
||||
const btnEl = this.btnRefs.get(buttonId);
|
||||
if (btnEl) {
|
||||
this.activeBtnIds.has(buttonId) ? btnEl.classList.add('active') : btnEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
private getIcon(icon?: string): string { return icon || this.DEFAULT_ICON; }
|
||||
public updateButtonVisibility(id: string, visible: boolean): void {
|
||||
if (!this.options.visibility) this.options.visibility = {};
|
||||
this.options.visibility[id] = visible;
|
||||
this.render();
|
||||
}
|
||||
public setShowLabel(show: boolean): void {
|
||||
this.options.showLabel = show;
|
||||
this.updateLabelsVisibility();
|
||||
}
|
||||
|
||||
private updateLabelsVisibility(): void {
|
||||
this.btnRefs.forEach((btnEl, buttonId) => {
|
||||
// 查找按钮配置
|
||||
const button = this.findButtonById(buttonId);
|
||||
if (!button) return;
|
||||
|
||||
const hasLabel = this.options.showLabel && button.label;
|
||||
|
||||
// 只需要更新 no-label 类,CSS 会处理显示/隐藏
|
||||
if (hasLabel) {
|
||||
btnEl.classList.remove('no-label');
|
||||
} else {
|
||||
btnEl.classList.add('no-label');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private findButtonById(id: string): OptButton | undefined {
|
||||
for (const group of this.groups) {
|
||||
const found = this.findButton(group.buttons, id);
|
||||
if (found) return found;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
public setBackgroundColor(color: string): void { this.setColors({ backgroundColor: color }); }
|
||||
private isVisible(id: string): boolean { return this.options.visibility?.[id] !== false; }
|
||||
public destroy(): void {
|
||||
if (this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale();
|
||||
this.unsubscribeLocale = null;
|
||||
}
|
||||
if (this.unsubscribeTheme) {
|
||||
this.unsubscribeTheme();
|
||||
this.unsubscribeTheme = null;
|
||||
}
|
||||
this.closeDropdown();
|
||||
this.container.innerHTML = '';
|
||||
this.btnRefs.clear();
|
||||
}
|
||||
}
|
||||
87
src/components/button-group/index.type.ts
Normal file
87
src/components/button-group/index.type.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
export type ButtonType = 'button' | 'menu';
|
||||
|
||||
/** 按钮配置 */
|
||||
export interface ButtonConfig {
|
||||
id: string;
|
||||
type: ButtonType;
|
||||
label: string;
|
||||
icon?: string;
|
||||
keepActive?: boolean;
|
||||
disabled?: boolean;
|
||||
onClick?: (button: OptButton) => void;
|
||||
children?: ButtonConfig[];
|
||||
groupId?: string;
|
||||
parentId?: string;
|
||||
/** 按钮内部图标文字排列 (默认 vertical,即图标在上) */
|
||||
align?: ButtonAlign;
|
||||
/** 图标大小 (正方形,单位 px,默认 32) */
|
||||
iconSize?: number;
|
||||
/** 按钮最小宽度 (单位 px,默认 50) */
|
||||
minWidth?: number;
|
||||
}
|
||||
|
||||
export interface OptButton extends ButtonConfig {
|
||||
children?: OptButton[];
|
||||
}
|
||||
|
||||
export interface ButtonGroup {
|
||||
id: string;
|
||||
buttons: OptButton[];
|
||||
}
|
||||
|
||||
export interface ButtonGroupColors {
|
||||
backgroundColor?: string;
|
||||
btnBackgroundColor?: string;
|
||||
btnHoverColor?: string;
|
||||
btnActiveColor?: string;
|
||||
iconColor?: string;
|
||||
iconActiveColor?: string;
|
||||
textColor?: string;
|
||||
textActiveColor?: string;
|
||||
}
|
||||
|
||||
// --- 新增布局类型 ---
|
||||
|
||||
/** 弹窗/按钮组位置 */
|
||||
export type GroupPosition =
|
||||
| 'center'
|
||||
| 'top-left' | 'top-center' | 'top-right'
|
||||
| 'left-center' | 'right-center'
|
||||
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||
| { x: number; y: number }
|
||||
| 'static'; // static 表示不绝对定位,随文档流
|
||||
|
||||
/** 按钮组排列方向 (Flex-direction) */
|
||||
export type GroupDirection = 'row' | 'column';
|
||||
|
||||
/** 按钮内部文字图标排列 */
|
||||
export type ButtonAlign = 'vertical' /* 图标在上 */ | 'horizontal' /* 图标在左 */;
|
||||
|
||||
/** 二级菜单展开方向 */
|
||||
export type ExpandDirection = 'up' | 'down' | 'left' | 'right';
|
||||
|
||||
export interface ButtonGroupOptions extends ButtonGroupColors {
|
||||
container: HTMLElement | string;
|
||||
|
||||
/** 屏幕位置 (如 top-left) */
|
||||
position?: GroupPosition;
|
||||
|
||||
/** 按钮组排列方向 (默认 row) */
|
||||
direction?: GroupDirection;
|
||||
|
||||
/** 按钮内部图标文字排列 (默认 vertical) */
|
||||
align?: ButtonAlign;
|
||||
|
||||
/** 菜单展开方向 */
|
||||
expand?: ExpandDirection;
|
||||
|
||||
showLabel?: boolean;
|
||||
visibility?: Record<string, boolean>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface ClickPayload {
|
||||
button: OptButton;
|
||||
action: 'activate' | 'deactivate' | 'trigger';
|
||||
isActive?: boolean;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ButtonConfig } from '../../index.type';
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
|
||||
/**
|
||||
* 首页按钮配置
|
||||
@@ -7,7 +7,7 @@ export const homeButton: ButtonConfig = {
|
||||
id: 'home',
|
||||
groupId: 'group-1',
|
||||
type: 'button',
|
||||
label: '首页',
|
||||
label: 'toolbar.home',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M4 21V9l8-6l8 6v12h-6v-7h-4v7z"/></svg>',
|
||||
keepActive: true,
|
||||
onClick: (button) => {
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ButtonConfig } from '../../index.type';
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
|
||||
/**
|
||||
* 定位按钮配置
|
||||
@@ -7,7 +7,7 @@ export const infoButton: ButtonConfig = {
|
||||
id: 'info',
|
||||
groupId: 'group-2',
|
||||
type: 'button',
|
||||
label: '信息',
|
||||
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) => {
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ButtonConfig } from '../../index.type';
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
|
||||
/**
|
||||
* 定位按钮配置
|
||||
@@ -7,7 +7,7 @@ export const locationButton: ButtonConfig = {
|
||||
id: 'location',
|
||||
groupId: 'group-1',
|
||||
type: 'button',
|
||||
label: '定位',
|
||||
label: 'toolbar.location',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 13h2v-2.75h2V13h2V8.25l-3-2l-3 2zm3 9q-4.025-3.425-6.012-6.362T4 10.2q0-3.75 2.413-5.975T12 2t5.588 2.225T20 10.2q0 2.5-1.987 5.438T12 22"/></svg>',
|
||||
keepActive: false,
|
||||
onClick: (button) => {
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ButtonConfig } from '../../index.type';
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
|
||||
/**
|
||||
* 定位按钮配置
|
||||
@@ -7,7 +7,7 @@ export const settingButton: ButtonConfig = {
|
||||
id: 'setting',
|
||||
groupId: 'group-2',
|
||||
type: 'button',
|
||||
label: '设置',
|
||||
label: 'toolbar.setting',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="m9.25 22l-.4-3.2q-.325-.125-.612-.3t-.563-.375L4.7 19.375l-2.75-4.75l2.575-1.95Q4.5 12.5 4.5 12.338v-.675q0-.163.025-.338L1.95 9.375l2.75-4.75l2.975 1.25q.275-.2.575-.375t.6-.3l.4-3.2h5.5l.4 3.2q.325.125.613.3t.562.375l2.975-1.25l2.75 4.75l-2.575 1.95q.025.175.025.338v.674q0 .163-.05.338l2.575 1.95l-2.75 4.75l-2.95-1.25q-.275.2-.575.375t-.6.3l-.4 3.2zM11 20h1.975l.35-2.65q.775-.2 1.438-.587t1.212-.938l2.475 1.025l.975-1.7l-2.15-1.625q.125-.35.175-.737T17.5 12t-.05-.787t-.175-.738l2.15-1.625l-.975-1.7l-2.475 1.05q-.55-.575-1.212-.962t-1.438-.588L13 4h-1.975l-.35 2.65q-.775.2-1.437.588t-1.213.937L5.55 7.15l-.975 1.7l2.15 1.6q-.125.375-.175.75t-.05.8q0 .4.05.775t.175.75l-2.15 1.625l.975 1.7l2.475-1.05q.55.575 1.213.963t1.437.587zm1.05-4.5q1.45 0 2.475-1.025T15.55 12t-1.025-2.475T12.05 8.5q-1.475 0-2.488 1.025T8.55 12t1.013 2.475T12.05 15.5M12 12"/></svg>',
|
||||
keepActive: false,
|
||||
onClick: (button) => {
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
import type { ButtonConfig } from '../../../../index.type';
|
||||
|
||||
export const walkBirdButton: ButtonConfig = {
|
||||
id: 'walk-bird',
|
||||
groupId: 'group-1',
|
||||
parentId: 'walk',
|
||||
align: 'vertical',
|
||||
type: 'button',
|
||||
label: '鸟瞰漫游',
|
||||
label: 'toolbar.walkBird',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||
onClick: (button) => {
|
||||
console.log('鸟瞰漫游被点击:', button.id);
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
import type { ButtonConfig } from '../../../../index.type';
|
||||
|
||||
/**
|
||||
* 漫游菜单按钮配置
|
||||
@@ -7,7 +7,8 @@ export const walkMenuButton: ButtonConfig = {
|
||||
id: 'walk',
|
||||
groupId: 'group-1',
|
||||
type: 'menu',
|
||||
label: '漫游',
|
||||
label: 'toolbar.walk',
|
||||
align: 'vertical',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||
keepActive: true,
|
||||
onClick: (button) => {
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { ButtonConfig } from '../../../index.type';
|
||||
import type { ButtonConfig } from '../../../../index.type';
|
||||
|
||||
export const walkPersonButton: ButtonConfig = {
|
||||
id: 'walk-person',
|
||||
groupId: 'group-1',
|
||||
parentId: 'walk',
|
||||
type: 'button',
|
||||
label: '人视漫游',
|
||||
align: 'vertical',
|
||||
label: 'toolbar.walkPerson',
|
||||
icon: '<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M9 22V8.775q-2.275-.6-3.637-2.512T4 2h2q0 2.075 1.338 3.538T10.75 7h2.5q.75 0 1.4.275t1.175.8L20.35 12.6l-1.4 1.4L15 10.05V22h-2v-6h-2v6zm3-16q-.825 0-1.412-.587T10 4t.588-1.412T12 2t1.413.588T14 4t-.587 1.413T12 6"/></svg>',
|
||||
onClick: (button) => {
|
||||
console.log('人视漫游被点击:', button.id);
|
||||
35
src/components/button-group/toolbar/index.ts
Normal file
35
src/components/button-group/toolbar/index.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { BimButtonGroup } from '../index';
|
||||
|
||||
/**
|
||||
* 底部工具栏 (Toolbar)
|
||||
* BimButtonGroup 的子类,专门用于加载工具栏默认按钮。
|
||||
*/
|
||||
export class Toolbar extends BimButtonGroup {
|
||||
/**
|
||||
* 重写初始化,加载默认按钮
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
await super.init();
|
||||
|
||||
// 动态加载默认按钮配置
|
||||
const { homeButton } = await import('./buttons/home');
|
||||
const { locationButton } = await import('./buttons/location');
|
||||
const { walkMenuButton } = await import('./buttons/walk/walk-menu');
|
||||
const { walkPersonButton } = await import('./buttons/walk/walk-person');
|
||||
const { walkBirdButton } = await import('./buttons/walk/walk-bird');
|
||||
const { settingButton } = await import('./buttons/setting');
|
||||
const { infoButton } = await import('./buttons/info');
|
||||
|
||||
this.addGroup('group-1');
|
||||
this.addButton(homeButton);
|
||||
this.addButton(walkMenuButton);
|
||||
this.addButton(walkPersonButton);
|
||||
this.addButton(walkBirdButton);
|
||||
this.addButton(locationButton);
|
||||
this.addGroup('group-2');
|
||||
this.addButton(settingButton);
|
||||
this.addButton(infoButton);
|
||||
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
@@ -2,18 +2,16 @@ import './index.css';
|
||||
import { BimDialog } from '../index';
|
||||
|
||||
/**
|
||||
* BimInfoDialog (二次封装示例)
|
||||
* 这是一个展示项目信息的业务弹窗组件,内部封装了 BimDialog。
|
||||
* BimInfoDialog (继承版)
|
||||
* 这是一个展示项目信息的业务弹窗组件,直接继承自 BimDialog。
|
||||
*/
|
||||
export class BimInfoDialog {
|
||||
private dialog: BimDialog;
|
||||
|
||||
export class BimInfoDialog extends BimDialog {
|
||||
/**
|
||||
* 构造函数
|
||||
* @param container 父容器
|
||||
*/
|
||||
constructor(container: HTMLElement) {
|
||||
// 创建自定义的 DOM 内容
|
||||
// 1. 准备内容 DOM
|
||||
const contentEl = document.createElement('div');
|
||||
contentEl.className = 'bim-info-dialog-content';
|
||||
|
||||
@@ -39,23 +37,29 @@ export class BimInfoDialog {
|
||||
contentEl.appendChild(infoList);
|
||||
contentEl.appendChild(actionBtn);
|
||||
|
||||
// 初始化 BimDialog,直接传入构建好的 HTMLElement
|
||||
this.dialog = new BimDialog({
|
||||
// 2. 调用父类构造函数,传入特定的配置
|
||||
super({
|
||||
container: container,
|
||||
title: 'Project Info (Wrapped)',
|
||||
content: contentEl,
|
||||
title: 'dialog.testTitle',
|
||||
content: contentEl,
|
||||
width: 320,
|
||||
height: 'auto',
|
||||
position: 'center',
|
||||
resizable: true,
|
||||
draggable: true
|
||||
draggable: true,
|
||||
// 可以在这里添加特定的 onClose 逻辑
|
||||
onClose: () => {
|
||||
console.log('Info dialog closed');
|
||||
},
|
||||
onOpen: () => {
|
||||
console.log('Info dialog opened');
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 如果有特定于子类的初始化逻辑,可以在 super() 之后执行
|
||||
// 例如:this.element.classList.add('my-special-class');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭弹窗
|
||||
*/
|
||||
public close() {
|
||||
this.dialog.close();
|
||||
}
|
||||
// 不需要再手动实现 setTheme, destroy, close, init
|
||||
// 它们都已从 BimDialog 继承
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
import './index.css';
|
||||
import type { DialogOptions } from './index.type';
|
||||
import type { ThemeConfig } from '../../themes/types';
|
||||
import { IBimComponent } from '../../types/component';
|
||||
import { themeManager } from '../../services/theme';
|
||||
import { t, localeManager } from '../../services/locale';
|
||||
|
||||
/**
|
||||
* 通用弹窗组件类
|
||||
* 支持拖拽、缩放、自定义内容和位置。
|
||||
*/
|
||||
export class BimDialog {
|
||||
export class BimDialog implements IBimComponent {
|
||||
private element: HTMLElement;
|
||||
private options: DialogOptions;
|
||||
private container: HTMLElement;
|
||||
private header: HTMLElement;
|
||||
private contentArea: HTMLElement;
|
||||
private _isDestroyed = false;
|
||||
private _isInitialized = false;
|
||||
private unsubscribeTheme: (() => void) | null = null;
|
||||
private unsubscribeLocale: (() => void) | null = null;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
@@ -37,10 +44,69 @@ export class BimDialog {
|
||||
this.header = this.element.querySelector('.bim-dialog-header') as HTMLElement;
|
||||
this.contentArea = this.element.querySelector('.bim-dialog-content') as HTMLElement;
|
||||
|
||||
// 初始化
|
||||
// 自动初始化 (为了兼容现有逻辑)
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
* @param theme 全局主题配置
|
||||
*/
|
||||
public setTheme(theme: ThemeConfig) {
|
||||
const style = this.element.style;
|
||||
if (!this.options.backgroundColor) style.setProperty('--bim-dialog-bg', theme.panelBackground);
|
||||
if (!this.options.headerBackgroundColor) style.setProperty('--bim-dialog-header-bg', theme.componentHover);
|
||||
if (!this.options.titleColor) style.setProperty('--bim-dialog-title-color', theme.textPrimary);
|
||||
if (!this.options.textColor) style.setProperty('--bim-dialog-text-color', theme.textPrimary);
|
||||
if (!this.options.borderColor) style.setProperty('--bim-dialog-border-color', theme.border);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件功能 (接口实现)
|
||||
*/
|
||||
public init() {
|
||||
if (this._isInitialized) return;
|
||||
|
||||
this.container.appendChild(this.element);
|
||||
|
||||
// 必须先挂载才能计算尺寸进行定位
|
||||
this.initPosition();
|
||||
|
||||
if (this.options.draggable) {
|
||||
this.initDrag();
|
||||
}
|
||||
|
||||
if (this.options.resizable) {
|
||||
this.initResize();
|
||||
}
|
||||
|
||||
this._isInitialized = true;
|
||||
|
||||
// 调用弹窗开启后回调
|
||||
if (this.options.onOpen) {
|
||||
this.options.onOpen();
|
||||
}
|
||||
|
||||
// 自动订阅主题变更
|
||||
this.unsubscribeTheme = themeManager.subscribe((theme) => {
|
||||
this.setTheme(theme);
|
||||
});
|
||||
|
||||
// 自动订阅语言变更
|
||||
this.unsubscribeLocale = localeManager.subscribe(() => {
|
||||
this.setLocales();
|
||||
});
|
||||
}
|
||||
|
||||
public setLocales(): void {
|
||||
if (this.options.title) {
|
||||
const titleEl = this.header.querySelector('.bim-dialog-title');
|
||||
if (titleEl) {
|
||||
titleEl.textContent = t(this.options.title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建弹窗的 DOM 结构
|
||||
*/
|
||||
@@ -49,7 +115,7 @@ export class BimDialog {
|
||||
el.className = 'bim-dialog';
|
||||
|
||||
if (this.options.id) el.id = this.options.id;
|
||||
|
||||
|
||||
// 应用颜色配置到 CSS 变量 (局部作用域)
|
||||
const style = el.style;
|
||||
if (this.options.backgroundColor) style.setProperty('--bim-dialog-bg', this.options.backgroundColor);
|
||||
@@ -68,7 +134,7 @@ export class BimDialog {
|
||||
|
||||
const title = document.createElement('span');
|
||||
title.className = 'bim-dialog-title';
|
||||
title.textContent = this.options.title || '';
|
||||
title.textContent = this.options.title ? t(this.options.title) : '';
|
||||
|
||||
const closeBtn = document.createElement('span');
|
||||
closeBtn.className = 'bim-dialog-close';
|
||||
@@ -112,24 +178,6 @@ export class BimDialog {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件功能
|
||||
*/
|
||||
private init() {
|
||||
this.container.appendChild(this.element);
|
||||
|
||||
// 必须先挂载才能计算尺寸进行定位
|
||||
this.initPosition();
|
||||
|
||||
if (this.options.draggable) {
|
||||
this.initDrag();
|
||||
}
|
||||
|
||||
if (this.options.resizable) {
|
||||
this.initResize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化弹窗位置
|
||||
*/
|
||||
@@ -137,7 +185,7 @@ export class BimDialog {
|
||||
const pos = this.options.position;
|
||||
|
||||
const elRect = this.element.getBoundingClientRect();
|
||||
|
||||
|
||||
// 计算相对父容器的定位
|
||||
let left = 0;
|
||||
let top = 0;
|
||||
@@ -285,10 +333,25 @@ export class BimDialog {
|
||||
*/
|
||||
public close() {
|
||||
if (this._isDestroyed) return;
|
||||
if (this.unsubscribeTheme) {
|
||||
this.unsubscribeTheme();
|
||||
this.unsubscribeTheme = null;
|
||||
}
|
||||
if (this.unsubscribeLocale) {
|
||||
this.unsubscribeLocale();
|
||||
this.unsubscribeLocale = null;
|
||||
}
|
||||
this.element.remove();
|
||||
this._isDestroyed = true;
|
||||
if (this.options.onClose) {
|
||||
this.options.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁组件 (接口实现)
|
||||
*/
|
||||
public destroy() {
|
||||
this.close();
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
* 可以是预设的字符串位置(如 'center', 'top-left' 等),
|
||||
* 也可以是具体的坐标对象 { x, y }
|
||||
*/
|
||||
export type DialogPosition =
|
||||
| 'center'
|
||||
| 'top-left' | 'top-center' | 'top-right'
|
||||
| 'left-center' | 'right-center'
|
||||
export type DialogPosition =
|
||||
| 'center'
|
||||
| 'top-left' | 'top-center' | 'top-right'
|
||||
| 'left-center' | 'right-center'
|
||||
| 'bottom-left' | 'bottom-center' | 'bottom-right'
|
||||
| { x: number; y: number };
|
||||
|
||||
@@ -52,6 +52,8 @@ export interface DialogOptions extends DialogColors {
|
||||
minHeight?: number;
|
||||
/** 关闭时的回调函数 */
|
||||
onClose?: () => void;
|
||||
/** 打开时的回调函数 */
|
||||
onOpen?: () => void;
|
||||
/** 弹窗唯一标识 ID (可选) */
|
||||
id?: string;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { BimEngine } from './bim-engine';
|
||||
|
||||
// 导出 OptBtnGroups 组件,用于工具栏操作
|
||||
export { OptBtnGroups } from './toolbar';
|
||||
// 导出通用组件
|
||||
export { BimButtonGroup } from './components/button-group';
|
||||
export { Toolbar } from './components/button-group/toolbar';
|
||||
|
||||
// 导出相关类型定义
|
||||
export type { OptButton, ButtonGroup, OptBtnGroupsOptions, ClickPayload } from './toolbar/index.type';
|
||||
export type { OptButton, ButtonGroup, ButtonGroupOptions, ClickPayload } from './components/button-group/index.type';
|
||||
|
||||
// 导出主引擎类
|
||||
export { BimEngine };
|
||||
24
src/locales/en-US.ts
Normal file
24
src/locales/en-US.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { TranslationDictionary } from './types';
|
||||
|
||||
export const enUS: TranslationDictionary = {
|
||||
common: {
|
||||
title: 'BimEngine',
|
||||
description: 'This is a BIM-ENGINE demo.',
|
||||
openTestDialog: 'Open Test Dialog',
|
||||
openInfoDialog: 'Open Info Dialog (Wrapped)',
|
||||
},
|
||||
toolbar: {
|
||||
home: 'Home',
|
||||
info: 'Info',
|
||||
location: 'Location',
|
||||
setting: 'Settings',
|
||||
walk: 'Walk',
|
||||
walkPerson: 'Person',
|
||||
walkBird: 'Bird Eye',
|
||||
walkMenu: 'Menu',
|
||||
},
|
||||
dialog: {
|
||||
testTitle: 'Test Dialog',
|
||||
testContent: '<div style="padding: 10px;">This is a <b>draggable</b> and <b>resizable</b> dialog.<br><br>Try dragging the title bar or resizing from the bottom-right corner.</div>',
|
||||
},
|
||||
};
|
||||
31
src/locales/types.ts
Normal file
31
src/locales/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 翻译字典接口
|
||||
* 定义所有可用的翻译键值对结构,保证类型安全
|
||||
*/
|
||||
export interface TranslationDictionary {
|
||||
common: {
|
||||
title: string;
|
||||
description: string;
|
||||
openTestDialog: string;
|
||||
openInfoDialog: string;
|
||||
};
|
||||
toolbar: {
|
||||
home: string;
|
||||
info: string;
|
||||
location: string;
|
||||
setting: string;
|
||||
walk: string;
|
||||
walkPerson: string;
|
||||
walkBird: string;
|
||||
walkMenu: string;
|
||||
};
|
||||
dialog: {
|
||||
testTitle: string;
|
||||
testContent: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言代码类型
|
||||
*/
|
||||
export type LocaleType = 'zh-CN' | 'en-US';
|
||||
24
src/locales/zh-CN.ts
Normal file
24
src/locales/zh-CN.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { TranslationDictionary } from './types';
|
||||
|
||||
export const zhCN: TranslationDictionary = {
|
||||
common: {
|
||||
title: 'BimEngine',
|
||||
description: '这是一个使用 BIM-ENGINE。',
|
||||
openTestDialog: '打开测试弹窗',
|
||||
openInfoDialog: '打开信息弹窗 (封装版)',
|
||||
},
|
||||
toolbar: {
|
||||
home: '首页',
|
||||
info: '信息',
|
||||
location: '定位',
|
||||
setting: '设置',
|
||||
walk: '漫游',
|
||||
walkPerson: '人视',
|
||||
walkBird: '鸟瞰',
|
||||
walkMenu: '菜单',
|
||||
},
|
||||
dialog: {
|
||||
testTitle: '测试弹窗',
|
||||
testContent: '<div style="padding: 10px;">这是一个 <b>可拖拽</b> 且 <b>可缩放</b> 的弹窗。<br><br>你可以尝试拖动标题栏,或者拖动右下角改变大小。</div>',
|
||||
},
|
||||
};
|
||||
48
src/managers/button-group-manager.ts
Normal file
48
src/managers/button-group-manager.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { BimButtonGroup } from '../components/button-group';
|
||||
import type { ButtonGroupOptions } from '../components/button-group/index.type';
|
||||
import type { ThemeConfig } from '../themes/types';
|
||||
|
||||
/**
|
||||
* 通用按钮组管理器
|
||||
* 负责创建和管理除底部工具栏以外的其他按钮组。
|
||||
*/
|
||||
export class ButtonGroupManager {
|
||||
private activeGroups: BimButtonGroup[] = [];
|
||||
private container: HTMLElement;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的按钮组
|
||||
*/
|
||||
public create(options: Omit<ButtonGroupOptions, 'container'>): BimButtonGroup {
|
||||
// 自动创建一个 div 作为容器
|
||||
const groupContainer = document.createElement('div');
|
||||
this.container.appendChild(groupContainer);
|
||||
|
||||
const group = new BimButtonGroup({
|
||||
container: groupContainer,
|
||||
...options
|
||||
});
|
||||
|
||||
// 立即初始化
|
||||
group.init();
|
||||
this.activeGroups.push(group);
|
||||
return group;
|
||||
}
|
||||
|
||||
public updateTheme(theme: ThemeConfig) {
|
||||
this.activeGroups.forEach(g => g.setTheme(theme));
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this.activeGroups.forEach(g => g.render());
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.activeGroups.forEach(g => g.destroy());
|
||||
this.activeGroups = [];
|
||||
}
|
||||
}
|
||||
69
src/managers/dialog-manager.ts
Normal file
69
src/managers/dialog-manager.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { BimDialog } from '../components/dialog';
|
||||
import { BimInfoDialog } from '../components/dialog/bimInfoDialog';
|
||||
import type { DialogOptions } from '../components/dialog/index.type';
|
||||
import type { ThemeConfig } from '../themes/types';
|
||||
import { themeManager } from '../services/theme'; // 修正路径
|
||||
|
||||
/**
|
||||
* 弹窗管理器
|
||||
* 负责创建和管理应用中的各类弹窗。
|
||||
*/
|
||||
export class DialogManager {
|
||||
/** 弹窗挂载的父容器 */
|
||||
private container: HTMLElement;
|
||||
/** 活跃的弹窗实例列表 */
|
||||
private activeDialogs: BimDialog[] = [];
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param container 弹窗挂载的目标容器
|
||||
*/
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个通用弹窗
|
||||
* @param options 弹窗配置选项(不需要传 container,自动使用管理器绑定的容器)
|
||||
* @returns BimDialog 实例
|
||||
*/
|
||||
public create(options: Omit<DialogOptions, 'container'>): BimDialog {
|
||||
const dialog = new BimDialog({
|
||||
container: this.container,
|
||||
...options,
|
||||
onClose: () => {
|
||||
// 从活跃列表中移除
|
||||
this.activeDialogs = this.activeDialogs.filter(d => d !== dialog);
|
||||
if (options.onClose) options.onClose();
|
||||
}
|
||||
});
|
||||
|
||||
// 应用当前主题
|
||||
dialog.setTheme(themeManager.getTheme());
|
||||
|
||||
this.activeDialogs.push(dialog);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示二次封装的模型信息弹窗
|
||||
* 演示如何调用特定的业务弹窗组件
|
||||
*/
|
||||
public showInfoDialog() {
|
||||
// 最佳实践:所有弹窗应通过 create 统一管理,或者手动加入管理。
|
||||
new BimInfoDialog(this.container);
|
||||
// 暂时不做主题追踪,作为遗留逻辑保留
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应全局主题变更
|
||||
* @param theme 全局主题配置
|
||||
*/
|
||||
public updateTheme(theme: ThemeConfig) {
|
||||
this.activeDialogs.forEach(dialog => {
|
||||
if (dialog.setTheme) {
|
||||
dialog.setTheme(theme);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
63
src/managers/toolbar-manager.ts
Normal file
63
src/managers/toolbar-manager.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { ButtonGroupColors, ButtonConfig } from '../components/button-group/index.type';
|
||||
import { Toolbar } from '../components/button-group/toolbar';
|
||||
import type { ThemeConfig } from '../themes/types';
|
||||
|
||||
/**
|
||||
* 底部工具栏管理器 (ToolbarManager)
|
||||
* 仅负责管理底部工具栏实例。
|
||||
*/
|
||||
export class ToolbarManager {
|
||||
private toolbar: Toolbar | null = null;
|
||||
private toolbarContainer: HTMLElement | null = null;
|
||||
private container: HTMLElement;
|
||||
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
this.init();
|
||||
}
|
||||
|
||||
private init() {
|
||||
// 创建底部工具栏专用容器
|
||||
this.toolbarContainer = document.createElement('div');
|
||||
this.toolbarContainer.id = 'opt-btn-groups';
|
||||
this.toolbarContainer.className = 'bim-engine-opt-btn-container is-bottom-toolbar';
|
||||
this.container.appendChild(this.toolbarContainer);
|
||||
|
||||
this.toolbar = new Toolbar({
|
||||
container: this.toolbarContainer,
|
||||
showLabel: true,
|
||||
direction: 'row',
|
||||
position: 'bottom-center', // 底部居中
|
||||
align: 'vertical', // 图标在上
|
||||
expand: 'up' // 向上展开
|
||||
});
|
||||
|
||||
this.toolbar.init();
|
||||
}
|
||||
|
||||
public updateTheme(theme: ThemeConfig) {
|
||||
this.toolbar?.setTheme(theme);
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this.toolbar?.render();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.toolbar?.destroy();
|
||||
this.toolbar = null;
|
||||
}
|
||||
|
||||
// --- 转发 API ---
|
||||
public addGroup(groupId: string, beforeGroupId?: string) { this.toolbar?.addGroup(groupId, beforeGroupId); this.toolbar?.render(); }
|
||||
public addButton(config: ButtonConfig) { this.toolbar?.addButton(config); this.toolbar?.render(); }
|
||||
public setButtonVisibility(id: string, v: boolean) { this.toolbar?.updateButtonVisibility(id, v); }
|
||||
public setShowLabel(show: boolean) { this.toolbar?.setShowLabel(show); }
|
||||
public setVisible(visible: boolean) {
|
||||
if (this.toolbarContainer) {
|
||||
this.toolbarContainer.style.visibility = visible ? 'visible' : 'hidden';
|
||||
}
|
||||
}
|
||||
public setBackgroundColor(color: string) { this.toolbar?.setBackgroundColor(color); }
|
||||
public setColors(colors: ButtonGroupColors) { this.toolbar?.setColors(colors); }
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { BimDialog } from '../dialog';
|
||||
import { BimInfoDialog } from '../dialog/bimInfoDialog';
|
||||
import type { DialogOptions } from '../dialog/index.type';
|
||||
|
||||
/**
|
||||
* 弹窗管理器
|
||||
* 负责创建和管理应用中的各类弹窗。
|
||||
*/
|
||||
export class DialogManager {
|
||||
/** 弹窗挂载的父容器 */
|
||||
private container: HTMLElement;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param container 弹窗挂载的目标容器
|
||||
*/
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个通用弹窗
|
||||
* @param options 弹窗配置选项(不需要传 container,自动使用管理器绑定的容器)
|
||||
* @returns BimDialog 实例
|
||||
*/
|
||||
public create(options: Omit<DialogOptions, 'container'>): BimDialog {
|
||||
return new BimDialog({
|
||||
container: this.container,
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示二次封装的模型信息弹窗
|
||||
* 演示如何调用特定的业务弹窗组件
|
||||
*/
|
||||
public showInfoDialog() {
|
||||
new BimInfoDialog(this.container);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
import type { ToolbarColors } from '../toolbar/index.type';
|
||||
import { OptBtnGroups } from '../toolbar';
|
||||
import type { ButtonConfig } from '../toolbar/index.type';
|
||||
|
||||
/**
|
||||
* 工具栏管理器
|
||||
* 负责管理底部操作栏的按钮组、按钮及其可见性等状态。
|
||||
*/
|
||||
export class ToolbarManager {
|
||||
/** 内部工具栏组件实例 */
|
||||
private optBtnGroups: OptBtnGroups | null = null;
|
||||
/** 工具栏挂载的容器 */
|
||||
private container: HTMLElement;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param container 工具栏挂载的容器元素
|
||||
*/
|
||||
constructor(container: HTMLElement) {
|
||||
this.container = container;
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化工具栏
|
||||
*/
|
||||
private init() {
|
||||
this.optBtnGroups = new OptBtnGroups({
|
||||
container: this.container,
|
||||
showLabel: true
|
||||
});
|
||||
|
||||
// 初始化并加载默认按钮配置
|
||||
this.optBtnGroups.init().catch(err => {
|
||||
console.error('Failed to initialize OptBtnGroups:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个工具栏按钮组
|
||||
* @param groupId 新组的 ID
|
||||
* @param beforeGroupId (可选) 插入到哪个组之前,不传则追加到最后
|
||||
*/
|
||||
public addGroup(groupId: string, beforeGroupId?: string) {
|
||||
if (this.optBtnGroups) {
|
||||
this.optBtnGroups.addGroup(groupId, beforeGroupId);
|
||||
this.optBtnGroups.render(); // 重新渲染以更新 UI
|
||||
} else {
|
||||
console.warn('Toolbar not initialized yet.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加一个工具栏按钮
|
||||
* @param config 按钮配置对象
|
||||
*/
|
||||
public addButton(config: ButtonConfig) {
|
||||
if (this.optBtnGroups) {
|
||||
this.optBtnGroups.addButton(config);
|
||||
this.optBtnGroups.render(); // 重新渲染以更新 UI
|
||||
} else {
|
||||
console.warn('Toolbar not initialized yet.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置按钮的可见性
|
||||
* @param buttonId 按钮 ID
|
||||
* @param visible 是否可见
|
||||
*/
|
||||
public setButtonVisibility(buttonId: string, visible: boolean) {
|
||||
if (this.optBtnGroups) {
|
||||
this.optBtnGroups.updateButtonVisibility(buttonId, visible);
|
||||
} else {
|
||||
console.warn('Toolbar not initialized yet.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否显示按钮下方的文字标签
|
||||
* @param show 是否显示
|
||||
*/
|
||||
public setShowLabel(show: boolean) {
|
||||
if (this.optBtnGroups) {
|
||||
this.optBtnGroups.setShowLabel(show);
|
||||
} else {
|
||||
console.warn('Toolbar not initialized yet.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置整个工具栏的可见性
|
||||
* @param visible 是否可见
|
||||
*/
|
||||
public setVisible(visible: boolean) {
|
||||
this.container.style.display = visible ? 'block' : 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具栏背景颜色
|
||||
* @param color CSS 颜色值
|
||||
*/
|
||||
public setBackgroundColor(color: string) {
|
||||
if (this.optBtnGroups) {
|
||||
this.optBtnGroups.setBackgroundColor(color);
|
||||
} else {
|
||||
console.warn('Toolbar not initialized yet.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置工具栏详细颜色配置
|
||||
* @param colors 颜色配置对象
|
||||
*/
|
||||
public setColors(colors: ToolbarColors) {
|
||||
if (this.optBtnGroups) {
|
||||
this.optBtnGroups.setColors(colors);
|
||||
} else {
|
||||
console.warn('Toolbar not initialized yet.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁工具栏管理器
|
||||
*/
|
||||
public destroy() {
|
||||
if (this.optBtnGroups) {
|
||||
this.optBtnGroups.destroy();
|
||||
this.optBtnGroups = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
80
src/services/locale.ts
Normal file
80
src/services/locale.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { LocaleType, TranslationDictionary } from '../locales/types';
|
||||
import { zhCN } from '../locales/zh-CN';
|
||||
import { enUS } from '../locales/en-US';
|
||||
|
||||
type LocaleChangeListener = (locale: LocaleType) => void;
|
||||
|
||||
/**
|
||||
* 语言管理器类
|
||||
*/
|
||||
export class LocaleManager {
|
||||
private currentLocale: LocaleType = 'zh-CN';
|
||||
private messages: Record<LocaleType, TranslationDictionary> = {
|
||||
'zh-CN': zhCN,
|
||||
'en-US': enUS,
|
||||
};
|
||||
private listeners: LocaleChangeListener[] = [];
|
||||
|
||||
constructor() {
|
||||
// 默认初始化
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
*/
|
||||
public getLocale(): LocaleType {
|
||||
return this.currentLocale;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换语言
|
||||
*/
|
||||
public setLocale(locale: LocaleType) {
|
||||
if (this.currentLocale === locale) return;
|
||||
this.currentLocale = locale;
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 翻译核心方法
|
||||
*/
|
||||
public t(key: string): string {
|
||||
if (!key) return '';
|
||||
|
||||
const keys = key.split('.');
|
||||
let value: any = this.messages[this.currentLocale];
|
||||
|
||||
for (const k of keys) {
|
||||
if (value && typeof value === 'object' && k in value) {
|
||||
value = value[k];
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return value as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅变更
|
||||
*/
|
||||
public subscribe(listener: LocaleChangeListener): () => void {
|
||||
this.listeners.push(listener);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(l => l !== listener);
|
||||
};
|
||||
}
|
||||
|
||||
private notifyListeners() {
|
||||
this.listeners.forEach(listener => listener(this.currentLocale));
|
||||
}
|
||||
}
|
||||
|
||||
// --- 导出单例 ---
|
||||
export const localeManager = new LocaleManager();
|
||||
|
||||
// --- 导出便捷方法 ---
|
||||
/**
|
||||
* 全局翻译函数
|
||||
* @param key 键路径 (如 'toolbar.home')
|
||||
*/
|
||||
export const t = (key: string): string => localeManager.t(key);
|
||||
70
src/services/theme.ts
Normal file
70
src/services/theme.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { ThemeConfig } from '../themes/types';
|
||||
import { darkTheme, lightTheme } from '../themes/presets';
|
||||
|
||||
type ThemeChangeListener = (theme: ThemeConfig) => void;
|
||||
|
||||
/**
|
||||
* 主题管理器 (单例)
|
||||
*/
|
||||
export class ThemeManager {
|
||||
private currentTheme: ThemeConfig = darkTheme;
|
||||
private listeners: ThemeChangeListener[] = [];
|
||||
|
||||
constructor() {
|
||||
// 默认初始化
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前主题配置
|
||||
*/
|
||||
public getTheme(): ThemeConfig {
|
||||
return this.currentTheme;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换预设主题
|
||||
* @param themeName 'dark' | 'light'
|
||||
*/
|
||||
public setTheme(themeName: 'dark' | 'light') {
|
||||
if (themeName === 'light') {
|
||||
this.applyTheme(lightTheme);
|
||||
} else {
|
||||
this.applyTheme(darkTheme);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用自定义主题配置
|
||||
* @param theme 配置对象
|
||||
*/
|
||||
public setCustomTheme(theme: ThemeConfig) {
|
||||
this.applyTheme(theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部应用主题逻辑
|
||||
*/
|
||||
private applyTheme(theme: ThemeConfig) {
|
||||
this.currentTheme = theme;
|
||||
this.notifyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅主题变更
|
||||
*/
|
||||
public subscribe(listener: ThemeChangeListener): () => void {
|
||||
this.listeners.push(listener);
|
||||
// 立即回调一次当前状态
|
||||
listener(this.currentTheme);
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter(l => l !== listener);
|
||||
};
|
||||
}
|
||||
|
||||
private notifyListeners() {
|
||||
this.listeners.forEach(listener => listener(this.currentTheme));
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const themeManager = new ThemeManager();
|
||||
95
src/themes/presets.ts
Normal file
95
src/themes/presets.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { ThemeConfig } from './types';
|
||||
|
||||
/**
|
||||
* 深色主题 (默认)
|
||||
*/
|
||||
export const darkTheme: ThemeConfig = {
|
||||
name: 'dark',
|
||||
primary: '#0078d4',
|
||||
primaryHover: '#0063b1',
|
||||
|
||||
// 修改:背景色统一为浅灰,不再跟随深色模式变黑
|
||||
background: '#f5f5f5',
|
||||
panelBackground: 'rgba(30, 30, 30, 0.9)',
|
||||
|
||||
// 注意:如果背景是浅色,主文字颜色通常需要是深色才能看清
|
||||
// 但这里的 textPrimary 主要是用于 UI 组件内部的。
|
||||
// 如果 BimEngine wrapper 上的文字直接显示在 background 上,
|
||||
// 我们可能需要区分 "UI文字" 和 "页面文字"。
|
||||
// 目前架构中:
|
||||
// theme.textPrimary 会应用到 wrapper.style.color (BimEngine.ts)
|
||||
// 以及 Toolbar/Dialog 的文字颜色。
|
||||
|
||||
// 如果背景是浅灰,而 wrapper 文字设置为白色 (#ffffff),那就看不清了。
|
||||
// 这是一个语义冲突:
|
||||
// 1. Panel (Toolbar/Dialog) 是黑底,需要白字。
|
||||
// 2. Background (Wrapper) 是白底,需要黑字。
|
||||
|
||||
// 既然您要求背景统一浅灰,那么 Wrapper 上的“直接子文本”应该是深色。
|
||||
// 但 Toolbar/Dialog 仍然是深色模式(黑底),它们需要白字。
|
||||
|
||||
// 妥协方案:
|
||||
// 保持 textPrimary 为白色(为了适配黑<E9858D><E9BB91><EFBFBD>的 Toolbar/Dialog)。
|
||||
// 但是在 BimEngine 中,如果背景强制改为浅色,Wrapper 的默认文字颜色可能需要单独处理,
|
||||
// 或者我们可以认为 "Wrapper" 主要是承载 UI 组件的,直接写在 Wrapper 上的文字(标题/描述)
|
||||
// 应该有自己的样式,而不是直接继承 theme.textPrimary。
|
||||
|
||||
// 在之前的 BimEngine.ts 中:
|
||||
// this.wrapper.style.color = theme.textPrimary;
|
||||
|
||||
// 如果背景变浅灰,这里 textPrimary 还是白色的话,标题就看不见了。
|
||||
// 所以,深色模式下:
|
||||
// 背景:浅灰
|
||||
// 组件:深黑
|
||||
// 组件文字:白
|
||||
// 页面文字:黑 (问题点)
|
||||
|
||||
// 让我们先按您的要求改背景。通常这种情况下,ThemeConfig 可能需要区分
|
||||
// contentText (页面内容文字) 和 uiText (组件文字)。
|
||||
// 但为了不破坏现有结构,我将假定 textPrimary 主要服务于 UI 组件。
|
||||
// 为了让 Wrapper 上的标题可见,我们可能需要在 BimEngine 中移除对 wrapper.style.color 的强制设置,
|
||||
// 或者在 presets 里把 textPrimary 改回来?不对,改回来 Toolbar 就看不清了。
|
||||
|
||||
// 方案:我将仅修改 background。
|
||||
// 至于 Wrapper 上的标题(BimEngine 标题),由于在最新的 BimEngine.ts 中
|
||||
// <20><><EFBFBD>们已经移除了 titleEl 和 descEl(在之前的重构中),
|
||||
// 所以现在 Wrapper 里主要是 Toolbar 和 Dialog,它们有自己的 panelBackground。
|
||||
// 只要 Toolbar/Dialog 内部正常即可。
|
||||
|
||||
textPrimary: '#ffffff',
|
||||
textSecondary: '#cccccc',
|
||||
|
||||
border: '#444444',
|
||||
|
||||
icon: '#cccccc',
|
||||
iconActive: '#ffffff',
|
||||
|
||||
componentBackground: 'transparent',
|
||||
componentHover: '#333333',
|
||||
componentActive: 'rgba(255, 255, 255, 0.1)'
|
||||
};
|
||||
|
||||
/**
|
||||
* 浅色主题
|
||||
*/
|
||||
export const lightTheme: ThemeConfig = {
|
||||
name: 'light',
|
||||
primary: '#0078d4',
|
||||
primaryHover: '#106ebe',
|
||||
|
||||
// 统一为浅灰
|
||||
background: '#f5f5f5',
|
||||
panelBackground: '#ffffff',
|
||||
|
||||
textPrimary: '#333333',
|
||||
textSecondary: '#666666',
|
||||
|
||||
border: '#e0e0e0',
|
||||
|
||||
icon: '#555555',
|
||||
iconActive: '#0078d4',
|
||||
|
||||
componentBackground: 'transparent',
|
||||
componentHover: '#f0f0f0',
|
||||
componentActive: '#e0e0e0'
|
||||
};
|
||||
43
src/themes/types.ts
Normal file
43
src/themes/types.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* 全局主题配置接口
|
||||
* 定义系统通用的语义化颜色
|
||||
*/
|
||||
export interface ThemeConfig {
|
||||
/** 主题名称 */
|
||||
name: string;
|
||||
|
||||
/** 品牌色/主色 */
|
||||
primary: string;
|
||||
/** 主色悬停/激活态 */
|
||||
primaryHover: string;
|
||||
|
||||
/** 基础背景色 (应用整体背景) */
|
||||
background: string;
|
||||
/** 面板背景色 (工具栏、弹窗背景) */
|
||||
panelBackground: string;
|
||||
|
||||
/** 主要文字颜色 */
|
||||
textPrimary: string;
|
||||
/** 次要文字颜色 */
|
||||
textSecondary: string;
|
||||
|
||||
/** 边框/分割线颜色 */
|
||||
border: string;
|
||||
|
||||
/** 图标默认颜色 */
|
||||
icon: string;
|
||||
/** 图标激活颜色 */
|
||||
iconActive: string;
|
||||
|
||||
/** 交互组件背景 (如按钮默认背景) */
|
||||
componentBackground: string;
|
||||
/** 交互组件悬停背景 */
|
||||
componentHover: string;
|
||||
/** 交互组件激活背景 */
|
||||
componentActive: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 主题类型定义
|
||||
*/
|
||||
export type ThemeType = 'dark' | 'light' | 'custom';
|
||||
@@ -1,179 +0,0 @@
|
||||
:root {
|
||||
--bim-toolbar-bg: rgba(17, 17, 17, 0.88);
|
||||
--bim-btn-bg: transparent;
|
||||
--bim-btn-hover-bg: #444;
|
||||
--bim-btn-active-bg: rgba(255, 255, 255, 0.15);
|
||||
--bim-icon-color: #ccc;
|
||||
--bim-icon-active-color: #fff;
|
||||
--bim-btn-text-color: #ccc;
|
||||
--bim-btn-text-active-color: #fff;
|
||||
}
|
||||
|
||||
/* 容器样式 */
|
||||
.toolbar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.toolbar-container::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 按钮组样式 */
|
||||
.opt-btn-group {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--bim-toolbar-bg);
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.has-divider {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
/* 按钮包装器 */
|
||||
.opt-btn-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 按钮样式 */
|
||||
.opt-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 50px;
|
||||
min-height: 50px;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
background-color: var(--bim-btn-bg);
|
||||
color: var(--bim-icon-color);
|
||||
transition: all 0.2s;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
|
||||
.opt-btn:hover {
|
||||
background-color: var(--bim-btn-hover-bg);
|
||||
color: var(--bim-icon-active-color);
|
||||
}
|
||||
|
||||
.opt-btn.active {
|
||||
background-color: var(--bim-btn-active-bg);
|
||||
color: var(--bim-icon-active-color);
|
||||
border-bottom: 2px solid #fff;
|
||||
}
|
||||
|
||||
.opt-btn.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.opt-btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.opt-btn-icon svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 文字标签样式 */
|
||||
.opt-btn-label {
|
||||
font-size: 10px;
|
||||
margin-top: 2px;
|
||||
color: var(--bim-btn-text-color);
|
||||
}
|
||||
.opt-btn:hover .opt-btn-label,
|
||||
.opt-btn.active .opt-btn-label {
|
||||
color: var(--bim-btn-text-active-color);
|
||||
}
|
||||
|
||||
|
||||
/* 下拉箭头样式 */
|
||||
.opt-btn-arrow {
|
||||
font-size: 8px;
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
opacity: 0.6;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.opt-btn-arrow.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.opt-btn.no-label .opt-btn-arrow {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
/* 下拉菜单样式 */
|
||||
.opt-btn-dropdown {
|
||||
position: fixed;
|
||||
transform: translate(-50%, -100%);
|
||||
background-color: var(--bim-toolbar-bg);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
min-width: 50px;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 下拉菜单项样式 */
|
||||
.opt-btn-dropdown-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bim-icon-color);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
white-space: nowrap;
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
padding: 4px;
|
||||
background-color: var(--bim-btn-bg);
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item:hover {
|
||||
background-color: var(--bim-btn-hover-bg);
|
||||
color: var(--bim-icon-active-color);
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item .opt-btn-icon.small {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item span {
|
||||
font-size: 10px;
|
||||
color: var(--bim-btn-text-color);
|
||||
}
|
||||
|
||||
.opt-btn-dropdown-item:hover span {
|
||||
color: var(--bim-btn-text-active-color);
|
||||
}
|
||||
|
||||
.opt-btn.no-label .opt-btn-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
@@ -1,536 +0,0 @@
|
||||
import './index.css';
|
||||
import type {
|
||||
OptButton,
|
||||
ButtonGroup,
|
||||
OptBtnGroupsOptions,
|
||||
ButtonConfig,
|
||||
ToolbarColors
|
||||
} from './index.type';
|
||||
|
||||
/**
|
||||
* 底部操作按钮组组件
|
||||
* 负责渲染和管理底部工具栏的按钮、下拉菜单及相关交互。
|
||||
*/
|
||||
export class OptBtnGroups {
|
||||
/** 挂载容器 */
|
||||
private container: HTMLElement;
|
||||
/** 组件配置选项 */
|
||||
private options: OptBtnGroupsOptions;
|
||||
/** 按钮组列表,按顺序存储 */
|
||||
private groups: ButtonGroup[] = [];
|
||||
/** 当前处于激活状态的按钮 ID 集合 */
|
||||
private activeBtnIds: Set<string> = new Set();
|
||||
/** 按钮 DOM 元素的引用映射,方便快速查找 */
|
||||
private btnRefs: Map<string, HTMLElement> = new Map();
|
||||
/** 当<><E5BD93>显示的下拉菜单元素 */
|
||||
private dropdownElement: HTMLElement | null = null;
|
||||
/** 鼠标悬停计时器,用于处理菜单显示的防抖 */
|
||||
private hoverTimeout: number | null = null;
|
||||
|
||||
/** 默认图标 SVG */
|
||||
private readonly DEFAULT_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect></svg>';
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param options 配置选项
|
||||
*/
|
||||
constructor(options: OptBtnGroupsOptions) {
|
||||
const el = typeof options.container === 'string'
|
||||
? document.getElementById(options.container)
|
||||
: options.container;
|
||||
|
||||
if (!el) throw new Error('Container not found');
|
||||
|
||||
this.container = el;
|
||||
this.options = {
|
||||
showLabel: true,
|
||||
visibility: {},
|
||||
...options
|
||||
};
|
||||
|
||||
this.initContainer();
|
||||
this.applyStyles(); // 应用初始样式配置
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化容器
|
||||
*/
|
||||
private initContainer(): void {
|
||||
this.container.innerHTML = '';
|
||||
this.container.classList.add('toolbar-root'); // 添加一个根类方便定位样式
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用样式配置到 CSS 变量
|
||||
*/
|
||||
private applyStyles(): void {
|
||||
const style = this.container.style;
|
||||
if (this.options.backgroundColor) style.setProperty('--bim-toolbar-bg', this.options.backgroundColor);
|
||||
if (this.options.btnBackgroundColor) style.setProperty('--bim-btn-bg', this.options.btnBackgroundColor);
|
||||
if (this.options.btnHoverColor) style.setProperty('--bim-btn-hover-bg', this.options.btnHoverColor);
|
||||
if (this.options.btnActiveColor) style.setProperty('--bim-btn-active-bg', this.options.btnActiveColor);
|
||||
if (this.options.iconColor) style.setProperty('--bim-icon-color', this.options.iconColor);
|
||||
if (this.options.iconActiveColor) style.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
|
||||
if (this.options.textColor) style.setProperty('--bim-btn-text-color', this.options.textColor);
|
||||
if (this.options.textActiveColor) style.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新颜色配置
|
||||
* @param colors 颜色配置对象
|
||||
*/
|
||||
public setColors(colors: ToolbarColors): void {
|
||||
// 更新 options
|
||||
this.options = { ...this.options, ...colors };
|
||||
// 应用到 CSS 变量
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加按钮组
|
||||
* @param groupId 组ID
|
||||
* @param beforeGroupId 在哪个组之前插入(可选<E58FAF><E98089><EFBFBD>,不传则插入到最后
|
||||
*/
|
||||
public addGroup(groupId: string, beforeGroupId?: string): void {
|
||||
if (this.groups.some(g => g.id === groupId)) {
|
||||
console.warn('Group ' + groupId + ' already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
const newGroup: ButtonGroup = { id: groupId, buttons: [] };
|
||||
|
||||
if (beforeGroupId) {
|
||||
const index = this.groups.findIndex(g => g.id === beforeGroupId);
|
||||
if (index !== -1) {
|
||||
this.groups.splice(index, 0, newGroup);
|
||||
} else {
|
||||
console.warn(`Target group ${beforeGroupId} not found, appending ${groupId} to end.`);
|
||||
this.groups.push(newGroup);
|
||||
}
|
||||
} else {
|
||||
this.groups.push(newGroup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加按钮到指定组
|
||||
* @param config 按钮配置(必须包含 groupId,可选包含 parentId)
|
||||
*/
|
||||
public addButton(config: ButtonConfig): void {
|
||||
const { groupId, parentId } = config;
|
||||
|
||||
if (!groupId) {
|
||||
throw new Error(`Button ${config.id} config must contain 'groupId'`);
|
||||
}
|
||||
|
||||
const group = this.groups.find(g => g.id === groupId);
|
||||
if (!group) {
|
||||
throw new Error(`Group ${groupId} not found. Please call addGroup first.`);
|
||||
}
|
||||
|
||||
const button: OptButton = {
|
||||
...config,
|
||||
children: config.children || []
|
||||
};
|
||||
|
||||
if (parentId) {
|
||||
// 添加为子按钮(菜单项)
|
||||
const parentBtn = this.findButton(group.buttons, parentId);
|
||||
if (!parentBtn) {
|
||||
throw new Error(`Parent button ${parentId} not found in group ${groupId}`);
|
||||
}
|
||||
if (!parentBtn.children) {
|
||||
parentBtn.children = [];
|
||||
}
|
||||
parentBtn.children.push(button);
|
||||
} else {
|
||||
// 添加为主按钮
|
||||
group.buttons.push(button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找按钮
|
||||
*/
|
||||
private findButton(buttons: OptButton[], id: string): OptButton | undefined {
|
||||
for (const btn of buttons) {
|
||||
if (btn.id === id) return btn;
|
||||
if (btn.children) {
|
||||
const found = this.findButton(btn.children, id);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化组件,加载默认按钮配置
|
||||
*/
|
||||
public async init(): Promise<void> {
|
||||
// 动态导入默认按钮配置
|
||||
const { homeButton } = await import('./buttons/home');
|
||||
const { locationButton } = await import('./buttons/location');
|
||||
const { walkMenuButton } = await import('./buttons/walk/walk-menu');
|
||||
const { walkPersonButton } = await import('./buttons/walk/walk-person');
|
||||
const { walkBirdButton } = await import('./buttons/walk/walk-bird');
|
||||
const { settingButton } = await import('./buttons/setting');
|
||||
const { infoButton } = await import('./buttons/info');
|
||||
|
||||
// 配置默认组和按钮
|
||||
this.addGroup('group-1');
|
||||
this.addButton(homeButton);
|
||||
this.addButton(walkMenuButton);
|
||||
this.addButton(walkPersonButton);
|
||||
this.addButton(walkBirdButton);
|
||||
this.addButton(locationButton);
|
||||
this.addGroup('group-2');
|
||||
this.addButton(settingButton);
|
||||
this.addButton(infoButton);
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染整个工具栏
|
||||
*/
|
||||
public render(): void {
|
||||
this.container.innerHTML = '';
|
||||
this.btnRefs.clear();
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'toolbar-container';
|
||||
|
||||
// 渲染所有组
|
||||
this.groups.forEach((group, index) => {
|
||||
const groupElement = this.renderGroup(group, index, this.groups.length);
|
||||
wrapper.appendChild(groupElement);
|
||||
});
|
||||
|
||||
this.container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单个按钮组
|
||||
*/
|
||||
private renderGroup(group: ButtonGroup, index: number, total: number): HTMLElement {
|
||||
const groupEl = document.createElement('div');
|
||||
groupEl.className = 'opt-btn-group';
|
||||
|
||||
if (index < total - 1) {
|
||||
groupEl.classList.add('has-divider');
|
||||
}
|
||||
|
||||
group.buttons.forEach(button => {
|
||||
if (this.isVisible(button.id)) {
|
||||
const btnWrapper = this.renderButton(button);
|
||||
groupEl.appendChild(btnWrapper);
|
||||
}
|
||||
});
|
||||
|
||||
return groupEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单个按钮
|
||||
*/
|
||||
private renderButton(button: OptButton): HTMLElement {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'opt-btn-wrapper';
|
||||
|
||||
const btnEl = document.createElement('div');
|
||||
btnEl.className = 'opt-btn';
|
||||
|
||||
// 设置激活状态
|
||||
if (this.activeBtnIds.has(button.id)) {
|
||||
btnEl.classList.add('active');
|
||||
}
|
||||
|
||||
if (button.disabled) {
|
||||
btnEl.classList.add('disabled');
|
||||
}
|
||||
|
||||
if (!this.options.showLabel) {
|
||||
btnEl.classList.add('no-label');
|
||||
if (button.label) {
|
||||
btnEl.title = button.label;
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染图标
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'opt-btn-icon';
|
||||
icon.innerHTML = this.getIcon(button.icon);
|
||||
btnEl.appendChild(icon);
|
||||
|
||||
// 渲染标签
|
||||
if (this.options.showLabel && button.label) {
|
||||
const label = document.createElement('span');
|
||||
label.className = 'opt-btn-label';
|
||||
label.textContent = button.label;
|
||||
btnEl.appendChild(label);
|
||||
}
|
||||
|
||||
// 如果有子菜单,渲染箭头
|
||||
if (button.children && button.children.length > 0) {
|
||||
const arrow = document.createElement('span');
|
||||
arrow.className = 'opt-btn-arrow';
|
||||
arrow.textContent = '▼';
|
||||
btnEl.appendChild(arrow);
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
btnEl.addEventListener('click', () => this.handleClick(button));
|
||||
btnEl.addEventListener('mouseenter', () => this.handleMouseEnter(button, btnEl));
|
||||
btnEl.addEventListener('mouseleave', () => this.handleMouseLeave());
|
||||
|
||||
this.btnRefs.set(button.id, btnEl);
|
||||
|
||||
wrapper.appendChild(btnEl);
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理按钮点击事件
|
||||
*/
|
||||
private handleClick(button: OptButton): void {
|
||||
if (button.disabled) return;
|
||||
|
||||
// 如果没有子菜单,直接触发
|
||||
if (!button.children || button.children.length === 0) {
|
||||
if (button.keepActive) {
|
||||
const wasActive = this.activeBtnIds.has(button.id);
|
||||
if (wasActive) {
|
||||
this.activeBtnIds.delete(button.id);
|
||||
} else {
|
||||
this.activeBtnIds.add(button.id);
|
||||
}
|
||||
this.updateButtonState(button.id);
|
||||
}
|
||||
|
||||
this.closeDropdown();
|
||||
|
||||
if (button.onClick) {
|
||||
button.onClick(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理子菜单项点击事件
|
||||
*/
|
||||
private handleSubClick(button: OptButton): void {
|
||||
if (button.keepActive) {
|
||||
const wasActive = this.activeBtnIds.has(button.id);
|
||||
if (wasActive) {
|
||||
this.activeBtnIds.delete(button.id);
|
||||
} else {
|
||||
this.activeBtnIds.add(button.id);
|
||||
}
|
||||
this.updateButtonState(button.id);
|
||||
}
|
||||
|
||||
this.closeDropdown();
|
||||
|
||||
if (button.onClick) {
|
||||
button.onClick(button);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标移入事件(显示菜单)
|
||||
*/
|
||||
private handleMouseEnter(button: OptButton, btnEl: HTMLElement): void {
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
this.hoverTimeout = null;
|
||||
}
|
||||
|
||||
if (button.children && button.children.length > 0) {
|
||||
this.showDropdown(button, btnEl);
|
||||
|
||||
const arrow = btnEl.querySelector('.opt-btn-arrow');
|
||||
if (arrow) {
|
||||
arrow.classList.add('rotated');
|
||||
}
|
||||
} else {
|
||||
this.closeDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标移出事件(隐藏菜单)
|
||||
*/
|
||||
private handleMouseLeave(): void {
|
||||
this.hoverTimeout = window.setTimeout(() => {
|
||||
this.closeDropdown();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示下拉菜单
|
||||
*/
|
||||
private showDropdown(button: OptButton, btnEl: HTMLElement): void {
|
||||
this.closeDropdown();
|
||||
|
||||
if (!button.children) return;
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'opt-btn-dropdown';
|
||||
|
||||
// 下拉菜单也应用当前的 CSS 变量样式,因为它们通常挂载在 body 上,所以需要单独设置或者确保能继承
|
||||
// 简单起见,我们可以直接将容器上的 CSS 变量复制过来,或者设置内联样式
|
||||
// 更好的是:如果我们在 this.container 上设置 CSS 变量,
|
||||
// 而 dropdown 挂载在 body 上,它无法继承。
|
||||
// 所以我们需要将 CSS 变量也应用到 dropdown 上。
|
||||
|
||||
const style = dropdown.style;
|
||||
if (this.options.backgroundColor) style.setProperty('--bim-toolbar-bg', this.options.backgroundColor);
|
||||
if (this.options.btnBackgroundColor) style.setProperty('--bim-btn-bg', this.options.btnBackgroundColor);
|
||||
if (this.options.btnHoverColor) style.setProperty('--bim-btn-hover-bg', this.options.btnHoverColor);
|
||||
if (this.options.btnActiveColor) style.setProperty('--bim-btn-active-bg', this.options.btnActiveColor);
|
||||
if (this.options.iconColor) style.setProperty('--bim-icon-color', this.options.iconColor);
|
||||
if (this.options.iconActiveColor) style.setProperty('--bim-icon-active-color', this.options.iconActiveColor);
|
||||
if (this.options.textColor) style.setProperty('--bim-btn-text-color', this.options.textColor);
|
||||
if (this.options.textActiveColor) style.setProperty('--bim-btn-text-active-color', this.options.textActiveColor);
|
||||
|
||||
const rect = btnEl.getBoundingClientRect();
|
||||
const centerX = rect.left + rect.width / 2;
|
||||
|
||||
dropdown.style.top = rect.top - 8 + 'px';
|
||||
dropdown.style.left = centerX + 'px';
|
||||
|
||||
button.children.forEach(subBtn => {
|
||||
if (this.isVisible(subBtn.id)) {
|
||||
const item = this.renderDropdownItem(subBtn);
|
||||
dropdown.appendChild(item);
|
||||
}
|
||||
});
|
||||
|
||||
// 保持菜单显<E58D95><E698BE><EFBFBD>
|
||||
dropdown.addEventListener('mouseenter', () => {
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
this.hoverTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
dropdown.addEventListener('mouseleave', () => this.handleMouseLeave());
|
||||
|
||||
document.body.appendChild(dropdown);
|
||||
this.dropdownElement = dropdown;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染下拉菜单项
|
||||
*/
|
||||
private renderDropdownItem(button: OptButton): HTMLElement {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'opt-btn-dropdown-item';
|
||||
|
||||
const icon = document.createElement('div');
|
||||
icon.className = 'opt-btn-icon small';
|
||||
icon.innerHTML = this.getIcon(button.icon);
|
||||
item.appendChild(icon);
|
||||
|
||||
if (this.options.showLabel) {
|
||||
const label = document.createElement('span');
|
||||
label.textContent = button.label;
|
||||
item.appendChild(label);
|
||||
}
|
||||
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.handleSubClick(button);
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭所有下拉菜单
|
||||
*/
|
||||
private closeDropdown(): void {
|
||||
if (this.dropdownElement) {
|
||||
this.dropdownElement.remove();
|
||||
this.dropdownElement = null;
|
||||
}
|
||||
|
||||
this.btnRefs.forEach(btnEl => {
|
||||
const arrow = btnEl.querySelector('.opt-btn-arrow');
|
||||
if (arrow) {
|
||||
arrow.classList.remove('rotated');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新按钮的激活状态样式
|
||||
*/
|
||||
private updateButtonState(buttonId: string): void {
|
||||
const btnEl = this.btnRefs.get(buttonId);
|
||||
if (btnEl) {
|
||||
if (this.activeBtnIds.has(buttonId)) {
|
||||
btnEl.classList.add('active');
|
||||
} else {
|
||||
btnEl.classList.remove('active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标 SVG 字符串
|
||||
*/
|
||||
private getIcon(icon?: string): string {
|
||||
return icon || this.DEFAULT_ICON;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新按钮可见性
|
||||
* @param buttonId 按钮ID
|
||||
* @param visible 是否可见
|
||||
*/
|
||||
public updateButtonVisibility(buttonId: string, visible: boolean): void {
|
||||
if (!this.options.visibility) {
|
||||
this.options.visibility = {};
|
||||
}
|
||||
this.options.visibility[buttonId] = visible;
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否显示标签
|
||||
* @param show 是否显示
|
||||
*/
|
||||
public setShowLabel(show: boolean): void {
|
||||
this.options.showLabel = show;
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置背景颜色 (兼容旧接口)
|
||||
* @param color CSS 颜色值
|
||||
*/
|
||||
public setBackgroundColor(color: string): void {
|
||||
this.setColors({ backgroundColor: color });
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查按钮是否可见
|
||||
*/
|
||||
private isVisible(id: string): boolean {
|
||||
return this.options.visibility?.[id] !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁组件,清理资源
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.closeDropdown();
|
||||
if (this.hoverTimeout) {
|
||||
clearTimeout(this.hoverTimeout);
|
||||
}
|
||||
this.container.innerHTML = '';
|
||||
this.btnRefs.clear();
|
||||
this.activeBtnIds.clear();
|
||||
this.groups = [];
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
export type ButtonType = 'button' | 'menu';
|
||||
|
||||
/**
|
||||
* 按钮配置接口(用于外部定义按钮)
|
||||
*/
|
||||
export interface ButtonConfig {
|
||||
/** 唯一标识 */
|
||||
id: string;
|
||||
/** 按钮类型:普通按钮或菜单按钮 */
|
||||
type: ButtonType;
|
||||
/** 按钮显示文字 */
|
||||
label: string;
|
||||
/** SVG 图标(内联 SVG 字符串) */
|
||||
icon?: string;
|
||||
/** 是否保持激活状态(默认 false) */
|
||||
keepActive?: boolean;
|
||||
/** 是否禁用 */
|
||||
disabled?: boolean;
|
||||
/** 点击回调函数 */
|
||||
onClick?: (button: OptButton) => void;
|
||||
/** 子按钮配置(可选,用于菜单按钮) */
|
||||
children?: ButtonConfig[];
|
||||
|
||||
/** 所属组ID */
|
||||
groupId?: string;
|
||||
/** 父按钮ID(如果是子按钮,则必填) */
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 操作按钮接口(内部使用,继承配置)
|
||||
*/
|
||||
export interface OptButton extends ButtonConfig {
|
||||
/** 内部使用的子按钮列表 */
|
||||
children?: OptButton[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按钮组接口
|
||||
*/
|
||||
export interface ButtonGroup {
|
||||
/** 组 ID */
|
||||
id: string;
|
||||
/** 组内按钮列表 */
|
||||
buttons: OptButton[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具栏颜色配置接口
|
||||
*/
|
||||
export interface ToolbarColors {
|
||||
/** 工具栏背景颜色 */
|
||||
backgroundColor?: string;
|
||||
/** 按钮默认背景颜色 */
|
||||
btnBackgroundColor?: string;
|
||||
/** 按钮 Hover 背景颜色 */
|
||||
btnHoverColor?: string;
|
||||
/** 按钮激活状态背景颜色 */
|
||||
btnActiveColor?: string;
|
||||
/** 图标默认颜色 */
|
||||
iconColor?: string;
|
||||
/** 图标激活/Hover 颜色 */
|
||||
iconActiveColor?: string;
|
||||
/** 文字默认颜色 */
|
||||
textColor?: string;
|
||||
/** 文字激活/Hover 颜色 */
|
||||
textActiveColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* OptBtnGroups 配置选项
|
||||
*/
|
||||
export interface OptBtnGroupsOptions extends ToolbarColors {
|
||||
/** 容器元素或 ID */
|
||||
container: HTMLElement | string;
|
||||
/** 是否显示标签 */
|
||||
showLabel?: boolean;
|
||||
/** 按钮可见性配置 Map */
|
||||
visibility?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击事件载荷
|
||||
*/
|
||||
export interface ClickPayload {
|
||||
/** 被点击的按钮对象 */
|
||||
button: OptButton;
|
||||
/** 触发的动作类型 */
|
||||
action: 'activate' | 'deactivate' | 'trigger';
|
||||
/** 当前激活状态 */
|
||||
isActive?: boolean;
|
||||
}
|
||||
32
src/types/component.ts
Normal file
32
src/types/component.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ThemeConfig } from '../themes/types';
|
||||
|
||||
/**
|
||||
* BIM 引擎组件通用接口
|
||||
* 所有受引擎管理的 UI 组件都必须实现此接口
|
||||
*/
|
||||
export interface IBimComponent {
|
||||
/**
|
||||
* 初始化组件
|
||||
* 用于创建 DOM、绑定事件、加载资源等
|
||||
* 支持同步或异步操作
|
||||
*/
|
||||
init(): void | Promise<void>;
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
* 组件应在此方法中将 ThemeConfig 映射为自身的 CSS 变量或样式
|
||||
*/
|
||||
setTheme(theme: ThemeConfig): void;
|
||||
|
||||
|
||||
/**
|
||||
* 设置语言
|
||||
*/
|
||||
setLocales(): void;
|
||||
|
||||
/**
|
||||
* 销毁组件
|
||||
* 清理 DOM 事件监听、定时器和引用
|
||||
*/
|
||||
destroy(): void;
|
||||
}
|
||||
Reference in New Issue
Block a user