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

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

@@ -0,0 +1,16 @@
import type { ButtonConfig } from '../../../index.type';
/**
* 首页按钮配置
*/
export const homeButton: ButtonConfig = {
id: 'home',
groupId: 'group-1',
type: 'button',
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) => {
console.log('首页按钮被点击:', button.id);
}
};

View File

@@ -0,0 +1,16 @@
import type { ButtonConfig } from '../../../index.type';
/**
* 定位按钮配置
*/
export const infoButton: ButtonConfig = {
id: 'info',
groupId: 'group-2',
type: 'button',
label: 'toolbar.info',
icon: '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12 7q.425 0 .713-.288T13 6t-.288-.712T12 5t-.712.288T11 6t.288.713T12 7m0 8q.425 0 .713-.288T13 14v-4q0-.425-.288-.712T12 9t-.712.288T11 10v4q0 .425.288.713T12 15m-6 3l-2.3 2.3q-.475.475-1.088.213T2 19.575V4q0-.825.588-1.412T4 2h16q.825 0 1.413.588T22 4v12q0 .825-.587 1.413T20 18z"/></svg>',
keepActive: false,
onClick: (button) => {
console.log('信息按钮被点击:', button.id);
}
};

View File

@@ -0,0 +1,16 @@
import type { ButtonConfig } from '../../../index.type';
/**
* 定位按钮配置
*/
export const locationButton: ButtonConfig = {
id: 'location',
groupId: 'group-1',
type: 'button',
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) => {
console.log('定位按钮被点击:', button.id);
}
};

View File

@@ -0,0 +1,16 @@
import type { ButtonConfig } from '../../../index.type';
/**
* 定位按钮配置
*/
export const settingButton: ButtonConfig = {
id: 'setting',
groupId: 'group-2',
type: 'button',
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) => {
console.log('设置按钮被点击:', button.id);
}
};

View File

@@ -0,0 +1,14 @@
import type { ButtonConfig } from '../../../../index.type';
export const walkBirdButton: ButtonConfig = {
id: 'walk-bird',
groupId: 'group-1',
parentId: 'walk',
align: 'vertical',
type: 'button',
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

@@ -0,0 +1,17 @@
import type { ButtonConfig } from '../../../../index.type';
/**
* 漫游菜单按钮配置
*/
export const walkMenuButton: ButtonConfig = {
id: 'walk',
groupId: 'group-1',
type: 'menu',
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) => {
console.log('漫游按钮被点击:', button.id);
}
};

View File

