洛谷私信图片/Bilibili 视频渲染
前言
本文章根据 私信图片渲染 的思路进行创作,
并在内容方面加入了 Bilibili 视频渲染功能,
修复了如“重复点击左侧同一用户的私信图片会消失”,“在点击查看更多消息以后,之前加载出来的图片会寄掉”等诸多 Bug。
安装 Tampermonkey(篡改猴)
Tampermonkey(篡改猴)允许用户自定义并增强您最喜爱的网页的功能。用户脚本是小型 JavaScript 程序,可用于向网页添加新功能或修改现有功能。使用 篡改猴,您可以轻松在任何网站上创建、管理和运行这些用户脚本。
如图,若你为 Microsoft Edge 浏览器,可直接点击“去商店”进行安装。而 Google Chrome 浏览器则需要霍尔沃兹环境,可以利用第三方途径下载 crx 格式插件,打开浏览器插件“开发者选项”,将 crx 格式插件拖入浏览器进行安装。
如图,在完成安装后,打开 Tampermonkey(篡改猴)插件的详情页面,开启“允许用户脚本”选项。
自此,Tampermonkey(篡改猴)已全部完成。
注:第二幅图中作者使用的为 Tampermonkey BETA(篡改猴测试版),但实际安装时可选择 Tampermonkey(篡改猴,即黑色皮肤),官方描述称“BETA”版可能会比正常版更新速度快,但可能会有 Bug,不能保证使用体验,所以作者建议使用 Tampermonkey(篡改猴)正常版。
安装脚本
(因为 Greasyfork 在国内因一些不可抗因素无法访问,所以本文使用直接复制代码来进行安装)
复制以下代码,点击拓展列表中的 Tampermonkey(篡改猴),点击“添加脚本”,窗口弹出后将代码粘贴,按“ Ctrl + S ”进行保存。
// ==UserScript==
// @name 洛谷私信markdown图片/视频渲染
// @version 1.2
// @description hhh
// @match https://www.luogu.com.cn/chat$
// @match https://www.luogu.com.cn/chat/*
// @author MlkMathew (fix by assistant)
// @license MIT
// @grant none
// @namespace https://greasyfork.org/users/1068192
// ==/UserScript==
(function() {
'use strict';
// ======================== 右侧滑出通知 ========================
function showNotification(message, duration = 3000) {
const oldNotify = document.getElementById('luogu-md-notify');
if (oldNotify) oldNotify.remove();
const notify = document.createElement('div');
notify.id = 'luogu-md-notify';
notify.textContent = message || '✨ 内容渲染完成';
Object.assign(notify.style, {
position: 'fixed',
top: '20px',
right: '0',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
color: '#333',
padding: '10px 20px',
borderRadius: '8px 0 0 8px',
fontSize: '14px',
fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
zIndex: '10000',
boxShadow: '-2px 2px 8px rgba(0,0,0,0.1)',
transition: 'transform 0.3s ease-in-out',
transform: 'translateX(100%)',
pointerEvents: 'none'
});
document.body.appendChild(notify);
notify.offsetHeight;
notify.style.transform = 'translateX(0)';
setTimeout(() => {
if (notify && notify.parentNode) {
notify.style.transform = 'translateX(100%)';
setTimeout(() => {
if (notify.parentNode) notify.remove();
}, 300);
}
}, duration);
}
// ======================== 获取正确的聊天消息(带区域排除) ========================
function getChatMessages() {
// 选择洛谷聊天区域中典型的消息容器
const mainArea = document.querySelector('main');
if (!mainArea) return [];
// 尝试多种可能的消息选择器
const selectors = [
'.message',
'.msg-item',
'[class*="message"]',
'.chat-message',
'[data-message-id]'
];
const allMessages = [];
for (const sel of selectors) {
const found = mainArea.querySelectorAll(sel);
if (found.length) {
allMessages.push(...found);
break; // 取第一个有效的
}
}
// 过滤掉侧边栏内的元素
return Array.from(allMessages).filter(el => {
return !el.closest('.side, aside, .user-list, .chat-user-list, .sidebar, .left-panel');
});
}
// ======================== 媒体渲染核心 ========================
function text(s) {
return document.createTextNode(s);
}
function image(url) {
const img = document.createElement('img');
img.src = url;
img.className = 'chat-image';
img.loading = 'lazy';
img.style.cssText = 'max-width:100%; height:auto; border-radius:4px; cursor:pointer;';
return img;
}
function bilibiliPlayer(raw) {
let rest = raw.replace(/^bilibili:/i, '').trim();
if (!rest) return null;
let query = '';
let base = rest;
const qIndex = rest.indexOf('?');
if (qIndex !== -1) {
base = rest.substring(0, qIndex);
query = rest.substring(qIndex + 1);
}
let aid = null, bvid = null;
const avMatch = base.match(/^av(\d+)$/i);
if (avMatch) {
aid = avMatch[1];
} else if (/^\d+$/.test(base)) {
aid = base;
} else if (base.toUpperCase().startsWith('BV')) {
bvid = base;
} else {
return null;
}
let page = 1;
let startTime = 0;
if (query) {
const params = new URLSearchParams(query);
if (params.has('page')) page = parseInt(params.get('page'), 10) || 1;
if (params.has('t')) startTime = parseInt(params.get('t'), 10) || 0;
}
let embedUrl;
if (bvid) {
embedUrl = `https://player.bilibili.com/player.html?bvid=${bvid}&page=${page}`;
if (startTime) embedUrl += `&t=${startTime}`;
} else if (aid) {
embedUrl = `https://player.bilibili.com/player.html?aid=${aid}&page=${page}`;
if (startTime) embedUrl += `&t=${startTime}`;
} else {
return null;
}
const container = document.createElement('div');
container.className = 'media-container bilibili-player';
container.style.cssText = 'position:relative; width:100%; max-width:640px; margin:0.5em 0;';
const iframe = document.createElement('iframe');
iframe.src = embedUrl;
iframe.className = 'bilibili-iframe';
iframe.style.cssText = 'width:100%; min-height:200px; border:none; border-radius:8px; background:#f5f5f5;';
iframe.setAttribute('allowfullscreen', 'true');
iframe.setAttribute('allow', 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share');
iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
iframe.setAttribute('sandbox', 'allow-same-origin allow-scripts allow-popups allow-forms allow-presentation');
container.appendChild(iframe);
return container;
}
function renderMedia(url) {
if (url.toLowerCase().startsWith('bilibili:')) {
const player = bilibiliPlayer(url);
if (player) return player;
const errSpan = document.createElement('span');
errSpan.textContent = `[B站视频解析失败: ${url}]`;
errSpan.style.color = '#999';
return errSpan;
} else {
return image(url);
}
}
// ======================== 安全解析(只替换纯文本中的标记) ========================
function processTextNode(textNode) {
const rawText = textNode.textContent;
if (!rawText) return null;
// 快速判断是否包含需要处理的标记
if (!rawText.includes(' {
if (s[j] === ']' && s[j + 1] === '(') {
closeBracket = j;
break;
}
}
if (closeBracket === -1) continue;
i = closeBracket; // 位置在 ']'
// 查找 )
let openParen = i + 2; // '(' 后一位
let closeParen = -1;
for (let j = openParen; j < s.length; j++) {
if (s[j] === ')') {
closeParen = j;
break;
}
}
if (closeParen === -1) continue;
const url = s.substring(openParen, closeParen);
fragment.appendChild(renderMedia(url));
p = closeParen + 1;
i = p - 1; // 循环会 i++,所以定位到下一个起始
}
}
// 剩余文本
if (p < s.length) {
fragment.appendChild(text(s.substr(p)));
}
return fragment;
}
// ======================== 处理单条消息(保留原有结构) ========================
function processMessage(msgElement) {
// 避免重复处理
if (msgElement.hasAttribute('data-md-processed')) return false;
// 检查是否在右侧聊天区域(已通过 getChatMessages 过滤,这里再保险)
if (msgElement.closest('.side, aside, .user-list, .chat-user-list, .sidebar, .left-panel')) return false;
try {
// 只遍历纯文本节点,不处理子元素(如已经存在的图片、链接)
let modified = false;
const walker = document.createTreeWalker(
msgElement,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
let node;
while (node = walker.nextNode()) {
textNodes.push(node);
}
for (const textNode of textNodes) {
const fragment = processTextNode(textNode);
if (fragment && fragment.childNodes.length > 0) {
// 用 fragment 替换该文本节点
textNode.parentNode.replaceChild(fragment, textNode);
modified = true;
}
}
if (modified) {
msgElement.setAttribute('data-md-processed', 'true');
return true;
}
return false;
} catch (e) {
console.error('[洛谷渲染] 处理消息时出错:', e);
return false;
}
}
function processAllMessages() {
const messages = getChatMessages();
console.log(`[洛谷渲染] 找到 ${messages.length} 条右侧消息`);
let count = 0;
for (const msg of messages) {
if (processMessage(msg)) count++;
}
if (count > 0) {
showNotification(`✨ 渲染了 ${count} 条消息`);
}
return count;
}
// ======================== 动态监听(只观察子节点添加,避免频繁触发) ========================
let observer = null;
function startObserver() {
if (observer) observer.disconnect();
const targetNode = document.querySelector('main');
if (!targetNode) {
console.warn('[洛谷渲染] 未找到 main 容器,延迟重试');
setTimeout(startObserver, 1000);
return;
}
observer = new MutationObserver((mutations) => {
// 只在有节点添加时处理,避免反复触发
let hasAdditions = false;
for (const mut of mutations) {
if (mut.type === 'childList' && mut.addedNodes.length > 0) {
hasAdditions = true;
break;
}
}
if (hasAdditions) {
// 延迟处理,等待DOM稳定
clearTimeout(window.lgChatTimer);
window.lgChatTimer = setTimeout(processAllMessages, 300);
}
});
observer.observe(targetNode, { childList: true, subtree: true });
console.log('[洛谷渲染] 已启动 MutationObserver');
}
// ======================== 加载更多按钮 ========================
function bindLoadMore() {
const loadmore = document.querySelector('.load-more');
if (loadmore && !loadmore.hasAttribute('data-listener')) {
loadmore.addEventListener('click', () => setTimeout(processAllMessages, 300));
loadmore.setAttribute('data-listener', 'true');
}
}
// ======================== 左侧用户切换监听 ========================
function bindSidebarUsers() {
const sidePanel = document.querySelector("#app > div.main-container > main > div > div.card.wrapper.padding-none > div.side > div.panel-content > div");
if (sidePanel) {
const userItems = sidePanel.childNodes;
for (let item of userItems) {
item.addEventListener('click', () => setTimeout(processAllMessages, 300));
}
}
}
// ======================== 图片预览 ========================
function setupImagePreview() {
document.body.addEventListener('click', (e) => {
const img = e.target.closest('.chat-image');
if (!img) return;
e.preventDefault();
e.stopPropagation();
const src = img.src;
const modal = document.createElement('div');
modal.className = 'image-preview-modal';
modal.style.cssText = 'position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.9); display:flex; justify-content:center; align-items:center; z-index:10001; cursor:pointer;';
const modalImg = document.createElement('img');
modalImg.src = src;
modalImg.style.cssText = 'max-width:90%; max-height:90%; object-fit:contain; border-radius:0;';
modal.appendChild(modalImg);
document.body.appendChild(modal);
modal.addEventListener('click', () => modal.remove());
modalImg.addEventListener('click', (e) => e.stopPropagation());
});
}
// ======================== 注入样式 ========================
function injectStyles() {
const style = document.createElement('style');
style.textContent = `
.chat-image {
max-width: 100%;
height: auto;
border-radius: 4px;
cursor: pointer;
transition: transform 0.2s ease;
}
.chat-image:hover {
transform: scale(1.02);
}
.bilibili-iframe {
width: 100%;
min-height: 200px;
border: none;
border-radius: 8px;
background-color: #f5f5f5;
}
.media-container {
margin: 5px 0;
}
.image-preview-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
display: flex;
justify-content: center;
align-items: center;
z-index: 10001;
cursor: pointer;
}
.image-preview-modal img {
max-width: 90%;
max-height: 90%;
object-fit: contain;
}
@media (max-width: 768px) {
.bilibili-iframe {
min-height: 150px;
}
}
`;
document.head.appendChild(style);
}
// ======================== 初始化 ========================
function init() {
injectStyles();
setupImagePreview();
startObserver();
bindLoadMore();
bindSidebarUsers();
setTimeout(processAllMessages, 800);
// 路由监听
let lastUrl = location.href;
setInterval(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(processAllMessages, 500);
}
}, 2000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// 暴露接口供调试
window.lgChatFix = {
process: processAllMessages,
version: '1.3'
};
console.log('[洛谷渲染] v1.2 已启动,使用反向排除法定位聊天区域');
})();
使用方法
图片使用方法:
)
Bilibili 视频使用方法:




渲染效果:
后记
此代码由
此代码并不完善,若有 Bug 或离谱问题,请在讨论区中留言,作者会积极完善 QwQ 。