// ==UserScript== // @name HDSky 体育沙龙面板 // @namespace http://tampermonkey.net/ // @version 5.2 // @description 在 HDSky 论坛页面右上角显示控制面板,自动高亮特殊关注用户的回复内容,支持快速翻页和收藏功能,可折叠面板,下拉加载翻页 // @author 江畔 | LOVE // @match https://hdsky.me/* // @match https://www.hdsky.me/* // @icon https://hdsky.me/favicon.ico // @grant GM_setValue // @grant GM_getValue // @charset UTF-8 // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // 配置管理 const Config = { // 获取配置 get(key, defaultValue) { return GM_getValue(key, defaultValue); }, // 设置配置 set(key, value) { GM_setValue(key, value); }, // 获取下拉加载开关状态 getAutoLoadEnabled() { return this.get('autoLoadEnabled', false); }, // 设置下拉加载开关状态 setAutoLoadEnabled(enabled) { this.set('autoLoadEnabled', enabled); }, // 获取面板展开状态 getPanelExpanded() { return this.get('panelExpanded', true); // 默认展开 }, // 设置面板展开状态 setPanelExpanded(expanded) { this.set('panelExpanded', expanded); }, // 获取检查有效下注开关状态 getCheckValidBetEnabled() { return this.get('checkValidBetEnabled', false); }, // 设置检查有效下注开关状态 setCheckValidBetEnabled(enabled) { this.set('checkValidBetEnabled', enabled); }, // 获取高亮特殊关注开关状态 getHighlightFollowEnabled() { return this.get('highlightFollowEnabled', true); // 默认开启 }, // 设置高亮特殊关注开关状态 setHighlightFollowEnabled(enabled) { this.set('highlightFollowEnabled', enabled); } }; // 从存储中获取特殊关注名单 function getSpecialFollowList() { const listStr = Config.get('specialFollowList', ''); if (!listStr) return []; return listStr.split(',').map(name => name.trim()).filter(name => name); } // 保存特殊关注名单到存储 function saveSpecialFollowList(list) { Config.set('specialFollowList', list.join(',')); } // 从存储中获取收藏列表 function getBookmarkList() { const listStr = Config.get('bookmarkList', '[]'); try { return JSON.parse(listStr); } catch (e) { return []; } } // 保存收藏列表到存储 function saveBookmarkList(list) { Config.set('bookmarkList', JSON.stringify(list)); } // 用户备注缓存,避免重复解析 let userNotesCache = null; // 获取用户备注映射 function getUserNotesMap() { if (userNotesCache) { return userNotesCache; } const notesStr = Config.get('userNotes', '{}'); try { userNotesCache = JSON.parse(notesStr) || {}; } catch (e) { userNotesCache = {}; } return userNotesCache; } // 保存用户备注映射 function saveUserNotesMap(notesMap) { userNotesCache = notesMap; Config.set('userNotes', JSON.stringify(notesMap)); } // 获取单个用户备注 function getUserNoteById(userId) { const notes = getUserNotesMap(); return notes[userId] || ''; } // 更新单个用户备注 function setUserNoteById(userId, note) { const notes = getUserNotesMap(); if (note) { notes[userId] = note; } else { delete notes[userId]; } saveUserNotesMap(notes); } // 创建控制面板 function createControlPanel() { // 创建容器,包含面板和折叠按钮 const container = document.createElement('div'); container.id = 'hdsky-panel-container'; container.style.cssText = ` position: fixed; top: 80px; right: 10px; display: flex; align-items: flex-start; z-index: 10000; `; // 创建折叠按钮 const toggleBtn = document.createElement('button'); toggleBtn.id = 'panel-toggle-btn'; toggleBtn.innerHTML = '◀'; toggleBtn.title = '收起面板'; toggleBtn.style.cssText = ` background: #e0e0e0; color: #666; border: none; border-radius: 4px 0 0 4px; width: 24px; height: 40px; cursor: pointer; font-size: 14px; transition: background 0.3s; margin-right: -2px; z-index: 1; touch-action: manipulation; -webkit-tap-highlight-color: transparent; `; toggleBtn.onmouseover = () => toggleBtn.style.background = '#d0d0d0'; toggleBtn.onmouseout = () => toggleBtn.style.background = '#e0e0e0'; // 支持移动端触摸事件 let touchStartTime = 0; toggleBtn.addEventListener('touchstart', (e) => { e.preventDefault(); touchStartTime = Date.now(); }, { passive: false }); toggleBtn.addEventListener('touchend', (e) => { e.preventDefault(); const touchDuration = Date.now() - touchStartTime; // 只有快速点击(小于300ms)才触发,避免与滚动冲突 if (touchDuration < 300) { togglePanel(); } }, { passive: false }); toggleBtn.addEventListener('click', (e) => { e.preventDefault(); togglePanel(); }); // 检测移动端并扩大按钮尺寸 const isMobile = window.innerWidth <= 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); if (isMobile) { toggleBtn.style.width = '72px'; // 24px * 3 toggleBtn.style.height = '120px'; // 40px * 3 toggleBtn.style.fontSize = '42px'; // 14px * 3 toggleBtn.style.borderRadius = '12px 0 0 12px'; // 4px * 3 } const panel = document.createElement('div'); panel.id = 'hdsky-special-follow-panel'; panel.style.cssText = ` background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 15px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); width: 220px; font-family: Arial, sans-serif; transition: all 0.3s ease; `; // 面板标题 const title = document.createElement('div'); title.textContent = '体育沙龙面板'; title.style.cssText = ` font-size: 16px; font-weight: bold; margin-bottom: 12px; color: #333; text-align: center; border-bottom: 1px solid #ddd; padding-bottom: 8px; `; panel.appendChild(title); // 按钮容器(统一管理所有按钮) const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: flex; flex-direction: column; gap: 10px; `; // 关注列表按钮 const followListBtn = document.createElement('button'); followListBtn.id = 'follow-list-btn'; followListBtn.textContent = '关注列表'; followListBtn.style.cssText = ` padding: 10px 15px; background: #2196F3; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; width: 100%; `; followListBtn.title = '点击编辑特殊关注名单'; followListBtn.onmouseover = () => followListBtn.style.background = '#0b7dda'; followListBtn.onmouseout = () => followListBtn.style.background = '#2196F3'; followListBtn.onclick = handleFollowListClick; buttonContainer.appendChild(followListBtn); // 下拉加载翻页按钮 const autoLoadBtn = document.createElement('button'); autoLoadBtn.id = 'auto-load-btn'; autoLoadBtn.style.cssText = ` padding: 10px 15px; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; width: 100%; `; autoLoadBtn.onclick = toggleAutoLoadFromPanel; updateAutoLoadButton(autoLoadBtn); // 在设置基础样式后再更新按钮状态 buttonContainer.appendChild(autoLoadBtn); // 检查有效下注按钮 const checkValidBetBtn = document.createElement('button'); checkValidBetBtn.id = 'check-valid-bet-btn'; checkValidBetBtn.style.cssText = ` padding: 10px 15px; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; width: 100%; `; checkValidBetBtn.onclick = toggleCheckValidBet; updateCheckValidBetButton(checkValidBetBtn); buttonContainer.appendChild(checkValidBetBtn); // 高亮特殊关注按钮 const highlightFollowBtn = document.createElement('button'); highlightFollowBtn.id = 'highlight-follow-btn'; highlightFollowBtn.style.cssText = ` padding: 10px 15px; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; width: 100%; `; highlightFollowBtn.onclick = toggleHighlightFollow; updateHighlightFollowButton(highlightFollowBtn); buttonContainer.appendChild(highlightFollowBtn); // 收藏功能按钮容器 const bookmarkContainer = document.createElement('div'); bookmarkContainer.id = 'bookmark-container'; bookmarkContainer.style.cssText = ` display: flex; flex-direction: row; gap: 10px; `; // 收藏按钮 const bookmarkBtn = document.createElement('button'); bookmarkBtn.textContent = '收藏'; bookmarkBtn.id = 'bookmark-btn'; bookmarkBtn.style.cssText = ` padding: 8px 12px; background: #2196F3; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 13px; font-weight: bold; transition: background 0.3s; flex: 1; `; bookmarkBtn.onmouseover = () => bookmarkBtn.style.background = '#0b7dda'; bookmarkBtn.onmouseout = () => bookmarkBtn.style.background = '#2196F3'; bookmarkBtn.onclick = addBookmark; bookmarkContainer.appendChild(bookmarkBtn); // 收藏夹按钮 const bookmarkListBtn = document.createElement('button'); bookmarkListBtn.textContent = '收藏夹'; bookmarkListBtn.id = 'bookmark-list-btn'; bookmarkListBtn.style.cssText = ` padding: 8px 12px; background: #2196F3; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 13px; font-weight: bold; transition: background 0.3s; flex: 1; `; bookmarkListBtn.onmouseover = () => bookmarkListBtn.style.background = '#0b7dda'; bookmarkListBtn.onmouseout = () => bookmarkListBtn.style.background = '#2196F3'; bookmarkListBtn.onclick = showBookmarkList; bookmarkContainer.appendChild(bookmarkListBtn); buttonContainer.appendChild(bookmarkContainer); // 数据分析按钮 const dataAnalysisBtn = document.createElement('button'); dataAnalysisBtn.id = 'data-analysis-btn'; dataAnalysisBtn.textContent = '数据分析'; dataAnalysisBtn.style.cssText = ` padding: 10px 15px; background: #4caf50; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; width: 100%; `; dataAnalysisBtn.onmouseover = () => dataAnalysisBtn.style.background = '#388e3c'; dataAnalysisBtn.onmouseout = () => dataAnalysisBtn.style.background = '#4caf50'; dataAnalysisBtn.onclick = openDataAnalysisDialog; buttonContainer.appendChild(dataAnalysisBtn); panel.appendChild(buttonContainer); // 将按钮和面板添加到容器 container.appendChild(toggleBtn); container.appendChild(panel); // 添加到页面 document.body.appendChild(container); // 应用保存的面板状态 const isPanelExpanded = Config.getPanelExpanded(); if (!isPanelExpanded) { // 如果保存的是收起状态,则收起面板 panel.style.display = 'none'; toggleBtn.innerHTML = '▶'; toggleBtn.title = '展开面板'; toggleBtn.style.borderRadius = '4px'; } } // 切换面板显示/隐藏 function togglePanel() { const panel = document.getElementById('hdsky-special-follow-panel'); const toggleBtn = document.getElementById('panel-toggle-btn'); if (panel.style.display === 'none') { // 展开面板 panel.style.display = 'block'; toggleBtn.innerHTML = '◀'; toggleBtn.title = '收起面板'; toggleBtn.style.borderRadius = '4px 0 0 4px'; Config.setPanelExpanded(true); // 保存展开状态 } else { // 收起面板 panel.style.display = 'none'; toggleBtn.innerHTML = '▶'; toggleBtn.title = '展开面板'; toggleBtn.style.borderRadius = '4px'; Config.setPanelExpanded(false); // 保存收起状态 } } // 更新下拉加载按钮的显示 function updateAutoLoadButton(button) { const isEnabled = Config.getAutoLoadEnabled(); button.style.background = '#2196F3'; // 统一使用蓝色 button.onmouseover = () => button.style.background = '#0b7dda'; button.onmouseout = () => button.style.background = '#2196F3'; if (isEnabled) { button.textContent = '✅ 下拉加载翻页'; button.title = '点击关闭下拉加载翻页功能'; } else { button.textContent = '❌ 下拉加载翻页'; button.title = '点击开启下拉加载翻页功能'; } } // 从面板切换下拉加载功能 function toggleAutoLoadFromPanel() { const currentState = Config.getAutoLoadEnabled(); const newState = !currentState; Config.setAutoLoadEnabled(newState); // 更新按钮显示 const button = document.getElementById('auto-load-btn'); if (button) { updateAutoLoadButton(button); } // 如果是开启,立即初始化功能 if (newState) { autoLoadNextPage(); } else { // 如果是关闭,需要刷新页面以移除事件监听器 if (confirm('需要刷新页面以完全关闭下拉加载功能,是否立即刷新?')) { location.reload(); } } } // 更新检查有效下注按钮的显示 function updateCheckValidBetButton(button) { const isEnabled = Config.getCheckValidBetEnabled(); button.style.background = '#2196F3'; // 统一使用蓝色 button.onmouseover = () => button.style.background = '#0b7dda'; button.onmouseout = () => button.style.background = '#2196F3'; if (isEnabled) { button.textContent = '✅ 检查有效下注'; button.title = '点击关闭检查有效下注功能\n如果超过截止时间或重复下注将禁用回复功能'; } else { button.textContent = '❌ 检查有效下注'; button.title = '点击开启检查有效下注功能\n如果超过截止时间或重复下注将禁用回复功能'; } } // 从面板切换检查有效下注功能 function toggleCheckValidBet() { const currentState = Config.getCheckValidBetEnabled(); const newState = !currentState; Config.setCheckValidBetEnabled(newState); // 更新按钮显示 const button = document.getElementById('check-valid-bet-btn'); if (button) { updateCheckValidBetButton(button); } // 如果关闭功能,恢复回复表单 if (!newState) { enableReplyForm(); } else { // 如果开启功能,重新检查并应用 checkValidBetAndDisableReply(); } } // 更新高亮特殊关注按钮的显示 function updateHighlightFollowButton(button) { const isEnabled = Config.getHighlightFollowEnabled(); button.style.background = '#2196F3'; // 统一使用蓝色 button.onmouseover = () => button.style.background = '#0b7dda'; button.onmouseout = () => button.style.background = '#2196F3'; if (isEnabled) { button.textContent = '✅ 高亮特殊关注'; button.title = '点击关闭高亮特殊关注功能'; } else { button.textContent = '❌ 高亮特殊关注'; button.title = '点击开启高亮特殊关注功能'; } } // 从面板切换高亮特殊关注功能 function toggleHighlightFollow() { const currentState = Config.getHighlightFollowEnabled(); const newState = !currentState; Config.setHighlightFollowEnabled(newState); // 更新按钮显示 const button = document.getElementById('highlight-follow-btn'); if (button) { updateHighlightFollowButton(button); } // 如果关闭功能,清除所有高亮 if (!newState) { clearHighlights(); } else { // 如果开启功能,重新应用高亮 autoHighlightFollowedPosts(); } } // 获取当前用户名 function getCurrentUsername() { const infoBlock = document.getElementById('info_block'); if (!infoBlock) return null; // 查找包含用户名的链接(userdetails.php?id=xxx) const userLink = infoBlock.querySelector('a[href*="userdetails.php?id"]'); if (!userLink) return null; // 提取用户名文本(去除HTML标签) const username = userLink.textContent.trim(); return username || null; } // 检查给定文档中当前用户是否已经下注(已回帖) function hasUserRepliedOnDocument(targetDoc, currentUsername) { if (!targetDoc || !currentUsername) return false; const userLinks = targetDoc.querySelectorAll('a[href*="userdetails.php?id"]'); for (let link of userLinks) { if (link.closest('#info_block')) continue; const username = link.textContent.trim(); if (username === currentUsername) { const postDiv = link.closest('div[style*="margin-top: 8pt"]'); if (postDiv) { return true; } } } return false; } // 获取当前主题所有页面的URL function getTopicPageUrls() { const urls = new Set(); // 从当前URL中提取topicid和基础URL const currentUrlObj = new URL(window.location.href); const topicid = currentUrlObj.searchParams.get('topicid'); if (!topicid) { console.log('未找到topicid参数'); return []; } // 构建基础URL(不包含page参数,保留forumid以匹配当前页面) const forumid = currentUrlObj.searchParams.get('forumid'); const baseParams = new URLSearchParams(); baseParams.set('action', 'viewtopic'); if (forumid) { baseParams.set('forumid', forumid); } baseParams.set('topicid', topicid); const baseUrl = `${currentUrlObj.origin}${currentUrlObj.pathname}?${baseParams.toString()}`; // 添加第一页(page=0或没有page参数) urls.add(baseUrl); // 从分页链接中提取所有页码 const pageNumbers = new Set(); const currentPageParam = currentUrlObj.searchParams.get('page'); const currentPage = currentPageParam ? parseInt(currentPageParam, 10) : 0; pageNumbers.add(0); // 第一页 pageNumbers.add(isNaN(currentPage) ? 0 : currentPage); // 当前页 // 查找所有包含page参数的分页链接 const pageLinks = document.querySelectorAll('a[href*="viewtopic"][href*="topicid="]'); pageLinks.forEach(link => { const href = link.getAttribute('href'); if (!href) return; let pageNum = null; // 先尝试用正则表达式提取(适用于相对路径和绝对路径) const match = href.match(/[?&]page=(\d+)/); if (match && match[1]) { pageNum = parseInt(match[1], 10); } else { // 如果正则没匹配到,尝试用URL对象解析 try { const linkUrl = new URL(href, window.location.href); const pageParam = linkUrl.searchParams.get('page'); if (pageParam !== null) { pageNum = parseInt(pageParam, 10); } } catch (e) { // 解析失败,跳过 } } if (pageNum !== null && !isNaN(pageNum)) { pageNumbers.add(pageNum); } }); // 构建所有页面的URL pageNumbers.forEach(pageNum => { if (pageNum === 0) { // 第一页:不添加page参数 urls.add(baseUrl); } else { // 其他页:添加page参数 urls.add(`${baseUrl}&page=${pageNum}`); } }); console.log('找到的分页URL:', Array.from(urls)); return Array.from(urls); } // 标准化URL用于比较(只保留topicid与page,忽略参数顺序) function normalizeUrlForCompare(url) { try { const urlObj = new URL(url, window.location.origin); const topicid = urlObj.searchParams.get('topicid') || ''; const page = urlObj.searchParams.get('page') || '0'; const path = urlObj.pathname || '/forums.php'; return `${urlObj.origin}${path}?topicid=${topicid}&page=${page}`; } catch (e) { return url.split('#')[0]; } } // 获取当前主题各分页的文档内容 async function fetchTopicDocuments() { const pageUrls = getTopicPageUrls(); if (pageUrls.length === 0) { return []; } const parser = new DOMParser(); const currentNormalized = normalizeUrlForCompare(window.location.href); const documents = []; for (let url of pageUrls) { const normalizedUrl = normalizeUrlForCompare(url); const isCurrentPage = normalizedUrl === currentNormalized; if (isCurrentPage) { documents.unshift({ url, doc: document, isCurrentPage: true }); continue; } try { const response = await fetch(url, { credentials: 'include', headers: { 'Accept': 'text/html' } }); if (!response.ok) { console.error('获取分页失败:', url, response.status); continue; } const html = await response.text(); const doc = parser.parseFromString(html, 'text/html'); documents.push({ url, doc, isCurrentPage: false }); } catch (error) { console.error('请求分页发生错误:', url, error); } } return documents; } // 检查当前用户是否已经在任意分页下注 async function hasUserReplied() { const currentUsername = getCurrentUsername(); if (!currentUsername) { console.log('未获取到当前用户名'); return false; } const topicDocuments = await fetchTopicDocuments(); console.log('开始检查分页下注情况,共', topicDocuments.length, '页'); for (let entry of topicDocuments) { if (!entry.doc) continue; if (hasUserRepliedOnDocument(entry.doc, currentUsername)) { console.log(`检测到用户在${entry.isCurrentPage ? '当前页' : '其他分页'}已下注:`, currentUsername, entry.url); return true; } else { console.log('该分页未检测到下注:', entry.url); } } console.log('所有分页检查完成,未发现重复下注'); return false; } // 禁用快速回复表单并显示提示(公共函数) function disableReplyForm(message, noticeClass, backgroundColor) { // 查找快速回复表单 const composeForm = document.getElementById('compose'); if (!composeForm) return false; // 查找包含快速回复的 table const replyTable = composeForm.closest('table'); if (!replyTable) return false; // 禁用所有表单元素 const formElements = composeForm.querySelectorAll('textarea, input[type="submit"], button'); formElements.forEach(element => { element.disabled = true; element.style.opacity = '0.5'; element.style.cursor = 'not-allowed'; }); // 检查是否已经添加过提示 if (!replyTable.querySelector('.' + noticeClass)) { // 在表单顶部添加提示 const notice = document.createElement('div'); notice.className = noticeClass; notice.style.cssText = ` background: ${backgroundColor}; color: white; padding: 10px 15px; margin: 10px 0; border-radius: 5px; text-align: center; font-weight: bold; font-size: 14px; `; notice.textContent = message; // 在"快速回复"标题后插入提示 const quickReplyTitle = Array.from(replyTable.querySelectorAll('b')).find(b => b.textContent === '快速回复'); if (quickReplyTitle && quickReplyTitle.parentElement) { quickReplyTitle.parentElement.appendChild(notice); } } return true; } // 恢复快速回复表单(移除禁用状态和提示) function enableReplyForm() { // 查找快速回复表单 const composeForm = document.getElementById('compose'); if (!composeForm) return false; // 查找包含快速回复的 table const replyTable = composeForm.closest('table'); if (!replyTable) return false; // 恢复所有表单元素 const formElements = composeForm.querySelectorAll('textarea, input[type="submit"], button'); formElements.forEach(element => { element.disabled = false; element.style.opacity = ''; element.style.cursor = ''; }); // 移除所有提示信息 const deadlineNotice = replyTable.querySelector('.deadline-notice'); if (deadlineNotice) { deadlineNotice.remove(); } const duplicateBetNotice = replyTable.querySelector('.duplicate-bet-notice'); if (duplicateBetNotice) { duplicateBetNotice.remove(); } console.log('已恢复快速回复表单'); return true; } // 检查有效下注并禁用回复(合并截止时间和重复下注检查) async function checkValidBetAndDisableReply() { // 只在开关开启时检查 if (!Config.getCheckValidBetEnabled()) { return; } // 1. 检查截止时间 const topSpan = document.getElementById('top'); if (topSpan) { const titleText = topSpan.textContent; // 匹配格式:截止时间:2025-11-13 21:00 const deadlineMatch = titleText.match(/截止时间[::]\s*(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})/); if (deadlineMatch) { const deadlineStr = deadlineMatch[1]; // 解析截止时间(北京时间) const deadlineDate = new Date(deadlineStr.replace(' ', 'T') + ':00+08:00'); // 获取当前时间 const now = new Date(); // 如果截止时间已过 if (now > deadlineDate) { if (disableReplyForm('⚠️ 投注截止时间已过,快速回复已禁用', 'deadline-notice', '#ff5252')) { console.log('截止时间已过,已禁用快速回复面板'); } return; // 截止时间已过,不再检查重复下注 } } } // 2. 检查是否已下注(重复下注) if (await hasUserReplied()) { if (disableReplyForm('⚠️ 您已经下注,禁止重复下注', 'duplicate-bet-notice', '#ff9800')) { console.log('检测到重复下注,已禁用快速回复面板'); } } } const ENABLE_SPECIAL_TILES = false; // 全局变量:高亮帖子列表 let highlightedPosts = []; // 添加收藏 function addBookmark() { const currentUrl = window.location.href; let currentTitle = document.title || '未命名页面'; // 从标题中提取引号里的内容 const match = currentTitle.match(/"([^"]+)"/); if (match && match[1]) { currentTitle = match[1]; } else { // 如果没有引号,则删掉常见的前后缀 currentTitle = currentTitle.replace(/^HDSky :: 查看主题\s+/i, ''); currentTitle = currentTitle.replace(/^HDSky :: /i, ''); currentTitle = currentTitle.replace(/\s*高清视界.*$/i, ''); currentTitle = currentTitle.replace(/\s*-\s*Powered by.*$/i, ''); } // 获取现有收藏列表 const bookmarks = getBookmarkList(); // 检查是否已经收藏 const exists = bookmarks.some(b => b.url === currentUrl); if (exists) { alert('该页面已经在收藏夹中了!'); return; } // 添加新收藏 bookmarks.push({ url: currentUrl, title: currentTitle, time: new Date().toLocaleString() }); // 保存 saveBookmarkList(bookmarks); alert('收藏成功!\n标题:' + currentTitle); } // 显示收藏夹 function showBookmarkList() { const bookmarks = getBookmarkList(); // 移除旧的收藏夹窗口(如果存在) const oldDialog = document.getElementById('bookmark-dialog'); if (oldDialog) { oldDialog.remove(); } // 创建遮罩层 const overlay = document.createElement('div'); overlay.id = 'bookmark-dialog'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10003; display: flex; justify-content: center; align-items: center; `; // 创建弹窗 const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; border-radius: 10px; padding: 20px; width: 700px; max-width: 95vw; max-height: 85vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3); `; // 标题栏 const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #2196F3; background: transparent; `; const title = document.createElement('h2'); title.textContent = '我的收藏夹'; title.style.cssText = ` margin: 0; padding: 0; color: #2196F3; font-size: 20px; background: transparent; border: none; outline: none; `; header.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.style.cssText = ` background: #f44336; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 18px; font-weight: bold; transition: background 0.3s; `; closeBtn.onmouseover = () => closeBtn.style.background = '#d32f2f'; closeBtn.onmouseout = () => closeBtn.style.background = '#f44336'; closeBtn.onclick = () => overlay.remove(); header.appendChild(closeBtn); dialog.appendChild(header); // 收藏列表 if (bookmarks.length === 0) { const emptyMsg = document.createElement('div'); emptyMsg.textContent = '收藏夹还是空的,快去收藏喜欢的页面吧!'; emptyMsg.style.cssText = ` text-align: center; color: #999; padding: 40px 20px; font-size: 14px; background: #f9f9f9; border-radius: 5px; margin-top: 10px; `; dialog.appendChild(emptyMsg); } else { bookmarks.forEach((bookmark, index) => { const item = createBookmarkItem(bookmark, index); dialog.appendChild(item); }); } overlay.appendChild(dialog); document.body.appendChild(overlay); // 点击遮罩层关闭 overlay.onclick = (e) => { if (e.target === overlay) { overlay.remove(); } }; } // 创建收藏项 function createBookmarkItem(bookmark, index) { const item = document.createElement('div'); item.style.cssText = ` display: flex; justify-content: space-between; align-items: center; padding: 12px; margin-bottom: 10px; background: #fafafa; border: 1px solid #e0e0e0; border-radius: 5px; transition: all 0.3s; `; item.onmouseover = () => { item.style.background = '#e3f2fd'; item.style.borderColor = '#2196F3'; item.style.transform = 'translateX(5px)'; item.style.boxShadow = '0 2px 8px rgba(33, 150, 243, 0.2)'; }; item.onmouseout = () => { item.style.background = '#fafafa'; item.style.borderColor = '#e0e0e0'; item.style.transform = 'translateX(0)'; item.style.boxShadow = 'none'; }; // 左侧内容区 const content = document.createElement('div'); content.style.cssText = ` flex: 1; cursor: pointer; overflow: hidden; `; content.onclick = () => window.location.href = bookmark.url; const titleDiv = document.createElement('div'); titleDiv.textContent = bookmark.title; titleDiv.style.cssText = ` font-size: 14px; font-weight: bold; color: #2196F3; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; `; content.appendChild(titleDiv); const timeDiv = document.createElement('div'); timeDiv.textContent = '收藏时间: ' + bookmark.time; timeDiv.style.cssText = ` font-size: 12px; color: #999; `; content.appendChild(timeDiv); item.appendChild(content); // 删除按钮 const deleteBtn = document.createElement('button'); deleteBtn.textContent = '✕'; deleteBtn.style.cssText = ` background: #ff5722; color: white; border: none; border-radius: 50%; width: 25px; height: 25px; cursor: pointer; font-size: 14px; font-weight: bold; transition: background 0.3s; margin-left: 10px; `; deleteBtn.onmouseover = () => deleteBtn.style.background = '#e64a19'; deleteBtn.onmouseout = () => deleteBtn.style.background = '#ff5722'; deleteBtn.onclick = (e) => { e.stopPropagation(); if (confirm('确定要删除这个收藏吗?\n' + bookmark.title)) { deleteBookmark(index); showBookmarkList(); // 刷新列表 } }; item.appendChild(deleteBtn); return item; } // 删除收藏 function deleteBookmark(index) { const bookmarks = getBookmarkList(); bookmarks.splice(index, 1); saveBookmarkList(bookmarks); } // 处理关注列表点击(编辑) function handleFollowListClick() { const currentList = getSpecialFollowList(); const currentStr = currentList.join(','); const input = prompt('请输入特殊关注名单(用逗号分隔):\n例如: DFBCOLD19,李知恩', currentStr); if (input !== null) { // 用户点击了确定(包括空字符串) const newList = input.split(',').map(name => name.trim()).filter(name => name); saveSpecialFollowList(newList); alert('特殊关注名单已更新!\n当前关注: ' + (newList.length > 0 ? newList.join(', ') : '无')); // 重新应用高亮 autoHighlightFollowedPosts(); renderSpecialFollowTiles(); } } // 自动高亮特殊关注用户的帖子(页面加载时调用) function autoHighlightFollowedPosts() { // 如果开关关闭,清除高亮并退出 if (!Config.getHighlightFollowEnabled()) { clearHighlights(); return; } const followList = getSpecialFollowList(); // 如果没有关注名单,不执行高亮 if (followList.length === 0) { return; } // 先清除之前的高亮 clearHighlights(); // 找到所有回复帖子(每个帖子在一个带有 margin-top 和 margin-bottom 的 div 中) const allPosts = document.querySelectorAll('div[style*="margin-top: 8pt"]'); // 重置高亮列表 highlightedPosts = []; allPosts.forEach(post => { // 在帖子中查找用户名链接 const userLinks = post.querySelectorAll('a[href*="userdetails.php"]'); let isFollowedUser = false; userLinks.forEach(link => { const username = link.textContent.trim(); // 检查是否在关注列表中 if (followList.some(followName => username === followName)) { isFollowedUser = true; } }); if (isFollowedUser) { // 高亮显示关注用户的信息div post.style.background = '#fffacd'; post.style.border = '2px solid #ffd700'; post.style.borderRadius = '5px'; post.style.padding = '5px'; // 找到并高亮div内部的table const innerTables = post.querySelectorAll('table'); innerTables.forEach(table => { table.style.background = '#fff8dc'; table.style.border = '2px solid #ffb700'; }); // 标记为已高亮 post.dataset.highlighted = 'true'; // 找到并高亮紧跟在div后面的回复内容table(class="main") let nextElement = post.nextElementSibling; if (nextElement && nextElement.tagName === 'TABLE' && nextElement.classList.contains('main')) { nextElement.style.background = '#fff8dc'; nextElement.style.border = '2px solid #ffb700'; nextElement.style.borderRadius = '5px'; // 标记这个table也被高亮了 nextElement.dataset.highlightedContent = 'true'; } // 添加到高亮列表 highlightedPosts.push(post); } }); } // 清除所有高亮 function clearHighlights() { // 找到所有被高亮的帖子div const posts = document.querySelectorAll('div[data-highlighted="true"]'); posts.forEach(post => { // 清除div的高亮样式 post.style.background = ''; post.style.border = ''; post.style.borderRadius = ''; post.style.padding = ''; post.removeAttribute('data-highlighted'); // 清除div内部table的高亮样式 const tables = post.querySelectorAll('table'); tables.forEach(table => { table.style.background = ''; table.style.border = ''; }); }); // 清除所有被高亮的回复内容table const contentTables = document.querySelectorAll('table[data-highlighted-content="true"]'); contentTables.forEach(table => { table.style.background = ''; table.style.border = ''; table.style.borderRadius = ''; table.removeAttribute('data-highlighted-content'); }); // 重置全局变量 highlightedPosts = []; } // 移除特殊关注磁贴容器 function removeSpecialFollowTiles() { const existing = document.getElementById('special-follow-tile-container'); if (existing) { existing.remove(); } } // 记录文档中特殊关注用户的回帖信息 function recordFollowRepliesFromDocument(doc, pageUrl, followSet, collectedMap, isCurrentPage) { if (!doc) return; const posts = doc.querySelectorAll('div[style*="margin-top: 8pt"]'); posts.forEach(post => { // 查找帖子中的所有用户名链接 const userLinks = post.querySelectorAll('a[href*="userdetails.php?id"]'); userLinks.forEach(link => { const username = link.textContent.trim(); if (!username || !followSet.has(username) || collectedMap.has(username)) { return; } // 查找带有 pid 的元素,用于定位锚点 let anchorId = ''; const pidElement = post.querySelector('[id^="pid"]') || post.querySelector('table[id^="pid"]'); if (pidElement && pidElement.id) { anchorId = pidElement.id; } else { const pidLink = post.querySelector('a[href*="#pid"]'); if (pidLink) { const hashMatch = pidLink.getAttribute('href').match(/#(pid\d+)/); if (hashMatch && hashMatch[1]) { anchorId = hashMatch[1]; } } } let targetUrl = pageUrl; const anchorNoHash = anchorId ? anchorId.replace('#', '') : ''; if (anchorNoHash) { try { const urlObj = new URL(pageUrl); urlObj.hash = `#${anchorNoHash}`; targetUrl = urlObj.href; } catch (e) { targetUrl = `${pageUrl.split('#')[0]}#${anchorNoHash}`; } } collectedMap.set(username, { username, targetUrl, anchorId: anchorNoHash, isOnCurrentPage: isCurrentPage && !!(anchorNoHash && document.getElementById(anchorNoHash)), pageUrl }); }); }); } // 收集特殊关注用户的回帖数据(遍历所有分页) async function collectSpecialFollowReplies(followList) { const followSet = new Set(followList); const collectedMap = new Map(); const topicDocuments = await fetchTopicDocuments(); for (let entry of topicDocuments) { if (!entry.doc) continue; recordFollowRepliesFromDocument( entry.doc, entry.url, followSet, collectedMap, entry.isCurrentPage ); if (collectedMap.size === followSet.size) { break; // 已经找到所有关注用户 } } return Array.from(collectedMap.values()); } // 点击磁贴时滚动或跳转 function handleFollowTileClick(tileInfo) { if (tileInfo.isOnCurrentPage && tileInfo.anchorId) { const target = document.getElementById(tileInfo.anchorId); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); target.style.boxShadow = '0 0 10px 2px rgba(33,150,243,0.6)'; setTimeout(() => { target.style.boxShadow = ''; }, 2000); return; } } // 如果不在当前页,跳转到对应链接 window.location.href = tileInfo.targetUrl; } // 渲染特殊关注磁贴 async function renderSpecialFollowTiles() { if (!ENABLE_SPECIAL_TILES) { removeSpecialFollowTiles(); return; } removeSpecialFollowTiles(); const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('action') !== 'viewtopic') { return; } const followList = getSpecialFollowList(); if (followList.length === 0) { return; } const tileData = await collectSpecialFollowReplies(followList); if (tileData.length === 0) { return; } const container = document.createElement('div'); container.id = 'special-follow-tile-container'; container.style.cssText = ` position: fixed; right: 10px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 10px; z-index: 10000; `; tileData.forEach(info => { const tile = document.createElement('div'); tile.className = 'special-follow-tile'; tile.textContent = info.username; tile.style.cssText = ` background: #fff; border: 1px solid #2196F3; border-radius: 8px; padding: 10px 14px; font-size: 13px; font-weight: bold; color: #2196F3; cursor: pointer; box-shadow: 0 2px 6px rgba(0,0,0,0.15); transition: transform 0.2s, box-shadow 0.2s; min-width: 120px; text-align: center; `; tile.onmouseover = () => { tile.style.transform = 'translateX(-4px)'; tile.style.boxShadow = '0 4px 10px rgba(33,150,243,0.3)'; }; tile.onmouseout = () => { tile.style.transform = 'translateX(0)'; tile.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)'; }; tile.onclick = () => handleFollowTileClick(info); container.appendChild(tile); }); document.body.appendChild(container); } // 自动加载下一页功能 let isLoadingNextPage = false; const scrollThreshold = 800; // 距离底部多少像素时触发加载 function autoLoadNextPage() { if (!Config.getAutoLoadEnabled()) { return; } // 检查URL中是否包含topicid参数 const urlParams = new URLSearchParams(window.location.search); if (!urlParams.has('topicid')) { return; } let lastScrollTop = 0; window.addEventListener('scroll', function() { const scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop; const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight; const clientHeight = document.documentElement.clientHeight || window.innerHeight; // 只在向下滚动时触发 if (scrollTop > lastScrollTop) { // 判断是否接近底部 if (scrollHeight <= clientHeight + scrollTop + scrollThreshold && !isLoadingNextPage) { // 查找下一页链接 const nextLinks = document.querySelectorAll('a'); let nextPageLink = null; for (let link of nextLinks) { const text = link.textContent.trim(); if (text.includes('下一页') || text === '下一页 >>') { nextPageLink = link; break; } } if (nextPageLink && nextPageLink.href) { loadNextPage(nextPageLink.href); } } } lastScrollTop = scrollTop; }, false); } function loadNextPage(url) { isLoadingNextPage = true; // 显示加载提示 const loadingDiv = document.createElement('div'); loadingDiv.id = 'auto-loading-indicator'; loadingDiv.style.cssText = ` position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: rgba(33, 150, 243, 0.9); color: white; padding: 12px 24px; border-radius: 25px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 9999; font-size: 14px; font-weight: bold; `; loadingDiv.textContent = '正在加载下一页...'; document.body.appendChild(loadingDiv); // 使用 fetch 加载下一页 fetch(url) .then(response => response.text()) .then(html => { const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 查找主要内容区域 - 找到所有回复帖子 const newPosts = doc.querySelectorAll('div[style*="margin-top: 8pt"]'); // 找到当前页面的最后一个帖子 const currentPosts = document.querySelectorAll('div[style*="margin-top: 8pt"]'); if (currentPosts.length > 0 && newPosts.length > 0) { const lastPost = currentPosts[currentPosts.length - 1]; // 找到最后一个帖子的下一个 table(回复内容) let lastTable = lastPost.nextElementSibling; while (lastTable && lastTable.tagName !== 'TABLE') { lastTable = lastTable.nextElementSibling; } // 确定插入位置:如果有 table 就在 table 后面,否则在 div 后面 let insertAfter = lastTable || lastPost; // 将新帖子插入到页面中 newPosts.forEach(post => { // 克隆 div(用户信息) const clonedPost = post.cloneNode(true); insertAfter.parentNode.insertBefore(clonedPost, insertAfter.nextSibling); insertAfter = clonedPost; // 查找并克隆紧跟的 table(回复内容) const nextTable = post.nextElementSibling; if (nextTable && nextTable.tagName === 'TABLE' && nextTable.classList.contains('main')) { const clonedTable = nextTable.cloneNode(true); insertAfter.parentNode.insertBefore(clonedTable, insertAfter.nextSibling); insertAfter = clonedTable; } }); // 更新页码导航(找到所有

