- 海夜为眠2026-10-1415833
前面发了个帖,吐 L 站的回复有时候不太好跟:
没人觉得论坛的回复看起来很不方便吗?
引用、展开的回复和后面的楼层混在一起,同一段内容容易看好几遍。于是找 AI 帮忙折腾了个油猴脚本,样式又来回改了几轮,发出来给有同样困扰的佬试试。
先看效果,下面是现在的 v4.2.0。
桌面效果

手机端效果

入口在主帖下面的浏览量/参与者那一行,可以切两种模式:
- 楼中楼:把同一串回复放到一起,显示“回复谁”,头像之间加了条浅灰色指引线。更深的回复也不继续往右挤,手机上能留够看正文的地方。重复的引用和展开预览会隐藏。
- 引用:按原楼层顺序看,保留原来的引用;没引用的回复,会补上被回复评论的原文,已有同一条引用就不再补一份。
主帖和论坛其他布局保留,点赞、回复还是原来的按钮。楼主标签、Boost、解决方案这些,也一起收了下大小和间距。
右侧进度条也改了。楼中楼重新排过顺序,再拿原楼号当进度容易对不上,所以现在显示实际滚动的阅读百分比,可以点击、拖动。总帖数仍然包含楼中楼回复;长帖没全加载时,会写明“当前阅读段”和“已加载 N/共 M 帖”。手机上是个小进度标签,点开仍能按原楼号跳转。
安装: 装好篡改猴 → 添加新脚本 → 用下面的完整代码替换默认内容 → 保存、刷新帖子。扩展记得允许访问
linux.do。旧版直接替换代码就行。手机的样式也做了适配,不过浏览器或扩展得支持用户脚本。想临时关掉,篡改猴菜单里有“开启 / 关闭评论整理”。
实现方式,想看技术细节的佬可以展开
复用论坛已有的评论节点和已加载数据,通过 CSS 调整显示顺序和样式,原来的操作事件保留。引用缺少父评论时才补取对应内容,请求串行并限制频率。
长帖继续跟着论坛滚动加载,没有为了统计主评论数量去把整帖一次性拉下来。原始引用节点也保留,切换模式时再显示出来。
不同主题下可能还有间距需要调。有排版问题可以贴截图,或者直接改源码,完整代码放下面。
完整源码(v4.2.0):
展开代码,复制全部内容到篡改猴```
// ==UserScript==
// @name Linux DO 楼中楼阅读
// @namespace local.linuxdo.threaded-reading
// @version 4.2.0
// @description 楼中楼与引用切换、回复指引线和阅读进度,保留原生操作,适配手机。
// @match https://linux.do/*
// @run-at document-idle
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// @grant unsafeWindow
// ==/UserScript==(() => {
'use strict';GM_addStyle(`
.post-stream.ldt-thread { display: flex; flex-direction: column; }
.post-stream.ldt-thread > .ldt-modebar { order: 1; }
.post-stream.ldt-active .embedded-posts,
.post-stream.ldt-active .show-replies,
.post-stream.ldt-active .post__collapse-button,
.post-stream.ldt-active [data-ldt-depth] .reply-to-tab,
.post-stream.ldt-thread [data-ldt-depth] .cooked aside.quote { display: none !important; }.ldt-active > [data-ldt-depth] .post-menu-area { margin: 6px 0 8px; }
.ldt-active > [data-ldt-depth] .cooked p:last-of-type { margin-bottom: 0; }
.ldt-active > [data-ldt-depth] .discourse-boosts__post-menu:not(:has(*)) { padding: 0; }
.ldt-active > [data-ldt-depth] .names { flex-wrap: wrap; flex-direction: row; align-items: center; gap: 3px 6px; }
.ldt-active > [data-ldt-depth] .names > .first { order: 0; margin: 0 !important; }
.ldt-active > [data-ldt-depth] .names > .second,
.ldt-active > [data-ldt-depth] .names > .user-title { order: 2; }
.ldt-active > [data-ldt-depth] .names > .user-status-message-wrap { order: 1; }
.ldt-active > .topic-owner[data-ldt-depth] .cooked::after { content: none !important; display: none !important; }
.ldt-owner-label {
order: 1; display: inline-flex !important; align-items: center; align-self: center; margin: 0 !important;
font-size: 10px !important; font-weight: 400; line-height: 14px !important; height: 16px; box-sizing: border-box;
color: var(--primary-medium, #777); border: 1px solid var(--primary-low, #ddd);
border-radius: 3px; padding: 0 4px; white-space: nowrap;
}/ 指引线沿头像栏连接整组讨论,不随回复深度增加缩进。 /
.ldt-thread > [data-ldt-root],
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]):not(.post-stream--cloaked) { position: relative; }
.ldt-thread > [data-ldt-root]::before {
content: ""; display: block; position: absolute; left: 23px; top: 74px; bottom: -8px;
width: 2px; border-radius: 2px; background: var(--primary-low, #e2e4e7); pointer-events: none;
}
.ldt-thread > [data-ldt-first]:not(.post-stream--cloaked)::after {
content: ""; display: block; position: absolute; left: -9px; top: 0; width: 15px; height: 28px;
border-left: 2px solid var(--primary-low, #e2e4e7); border-bottom: 2px solid var(--primary-low, #e2e4e7);
border-bottom-left-radius: 12px; box-sizing: border-box; pointer-events: none;
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]):not([data-ldt-last]):not(.post-stream--cloaked)::before {
content: ""; display: block; position: absolute; left: 25px; top: 49px; bottom: -5px;
width: 2px; border-radius: 2px; background: var(--primary-low, #e2e4e7); pointer-events: none;
}/ 一条讨论只缩进一次;具体回复对象放在作者旁边。 /
.post-stream.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]):not(.post-stream--cloaked) {
--topic-avatar-width: 28px;
margin-block: 0; margin-inline-start: 32px !important; padding: 0 12px;
min-width: 0; box-sizing: border-box;
background: var(--primary-very-low, #f7f8fa);
}
.post-stream.ldt-thread > [data-ldt-first] { border-radius: 9px 9px 0 0; margin-top: 8px !important; }
.post-stream.ldt-thread > [data-ldt-last] { border-radius: 0 0 9px 9px; margin-bottom: 20px !important; padding-bottom: 6px; }
.post-stream.ldt-thread > [data-ldt-first][data-ldt-last] { border-radius: 9px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .topic-avatar {
width: 28px; padding: 13px 0 0; margin: 0 !important; border: 0 !important; position: static;
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .topic-avatar .avatar { width: 28px; height: 28px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .avatar-flair {
width: 12px; height: 12px; background-size: 12px; right: -2px; bottom: -2px;
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .boxed { padding: 0; border: 0; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .topic-body { padding: 12px 0 9px; border: 0; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .contents { padding-top: 0; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .topic-meta-data { padding: 0 0 4px 10px; gap: 8px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .names {
flex-wrap: wrap; align-items: center; gap: 3px 7px; min-width: 0; font-size: 13px;
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .names .second,
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .names .user-title { display: none; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-date { font-size: 12px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .cooked {
padding: 2px 0 0 10px; font-size: 15px; line-height: 1.65;
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .cooked p { margin: 0 0 .6em; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .cooked > :last-child,
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .cooked p:last-of-type { margin-bottom: 0; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area { margin: 6px 0 0; padding: 0 0 0 10px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area .btn {
min-height: 30px; padding: 5px 6px; font-size: 12px; color: var(--primary-medium, #777);
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area .d-icon { width: 14px; height: 14px; color: inherit; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area {
display: grid; grid-template-columns: minmax(0, 1fr) auto;
align-items: center; column-gap: 10px; row-gap: 0;
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-controls { grid-column: 2; grid-row: 1; width: auto; margin: 0; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .actions { display: flex; align-items: center; gap: 4px; float: none; margin: 0; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .small-user-list { grid-column: 1 / -1; grid-row: 2; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .extra-buttons { display: inline-flex; align-items: center; margin: 0; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .accepted-text {
display: inline-flex; align-items: center; gap: 4px; padding: 3px 7px;
border: 1px solid color-mix(in srgb, var(--success, #38955b) 15%, var(--secondary, #fff));
border-radius: 5px; box-sizing: border-box; font-size: 11px !important; line-height: 18px;
color: color-mix(in srgb, var(--success, #38955b) 40%, var(--primary, #333));
background: color-mix(in srgb, var(--success, #38955b) 8%, var(--secondary, #fff));
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .accepted-label { font-size: 11px !important; line-height: 18px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .accepted-text .d-icon { width: 12px; height: 12px; color: inherit; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .discourse-boosts__post-menu { grid-column: 1; grid-row: 1; padding: 0; min-width: 0; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .discourse-boosts__list { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .discourse-boosts__bubble {
display: inline-flex; align-items: center; gap: 5px; max-width: 100%;
padding: 3px 7px; margin: 0; border: 1px solid var(--primary-low, #ddd);
border-radius: 5px; background: var(--secondary, #fff); box-shadow: none;
font-size: 12px; line-height: 18px; box-sizing: border-box;
}
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .discourse-boosts__bubble > a { display: inline-flex; flex: 0 0 auto; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .discourse-boosts__bubble .avatar { width: 16px; height: 16px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .discourse-boosts__cooked { min-width: 0; font-size: inherit; line-height: inherit; overflow-wrap: anywhere; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .discourse-boosts__cooked p { margin: 0; }
.ldt-relation { order: 3; margin: 0 !important; font-size: 12px; font-weight: 400; color: var(--primary-medium, #777); line-height: 1.5; overflow-wrap: anywhere; }
.post-stream:not(.ldt-thread) .ldt-relation { display: none; }/ 融入原有统计行,不增加独立标题或大号切换面板。 /
.ldt-modebar { display: inline-flex; align-items: center; margin-inline-start: auto; flex: 0 0 auto; }
.ldt-modes { display: inline-flex; align-items: center; gap: 14px; }
.ldt-modes .btn {
position: relative; min-height: 32px; padding: 5px 0; border: 0;
background: transparent !important; box-shadow: none !important;
font-size: 12px; line-height: 1.5; color: var(--primary-medium, #777);
}
.ldt-modes .btn[aria-pressed="true"] { color: var(--tertiary, #3979a3); font-weight: 600; }
.ldt-modes .btn[aria-pressed="true"]::after {
content: ""; position: absolute; bottom: 2px; left: 0; right: 0;
height: 2px; border-radius: 2px; background: currentColor;
}
.ldt-modes .btn:hover { color: var(--primary, #333); }
.ldt-modes .btn:focus-visible { outline: 2px solid var(--tertiary, #3979a3); outline-offset: 4px; border-radius: 2px; }
.topic-map__stats:has(> .ldt-modebar) .topic-map__users-list { min-width: 0; overflow: hidden; }
.post-stream > .ldt-modebar { justify-content: flex-end; margin: 0; padding: 4px 0; }
[data-ldt-native-progress] { display: none !important; }
.ldt-reading-progress { display: flex; flex-direction: column; gap: 8px; height: 300px; width: 140px; max-width: 100%; }
.ldt-progress-caption, .ldt-progress-total { font-size: 12px; line-height: 1.5; color: var(--primary-medium, #777); }
.ldt-progress-total { font-size: 11px; }
.ldt-progress-rail { position: relative; flex: 1; width: 100%; padding: 0; border: 0; background: transparent; touch-action: none; cursor: ns-resize; }
.ldt-progress-rail::before { content: ""; position: absolute; left: 8px; top: 10px; bottom: 10px; width: 2px; background: var(--primary-low, #ddd); }
.ldt-progress-fill { position: absolute; left: 8px; top: 10px; width: 2px; height: calc((100% - 20px) * var(--ldt-progress, 0)); background: var(--tertiary, #3979a3); }
.ldt-progress-handle { position: absolute; left: 6px; top: calc((100% - 20px) * var(--ldt-progress, 0)); width: 6px; height: 20px; border-radius: 4px; background: var(--tertiary, #3979a3); }
.ldt-progress-percent { position: absolute; left: 23px; top: calc((100% - 20px) * var(--ldt-progress, 0)); font-size: 16px; font-weight: 600; line-height: 20px; color: var(--primary, #333); }
#topic-progress[data-ldt-mobile-progress] { display: flex; align-items: center; justify-content: center; box-sizing: border-box; height: auto; min-height: 40px; padding: 4px 10px; border: 1px solid var(--primary-low, #ddd); border-radius: 6px; background: var(--secondary, #fff); }
#topic-progress[data-ldt-mobile-progress] > .nums,
#topic-progress[data-ldt-mobile-progress] > .bg { display: none !important; }
.ldt-progress-navigation { display: block; margin-bottom: 6px; color: var(--primary-medium, #777); font-size: 12px; }
.ldt-progress-mobile { display: block; color: var(--primary, #333); font-size: 12px; font-weight: 600; line-height: 1.5; white-space: nowrap; }
.ldt-progress-rail:focus-visible { outline: 2px solid var(--tertiary, #3979a3); outline-offset: 3px; border-radius: 4px; }
.ldt-auto-quote blockquote { white-space: pre-wrap; overflow-wrap: anywhere; }
@media (max-width: 640px) {
.ldt-thread > [data-ldt-root] .cooked { padding-inline-start: 58px; }
.ldt-thread > [data-ldt-root] .post-menu-area { margin-inline-start: 58px; }
.ldt-thread > [data-ldt-first]:not(.post-stream--cloaked)::after { left: -5px; width: 9px; border-bottom-left-radius: 9px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]):not([data-ldt-last]):not(.post-stream--cloaked)::before { left: 23px; }
.post-stream.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]):not(.post-stream--cloaked) { margin-inline-start: 28px !important; padding-inline: 10px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .topic-meta-data { padding-inline-start: 0; margin-inline-start: 38px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .cooked { padding-inline-start: 38px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area { padding-inline-start: 38px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area .btn { min-height: 40px; min-width: 36px; font-size: 13px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .ldt-relation { flex-basis: 100%; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area:has(.discourse-boosts__bubble) { grid-template-columns: minmax(0, 1fr); row-gap: 5px; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area:has(.discourse-boosts__bubble) .post-controls { grid-column: 1; grid-row: 2; justify-self: end; max-width: 100%; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .post-menu-area:has(.discourse-boosts__bubble) .small-user-list { grid-row: 3; }
.ldt-thread > [data-ldt-depth]:not([data-ldt-depth="0"]) .actions { flex-wrap: wrap; justify-content: flex-end; }
.ldt-modes { gap: 16px; }
.ldt-modes .btn { min-height: 40px; min-width: 44px; justify-content: center; }
}
`);let mode = typeof GM_getValue === 'function' ? GM_getValue('reading-mode', 'thread') : 'thread';
if (!['thread', 'quote'].includes(mode)) mode = 'thread';
let enabled = true;
let state;
let timer;
let progressFrame;
const observer = new MutationObserver(schedule);
const observe = () => observer.observe(document.body, { childList: true, subtree: true });function topicId() {
const parts = location.pathname.split('/').filter(Boolean);
return parts[0] === 't' ? Number(/^\d+$/.test(parts[1] || '') ? parts[1] : parts[2]) || 0 : 0;
}function element(tag, className, text) {
const node = document.createElement(tag);
node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}function merge(s, posts) {
for (const p of posts || []) {
if (p.post_number > 0) s.posts.set(Number(p.post_number), {
number: Number(p.post_number), parent: Number(p.reply_to_post_number) || 0,
author: p.display_username || p.name || p.username || '', cooked: p.cooked || '',
});
}
}// 直接复用论坛已加载的数据,评论 DOM 和原生事件保持不变。
function snapshot(s) {
try {
const page = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
const model = page.Discourse?.lookup('controller:topic')?.model;
if (Number(model?.id) !== s.id) return false;
s.total = Number(model.postStream?.filteredPostsCount || model.posts_count) || 0;
merge(s, model.postStream?.posts);
return s.posts.size > 0;
} catch { return false; }
}async function load(s, parent = 0) {
if (s.requested.has(parent)) return;
s.requested.add(parent);
s.queue = s.queue.then(async () => {
const delay = Math.max(0, s.nextRequest - Date.now());
if (delay) await new Promise(resolve => setTimeout(resolve, delay));
if (s !== state || s.abort.signal.aborted) return;
s.nextRequest = Date.now() + 1000;
try {
const response = await fetch(parent ?/t/${s.id}/${parent}.json:/t/${s.id}.json, {
credentials: 'same-origin', signal: s.abort.signal,
headers: { Accept: 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
});
if (!response.ok) return;
const data = await response.json();
if (s !== state) return;
s.total = Number(data.posts_count) || s.total;
merge(s, data.post_stream?.posts);
schedule();
} catch { / 权限或网络失败时仍保留原生评论。 / }
});
}function layout(s, wrappers) {
const posts = new Map(s.posts);
for (const node of wrappers) {
const n = Number(node.dataset.postNumber);
if (!posts.has(n)) posts.set(n, { number: n, parent: 0 });
}
const children = new Map();
const roots = [];
for (const p of [...posts.values()].sort((a, b) => a.number - b.number)) {
if (p.number === 1) continue;
if (p.parent > 1 && p.parent < p.number && posts.has(p.parent)) {
if (!children.has(p.parent)) children.set(p.parent, []);
children.get(p.parent).push(p);
} else roots.push(p);
}
const result = new Map();
let rank = 2;
for (const root of roots) {
const discussion = [];
function collect(p, depth) {
discussion.push({ post: p, depth });
for (const child of children.get(p.number) || []) collect(child, depth + 1);
}
collect(root, 0);
discussion.sort((a, b) => a.post.number - b.post.number);
for (const { post, depth } of discussion) result.set(post.number, { rank: rank++ * 10, depth, root: root.number });
}
return result;
}function quoteText(cooked) {
const body = document.createElement('div');
body.innerHTML = cooked;
body.querySelectorAll('aside.quote, script, style, iframe').forEach(n => n.remove());
body.querySelectorAll('br').forEach(n => n.replaceWith('\n'));
body.querySelectorAll('p, li, pre, h1, h2, h3, h4, h5, h6').forEach(n => n.append('\n'));
body.querySelectorAll('img').forEach(n => n.replaceWith(n.alt || '[图片]'));
return body.textContent.trim().replace(/\n{3,}/g, '\n\n');
}function updateQuote(s, wrapper, post) {
const cooked = [...wrapper.querySelectorAll('.topic-body > .contents > .cooked, .topic-body > .cooked')]
.find(n => !n.closest('.embedded-posts'));
if (!cooked) return;
const owned = cooked.querySelector(':scope > .ldt-auto-quote');
const authored = [...cooked.querySelectorAll('aside.quote:not(.ldt-auto-quote)')].some(q =>
Number(q.dataset.post) === post?.parent && (!q.dataset.topic || Number(q.dataset.topic) === s.id));
if (mode !== 'quote' || !post?.parent || authored) { owned?.remove(); return; }
const parent = s.posts.get(post.parent);
if (!parent?.cooked) { owned?.remove(); load(s, post.parent); return; }
const text = quoteText(parent.cooked);
if (!text) { owned?.remove(); return; }
const signature = JSON.stringify([post.parent, parent.author, text]);
if (owned?.dataset.signature === signature) return;
owned?.remove();
const quote = element('aside', 'quote ldt-auto-quote');
quote.dataset.signature = signature;
quote.dataset.post = post.parent;
quote.dataset.topic = s.id;
quote.append(element('div', 'title',${parent.author || '被回复的评论'}:),
element('blockquote', '', text));
cooked.prepend(quote);
}function modebar(s) {
const bar = element('div', 'ldt-modebar');
const group = element('div', 'ldt-modes');
group.setAttribute('role', 'group');
group.setAttribute('aria-label', '评论阅读方式');
for (const [value, label] of [['thread', '楼中楼'], ['quote', '引用']]) {
const button = element('button', 'btn btn-flat', label);
button.type = 'button';
button.dataset.mode = value;
button.title = value === 'thread' ? '按讨论关系阅读' : '按楼层顺序和引用阅读';
button.addEventListener('click', () => {
const anchor = [...s.stream.querySelectorAll(':scope > [data-ldt-depth]')].find(n => n.getBoundingClientRect().bottom > 0);
const top = anchor?.getBoundingClientRect().top;
mode = value;
if (typeof GM_setValue === 'function') GM_setValue('reading-mode', mode);
refresh();
if (anchor?.isConnected && top !== undefined) window.scrollBy(0, anchor.getBoundingClientRect().top - top);
});
group.append(button);
}
bar.append(group);
return bar;
}function clearProgress(s) {
s.progress?.remove();
s.progress = null;
s.mobileProgress?.remove();
s.mobileProgress = null;
s.navigationNote?.remove();
s.navigationNote = null;
document.querySelectorAll('[data-ldt-native-progress], [data-ldt-mobile-progress]').forEach(n => { delete n.dataset.ldtNativeProgress; delete n.dataset.ldtMobileProgress; });
}function updateProgress(s) {
if (mode !== 'thread') { clearProgress(s); return; }
const candidate = document.querySelector('.timeline-scrollarea');
const fullscreen = candidate?.closest('.timeline-fullscreen');
const native = fullscreen ? null : candidate;
if (fullscreen) {
delete candidate.dataset.ldtNativeProgress;
if (!s.navigationNote) s.navigationNote = element('span', 'ldt-progress-navigation', '原楼号导航 · 含回复');
if (candidate.previousElementSibling !== s.navigationNote) candidate.before(s.navigationNote);
} else s.navigationNote?.remove();
const mobile = document.querySelector('#topic-progress');
if (!native) s.progress?.remove();
if (!mobile) s.mobileProgress?.remove();
if (!native && !mobile) return;
if (native && !s.progress) {
const panel = element('div', 'ldt-reading-progress');
const rail = element('button', 'ldt-progress-rail');
rail.type = 'button';
rail.setAttribute('role', 'slider');
rail.setAttribute('aria-label', '阅读位置');
rail.setAttribute('aria-orientation', 'vertical');
rail.setAttribute('aria-valuemin', '0');
rail.setAttribute('aria-valuemax', '100');
rail.append(element('span', 'ldt-progress-fill'), element('span', 'ldt-progress-handle'), element('span', 'ldt-progress-percent'));
const jump = ratio => window.scrollTo({ top: s.progressStart + Math.max(0, Math.min(1, ratio)) * s.progressRange, behavior: 'auto' });
const move = event => {
const r = rail.getBoundingClientRect();
if (r.height) jump((event.clientY - r.top - 10) / Math.max(1, r.height - 20));
};
rail.addEventListener('pointerdown', event => {
if (event.button !== 0) return;
event.preventDefault();
rail.focus();
rail.setPointerCapture(event.pointerId);
move(event);
});
rail.addEventListener('pointermove', event => { if (rail.hasPointerCapture(event.pointerId)) move(event); });
rail.addEventListener('keydown', event => {
const value = Number(rail.getAttribute('aria-valuenow')) / 100;
const target = { ArrowUp: value - .03, ArrowDown: value + .03, PageUp: value - .15, PageDown: value + .15, Home: 0, End: 1 }[event.key];
if (target !== undefined) { event.preventDefault(); jump(target); }
});
panel.append(element('span', 'ldt-progress-caption'), rail, element('span', 'ldt-progress-total'));
s.progress = panel;
}
if (native) {
native.dataset.ldtNativeProgress = '';
if (native.nextElementSibling !== s.progress) native.after(s.progress);
s.progress.style.height = native.style.height || '300px';
}
const rect = s.stream.getBoundingClientRect();
const header = Math.max(0, document.querySelector('.d-header')?.getBoundingClientRect().bottom || 0);
s.progressStart = Math.max(0, rect.top + window.scrollY - header);
s.progressRange = Math.max(0, rect.height - Math.max(1, window.innerHeight - header));
const ratio = s.progressRange ? Math.max(0, Math.min(1, (window.scrollY - s.progressStart) / s.progressRange)) : (rect.bottom <= window.innerHeight ? 1 : 0);
const loaded = s.stream.querySelectorAll(':scope > [data-post-number]').length;
const partial = !s.total || loaded < s.total;
const count = s.total ? (partial ?已加载 ${loaded} / 共 ${s.total} 帖:共 ${s.total} 帖 · 含回复) :已加载 ${loaded} 帖 · 含回复;
const percent = Math.round(ratio * 100);
if (native) {
for (const [selector, text] of [['.ldt-progress-caption', partial ? '当前阅读段' : '阅读进度'], ['.ldt-progress-total', count], ['.ldt-progress-percent',${percent}%]]) {
const node = s.progress.querySelector(selector);
if (node.textContent !== text) node.textContent = text;
}
const rail = s.progress.querySelector('.ldt-progress-rail');
rail.style.setProperty('--ldt-progress', ratio);
rail.setAttribute('aria-valuenow', percent);
rail.setAttribute('aria-valuetext',${partial ? '当前阅读段' : '阅读进度'} ${percent}%,${count});
}
if (mobile) {
if (!s.mobileProgress) s.mobileProgress = element('span', 'ldt-progress-mobile');
mobile.dataset.ldtMobileProgress = '';
if (s.mobileProgress.parentElement !== mobile) mobile.append(s.mobileProgress);
const text =${partial ? '本段' : '阅读'} ${percent}% · ${partial && s.total ?${loaded}/${s.total}: s.total || loaded}帖;
if (s.mobileProgress.textContent !== text) s.mobileProgress.textContent = text;
s.mobileProgress.title =${partial ? '当前阅读段' : '阅读进度'},${count};
}
}function scheduleProgress() {
if (!state || mode !== 'thread' || progressFrame) return;
progressFrame = requestAnimationFrame(() => { progressFrame = 0; if (state) updateProgress(state); });
}function cleanup() {
if (!state) return;
state.abort.abort();
clearProgress(state);
state.bar.remove();
state.style.remove();
const stream = state.stream;
stream.classList.remove('ldt-active', 'ldt-thread');
stream.querySelectorAll('.ldt-relation, .ldt-owner-label, .ldt-auto-quote').forEach(n => n.remove());
for (const n of stream.children) {
delete n.dataset.ldtDepth;
delete n.dataset.ldtRank;
delete n.dataset.ldtFirst;
delete n.dataset.ldtLast;
delete n.dataset.ldtRoot;
}
state = null;
}function refresh() {
observer.disconnect();
const id = topicId();
const stream = document.querySelector('.post-stream');
if (!enabled || !id || !stream) { cleanup(); observe(); return; }
if (state?.id !== id || state.stream !== stream) {
cleanup();
state = { id, stream, posts: new Map(), requested: new Set(), abort: new AbortController(), queue: Promise.resolve(), nextRequest: 0 };
state.bar = modebar(state);
state.style = element('style', 'ldt-layout');
document.head.append(state.style);
}
const s = state;
if (!snapshot(s) && !s.posts.size) load(s);
const wrappers = [...stream.children].filter(n => Number(n.dataset.postNumber) > 0);
const positions = layout(s, wrappers);
stream.classList.add('ldt-active');
stream.classList.toggle('ldt-thread', mode === 'thread');
const op = wrappers.find(n => Number(n.dataset.postNumber) === 1);
const stats = op?.querySelector('.topic-map__stats');
if (stats && s.bar.parentElement !== stats) stats.append(s.bar);
else if (!stats && op && op.nextElementSibling !== s.bar) op.after(s.bar);
else if (!stats && !op && stream.firstElementChild !== s.bar) stream.prepend(s.bar);
s.bar.querySelectorAll('button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.mode === mode)));
const rules = [];
const branches = new Map();
for (const wrapper of wrappers) {
const number = Number(wrapper.dataset.postNumber);
if (number === 1) continue;
const post = s.posts.get(number);
const position = positions.get(number);
wrapper.dataset.ldtDepth = position.depth;
rules.push(.post-stream.ldt-thread > [data-post-number="${number}"] { order: ${position.rank}; });
delete wrapper.dataset.ldtFirst;
delete wrapper.dataset.ldtLast;
delete wrapper.dataset.ldtRoot;
if (position.depth > 0) {
if (!branches.has(position.root)) branches.set(position.root, []);
branches.get(position.root).push(wrapper);
}
const names = [...wrapper.querySelectorAll('.topic-body > .topic-meta-data .names')]
.find(n => !n.closest('.embedded-posts'));
let badge = names?.querySelector('.ldt-owner-label');
if (names && wrapper.classList.contains('topic-owner')) {
if (!badge) badge = element('span', 'ldt-owner-label', '楼主');
const author = names.querySelector(':scope > .first');
if (author && author.nextElementSibling !== badge) author.after(badge);
else if (!author && badge.parentElement !== names) names.append(badge);
} else badge?.remove();
let relation = wrapper.querySelector('.ldt-relation');
if (post?.parent > 1 && names) {
if (!relation) { relation = element('span', 'ldt-relation'); names.append(relation); }
const parent = s.posts.get(post.parent);
const label = parent?.author ?回复 ${parent.author}: '回复此前的评论';
if (relation.textContent !== label) relation.textContent = label;
} else relation?.remove();
updateQuote(s, wrapper, post);
}
for (const [root, replies] of branches) {
const parent = wrappers.find(n => Number(n.dataset.postNumber) === root);
if (parent && !parent.classList.contains('post-stream--cloaked')) parent.dataset.ldtRoot = '';
replies.sort((a, b) => positions.get(Number(a.dataset.postNumber)).rank - positions.get(Number(b.dataset.postNumber)).rank);
replies[0].dataset.ldtFirst = '';
replies.at(-1).dataset.ldtLast = '';
}
// 原生加载边界、时间分隔线仍跟随对应楼层,不参与回复关系排序。
const children = [...stream.children];
children.forEach((node, i) => {
if (node === s.bar || node.dataset.postNumber) return;
const next = children.slice(i + 1).find(n => Number(n.dataset.postNumber) > 0);
node.dataset.ldtRank = i;
const rank = next ? (positions.get(Number(next.dataset.postNumber))?.rank || 0) - 1 : (positions.size + 3) * 10;
rules.push(.post-stream.ldt-thread > [data-ldt-rank="${i}"] { order: ${rank}; });
});
const css = rules.join('\n');
if (s.style.textContent !== css) s.style.textContent = css;
updateProgress(s);
observe();
}function schedule() { clearTimeout(timer); timer = setTimeout(refresh, 80); }
if (typeof GM_registerMenuCommand === 'function') GM_registerMenuCommand('开启 / 关闭评论整理', () => { enabled = !enabled; refresh(); });
window.addEventListener('popstate', schedule);
window.addEventListener('scroll', scheduleProgress, { passive: true });
window.addEventListener('resize', scheduleProgress);
setInterval(() => { if (enabled && topicId() !== (state?.id || 0)) schedule(); }, 1000);
refresh();
})();主楼 - 文月2026-10-1420366
之前有段时间还原生自带这种看帖模式的,唉
- 拾叁2026-10-144675
- 海夜为眠2026-10-1415833
唉,加了脚本现在看舒服多了。。
- lekko2026-10-146932
哎我记得站里不是支持 nested 风格吗?怎么找不到了
- zhengnl2026-10-1422043
已经严肃使用,好用
- BadWomen2026-10-1416386
- 霜枫_王2026-10-148445
以前好像有一个自带看贴的脚本,现在找不到了,大佬辛苦你也整一个。
- HaxIOX2026-10-1422044
已严肃使用,感谢佬
- Blazer2026-10-1416860
这个好啊,不用一个个翻下去了
- Paulwalker2026-10-1422045
一直都觉得这个很好,没明白l站楼中楼为什么这样设计
- Adularia_Z2026-10-1421334
已严肃使用 效果超棒! 感谢佬
- 海夜为眠2026-10-1415833
对的,没找到
- 海夜为眠2026-10-1415833
自带看贴是啥意思
- Kevin90102026-10-1417155
这个脚本确实很舒服
- Johnnnnn2026-10-1422046
非常好用,感谢佬的分享!
- Asuna2026-10-1412640
太好用了,感谢佬友分享 ^-^
- IAmor2026-10-149470
舒服了 太舒服了 佬牛逼~
- Wlong_yan2026-10-1417216
使用了!谢谢佬
- L_Ysunshine1232026-10-1412192
这个确实好啊,不用点开下拉箭头在看了,直接一目了然,牛的佬

