Toast组件模块(深色系)- 支持拖拽

深色系提示的toast组件,支持操作按钮和拖拽移动位置

Αυτός ο κώδικας δεν πρέπει να εγκατασταθεί άμεσα. Είναι μια βιβλιοθήκη για άλλους κώδικες που περιλαμβάνεται μέσω της οδηγίας meta // @require https://update.greasyfork.org/scripts/574015/1800165/Toast%E7%BB%84%E4%BB%B6%E6%A8%A1%E5%9D%97%EF%BC%88%E6%B7%B1%E8%89%B2%E7%B3%BB%EF%BC%89-%20%E6%94%AF%E6%8C%81%E6%8B%96%E6%8B%BD.js

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

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

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

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

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

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.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

// ==UserScript==
// @name         Toast组件模块(深色系)- 支持拖拽和关闭
// @namespace    http://tampermonkey.net/
// @version      3.4
// @description  深色系提示的toast组件,支持操作按钮、拖拽移动和手动关闭
// @author       You
// @match        *
// @grant        none
// @noframes
// ==/UserScript==

(function(window) {
    'use strict';

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

    // 存储位置配置的key
    const STORAGE_KEY = 'monkey_toast_position';

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

    // 深色系颜色配置
    const COLORS = {
        default: {
            background: 'rgba(45, 45, 45, 0.85)',
            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'
            },
            closeButton: {
                color: '#999',
                hoverColor: '#fff'
            }
        }
    };

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

    // 拖拽相关变量
    let savedDragPosition = null;
    let currentBasePosition = null;

    /**
     * 加载保存的位置
     */
    function loadSavedPosition() {
        try {
            const saved = localStorage.getItem(STORAGE_KEY);
            if (saved) {
                const position = JSON.parse(saved);
                if (position.left !== undefined && position.top !== undefined) {
                    savedDragPosition = position;
                    currentBasePosition = { ...position };
                    return position;
                }
            }
        } catch (e) {
            console.warn('加载toast位置失败:', e);
        }
        return null;
    }

    /**
     * 保存位置
     */
    function savePosition(left, top) {
        try {
            const position = { left, top };
            savedDragPosition = position;
            currentBasePosition = { ...position };
            localStorage.setItem(STORAGE_KEY, JSON.stringify(position));
        } catch (e) {
            console.warn('保存toast位置失败:', e);
        }
    }

    /**
     * 参数解析函数
     */
    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提示
     */
    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) {
            toastQueue.push(params);
            return null;
        }

        const toastKey = generateToastKey(toastMessage, action);
        
        if (activeToasts.has(toastKey)) {
            return null;
        }

        return createAndShowToast(toastKey, toastMessage, duration, action, toastOptions);
    }

    /**
     * 生成toast的唯一标识
     */
    function generateToastKey(message, action) {
        if (action) {
            const actionStr = action.text || '';
            const actionFunc = action.onClick ? 'hasFunc' : 'noFunc';
            return `${message}_${actionStr}_${actionFunc}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
        }
        return `${message}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
    }

    /**
     * 更新所有Toast位置(在基准位置下方堆叠)
     */
    function updateAllToastPositions() {
        if (!currentBasePosition) {
            // 没有基准位置时使用默认居中
            let currentTop = TOAST_CONFIG.baseOffset;
            Array.from(activeToasts.values()).forEach(({ element }) => {
                element.style.left = '50%';
                element.style.transform = 'translateX(-50%)';
                element.style.top = `${currentTop}px`;
                currentTop += element.offsetHeight + TOAST_CONFIG.spacing;
            });
            return;
        }
        
        // 在基准位置下方堆叠
        let currentTop = currentBasePosition.top;
        const baseLeft = currentBasePosition.left;
        
        Array.from(activeToasts.values()).forEach(({ element, isDragged }) => {
            // 被拖拽的toast保持自己的位置
            if (isDragged && element.style.left !== '50%') {
                return;
            }
            
            element.style.left = `${baseLeft}px`;
            element.style.transform = 'none';
            element.style.top = `${currentTop}px`;
            
            currentTop += element.offsetHeight + TOAST_CONFIG.spacing;
        });
    }

    /**
     * 设置拖拽功能
     */
    function setupDraggable(toast, toastKey) {
        let startX, startY, startLeft, startTop;
        let isDragging = false;
        let animationFrameId = null;
        
        const onMouseMove = (e) => {
            if (!isDragging) return;
            e.preventDefault();
            
            if (animationFrameId) {
                cancelAnimationFrame(animationFrameId);
            }
            
            animationFrameId = requestAnimationFrame(() => {
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                
                let newLeft = startLeft + dx;
                let newTop = startTop + dy;
                
                // 边界限制
                const maxX = window.innerWidth - toast.offsetWidth;
                const maxY = window.innerHeight - toast.offsetHeight;
                newLeft = Math.max(0, Math.min(newLeft, maxX));
                newTop = Math.max(0, Math.min(newTop, maxY));
                
                toast.style.left = `${newLeft}px`;
                toast.style.top = `${newTop}px`;
                toast.style.transform = 'none';
            });
        };
        
        const onMouseUp = (e) => {
            if (!isDragging) return;
            
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
            
            if (animationFrameId) {
                cancelAnimationFrame(animationFrameId);
            }
            
            const currentLeft = parseFloat(toast.style.left);
            const currentTop = parseFloat(toast.style.top);
            
            if (!isNaN(currentLeft) && !isNaN(currentTop)) {
                savePosition(currentLeft, currentTop);
                const toastData = activeToasts.get(toastKey);
                if (toastData) {
                    toastData.isDragged = true;
                }
                updateAllToastPositions();
            }
            
            isDragging = false;
            document.body.style.userSelect = '';
            toast.style.cursor = 'grab';
        };
        
        const onMouseDown = (e) => {
            // 如果点击的是关闭按钮或操作按钮,不启动拖拽
            if (e.target.closest('.tm-toast-close') || e.target.closest('.tm-toast-action')) {
                return;
            }
            
            e.preventDefault();
            
            startX = e.clientX;
            startY = e.clientY;
            
            const rect = toast.getBoundingClientRect();
            startLeft = rect.left;
            startTop = rect.top;
            
            toast.style.left = `${startLeft}px`;
            toast.style.top = `${startTop}px`;
            toast.style.transform = 'none';
            
            isDragging = true;
            
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
            
            document.body.style.userSelect = 'none';
            toast.style.cursor = 'grabbing';
        };
        
        toast.addEventListener('mousedown', onMouseDown);
        toast.style.cursor = 'grab';
        
        return () => {
            toast.removeEventListener('mousedown', onMouseDown);
        };
    }

    /**
     * 创建关闭按钮
     */
    function createCloseButton(toastKey) {
        const closeBtn = document.createElement('button');
        closeBtn.className = 'tm-toast-close';
        closeBtn.innerHTML = '×';
        closeBtn.style.cssText = `
            position: absolute;
            top: 8px;
            right: 8px;
            background: transparent;
            border: none;
            color: ${COLORS.default.closeButton.color};
            font-size: 20px;
            line-height: 1;
            cursor: pointer;
            padding: 0;
            width: 24px;
            height: 24px;
            display: flex;
            align-items: center;
            justify-content: center;
            border-radius: 3px;
            transition: all 0.2s ease;
            font-weight: bold;
        `;
        
        closeBtn.addEventListener('mouseenter', () => {
            closeBtn.style.color = COLORS.default.closeButton.hoverColor;
            closeBtn.style.background = 'rgba(255, 255, 255, 0.1)';
        });
        
        closeBtn.addEventListener('mouseleave', () => {
            closeBtn.style.color = COLORS.default.closeButton.color;
            closeBtn.style.background = 'transparent';
        });
        
        closeBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            removeToast(toastKey);
        });
        
        return closeBtn;
    }

    /**
     * 创建并显示Toast
     */
    function createAndShowToast(toastKey, message, duration, actionConfig, options) {
        const bgColor = options.backgroundColor || COLORS.default.background;
        const textColor = options.color || COLORS.default.text;
        const hoverBgColor = options.hoverBackground || COLORS.default.hoverBackground;
        const hoverTextColor = options.hoverText || COLORS.default.hoverText;

        const toast = document.createElement('div');
        toast.className = 'tm-toast';
        
        // 基础样式(添加相对定位以支持绝对定位的关闭按钮)
        toast.style.cssText = `
            position: fixed;
            background: ${bgColor};
            color: ${textColor};
            padding: 12px 32px 12px 16px;
            border-radius: 6px;
            z-index: 999999;
            opacity: 1;
            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;
            transition: top 0.2s ease, opacity 0.2s ease, left 0.1s ease;
            cursor: grab;
        `;
        
        // 设置初始位置
        if (currentBasePosition) {
            toast.style.left = `${currentBasePosition.left}px`;
            toast.style.transform = 'none';
            toast.style.top = `${currentBasePosition.top + (activeToasts.size * (42 + TOAST_CONFIG.spacing))}px`;
        } else {
            toast.style.left = '50%';
            toast.style.transform = 'translateX(-50%)';
            toast.style.top = `${TOAST_CONFIG.baseOffset + (activeToasts.size * (42 + TOAST_CONFIG.spacing))}px`;
        }
        
        // 添加关闭按钮
        const closeButton = createCloseButton(toastKey);
        toast.appendChild(closeButton);
        
        // 内容容器
        const contentContainer = document.createElement('div');
        contentContainer.style.cssText = `
            flex: 1;
            overflow: hidden;
            text-overflow: ellipsis;
            user-select: text;
            padding-right: 4px;
        `;
        contentContainer.textContent = message;
        toast.appendChild(contentContainer);
        
        // 操作按钮
        if (actionConfig) {
            const 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.15s 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();
                if (typeof actionConfig.onClick === 'function') {
                    try {
                        actionConfig.onClick();
                    } catch (error) {
                        console.error('Toast action执行错误:', error);
                    }
                }
                if (actionConfig.closeOnClick !== false) {
                    removeToast(toastKey);
                }
            });
            
            toast.appendChild(actionButton);
        }
        
        // 设置拖拽功能
        const cleanupDrag = setupDraggable(toast, toastKey);
        
        // 添加到页面
        const container = document.body || document.documentElement;
        container.appendChild(toast);
        
        // 淡入效果
        toast.style.opacity = '0';
        setTimeout(() => {
            toast.style.opacity = '1';
        }, 10);
        
        // 设置定时器
        const timer = setTimeout(() => {
            removeToast(toastKey);
        }, duration);
        
        // 存储toast数据
        activeToasts.set(toastKey, { 
            element: toast, 
            timer, 
            actionConfig,
            options,
            originalBg: bgColor,
            originalText: textColor,
            hoverBg: hoverBgColor,
            hoverText: hoverTextColor,
            duration: duration,
            isDragged: false,
            cleanupDrag
        });
        
        // 更新所有toast位置
        setTimeout(() => {
            updateAllToastPositions();
        }, 50);
        
        // 鼠标悬停效果
        toast.addEventListener('mouseenter', () => {
            const toastData = activeToasts.get(toastKey);
            if (toastData && toastData.timer) {
                clearTimeout(toastData.timer);
                toastData.timer = null;
                toast.style.background = toastData.hoverBg;
                toast.style.color = toastData.hoverText;
            }
        });
        
        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;
            }
        });
        
        return toastKey;
    }

    /**
     * 移除Toast
     */
    function removeToast(toastKey) {
        const toastData = activeToasts.get(toastKey);
        if (!toastData) return;
        
        const { element, timer, cleanupDrag } = toastData;
        if (timer) clearTimeout(timer);
        
        if (cleanupDrag) cleanupDrag();
        
        element.style.opacity = '0';
        
        setTimeout(() => {
            try {
                element.remove();
            } catch (e) {}
            
            activeToasts.delete(toastKey);
            
            // 更新剩余toast位置
            updateAllToastPositions();
            
            // 处理队列
            if (toastQueue.length > 0 && activeToasts.size < TOAST_CONFIG.maxCount) {
                const nextToastParams = toastQueue.shift();
                createAndShowToast(
                    generateToastKey(nextToastParams.message, nextToastParams.action),
                    nextToastParams.message,
                    nextToastParams.duration,
                    nextToastParams.action,
                    nextToastParams.options
                );
            }
        }, TOAST_CONFIG.animationDuration);
    }
    
    /**
     * 清除所有toast
     */
    function clearAllToasts() {
        Array.from(activeToasts.keys()).forEach(toastKey => {
            removeToast(toastKey);
        });
        toastQueue.length = 0;
    }
    
    /**
     * 重置位置到默认
     */
    function resetPosition() {
        localStorage.removeItem(STORAGE_KEY);
        savedDragPosition = null;
        currentBasePosition = null;
        
        Array.from(activeToasts.values()).forEach(data => {
            data.isDragged = false;
        });
        
        updateAllToastPositions();
        showToast('位置已重置', 1500);
    }
    
    /**
     * 获取队列长度
     */
    function getQueueLength() {
        return toastQueue.length;
    }
    
    /**
     * 获取活跃Toast数量
     */
    function getActiveCount() {
        return activeToasts.size;
    }
    
    /**
     * 配置全局参数
     */
    function configToast(config) {
        Object.assign(TOAST_CONFIG, config);
    }
    
    /**
     * 配置全局颜色
     */
    function configColors(colorConfig) {
        Object.assign(COLORS.default, colorConfig);
    }
    
    // 监听窗口大小改变
    window.addEventListener('resize', () => {
        if (currentBasePosition) {
            const maxX = window.innerWidth - 200;
            const maxY = window.innerHeight - 50;
            
            let newLeft = currentBasePosition.left;
            let newTop = currentBasePosition.top;
            
            if (newLeft > maxX) newLeft = maxX;
            if (newLeft < 0) newLeft = 0;
            if (newTop > maxY) newTop = maxY;
            if (newTop < 0) newTop = 0;
            
            if (newLeft !== currentBasePosition.left || newTop !== currentBasePosition.top) {
                savePosition(newLeft, newTop);
            }
        }
        updateAllToastPositions();
    });
    
    // 初始化
    loadSavedPosition();
    
    // 暴露API
    window.MonkeyToast = {
        show: showToast,
        remove: removeToast,
        clearAll: clearAllToasts,
        config: configToast,
        configColors: configColors,
        getQueueLength: getQueueLength,
        getActiveCount: getActiveCount,
        resetPosition: resetPosition
    };
    
})(window);