Toast组件模块

可被其他油猴脚本引用的toast提示组件

Dette scriptet burde ikke installeres direkte. Det er et bibliotek for andre script å inkludere med det nye metadirektivet // @require https://update.greasyfork.org/scripts/548898/1716021/Toast%E7%BB%84%E4%BB%B6%E6%A8%A1%E5%9D%97.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Toast组件模块(深色系)
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  深色系提示的toast组件,支持操作按钮(修复队列问题)
// @author       You
// @match        *
// @grant        none
// @noframes
// ==/UserScript==

(function(window) {
    'use strict';

    // 防止重复加载
    if (window.MonkeyToast) {
        return;
    }

    const TOAST_CONFIG = {
        maxCount: 5,          // 最大同时显示数量
        baseOffset: 20,       // 基础偏移量(px)
        spacing: 10,          // 每个Toast之间的间距
        defaultDuration: 3000,// 默认显示时长(ms)
        animationDuration: 300// 动画过渡时间(ms)
    };

    // 深色系颜色配置
    const COLORS = {
        default: {
            background: 'rgba(45, 45, 45, 0.8)',   // 深灰黑色背景
            text: '#f0f0f0',          // 浅灰文字
            border: '1px solid rgba(68, 68, 68, 0.9)',// 深灰边框
            hoverBackground: '#1a1a1a',// 悬停时更深的背景
            hoverText: '#ffffff',     // 悬停时文字更亮
            hoverOpacity: 1,          // 悬停透明度
            actionButton: {
                background: 'rgba(80, 80, 80, 0.6)', // 操作按钮背景
                text: '#4da6ff',      // 操作按钮文字颜色(蓝色系)
                hoverBackground: 'rgba(100, 100, 100, 0.8)', // 悬停背景
                hoverText: '#66b3ff'  // 悬停文字颜色
            }
        }
    };

    // 存储活跃的Toast
    const activeToasts = new Map();
    // 等待显示的Toast队列
    const toastQueue = [];

    /**
     * 参数解析函数
     * @param {*} message - 参数1
     * @param {*} durationOrOptions - 参数2
     * @param {*} options - 参数3
     * @returns {Object} 解析后的参数对象
     */
    function parseArguments(message, durationOrOptions, options) {
        let parsedMessage, parsedDuration, parsedAction, parsedOptions;
        
        if (typeof message === 'object') {
            // 第一个参数是对象配置
            const config = message;
            parsedMessage = config.message || config.text || '';
            parsedDuration = config.duration || TOAST_CONFIG.defaultDuration;
            parsedAction = config.action;
            parsedOptions = config.options || config;
        } else if (typeof durationOrOptions === 'object') {
            // 第二个参数是对象配置
            parsedMessage = message;
            parsedDuration = durationOrOptions.duration || TOAST_CONFIG.defaultDuration;
            parsedAction = durationOrOptions.action;
            parsedOptions = durationOrOptions.options || durationOrOptions;
        } else {
            // 传统参数格式
            parsedMessage = message;
            parsedDuration = durationOrOptions || TOAST_CONFIG.defaultDuration;
            parsedAction = null;
            parsedOptions = options || {};
        }
        
        return {
            message: parsedMessage,
            duration: parsedDuration,
            action: parsedAction,
            options: parsedOptions
        };
    }

    /**
     * 显示Toast提示
     * @param {string|Object} message - 提示内容或配置对象
     * @param {number|Object} durationOrOptions - 显示时长(ms)或配置对象
     * @param {Object} options - 额外选项,可选
     * @returns {string} toast的唯一标识
     */
    function showToast(message, durationOrOptions = TOAST_CONFIG.defaultDuration, options = {}) {
        // 解析参数
        const params = parseArguments(message, durationOrOptions, options);
        const { message: toastMessage, duration, action, options: toastOptions } = params;
        
        // 检查是否已达到最大显示数量
        if (activeToasts.size >= TOAST_CONFIG.maxCount) {
            // 将完整参数加入队列等待(包含action信息)
            toastQueue.push(params);
            console.log('Toast加入队列,队列长度:', toastQueue.length, '包含action:', !!action);
            return null; // 队列中的toast不返回标识
        }

        // 生成toast唯一标识
        const toastKey = generateToastKey(toastMessage, action);
        
        // 检查是否为重复消息
        if (activeToasts.has(toastKey)) {
            return null;
        }

        // 创建并显示Toast
        return createAndShowToast(toastKey, toastMessage, duration, action, toastOptions);
    }

    /**
     * 生成toast的唯一标识
     * @param {string} message - 消息内容
     * @param {Object|null} action - 操作配置
     * @returns {string} 唯一标识
     */
    function generateToastKey(message, action) {
        if (action) {
            // 对于有action的toast,生成更复杂的唯一标识
            const actionStr = action.text || '';
            const actionFunc = action.onClick ? 'hasFunc' : 'noFunc';
            return `${message}_${actionStr}_${actionFunc}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
        }
        // 对于普通toast,使用消息内容作为标识
        return message;
    }

    /**
     * 创建并显示Toast
     * @param {string} toastKey - toast的唯一标识
     * @param {string} message - 提示内容
     * @param {number} duration - 显示时长
     * @param {Object|null} actionConfig - 操作配置
     * @param {Object} options - 样式选项
     * @returns {string} toast的唯一标识
     */
    function createAndShowToast(toastKey, message, duration, actionConfig, options) {
        // 合并默认样式和自定义样式
        const bgColor = options.backgroundColor || COLORS.default.background;
        const textColor = options.color || COLORS.default.text;
        const border = options.border || COLORS.default.border;
        const hoverBgColor = options.hoverBackground || COLORS.default.hoverBackground;
        const hoverTextColor = options.hoverText || COLORS.default.hoverText;
        const hoverOpacity = options.hoverOpacity ?? COLORS.default.hoverOpacity;

        // 创建Toast元素
        const toast = document.createElement('div');
        toast.className = 'tm-toast';
        
        // 设置基础样式
        toast.style.cssText = `
            position: fixed;
            top: ${TOAST_CONFIG.baseOffset}px;
            left: 50%;
            transform: translateX(-50%);
            background: ${bgColor};
            color: ${textColor};
            padding: 12px 16px;
            border-radius: 6px;
            z-index: 999999;
            opacity: 1;
            transition: all ${TOAST_CONFIG.animationDuration}ms ease;
            box-shadow: 0 4px 12px rgba(0,0,0,0.3);
            pointer-events: auto;
            max-width: 400px;
            min-width: 200px;
            word-wrap: break-word;
            font-size: 14px;
            line-height: 1.4;
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 12px;
            white-space: pre-line;
        `;

        // 创建内容容器
        const contentContainer = document.createElement('div');
        contentContainer.style.cssText = `
            flex: 1;
            overflow: hidden;
            text-overflow: ellipsis;
        `;
        contentContainer.textContent = message;
        
        toast.appendChild(contentContainer);

        // 如果有操作按钮,添加到Toast
        let actionButton = null;
        if (actionConfig) {
            actionButton = document.createElement('button');
            actionButton.className = 'tm-toast-action';
            actionButton.textContent = actionConfig.text || '操作';
            actionButton.style.cssText = `
                background: ${COLORS.default.actionButton.background};
                color: ${COLORS.default.actionButton.text};
                border: none;
                border-radius: 4px;
                padding: 4px 12px;
                font-size: 12px;
                cursor: pointer;
                transition: all 0.2s ease;
                white-space: nowrap;
                flex-shrink: 0;
            `;

            // 鼠标悬停效果
            actionButton.addEventListener('mouseenter', () => {
                actionButton.style.background = COLORS.default.actionButton.hoverBackground;
                actionButton.style.color = COLORS.default.actionButton.hoverText;
            });

            actionButton.addEventListener('mouseleave', () => {
                actionButton.style.background = COLORS.default.actionButton.background;
                actionButton.style.color = COLORS.default.actionButton.text;
            });

            // 点击事件
            actionButton.addEventListener('click', (e) => {
                e.stopPropagation(); // 防止触发Toast的其他事件
                
                if (typeof actionConfig.onClick === 'function') {
                    try {
                        actionConfig.onClick();
                    } catch (error) {
                        console.error('Toast action执行错误:', error);
                    }
                }
                
                // 如果配置了点击后关闭Toast
                if (actionConfig.closeOnClick !== false) {
                    removeToast(toastKey);
                }
            });

            toast.appendChild(actionButton);
        }

        // 添加到文档
        const container = document.body || document.documentElement;
        container.appendChild(toast);

        // 入场动画
        setTimeout(() => {
            toast.style.transform = 'translateX(-50%) translateY(0)';
        }, 10);

        // 记录到活跃列表
        const timer = setTimeout(() => {
            removeToast(toastKey);
        }, duration);
        
        activeToasts.set(toastKey, { 
            element: toast, 
            timer, 
            actionConfig,
            options,
            originalBg: bgColor,
            originalText: textColor,
            hoverBg: hoverBgColor,
            hoverText: hoverTextColor,
            hoverOpacity: hoverOpacity,
            duration: duration
        });

        // 更新所有Toast位置
        updateToastPositions();

        // 鼠标悬停暂停计时并改变样式
        toast.addEventListener('mouseenter', () => {
            const toastData = activeToasts.get(toastKey);
            if (toastData && toastData.timer) {
                clearTimeout(toastData.timer);
                toastData.timer = null;
                // 应用hover样式
                toast.style.background = toastData.hoverBg;
                toast.style.color = toastData.hoverText;
                toast.style.opacity = toastData.hoverOpacity;
            }
        });

        // 鼠标离开恢复计时和样式
        toast.addEventListener('mouseleave', () => {
            const toastData = activeToasts.get(toastKey);
            if (toastData && !toastData.timer) {
                toastData.timer = setTimeout(() => {
                    removeToast(toastKey);
                }, duration);
                // 恢复原始样式
                toast.style.background = toastData.originalBg;
                toast.style.color = toastData.originalText;
                toast.style.opacity = 1;
            }
        });

        // 返回toast标识,可用于手动移除
        return toastKey;
    }

    /**
     * 移除指定Toast
     * @param {string} toastKey - 要移除的toast标识
     */
    function removeToast(toastKey) {
        const toastData = activeToasts.get(toastKey);
        if (!toastData) return;

        const { element, timer } = toastData;
        if (timer) clearTimeout(timer);

        // 淡出动画
        element.style.opacity = 0;
        element.style.transform = 'translateX(-50%) translateY(-10px)';

        // 动画结束后移除元素
        setTimeout(() => {
            try {
                element.remove();
            } catch (e) { /* 忽略已移除的情况 */ }

            activeToasts.delete(toastKey);

            // 更新位置
            updateToastPositions();

            // 检查队列并显示下一个
            if (toastQueue.length > 0) {
                const nextToastParams = toastQueue.shift();
                console.log('从队列中取出Toast显示:', nextToastParams);
                createAndShowToast(
                    generateToastKey(nextToastParams.message, nextToastParams.action),
                    nextToastParams.message,
                    nextToastParams.duration,
                    nextToastParams.action,
                    nextToastParams.options
                );
            }
        }, TOAST_CONFIG.animationDuration);
    }

    /**
     * 更新所有活跃Toast的位置,实现自动堆叠
     */
    function updateToastPositions() {
        let currentOffset = TOAST_CONFIG.baseOffset;

        // 按添加顺序遍历并更新位置
        Array.from(activeToasts.values()).forEach(({ element }) => {
            // 设置新位置
            element.style.top = `${currentOffset}px`;

            // 计算下一个位置(当前元素高度 + 间距)
            currentOffset += element.offsetHeight + TOAST_CONFIG.spacing;
        });
    }

    /**
     * 清除所有toast
     */
    function clearAllToasts() {
        // 清除活跃的toast
        Array.from(activeToasts.keys()).forEach(toastKey => {
            removeToast(toastKey);
        });
        // 清空队列
        toastQueue.length = 0;
    }

    /**
     * 获取当前队列长度
     * @returns {number} 队列长度
     */
    function getQueueLength() {
        return toastQueue.length;
    }

    /**
     * 获取活跃Toast数量
     * @returns {number} 活跃Toast数量
     */
    function getActiveCount() {
        return activeToasts.size;
    }

    /**
     * 配置toast全局参数
     * @param {Object} config - 配置对象
     */
    function configToast(config) {
        Object.assign(TOAST_CONFIG, config);
    }

    /**
     * 配置全局颜色
     * @param {Object} colorConfig - 颜色配置对象
     */
    function configColors(colorConfig) {
        Object.assign(COLORS.default, colorConfig);
    }

    // 暴露公共API
    window.MonkeyToast = {
        show: showToast,
        remove: removeToast,
        clearAll: clearAllToasts,
        config: configToast,
        configColors: configColors,
        getQueueLength: getQueueLength,
        getActiveCount: getActiveCount
    };

})(window);