refactor: reorganize project structure and implement self-managed i18n/theme for components

This commit is contained in:
yuding
2025-12-04 15:24:44 +08:00
parent 4dd923f19e
commit c45cdc9f7d
41 changed files with 3081 additions and 2010 deletions

View File

@@ -6,23 +6,163 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BIM Engine SDK Demo</title>
<script src="../dist/bim-engine-sdk.umd.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f5f5;
height: 100vh;
display: flex;
overflow: hidden;
/* 防止整个页面滚动 */
}
/* 左侧侧边栏:控制面板 */
.sidebar {
width: 320px;
background: white;
border-right: 1px solid #e0e0e0;
padding: 20px;
overflow-y: auto;
/* 内容过多时可滚动 */
display: flex;
flex-direction: column;
gap: 20px;
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05);
z-index: 10;
}
.sidebar h1 {
font-size: 1.4rem;
color: #333;
margin-bottom: 10px;
}
.control-group {
background: #f9f9f9;
padding: 15px;
border-radius: 8px;
border: 1px solid #eee;
}
.control-group h2 {
font-size: 1rem;
color: #555;
margin-bottom: 10px;
padding-bottom: 5px;
border-bottom: 2px solid #e0e0e0;
}
.btn-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
button {
padding: 6px 12px;
font-size: 0.9rem;
border: 1px solid #ddd;
background: #fff;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
flex: 1 1 auto;
/* 按钮自动填充 */
}
button:hover {
background: #f0f0f0;
border-color: #ccc;
}
button.primary {
background: #0078d4;
color: white;
border-color: #0063b1;
}
button.primary:hover {
background: #0063b1;
}
/* 右侧主内容区:引擎展示 */
.main-content {
flex: 1;
position: relative;
background: #eef2f5;
display: flex;
justify-content: center;
align-items: center;
}
#app {
width: 100%;
height: 100%;
background: #fff;
position: relative;
}
</style>
</head>
<body>
<div id="app" style="width: 100%; height: 500px; border: 1px dashed #ccc;"></div>
<!-- 左侧控制面板 -->
<aside class="sidebar">
<h1>BIM SDK Demo</h1>
<div style="margin-top: 20px; display: flex; gap: 10px; flex-wrap: wrap;">
<button onclick="toggleToolbar()">切换工具栏显隐</button>
<button onclick="toggleLabel()">切换文字标签</button>
<button onclick="addCustomGroup()">添加自定义组(最前)</button>
<button onclick="addCustomButton()">添加按钮到自定义组</button>
<button onclick="toggleLocationBtn()">切换"定位"按钮显隐</button>
<button onclick="changeToolbarColor()">修改工具栏颜色 (蓝色)</button>
<button onclick="changeToolbarStyleFull()">修改工具栏样式 (浅色主题)</button>
<button onclick="resetToolbarStyle()">重置工具栏样式</button>
<button onclick="openRedDialog()">打开红色弹窗 (样式定制)</button>
<button onclick="openCustomHeaderDialog()">打开自定义标题弹窗</button>
</div>
<!-- 1. 语言设置 -->
<div class="control-group">
<h2>🌍 语言 (Language)</h2>
<div class="btn-container">
<button class="primary" onclick="setLang('zh-CN')">中文</button>
<button class="primary" onclick="setLang('en-US')">English</button>
</div>
</div>
<!-- 2. 弹窗测试 -->
<div class="control-group">
<h2>🪟 弹窗 (Dialog)</h2>
<div class="btn-container">
<button onclick="openTestDialog()">测试弹窗</button>
<button onclick="openInfoDialog()">信息弹窗</button>
<button onclick="openRedDialog()">警告弹窗</button>
</div>
</div>
<!-- 3. 工具栏操作 -->
<div class="control-group">
<h2>🛠️ 工具栏 (Toolbar)</h2>
<div class="btn-container">
<button onclick="toggleToolbar()">显隐工具栏</button>
<button onclick="toggleLabel()">显隐标签</button>
<button onclick="toggleLocationBtn()">显隐定位按钮</button>
</div>
<div class="btn-container" style="margin-top: 8px;">
<button onclick="addCustomGroup()">加组</button>
<button onclick="addCustomButton()">加按钮</button>
</div>
</div>
<!-- 4. 样式主题 -->
<div class="control-group">
<h2>🎨 样式 (Theme)</h2>
<div class="btn-container">
<button onclick="setTheme('dark')">深色 (Dark)</button>
<button onclick="setTheme('light')">浅色 (Light)</button>
<button onclick="setCustomTheme()">自定义 (Red)</button>
</div>
</div>
</aside>
<!-- 右侧主区域 -->
<main class="main-content">
<div id="app"></div>
</main>
<script>
let engine = null;
@@ -31,13 +171,12 @@
let isLocationVisible = true;
let customGroupAdded = false;
// 等 SDK <20><>
// 初始化引擎
window.onload = () => {
if (window.LyzBimEngineSDK) {
const Engine = window.LyzBimEngineSDK.BimEngine;
try {
engine = new Engine(document.getElementById('app'));
engine = new Engine('app', { locale: 'zh-CN' });
console.log('Engine initialized:', engine);
} catch (err) {
console.error('Init failed:', err);
@@ -47,6 +186,45 @@
}
};
// --- 语言设置 ---
function setLang(lang) {
if (engine) engine.setLocale(lang);
}
// --- 弹窗测试 ---
function openTestDialog() {
if (!engine || !engine.dialog) return;
engine.dialog.create({
title: 'dialog.testTitle',
width: 350,
height: 200,
position: 'center',
draggable: true,
resizable: true
});
}
function openInfoDialog() {
if (!engine || !engine.dialog) return;
engine.dialog.showInfoDialog();
}
function openRedDialog() {
if (!engine || !engine.dialog) return;
engine.dialog.create({
title: 'Alert',
content: '<div style="color: #ffcccc;">Critical Warning!</div>',
width: 300,
height: 150,
backgroundColor: 'rgba(100, 0, 0, 0.95)',
headerBackgroundColor: '#cc0000',
titleColor: '#ffffff',
borderColor: '#ff6666',
position: { x: 50, y: 50 }
});
}
// --- 工具栏操作 ---
function toggleToolbar() {
if (!engine || !engine.toolbar) return;
isToolbarVisible = !isToolbarVisible;
@@ -59,110 +237,68 @@
engine.toolbar.setShowLabel(isLabelVisible);
}
function toggleLocationBtn() {
if (!engine || !engine.toolbar) return;
isLocationVisible = !isLocationVisible;
engine.toolbar.setButtonVisibility('location', isLocationVisible);
}
function addCustomGroup() {
if (!engine || !engine.toolbar) return;
if (customGroupAdded) {
alert('已添加过');
return;
}
// 添加到 group-1 之前
engine.toolbar.addGroup('custom-group', 'group-1');
customGroupAdded = true;
console.log('Added custom group');
}
function addCustomButton() {
if (!engine || !engine.toolbar) return;
if (!customGroupAdded) {
alert('请先添加自定义组');
alert('Please add custom group first / 请先添加自定义组');
return;
}
const btnId = 'custom-btn-' + Date.now();
engine.toolbar.addButton({
id: btnId,
groupId: 'custom-group',
type: 'button',
label: '新按钮',
// 一个简单的笑脸 SVG
label: 'New',
icon: '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><path d="M8 14s1.5 2 4 2 4-2 4-2"></path><line x1="9" y1="9" x2="9.01" y2="9"></line><line x1="15" y1="9" x2="15.01" y2="9"></line></svg>',
onClick: (btn) => {
alert('你点击了动态添加的按钮: ' + btn.label);
alert('Clicked: ' + btn.label);
}
});
}
function toggleLocationBtn() {
if (!engine || !engine.toolbar) return;
isLocationVisible = !isLocationVisible;
// location 按钮的 ID 是 'location'
engine.toolbar.setButtonVisibility('location', isLocationVisible);
// --- 主题操作 ---
function setTheme(themeName) {
if (engine) engine.setTheme(themeName);
}
function changeToolbarColor() {
if (!engine || !engine.toolbar) return;
// 仅修改背景色
engine.toolbar.setBackgroundColor('rgba(0, 100, 200, 0.9)');
}
function setCustomTheme() {
if (!engine) return;
// 定义一个红色主题
engine.setCustomTheme({
name: 'red-alert',
primary: '#d32f2f',
primaryHover: '#b71c1c',
function changeToolbarStyleFull() {
if (!engine || !engine.toolbar) return;
// 完整的浅色主题配置
engine.toolbar.setColors({
backgroundColor: '#f0f0f0',
btnBackgroundColor: '#ffffff',
btnHoverColor: '#e0e0e0',
btnActiveColor: '#d0d0d0',
iconColor: '#333333',
iconActiveColor: '#0078d4',
textColor: '#666666',
textActiveColor: '#000000'
});
}
background: '#ffebee', // 浅红背景
panelBackground: 'rgba(255, 255, 255, 0.9)',
function resetToolbarStyle() {
if (!engine || !engine.toolbar) return;
// 重置为默认深色主题
engine.toolbar.setColors({
backgroundColor: 'rgba(17, 17, 17, 0.88)',
btnBackgroundColor: 'transparent',
btnHoverColor: '#444',
btnActiveColor: 'rgba(255, 255, 255, 0.15)',
iconColor: '#ccc',
iconActiveColor: '#fff',
textColor: '#ccc',
textActiveColor: '#fff'
});
}
textPrimary: '#b71c1c',
textSecondary: '#e57373',
function openRedDialog() {
if (!engine || !engine.dialog) return;
engine.dialog.create({
title: '警报',
content: '<div style="color: #ffcccc;">这是一个高度定制的警告弹窗。<br>注意查看标题栏和边框颜色。</div>',
width: 300,
height: 150,
backgroundColor: 'rgba(100, 0, 0, 0.95)',
headerBackgroundColor: '#cc0000',
titleColor: '#ffffff',
borderColor: '#ff6666',
position: { x: 50, y: 50 } // 自定义坐标
});
}
border: '#ffcdd2',
function openCustomHeaderDialog() {
if (!engine || !engine.dialog) return;
engine.dialog.create({
title: '自定义样式弹窗',
content: '观察标题栏背景和文字颜色。',
width: 300,
headerBackgroundColor: '#0078d4', // 蓝色标题栏
titleColor: '#ffffff',
backgroundColor: '#ffffff', // 白色内容背景
textColor: '#333333', // 深色文字
borderColor: '#0078d4', // 蓝色边框
position: 'center'
icon: '#d32f2f',
iconActive: '#b71c1c',
componentBackground: 'rgba(255, 205, 210, 0.3)',
componentHover: 'rgba(255, 205, 210, 0.8)',
componentActive: '#e57373'
});
}
</script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

558
dist/index.d.ts vendored
View File

@@ -1,19 +1,89 @@
/**
* 通用按钮组组件 (BimButtonGroup)
*/
export declare class BimButtonGroup implements IBimComponent {
private container;
private options;
private groups;
private activeBtnIds;
private btnRefs;
private dropdownElement;
private hoverTimeout;
private customColors;
private unsubscribeLocale;
private unsubscribeTheme;
private readonly DEFAULT_ICON;
constructor(options: ButtonGroupOptions);
private initContainer;
private updatePosition;
/**
* 应用样式到容器
*/
private applyStyles;
/**
* 设置主题颜色
* 只会应用到没有被用户自定义的颜色属性上
*/
setTheme(theme: ThemeConfig): void;
/**
* 直接设置颜色(强制覆盖)
* 设置的颜色会被标记为自定义,后续的 setTheme 不会覆盖它们
*/
setColors(colors: ButtonGroupColors): void;
init(): Promise<void>;
setLocales(): void;
addGroup(groupId: string, beforeGroupId?: string): void;
addButton(config: ButtonConfig): void;
private findButton;
render(): void;
private renderGroup;
private renderButton;
private handleClick;
private handleMouseEnter;
private handleMouseLeave;
private showDropdown;
private renderDropdownItem;
private closeDropdown;
private updateButtonState;
private getIcon;
updateButtonVisibility(id: string, visible: boolean): void;
setShowLabel(show: boolean): void;
private updateLabelsVisibility;
private findButtonById;
setBackgroundColor(color: string): void;
private isVisible;
destroy(): void;
}
/**
* 通用弹窗组件类
* 支持拖拽、缩放、自定义内容和位置。
*/
declare class BimDialog {
declare class BimDialog implements IBimComponent {
private element;
private options;
private container;
private header;
private contentArea;
private _isDestroyed;
private _isInitialized;
private unsubscribeTheme;
private unsubscribeLocale;
/**
* 构造函数
* @param options 弹窗配置选项
*/
constructor(options: DialogOptions);
/**
* 设置主题
* @param theme 全局主题配置
*/
setTheme(theme: ThemeConfig): void;
/**
* 初始化组件功能 (接口实现)
*/
init(): void;
setLocales(): void;
/**
* 创建弹窗的 DOM 结构
*/
@@ -22,10 +92,6 @@ declare class BimDialog {
* 设置元素尺寸
*/
private setSize;
/**
* 初始化组件功能
*/
private init;
/**
* 初始化弹窗位置
*/
@@ -47,85 +113,111 @@ declare class BimDialog {
* 关闭弹窗并销毁
*/
close(): void;
}
/**
* BimEngine 主类
* 负责初始化整个应用界面,协调各个子模块(如工具栏、弹窗等)。
*/
export declare class BimEngine {
/** 主容器元素 */
private container;
/** 内部包装器元素,用于承载所有 UI 组件 */
private wrapper;
/** 工具栏管理器实例 */
toolbar: ToolbarManager | null;
/** 弹窗管理器实例 */
dialog: DialogManager | null;
/**
* 构造函数
* @param container 容器元素或容器 ID
*/
constructor(container: HTMLElement | string);
/**
* 初始化方法
* 创建 DOM 结构并初始化各子模块
*/
private init;
/**
* 销毁实例
* 清理所有资源和 DOM 元素
* 销毁组件 (接口实现)
*/
destroy(): void;
}
/**
* 按钮配置接口(用于外部定义按钮)
*/
export declare class BimEngine {
private container;
private wrapper;
private topLeftGroup;
toolbar: ToolbarManager | null;
buttonGroup: ButtonGroupManager | null;
dialog: DialogManager | null;
get localeManager(): LocaleManager;
get themeManager(): ThemeManager;
constructor(container: HTMLElement | string, options?: {
locale?: LocaleType;
theme?: ThemeType;
});
setLocale(locale: LocaleType): void;
getLocale(): LocaleType;
setTheme(theme: 'dark' | 'light'): void;
setCustomTheme(theme: ThemeConfig): void;
private init;
private createTopLeftGroup;
private updateTheme;
destroy(): void;
}
/** 按钮内部文字图标排列 */
declare type ButtonAlign = 'vertical' | 'horizontal';
/** 按钮配置 */
declare 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;
/** 按钮内部图标文字排列 (默认 vertical即图标在上) */
align?: ButtonAlign;
/** 图标大小 (正方形,单位 px默认 32) */
iconSize?: number;
/** 按钮最小宽度 (单位 px默认 50) */
minWidth?: number;
}
export declare interface ButtonGroup {
id: string;
buttons: OptButton[];
}
declare interface ButtonGroupColors {
backgroundColor?: string;
btnBackgroundColor?: string;
btnHoverColor?: string;
btnActiveColor?: string;
iconColor?: string;
iconActiveColor?: string;
textColor?: string;
textActiveColor?: string;
}
/**
* 按钮组接口
* 通用按钮组管理器
* 负责创建和管理除底部工具栏以外的其他按钮组。
*/
export declare interface ButtonGroup {
/** 组 ID */
id: string;
/** 组内按钮列表 */
buttons: OptButton[];
declare class ButtonGroupManager {
private activeGroups;
private container;
constructor(container: HTMLElement);
/**
* 创建一个新的按钮组
*/
create(options: Omit<ButtonGroupOptions, 'container'>): BimButtonGroup;
updateTheme(theme: ThemeConfig): void;
refresh(): void;
destroy(): void;
}
export declare 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;
}
declare type ButtonType = 'button' | 'menu';
/**
* 点击事件载荷
*/
export declare interface ClickPayload {
/** 被点击的按钮对象 */
button: OptButton;
/** 触发的动作类型 */
action: 'activate' | 'deactivate' | 'trigger';
/** 当前激活状态 */
isActive?: boolean;
}
@@ -152,6 +244,8 @@ declare interface DialogColors {
declare class DialogManager {
/** 弹窗挂载的父容器 */
private container;
/** 活跃的弹窗实例列表 */
private activeDialogs;
/**
* 构造函数
* @param container 弹窗挂载的目标容器
@@ -168,6 +262,11 @@ declare class DialogManager {
* 演示如何调用特定的业务弹窗组件
*/
showInfoDialog(): void;
/**
* 响应全局主题变更
* @param theme 全局主题配置
*/
updateTheme(theme: ThemeConfig): void;
}
/**
@@ -196,6 +295,8 @@ declare interface DialogOptions extends DialogColors {
minHeight?: number;
/** 关闭时的回调函数 */
onClose?: () => void;
/** 打开时的回调函数 */
onOpen?: () => void;
/** 弹窗唯一标识 ID (可选) */
id?: string;
}
@@ -210,239 +311,186 @@ declare type DialogPosition = 'center' | 'top-left' | 'top-center' | 'top-right'
y: number;
};
/** 二级菜单展开方向 */
declare type ExpandDirection = 'up' | 'down' | 'left' | 'right';
/** 按钮组排列方向 (Flex-direction) */
declare type GroupDirection = 'row' | 'column';
/** 弹窗/按钮组位置 */
declare type GroupPosition = 'center' | 'top-left' | 'top-center' | 'top-right' | 'left-center' | 'right-center' | 'bottom-left' | 'bottom-center' | 'bottom-right' | {
x: number;
y: number;
} | 'static';
/**
* 底部操作按钮组组件
* 负责渲染和管理底部工具栏的按钮、下拉菜单及相关交互。
* BIM 引擎组件通用接口
* 所有受引擎管理的 UI 组件都必须实现此接口
*/
export declare class OptBtnGroups {
/** 挂载容器 */
private container;
/** 组件配置选项 */
private options;
/** 按钮组列表,按顺序存储 */
private groups;
/** 当前处于激活状态的按钮 ID 集合 */
private activeBtnIds;
/** 按钮 DOM 元素的引用映射,方便快速查找 */
private btnRefs;
/** 当<><E5BD93>显示的下拉菜单元素 */
private dropdownElement;
/** 鼠标悬停计时器,用于处理菜单显示的防抖 */
private hoverTimeout;
/** 默认图标 SVG */
private readonly DEFAULT_ICON;
declare interface IBimComponent {
/**
* 构造函数
* @param options 配置选项
* 初始化组件
* 用于创建 DOM、绑定事件、加载资源等
* 支持同步或异步操作
*/
constructor(options: OptBtnGroupsOptions);
init(): void | Promise<void>;
/**
* 初始化容器
* 设置主题
* 组件应在此方法中将 ThemeConfig 映射为自身的 CSS 变量或样式
*/
private initContainer;
setTheme(theme: ThemeConfig): void;
/**
* 应用样式配置到 CSS 变量
* 设置语言
*/
private applyStyles;
setLocales(): void;
/**
* 更新颜色配置
* @param colors 颜色配置对象
*/
setColors(colors: ToolbarColors): void;
/**
* 添加按钮组
* @param groupId 组ID
* @param beforeGroupId 在哪个组之前插入可选<E58FAF><E98089><EFBFBD>不传则插入到最后
*/
addGroup(groupId: string, beforeGroupId?: string): void;
/**
* 添加按钮到指定组
* @param config 按钮配置(必须包含 groupId可选包含 parentId
*/
addButton(config: ButtonConfig): void;
/**
* 递归查找按钮
*/
private findButton;
/**
* 初始化组件,加载默认按钮配置
*/
init(): Promise<void>;
/**
* 渲染整个工具栏
*/
render(): void;
/**
* 渲染单个按钮组
*/
private renderGroup;
/**
* 渲染单个按钮
*/
private renderButton;
/**
* 处理按钮点击事件
*/
private handleClick;
/**
* 处理子菜单项点击事件
*/
private handleSubClick;
/**
* 处理鼠标移入事件(显示菜单)
*/
private handleMouseEnter;
/**
* 处理鼠标移出事件(隐藏菜单)
*/
private handleMouseLeave;
/**
* 显示下拉菜单
*/
private showDropdown;
/**
* 渲染下拉菜单项
*/
private renderDropdownItem;
/**
* 关闭所有下拉菜单
*/
private closeDropdown;
/**
* 更新按钮的激活状态样式
*/
private updateButtonState;
/**
* 获取图标 SVG 字符串
*/
private getIcon;
/**
* 更新按钮可见性
* @param buttonId 按钮ID
* @param visible 是否可见
*/
updateButtonVisibility(buttonId: string, visible: boolean): void;
/**
* 设置是否显示标签
* @param show 是否显示
*/
setShowLabel(show: boolean): void;
/**
* 设置背景颜色 (兼容旧接口)
* @param color CSS 颜色值
*/
setBackgroundColor(color: string): void;
/**
* 检查按钮是否可见
*/
private isVisible;
/**
* 销毁组件,清理资源
* 销毁组件
* 清理 DOM 事件监听、定时器和引用
*/
destroy(): void;
}
declare type LocaleChangeListener = (locale: LocaleType) => void;
/**
* OptBtnGroups 配置选项
* 语言管理器类
*/
export declare interface OptBtnGroupsOptions extends ToolbarColors {
/** 容器元素或 ID */
container: HTMLElement | string;
/** 是否显示标签 */
showLabel?: boolean;
/** 按钮可见性配置 Map */
visibility?: Record<string, boolean>;
declare class LocaleManager {
private currentLocale;
private messages;
private listeners;
constructor();
/**
* 获取当前语言
*/
getLocale(): LocaleType;
/**
* 切换语言
*/
setLocale(locale: LocaleType): void;
/**
* 翻译核心方法
*/
t(key: string): string;
/**
* 订阅变更
*/
subscribe(listener: LocaleChangeListener): () => void;
private notifyListeners;
}
/**
* 操作按钮接口(内部使用,继承配置)
* 语言代码类型
*/
declare type LocaleType = 'zh-CN' | 'en-US';
export declare interface OptButton extends ButtonConfig {
/** 内部使用的子按钮列表 */
children?: OptButton[];
}
declare type ThemeChangeListener = (theme: ThemeConfig) => void;
/**
* 工具栏颜色配置接口
* 全局主题配置接口
* 定义系统通用的语义化颜色
*/
declare interface ToolbarColors {
/** 工具栏背景颜色 */
backgroundColor?: string;
/** 按钮默认背景颜色 */
btnBackgroundColor?: string;
/** 按钮 Hover 背景颜色 */
btnHoverColor?: string;
/** 按钮激活状态背景颜色 */
btnActiveColor?: string;
declare interface ThemeConfig {
/** 主题名称 */
name: string;
/** 品牌色/主色 */
primary: string;
/** 主色悬停/激活态 */
primaryHover: string;
/** 基础背景色 (应用整体背景) */
background: string;
/** 面板背景色 (工具栏、弹窗背景) */
panelBackground: string;
/** 主要文字颜色 */
textPrimary: string;
/** 次要文字颜色 */
textSecondary: string;
/** 边框/分割线颜色 */
border: string;
/** 图标默认颜色 */
iconColor?: string;
/** 图标激活/Hover 颜色 */
iconActiveColor?: string;
/** 文字默认颜色 */
textColor?: string;
/** 文字激活/Hover 颜色 */
textActiveColor?: string;
icon: string;
/** 图标激活颜色 */
iconActive: string;
/** 交互组件背景 (如按钮默认背景) */
componentBackground: string;
/** 交互组件悬停背景 */
componentHover: string;
/** 交互组件激活背景 */
componentActive: string;
}
/**
* 工具栏管理器
* 负责管理底部操作栏的按钮组、按钮及其可见性等状态。
* 主题管理器 (单例)
*/
declare class ThemeManager {
private currentTheme;
private listeners;
constructor();
/**
* 获取当前主题配置
*/
getTheme(): ThemeConfig;
/**
* 切换预设主题
* @param themeName 'dark' | 'light'
*/
setTheme(themeName: 'dark' | 'light'): void;
/**
* 应用自定义主题配置
* @param theme 配置对象
*/
setCustomTheme(theme: ThemeConfig): void;
/**
* 内部应用主题逻辑
*/
private applyTheme;
/**
* 订阅主题变更
*/
subscribe(listener: ThemeChangeListener): () => void;
private notifyListeners;
}
/**
* 主题类型定义
*/
declare type ThemeType = 'dark' | 'light' | 'custom';
/**
* 底部工具栏 (Toolbar)
* BimButtonGroup 的子类,专门用于加载工具栏默认按钮。
*/
export declare class Toolbar extends BimButtonGroup {
/**
* 重写初始化,加载默认按钮
*/
init(): Promise<void>;
}
/**
* 底部工具栏管理器 (ToolbarManager)
* 仅负责管理底部工具栏实例。
*/
declare class ToolbarManager {
/** 内部工具栏组件实例 */
private optBtnGroups;
/** 工具栏挂载的容器 */
private toolbar;
private toolbarContainer;
private container;
/**
* 构造函数
* @param container 工具栏挂载的容器元素
*/
constructor(container: HTMLElement);
/**
* 初始化工具栏
*/
private init;
/**
* 添加一个工具栏按钮组
* @param groupId 新组的 ID
* @param beforeGroupId (可选) 插入到哪个组之前,不传则追加到最后
*/
addGroup(groupId: string, beforeGroupId?: string): void;
/**
* 添加一个工具栏按钮
* @param config 按钮配置对象
*/
addButton(config: ButtonConfig): void;
/**
* 设置按钮的可见性
* @param buttonId 按钮 ID
* @param visible 是否可见
*/
setButtonVisibility(buttonId: string, visible: boolean): void;
/**
* 设置是否显示按钮下方的文字标签
* @param show 是否显示
*/
setShowLabel(show: boolean): void;
/**
* 设置整个工具栏的可见性
* @param visible 是否可见
*/
setVisible(visible: boolean): void;
/**
* 设置工具栏背景颜色
* @param color CSS 颜色值
*/
setBackgroundColor(color: string): void;
/**
* 设置工具栏详细颜色配置
* @param colors 颜色配置对象
*/
setColors(colors: ToolbarColors): void;
/**
* 销毁工具栏管理器
*/
updateTheme(theme: ThemeConfig): void;
refresh(): void;
destroy(): void;
addGroup(groupId: string, beforeGroupId?: string): void;
addButton(config: ButtonConfig): void;
setButtonVisibility(id: string, v: boolean): void;
setShowLabel(show: boolean): void;
setVisible(visible: boolean): void;
setBackgroundColor(color: string): void;
setColors(colors: ButtonGroupColors): void;
}
export { }

300
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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 = '';
}
}
}

View 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;
}

View 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();
}
}

View 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;
}

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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) => {

View File

@@ -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);

View File

@@ -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) => {

View File

@@ -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);

View 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();
}
}

View File

@@ -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 继承
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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
View 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
View 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
View 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>',
},
};

View 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 = [];
}
}

View 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);
}
});
}
}

View 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); }
}

View File

@@ -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);
}
}

View File

@@ -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
View 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
View 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
View 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
View 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';

View File

@@ -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;
}

View File

@@ -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 = [];
}
}

View File

@@ -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
View 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;
}