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

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

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @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와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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);