// ==UserScript==
// @name 妖火回帖引用显示(他到底说了啥)
// @namespace https://www.yaohuo.me/bbs/userinfo.aspx?touserid=20740
// @version 1.1.0
// @description 在妖火论坛的回帖楼层中显示被引用楼层的内容,并可直接跳转到引用楼层
// @author SiXi
// @match https://www.yaohuo.me/bbs*
// @match https://yaohuo.me/bbs*
// @icon https://www.yaohuo.me/css/favicon.ico
// @license Apache 2
// @grant none
// ==/UserScript==
(function() {
'use strict';
const loadedFloors = new Map();
let isLoading = false;
let loadAttempts = 0;
const MAX_ATTEMPTS = 5;
function log(message, data = '') {
console.log(`[引用显示] ${message}`, data);
}
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
function getFloorNumber(text) {
const floorMap = {
"沙发": 1,
"椅子": 2,
"板凳": 3
};
return floorMap[text] || parseInt(text);
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function waitForNewContent() {
const startCount = document.querySelectorAll('.forum-post, .list-reply').length;
let attempts = 0;
while (attempts < 5) {
await sleep(800);
const currentCount = document.querySelectorAll('.forum-post, .list-reply').length;
if (currentCount > startCount) return true;
attempts++;
}
return false;
}
async function getQuotedContent(floorNumber, retryCount = 0) {
if (loadedFloors.has(floorNumber)) return loadedFloors.get(floorNumber);
if (isLoading) {
if (retryCount < 3) {
await sleep(1000);
return getQuotedContent(floorNumber, retryCount + 1);
}
return { text: '加载超时', isError: true };
}
const findFloorContent = () => {
const floors = document.querySelectorAll('.forum-post, .list-reply');
for (const floor of floors) {
const floorInfo = floor.querySelector('.floor-info, .floornumber');
if (!floorInfo) continue;
const currentFloor = getFloorNumber(floorInfo.textContent.replace(/[楼\s]/g, ''));
if (currentFloor === floorNumber) {
const contentElement = floor.querySelector('.retext') || floor.querySelector('.post-content');
let content = contentElement ? contentElement.innerHTML : '';
const tempDiv = document.createElement('div');
tempDiv.innerHTML = content;
const quoteElement = tempDiv.querySelector('.replay-other, .reother');
if (quoteElement) quoteElement.remove();
content = tempDiv.innerHTML.trim();
const userNick = floor.querySelector('.user-nick a, .renick a')?.textContent || '未知用户';
const userId = floor.querySelector('.user-id a, .reidlink .renickid')?.textContent.replace(/[()]/g, '') || '未知ID';
return {
userNick,
userId,
floorNumber: currentFloor,
content,
text: `<strong>${userNick}(${userId}) ${currentFloor}楼:</strong>${content}`
};
}
}
return null;
};
let content = findFloorContent();
if (content) {
loadedFloors.set(floorNumber, content);
return content;
}
if (loadAttempts >= MAX_ATTEMPTS) return { text: '加载失败:已达到最大尝试次数', isError: true };
const loadMoreBtn = document.querySelector('#KL_show_tip, #YH_show_tip');
if (!loadMoreBtn || loadMoreBtn.textContent.includes('没有了')) {
return { text: '未找到该楼层内容', isError: true };
}
log('点击加载更多按钮');
isLoading = true;
loadAttempts++;
try {
loadMoreBtn.click();
const hasNewContent = await waitForNewContent();
isLoading = false;
return hasNewContent ? await getQuotedContent(floorNumber) : { text: '加载失败', isError: true };
} catch (error) {
console.error('加载失败:', error);
return { text: '加载失败', isError: true };
} finally {
isLoading = false;
}
}
function addAnchorsToFloors() {
document.querySelectorAll('.forum-post, .list-reply').forEach(floor => {
const floorInfo = floor.querySelector('.floor-info, .floornumber');
if (!floorInfo) return;
const floorText = floorInfo.textContent.replace(/[楼\s]/g, '');
const floorNumber = getFloorNumber(floorText);
if (!floor.id && !isNaN(floorNumber)) {
floor.id = `floor-${floorNumber}`;
}
});
}
function scrollToFloor(floorNumber) {
const targetId = `floor-${floorNumber}`;
const targetFloor = document.getElementById(targetId);
if (targetFloor) {
targetFloor.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest'
});
const originalBg = targetFloor.style.backgroundColor;
targetFloor.style.transition = 'background-color 1s ease';
targetFloor.style.backgroundColor = '#fff3cd';
setTimeout(() => {
targetFloor.style.backgroundColor = originalBg;
setTimeout(() => {
targetFloor.style.transition = '';
}, 1000);
}, 2000);
return true;
}
console.warn(`未找到目标楼层: ${targetId}`);
return false;
}
function createImageOverlay(img) {
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.8);
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
cursor: zoom-out;
`;
const overlayImg = new Image();
overlayImg.src = img.src;
overlayImg.style.cssText = `
max-width: 90vw;
max-height: 90vh;
object-fit: contain;
cursor: default;
`;
overlay.appendChild(overlayImg);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
document.body.removeChild(overlay);
}
});
return overlay;
}
function processImage(img) {
const MAX_HEIGHT = 250;
const TARGET_WIDTH = 120;
const TARGET_HEIGHT = 200;
const applyScaling = () => {
if (img.naturalHeight <= MAX_HEIGHT) return;
// 计算缩放比例
const ratio = Math.min(
TARGET_WIDTH / img.naturalWidth,
TARGET_HEIGHT / img.naturalHeight
);
// 应用缩放样式
img.style.cssText = `
width: ${img.naturalWidth * ratio}px;
height: ${img.naturalHeight * ratio}px;
object-fit: contain;
display: inline-block;
`;
};
if (img.complete) {
applyScaling();
} else {
img.addEventListener('load', applyScaling);
}
}
function processImages(contentDiv) {
contentDiv.querySelectorAll('img').forEach(processImage);
}
function createQuoteBox(quoteData) {
const box = document.createElement('div');
box.className = 'quoted-content';
box.style.cssText = `
margin: 5px 0;
padding: 8px;
background-color: #f5f5f5;
border-left: 3px solid #4CAF50;
border-radius: 3px;
font-size: 14px;
color: #666;
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 10px;
`;
const contentDiv = document.createElement('div');
contentDiv.style.flex = '1';
const userInfo = typeof quoteData === 'string' ? '' : `<strong>${quoteData.userNick}(${quoteData.userId}) ${quoteData.floorNumber}楼:</strong>`;
const mainContent = typeof quoteData === 'string' ? quoteData : quoteData.content;
contentDiv.innerHTML = userInfo + mainContent;
const textContent = contentDiv.textContent;
if (textContent.length > 70 && !quoteData.isError) {
const mainContentText = typeof quoteData === 'string' ? quoteData : quoteData.content;
const truncatedContent = mainContentText.substring(0, 70) + '...';
contentDiv.innerHTML = userInfo + truncatedContent;
const expandBtn = document.createElement('button');
expandBtn.className = 'bt4';
expandBtn.textContent = '展开';
expandBtn.style.marginLeft = '5px';
expandBtn.style.cursor = 'pointer';
expandBtn.style.width = '50px';
expandBtn.addEventListener('click', () => {
contentDiv.innerHTML = userInfo + mainContent;
processImages(contentDiv);
expandBtn.remove();
});
contentDiv.appendChild(expandBtn);
}
processImages(contentDiv);
const jumpBtn = document.createElement('a');
jumpBtn.innerHTML = '跳转';
jumpBtn.style.cssText = `
margin-left: 10px;
color: #4CAF50;
cursor: pointer;
white-space: nowrap;
font-size: 12px;
text-decoration: none;
`;
if (quoteData.isError) jumpBtn.style.display = 'none';
jumpBtn.addEventListener('click', async (e) => {
e.preventDefault();
if (quoteData.floorNumber) {
if (!scrollToFloor(quoteData.floorNumber)) {
const loadBtn = document.querySelector('#KL_show_tip, #YH_show_tip');
if (loadBtn && !loadBtn.textContent.includes('没有了')) {
alert('正在加载更多内容...');
loadBtn.click();
await waitForNewContent();
let retry = 0;
while (retry++ < 3 && !scrollToFloor(quoteData.floorNumber)) {
await sleep(800);
}
if (!scrollToFloor(quoteData.floorNumber)) alert('未找到目标楼层');
}
}
}
});
box.appendChild(contentDiv);
box.appendChild(jumpBtn);
return box;
}
const handleQuoteReplies = throttle(async () => {
addAnchorsToFloors();
document.querySelectorAll('.forum-post, .list-reply').forEach(reply => {
if (reply.dataset.processed) return;
const quoteElement = reply.querySelector('.replay-other, .reother');
if (!quoteElement) return;
const floorMatch = quoteElement.textContent.match(/回复(\d+)楼|回复(沙发|椅子|板凳)/);
if (!floorMatch) return;
reply.dataset.processed = 'true';
const floorNumber = getFloorNumber(floorMatch[1] || floorMatch[2]);
getQuotedContent(floorNumber).then(content => {
if (!reply.querySelector('.quoted-content')) {
const replyContent = reply.querySelector('.retext') || reply.querySelector('.post-content');
if (replyContent) replyContent.prepend(createQuoteBox(content));
}
});
});
}, 500);
function unifyFontColor() {
document.querySelectorAll('.recontent font[color]').forEach(text => {
text.style.color = '#000';
});
}
const observer = new MutationObserver(mutations => {
mutations.forEach(mutation => {
if (mutation.addedNodes.length) {
setTimeout(() => {
addAnchorsToFloors();
handleQuoteReplies();
unifyFontColor();
}, 300);
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false
});
handleQuoteReplies();
unifyFontColor();
})();