/**
 * 对话框与通知系统
 * 现代化的、基于 Promise 的 layer.js 替代方案
 */

class DialogUI {
    constructor() {
        // 通知框实例集合
        this._notificationInstances = new Set();
    }

    /**
     * 创建通用的遮罩层和对话框结构
     * @param {string} title - 标题
     * @param {string} content - 内容
     * @param {string} type - 类型:'alert' | 'confirm' | 'prompt'
     * @returns {Object} 包含 overlay 和 dialog 的对象
     */
    createDialog(title, content, type = 'alert') {
        const overlay = document.createElement('div');
        overlay.className = 'dialog-overlay';
        
        const dialog = document.createElement('div');
        dialog.className = 'dialog';
        
        // 头部
        const header = document.createElement('div');
        header.className = 'dialog__header';
        header.innerHTML = `
            <span>${title || '提示'}</span>
            <button type="button" class="dialog__close" aria-label="关闭">
                <i class="fa fa-times"></i>
            </button>
        `;
        
        // 主体
        const body = document.createElement('div');
        body.className = 'dialog__body';
        body.innerHTML = `<div class="dialog__message">${content}</div>`;
        
        if (type === 'prompt') {
            const inputWrapper = document.createElement('div');
            inputWrapper.className = 'dialog__input-wrapper';
            inputWrapper.innerHTML = `<input type="text" class="dialog__input" autocomplete="off">`;
            body.appendChild(inputWrapper);
        }

        // 底部
        const footer = document.createElement('div');
        footer.className = 'dialog__footer';
        
        if (type === 'alert') {
            footer.innerHTML = `
                <button type="button" class="dialog__btn dialog__btn--primary" data-action="confirm">确定</button>
            `;
        } else if (type === 'confirm' || type === 'prompt') {
            footer.innerHTML = `
                <button type="button" class="dialog__btn dialog__btn--default" data-action="cancel">取消</button>
                <button type="button" class="dialog__btn dialog__btn--primary" data-action="confirm">确定</button>
            `;
        }

        dialog.appendChild(header);
        dialog.appendChild(body);
        dialog.appendChild(footer);
        overlay.appendChild(dialog);
        document.body.appendChild(overlay);

        // 强制重排以触发动画
        overlay.offsetHeight; 
        overlay.classList.add('dialog-overlay--visible');

        return { overlay, dialog };
    }

    closeDialog(overlay) {
        overlay.classList.remove('dialog-overlay--visible');
        overlay.addEventListener('transitionend', () => {
            if (overlay.parentNode) {
                overlay.parentNode.removeChild(overlay);
            }
        }, { once: true });
    }

    /**
     * 显示模态提示框
     * @param {string} message - 消息内容
     * @param {string} title - 标题
     * @returns {Promise<void>}
     */
    alert(message, title = '提示') {
        return new Promise((resolve) => {
            const { overlay, dialog } = this.createDialog(title, message, 'alert');
            
            const handleClose = () => {
                this.closeDialog(overlay);
                resolve();
            };

            dialog.querySelector('[data-action="confirm"]').addEventListener('click', handleClose);
            dialog.querySelector('.dialog__close').addEventListener('click', handleClose);
            
            // 聚焦主要按钮
            dialog.querySelector('.dialog__btn--primary').focus();
        });
    }

    /**
     * 显示模态确认框
     * @param {string} message - 消息内容
     * @param {string} title - 标题
     * @returns {Promise<boolean>} 用户点击确定返回 true,取消返回 false
     */
    confirm(message, title = '确认') {
        return new Promise((resolve) => {
            const { overlay, dialog } = this.createDialog(title, message, 'confirm');
            
            const handleConfirm = () => {
                this.closeDialog(overlay);
                resolve(true);
            };
            
            const handleCancel = () => {
                this.closeDialog(overlay);
                resolve(false);
            };

            dialog.querySelector('[data-action="confirm"]').addEventListener('click', handleConfirm);
            dialog.querySelector('[data-action="cancel"]').addEventListener('click', handleCancel);
            dialog.querySelector('.dialog__close').addEventListener('click', handleCancel);
            
            dialog.querySelector('.dialog__btn--primary').focus();
        });
    }

