refactor: reorganize project structure and implement self-managed i18n/theme for components
This commit is contained in:
30
src/components/dialog/bimInfoDialog/index.css
Normal file
30
src/components/dialog/bimInfoDialog/index.css
Normal 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;
|
||||
}
|
||||
65
src/components/dialog/bimInfoDialog/index.ts
Normal file
65
src/components/dialog/bimInfoDialog/index.ts
Normal 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 继承
|
||||
}
|
||||
95
src/components/dialog/index.css
Normal file
95
src/components/dialog/index.css
Normal 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;
|
||||
}
|
||||
357
src/components/dialog/index.ts
Normal file
357
src/components/dialog/index.ts
Normal 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 = '×';
|
||||
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();
|
||||
}
|
||||
}
|
||||
59
src/components/dialog/index.type.ts
Normal file
59
src/components/dialog/index.type.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user