中包含"上一页"和"下一页"的元素) const newPagers = doc.querySelectorAll('p[align="center"]'); const currentPagers = document.querySelectorAll('p[align="center"]'); // 遍历并更新所有分页器 let pagerUpdateCount = 0; for (let i = 0; i < currentPagers.length && i < newPagers.length; i++) { const currentPager = currentPagers[i]; const newPager = newPagers[i]; // 检查是否包含分页链接(包含"上一页"或"下一页") if (currentPager.innerHTML.includes('上一页') || currentPager.innerHTML.includes('下一页')) { currentPager.innerHTML = newPager.innerHTML; pagerUpdateCount++; } } console.log(`已更新 ${pagerUpdateCount} 个分页导航`); // 重新应用高亮 autoHighlightFollowedPosts(); renderSpecialFollowTiles(); enhanceAuthorRemarks(); loadingDiv.textContent = '✓ 加载完成'; loadingDiv.style.background = 'rgba(76, 175, 80, 0.9)'; setTimeout(() => { loadingDiv.remove(); }, 2000); // 更新 URL(不刷新页面) history.pushState(null, '', url); } else { loadingDiv.textContent = '没有更多内容了'; loadingDiv.style.background = 'rgba(255, 152, 0, 0.9)'; setTimeout(() => { loadingDiv.remove(); }, 2000); } isLoadingNextPage = false; }) .catch(error => { console.error('加载下一页失败:', error); loadingDiv.textContent = '✗ 加载失败'; loadingDiv.style.background = 'rgba(244, 67, 54, 0.9)'; setTimeout(() => { loadingDiv.remove(); }, 2000); isLoadingNextPage = false; }); } // 提取收藏夹中所有URL的topicid并打印到控制台 function printBookmarkTopicIds() { const bookmarks = getBookmarkList(); const topicIds = []; bookmarks.forEach(bookmark => { try { const url = new URL(bookmark.url); const topicid = url.searchParams.get('topicid'); if (topicid) { topicIds.push(topicid); } } catch (e) { // 如果URL格式不正确,尝试用正则表达式提取 const match = bookmark.url.match(/topicid=(\d+)/); if (match && match[1]) { topicIds.push(match[1]); } } }); if (topicIds.length > 0) { console.log('收藏夹中的topicid列表:'); console.log(topicIds); console.log('topicid列表(逗号分隔):' + topicIds.join(',')); } else { console.log('收藏夹中没有找到包含topicid的URL'); } return topicIds; } // 将函数暴露到全局,方便在控制台中手动调用 window.printBookmarkTopicIds = printBookmarkTopicIds; // 在embedded左侧添加星星标记收藏的帖子 function highlightTopicIdRows() { // 只在URL包含viewforum时生效 const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('action') !== 'viewforum') { return; } // 获取收藏夹中的topicid列表 const bookmarks = getBookmarkList(); const topicIds = []; bookmarks.forEach(bookmark => { try { const url = new URL(bookmark.url); const topicid = url.searchParams.get('topicid'); if (topicid) { topicIds.push(topicid); } } catch (e) { // 如果URL格式不正确,尝试用正则表达式提取 const match = bookmark.url.match(/topicid=(\d+)/); if (match && match[1]) { topicIds.push(match[1]); } } }); if (topicIds.length === 0) { return; // 如果没有收藏的topicid,不执行标记 } // 查找所有包含topicid链接的embedded元素 const embeddedTds = document.querySelectorAll('td.embedded'); embeddedTds.forEach(td => { // 查找td中所有包含topicid的链接 const links = td.querySelectorAll('a[href*="topicid"]'); links.forEach(link => { // 从链接中提取topicid const href = link.getAttribute('href'); const match = href.match(/topicid[=&](\d+)/); if (match && match[1]) { const topicid = match[1]; // 如果这个topicid在收藏列表中,添加星星 if (topicIds.includes(topicid)) { // 检查是否已经添加过星星 if (!td.querySelector('.bookmark-star')) { // 创建星星元素 const star = document.createElement('span'); star.className = 'bookmark-star'; star.textContent = '⭐'; star.title = '已收藏'; star.style.cssText = ` margin-right: 5px; font-size: 14px; cursor: pointer; `; // 在embedded的左侧插入星星(在所有内容之前) td.insertBefore(star, td.firstChild); } } } }); }); console.log(`已标记 ${document.querySelectorAll('.bookmark-star').length} 个收藏的帖子`); } // 清理下注文本,移除【数字】或[数字]等前缀 function sanitizeBetValue(value) { if (!value) { return ''; } return value .replace(/[\[【]\s*[\d.]+\s*[\]】]/g, '') .replace(/\s+/g, ' ') .trim(); } // 采集帖子下注数据 function collectBetPostsData() { const postTables = document.querySelectorAll('table[id^="pid"]'); const results = []; let maxBetIndex = 0; let skipFirstPost = true; postTables.forEach(table => { if (!table.id || table.id.endsWith('body')) { return; } if (skipFirstPost) { skipFirstPost = false; return; } const userLink = table.querySelector('a[href*="userdetails.php?id="]'); if (!userLink) { return; } const username = (userLink.textContent || '').trim(); const body = document.getElementById(`${table.id}body`); if (!body) { return; } const textContent = body.innerText || ''; const lines = textContent.split(/\r?\n/).map(line => line.trim()).filter(line => line); const betMap = {}; let betAmount = ''; lines.forEach(line => { const match = line.match(/^(\d+)\.(?:下注球队|下注球隊)[::]\s*(.+)$/); if (match) { const index = parseInt(match[1], 10); if (!isNaN(index)) { const sanitized = sanitizeBetValue(match[2]); if (sanitized) { betMap[index] = sanitized; } if (index > maxBetIndex) { maxBetIndex = index; } } return; } if (!betAmount) { const betMatch = line.match(/下注点数[::]\s*([\d,]+)/); if (betMatch && betMatch[1]) { betAmount = betMatch[1].replace(/,/g, ''); } } }); if (Object.keys(betMap).length === 0) { return; } results.push({ username, bets: betMap, betAmount, anchorId: table.id || '' }); }); return { rows: results, maxBetIndex }; } // 渲染数据分析表格 function renderDataAnalysisTable(dataRows, maxBetIndex, container, onlyFollow) { container.innerHTML = ''; const followList = new Set(getSpecialFollowList()); const filteredRows = onlyFollow ? dataRows.filter(row => followList.has(row.username)) : dataRows.slice(); if (filteredRows.length === 0) { const empty = document.createElement('div'); empty.style.cssText = ` padding: 40px 20px; text-align: center; color: #999; font-size: 14px; `; empty.textContent = onlyFollow ? '特殊关注列表中没有匹配的下注记录。' : '当前页面未找到下注记录。'; container.appendChild(empty); return; } const table = document.createElement('table'); table.style.cssText = ` width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; background: #fff; border-radius: 8px; overflow: hidden; `; const thead = document.createElement('thead'); const headerRow = document.createElement('tr'); headerRow.style.background = '#f5f5f5'; const userTh = document.createElement('th'); userTh.textContent = '用户名'; userTh.style.cssText = 'border: 1px solid #ddd; padding: 8px; width: 120px;'; headerRow.appendChild(userTh); for (let i = 1; i <= maxBetIndex; i++) { const th = document.createElement('th'); th.textContent = i; th.style.cssText = 'border: 1px solid #ddd; padding: 8px;'; headerRow.appendChild(th); } const betAmountTh = document.createElement('th'); betAmountTh.textContent = '下注点数'; betAmountTh.style.cssText = 'border: 1px solid #ddd; padding: 8px; width: 100px;'; headerRow.appendChild(betAmountTh); thead.appendChild(headerRow); table.appendChild(thead); const tbody = document.createElement('tbody'); const columnTopMap = new Map(); for (let i = 1; i <= maxBetIndex; i++) { const freq = new Map(); filteredRows.forEach(row => { const val = (row.bets[i] || '').trim(); if (!val) return; freq.set(val, (freq.get(val) || 0) + 1); }); let topValue = ''; let topCount = 0; freq.forEach((count, val) => { if (count > topCount) { topValue = val; topCount = count; } }); columnTopMap.set(i, { value: topValue, count: topCount }); } filteredRows.forEach(row => { const tr = document.createElement('tr'); tr.style.background = '#fff'; const userTd = document.createElement('td'); userTd.style.cssText = 'border: 1px solid #ddd; padding: 8px; font-weight: bold; color: #2196F3;'; const anchorLink = document.createElement('a'); anchorLink.textContent = row.username; anchorLink.style.cssText = 'color: #2196F3; text-decoration: none; cursor: pointer;'; anchorLink.title = '点击跳转到该用户的楼层'; anchorLink.onclick = () => { if (row.anchorId) { const target = document.getElementById(row.anchorId); if (target) { target.scrollIntoView({ behavior: 'smooth', block: 'start' }); target.style.boxShadow = '0 0 12px rgba(33,150,243,0.7)'; setTimeout(() => target.style.boxShadow = '', 2000); } else { window.location.href = `#${row.anchorId}`; } } const overlay = document.getElementById('data-analysis-dialog'); if (overlay) { overlay.remove(); } }; userTd.appendChild(anchorLink); tr.appendChild(userTd); for (let i = 1; i <= maxBetIndex; i++) { const td = document.createElement('td'); const value = row.bets[i] || ''; td.textContent = value; td.style.cssText = 'border: 1px solid #eee; padding: 8px;'; const topInfo = columnTopMap.get(i); if (topInfo && topInfo.value && value === topInfo.value) { td.style.background = '#fff7d6'; td.style.fontWeight = 'bold'; td.style.color = '#e65100'; } tr.appendChild(td); } const betAmountTd = document.createElement('td'); betAmountTd.textContent = row.betAmount || ''; betAmountTd.style.cssText = 'border: 1px solid #ddd; padding: 8px; text-align: right;'; tr.appendChild(betAmountTd); tbody.appendChild(tr); }); table.appendChild(tbody); const summaryRow = document.createElement('tr'); summaryRow.style.background = '#e3f2fd'; const summaryLabel = document.createElement('td'); summaryLabel.textContent = '最常见'; summaryLabel.style.cssText = 'border: 1px solid #ddd; padding: 8px; font-weight: bold;'; summaryRow.appendChild(summaryLabel); for (let i = 1; i <= maxBetIndex; i++) { const td = document.createElement('td'); const info = columnTopMap.get(i); if (info && info.value) { td.textContent = `${info.value}(${info.count}次)`; td.style.fontWeight = 'bold'; td.style.color = '#0d47a1'; } else { td.textContent = '无数据'; td.style.color = '#999'; } td.style.cssText = (td.style.cssText || '') + 'border: 1px solid #ddd; padding: 8px;'; summaryRow.appendChild(td); } const summaryBetTd = document.createElement('td'); summaryBetTd.textContent = '—'; summaryBetTd.style.cssText = 'border: 1px solid #ddd; padding: 8px; text-align: center; color: #777;'; summaryRow.appendChild(summaryBetTd); if (tbody.firstChild) { tbody.insertBefore(summaryRow, tbody.firstChild); } else { tbody.appendChild(summaryRow); } // 构建最常见下注文本 const popularLines = []; for (let i = 1; i <= maxBetIndex; i++) { const info = columnTopMap.get(i); if (info && info.value) { popularLines.push(`${i}.下注球隊: [0.9]${info.value}`); } } const textAreaWrapper = document.createElement('div'); textAreaWrapper.style.cssText = 'margin-top: 12px;'; const textAreaLabel = document.createElement('div'); textAreaLabel.textContent = '最常见下注组合:'; textAreaLabel.style.cssText = 'font-weight: bold; margin-bottom: 6px;'; textAreaWrapper.appendChild(textAreaLabel); const summaryTextarea = document.createElement('textarea'); summaryTextarea.readOnly = false; summaryTextarea.style.cssText = ` width: 98%; min-height: 160px; border: 1px solid #ccc; border-radius: 6px; padding: 10px; font-size: 13px; line-height: 1.5; resize: none; overflow-y: hidden; box-sizing: border-box; `; if (popularLines.length > 0) { popularLines.push('', '下注点数:1000000'); summaryTextarea.value = popularLines.join('\n'); } else { summaryTextarea.value = '暂无可用的最常见下注数据'; } textAreaWrapper.appendChild(summaryTextarea); // 自动调整高度的函数 const adjustTextareaHeight = () => { // 先重置高度,让scrollHeight能正确计算 summaryTextarea.style.height = 'auto'; // 获取实际需要的高度(scrollHeight已经包含了padding) const scrollHeight = summaryTextarea.scrollHeight; // 设置新高度,最小160px,减去1px避免底部多余空白 summaryTextarea.style.height = Math.max(160, scrollHeight - 1) + 'px'; }; // 等待DOM更新后再设置初始高度 setTimeout(() => { adjustTextareaHeight(); }, 0); // 监听输入事件,动态调整高度 summaryTextarea.addEventListener('input', adjustTextareaHeight); summaryTextarea.addEventListener('paste', () => { setTimeout(adjustTextareaHeight, 0); }); container.appendChild(textAreaWrapper); const divider = document.createElement('hr'); divider.style.cssText = 'border: none; border-top: 1px dashed #ccc; margin: 16px 0;'; container.appendChild(divider); container.appendChild(table); return summaryTextarea; } // 打开数据分析弹窗 function openDataAnalysisDialog() { const { rows, maxBetIndex } = collectBetPostsData(); if (!rows || rows.length === 0) { alert('当前页面未找到下注内容,无法生成数据分析。'); return; } const existing = document.getElementById('data-analysis-dialog'); if (existing) { existing.remove(); } const overlay = document.createElement('div'); overlay.id = 'data-analysis-dialog'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); z-index: 10003; display: flex; justify-content: center; align-items: center; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; border-radius: 10px; padding: 20px; width: 1200px; max-width: 95vw; max-height: 90vh; overflow-y: auto; box-shadow: 0 4px 20px rgba(0,0,0,0.3); `; const header = document.createElement('div'); header.style.cssText = ` display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 10px; border-bottom: 2px solid #2196F3; background: transparent; `; const title = document.createElement('h2'); title.textContent = '下注数据分析'; title.style.cssText = ` margin: 0; padding: 0; color: #2196F3; font-size: 20px; background: transparent; border: none; outline: none; `; header.appendChild(title); const closeBtn = document.createElement('button'); closeBtn.textContent = '✕'; closeBtn.style.cssText = ` background: #f44336; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 18px; font-weight: bold; transition: background 0.3s; `; closeBtn.onmouseover = () => closeBtn.style.background = '#d32f2f'; closeBtn.onmouseout = () => closeBtn.style.background = '#f44336'; closeBtn.onclick = () => overlay.remove(); header.appendChild(closeBtn); const controlBar = document.createElement('div'); controlBar.style.cssText = ` display: flex; justify-content: flex-start; gap: 10px; align-items: center; margin-bottom: 10px; `; let summaryTextareaRef = null; const followBtn = document.createElement('button'); followBtn.textContent = '只看特殊关注:关闭'; followBtn.style.cssText = ` padding: 6px 12px; border: 1px solid #2196F3; background: #fff; color: #2196F3; border-radius: 4px; cursor: pointer; transition: box-shadow 0.2s; `; const copyAnswerBtn = document.createElement('button'); copyAnswerBtn.textContent = '复制答案'; copyAnswerBtn.style.cssText = ` padding: 6px 12px; border: 1px solid #4caf50; background: #fff; color: #4caf50; border-radius: 4px; cursor: pointer; transition: box-shadow 0.2s; `; const tableWrapper = document.createElement('div'); tableWrapper.style.cssText = ` border: 1px solid #ddd; border-radius: 8px; padding: 10px; background: #fafafa; max-height: 70vh; overflow: auto; `; let onlyFollow = Config.get('dataAnalysisOnlyFollow', false); const refreshTable = () => { followBtn.textContent = `只看特殊关注:${onlyFollow ? '开启' : '关闭'}`; followBtn.style.background = onlyFollow ? '#2196F3' : '#fff'; followBtn.style.color = onlyFollow ? '#fff' : '#2196F3'; summaryTextareaRef = renderDataAnalysisTable(rows, maxBetIndex, tableWrapper, onlyFollow); }; followBtn.onclick = () => { onlyFollow = !onlyFollow; Config.set('dataAnalysisOnlyFollow', onlyFollow); refreshTable(); }; copyAnswerBtn.onclick = () => { if (!summaryTextareaRef) return; const text = summaryTextareaRef.value; navigator.clipboard.writeText(text).then(() => { copyAnswerBtn.textContent = '已复制'; setTimeout(() => copyAnswerBtn.textContent = '复制答案', 1500); }).catch(() => { alert('复制失败,请手动复制。'); }); }; const addHoverEffect = (btn, baseColor) => { btn.onmouseover = () => btn.style.boxShadow = '0 0 8px rgba(0,0,0,0.15)'; btn.onmouseout = () => btn.style.boxShadow = ''; }; addHoverEffect(followBtn); addHoverEffect(copyAnswerBtn); controlBar.appendChild(followBtn); controlBar.appendChild(copyAnswerBtn); dialog.appendChild(header); dialog.appendChild(controlBar); dialog.appendChild(tableWrapper); overlay.appendChild(dialog); document.body.appendChild(overlay); overlay.addEventListener('click', event => { if (event.target === overlay) { overlay.remove(); } }); refreshTable(); } // 更新单个帖子行的备注展示(通过按钮文字体现) function updateUserNoteDisplayForTd(td, note) { const remarkBtn = td.querySelector('.author-remark-btn'); if (!remarkBtn) { return; } if (note) { remarkBtn.textContent = `备注:${note}`; remarkBtn.style.fontWeight = 'bold'; } else { remarkBtn.textContent = '备注'; remarkBtn.style.fontWeight = 'normal'; } } // 刷新指定用户的全部备注展示 function refreshUserNoteDisplayByUserId(userId) { const note = getUserNoteById(userId); document.querySelectorAll(`td.embedded[data-user-id="${userId}"]`).forEach(td => { updateUserNoteDisplayForTd(td, note); }); } // 为帖子添加备注按钮与展示 function enhanceAuthorRemarks() { const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('action') !== 'viewtopic') { return; } const embeddedTds = document.querySelectorAll('td.embedded'); embeddedTds.forEach(td => { if (td.dataset.remarkBound === '1') { return; } const userLink = td.querySelector('a[href*="userdetails.php?id="]'); if (!userLink) { return; } const userHref = userLink.getAttribute('href') || ''; let userId = ''; try { userId = new URL(userHref, window.location.href).searchParams.get('id') || ''; } catch (e) { const match = userHref.match(/id=(\d+)/); if (match && match[1]) { userId = match[1]; } } if (!userId) { return; } const authorLink = Array.from(td.querySelectorAll('a[href*="authorid="]')).find(link => { const href = link.getAttribute('href') || ''; try { const authorId = new URL(href, window.location.href).searchParams.get('authorid'); return authorId === userId; } catch (e) { const match = href.match(/authorid=(\d+)/); return match && match[1] === userId; } }); if (!authorLink) { return; } td.dataset.remarkBound = '1'; td.dataset.userId = userId; const remarkBtn = document.createElement('button'); remarkBtn.className = 'author-remark-btn'; remarkBtn.textContent = '备注'; remarkBtn.style.cssText = ` padding: 0 4px; border: none; background: transparent; color: #1e73c1; cursor: pointer; font-size: 12px; text-decoration: none; font-weight: normal; `; remarkBtn.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); const username = (userLink.textContent || '').trim(); const currentNote = getUserNoteById(userId); const result = prompt(`请输入对 ${username || '该用户'} 的备注(留空删除):`, currentNote); if (result === null) { return; } const trimmed = result.trim(); setUserNoteById(userId, trimmed); refreshUserNoteDisplayByUserId(userId); }); authorLink.insertAdjacentElement('afterend', remarkBtn); const separator = document.createElement('font'); separator.color = 'gray'; separator.textContent = '\u00A0\u00A0|\u00A0'; remarkBtn.parentNode.insertBefore(separator, remarkBtn); updateUserNoteDisplayForTd(td, getUserNoteById(userId)); }); } // 检查指定topicid的帖子是否已下注(检查所有分页),同时收集特殊关注用户 async function checkTopicBetStatus(topicid) { const currentUsername = getCurrentUsername(); if (!currentUsername) { return { hasBet: false, followUsers: [] }; } const followList = getSpecialFollowList(); const followSet = new Set(followList); const foundUsers = new Set(); let hasBet = false; // 构建基础URL const baseUrl = `${window.location.origin}/forums.php?action=viewtopic&topicid=${topicid}`; // 先检查第一页 try { const response = await fetch(baseUrl, { credentials: 'include', headers: { 'Accept': 'text/html' } }); if (!response.ok) { return { hasBet: false, followUsers: [] }; } const html = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // 检查第一页是否已下注 if (hasUserRepliedOnDocument(doc, currentUsername)) { hasBet = true; } // 收集第一页中的特殊关注用户 if (followSet.size > 0) { const userLinks = doc.querySelectorAll('a[href*="userdetails.php?id"]'); userLinks.forEach(link => { if (link.closest('#info_block')) return; // 跳过当前用户信息 const username = link.textContent.trim(); if (followSet.has(username)) { foundUsers.add(username); } }); } // 从第一页获取所有分页链接 const pageNumbers = new Set(); pageNumbers.add(0); // 第一页 const pageLinks = doc.querySelectorAll('a[href*="viewtopic"][href*="topicid="]'); pageLinks.forEach(link => { const href = link.getAttribute('href'); if (!href) return; const match = href.match(/[?&]page=(\d+)/); if (match && match[1]) { const pageNum = parseInt(match[1], 10); if (!isNaN(pageNum)) { pageNumbers.add(pageNum); } } }); // 检查其他分页 for (let pageNum of pageNumbers) { if (pageNum === 0) continue; // 第一页已经检查过了 const pageUrl = `${baseUrl}&page=${pageNum}`; try { const pageResponse = await fetch(pageUrl, { credentials: 'include', headers: { 'Accept': 'text/html' } }); if (pageResponse.ok) { const pageHtml = await pageResponse.text(); const pageDoc = parser.parseFromString(pageHtml, 'text/html'); // 检查该分页是否已下注 if (!hasBet && hasUserRepliedOnDocument(pageDoc, currentUsername)) { hasBet = true; } // 收集该分页中的特殊关注用户 if (followSet.size > 0) { const pageUserLinks = pageDoc.querySelectorAll('a[href*="userdetails.php?id"]'); pageUserLinks.forEach(link => { if (link.closest('#info_block')) return; const username = link.textContent.trim(); if (followSet.has(username)) { foundUsers.add(username); } }); } } } catch (e) { // 忽略单个分页的错误,继续检查其他分页 } } } catch (error) { console.error(`检查topicid ${topicid} 时发生错误:`, error); } return { hasBet, followUsers: Array.from(foundUsers) }; } // 在论坛列表页面标记每个帖子是否已下注 async function markTopicBetStatus() { // 只在viewforum页面生效 const urlParams = new URLSearchParams(window.location.search); if (urlParams.get('action') !== 'viewforum') { return; } const currentUsername = getCurrentUsername(); if (!currentUsername) { console.log('未获取到当前用户名,无法检查下注状态'); return; } // 排除的topicid列表(不检查这些帖子的用户名和下注状态) const excludedTopicIds = new Set(['35207', '35212', '43558', '3239']); // 查找所有帖子链接(主题链接,不是分页链接) // 主题链接通常包含forumid参数,或者不包含page参数 const topicLinks = document.querySelectorAll('td.embedded a[href*="viewtopic"][href*="topicid="]'); const topicMap = new Map(); // 用于去重,key是topicid,value是链接元素 topicLinks.forEach(link => { const href = link.getAttribute('href'); if (!href) return; // 只处理主题链接(不包含page参数,且不是分页链接) // 分页链接通常格式是:?action=viewtopic&topicid=xxx&page=0 // 主题链接通常格式是:?action=viewtopic&forumid=xx&topicid=xxx 或 ?action=viewtopic&topicid=xxx(没有page参数) if (href.includes('page=') || href.includes('page=p')) { return; // 跳过分页链接 } // 提取topicid const match = href.match(/topicid=(\d+)/); if (match && match[1]) { const topicid = match[1]; // 跳过排除列表中的topicid if (excludedTopicIds.has(topicid)) { return; } // 每个topicid只保留第一个链接(避免重复检查) if (!topicMap.has(topicid)) { topicMap.set(topicid, link); } } }); console.log(`找到 ${topicMap.size} 个帖子,开始检查下注状态...`); // 对每个帖子检查是否已下注 for (let [topicid, link] of topicMap) { // 检查是否已经添加过标记 const parent = link.closest('td.embedded'); if (!parent) continue; if (parent.querySelector('.bet-status-mark')) { continue; // 已经标记过,跳过 } // 异步检查下注状态和特殊关注用户 checkTopicBetStatus(topicid).then(({ hasBet, followUsers }) => { // 检查标记是否已存在(防止重复添加) if (parent.querySelector('.bet-status-mark')) { return; } // 创建标记元素 const mark = document.createElement('span'); mark.className = 'bet-status-mark'; mark.textContent = hasBet ? '【已下注】' : '【未下注】'; mark.style.cssText = ` margin-left: 8px; font-size: 12px; font-weight: bold; color: ${hasBet ? '#4caf50' : '#ff9800'}; `; mark.title = hasBet ? '您已在此帖子下注' : '您尚未在此帖子下注'; // 在链接后面插入标记 // 如果链接后面有其他元素(如分页链接),插入到链接和分页链接之间 link.parentNode.insertBefore(mark, link.nextSibling); // 如果有特殊关注用户回复,显示用户名列表 if (followUsers && followUsers.length > 0) { const followMark = document.createElement('span'); followMark.className = 'follow-users-mark'; followMark.textContent = ` [${followUsers.join(',')}]`; followMark.style.cssText = ` margin-left: 4px; font-size: 12px; color: #2196F3; font-weight: bold; `; followMark.title = '在此帖子回复过的特殊关注用户'; mark.parentNode.insertBefore(followMark, mark.nextSibling); } }).catch(err => { console.error(`检查topicid ${topicid} 下注状态失败:`, err); }); } } // 初始化 function init() { // 等待页面加载完成 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { createControlPanel(); autoHighlightFollowedPosts(); autoLoadNextPage(); checkValidBetAndDisableReply().catch(err => { console.error('检查有效下注时发生错误:', err); }); // 检查有效下注(截止时间和重复下注) printBookmarkTopicIds(); // 打印收藏夹中的topicid列表 highlightTopicIdRows(); // 高亮包含topicid的行 markTopicBetStatus(); // 标记论坛列表页面每个帖子的下注状态 renderSpecialFollowTiles(); // 渲染特殊关注磁贴 enhanceAuthorRemarks(); // 添加备注按钮与展示 }); } else { createControlPanel(); autoHighlightFollowedPosts(); autoLoadNextPage(); checkValidBetAndDisableReply().catch(err => { console.error('检查有效下注时发生错误:', err); }); // 检查有效下注(截止时间和重复下注) printBookmarkTopicIds(); // 打印收藏夹中的topicid列表 highlightTopicIdRows(); // 高亮包含topicid的行 markTopicBetStatus(); // 标记论坛列表页面每个帖子的下注状态 renderSpecialFollowTiles(); // 渲染特殊关注磁贴 enhanceAuthorRemarks(); // 添加备注按钮与展示 } } // 启动脚本 init(); })();