    /**
     * 显示模态输入框
     * @param {string} message - 提示消息
     * @param {string} title - 标题
     * @param {string} defaultValue - 默认值
     * @returns {Promise<string|null>} 用户输入的值,取消返回 null
     */
    prompt(message, title = '输入', defaultValue = '') {
        return new Promise((resolve) => {
            const { overlay, dialog } = this.createDialog(title, message, 'prompt');
            const input = dialog.querySelector('.dialog__input');
            input.value = defaultValue;
            
            const handleConfirm = () => {
                const val = input.value;
                this.closeDialog(overlay);
                resolve(val);
            };
            
            const handleCancel = () => {
                this.closeDialog(overlay);
                resolve(null);
            };

            dialog.querySelector('[data-action="confirm"]').addEventListener('click', handleConfirm);
            dialog.querySelector('[data-action="cancel"]').addEventListener('click', handleCancel);
            dialog.querySelector('.dialog__close').addEventListener('click', handleCancel);
            
            // 处理输入框的 Enter 和 Escape 键
            input.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') handleConfirm();
                if (e.key === 'Escape') handleCancel();
            });

            setTimeout(() => input.focus(), 50);
        });
    }

    /**
     * 显示 Toast 消息
     * @param {string} message - 消息内容
     * @param {string} type - 类型:'info' | 'success' | 'warning' | 'error'
     * @param {number} duration - 显示时长(毫秒)
     * @param {string} position - 位置:'top-center' | 'top-left' | 'top-right' | 'bottom-center' | 'bottom-left' | 'bottom-right'
     */
    toast(message, type = 'info', duration = 3000, position = 'top-center') {
        const container = this.getToastContainer(position);
        
        const toast = document.createElement('div');
        toast.className = `toast toast--${type}`;
        
        let iconClass = 'fa-info-circle';
        if (type === 'success') iconClass = 'fa-check-circle';
        if (type === 'warning') iconClass = 'fa-exclamation-triangle';
        if (type === 'error') iconClass = 'fa-times-circle';

        toast.innerHTML = `
            <i class="toast__icon fa ${iconClass}"></i>
            <span class="toast__content">${message}</span>
        `;
        
        container.appendChild(toast);

        // 计时器管理
        let timeoutId;
        let remainingTime = duration;
        let startTime = Date.now();

        const closeToast = () => {
            toast.classList.add('is-leaving');
            toast.addEventListener('animationend', () => {
                if (toast.parentNode) {
                    toast.parentNode.removeChild(toast);
                }
            }, { once: true });
        };

        const startTimer = () => {
            startTime = Date.now();
            timeoutId = setTimeout(closeToast, remainingTime);
        };

        const pauseTimer = () => {
            if (timeoutId) {
                clearTimeout(timeoutId);
                remainingTime -= (Date.now() - startTime);
                // 确保剩余时间不为负数
                if (remainingTime < 0) remainingTime = 0;
            }
        };

        const resumeTimer = () => {
            if (remainingTime > 0) {
                startTimer();
            } else {
                closeToast();
            }
        };

        // 鼠标悬停时暂停,移开时继续
        toast.addEventListener('mouseenter', pauseTimer);
        toast.addEventListener('mouseleave', resumeTimer);
        
        // 移动端触摸支持
        toast.addEventListener('touchstart', pauseTimer, { passive: true });
        toast.addEventListener('touchend', resumeTimer, { passive: true });

        // 启动初始计时器
        startTimer();
    }

    /**
     * 获取或创建指定位置的 toast 容器
     * @param {string} position - 位置
     * @returns {HTMLElement}
     */
    getToastContainer(position) {
        // 使用 data-position 属性查找容器
        let container = document.querySelector(`.toast-container[data-position="${position}"]`);
        
        if (!container) {
            container = document.createElement('div');
            container.className = `toast-container toast-container--${position}`;
            container.dataset.position = position;
            document.body.appendChild(container);
        }
        
        return container;
    }

    /**
     * 显示工具提示
     * @param {HTMLElement} element - 目标元素
     * @param {string} text - 提示内容
     */
    showTooltip(element, text) {
        if (!text) return;
        
        let tooltip = document.getElementById('dialog-tooltip');
        if (!tooltip) {
            tooltip = document.createElement('div');
            tooltip.id = 'dialog-tooltip';
            tooltip.className = 'dialog-tooltip';
            document.body.appendChild(tooltip);
        }

        // 清除旧的检测定时器
        if (this._tooltipTimer) {
            clearInterval(this._tooltipTimer);
            this._tooltipTimer = null;
        }

        tooltip.textContent = text;
        
        // 在计算位置前重置状态
        tooltip.classList.remove('is-bottom');
        tooltip.classList.add('is-visible');

        const updatePosition = () => {
            if (!element || !document.body.contains(element)) {
                this.hideTooltip();
                return;
            }

            const rect = element.getBoundingClientRect();
            // 如果元素不可见(不在视口内或 display:none),隐藏 tooltip
            if (rect.width === 0 && rect.height === 0) {
                this.hideTooltip();
                return;
            }

            const tooltipRect = tooltip.getBoundingClientRect();
            
            // 默认位置:顶部居中
            let top = rect.top - tooltipRect.height - 8; // 8px 间距
            let left = rect.left + (rect.width - tooltipRect.width) / 2;

            // 检查顶部边界
            if (top < 0) {
                // 翻转到底部
                top = rect.bottom + 8;
                tooltip.classList.add('is-bottom');
            } else {
                 tooltip.classList.remove('is-bottom');
            }

            // 检查左右边界
            if (left < 0) {
                left = 4; // 距离左边缘的边距
            } else if (left + tooltipRect.width > window.innerWidth) {
                left = window.innerWidth - tooltipRect.width - 4; // 距离右边缘的边距
            }

            tooltip.style.top = `${top}px`;
            tooltip.style.left = `${left}px`;
        };

        updatePosition();

        // 启动定时器检测元素状态(每 200ms 检查一次)
        this._tooltipTimer = setInterval(updatePosition, 200);
    }

    /**
     * 隐藏工具提示
     */
    hideTooltip() {
        const tooltip = document.getElementById('dialog-tooltip');
        if (tooltip) {
            tooltip.classList.remove('is-visible');
        }
        if (this._tooltipTimer) {
            clearInterval(this._tooltipTimer);
            this._tooltipTimer = null;
        }
    }

    // ========== 通知框相关方法 ==========

    /**
     * 显示通知框
     * @typedef {Object} NotificationOptions
     * @property {string} [title=""] - 通知框标题
     * @property {string} [content=""] - 通知框内容
     * @property {number[]} [bars=[]] - 进度条数组
     * @property {number} [autoClose=0] - 自动关闭时间(毫秒),0 表示不自动关闭
     * @property {string} [type="info"] - 通知类型:info/success/warning/error
     * @param {NotificationOptions} option - 通知选项
     * @returns {HTMLElement} 通知框 DOM 元素
     */
    showNotification(option = {}) {
        const setting = {
            title: "",
            content: "",
            bars: [],
            autoClose: 0,
            type: "info",
            ...option
        };

        // 计算位置
        const top = this._calculateNotificationTopPosition();
        
        // 创建通知框
        const box = this._createNotificationBox(setting);
        box.style.top = `${top}px`;
        
        // 添加到 DOM 并跟踪实例
        document.body.appendChild(box);
        this._notificationInstances.add(box);
        
        // 设置自动关闭
        if (setting.autoClose > 0) {
            setTimeout(() => this.closeNotification(box), setting.autoClose);
        }
        
        // 添加入场动画
        requestAnimationFrame(() => {
            box.classList.add('notification-box--enter');
        });

        return box;
    }

    /**
     * 设置通知框内容
     * @param {HTMLElement} box - 通知框元素
     * @param {string} content - 新内容
     */
    setNotificationContent(box, content) {
        if (!this._validateNotificationBox(box)) return;
        
        const contentEl = box.querySelector('.notification-box__content');
        if (contentEl) {
            contentEl.textContent = String(content || "");
        }
    }

    /**
     * 设置通知框标题
     * @param {HTMLElement} box - 通知框元素
     * @param {string} title - 新标题
     */
    setNotificationTitle(box, title) {
        if (!this._validateNotificationBox(box)) return;
        
        const titleEl = box.querySelector('.notification-box__title');
        if (titleEl) {
            titleEl.textContent = String(title || "");
        }
    }

    /**
     * 设置进度条的进度
     * @param {HTMLElement} box - 通知框元素
     * @param {number[]} values - 进度值数组
     */
    setNotificationProgress(box, values) {
        if (!this._validateNotificationBox(box) || !Array.isArray(values)) return;

        const progressContainers = box.querySelectorAll('.notification-box__progress');
        
        values.forEach((value, index) => {
            if (typeof value === "number" && index < progressContainers.length) {
                const clampedValue = Math.max(0, Math.min(100, value));
                const progress = progressContainers[index];
                const track = progress.querySelector('.notification-box__progress-track');
                const bar = progress.querySelector('.notification-box__progress-bar');
                const textValue = `${clampedValue.toFixed(1)}%`;
                
                // 更新进度条宽度和显示状态
                if (bar) {
                    if (clampedValue > 0) {
                        bar.style.width = `${clampedValue}%`;
                        bar.style.display = '';
                    } else {
                        bar.style.width = '0%';
                        bar.style.display = 'none';
                    }
                }
                
                // 更新百分比文字(始终显示在轨道中间)
                const textInternal = track ? track.querySelector('.notification-box__progress-text') : null;
                const textExternal = progress.querySelector('.notification-box__progress-text--external');
                
                if (textInternal) {
                    // 如果已经有内部文字,直接更新
                    textInternal.textContent = textValue;
                } else if (textExternal) {
                    // 如果之前是外部文字,移动到内部(中间)
                    textExternal.remove();
                    const newText = document.createElement('span');
                    newText.className = 'notification-box__progress-text';
                    newText.textContent = textValue;
                    if (track) {
                        track.appendChild(newText);
                    }
                } else if (track) {
                    // 如果都没有,创建新的
                    const newText = document.createElement('span');
                    newText.className = 'notification-box__progress-text';
                    newText.textContent = textValue;
                    track.appendChild(newText);
                }
            }
        });
    }

    /**
     * 关闭通知框
     * @param {HTMLElement} box - 要关闭的通知框
     */
    closeNotification(box) {
        if (!this._validateNotificationBox(box)) return;

        // 添加退场动画
        box.classList.add('notification-box--exit');
        
        // 动画完成后移除
        setTimeout(() => {
            if (box.parentNode) {
                this._notificationInstances.delete(box);
                box.parentNode.removeChild(box);
                this._recalculateNotificationPositions();
            }
        }, 300);
    }

    /**
     * 关闭所有通知框
     */
    closeAllNotifications() {
        const boxes = Array.from(this._notificationInstances);
        boxes.forEach(box => {
            this.closeNotification(box);
        });
    }

    /**
     * 获取当前活跃的通知框数量
     * @returns {number}
     */
    getNotificationCount() {
        return this._notificationInstances.size;
    }

    // ========== 通知框私有方法 ==========

    /**
     * 验证通知框元素
     * @private
     * @param {HTMLElement} box - 通知框元素
     * @returns {boolean}
     */
    _validateNotificationBox(box) {
        return box && box.parentNode && this._notificationInstances.has(box);
    }

    /**
     * 计算顶部位置
     * @private
     * @returns {number}
     */
    _calculateNotificationTopPosition() {
        let top = 15;
        this._notificationInstances.forEach(box => {
            if (box.offsetParent !== null) {
                top += box.offsetHeight + 15;
            }
        });
        return top;
    }

    /**
     * 重新计算所有通知框位置
     * @private
     */
    _recalculateNotificationPositions() {
        let currentTop = 15;
        this._notificationInstances.forEach(box => {
            if (box.offsetParent !== null) {
                box.style.top = `${currentTop}px`;
                currentTop += box.offsetHeight + 15;
            }
        });
    }

    /**
     * 创建通知框 DOM
     * @private
     * @param {Object} setting - 设置对象
     * @returns {HTMLElement}
     */
    _createNotificationBox(setting) {
        const box = document.createElement('div');
        box.className = `notification-box notification-box--${setting.type}`;
        
        // 创建标题
        if (setting.title) {
            const title = document.createElement('div');
            title.className = 'notification-box__title';
            title.textContent = setting.title;
            box.appendChild(title);
        }
        
        // 创建内容容器
        const container = document.createElement('div');
        container.className = 'notification-box__content-container';
        
        // 创建内容
        const content = document.createElement('div');
        content.className = 'notification-box__content';
        content.textContent = setting.content;
        container.appendChild(content);
        
        // 创建进度条
        this._createNotificationProgressBars(container, setting.bars);
        
        box.appendChild(container);
        
        // 添加关闭按钮
        this._addNotificationCloseButton(box);
        
        return box;
    }

    /**
     * 创建进度条
     * @private
     * @param {HTMLElement} container - 容器元素
     * @param {number[]} bars - 进度值数组
     */
    _createNotificationProgressBars(container, bars) {
        if (!Array.isArray(bars) || bars.length === 0) return;
        
        bars.forEach(value => {
            if (typeof value === "number") {
                const clampedValue = Math.max(0, Math.min(100, value));
                const progress = document.createElement('div');
                progress.className = 'notification-box__progress';
                
                // 创建进度条轨道
                const track = document.createElement('div');
                track.className = 'notification-box__progress-track';
                
                // 创建进度条
                const bar = document.createElement('div');
                bar.className = 'notification-box__progress-bar';
                if (clampedValue > 0) {
                    bar.style.width = `${clampedValue}%`;
                    bar.style.display = '';
                } else {
                    bar.style.width = '0%';
                    bar.style.display = 'none';
                }
                
                // 创建百分比文字(始终显示在进度条轨道中间)
                const textValue = `${clampedValue.toFixed(1)}%`;
                const text = document.createElement('span');
                text.className = 'notification-box__progress-text';
                text.textContent = textValue;
                
                // 文字始终添加到 track 中,使用绝对定位固定在中间
                track.appendChild(bar);
                track.appendChild(text);
                progress.appendChild(track);
                
                container.appendChild(progress);
            }
        });
    }

    /**
     * 添加关闭按钮
     * @private
     * @param {HTMLElement} box - 通知框元素
     */
    _addNotificationCloseButton(box) {
        const closeBtn = document.createElement('button');
        closeBtn.className = 'notification-box__close';
        closeBtn.innerHTML = '&times;';
        closeBtn.setAttribute('aria-label', '关闭通知');
        closeBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            this.closeNotification(box);
        });
        
        box.appendChild(closeBtn);
    }
}

