深色系提示的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
// ==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);