Greasy Fork is available in English.
在 HDSky 论坛页面右上角显示控制面板,自动高亮特殊关注用户的回复内容,支持快速翻页和收藏功能,可折叠面板,下拉加载翻页
// ==UserScript==
// @name HDSky 体育沙龙面板
// @namespace http://tampermonkey.net/
// @version 6.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
// ==/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);
}
};
// ==================== 工具函数 ====================
/**
* 检测是否为移动设备
* @returns {boolean} 是否为移动设备
*/
function isMobileDevice() {
return window.innerWidth <= 768 ||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
/**
* 检查当前页面是否为帖子查看页面
* @returns {boolean} 是否为帖子查看页面
*/
function isViewTopicPage() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('action') === 'viewtopic';
}
/**
* 统一错误处理函数
* @param {string} context - 错误上下文描述
* @param {Error} error - 错误对象
*/
function handleError(context, error) {
console.error(`${context}时发生错误:`, error);
}
/**
* 延迟执行函数
* @param {Function} fn - 要执行的函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {number} setTimeout返回的ID
*/
function delayExecute(fn, delay = 0) {
return setTimeout(fn, delay);
}
/**
* DOM元素缓存
*/
const PanelElements = {
get panel() {
return document.getElementById('hdsky-special-follow-panel');
},
get toggleBtn() {
return document.getElementById('panel-toggle-btn');
}
};
/**
* 创建通用面板按钮
* @param {Object} config - 按钮配置
* @param {string} config.id - 按钮ID
* @param {string} config.text - 按钮文本
* @param {string} [config.backgroundColor='#2196F3'] - 背景颜色
* @param {string} [config.hoverColor='#0b7dda'] - 悬停颜色
* @param {Function} config.onClick - 点击事件处理函数
* @param {string} [config.fontSize='14px'] - 字体大小
* @param {string} [config.padding='10px 15px'] - 内边距
* @param {string} [config.width='100%'] - 宽度
* @returns {HTMLButtonElement} 创建的按钮元素
*/
function createPanelButton(config) {
const {
id,
text,
backgroundColor = '#2196F3',
hoverColor = '#0b7dda',
onClick,
fontSize = '14px',
padding = '10px 15px',
width = '100%'
} = config;
const button = document.createElement('button');
button.id = id;
button.textContent = text;
button.style.cssText = `
padding: ${padding};
background: ${backgroundColor};
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: ${fontSize};
font-weight: bold;
transition: background 0.3s;
width: ${width};
`;
button.onmouseover = () => button.style.background = hoverColor;
button.onmouseout = () => button.style.background = backgroundColor;
button.onclick = onClick;
return button;
}
/**
* 更新切换按钮状态
* @param {HTMLButtonElement} button - 按钮元素
* @param {boolean} isEnabled - 是否启用
* @param {string} enabledText - 启用时的文本
* @param {string} disabledText - 禁用时的文本
* @param {string} enabledTitle - 启用时的标题
* @param {string} disabledTitle - 禁用时的标题
*/
function updateToggleButton(button, isEnabled, enabledText, disabledText, enabledTitle, disabledTitle) {
button.style.background = '#2196F3';
button.onmouseover = () => button.style.background = '#0b7dda';
button.onmouseout = () => button.style.background = '#2196F3';
if (isEnabled) {
button.textContent = `✅ ${enabledText}`;
button.title = enabledTitle;
} else {
button.textContent = `❌ ${disabledText}`;
button.title = disabledTitle;
}
}
// ==================== 业务函数 ====================
// 获取帖子id
function getThreadId(str) {
let id = 5381;
for (let i = 0; i < str.length; i++) {
id = ((id << 5) + id) + str.charCodeAt(i);
id = id & id;
}
return id >>> 0;
}
// 部分官方回帖id
// const threadIdList = [223214241];
const threadIdList = [];
// 从存储中获取特殊关注名单
function getSpecialFollowList() {
const listStr = Config.get('specialFollowList', '');
if (!listStr) return [];
const followList = listStr.split(',').map(name => name.trim()).filter(name => name);
return followList.filter(name => {
const threadId = getThreadId(name);
return !threadIdList.includes(threadId);
});
}
// 保存特殊关注名单到存储
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));
}
// 创建控制面板
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 isMobile = isMobileDevice();
if (isMobile) {
container.style.top = '160px'; // 80px * 2
container.style.right = '20px'; // 10px * 2
}
// 创建折叠按钮
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();
});
// 移动端扩大折叠按钮尺寸
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;
`;
// 移动端放大整个面板
if (isMobile) {
panel.style.width = '440px'; // 220px * 2
panel.style.padding = '30px'; // 15px * 2
panel.style.borderRadius = '16px'; // 8px * 2
panel.style.boxShadow = '0 8px 16px rgba(0,0,0,0.2)'; // 阴影也放大
}
// 面板标题
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;
`;
if (isMobile) {
title.style.fontSize = '32px'; // 16px * 2
title.style.marginBottom = '24px'; // 12px * 2
title.style.paddingBottom = '16px'; // 8px * 2
title.style.borderBottomWidth = '2px'; // 1px * 2
}
panel.appendChild(title);
// 按钮容器(统一管理所有按钮)
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 10px;
`;
if (isMobile) {
buttonContainer.style.gap = '20px'; // 10px * 2
}
// 关注列表按钮
const followListBtn = createPanelButton({
id: 'follow-list-btn',
text: '关注列表',
onClick: handleFollowListClick
});
followListBtn.title = '点击编辑特殊关注名单';
buttonContainer.appendChild(followListBtn);
// 下拉加载翻页按钮
const autoLoadBtn = createPanelButton({
id: 'auto-load-btn',
text: '下拉加载翻页',
onClick: toggleAutoLoadFromPanel
});
updateAutoLoadButton(autoLoadBtn); // 在设置基础样式后再更新按钮状态
buttonContainer.appendChild(autoLoadBtn);
// 检查有效下注按钮
const checkValidBetBtn = createPanelButton({
id: 'check-valid-bet-btn',
text: '检查有效下注',
onClick: toggleCheckValidBet
});
updateCheckValidBetButton(checkValidBetBtn);
buttonContainer.appendChild(checkValidBetBtn);
// 高亮特殊关注按钮
const highlightFollowBtn = createPanelButton({
id: 'highlight-follow-btn',
text: '高亮特殊关注',
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;
`;
if (isMobile) {
bookmarkContainer.style.gap = '20px'; // 10px * 2
}
// 收藏按钮
const bookmarkBtn = createPanelButton({
id: 'bookmark-btn',
text: '收藏',
padding: '8px 12px',
fontSize: '13px',
width: 'auto',
onClick: addBookmark
});
bookmarkBtn.style.flex = '1';
bookmarkContainer.appendChild(bookmarkBtn);
// 收藏夹按钮
const bookmarkListBtn = createPanelButton({
id: 'bookmark-list-btn',
text: '收藏夹',
padding: '8px 12px',
fontSize: '13px',
width: 'auto',
onClick: showBookmarkList
});
bookmarkListBtn.style.flex = '1';
bookmarkContainer.appendChild(bookmarkListBtn);
buttonContainer.appendChild(bookmarkContainer);
// 数据分析按钮
const dataAnalysisBtn = createPanelButton({
id: 'data-analysis-btn',
text: '数据分析',
backgroundColor: '#4caf50',
hoverColor: '#388e3c',
onClick: openDataAnalysisDialog
});
buttonContainer.appendChild(dataAnalysisBtn);
// 快捷回复按钮
const quickReplyBtn = createPanelButton({
id: 'quick-reply-btn',
text: '快捷回复',
backgroundColor: '#ff9800',
hoverColor: '#f57c00',
onClick: openQuickReply
});
buttonContainer.appendChild(quickReplyBtn);
// 全部标记为已读按钮
const markAllReadBtn = createPanelButton({
id: 'mark-all-read-btn',
text: '全部消息标记为已读',
backgroundColor: '#9c27b0',
hoverColor: '#7b1fa2',
onClick: markAllMessagesAsRead
});
buttonContainer.appendChild(markAllReadBtn);
panel.appendChild(buttonContainer);
// 移动端放大所有按钮
if (isMobile) {
const allButtons = buttonContainer.querySelectorAll('button');
allButtons.forEach(btn => {
const currentPadding = btn.style.padding || '10px 15px';
const currentFontSize = btn.style.fontSize || '14px';
const currentBorderRadius = btn.style.borderRadius || '5px';
// 解析padding值并放大
const paddingMatch = currentPadding.match(/(\d+)px\s+(\d+)px/);
if (paddingMatch) {
btn.style.padding = `${parseInt(paddingMatch[1]) * 2}px ${parseInt(paddingMatch[2]) * 2}px`;
}
// 解析fontSize并放大
const fontSizeMatch = currentFontSize.match(/(\d+)px/);
if (fontSizeMatch) {
btn.style.fontSize = `${parseInt(fontSizeMatch[1]) * 2}px`;
}
// 解析borderRadius并放大
const borderRadiusMatch = currentBorderRadius.match(/(\d+)px/);
if (borderRadiusMatch) {
btn.style.borderRadius = `${parseInt(borderRadiusMatch[1]) * 2}px`;
}
});
}
// 将按钮和面板添加到容器
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 = PanelElements.panel;
const toggleBtn = PanelElements.toggleBtn;
if (!panel || !toggleBtn) return;
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();
updateToggleButton(
button,
isEnabled,
'下拉加载翻页',
'下拉加载翻页',
'点击关闭下拉加载翻页功能',
'点击开启下拉加载翻页功能'
);
}
// 从面板切换下拉加载功能
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();
const titleText = '如果超过截止时间或重复下注将禁用回复功能';
updateToggleButton(
button,
isEnabled,
'检查有效下注',
'检查有效下注',
`点击关闭检查有效下注功能\n${titleText}`,
`点击开启检查有效下注功能\n${titleText}`
);
}
// 从面板切换检查有效下注功能
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();
updateToggleButton(
button,
isEnabled,
'高亮特殊关注',
'高亮特殊关注',
'点击关闭高亮特殊关注功能',
'点击开启高亮特殊关注功能'
);
}
// 从面板切换高亮特殊关注功能
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;
}
// 获取当前主题所有页面的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) {
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}`);
}
});
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];
}
}
/**
* 根据 URL 获取主题的所有分页文档
* @param {string} topicUrl - 主题URL(可以是任意页的URL,或从帖子列表获取的URL)
* @returns {Promise<Array>} 返回包含所有分页文档的数组,每个元素包含 {url, doc, isCurrentPage, pageNum}
*/
async function fetchTopicDocumentsByUrl(topicUrl) {
if (!topicUrl) return [];
const parser = new DOMParser();
const documents = [];
try {
// 提取 topicid
const urlObj = new URL(topicUrl, window.location.origin);
const topicid = urlObj.searchParams.get('topicid');
if (!topicid) {
return [];
}
// 构建基础URL
const forumid = urlObj.searchParams.get('forumid');
const baseParams = new URLSearchParams();
baseParams.set('action', 'viewtopic');
if (forumid) {
baseParams.set('forumid', forumid);
}
baseParams.set('topicid', topicid);
const baseUrl = `${urlObj.origin}${urlObj.pathname}?${baseParams.toString()}`;
// 获取第一页以获取所有分页链接
const firstPageUrl = baseUrl;
const response = await fetch(firstPageUrl, {
credentials: 'include',
headers: { 'Accept': 'text/html' }
});
if (!response.ok) {
return [];
}
const html = await response.text();
const doc = parser.parseFromString(html, 'text/html');
// 收集所有页码
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);
}
}
});
// 检查当前页面是否匹配
const currentNormalized = normalizeUrlForCompare(window.location.href);
// 获取所有分页的文档
for (const pageNum of Array.from(pageNumbers).sort((a, b) => a - b)) {
const pageUrl = pageNum === 0 ? baseUrl : `${baseUrl}&page=${pageNum}`;
const normalizedUrl = normalizeUrlForCompare(pageUrl);
const isCurrentPage = normalizedUrl === currentNormalized;
if (isCurrentPage && pageNum === 0) {
// 如果是当前页面且是第一页,使用已加载的文档
documents.push({ url: pageUrl, doc: document, isCurrentPage: true, pageNum: 0 });
} else if (pageNum === 0) {
// 第一页已经获取过
documents.push({ url: pageUrl, doc, isCurrentPage, pageNum: 0 });
} else {
// 获取其他分页
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');
documents.push({ url: pageUrl, doc: pageDoc, isCurrentPage, pageNum });
}
} catch (error) {
handleError(`请求分页发生错误: ${pageUrl}`, error);
}
}
}
} catch (error) {
handleError('获取主题文档', error);
}
return documents;
}
/**
* 解析帖子数据,提取标准化字段
* @param {Document} doc - 文档对象
* @param {number} pageNum - 页码
* @returns {Array} 返回帖子数据数组,每个元素包含 {username, floor, bets, betAmount, anchorId, ratingSum, postElement}
*/
function parsePostsFromDocument(doc, pageNum) {
const posts = [];
const postTables = doc.querySelectorAll('table[id^="pid"]');
let floorNum = pageNum * 10 + 1; // 假设每页10个帖子
let skipFirstPost = (pageNum === 0); // 只在第一页跳过第一楼
postTables.forEach(table => {
if (!table.id || table.id.endsWith('body')) {
return;
}
// 跳过第一楼
if (skipFirstPost) {
skipFirstPost = false;
floorNum++;
return;
}
// 提取用户名
const userLink = table.querySelector('a[href*="userdetails.php?id="]');
if (!userLink) {
return;
}
const username = (userLink.textContent || '').trim();
// 提取帖子内容
const body = doc.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;
}
}
return;
}
if (!betAmount) {
const betMatch = line.match(/下注点数[::]\s*([\d,]+)/);
if (betMatch && betMatch[1]) {
betAmount = betMatch[1].replace(/,/g, '');
}
}
});
// 计算评分总和
const ratingSum = calculateRatingSum(body);
// 提取锚点ID
const anchorId = table.id || '';
// 查找对应的 div 元素(用于高亮等操作)
const postDiv = table.closest('div[style*="margin-top: 8pt"]') ||
table.previousElementSibling;
posts.push({
username,
floor: floorNum,
bets: betMap,
betAmount,
anchorId,
ratingSum,
postElement: postDiv,
bodyElement: body,
hasBet: Object.keys(betMap).length > 0
});
floorNum++;
});
return posts;
}
/**
* 获取主题的所有回复数据(解析后的标准化数据)
* @param {string} topicUrl - 主题URL
* @returns {Promise<Array>} 返回所有帖子的解析数据
*/
async function fetchTopicPostsData(topicUrl) {
const documents = await fetchTopicDocumentsByUrl(topicUrl);
const allPosts = [];
for (const entry of documents) {
if (!entry.doc) continue;
const posts = parsePostsFromDocument(entry.doc, entry.pageNum);
allPosts.push(...posts);
}
return allPosts;
}
// 检查当前用户是否已经在任意分页下注
async function hasUserReplied() {
const currentUsername = getCurrentUsername();
if (!currentUsername) {
return false;
}
// 使用缓存的数据或获取所有分页数据
const currentUrl = window.location.href;
const allPosts = await fetchTopicPostsData(currentUrl);
// 检查是否有当前用户的回复
for (let post of allPosts) {
if (post.username === currentUsername) {
return true;
}
}
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';
});
// 先移除所有旧的检查提示(避免重复显示)
const oldNotices = replyTable.querySelectorAll('.deadline-notice, .duplicate-bet-notice, .checking-notice');
oldNotices.forEach(notice => notice.remove());
// 添加新的提示
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 checkBetAmountInReplyBox() {
// 只在开关开启时检查
if (!Config.getCheckValidBetEnabled()) {
return;
}
const composeForm = document.getElementById('compose');
if (!composeForm) return;
const textarea = composeForm.querySelector('textarea[name="body"]');
if (!textarea) return;
const submitButton = document.getElementById('qr') || composeForm.querySelector('input[type="submit"], button[type="submit"]');
if (!submitButton) return;
const replyTable = composeForm.closest('table');
if (!replyTable) return;
const content = textarea.value || '';
const betAmountMatch = content.match(/下注点数[::]\s*([\d,]+)/);
if (betAmountMatch && betAmountMatch[1]) {
const betAmount = parseInt(betAmountMatch[1].replace(/,/g, ''), 10);
if (!isNaN(betAmount) && betAmount > 1000000) {
// 禁用提交按钮
submitButton.disabled = true;
submitButton.style.opacity = '0.5';
submitButton.style.cursor = 'not-allowed';
// 显示提示
if (!replyTable.querySelector('.bet-amount-notice')) {
const notice = document.createElement('div');
notice.className = 'bet-amount-notice';
notice.style.cssText = `
background: #ff5252;
color: white;
padding: 10px 15px;
margin: 10px 0;
border-radius: 5px;
text-align: center;
font-weight: bold;
font-size: 14px;
`;
notice.textContent = `⚠️ 下注点数(${betAmount.toLocaleString()})超过1000000,禁止提交`;
const quickReplyTitle = Array.from(replyTable.querySelectorAll('b')).find(b => b.textContent === '快速回复');
if (quickReplyTitle && quickReplyTitle.parentElement) {
quickReplyTitle.parentElement.appendChild(notice);
}
}
} else {
// 恢复提交按钮
submitButton.disabled = false;
submitButton.style.opacity = '';
submitButton.style.cursor = '';
// 移除提示
const notice = replyTable.querySelector('.bet-amount-notice');
if (notice) notice.remove();
}
} else {
// 恢复提交按钮
submitButton.disabled = false;
submitButton.style.opacity = '';
submitButton.style.cursor = '';
// 移除提示
const notice = replyTable.querySelector('.bet-amount-notice');
if (notice) notice.remove();
}
}
// 启用回复表单
function enableReplyForm() {
const composeForm = document.getElementById('compose');
if (!composeForm) return;
const replyTable = composeForm.closest('table');
if (!replyTable) return;
// 启用所有表单元素
const formElements = composeForm.querySelectorAll('textarea, input[type="submit"], button');
formElements.forEach(element => {
element.disabled = false;
element.style.opacity = '';
element.style.cursor = '';
});
// 移除所有检查提示
const notices = replyTable.querySelectorAll('.deadline-notice, .duplicate-bet-notice, .checking-notice');
notices.forEach(notice => notice.remove());
}
// 检查有效下注并禁用回复(合并截止时间和重复下注检查)
async function checkValidBetAndDisableReply() {
// 只在开关开启时检查
if (!Config.getCheckValidBetEnabled()) {
enableReplyForm(); // 如果开关关闭,确保表单可用
return;
}
// 先禁用回复表单,显示检查中提示
if (disableReplyForm('⏳ 正在检查下注状态,请稍候...', 'checking-notice', '#2196F3')) {
}
// 1. 检查截止时间
const topSpan = document.getElementById('top');
if (topSpan) {
const titleText = topSpan.textContent;
// 匹配格式:下注截止时间 2025-12-03 03:30:00 或 截止时间:2025-11-13 21:00(:00)
const deadlineMatch = titleText.match(/(?:下注)?截止时间[::\s]+(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}(?::\d{2})?)/);
if (deadlineMatch) {
let deadlineStr = deadlineMatch[1];
// 如果时间格式没有秒(只有 HH:MM),添加秒
const timePart = deadlineStr.split(' ')[1]; // 获取时间部分
if (timePart && timePart.split(':').length === 2) {
deadlineStr = deadlineStr + ':00';
}
// 解析截止时间(北京时间)
const deadlineDate = new Date(deadlineStr.replace(' ', 'T') + '+08:00');
// 获取当前时间
const now = new Date();
// 如果截止时间已过
if (now > deadlineDate) {
if (disableReplyForm('⚠️ 投注截止时间已过,快速回复已禁用', 'deadline-notice', '#ff5252')) {
}
return; // 截止时间已过,不再检查重复下注
}
}
}
// 2. 检查是否已下注(重复下注)
if (await hasUserReplied()) {
if (disableReplyForm('⚠️ 您已经下注,禁止重复下注', 'duplicate-bet-notice', '#ff9800')) {
}
return;
}
// 3. 检查通过,启用回复表单
enableReplyForm();
// 4. 设置下注点数检查监听器
const composeForm = document.getElementById('compose');
if (composeForm) {
const textarea = composeForm.querySelector('textarea[name="body"]');
if (textarea && !textarea.dataset.betAmountListenerAdded) {
textarea.dataset.betAmountListenerAdded = 'true';
textarea.addEventListener('input', checkBetAmountInReplyBox);
textarea.addEventListener('paste', () => setTimeout(checkBetAmountInReplyBox, 0));
checkBetAmountInReplyBox(); // 初始检查
}
}
}
const ENABLE_SPECIAL_TILES = false;
// 全局变量:高亮帖子列表
let highlightedPosts = [];
// 全局变量:记录是否已经跳过第一楼(用于翻页加载时避免误判)
let hasSkippedFirstFloor = false;
// 全局变量:缓存当前主题的所有帖子数据
let cachedTopicPostsData = null;
let cachedTopicUrl = null;
/**
* 预加载当前主题的所有回复数据(后台异步加载)
*/
async function preloadCurrentTopicPostsData() {
if (!isViewTopicPage()) {
return;
}
const currentUrl = window.location.href;
const normalizedUrl = normalizeUrlForCompare(currentUrl);
// 如果已经有缓存且URL相同,不需要重新加载
if (cachedTopicPostsData && cachedTopicUrl === normalizedUrl) {
printAllPostsInfo(cachedTopicPostsData);
return;
}
console.log('[HDSky特殊关注] 开始加载所有分页的回复数据...');
try {
const allPosts = await fetchTopicPostsData(currentUrl);
console.log('[HDSky特殊关注] 数据加载完成,共', allPosts.length, '条回复');
printAllPostsInfo(allPosts);
} catch (err) {
handleError('预加载主题数据', err);
}
}
/**
* 打印所有帖子信息到控制台
* @param {Array} posts - 帖子数据数组
*/
function printAllPostsInfo(posts) {
console.log('='.repeat(80));
console.log('[HDSky特殊关注] 所有回复信息列表');
console.log('='.repeat(80));
console.log('总回复数:', posts.length);
console.log('-'.repeat(80));
posts.forEach((post, index) => {
console.log(`\n【回复 ${index + 1}】`);
console.log(' 用户名:', post.username);
console.log(' 锚点ID:', post.anchorId || '无');
if (post.hasBet) {
console.log(' 下注内容:', post.bets);
console.log(' 下注点数:', post.betAmount || '未指定');
} else {
console.log(' 下注内容: 未下注');
}
if (post.ratingSum !== 0) {
const prefix = post.ratingSum > 0 ? '+' : '';
console.log(' 评分总和:', `${prefix}${post.ratingSum}`);
}
});
console.log('\n' + '='.repeat(80));
// 统计信息
const betCount = posts.filter(p => p.hasBet).length;
const ratingCount = posts.filter(p => p.ratingSum !== 0).length;
const uniqueUsers = new Set(posts.map(p => p.username)).size;
console.log('[统计信息]');
console.log(' 独立用户数:', uniqueUsers);
console.log(' 已下注回复数:', betCount);
console.log(' 有评分回复数:', ratingCount);
// 特殊关注用户统计
const followList = getSpecialFollowList();
const followSet = new Set(followList);
const followedPosts = posts.filter(p => followSet.has(p.username));
if (followedPosts.length > 0) {
console.log(' 特殊关注回复数:', followedPosts.length);
const followedUsers = [...new Set(followedPosts.map(p => p.username))];
console.log(' 特殊关注用户:', followedUsers.join('、'));
}
console.log('='.repeat(80));
}
/**
* 获取主题的所有回复数据(解析后的标准化数据),使用缓存优化
* @param {string} topicUrl - 主题URL
* @returns {Promise<Array>} 返回所有帖子的解析数据
*/
async function fetchTopicPostsData(topicUrl) {
const normalizedUrl = normalizeUrlForCompare(topicUrl);
// 如果缓存存在且URL相同,直接返回缓存
if (cachedTopicPostsData && cachedTopicUrl === normalizedUrl) {
return cachedTopicPostsData;
}
const documents = await fetchTopicDocumentsByUrl(topicUrl);
const allPosts = [];
for (const entry of documents) {
const posts = parsePostsFromDocument(entry.doc, entry.pageNum);
allPosts.push(...posts);
}
// 更新缓存
cachedTopicPostsData = allPosts;
cachedTopicUrl = normalizedUrl;
return allPosts;
}
// 添加收藏
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();
const followSet = new Set(followList);
// 如果没有关注名单,不执行高亮
if (followList.length === 0) {
return;
}
// 先清除之前的高亮
clearHighlights();
// 重置高亮列表
highlightedPosts = [];
// 获取当前页面的帖子数据(使用解析函数)
const pageNum = hasSkippedFirstFloor ? 1 : 0; // 如果已经跳过第一楼,表示这是翻页加载
const posts = parsePostsFromDocument(document, pageNum);
// 标记已跳过第一楼
if (!hasSkippedFirstFloor) {
hasSkippedFirstFloor = true;
}
// 高亮特殊关注用户的帖子
posts.forEach(post => {
if (!post.postElement || !followSet.has(post.username)) {
return;
}
const postElement = post.postElement;
// 高亮显示关注用户的信息div
postElement.style.background = '#fffacd';
postElement.style.border = '2px solid #ffd700';
postElement.style.borderRadius = '5px';
postElement.style.padding = '5px';
// 找到并高亮div内部的table
const innerTables = postElement.querySelectorAll('table');
innerTables.forEach(table => {
table.style.background = '#fff8dc';
table.style.border = '2px solid #ffb700';
});
// 标记为已高亮
postElement.dataset.highlighted = 'true';
// 找到并高亮紧跟在div后面的回复内容table(class="main")
let nextElement = postElement.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(postElement);
});
}
// 清除所有高亮
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();
}
}
// 收集特殊关注用户的回帖数据(使用缓存数据)
async function collectSpecialFollowReplies(followList) {
const followSet = new Set(followList);
const collectedMap = new Map();
// 使用缓存的数据或获取所有分页数据
const currentUrl = window.location.href;
const allPosts = await fetchTopicPostsData(currentUrl);
// 遍历所有帖子,收集特殊关注用户的信息
for (let post of allPosts) {
const username = post.username;
// 如果是特殊关注用户且尚未收集
if (followSet.has(username) && !collectedMap.has(username)) {
// 构建目标URL(带锚点)
let targetUrl = currentUrl.split('#')[0];
if (post.anchorId) {
targetUrl += `#${post.anchorId}`;
}
// 检查是否在当前页面
const isOnCurrentPage = post.postElement && post.postElement.ownerDocument === document;
collectedMap.set(username, {
username,
targetUrl,
anchorId: post.anchorId,
isOnCurrentPage: isOnCurrentPage,
pageUrl: currentUrl
});
}
// 如果已经找到所有关注用户,提前退出
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();
if (!isViewTopicPage()) {
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;
}
});
// 更新页码导航(找到所有 <p align="center"> 中包含"上一页"和"下一页"的元素)
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++;
}
}
// 重新应用高亮
autoHighlightFollowedPosts();
renderSpecialFollowTiles();
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 => {
handleError('加载下一页', 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) {
} else {
}
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);
}
}
}
});
});
}
// 清理下注文本,移除【数字】或[数字]等前缀
function sanitizeBetValue(value) {
if (!value) {
return '';
}
return value
.replace(/[\[【]\s*[\d.]+\s*[\]】]/g, '')
.replace(/\s+/g, ' ')
.trim();
}
// 采集帖子下注数据
// 计算评分总和
function calculateRatingSum(postBody) {
const container = postBody.parentElement;
if (!container) return 0;
// 查找包含"[评分]"的 div
const ratingDiv = Array.from(container.querySelectorAll('div')).find(div => {
const text = div.textContent || '';
return text.includes('[评分]');
});
if (!ratingDiv) {
return 0;
}
const html = ratingDiv.innerHTML || '';
const lines = html.split(/<br\s*\/?>|\n/i);
let sum = 0;
let foundScore = false;
lines.forEach(line => {
const clean = line.replace(/<[^>]+>/g, '').trim();
if (!clean || !clean.includes('评分理由')) {
return;
}
const match = clean.match(/\s([+-]\d[\d,]*)\s*评分理由/);
if (match) {
const value = parseInt(match[1].trim().replace(/,/g, ''), 10);
if (!isNaN(value)) {
sum += value;
foundScore = true;
}
}
});
return foundScore ? sum : 0;
}
async function collectBetPostsData() {
// 使用 fetchTopicPostsData 获取当前主题的所有分页数据
const currentUrl = window.location.href;
const posts = await fetchTopicPostsData(currentUrl);
const results = [];
let maxBetIndex = 0;
let hasRating = false;
posts.forEach(post => {
// 只收集有下注的帖子
if (!post.hasBet) {
return;
}
// 计算最大下注索引
Object.keys(post.bets).forEach(key => {
const index = parseInt(key, 10);
if (!isNaN(index) && index > maxBetIndex) {
maxBetIndex = index;
}
});
// 检查是否有评分
if (post.ratingSum !== 0) {
hasRating = true;
// 只在当前页面的帖子上显示评分总和
if (post.bodyElement && post.bodyElement.ownerDocument === document) {
appendRatingSummary(post.bodyElement, post.ratingSum);
}
}
results.push({
username: post.username,
bets: post.bets,
betAmount: post.betAmount,
anchorId: post.anchorId,
ratingSum: post.ratingSum
});
});
return {
rows: results,
maxBetIndex,
hasRating
};
}
// 渲染数据分析表格
function renderDataAnalysisTable(dataRows, maxBetIndex, container, onlyFollow, hasRating) {
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);
// 如果有评分,添加"评分总和"列
if (hasRating) {
const ratingSumTh = document.createElement('th');
ratingSumTh.textContent = '评分总和';
ratingSumTh.style.cssText = 'border: 1px solid #ddd; padding: 8px; width: 100px;';
headerRow.appendChild(ratingSumTh);
}
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 isFollowedUser = followList.has(row.username);
const userColor = isFollowedUser ? '#f44336' : '#2196F3'; // 特殊关注用红色,普通用户用蓝色
const userTd = document.createElement('td');
userTd.style.cssText = `border: 1px solid #ddd; padding: 8px; font-weight: bold; color: ${userColor};`;
const anchorLink = document.createElement('a');
anchorLink.textContent = row.username;
anchorLink.style.cssText = `color: ${userColor}; 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);
// 关闭对话框
const overlay = document.getElementById('data-analysis-dialog');
if (overlay) {
overlay.remove();
}
} else {
// 元素不在当前页面,跳转到带锚点的URL
window.location.href = `#${row.anchorId}`;
// 延迟关闭对话框,等待页面跳转
setTimeout(() => {
const overlay = document.getElementById('data-analysis-dialog');
if (overlay) {
overlay.remove();
}
}, 100);
}
}
};
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);
// 如果有评分,添加"评分总和"单元格
if (hasRating) {
const ratingSumTd = document.createElement('td');
if (row.ratingSum !== null && row.ratingSum !== undefined && row.ratingSum !== 0) {
const prefix = row.ratingSum > 0 ? '+' : '';
ratingSumTd.textContent = `${prefix}${row.ratingSum}`;
ratingSumTd.style.cssText = 'border: 1px solid #ddd; padding: 8px; text-align: right; font-weight: bold;';
if (row.ratingSum > 0) {
ratingSumTd.style.color = '#4caf50';
} else if (row.ratingSum < 0) {
ratingSumTd.style.color = '#f44336';
}
} else {
ratingSumTd.textContent = '-';
ratingSumTd.style.cssText = 'border: 1px solid #ddd; padding: 8px; text-align: center; color: #999;';
}
tr.appendChild(ratingSumTd);
}
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 (hasRating) {
const summaryRatingTd = document.createElement('td');
summaryRatingTd.textContent = '—';
summaryRatingTd.style.cssText = 'border: 1px solid #ddd; padding: 8px; text-align: center; color: #777;';
summaryRow.appendChild(summaryRatingTd);
}
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;
}
// 打开快捷回复悬浮框
let isQuickReplyOpen = false;
function openQuickReply() {
if (!isViewTopicPage()) {
alert('快捷回复功能仅在帖子页面可用');
return;
}
if (isQuickReplyOpen) {
return;
}
// 查找原始回复框,获取表单信息
const originalCompose = document.getElementById('compose');
if (!originalCompose) {
alert('未找到回复框');
return;
}
// 获取原始表单的 action 和 method
const form = originalCompose.querySelector('form');
const formAction = form ? form.getAttribute('action') : '';
const formMethod = form ? form.getAttribute('method') || 'post' : 'post';
// 创建悬浮框容器(参考 a.html 的 md-editor 风格)
const quickReplyBox = document.createElement('div');
quickReplyBox.id = 'quick-reply-box';
quickReplyBox.className = 'md-editor';
quickReplyBox.style.cssText = `
position: fixed;
background: white;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.15);
z-index: 10001;
width: 600px;
display: flex;
flex-direction: column;
overflow: hidden;
`;
// 创建标题栏(参考 a.html 的 window_header 风格,可拖动)
const header = document.createElement('div');
header.className = 'tab-select window_header';
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 15px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
cursor: move;
user-select: none;
`;
const headerTitle = document.createElement('div');
headerTitle.textContent = '快捷回复';
headerTitle.style.cssText = `
font-weight: 500;
color: #333;
font-size: 18px;
`;
header.appendChild(headerTitle);
// 关闭按钮(参考 a.html 风格)
const closeBtn = document.createElement('a');
closeBtn.href = 'javascript:void(0)';
closeBtn.className = 'editor-top-button';
closeBtn.title = '关闭';
closeBtn.style.cssText = `
background-color: rgba(0, 0, 0, 0.1);
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
`;
closeBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 48 48" fill="none"><path d="M8 8L40 40" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path><path d="M8 40L40 8" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"></path></svg>';
closeBtn.onmouseover = () => closeBtn.style.backgroundColor = 'rgba(0, 0, 0, 0.15)';
closeBtn.onmouseout = () => closeBtn.style.backgroundColor = 'rgba(0, 0, 0, 0.1)';
header.appendChild(closeBtn);
// 内容区域
const content = document.createElement('div');
content.id = 'editor-body';
content.style.cssText = `
padding: 15px;
display: flex;
flex-direction: column;
gap: 15px;
`;
// 创建文本输入框
const textarea = document.createElement('textarea');
textarea.name = 'body';
textarea.placeholder = '输入回复内容...';
textarea.style.cssText = `
width: 100%;
min-height: 200px;
max-height: 500px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
font-family: inherit;
resize: none;
overflow-y: auto;
box-sizing: border-box;
outline: none;
`;
// 自动调整高度的函数
const adjustTextareaHeight = () => {
// 先重置高度,让scrollHeight能正确计算
textarea.style.height = 'auto';
// 获取实际需要的高度(scrollHeight已经包含了padding)
const scrollHeight = textarea.scrollHeight;
// 设置新高度,最小200px,最大500px,减去1px避免底部多余空白
const newHeight = Math.max(200, Math.min(500, scrollHeight - 1));
textarea.style.height = newHeight + 'px';
// 如果内容超过最大高度,显示滚动条
if (scrollHeight > 500) {
textarea.style.overflowY = 'auto';
} else {
textarea.style.overflowY = 'hidden';
}
};
// 等待DOM更新后再设置初始高度
setTimeout(() => {
adjustTextareaHeight();
}, 0);
// 监听输入事件,动态调整高度
textarea.addEventListener('input', adjustTextareaHeight);
textarea.addEventListener('paste', () => {
setTimeout(adjustTextareaHeight, 0);
});
// 创建提交按钮区域
const footer = document.createElement('div');
footer.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 10px;
border-top: 1px solid #e0e0e0;
`;
const submitBtn = document.createElement('button');
submitBtn.type = 'submit';
submitBtn.textContent = '提交回复';
submitBtn.className = 'submit btn';
submitBtn.style.cssText = `
padding: 8px 20px;
background: #2196F3;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: background 0.3s;
`;
submitBtn.onmouseover = () => submitBtn.style.background = '#1976D2';
submitBtn.onmouseout = () => submitBtn.style.background = '#2196F3';
// 创建表单
const formElement = document.createElement('form');
formElement.action = formAction;
formElement.method = formMethod;
formElement.style.cssText = 'display: flex; flex-direction: column; gap: 15px;';
// 复制原始表单的所有隐藏字段
if (form) {
const hiddenInputs = form.querySelectorAll('input[type="hidden"]');
hiddenInputs.forEach(input => {
const clonedInput = input.cloneNode(true);
formElement.appendChild(clonedInput);
});
}
formElement.appendChild(textarea);
footer.appendChild(submitBtn);
formElement.appendChild(footer);
content.appendChild(formElement);
// 表单提交处理 - 将快捷回复框的内容复制到原始表单并提交
formElement.onsubmit = (e) => {
e.preventDefault();
// 确保使用快捷回复框中的 textarea 值
const quickReplyTextarea = formElement.querySelector('textarea[name="body"]');
if (!quickReplyTextarea || !quickReplyTextarea.value.trim()) {
alert('请输入回复内容');
return;
}
// 重新查找原始回复框和表单(确保获取最新的)
const currentOriginalCompose = document.getElementById('compose');
if (!currentOriginalCompose) {
alert('未找到原始回复框');
return;
}
// 查找原始表单(compose 可能是 form,或者 form 在 compose 内部)
let originalForm = currentOriginalCompose;
if (currentOriginalCompose.tagName !== 'FORM') {
originalForm = currentOriginalCompose.querySelector('form');
}
if (!originalForm) {
// 如果 compose 本身不是 form,也没有内部的 form,尝试查找父级 form
originalForm = currentOriginalCompose.closest('form');
}
if (!originalForm) {
alert('未找到原始表单');
return;
}
// 查找原始表单的 textarea
const originalTextarea = originalForm.querySelector('textarea[name="body"]');
if (!originalTextarea) {
alert('未找到原始回复框的文本框');
return;
}
// 保存快捷回复框的内容
const replyContent = quickReplyTextarea.value;
// 将内容设置到原始表单
originalTextarea.value = replyContent;
// 触发 input 事件,确保表单验证通过
originalTextarea.dispatchEvent(new Event('input', { bubbles: true }));
// 提交原始表单
originalForm.submit();
};
// 组装悬浮框
quickReplyBox.appendChild(header);
quickReplyBox.appendChild(content);
// 居中显示
const clientHeight = document.documentElement.clientHeight;
const clientWidth = document.documentElement.clientWidth;
const boxHeight = 400;
const boxWidth = 600;
const top = (clientHeight / 2) - (boxHeight / 2);
const left = (clientWidth / 2) - (boxWidth / 2);
quickReplyBox.style.top = `${top}px`;
quickReplyBox.style.left = `${left}px`;
// 拖动功能
let isDragging = false;
let currentX = 0;
let currentY = 0;
let initialX = 0;
let initialY = 0;
const handleMouseDown = (e) => {
if (e.button !== 0) return;
isDragging = true;
initialX = e.clientX - quickReplyBox.offsetLeft;
initialY = e.clientY - quickReplyBox.offsetTop;
};
const handleMouseMove = (e) => {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// 限制在窗口内
const maxX = window.innerWidth - quickReplyBox.offsetWidth;
const maxY = window.innerHeight - quickReplyBox.offsetHeight;
currentX = Math.max(0, Math.min(currentX, maxX));
currentY = Math.max(0, Math.min(currentY, maxY));
quickReplyBox.style.left = currentX + 'px';
quickReplyBox.style.top = currentY + 'px';
};
const handleMouseUp = () => {
isDragging = false;
};
const closeQuickReply = () => {
quickReplyBox.remove();
isQuickReplyOpen = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
header.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
closeBtn.onclick = closeQuickReply;
document.body.appendChild(quickReplyBox);
isQuickReplyOpen = true;
// 聚焦到文本框
setTimeout(() => textarea.focus(), 100);
}
// 打开数据分析弹窗
async function openDataAnalysisDialog() {
const { rows, maxBetIndex, hasRating } = await 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 titleContainer = document.createElement('div');
titleContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
`;
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;
`;
titleContainer.appendChild(title);
// 添加帖子标题
const topSpan = document.getElementById('top');
if (topSpan) {
const topicTitle = document.createElement('div');
topicTitle.textContent = topSpan.textContent.trim();
topicTitle.style.cssText = `
margin: 0;
padding: 0;
color: #666;
font-size: 14px;
font-weight: normal;
line-height: 1.4;
`;
titleContainer.appendChild(topicTitle);
}
header.appendChild(titleContainer);
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, hasRating);
};
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();
}
// 追加评分总和
// ratingSum: 可选的评分总和,如果不提供则自动计算
function appendRatingSummary(postBody, ratingSum = null) {
// 如果没有提供评分总和,则计算
if (ratingSum === null) {
ratingSum = calculateRatingSum(postBody);
}
// 如果评分为0,不显示
if (ratingSum === 0) {
return;
}
const container = postBody.parentElement;
if (!container) return;
// 查找包含"[评分]"的 div
const ratingDiv = Array.from(container.querySelectorAll('div')).find(div => {
const text = div.textContent || '';
return text.includes('[评分]');
});
if (!ratingDiv) {
return;
}
// 检查是否已经存在评分总和,如果存在则先移除
const existingSummary = ratingDiv.querySelector('.rating-summary');
if (existingSummary) {
existingSummary.remove();
}
const prefix = ratingSum > 0 ? '+' : '';
const summaryDiv = document.createElement('div');
summaryDiv.className = 'rating-summary';
summaryDiv.style.cssText = `
margin-top: 6px;
font-size: 14px;
font-weight: bold;
`;
summaryDiv.textContent = `评分总和:${prefix}${ratingSum}`;
ratingDiv.appendChild(summaryDiv);
}
// 在toolbox中添加复制按钮
function addCopyButtonsToToolbox() {
if (!isViewTopicPage()) return;
document.querySelectorAll('td.toolbox').forEach((toolbox) => {
// 如果已经添加过按钮,跳过
if (toolbox.querySelector('.copy-post-btn')) return;
// 设置toolbox样式,支持左右布局
let rightContainer = toolbox.querySelector('.toolbox-right');
if (!toolbox.dataset.styleSet) {
toolbox.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
text-align: left;
border: none !important;
`;
toolbox.dataset.styleSet = 'true';
// 创建右侧容器,存放原有按钮
if (!rightContainer) {
rightContainer = document.createElement('span');
rightContainer.className = 'toolbox-right';
rightContainer.style.cssText = 'margin-left: auto; text-align: right;';
// 将原有内容移到右侧容器
const existingElements = Array.from(toolbox.children);
existingElements.forEach(element => {
rightContainer.appendChild(element);
});
toolbox.appendChild(rightContainer);
}
}
// 确保动态添加的引用按钮也在右侧容器中
if (!rightContainer) {
rightContainer = toolbox.querySelector('.toolbox-right');
}
if (rightContainer) {
// 检查toolbox直接子元素中是否有非复制按钮的元素,移到右侧容器
Array.from(toolbox.children).forEach(child => {
if (child.className !== 'toolbox-right' &&
!child.classList.contains('copy-post-btn') &&
!child.classList.contains('copy-paste-post-btn')) {
rightContainer.appendChild(child);
}
});
}
// 查找对应的post body - 通过toolbox所在的table.main来查找
let postBody = null;
// 从toolbox向上查找table.main
const mainTable = toolbox.closest('table.main');
if (mainTable) {
// 在table.main的第一行tr中查找div[id$="body"]
const firstTr = mainTable.querySelector('tr');
if (firstTr) {
postBody = firstTr.querySelector('div[id$="body"]');
}
}
// 如果还没找到,尝试通过pid table查找
if (!postBody) {
// 向上查找包含pid的table
let current = toolbox.parentElement;
while (current && !postBody) {
const pidTable = current.querySelector('table[id^="pid"]') ||
(current.tagName === 'TABLE' && current.id && current.id.match(/^pid/) ? current : null);
if (pidTable && pidTable.id) {
const pidMatch = pidTable.id.match(/^pid(\d+)/);
if (pidMatch && pidMatch[1]) {
postBody = document.getElementById(`pid${pidMatch[1]}body`);
}
}
if (!postBody) {
current = current.parentElement;
if (!current || current === document.body) break;
}
}
}
if (!postBody) return;
const content = postBody.innerText || postBody.textContent || '';
// 创建"复制"按钮
const copyLink = document.createElement('a');
copyLink.href = '#';
copyLink.className = 'copy-post-btn';
copyLink.textContent = '复制';
copyLink.style.cssText = `
display: inline-block;
margin-right: 8px;
padding: 6px 12px;
background: #2196F3;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
vertical-align: middle;
line-height: 1.5;
min-width: 60px;
text-align: center;
`;
copyLink.onmouseover = function() {
this.style.background = '#1976D2';
};
copyLink.onmouseout = function() {
this.style.background = '#2196F3';
};
copyLink.onclick = (e) => {
e.preventDefault();
navigator.clipboard.writeText(content).then(() => {
const originalText = copyLink.textContent;
copyLink.textContent = '已复制';
setTimeout(() => {
copyLink.textContent = originalText;
}, 1500);
}).catch(() => alert('复制失败'));
};
// 创建"复制到快捷回复"按钮
const copyPasteLink = document.createElement('a');
copyPasteLink.href = '#';
copyPasteLink.className = 'copy-paste-post-btn';
copyPasteLink.textContent = '复制到快捷回复';
copyPasteLink.style.cssText = `
display: inline-block;
margin-right: 8px;
padding: 6px 12px;
background: #2196F3;
color: white;
text-decoration: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
vertical-align: middle;
line-height: 1.5;
min-width: 120px;
text-align: center;
`;
copyPasteLink.onmouseover = function() {
this.style.background = '#1976D2';
};
copyPasteLink.onmouseout = function() {
this.style.background = '#2196F3';
};
copyPasteLink.onclick = (e) => {
e.preventDefault();
navigator.clipboard.writeText(content).then(() => {
// 打开快捷回复框
if (!isQuickReplyOpen) {
openQuickReply();
}
// 等待快捷回复框加载后粘贴内容
setTimeout(() => {
const quickReplyTextarea = document.querySelector('#quick-reply-box textarea[name="body"]');
if (quickReplyTextarea) {
// 保存原始背景色
const originalBg = quickReplyTextarea.style.backgroundColor || '';
const originalTransition = quickReplyTextarea.style.transition || '';
// 设置过渡效果
quickReplyTextarea.style.transition = 'background-color 0.3s ease';
// 粘贴内容
quickReplyTextarea.value = content;
quickReplyTextarea.dispatchEvent(new Event('input', { bubbles: true }));
// // 背景色闪动效果:原始 -> 浅蓝 -> 原始,持续1秒
// quickReplyTextarea.style.backgroundColor = '#e3f2fd'; // 浅蓝色
// setTimeout(() => {
// quickReplyTextarea.style.backgroundColor = originalBg;
// // 恢复原始过渡设置
// setTimeout(() => {
// quickReplyTextarea.style.transition = originalTransition;
// }, 300);
// }, 700); // 700ms后恢复,加上过渡时间300ms,总共约1秒
// 聚焦到回复框并设置光标位置
quickReplyTextarea.focus();
quickReplyTextarea.setSelectionRange(content.length, content.length);
}
}, 200);
const originalText = copyPasteLink.textContent;
copyPasteLink.textContent = '已粘贴';
setTimeout(() => {
copyPasteLink.textContent = originalText;
}, 800);
}).catch(() => alert('复制失败'));
};
// 插入到最左侧(在rightContainer之前)
// 先插入"复制",再插入"复制到快捷回复",这样"复制"在左侧
const insertBefore = toolbox.querySelector('.toolbox-right') || toolbox.firstChild;
toolbox.insertBefore(copyLink, insertBefore);
toolbox.insertBefore(copyPasteLink, insertBefore);
});
}
// 全部标记为已读
async function markAllMessagesAsRead() {
try {
// 访问未读消息页面(这个URL会直接显示所有未读消息)
const baseUrl = `${window.location.origin}/messages.php`;
const unreadMessagesBaseUrl = `${baseUrl}?action=viewmailbox&box=1&unread=yes`;
// 显示加载提示
const loadingDiv = document.createElement('div');
loadingDiv.id = 'mark-all-read-loading';
loadingDiv.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(33, 150, 243, 0.9);
color: white;
padding: 20px 40px;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 10004;
font-size: 16px;
font-weight: bold;
`;
loadingDiv.textContent = '正在获取未读消息...';
document.body.appendChild(loadingDiv);
// 获取未读消息页面(第一页)
const response = await fetch(unreadMessagesBaseUrl, {
credentials: 'include',
headers: { 'Accept': 'text/html' }
});
if (!response.ok) {
loadingDiv.remove();
alert('获取消息页面失败,请检查网络连接');
return;
}
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// 收集所有未读消息ID
const messageIds = new Set();
// 处理当前页面的checkbox
const processPage = (pageDoc) => {
const checkboxes = pageDoc.querySelectorAll('input[type="checkbox"][name="messages[]"]');
checkboxes.forEach(checkbox => {
if (checkbox.value) {
messageIds.add(checkbox.value);
}
});
};
processPage(doc);
// 检查是否有分页,如果有则遍历所有分页
const pageLinks = doc.querySelectorAll('a[href*="viewmailbox"][href*="unread=yes"]');
const pageNumbers = new Set();
pageNumbers.add(0); // 第一页已经处理
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);
}
}
});
// 处理其他分页
if (pageNumbers.size > 1) {
loadingDiv.textContent = `正在检查 ${pageNumbers.size} 页未读消息...`;
for (const pageNum of pageNumbers) {
if (pageNum === 0) continue; // 第一页已经处理
const pageUrl = `${unreadMessagesBaseUrl}&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');
processPage(pageDoc);
}
} catch (e) {
// 忽略单个页面的错误,继续处理其他页面
handleError(`处理未读消息第${pageNum}页`, e);
}
}
}
if (messageIds.size === 0) {
loadingDiv.textContent = '没有未读消息';
loadingDiv.style.background = 'rgba(76, 175, 80, 0.9)';
setTimeout(() => loadingDiv.remove(), 2000);
return;
}
loadingDiv.textContent = `找到 ${messageIds.size} 条未读消息,正在标记为已读...`;
// 构建表单数据(按照表单的实际结构)
const formData = new FormData();
formData.append('action', 'moveordel');
formData.append('markread', '设为已读');
// 添加所有消息的ID(全选)
messageIds.forEach(id => {
formData.append('messages[]', id);
});
// 提交表单(提交到 messages.php,不带查询参数)
// 注意:即使 fetch 返回状态码 0 或抛出错误,表单实际上已经提交成功
// 这是因为服务器已经处理了请求,只是响应可能因为重定向或 CORS 问题无法正确返回
fetch(baseUrl, {
method: 'POST',
credentials: 'include',
body: formData,
redirect: 'follow' // 自动跟随重定向
}).catch(() => {
// 忽略 fetch 错误,因为请求实际上已经发送成功
});
// 直接显示成功消息(因为表单提交后,服务器已经处理了请求)
loadingDiv.textContent = `✓ 成功标记 ${messageIds.size} 条消息为已读`;
loadingDiv.style.background = 'rgba(76, 175, 80, 0.9)';
setTimeout(() => loadingDiv.remove(), 2000);
} catch (error) {
handleError('标记全部消息为已读', error);
const loadingDiv = document.getElementById('mark-all-read-loading');
if (loadingDiv) {
loadingDiv.textContent = '✗ 操作失败:' + error.message;
loadingDiv.style.background = 'rgba(244, 67, 54, 0.9)';
setTimeout(() => loadingDiv.remove(), 3000);
} else {
alert('操作失败:' + error.message);
}
}
}
// 自动为所有帖子显示评分总和
function autoDisplayRatingSummary() {
if (!isViewTopicPage()) {
return;
}
// 查找所有帖子body
const postBodies = document.querySelectorAll('div[id^="pid"][id$="body"]');
postBodies.forEach(postBody => {
// 计算评分总和,如果不为0则显示(传入已计算的值避免重复计算)
const ratingSum = calculateRatingSum(postBody);
if (ratingSum !== 0) {
appendRatingSummary(postBody, ratingSum);
}
});
}
// 检查指定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();
// 构建基础URL
const topicUrl = `${window.location.origin}/forums.php?action=viewtopic&topicid=${topicid}`;
try {
// 获取所有帖子数据
const posts = await fetchTopicPostsData(topicUrl);
// 检查当前用户是否已下注
const hasBet = posts.some(post => post.username === currentUsername);
// 收集特殊关注用户
if (followSet.size > 0) {
posts.forEach(post => {
if (followSet.has(post.username)) {
foundUsers.add(post.username);
}
});
}
return {
hasBet,
followUsers: Array.from(foundUsers)
};
} catch (error) {
handleError(`检查topicid ${topicid}`, error);
return { hasBet: false, followUsers: [] };
}
}
// 在论坛列表页面标记每个帖子是否已下注
async function markTopicBetStatus() {
// 只在viewforum页面生效
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('action') !== 'viewforum') {
return;
}
const currentUsername = getCurrentUsername();
if (!currentUsername) {
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);
}
}
});
// 对每个帖子检查是否已下注
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 => {
handleError(`检查topicid ${topicid} 下注状态`, err);
});
}
}
// 初始化
/**
* 初始化所有功能
*/
function initializeFeatures() {
// 重置第一楼跳过标志(页面初始化时)
hasSkippedFirstFloor = false;
createControlPanel();
autoHighlightFollowedPosts();
autoLoadNextPage();
checkValidBetAndDisableReply().catch(err => {
handleError('检查有效下注', err);
});
printBookmarkTopicIds();
highlightTopicIdRows();
markTopicBetStatus();
renderSpecialFollowTiles();
autoDisplayRatingSummary();
delayExecute(() => addCopyButtonsToToolbox(), 500);
// 在帖子页面预加载所有分页的数据(后台异步)
if (isViewTopicPage()) {
delayExecute(() => preloadCurrentTopicPostsData(), 1000);
}
// 使用MutationObserver监听动态加载的toolbox
if (!window.toolboxObserver) {
window.toolboxObserver = new MutationObserver(() => {
addCopyButtonsToToolbox();
});
window.toolboxObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
/**
* 初始化脚本
*/
function init() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeFeatures);
} else {
initializeFeatures();
}
}
// 启动脚本
init();
})();