export const dialog = new DialogUI();

⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个现代化的对话框与通知系统的JavaScript类,作为layer.js的替代方案。让我详细解释其主要功能和结构:

核心功能模块

1. 模态对话框系统

  • Alert 提示框: alert(message, title) - 显示简单的提示信息
  • Confirm 确认框: confirm(message, title) - 显示确认/取消选择
  • Prompt 输入框: prompt(message, title, defaultValue) - 获取用户输入

所有对话框都基于Promise,支持现代化的async/await语法。

2. Toast 消息通知

  • 多种类型: info、success、warning、error
  • 多种位置: 6个预设位置(顶部/底部 × 左/中/右)
  • 智能计时: 支持鼠标悬停暂停、移动端触摸暂停
  • 自动管理: 自动创建容器,动画进出场

3. 工具提示 (Tooltip)

  • 智能定位: 自动检测边界并调整位置
  • 实时跟踪: 定时器监控目标元素状态
  • 响应式: 支持元素移动和隐藏检测

4. 通知框系统

  • 丰富内容: 支持标题、内容、多进度条
  • 实时更新: 可动态修改标题、内容、进度
  • 位置管理: 自动计算堆叠位置,支持关闭后重排
  • 类型样式: 支持不同类型的视觉样式

技术特点

现代化设计

