// ==UserScript==
// @name 鱼排红包板块
// @namespace https://fishpi.cn
// @license MIT
// @version 1.2
// @description 右侧新增红包板块,将聊天室红包同步到红包板块,保持实时更新,支持多类型红包
// @author muli
// @match https://fishpi.cn/cr
// @icon https://file.fishpi.cn/2025/11/blob-4d0e46ad.png?imageView2/1/w/48/h/48/interlace/0/q/100
// @grant none
// @run-at document-end
// @downloadURL https://raw.githubusercontent.com/mu-xiao-li/yupai-extend-js/main/hongbao_module.user.js
// @updateURL https://raw.githubusercontent.com/mu-xiao-li/yupai-extend-js/main/hongbao_module.user.js
// ==/UserScript==
// 2026-01-13 新增“是否自动删除已抢光的红包”配置,可配置无效红包是否自动删除
// 2026-01-13 muli 新增切换浮窗模式按钮,新增不捕获的红包类型配置,新增配置面板
(function() {
'use strict';
// 配置
const CONFIG = {
maxDisplayCount: 20, // 最多显示红包数量
visibleCount: 5, // 默认可见红包数量
refreshInterval: 10000, // 全量扫描间隔(延长到10秒)
syncInterval: 1000, // 同步状态间隔
preserveOriginal: true, // 保留原清风明月
position: 'above', // 红包面板位置: above(上方) / below(下方)
autoScrollNew: false, // 关闭自动滚动到新红包(新的在上面)
monitorNewMessages: true, // 监听新消息
newMessageThreshold: 5, // 每次扫描的新消息数量
autoDelRedPackets: false, // 是否自动删除已抢光的红包
// 新增:过滤的红包类型
filterRedPacketTypes: ['猜拳红包'], // 例如:['普通红包', '专属红包', '猜拳红包']
// 新增:是否启用红包类型过滤
enableRedPacketFilter: false
};
// 存储红包数据
let redPackets = new Map(); // 红包ID -> 红包数据
let displayOrder = []; // 显示顺序(红包ID数组)
let currentDisplayed = new Set(); // 当前显示的红包ID
let observers = new Map(); // 观察器映射
let isInitialized = false;
let originalBreezeMoon = null; // 原清风明月内容
let chatObserver = null; // 聊天室观察器
let lastProcessedTime = 0; // 上次处理时间
let processedMessageIds = new Set(); // 已处理的消息ID
// 主初始化函数
function init() {
if (isInitialized) return;
console.log('红包同步脚本初始化...');
// 从localStorage加载配置
loadConfigFromStorage();
// 查找清风明月模块
const breezeMoonModule = findBreezeMoonModule();
if (!breezeMoonModule) {
console.error('未找到清风明月模块,重试中...');
setTimeout(init, 2000);
return;
}
// 保存原清风明月内容
saveOriginalContent(breezeMoonModule);
// 创建红包显示面板
const redPacketPanel = createRedPacketPanel();
// 插入到指定位置
if (CONFIG.position === 'above') {
// 插入到清风明月模块的上方
const parent = breezeMoonModule.parentNode;
if (parent) {
parent.insertBefore(redPacketPanel, breezeMoonModule);
}
} else {
// 插入到清风明月模块的下方
if (breezeMoonModule.nextSibling) {
breezeMoonModule.parentNode.insertBefore(redPacketPanel, breezeMoonModule.nextSibling);
} else {
breezeMoonModule.parentNode.appendChild(redPacketPanel);
}
}
// 初始全量扫描
scanRedPackets();
// 开始监听聊天室变化
startChatroomMonitoring();
// 开始定时任务
startTimers();
// 添加事件监听
addEventListeners();
isInitialized = true;
console.log('红包同步脚本初始化完成');
}
// 查找清风明月模块
function findBreezeMoonModule() {
const selectors = [
'.module:has(#breezemoonInput)',
'.module:has(.breezemoon__input)',
'.breezemoon__input, #breezemoonInput'
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element) {
return element.closest('.module');
}
}
return null;
}
// 保存原清风明月内容
function saveOriginalContent(breezeMoonModule) {
originalBreezeMoon = breezeMoonModule.cloneNode(true);
}
// 创建红包面板
function createRedPacketPanel() {
const panel = document.createElement('div');
panel.className = 'module red-packet-module';
panel.style.cssText = `
margin-bottom: 15px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: #fff;
`;
// 面板头部
const header = document.createElement('div');
header.className = 'module-header form';
header.style.cssText = `
background: linear-gradient(135deg, #ff6b6b, #ff8e53);
color: white;
border-bottom: 2px solid #ff4757;
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 15px;
`;
const title = document.createElement('div');
title.style.cssText = 'font-size: 16px; font-weight: bold;';
title.innerHTML = ' 🧧 聊天室红包';
const controls = document.createElement('div');
controls.style.cssText = 'display: flex; align-items: center; gap: 10px;';
const countBadge = document.createElement('span');
countBadge.className = 'red-packet-count';
countBadge.style.cssText = `
background: rgba(255,255,255,0.2);
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
`;
countBadge.textContent = '0';
const expandBtn = document.createElement('button');
expandBtn.className = 'expand-btn';
expandBtn.style.cssText = `
background: rgba(255,255,255,0.2);
border: none;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
font-size: 12px;
display: flex;
align-items: center;
justify-content: center;
`;
expandBtn.innerHTML = '▼';
expandBtn.title = '展开/收起';
controls.appendChild(countBadge);
controls.appendChild(expandBtn);
// 添加切换浮窗模式按钮
const floatingBtn = document.createElement('button');
floatingBtn.className = 'floating-window-btn';
floatingBtn.innerHTML = '⇄';
floatingBtn.title = '切换浮窗模式';
floatingBtn.style.cssText = `
width: 24px;
height: 24px;
border-radius: 50% !important;
background: linear-gradient(135deg, #2b8a3e 0%, #20c997 100%) !important;
color: white !important;
font-size: 14px !important;
font-weight: bold !important;
cursor: pointer !important;
box-shadow: 0 4px 15px rgba(32, 201, 151, 0.4) !important;
border: 3px solid white !important;
display: flex;
align-items: center;
justify-content: center;
margin-right: 5px;
padding: 0;
line-height: 1;
`;
// 添加浮窗切换功能
floatingBtn.addEventListener('click', function(e) {
e.stopPropagation();
toggleFloatingWindow(panel);
});
controls.appendChild(floatingBtn);
header.appendChild(title);
header.appendChild(controls);
// 面板主体
const body = document.createElement('div');
body.className = 'module-panel red-packet-body';
body.style.cssText = `
max-height: ${CONFIG.visibleCount * 120}px;
overflow-y: auto;
transition: max-height 0.3s ease;
padding: 10px;
`;
// 红包列表
const list = document.createElement('div');
list.className = 'red-packet-list';
list.style.cssText = 'display: flex; flex-direction: column; gap: 10px;';
body.appendChild(list);
panel.appendChild(header);
panel.appendChild(body);
// 添加交互事件
expandBtn.addEventListener('click', function() {
const isExpanded = body.style.maxHeight === 'none';
if (isExpanded) {
body.style.maxHeight = `${CONFIG.visibleCount * 120}px`;
expandBtn.innerHTML = '▼';
} else {
body.style.maxHeight = 'none';
expandBtn.innerHTML = '▲';
}
});
return panel;
}
// 扫描红包(全量)
function scanRedPackets() {
console.log('执行全量红包扫描...');
const chatItems = document.querySelectorAll('#comments .chats__item');
const newPackets = [];
chatItems.forEach(item => {
processChatItem(item, newPackets);
});
// 处理新红包
if (newPackets.length > 0) {
handleNewPackets(newPackets);
}
// 更新显示顺序
updateDisplayOrder();
// 更新显示
updateRedPacketDisplay();
}
// 处理单个聊天项
function processChatItem(item, newPackets) {
const redPacket = item.querySelector('.hongbao__item');
if (!redPacket) return;
const packetId = getRedPacketId(item, redPacket);
if (!packetId) return;
// 如果消息已处理过,跳过
if (processedMessageIds.has(packetId)) return;
// 检查是否应该过滤此红包类型
if (CONFIG.enableRedPacketFilter && CONFIG.filterRedPacketTypes.length > 0) {
// 获取红包类型
const redPacketType = getRedPacketType(redPacket);
if (CONFIG.filterRedPacketTypes.includes(redPacketType)) {
//console.log(`过滤红包类型: ${redPacketType} (红包ID: ${packetId})`);
return; // 跳过此红包
}
}
if (!redPackets.has(packetId)) {
const packetData = {
id: packetId,
element: item.cloneNode(true),
originalElement: item,
redPacketElement: redPacket,
originalRedPacket: redPacket,
time: getMessageTime(item),
user: getMessageUser(item),
status: getRedPacketStatus(redPacket),
lastUpdated: Date.now(),
observer: null,
isNew: true
};
// 自动删除打开的话 过滤已抢光的红包
if (CONFIG.autoDelRedPackets && packetData.status == 'empty') {
return;
}
redPackets.set(packetId, packetData);
processedMessageIds.add(packetId);
newPackets.push(packetData);
// 创建观察器来监听红包状态变化
setupRedPacketObserver(packetData);
}
}
// 获取红包类型
function getRedPacketType(redPacket) {
const typeElement = redPacket.querySelector('b');
if (!typeElement) return '未知红包';
const typeText = typeElement.textContent.trim();
// 定义已知的红包类型
const knownTypes = [
'拼手气红包',
'普通红包',
'专属红包',
'心跳红包',
'猜拳红包',
'石头剪刀布红包' // 有些页面可能显示这个
];
// 检查是否为已知类型
for (const knownType of knownTypes) {
if (typeText.includes(knownType)) {
return knownType;
}
}
return typeText; // 返回原始文本
}
// 处理新红包
function handleNewPackets(newPackets) {
// 标记最新红包为新增
newPackets.forEach(packetData => {
packetData.isNew = true;
// 5秒后移除新标记
setTimeout(() => {
packetData.isNew = false;
updateRedPacketItem(packetData.id);
}, 5000);
});
}
// 扫描最新消息
function scanLatestMessages() {
if (!CONFIG.monitorNewMessages) return;
console.log('扫描最新消息...');
const chatItems = document.querySelectorAll('#comments .chats__item');
const newPackets = [];
// 只扫描最新的N条消息
const latestItems = Array.from(chatItems).slice(0, CONFIG.newMessageThreshold);
latestItems.forEach(item => {
processChatItem(item, newPackets);
});
// 如果有新红包,更新显示
if (newPackets.length > 0) {
handleNewPackets(newPackets);
updateDisplayOrder();
updateRedPacketDisplay();
}
}
// 开始监听聊天室变化
function startChatroomMonitoring() {
const chatContainer = document.getElementById('comments');
if (!chatContainer) {
console.error('未找到聊天室容器');
setTimeout(startChatroomMonitoring, 2000);
return;
}
// 创建观察器
chatObserver = new MutationObserver((mutations) => {
// 防抖处理,避免频繁调用
const now = Date.now();
if (now - lastProcessedTime < 500) return; // 500ms防抖
lastProcessedTime = now;
let hasNewMessages = false;
mutations.forEach((mutation) => {
if (mutation.addedNodes.length > 0) {
hasNewMessages = true;
}
});
if (hasNewMessages) {
// 延迟执行,确保DOM完全加载
setTimeout(scanLatestMessages, 200);
}
});
// 开始观察
chatObserver.observe(chatContainer, {
childList: true,
subtree: true
});
console.log('已开始监听聊天室变化');
}
// 获取红包ID
function getRedPacketId(chatItem, redPacket) {
// 从聊天项ID获取
const chatId = chatItem.id;
if (chatId && chatId.startsWith('chatroom')) {
return chatId.replace('chatroom', '');
}
// 从红包点击事件获取
const onclick = redPacket.getAttribute('onclick');
if (onclick) {
const match = onclick.match(/unpackRedPacket\('([^']+)'\)/);
if (match) return match[1];
}
// 生成唯一ID
const user = getMessageUser(chatItem).name;
const time = getMessageTime(chatItem);
return `${user}_${time}_${Math.random().toString(36).substr(2, 9)}`;
}
// 获取消息时间
function getMessageTime(chatItem) {
const timeElement = chatItem.querySelector('.date-bar');
if (timeElement) {
return timeElement.textContent.trim();
}
return '';
}
// 获取消息用户
function getMessageUser(chatItem) {
const userElement = chatItem.querySelector('#userName .ft-gray');
const avatarElement = chatItem.querySelector('.avatar');
return {
name: userElement ? userElement.textContent.trim() : '匿名',
avatar: avatarElement ? avatarElement.style.backgroundImage : '',
level: chatItem.querySelector('.tip-wrapper') ? '高级用户' : '普通用户'
};
}
// 获取红包状态
function getRedPacketStatus(redPacket) {
const desc = redPacket.querySelector('.redPacketDesc');
if (!desc) return 'unknown';
const descText = desc.textContent.toLowerCase();
if (descText.includes('已经') || descText.includes('抢光') || descText.includes('抢完')) return 'empty';
if (descText.includes('已领取')) return 'opened';
if (descText.includes('未领取')) return 'available';
if (descText.includes('过期')) return 'expired';
return 'available';
}
// 删除指定红包
function delRedPacket(id) {
if (!CONFIG.autoDelRedPackets) {
return;
}
// 使用 querySelector
var element = document.querySelector('.red-packet-list .red-packet-item[data-packet-id="' + id +'"]');
if (element) {
// 删除红包
element.remove();
// 删除缓存数据
redPackets.delete(id);
displayOrder = displayOrder.filter(item => item == id);
currentDisplayed.delete(id);
//processedMessageIds.delete(id);
}
}
// 设置红包观察器
function setupRedPacketObserver(packetData) {
// 如果红包已抢光,则不设置观察器
if (packetData.status === 'empty') {
console.log(`红包 ${packetData.id} 已抢光,不设置观察器`);
//delRedPacket(packetData.id);
return;
}
// 清理旧的观察器
if (packetData.observer) {
packetData.observer.disconnect();
}
// 创建新的观察器来监听红包状态变化
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
mutations.forEach((mutation) => {
// 检查红包描述是否变化
if (mutation.type === 'childList' || mutation.type === 'characterData') {
const newStatus = getRedPacketStatus(packetData.originalRedPacket);
if (newStatus !== packetData.status) {
packetData.status = newStatus;
shouldUpdate = true;
// 如果红包被抢光,断开观察器以优化性能
if (newStatus === 'empty') {
//console.log(`红包 ${packetData.id} 已抢光,断开观察器`);
observer.disconnect();
packetData.observer = null;
observers.delete(packetData.id);
if (CONFIG.autoDelRedPackets) {
delRedPacket(packetData.id);
return;
}
}
}
}
// 检查类名或样式变化
if (mutation.type === 'attributes') {
shouldUpdate = true;
}
});
if (shouldUpdate) {
updateRedPacketItem(packetData.id);
packetData.lastUpdated = Date.now();
}
});
// 观察红包元素的子节点和属性变化
observer.observe(packetData.originalRedPacket, {
childList: true,
subtree: true,
characterData: true,
attributes: true,
attributeFilter: ['class', 'style', 'onclick']
});
packetData.observer = observer;
observers.set(packetData.id, observer);
}
// 更新显示顺序
function updateDisplayOrder() {
// 按时间排序(最新的在前)
displayOrder = Array.from(redPackets.keys()).sort((a, b) => {
const packetA = redPackets.get(a);
const packetB = redPackets.get(b);
const timeA = parseTimeString(packetA.time);
const timeB = parseTimeString(packetB.time);
return timeB - timeA; // 降序排序,最新的在前
});
}
// 辅助函数:将时间字符串转换为Date对象
function parseTimeString(timeStr) {
if (!timeStr) return new Date(0);
const match = timeStr.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
if (match) {
return new Date(match[0].replace(/-/g, '/'));
}
return new Date(0);
}
// 更新红包显示
function updateRedPacketDisplay() {
const listContainer = document.querySelector('.red-packet-list');
const countBadge = document.querySelector('.red-packet-count');
if (!listContainer || !countBadge) return;
// 更新计数
countBadge.textContent = displayOrder.length;
// 如果没有红包,显示提示
if (displayOrder.length === 0) {
listContainer.innerHTML = `
🎈 还没有红包消息
聊天室发红包后会在这里显示
`;
return;
}
// 显示最新的红包(新的在最上面)
const displayIds = displayOrder.slice(0, CONFIG.maxDisplayCount);
currentDisplayed = new Set(displayIds);
// 清空列表
listContainer.innerHTML = '';
// 按顺序添加红包,新的在最上面
displayIds.forEach(packetId => {
const packetData = redPackets.get(packetId);
if (!packetData) return;
const listItem = createRedPacketListItem(packetData);
listContainer.appendChild(listItem);
});
}
// 创建红包列表项
function createRedPacketListItem(packetData) {
// 创建容器
const container = document.createElement('div');
container.className = 'red-packet-item';
container.dataset.packetId = packetData.id;
// 根据是否为新红包设置样式
if (packetData.isNew) {
container.style.cssText = `
border: 2px solid #ff4757;
border-radius: 6px;
padding: 10px;
background: #fff;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
margin-bottom: 10px;
animation: newRedPacket 0.5s ease-out;
`;
// 添加新红包标记
const newBadge = document.createElement('div');
newBadge.style.cssText = `
position: absolute;
top: -6px;
right: -6px;
background: #ff4757;
color: white;
font-size: 10px;
padding: 2px 6px;
border-radius: 10px;
z-index: 2;
font-weight: bold;
`;
newBadge.textContent = 'NEW';
container.appendChild(newBadge);
} else {
container.style.cssText = `
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 10px;
background: #fff;
transition: all 0.2s ease;
position: relative;
overflow: hidden;
margin-bottom: 10px;
`;
}
// 状态指示器
const statusIndicator = document.createElement('div');
statusIndicator.className = 'status-indicator';
statusIndicator.style.cssText = `
position: absolute;
top: 0;
right: 0;
width: 30px;
height: 30px;
clip-path: polygon(100% 0, 100% 100%, 0 0);
z-index: 1;
`;
// 根据状态设置颜色
const statusColors = {
available: '#4CAF50',
opened: '#2196F3',
empty: '#FF9800',
expired: '#9E9E9E',
unknown: '#607D8B'
};
statusIndicator.style.background = statusColors[packetData.status] || '#607D8B';
container.appendChild(statusIndicator);
// 用户信息行
const userRow = document.createElement('div');
userRow.style.cssText = `
display: flex;
align-items: center;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
`;
// 头像
const avatar = document.createElement('div');
avatar.className = 'red-packet-avatar';
avatar.style.cssText = `
width: 24px;
height: 24px;
border-radius: 50%;
margin-right: 8px;
background-size: cover;
background-position: center;
`;
avatar.style.backgroundImage = packetData.user.avatar || 'none';
avatar.style.backgroundColor = packetData.user.avatar ? 'transparent' : '#ccc';
// 用户名
const userName = document.createElement('span');
userName.style.cssText = `
font-weight: bold;
color: #333;
font-size: 14px;
flex: 1;
`;
userName.textContent = packetData.user.name;
// 时间
const timeSpan = document.createElement('span');
timeSpan.style.cssText = `
font-size: 12px;
color: #888;
`;
timeSpan.textContent = packetData.time.split(' ')[0]; // 只显示日期部分
userRow.appendChild(avatar);
userRow.appendChild(userName);
userRow.appendChild(timeSpan);
// 红包内容(完整克隆)
const redPacketContent = packetData.element.querySelector('.chats__content').cloneNode(true);
// 调整红包内容样式
redPacketContent.style.cssText = `
margin: 0;
padding: 0;
transform: scale(0.85);
transform-origin: top left;
`;
// 更新点击事件,使其指向原红包
// const redPacketBtn = redPacketContent.querySelector('.hongbao__item');
// if (redPacketBtn && packetData.originalRedPacket) {
// const originalOnclick = packetData.originalRedPacket.getAttribute('onclick');
// if (originalOnclick) {
// redPacketBtn.setAttribute('onclick', originalOnclick);
//
// // 确保点击时触发原事件
// redPacketBtn.addEventListener('click', function(e) {
// e.stopPropagation();
// // 调用原红包的点击事件
// if (packetData.originalRedPacket) {
// packetData.originalRedPacket.click();
// }
// });
// }
// }
// 移除多余的操作按钮
const actionButtons = redPacketContent.querySelectorAll('.action__item, .fn__layer, details');
actionButtons.forEach(btn => btn.remove());
// 组装容器
container.appendChild(userRow);
container.appendChild(redPacketContent);
// 添加悬停效果
container.addEventListener('mouseenter', function() {
this.style.boxShadow = '0 4px 12px rgba(0,0,0,0.1)';
this.style.transform = 'translateY(-2px)';
});
container.addEventListener('mouseleave', function() {
this.style.boxShadow = '';
this.style.transform = '';
});
// 点击事件:高亮原聊天室中的红包
container.addEventListener('click', function(e) {
if (!e.target.closest('.hongbao__item')) {
highlightOriginalRedPacket(packetData.id);
}
});
return container;
}
// 更新单个红包项
function updateRedPacketItem(packetId) {
const packetData = redPackets.get(packetId);
if (!packetData) return;
// 重新克隆最新的红包元素
packetData.element = packetData.originalElement.cloneNode(true);
packetData.redPacketElement = packetData.element.querySelector('.hongbao__item');
packetData.status = getRedPacketStatus(packetData.originalRedPacket);
packetData.lastUpdated = Date.now();
// 如果这个红包正在显示中,更新显示
if (currentDisplayed.has(packetId)) {
const listContainer = document.querySelector('.red-packet-list');
if (!listContainer) return;
const existingItem = listContainer.querySelector(`[data-packet-id="${packetId}"]`);
if (existingItem) {
// 获取当前红包的位置
const allItems = Array.from(listContainer.children);
const currentIndex = allItems.findIndex(item =>
item.dataset.packetId === packetId
);
// 移除旧项
existingItem.remove();
// 创建新项
const newItem = createRedPacketListItem(packetData);
// 插入到相同位置(保持红包在列表中的位置不变)
if (currentIndex >= 0 && currentIndex < allItems.length - 1) {
listContainer.insertBefore(newItem, listContainer.children[currentIndex]);
} else {
listContainer.appendChild(newItem);
}
}
}
}
// 高亮原聊天室中的红包
function highlightOriginalRedPacket(packetId) {
const packetData = redPackets.get(packetId);
if (!packetData || !packetData.originalElement) return;
// 移除之前的高亮
document.querySelectorAll('.red-packet-highlight').forEach(el => {
el.classList.remove('red-packet-highlight');
});
// 添加高亮效果
packetData.originalElement.classList.add('red-packet-highlight');
// 滚动到原红包位置
packetData.originalElement.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
// 3秒后移除高亮
setTimeout(() => {
packetData.originalElement.classList.remove('red-packet-highlight');
}, 3000);
}
// 开始定时任务
function startTimers() {
// 定期全量扫描(间隔延长到10秒)
setInterval(scanRedPackets, CONFIG.refreshInterval);
// 定期同步红包状态
setInterval(syncRedPacketStates, CONFIG.syncInterval);
}
// 同步红包状态
function syncRedPacketStates() {
redPackets.forEach((packetData, packetId) => {
if (!packetData.originalElement || !packetData.originalElement.parentNode) {
// 原红包已不存在,清理
cleanupRedPacket(packetId);
return;
}
// 检查状态是否变化
const newStatus = getRedPacketStatus(packetData.originalRedPacket);
if (newStatus !== packetData.status) {
updateRedPacketItem(packetId);
// 如果红包被抢光,清理观察器
if (newStatus === 'empty' && packetData.observer) {
packetData.observer.disconnect();
packetData.observer = null;
observers.delete(packetId);
}
}
});
}
// 清理红包
function cleanupRedPacket(packetId) {
const packetData = redPackets.get(packetId);
if (packetData && packetData.observer) {
packetData.observer.disconnect();
}
redPackets.delete(packetId);
observers.delete(packetId);
currentDisplayed.delete(packetId);
processedMessageIds.delete(packetId);
// 从DOM中移除
const item = document.querySelector(`[data-packet-id="${packetId}"]`);
if (item) item.remove();
}
// 添加事件监听
function addEventListeners() {
// 监听红包面板的滚动事件
const panelBody = document.querySelector('.red-packet-body');
if (panelBody) {
panelBody.addEventListener('scroll', function() {
// 可以在这里添加懒加载等逻辑
});
}
}
// 浮窗状态存储
let isFloatingWindow = false;
let floatingWindowData = null;
// 切换浮窗模式
function toggleFloatingWindow(panel) {
if (isFloatingWindow) {
// 切换到停靠模式
restoreToDockedMode(panel);
} else {
// 切换到浮窗模式
switchToFloatingMode(panel);
}
isFloatingWindow = !isFloatingWindow;
updateFloatingButtonState();
}
// 切换到浮窗模式
function switchToFloatingMode(panel) {
// 保存原始位置信息
const parent = panel.parentNode;
const nextSibling = panel.nextSibling;
const originalStyle = panel.getAttribute('style');
floatingWindowData = {
parent: parent,
nextSibling: nextSibling,
originalStyle: originalStyle,
originalPosition: {
top: panel.offsetTop,
left: panel.offsetLeft
}
};
// 设置为浮窗样式
panel.style.cssText = `
position: fixed !important;
top: 100px !important;
right: 20px !important;
width: 320px !important;
z-index: 10000 !important;
background: #fff !important;
border: 2px solid #20c997 !important;
border-radius: 12px !important;
box-shadow: 0 10px 30px rgba(0,0,0,0.2) !important;
margin-bottom: 0 !important;
max-height: 70vh !important;
overflow: hidden !important;
resize: both !important;
min-width: 300px !important;
min-height: 200px !important;
`;
// 使面板可拖动
makePanelDraggable(panel);
// 添加到body
document.body.appendChild(panel);
//console.log('已切换到浮窗模式');
}
// 恢复到停靠模式
function restoreToDockedMode(panel) {
if (!floatingWindowData) return;
// 移除可拖动功能
panel.style.cursor = '';
panel.removeAttribute('data-dragging');
// 恢复原始样式
panel.style.cssText = floatingWindowData.originalStyle || `
margin-bottom: 15px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
background: #fff;
`;
// 恢复到原始位置
if (floatingWindowData.nextSibling) {
floatingWindowData.parent.insertBefore(panel, floatingWindowData.nextSibling);
} else {
floatingWindowData.parent.appendChild(panel);
}
//console.log('已恢复到停靠模式');
}
// 更新浮窗按钮状态
function updateFloatingButtonState() {
const floatingBtn = document.querySelector('.floating-window-btn');
if (!floatingBtn) return;
if (isFloatingWindow) {
floatingBtn.style.background = 'linear-gradient(135deg, #ff6b6b 0%, #ff8e53 100%) !important';
floatingBtn.style.boxShadow = '0 4px 15px rgba(255, 107, 107, 0.4) !important';
floatingBtn.title = '切换为停靠模式';
} else {
floatingBtn.style.background = 'linear-gradient(135deg, #2b8a3e 0%, #20c997 100%) !important';
floatingBtn.style.boxShadow = '0 4px 15px rgba(32, 201, 151, 0.4) !important';
floatingBtn.title = '切换为浮窗模式';
}
}
// 使面板可拖动
function makePanelDraggable(panel) {
let isDragging = false;
let dragOffsetX = 0;
let dragOffsetY = 0;
// 面板头部作为拖动区域
const header = panel.querySelector('.module-header');
if (!header) return;
header.style.cursor = 'move';
header.addEventListener('mousedown', startDrag);
function startDrag(e) {
isDragging = true;
const rect = panel.getBoundingClientRect();
dragOffsetX = e.clientX - rect.left;
dragOffsetY = e.clientY - rect.top;
panel.style.cursor = 'grabbing';
panel.setAttribute('data-dragging', 'true');
document.addEventListener('mousemove', doDrag);
document.addEventListener('mouseup', stopDrag);
e.preventDefault();
}
function doDrag(e) {
if (!isDragging) return;
// 计算新位置
const newLeft = e.clientX - dragOffsetX;
const newTop = e.clientY - dragOffsetY;
// 限制在可视区域内
const maxX = window.innerWidth - panel.offsetWidth;
const maxY = window.innerHeight - panel.offsetHeight;
panel.style.left = Math.max(0, Math.min(newLeft, maxX)) + 'px';
panel.style.top = Math.max(0, Math.min(newTop, maxY)) + 'px';
panel.style.right = 'auto';
}
function stopDrag() {
isDragging = false;
panel.style.cursor = '';
panel.removeAttribute('data-dragging');
document.removeEventListener('mousemove', doDrag);
document.removeEventListener('mouseup', stopDrag);
}
}
// 添加CSS样式
function addStyles() {
const style = document.createElement('style');
style.textContent = `
/* 红包面板样式 */
.red-packet-module {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.red-packet-body {
scrollbar-width: thin;
scrollbar-color: #ff6b6b #f0f0f0;
}
.red-packet-body::-webkit-scrollbar {
width: 6px;
}
.red-packet-body::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 3px;
}
.red-packet-body::-webkit-scrollbar-thumb {
background: #ff6b6b;
border-radius: 3px;
}
.red-packet-body::-webkit-scrollbar-thumb:hover {
background: #ff4757;
}
/* 红包项样式 */
.red-packet-item {
position: relative;
}
.red-packet-item:hover {
border-color: #ff6b6b;
}
.red-packet-avatar::before {
content: '';
display: block;
width: 100%;
height: 100%;
border-radius: 50%;
background: linear-gradient(45deg, #ff6b6b, #ff8e53);
opacity: 0.1;
}
/* 新红包动画 */
@keyframes newRedPacket {
0% {
transform: translateY(-20px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
/* 高亮效果 */
.red-packet-highlight {
animation: highlightPulse 1s ease-in-out 3;
border: 2px solid #ff6b6b !important;
}
@keyframes highlightPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.4); }
50% { box-shadow: 0 0 0 10px rgba(255, 107, 107, 0); }
}
/* 浮窗模式样式 */
.floating-window-mode {
z-index: 9999 !important;
}
.red-packet-module[data-dragging="true"] {
opacity: 0.9;
box-shadow: 0 15px 40px rgba(0,0,0,0.3) !important;
}
/* 响应式调整浮窗 */
@media (max-width: 768px) {
.red-packet-module.floating-window-mode {
width: 280px !important;
max-height: 60vh !important;
top: 50px !important;
right: 10px !important;
}
}
`;
document.head.appendChild(style);
}
// 页面加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
addStyles();
setTimeout(init, 1500);
});
} else {
addStyles();
setTimeout(init, 1500);
}
// 导出调试函数
window.RedPacketManager = {
rescan: function() {
scanRedPackets();
//console.log(`重新扫描,共发现 ${redPackets.size} 个红包`);
},
scanLatest: function() {
scanLatestMessages();
//console.log('扫描最新消息完成');
},
syncAll: function() {
syncRedPacketStates();
//console.log('已同步所有红包状态');
},
getStats: function() {
const stats = {
total: redPackets.size,
available: 0,
opened: 0,
empty: 0,
expired: 0,
observed: observers.size
};
redPackets.forEach(packet => {
stats[packet.status] = (stats[packet.status] || 0) + 1;
});
return stats;
},
getPacket: function(packetId) {
return redPackets.get(packetId);
},
cleanupObservers: function() {
let cleaned = 0;
redPackets.forEach((packet, id) => {
if (packet.status === 'empty' && packet.observer) {
packet.observer.disconnect();
packet.observer = null;
observers.delete(id);
cleaned++;
}
});
//console.log(`清理了 ${cleaned} 个已抢光红包的观察器`);
},
// 新增:设置过滤的红包类型
setFilterTypes: function(types) {
CONFIG.filterRedPacketTypes = Array.isArray(types) ? types : [];
console.log(`已设置过滤的红包类型: ${CONFIG.filterRedPacketTypes.join(', ')}`);
// 重新扫描以应用新的过滤规则
this.rescan();
},
// 新增:启用/禁用红包过滤
toggleFilter: function(enabled) {
CONFIG.enableRedPacketFilter = enabled;
console.log(`红包类型过滤已${enabled ? '启用' : '禁用'}`);
this.rescan();
},
// 新增:获取当前过滤的红包类型
getFilterTypes: function() {
return CONFIG.filterRedPacketTypes;
},
getConfig: function() {
return Object.assign({}, CONFIG);
},
setConfig: function(newConfig) {
Object.assign(CONFIG, newConfig);
saveConfigToStorage();
updatePanelStyles();
console.log('配置已更新');
},
openConfig: function() {
const configPanel = document.querySelector('.red-packet-config');
if (configPanel) {
initializeConfigForm(); // 刷新表单
configPanel.style.display = 'block';
}
},
closeConfig: function() {
const configPanel = document.querySelector('.red-packet-config');
if (configPanel) {
configPanel.style.display = 'none';
}
},
};
//console.log('红包同步脚本已加载,使用 RedPacketManager 进行调试');
// 创建综合配置面板
function createConfigPanel() {
// 如果已存在配置面板,则移除重建
const existingPanel = document.querySelector('.red-packet-config');
if (existingPanel) existingPanel.remove();
// 创建配置面板容器
const configPanel = document.createElement('div');
configPanel.className = 'red-packet-config';
configPanel.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 2px solid #ff6b6b;
border-radius: 12px;
padding: 20px;
z-index: 10002;
box-shadow: 0 10px 40px rgba(0,0,0,0.25);
width: 400px;
max-width: 90vw;
max-height: 80vh;
overflow-y: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
display: none;
`;
// 配置面板标题
configPanel.innerHTML = `
🎁 红包面板配置
🎨 外观设置
配置将保存在本地,刷新页面后仍然有效
`;
document.body.appendChild(configPanel);
// 初始化配置表单
initializeConfigForm();
// 事件监听
const closeBtn = configPanel.querySelector('#closeConfig');
const applyBtn = configPanel.querySelector('#applyConfig');
const resetBtn = configPanel.querySelector('#resetConfig');
closeBtn.addEventListener('click', function() {
configPanel.style.display = 'none';
});
applyBtn.addEventListener('click', applyConfig);
resetBtn.addEventListener('click', resetConfig);
// 点击外部关闭配置面板
configPanel.addEventListener('click', function(e) {
if (e.target === configPanel) {
configPanel.style.display = 'none';
}
});
// 添加配置按钮
const floatingBtn = document.querySelector('.floating-window-btn');
// 显示/隐藏配置面板
window.toggleRedPacketConfig = function() {
configPanel.style.display = configPanel.style.display === 'none' ? 'block' : 'none';
};
if (floatingBtn && floatingBtn.parentNode) {
// 在原有配置按钮的样式基础上添加:
const configBtn = document.createElement('button');
configBtn.innerHTML = '⚙️';
configBtn.title = '配置面板';
configBtn.style.cssText = `
width: 24px;
height: 24px;
border-radius: 50% !important;
background: linear-gradient(135deg, #6c5ce7 0%, #a29bfe 100%) !important;
color: white !important;
font-size: 12px !important;
cursor: pointer !important;
border: 3px solid white !important;
display: flex;
align-items: center;
justify-content: center;
margin-right: 5px;
padding: 0;
line-height: 1;
box-shadow: 0 4px 15px rgba(108, 92, 231, 0.4) !important;
`;
configBtn.addEventListener('click', function(e) {
e.stopPropagation();
window.toggleRedPacketConfig();
});
floatingBtn.parentNode.insertBefore(configBtn, floatingBtn);
}
return configPanel;
}
// 初始化配置表单
function initializeConfigForm() {
// 显示设置
document.getElementById('maxDisplayCount').value = CONFIG.maxDisplayCount;
document.getElementById('visibleCount').value = CONFIG.visibleCount;
document.getElementById('newMessageThreshold').value = CONFIG.newMessageThreshold;
// 功能设置
document.getElementById('autoScrollNew').checked = CONFIG.autoScrollNew;
document.getElementById('monitorNewMessages').checked = CONFIG.monitorNewMessages;
document.getElementById('autoDelRedPackets').checked = CONFIG.autoDelRedPackets;
// 红包类型过滤
document.getElementById('enableRedPacketFilter').checked = CONFIG.enableRedPacketFilter;
// 设置选中的红包类型
const typeCheckboxes = document.querySelectorAll('.redpacket-type');
typeCheckboxes.forEach(checkbox => {
checkbox.checked = CONFIG.filterRedPacketTypes.includes(checkbox.value);
});
// 外观设置
document.getElementById('panelPosition').value = CONFIG.position;
}
// 应用配置
function applyConfig() {
// 显示设置
CONFIG.maxDisplayCount = parseInt(document.getElementById('maxDisplayCount').value) || 20;
CONFIG.visibleCount = parseInt(document.getElementById('visibleCount').value) || 5;
CONFIG.newMessageThreshold = parseInt(document.getElementById('newMessageThreshold').value) || 5;
// 功能设置
CONFIG.autoScrollNew = document.getElementById('autoScrollNew').checked;
CONFIG.monitorNewMessages = document.getElementById('monitorNewMessages').checked;
CONFIG.autoDelRedPackets = document.getElementById('autoDelRedPackets').checked;
// 红包类型过滤
CONFIG.enableRedPacketFilter = document.getElementById('enableRedPacketFilter').checked;
CONFIG.filterRedPacketTypes = [];
document.querySelectorAll('.redpacket-type:checked').forEach(checkbox => {
CONFIG.filterRedPacketTypes.push(checkbox.value);
});
// 外观设置
CONFIG.position = document.getElementById('panelPosition').value;
// 保存到localStorage
saveConfigToStorage();
// 更新面板样式
updatePanelStyles();
// 重新扫描红包
if (RedPacketManager && typeof RedPacketManager.rescan === 'function') {
RedPacketManager.rescan();
}
// 显示成功消息
muliShowToast('配置已保存并应用');
// 关闭配置面板
const configPanel = document.querySelector('.red-packet-config');
if (configPanel) {
configPanel.style.display = 'none';
}
}
// 恢复默认配置
function resetConfig() {
// 默认配置
const defaultConfig = {
maxDisplayCount: 20,
visibleCount: 5,
refreshInterval: 10000,
syncInterval: 1000,
preserveOriginal: true,
position: 'above',
autoScrollNew: false,
monitorNewMessages: true,
newMessageThreshold: 5,
autoDelRedPackets: false,
enableRedPacketFilter: false,
filterRedPacketTypes: []
};
// 更新CONFIG
Object.assign(CONFIG, defaultConfig);
// 更新表单
initializeConfigForm();
// 保存到localStorage
saveConfigToStorage();
// 重新扫描红包
if (RedPacketManager && typeof RedPacketManager.rescan === 'function') {
RedPacketManager.rescan();
}
muliShowToast('已恢复默认配置');
}
// 保存配置到localStorage
function saveConfigToStorage() {
try {
// 只保存必要的配置项
const configToSave = {
maxDisplayCount: CONFIG.maxDisplayCount,
visibleCount: CONFIG.visibleCount,
autoScrollNew: CONFIG.autoScrollNew,
monitorNewMessages: CONFIG.monitorNewMessages,
newMessageThreshold: CONFIG.newMessageThreshold,
autoDelRedPackets: CONFIG.autoDelRedPackets,
enableRedPacketFilter: CONFIG.enableRedPacketFilter,
filterRedPacketTypes: CONFIG.filterRedPacketTypes,
position: CONFIG.position
};
localStorage.setItem('redPacketConfig', JSON.stringify(configToSave));
//console.log('配置已保存到localStorage');
} catch (error) {
//console.error('保存配置失败:', error);
}
}
// 从localStorage加载配置
function loadConfigFromStorage() {
try {
const savedConfig = localStorage.getItem('redPacketConfig');
if (savedConfig) {
const parsedConfig = JSON.parse(savedConfig);
// 更新CONFIG
Object.keys(parsedConfig).forEach(key => {
if (CONFIG.hasOwnProperty(key)) {
CONFIG[key] = parsedConfig[key];
}
});
//console.log('从localStorage加载配置成功');
return true;
}
} catch (error) {
//console.error('加载配置失败:', error);
}
return false;
}
// 更新面板样式
function updatePanelStyles() {
const redPacketBody = document.querySelector('.red-packet-body');
if (redPacketBody) {
redPacketBody.style.maxHeight = `${CONFIG.visibleCount * 120}px`;
}
}
// 配置面板的CSS样式(添加到addStyles函数中)
const configStyles = `
/* 配置面板样式 */
.red-packet-config {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, -48%); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
.config-section {
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
border-left: 4px solid #ff6b6b;
}
.config-item {
transition: all 0.2s ease;
}
.config-item:hover {
background: rgba(255, 107, 107, 0.05);
padding: 4px 8px;
border-radius: 4px;
}
input[type="number"]:focus,
select:focus {
outline: none;
border-color: #ff6b6b !important;
box-shadow: 0 0 0 2px rgba(255, 107, 107, 0.2);
}
#applyConfig:hover {
background: linear-gradient(135deg, #ff4757, #ff7b4a) !important;
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4) !important;
}
#resetConfig:hover {
background: #e0e0e0 !important;
transform: translateY(-1px);
}
/* 响应式调整 */
@media (max-width: 480px) {
.red-packet-config {
width: 95vw;
padding: 15px;
}
.config-section {
padding: 12px;
}
}
`;
// 将配置样式添加到现有的样式表中
const styleElement = document.createElement('style');
styleElement.textContent = configStyles;
document.head.appendChild(styleElement);
setTimeout(createConfigPanel, 3000);
/**
* 消息提示
* @param message
* @param duration
* @param type
*/
function muliShowToast(message, duration = 2000, type = 'info') {
const oldToast = document.getElementById('muli-toast');
if (oldToast) oldToast.remove();
const toast = document.createElement('div');
toast.id = 'muli-toast';
toast.innerHTML = message;
Object.assign(toast.style, {
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'rgba(0, 0, 0, 0.85)',
color: 'white',
padding: '14px 24px',
borderRadius: '8px',
fontSize: '14px',
fontWeight: '500',
zIndex: '999999',
textAlign: 'center',
maxWidth: '80%',
boxShadow: '0 6px 20px rgba(0,0,0,0.2)',
pointerEvents: 'none',
opacity: '0',
transition: 'all 0.3s ease'
});
const typeColors = {
success: '#51cf66',
info: '#339af0',
warning: '#ff922b',
error: '#ff6b6b'
};
toast.style.borderLeft = `4px solid ${typeColors[type] || typeColors.info}`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '1';
toast.style.transform = 'translate(-50%, -50%) scale(1.05)';
}, 10);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translate(-50%, -50%) scale(0.95)';
setTimeout(() => {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 300);
}, duration);
}
})();