@@ -0,0 +1,14 @@
import type { ButtonConfig } from '../../../../index.type';
export const walkPersonButton: ButtonConfig = {
id: 'walk-person',
groupId: 'group-1',
parentId: 'walk',
type: 'button',
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

@@ -0,0 +1,30 @@
.bim-info-dialog-content {
padding: 16px;
font-family: sans-serif;
color: #333;
}
.bim-info-dialog-content h3 {
margin-top: 0;
margin-bottom: 12px;
border-bottom: 1px solid #eee;
padding-bottom: 8px;
color: #0078d4;
}
.bim-info-dialog-content ul {
list-style: none;
padding: 0;
margin: 0;
}
.bim-info-dialog-content li {
margin-bottom: 8px;
font-size: 14px;
display: flex;
}
.bim-info-dialog-content li strong {
width: 80px;
color: #555;
}

View File

@@ -0,0 +1,65 @@
import './index.css';
import { BimDialog } from '../index';
/**
* BimInfoDialog (继承版)
* 这是一个展示项目信息的业务弹窗组件,直接继承自 BimDialog。
*/
export class BimInfoDialog extends BimDialog {
/**
* 构造函数
* @param container 父容器
*/
constructor(container: HTMLElement) {
// 1. 准备内容 DOM
const contentEl = document.createElement('div');
contentEl.className = 'bim-info-dialog-content';
const infoTitle = document.createElement('h3');
infoTitle.textContent = 'Model Information';
const infoList = document.createElement('ul');
infoList.innerHTML = `
<li><strong>Name:</strong> Sample Project</li>
<li><strong>Version:</strong> 1.0.0</li>
<li><strong>Date:</strong> ${new Date().toLocaleDateString()}</li>
<li><strong>Status:</strong> <span style="color: green;">Active</span></li>
`;
const actionBtn = document.createElement('button');
actionBtn.textContent = 'Update Status';
actionBtn.style.marginTop = '10px';
actionBtn.onclick = () => {
alert('Status updated!');
};
contentEl.appendChild(infoTitle);
contentEl.appendChild(infoList);
contentEl.appendChild(actionBtn);
// 2. 调用父类构造函数,传入特定的配置
super({
container: container,
title: 'dialog.testTitle',
content: contentEl,
width: 320,
height: 'auto',
position: 'center',
resizable: 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');
}
// 不需要再手动实现 setTheme, destroy, close, init
// 它们都已从 BimDialog 继承
}

View File

@@ -0,0 +1,95 @@
:root {
--bim-dialog-bg: rgba(17, 17, 17, 0.95);
--bim-dialog-header-bg: #2a2a2a;
--bim-dialog-title-color: #fff;
--bim-dialog-text-color: #ccc;
--bim-dialog-border-color: #444;
}
.bim-dialog {
position: absolute;
background-color: var(--bim-dialog-bg);
border: 1px solid var(--bim-dialog-border-color);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column;
z-index: 1000;
color: var(--bim-dialog-title-color);
overflow: hidden;
min-width: 200px;
min-height: 100px;
}
.bim-dialog-header {
height: 32px;
background-color: var(--bim-dialog-header-bg);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
cursor: default;
user-select: none;
border-bottom: 1px solid var(--bim-dialog-border-color);
flex-shrink: 0;
}
.bim-dialog-header.draggable {
cursor: move;
}
.bim-dialog-title {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--bim-dialog-title-color);
}
.bim-dialog-close {
cursor: pointer;
font-size: 18px;
color: #999;
line-height: 1;
margin-left: 8px;
}
.bim-dialog-close:hover {
color: #fff;
}
.bim-dialog-content {
flex: 1;
padding: 10px;
overflow: auto;
font-size: 14px;
color: var(--bim-dialog-text-color);
}
/* 缩放句柄 */
.bim-dialog-resize-handle {
position: absolute;
width: 10px;
height: 10px;
bottom: 0;
right: 0;
cursor: se-resize;
z-index: 10;
}
/* 右下角装饰,类似斜线 */
.bim-dialog-resize-handle::after {
content: '';
position: absolute;
bottom: 3px;
right: 3px;
width: 6px;
height: 6px;
border-right: 2px solid #666;
border-bottom: 2px solid #666;
}
.bim-dialog-resize-handle:hover::after {
border-color: #fff;
}

View File

@@ -0,0 +1,357 @@
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 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;
/**
* 构造函数
* @param options 弹窗配置选项
*/
constructor(options: DialogOptions) {
// 合并默认配置
this.options = {
title: 'Dialog',
width: 300,
height: 'auto',
position: 'center',
draggable: true,
resizable: false,
minWidth: 200,
minHeight: 100,
...options
};
this.container = options.container;
// 创建 DOM 结构
this.element = this.createDom();
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 结构
*/
private createDom(): HTMLElement {
const el = document.createElement('div');
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);
if (this.options.headerBackgroundColor) style.setProperty('--bim-dialog-header-bg', this.options.headerBackgroundColor);
if (this.options.titleColor) style.setProperty('--bim-dialog-title-color', this.options.titleColor);
if (this.options.textColor) style.setProperty('--bim-dialog-text-color', this.options.textColor);
if (this.options.borderColor) style.setProperty('--bim-dialog-border-color', this.options.borderColor);
// 设置初始尺寸
this.setSize(el, this.options.width, this.options.height);
// 创建标题栏 (Header)
const header = document.createElement('div');
header.className = 'bim-dialog-header';
if (this.options.draggable) header.classList.add('draggable');
const title = document.createElement('span');
title.className = 'bim-dialog-title';
title.textContent = this.options.title ? t(this.options.title) : '';
const closeBtn = document.createElement('span');
closeBtn.className = 'bim-dialog-close';
closeBtn.innerHTML = '&times;';
closeBtn.onclick = () => this.close();
header.appendChild(title);
header.appendChild(closeBtn);
// 创建内容区域 (Content)
const content = document.createElement('div');
content.className = 'bim-dialog-content';
if (typeof this.options.content === 'string') {
content.innerHTML = this.options.content;
} else if (this.options.content instanceof HTMLElement) {
content.appendChild(this.options.content);
}
el.appendChild(header);
el.appendChild(content);
// 如果允许缩放,创建缩放手柄
if (this.options.resizable) {
const resizeHandle = document.createElement('div');
resizeHandle.className = 'bim-dialog-resize-handle';
el.appendChild(resizeHandle);
}
return el;
}
/**
* 设置元素尺寸
*/
private setSize(el: HTMLElement, width?: number | string, height?: number | string) {
if (width !== undefined) {
el.style.width = typeof width === 'number' ? `${width}px` : width;
}
if (height !== undefined) {
el.style.height = typeof height === 'number' ? `${height}px` : height;
}
}
/**
* 初始化弹窗位置
*/
private initPosition() {
const pos = this.options.position;
const elRect = this.element.getBoundingClientRect();
// 计算相对父容器的定位
let left = 0;
let top = 0;
const pW = this.container.clientWidth;
const pH = this.container.clientHeight;
const elW = elRect.width;
const elH = elRect.height;
if (typeof pos === 'object' && 'x' in pos) {
left = pos.x;
top = pos.y;
} else {
switch (pos) {
case 'center':
left = (pW - elW) / 2;
top = (pH - elH) / 2;
break;
case 'top-left': left = 0; top = 0; break;
case 'top-center': left = (pW - elW) / 2; top = 0; break;
case 'top-right': left = pW - elW; top = 0; break;
case 'left-center': left = 0; top = (pH - elH) / 2; break;
case 'right-center': left = pW - elW; top = (pH - elH) / 2; break;
case 'bottom-left': left = 0; top = pH - elH; break;
case 'bottom-center': left = (pW - elW) / 2; top = pH - elH; break;
case 'bottom-right': left = pW - elW; top = pH - elH; break;
default:
left = (pW - elW) / 2;
top = (pH - elH) / 2;
}
}
// 简单的边界检查,防止初始位置溢出
left = Math.max(0, Math.min(left, pW - elW));
top = Math.max(0, Math.min(top, pH - elH));
this.element.style.left = `${left}px`;
this.element.style.top = `${top}px`;
}
/**
* 初始化拖拽功能
*/
private initDrag() {
let startX = 0;
let startY = 0;
let startLeft = 0;
let startTop = 0;
const onMouseDown = (e: MouseEvent) => {
e.preventDefault();
startX = e.clientX;
startY = e.clientY;
startLeft = this.element.offsetLeft;
startTop = this.element.offsetTop;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const onMouseMove = (e: MouseEvent) => {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newLeft = startLeft + dx;
let newTop = startTop + dy;
// 边界限制,防止拖出容器
const maxLeft = this.container.clientWidth - this.element.offsetWidth;
const maxTop = this.container.clientHeight - this.element.offsetHeight;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
newTop = Math.max(0, Math.min(newTop, maxTop));
this.element.style.left = `${newLeft}px`;
this.element.style.top = `${newTop}px`;
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
this.header.addEventListener('mousedown', onMouseDown);
}
/**
* 初始化缩放功能
*/
private initResize() {
const handle = this.element.querySelector('.bim-dialog-resize-handle') as HTMLElement;
if (!handle) return;
let startX = 0;
let startY = 0;
let startW = 0;
let startH = 0;
const onMouseDown = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
startX = e.clientX;
startY = e.clientY;
startW = this.element.offsetWidth;
startH = this.element.offsetHeight;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
};
const onMouseMove = (e: MouseEvent) => {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const newW = Math.max(this.options.minWidth || 100, startW + dx);
const newH = Math.max(this.options.minHeight || 50, startH + dy);
this.element.style.width = `${newW}px`;
this.element.style.height = `${newH}px`;
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
handle.addEventListener('mousedown', onMouseDown);
}
/**
* 动态设置内容
* @param content 内容元素或 HTML 字符串
*/
public setContent(content: HTMLElement | string) {
this.contentArea.innerHTML = '';
if (typeof content === 'string') {
this.contentArea.innerHTML = content;
} else {
this.contentArea.appendChild(content);
}
}
/**
* 关闭弹窗并销毁
*/
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

@@ -0,0 +1,59 @@
/**
* 弹窗位置类型定义
* 可以是预设的字符串位置(如 'center', 'top-left' 等),
* 也可以是具体的坐标对象 { x, y }
*/
export type DialogPosition =
| 'center'
| 'top-left' | 'top-center' | 'top-right'
| 'left-center' | 'right-center'
| 'bottom-left' | 'bottom-center' | 'bottom-right'
| { x: number; y: number };
/**
* 弹窗颜色配置
*/
export interface DialogColors {
/** 窗体背景颜色,默认 rgba(17, 17, 17, 0.95) */
backgroundColor?: string;
/** 标题栏背景颜色,默认 #2a2a2a */
headerBackgroundColor?: string;
/** 标题文字颜色,默认 #fff */
titleColor?: string;
/** 内容文字颜色,默认 #ccc */
textColor?: string;
/** 边框颜色,默认 #444 */
borderColor?: string;
}
/**
* 弹窗配置选项接口
*/
export interface DialogOptions extends DialogColors {
/** 弹窗挂载的父容器 */
container: HTMLElement;
/** 弹窗标题 */
title?: string;
/** 弹窗内容,支持 HTML 字符串或 HTMLElement */
content?: HTMLElement | string;
/** 弹窗宽度,数字(像素)或字符串(如 '50%' */
width?: number | string;
/** 弹窗高度 */
height?: number | string;
/** 弹窗位置 */
position?: DialogPosition;
/** 是否可拖拽 */
draggable?: boolean;
/** 是否可调整大小 */
resizable?: boolean;
/** 最小宽度限制 */
minWidth?: number;
/** 最小高度限制 */
minHeight?: number;
/** 关闭时的回调函数 */
onClose?: () => void;
/** 打开时的回调函数 */
onOpen?: () => void;
/** 弹窗唯一标识 ID (可选) */
id?: string;
}