// 基于Promise的API
await dialog.alert('操作完成');
const confirmed = await dialog.confirm('确定删除吗?');
const input = await dialog.prompt('请输入名称');

智能交互

// Toast支持暂停/恢复
toast.addEventListener('mouseenter', pauseTimer);
toast.addEventListener('mouseleave', resumeTimer);

实例管理

// 使用Set集合管理通知实例
this._notificationInstances = new Set();

动画支持

  • CSS类切换实现流畅动画
  • 支持进场/退场动画
  • 过渡事件监听确保DOM清理

使用示例

// 基础对话框
await dialog.alert('保存成功');

// 确认操作
if (await dialog.confirm('确定删除此项吗?')) {
    // 执行删除
}

// 获取输入
const name = await dialog.prompt('请输入姓名', '输入', '默认值');

// 显示Toast
dialog.toast('操作成功', 'success', 3000, 'top-center');

// 通知框with进度条
const notification = dialog.showNotification({
    title: '文件上传',
    content: '正在上传...',
    bars: [0, 0], // 两个进度条
    type: 'info'
});

// 更新进度
dialog.setNotificationProgress(notification, [50, 30]);

这个类提供了完整的UI反馈解决方案,代码结构清晰,功能丰富,是一个很好的现代化对话框系统实现。

评论加载中...