标题党太多了,有没有办法不点开贴子直接预览主贴内容

如题,其实大多数人的贴子内容也就是一句话的事,但标题总讲不清楚,站长能不能给贴子在主界面开个预览,主贴前一定字数(比如30字就行)直接显示在标题下面

3 Likes

用插件啊
Linux do增强插件,你值得拥有

这插件我装了,它这个预览和默认逻辑区别不大吧,也要手动点击打开和关闭,我希望的逻辑是主界面直接显示就可以,完全省略用户自己的操作

像置顶公告那种?

1 Like

对的,简洁省事


这样吗

2 Likes

以前电脑版是所有帖子都支持的 后来不支持了
像我手机版现在是没有任何帖子会提供预览功能

1 Like

其实点开瞄一眼就回退也影响不大,预览功能会不会比较吃机器性能呢?

对的对的,教练我就想要这个

能不能梦回一下 :laughing:

该帖子仅回复后可见 :tieba_025:

// ==UserScript==
// @name         Linux.do 话题自动预览(v2)
// @namespace    https://tampermonkey.net/
// @version      2.0
// @description  自动显示话题首帖,支持懒加载、缓存、展开动画和清除缓存按钮!
// @author       星缘
// @match        https://linux.do/*
// @grant        GM_xmlhttpRequest
// @connect      linux.do
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    const CACHE_EXPIRY_HOURS = 24;

    // 插入清除缓存按钮
    function insertClearCacheButton() {
        const btn = document.createElement('button');
        btn.textContent = '🧹 清除预览缓存';
        btn.style.cssText = `
            position: fixed;
            bottom: 20px;
            right: 20px;
            z-index: 9999;
            padding: 8px 12px;
            font-size: 14px;
            background: #444;
            color: #fff;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            opacity: 0.8;
        `;
        btn.onclick = () => {
            Object.keys(localStorage).forEach(k => {
                if (k.startsWith('preview_')) localStorage.removeItem(k);
            });
            alert('已清除所有话题预览缓存!请刷新页面查看效果~');
        };
        document.body.appendChild(btn);
    }

    // 获取话题ID
    function getTopicId(href) {
        const match = href.match(/\/t\/[^/]+\/(\d+)/);
        return match ? match[1] : null;
    }

    // 插入预览内容 + 折叠按钮
    function insertPreview(row, html) {
        if (row.querySelector('.topic-preview')) return;

        const wrapper = document.createElement('div');
        wrapper.className = 'topic-preview';
        wrapper.style.cssText = `
            overflow: hidden;
            transition: max-height 0.4s ease, opacity 0.4s ease;
            background: #f7f7f7;
            border: 1px solid #ddd;
            border-radius: 5px;
            margin-top: 8px;
            padding: 10px;
            max-height: 1000px;
            opacity: 1;
        `;

        const closeBtn = document.createElement('button');
        closeBtn.textContent = '收起预览';
        closeBtn.style.cssText = `
            float: right;
            font-size: 12px;
            margin-bottom: 8px;
            background: #eee;
            border: 1px solid #aaa;
            border-radius: 3px;
            cursor: pointer;
        `;
        closeBtn.onclick = () => {
            wrapper.style.maxHeight = '0';
            wrapper.style.opacity = '0';
            setTimeout(() => wrapper.remove(), 400);
        };

        wrapper.innerHTML = html;
        wrapper.prepend(closeBtn);
        row.appendChild(wrapper);
    }

    // 缓存读取与写入
    function getCachedPreview(topicId) {
        const cache = localStorage.getItem(`preview_${topicId}`);
        if (!cache) return null;
        const { html, timestamp } = JSON.parse(cache);
        const age = (Date.now() - timestamp) / (1000 * 60 * 60);
        return age < CACHE_EXPIRY_HOURS ? html : null;
    }

    function setCachedPreview(topicId, html) {
        localStorage.setItem(`preview_${topicId}`, JSON.stringify({
            html,
            timestamp: Date.now()
        }));
    }

    // 请求 JSON 数据并渲染
    function fetchAndInsertPreview(row, topicId) {
        const cached = getCachedPreview(topicId);
        if (cached) {
            insertPreview(row, cached);
            return;
        }

        const url = `https://linux.do/t/${topicId}.json`;
        GM_xmlhttpRequest({
            method: 'GET',
            url,
            onload: res => {
                try {
                    const json = JSON.parse(res.responseText);
                    const cooked = json.post_stream.posts[0].cooked;
                    insertPreview(row, cooked);
                    setCachedPreview(topicId, cooked);
                } catch (err) {
                    console.error('预览加载失败', err);
                }
            }
        });
    }

    // 懒加载处理器
    const observer = new IntersectionObserver(entries => {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                const row = entry.target;
                const link = row.querySelector('a.title');
                if (link && link.href) {
                    const topicId = getTopicId(link.href);
                    if (topicId) {
                        observer.unobserve(row);
                        fetchAndInsertPreview(row, topicId);
                    }
                }
            }
        }
    }, {
        rootMargin: '150px',
        threshold: 0.1
    });

    function scanTopics() {
        const rows = document.querySelectorAll('.topic-list tr:not(.preview-observed)');
        rows.forEach(row => {
            const link = row.querySelector('a.title');
            if (link && link.href) {
                row.classList.add('preview-observed');
                observer.observe(row);
            }
        });
    }

    const mutationObserver = new MutationObserver(scanTopics);
    mutationObserver.observe(document.body, { childList: true, subtree: true });
    scanTopics();
    insertClearCacheButton();
})();

闲的没事加了一个小动画 可以试毒一下

我这边手机测试没什么问题 目前的逻辑是直接把第一个帖子的内容给你展示出来 相当于直接在主界面刷所有帖子~
如果需要前30字预览的话可以改一下下~
预览的缓存是一整天 如果不想看了可以清除
动画好像没有成功展示 问题不大

3 Likes

性能不是问题吧,毕竟每页显示数量都会限制的;对于标题党的贴子点开总感觉有点亏

说的就是我 :rofl:

// ==UserScript==
// @name         Linux do 在话题列表下方显示话题内容
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  在话题/搜索列表下方显示话题内容,点击重新加载内容开始加载
// @match        https://linux.do/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-end
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
// ==/UserScript==

(function () {
    'use strict';

    require("discourse/routes/topic").default.disableReplaceState = true;

    var theDate = getFormattedDate();
    var today = new Date();
    var yesterday = theDate[1];


    //加载摘要的间隔事件, 单位毫秒
    var waitTime = 2000;

    //新增内容
    var addDiv = '<div is_insert="1" style="background: #C6FFC6;width: 60vw;"></div>';

    const linuxDo = "linux.do";
    //加载的内容
    var contentCache = {};
    var yesterdayCache = {};
    var totalCache = {};
    //保存的数据
    var linuxDoData = {};

    function init() {
        console.log('初始化数据')
        linuxDoData = GM_getValue(linuxDo, {});
        Object.keys(linuxDoData).forEach(dateKey => {
            const keyDate = new Date(dateKey);
            const diffDays = Math.floor((today - keyDate) / (1000 * 60 * 60 * 24));
            if (diffDays > 3) {
                delete linuxDoData[dateKey];
            } else {
                linuxDoData[dateKey] = linuxDoData[dateKey] || {};
                Object.assign(totalCache, linuxDoData[dateKey]);
            }
        });
        // console.log('totalCache', totalCache)
    }

    function finish() {
        linuxDoData[theDate[0]] = linuxDoData[theDate[0]] || {};
        Object.assign(linuxDoData[theDate[0]], contentCache);
        GM_setValue(linuxDo, linuxDoData);
    }

    async function getPreviewContent(topicId) {
        try {

            // Check if content is already cached
            if (totalCache[topicId]) {
                //console.log("Content found in cache:", contentCache[linkHref]);
                return totalCache[topicId];
            }
            let url = `https://linux.do/t/topic/${topicId}.json`;

            //console.log("linkHref url: " + linkHref);
            // Fetch content from the link URL
            var response = await fetch(url);
            if (!response.ok) {
                throw new Error("Network response was not ok");
            }

            let jsonData = await response.json();
            let cookedContent = jsonData.post_stream.posts[0].cooked;

            if (cookedContent != null) {
                contentCache[topicId] = cookedContent;
                totalCache[topicId] = cookedContent;
                await sleep(waitTime);
            }
            // Return the extracted cooked content
            return cookedContent;
        } catch (error) {
            console.error("Error fetching link preview content:", error);
            return null;
        }
    }

    function ascPost() {
        // 获取当前页面的URL, 加上升序排列
        let currentUrl = window.location.href;

        // 判断URL是否已经包含?ascending=true
        if (!currentUrl.includes('?ascending=true')) {
            // 如果URL已经有其他参数,使用&符号添加新的参数
            if (currentUrl.includes('?')) {
                currentUrl += '&ascending=true';
            } else {
                // 如果URL没有任何参数,使用?符号添加新的参数
                currentUrl += '?ascending=true';
            }

            // 重新加载页面,并且包含新的URL
            window.location.href = currentUrl;
        }
    }

    async function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms))
    }


    async function work() {
        // 查找所有的<tr>标签,并筛选出有data-topic-id属性的标签
        const rows = $('tr[data-topic-id]');
        if (rows && rows.length === 0) return;
        init();
        try {
            for (const tr of rows) {
                let $tr = $(tr);
                // 判断是否是隐藏的
                if ($tr.is(':hidden')) {
                    continue;
                }
                let topicId = $tr.attr('data-topic-id');
                let cookedContent = await getPreviewContent(topicId);
                //console.log("url: " + url, "cookedContent:" + cookedContent);
                if (cookedContent) {
                    //数据是否已插入
                    var nextTr = $tr.next('tr')
                    if (nextTr && nextTr.find('div[is_insert="1"]').length > 0) {
                        continue;
                    }
                    // 在<tr>标签最后插入新的<div>
                    //$tr.appendChild(newCell1);
                    // 在新的 tr 中插入包含 cookedContent 的 div
                    let $newRow = $('<tr></tr>');
                    let $newDiv = $(addDiv); // 创建新的 div 元素
                    $newDiv.html(cookedContent); // 设置 div 的 HTML 内容
                    $newRow.append($newDiv);
                    $tr.after($newRow);
                }
            }
        } catch (error) {
            console.error('处理行时出错:', error);
        }
        finish()

    }

    async function workSearchData() {
        // 查找 class="fps-result-entries" 的 div 标签
        const fpsResultEntries = document.querySelector('.fps-result-entries');
        if (!fpsResultEntries) return;

        // 遍历下面 role="listitem" 的 div
        const listItems = fpsResultEntries.querySelectorAll('div[role="listitem"]');
        //判断是否有数据
        if (listItems && listItems.length === 0) return;

        init();
        for (let divItem of listItems) {
            // 判断是否是隐藏的
            if ($(divItem).is(':hidden')) {
                continue;
            }
            // 从 divItem 中获取 href="/t/topic/数字" 的 a 标签
            const aTag = divItem.querySelector('a[href^="/t/topic/"]');
            if (aTag) {
                // 取 href 中的数字值为 topicId
                const topicId = aTag.getAttribute('href').match(/\/t\/topic\/(\d+)/)[1];

                //console.log('url', url)
                const cookedContent = await getPreviewContent(topicId);

                // 移除 divItem 中 class="blurb container" 的 div
                const blurbContainer = divItem.querySelector('.blurb.container');
                if (cookedContent && blurbContainer) {
                    var $listItem = $(divItem);
                    // 查找 is_insert="1" 的 div 元素
                    var targetDiv = $listItem.find('div[is_insert="1"]');
                    // 检查是否已插入
                    if (targetDiv.length > 0) {
                        continue
                    }
                    // 将 blurbContainer 的内容替换为 cookedContent
                    let $newDiv = $(addDiv); // 创建新的 div 元素
                    $newDiv.html(cookedContent); // 设置 div 的 HTML 内容
                    $(blurbContainer).after($newDiv);
                }
            }
        }
        finish()
    }

    // 添加 Tampermonkey 菜单按钮
    GM_registerMenuCommand("滚动到最后未看贴", async function () {
        let currentUrl = window.location.href;
        if (currentUrl === 'https://linux.do/latest?order=created') {
            if (Object.keys(totalCache).length === 0) {
                init();
                if (Object.keys(totalCache).length === 0) {
                    console.log('没有历史浏览数据');
                    return;
                }
            }

            // 获取最新的日期键
             // 获取最新的日期键
            let dateKeys = Object.keys(totalCache).sort();
            // 获取最后一个已读帖子的ID
            let lastReadTopicId = dateKeys[dateKeys.length - 1];
            console.log('最后一个已读帖子的ID', lastReadTopicId);
            while(1){
                let rows = $('tr[data-topic-id]');
                if (rows.length == 0) {
                    console.log('获取帖子失败');
                    break;
                }
                let lastTr = rows.last()[0];
                let currentTopicId = parseInt($(lastTr).attr('data-topic-id'));
                if(currentTopicId > lastReadTopicId){
                    lastTr.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    await sleep(2000);
                } else {
                    console.log('滑动到最后一个没有看的贴');
                    console.log('隐藏已看贴');
                    toggleHidePosts(true)
                    await sleep(2000);
                    console.log('加载内容');
                    work();
                    break;
                }
            }
        }
    });

    GM_registerMenuCommand("重新加载内容", function () {
        let currentUrl = window.location.href;
        if (currentUrl.includes('/search?')) {
            workSearchData()
        } else {
            work();
        }
    });
    // 添加 Tampermonkey 菜单按钮
    GM_registerMenuCommand("升序排序", function () {
        ascPost();
    });
    // 添加 Tampermonkey 菜单按钮
    GM_registerMenuCommand("搜索当日帖子", function () {
        var today = new Date();

        var toDayDate = formatDate(today)

        today.setDate(today.getDate() + 1);
        var beforeDate = formatDate(today)

        var url = 'https://linux.do/search?q=after:' + toDayDate + ' before:' + beforeDate + ' in:first order:oldest -tags:KFC,刺猬';

        window.location.href = url;
    });


    GM_registerMenuCommand("隐藏已加载帖子", () => toggleHidePosts(true));
    GM_registerMenuCommand("显示已加载帖子", () => toggleHidePosts(false));
    GM_registerMenuCommand("只隐藏上次隐藏帖子", () => onlyHidePosts());
    function toggleHidePosts(isHidden) {
        if (Object.keys(totalCache).length == 0) {
            init()
            if (Object.keys(totalCache).length == 0) {
                console.log('没数据')
                return;
            }
        }
        console.log('totalCache', Object.keys(totalCache).length)
        const href = window.location.href;
        const selector = href.includes('/search?') ?
            $('.fps-result-entries').find('div[role="listitem"]') :
            $('tr[data-topic-id]');
        // 记录隐藏的帖子
        var hidePosts = []
        selector.each(function () {
            let $this = $(this);
            let topicId;

            // 根据不同的容器获取topicId
            if ($this.is('div')) {
                topicId = $this.find('div[data-topic-id]').first().attr('data-topic-id');
            } else if ($this.is('tr')) {
                topicId = $this.attr('data-topic-id');
            }
            //console.log(topicId, totalCache[topicId])
            if (topicId && totalCache[topicId]) {
                if (isHidden) {
                    hidePosts.push(topicId)
                }
                $this[isHidden ? 'hide' : 'show']();
                if ($this.is('tr')) {
                    $this.next('tr')[isHidden ? 'hide' : 'show']();
                }
            }
        });
        if (isHidden) {
            GM_setValue('hide', hidePosts);
        }
    }

    function onlyHidePosts() {
        if (Object.keys(totalCache).length == 0) {
            init()
            if (Object.keys(totalCache).length == 0) {
                console.log('没数据')
                return;
            }
        }
        //console.log('linuxDoData', linuxDoData)
        const href = window.location.href;
        const selector = href.includes('/search?') ?
            $('.fps-result-entries').find('div[role="listitem"]') :
            $('tr[data-topic-id]');
        // 记录隐藏的帖子
        var hidePosts = GM_getValue('hide', []);
        // console.log('hide', hidePosts)
        selector.each(function () {
            let $this = $(this);
            let topicId;

            // 根据不同的容器获取topicId
            if ($this.is('div')) {
                topicId = $this.find('div[data-topic-id]').first().attr('data-topic-id');
            } else if ($this.is('tr')) {
                topicId = $this.attr('data-topic-id');
            }

            if (topicId && totalCache[topicId] && hidePosts.includes(topicId)) {
                $this.hide();
                if ($this.is('tr')) {
                    $this.next('tr').hide();
                }
            }
        });
    }


    function formatDate(date) {
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');
        return `${year}-${month}-${day}`;
    }
    /*
    // 页面加载完成后执行
    $(document).ready(function () {
        if (window.location.href.includes('linux.do/new')) {
            work();
        }

    });
  */
    function getFormattedDate() {
        const date = new Date();
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, '0');
        const day = String(date.getDate()).padStart(2, '0');

        // 获取昨天的日期
        const yesterday = new Date(date);
        yesterday.setDate(date.getDate() - 1);
        const yYear = yesterday.getFullYear();
        const yMonth = String(yesterday.getMonth() + 1).padStart(2, '0');
        const yDay = String(yesterday.getDate()).padStart(2, '0');
        const beforeDate = `${yYear}-${yMonth}-${yDay}`;

        return [`${year}-${month}-${day}`, beforeDate];
    }


})();


搞的油猴脚本, 配合下面的

// ==UserScript==
// @name         linux.do多功能脚本
// @namespace
// @version      2024-10-01
// @description  1. 查看/不看回复 2. 展开/折叠回复 3. 显示楼层 4. 禁用视频自动播放 5. 预览界面
// @author       Jason+马克思+神奇的哆啦z梦
// @match        https://linux.do/*
// @match        https://meta.appinn.net/t/topic/*
// @icon
// @grant         GM_registerMenuCommand
// @grant         GM_unregisterMenuCommand
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';
    //by Jason

    // 简化对localStorage的访问
    const storage = {
        set: (key, value) => window.localStorage.setItem(key, value),
        get: (key, defaultValue) => window.localStorage.getItem(key) || defaultValue,
    };


    GM_registerMenuCommand("查看回复", () => createToggleButton("off"));
    GM_registerMenuCommand("不看回复", () => createToggleButton("on"));
    // 创建并配置按钮
    function createToggleButton(status) {

        storage.set("hide_replies", status);
        toggleRepliesVisibility(1); //立即应用显示设置

        /**
        if (document.getElementById("toggleRepliesVisibilityBtn")) {
            // 如果按钮已存在,则无需重复创建
            return;
        }
        const btn = document.createElement("button");
        btn.id = "toggleRepliesVisibilityBtn";
        btn.textContent = storage.get("hide_replies", "off") === "on" ? '查看回复' : '不看回复';
        btn.onclick = function() {
            const currentState = storage.get("hide_replies", "off");
            const newState = currentState === "on" ? "off" : "on";
            btn.textContent = newState === "on" ? '查看回复' : '不看回复';
            storage.set("hide_replies", newState);
            toggleRepliesVisibility(); //立即应用显示设置
        };
        // 设置按钮样式
        // btn.style.backgroundColor = "#555";
        // btn.style.color = "#FFF";
        // btn.style.border = "none";
        // btn.style.padding = "10px 20px";
        // btn.style.margin = "10px";
        // btn.style.borderRadius = "5px";
        // btn.style.cursor = "pointer";
        btn.className = "btn btn-icon-text btn-default";

        // 添加按钮到页面特定位置
        const controlsContainer = document.querySelector(".timeline-footer-controls");
        if (controlsContainer) {
            controlsContainer.appendChild(btn);
        }
        */
    }

    var replylength = 0
    // 根据设置隐藏或显示回复帖子
    function toggleRepliesVisibility(isOprate = 0) {
        const isHidden = storage.get("hide_replies", "off") === "on";
        const posts = document.querySelectorAll(".topic-post");
        if (replylength != posts.length || isOprate) {
            posts.forEach(post => {
                //当前楼是否是回复其他人
                const hasReply = post.querySelector(".reply-to-tab") !== null;
                //是否有回复
                const hasReply2 = post.querySelector("nav.post-controls .show-replies") !== null;
                post.style.display = isHidden && hasReply && !hasReply2 ? 'none' : '';
                // console.log("隐藏或显示回复帖子:", posts.length);
            });
            //显示楼层
            showFloor(posts)
        }
        replylength = posts.length;

    }

    //by linux.do增加插件

    GM_registerMenuCommand("展开回复", () => toggleReplyVisibility("on"));
    GM_registerMenuCommand("折叠回复", () => toggleReplyVisibility("off"));
    function toggleReplyVisibility(status) {
        storage.set("collapse_replies", status);
    }
    const replyButtonSelector = "nav.post-controls .show-replies";
    var pollinglength2 = 0
    function clickReplyButtons() {
        const isCollapse = storage.get("collapse_replies", "off") === "on";
        if (isCollapse) {
            var le = document.querySelectorAll(".post-stream .topic-post");
            if (pollinglength2 != le.length) {
                pollinglength2 = le.length;
                const replyButtons = document.querySelectorAll(replyButtonSelector);
                replyButtons.forEach(button => {
                    button.click();  // 模拟点击事件
                    // console.log("点击了回复按钮:", replyButtons.length);
                });
            }

        }
    }

    //禁用自动播放
    function disableAutoPlay() {
        // console.log("禁用自动播放搜索调用");
        document.querySelectorAll('iframe, video').forEach((element) => {
            // console.log("禁用自动播放搜索:", element.length);

            let src = element.getAttribute('src');

            // 检查 src 是否存在
            if (src) {
                // 检查是否已有 autoplay=false
                if (!src.includes('autoplay=false')) {
                    // 检查是否已有 autoplay 参数
                    if (src.includes('autoplay=')) {
                        // 如果存在,替换为 autoplay=false
                        src = src.replace(/autoplay=[^&]*/, 'autoplay=false');
                    } else {
                        // 如果不存在,添加 autoplay=false
                        const separator = src.includes('?') ? '&' : '?';
                        src += `${separator}autoplay=false`;
                    }

                    // 更新 src 属性
                    element.setAttribute('src', src);
                }
            }

            // 针对 video 标签设置 autoplay 属性为 false
            if (element.tagName.toLowerCase() === 'video' && element.autoplay) {
                element.autoplay = false;
            }
        });
    }

    //显示楼层
    function showFloor(posts) {
        // console.log("显示楼层搜索调用");
        posts.forEach(post => {
            const article = post.querySelector('article');
            if (article) {
                const num = article.id.replace(/^post_/, "");  // 获取文章的ID并去掉 "post_"

                // 如果尚未添加楼层标识,添加到 post-infos 中
                if (!post.querySelector('.linuxfloor')) {
                    const postInfos = post.querySelector('.post-infos');
                    if (postInfos) {
                        const floorSpan = document.createElement('span');
                        floorSpan.className = 'linuxfloor';
                        floorSpan.textContent = `#${num}`;
                        postInfos.appendChild(floorSpan);
                    }
                }
            }
        });
    }

    //预览帖子
    let previousTopicCount = 0;
    // let previousPostCount = 0;
    function previewTopic() {
        // console.log("预览帖子搜索调用");
        let trs = document.querySelectorAll(".topic-list-body tr")
        const topicCount = trs.length;
        // const postCount = document.querySelectorAll(".post-stream .topic-post").length;
        if (topicCount !== previousTopicCount) {
            previousTopicCount = topicCount;
            //创建背景
            previousTopic();
            setClick();
            //过滤帖子
            filterTopic(trs);
        }

        // if (postCount !== previousPostCount) {
        //     previousPostCount = postCount;
        //     setClick();
        // }
    }

    //过滤帖子
    let filterTexts = GM_getValue('filterTitles', []);
    let filterUsers = GM_getValue('filterUsers', []); // 新增屏蔽发帖人
    function filterTopic(trs) {
        trs.forEach((tr, index) => {
            if (tr.style.display === 'none') return; // Skip if already hidden

            let isFilter = false;
            let aDiv = tr.querySelector(".title");

            // Filter titles
            if (aDiv) {
                let title = aDiv.textContent.toLowerCase();
                isFilter = filterTexts.some(text => title.includes(text.toLowerCase()));
            }

            // Filter users
            if (!isFilter) {
                let posterLink = tr.querySelector(".posters a");
                let username = posterLink ? posterLink.getAttribute("href").split("/u/")[1] : "";

                if (username) {
                    isFilter = filterUsers.some(user => username.toLowerCase() === user.toLowerCase());
                }
            }

            // Hide or show based on filtering
            tr.style.display = isFilter ? (index !== trs.length - 1 ? 'none' : '') : '';
        });
    }

    // 添加设置屏蔽菜单
    GM_registerMenuCommand("设置屏蔽", showFilterSettings);

    // 添加设置弹框相关函数
    function showFilterSettings() {
        const dialog = document.createElement('div');
        dialog.className = 'filter-settings-dialog';
        dialog.innerHTML = `
            <div class="filter-settings-content">
                <h3>屏蔽设置</h3>
                <div class="filter-section">
                    <label>屏蔽标题(每行一个):</label>
                    <textarea id="filterTitles">${filterTexts.join('\n')}</textarea>
                </div>
                <div class="filter-section">
                    <label>屏蔽用户(每行一个):</label>
                    <textarea id="filterUsers">${filterUsers.join('\n')}</textarea>
                </div>
                <div class="filter-buttons">
                    <button id="saveFilters">保存</button>
                    <button id="cancelFilters">取消</button>
                </div>
            </div>
        `;

        document.body.appendChild(dialog);

        // 绑定按钮事件
        document.getElementById('saveFilters').onclick = () => {
            const newTitles = document.getElementById('filterTitles').value.split('\n').filter(x => x.trim());
            const newUsers = document.getElementById('filterUsers').value.split('\n').filter(x => x.trim());

            GM_setValue('filterTitles', newTitles);
            GM_setValue('filterUsers', newUsers);

            filterTexts = newTitles;
            filterUsers = newUsers;

            document.body.removeChild(dialog);

            // 重新过滤当前页面
            const trs = document.querySelectorAll(".topic-list-body tr");
            filterTopic(trs);
        };

        document.getElementById('cancelFilters').onclick = () => {
            document.body.removeChild(dialog);
        };
    }

    // 修改过小的内容
    function sizeMotified() {
        document.querySelectorAll("span").forEach((e) => {
            let style = window.getComputedStyle(e);
            let fontSize = parseFloat(style.fontSize);
            // 如果 font-size 低于75%, 则将其设置为75%
            if (fontSize < 16 * 0.75) { // 假设默认字体大小为16px
                e.style.fontSize = "75%";
                e.style.color = "grey";
            }
        });
    };

    //添加上/下一贴按钮
    function addPrevNextBtn() {
        // 获取标题元素
        const titleElement = document.querySelector('.title');
        // 判断是否有上/下一贴按钮
        if (document.querySelector('#myprev-topic-btn') === null) {
            // 创建上一贴按钮
            const prevButton = document.createElement('button');
            prevButton.textContent = '上一贴';
            prevButton.className = 'btn btn-prev';
            prevButton.id = 'myprev-topic-btn';
            prevButton.onclick = function () {
                prevNextTopic(true);
            };
            // 将按钮插入到标题元素右边
            titleElement.appendChild(prevButton);

        }
        if (document.querySelector('#mynext-topic-btn') === null) {
            // 创建下一贴按钮
            const nextButton = document.createElement('button');
            nextButton.textContent = '下一贴';
            nextButton.className = 'btn btn-next';
            nextButton.id = 'mynext-topic-btn';
            nextButton.onclick = function () {
                prevNextTopic(false);
            };
            // 将按钮插入到标题元素右边
            titleElement.appendChild(nextButton);
        }
        navigationBarHeight = getNavigationBarHeight();
    }

    //上一贴下一贴
    function prevNextTopic(isUp) {
        // 确定方向,isUp为true表示向上,-1代表上一个,1代表下一个
        const direction = isUp ? -1 : 1;

        // 获取所有topic行的列表
        const trs = document.querySelectorAll(".topic-list-body tr");
        const emberList = Array.from(trs).filter(tr => {
            return tr.style.display !== 'none';  // 过滤掉 display: none; 的行
        });

        // 查找第一个出现在视口中的元素
        const currentElement = emberList.find(tr => isElementPartiallyInViewport(tr));
        if (!currentElement) {
            return;
        }

        // 获取当前元素的索引位置
        const currentIndex = emberList.indexOf(currentElement);
        // 获取下一个或上一个元素的索引
        let nextIndex = currentIndex + direction;

        // 如果下一个元素存在且需要滚动,找到不是"data-topic-id"前缀的元素
        while (nextIndex >= 0 && nextIndex < emberList.length && !emberList[nextIndex].hasAttribute("data-topic-id")) {
            nextIndex += direction;
        }
        // 如果下一个合法元素存在,则进行滚动
        if (nextIndex >= 0 && nextIndex < emberList.length) {
            const nextElement = emberList[nextIndex];
            window.scrollBy(0, nextElement.getBoundingClientRect().top - navigationBarHeight);
        }
    }
    //获取导航栏高度
    function getNavigationBarHeight() {
        return document.querySelector("#ember3 > div.drop-down-mode.d-header-wrap > header > div > div").offsetHeight;
    }
    let navigationBarHeight = 0;
    //判断元素是否在可视范围内
    function isElementPartiallyInViewport(el) {
        const rect = el.getBoundingClientRect();

        const partiallyInViewport = (
            rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
            rect.bottom > navigationBarHeight + 5
        );

        return partiallyInViewport;
    }



    // 监听页面变化来重新应用显示设置
    function observePageChanges() {
        const observer = new MutationObserver((mutations) => {
            //判断当前页面是否是帖子页面
            const url = window.location.href;
            //帖子内
            if (url.indexOf("linux.do/t/topic/") != -1 || url.indexOf("meta.appinn.net/t/topic/") != -1) {
                //是否显示回复的楼
                toggleRepliesVisibility();
                // 是否展开回复
                clickReplyButtons();
                //禁用自动播放
                disableAutoPlay();
                //修改过小的内容
                //sizeMotified()
            } else if (url.indexOf("linux.do/search") != -1 || url.indexOf("meta.appinn.net/search") != -1) {

            } else {
                previewTopic()
            }

        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    // 初始化脚本
    function init() {
        if (document.readyState === 'complete') {
            observePageChanges();
            toggleRepliesVisibility(); // 初始应用显示设置
        } else {
            window.addEventListener('load', () => {
                observePageChanges();
                toggleRepliesVisibility();
            });
        }
    }

    init();

    //by 马克思
    //按帖子创建时间排序
    function waitForLoad(callback) {
        var observer = new MutationObserver(function (mutations) {
            if (document.readyState === 'complete') {
                observer.disconnect();
                callback();
            }
        });

        observer.observe(document.documentElement, { childList: true, subtree: true });
    }
    // 等待页面加载完成
    waitForLoad(function () {
        modifyNavigationBar();
        observeDocumentChanges();
    });
    function modifyNavigationBar() {
        var navigationBar = document.querySelector('ul#navigation-bar');
        if (navigationBar) {
            var navigationItems = navigationBar.querySelectorAll('li');
            navigationItems.forEach(function (item) {
                var anchor = item.querySelector('a');
                if (anchor && anchor.textContent.trim() === "最新") {
                    anchor.textContent = "新回复";
                    var newestCreatedElement = document.createElement('li');
                    newestCreatedElement.title = "新发的帖子";
                    newestCreatedElement.id = "ember999";
                    newestCreatedElement.className = "active latest_created ember-view nav-item_latest_created";
                    newestCreatedElement.innerHTML = '<a href="' + anchor.getAttribute('href') + '?order=created" pcked="1">新创建</a>';
                    item.insertAdjacentElement('afterend', newestCreatedElement);
                }
            });
            //添加上/下一贴按钮
            addPrevNextBtn();
        }
    }

    function observeDocumentChanges() {
        var observer = new MutationObserver(function (mutations) {
            mutations.forEach(function (mutation) {
                if (mutation.type === 'childList' && mutation.target.id === 'navigation-bar') {
                    modifyNavigationBar();
                }
            });
        });

        observer.observe(document.documentElement, { childList: true, subtree: true });
    }

    // 初始化话题预览界面
    function previousTopic() {
        if (document.querySelector(".topicpreview") === null) {
            const topicPreviewDiv = document.createElement("div");
            topicPreviewDiv.className = "topicpreview";
            topicPreviewDiv.style.display = 'none';  // 隐藏初始预览窗口
            topicPreviewDiv.innerHTML = `
        <div class="topicpreview-opacity"></div>
        <div class="topicpreview-container">
          <p style="text-align: center">正在加载中...</p>
        </div>
      `;
            document.body.appendChild(topicPreviewDiv);
        }

        // 为每个话题添加预览按钮
        document.querySelectorAll(".topic-list .main-link a.title").forEach((element) => {
            const id = element.getAttribute("data-topic-id");
            const parent = element.closest(".link-top-line");
            if (parent && parent.querySelector(".topicpreview-btn") === null) {
                const button = document.createElement("button");
                button.className = "btn btn-icon-text btn-default topicpreview-btn";
                button.setAttribute("data-id", id);
                button.textContent = "预览";
                parent.appendChild(button);
                button.addEventListener("click", function () {
                    console.log("点击预览按钮:", this.getAttribute("data-id"));
                    const previewContainer = document.querySelector(".topicpreview-container");
                    const previewOverlay = document.querySelector(".topicpreview");
                    previewOverlay.style.display = "block";  // 显示预览弹窗;
                    const previewId = this.getAttribute("data-id");


                    fetch(`/t/${previewId}.json`)
                        .then((response) => response.json())
                        .then((data) => {

                        const previewData = data;


                        // 更新预览窗口内容
                        previewContainer.innerHTML = `
                  <div class="topicpreview-title">${previewData.title}</div>
                  <p class="topicpreview-date">发帖时间:${formatDate(previewData.created_at)}</p>
                  <div class="topicpreview-content"></div>
                  <p style="text-align: center;">仅显示前 20 条,<a href="/t/topic/${previewId}/">查看更多</a></p>
                `;

                        // 显示每个帖子
                        previewData.post_stream.posts.forEach((post, index) => {
                            const postElement = document.createElement("div");
                            postElement.className = "item";
                            postElement.innerHTML = `
                    <span class="itemfloor">${index + 1}楼</span>
                    <div class="itempost">
                      <div class="itemname">
                        ${post.display_username} <span>${post.username}</span>
                        <div class="itemdate">${formatDate(post.created_at)}</div>
                      </div>
                      ${post.cooked}
                    </div>
                  `;
                            document.querySelector(".topicpreview .topicpreview-content").appendChild(postElement);
                        });

                        // 防止图片点击打开 lightbox
                        setInterval(() => {
                            document.querySelectorAll(".lightbox").forEach((el) => {
                                el.href = "javascript:void(0)";
                            });
                        }, 3000);
                    });
                });
            }
        });
    }
    // 格式化时间函数
    function formatDate(isoString) {
        const date = new Date(isoString);
        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, "0");
        const day = String(date.getDate()).padStart(2, "0");
        const hours = String(date.getHours()).padStart(2, "0");
        const minutes = String(date.getMinutes()).padStart(2, "0");
        const seconds = String(date.getSeconds()).padStart(2, "0");
        return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
    }

    // 设置按钮点击事件,显示预览弹窗
    function setClick() {
        // 点击背景关闭预览
        document.querySelector(".topicpreview-opacity").addEventListener("click", function () {
            document.querySelector(".topicpreview").style.display = "none";
            document.querySelector(".topicpreview-container").innerHTML = `<p style="text-align: center">正在加载中...</p>`;
        });
    }
    GM_addStyle(`
        .topicpreview-btn {
            padding: 4px 12px !important;
            font-size: 14px !important;
            opacity: 0 !important
          }

          .topic-list-item:hover .topicpreview-btn {
            opacity: 1 !important;
          }

          .topicpreview {
            position: fixed;
            top: 0;
            left: 0;
            z-index: 99999;
            width: 100vw;
            height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            display: none;

            .topicpreview-container {
              padding: 30px 0;
              border-radius: 5px;
              width: 100%;
              max-width: 800px;
              overflow-y: auto;
              height: 80vh;
              z-index: 10;
              background: var(--header_background);
              position: absolute;
              left: 50%;
              top: 50%;
              transform: translate(-50%, -50%);

              .topicpreview-title {
                font-size: 22px;
                font-weight: 600;
                padding: 0 30px;
              }

              .topicpreview-date {
                padding: 0 30px;
                color: #666;
              }

              .topicpreview-content {
                &>.item {
                  display: flex;
                  align-items: flex-start;
                  padding: 20px 30px;

                  .itemfloor {
                    width: 50px;
                    text-align: left;
                    font-size: 16px;
                    padding-top: 15px;
                    color: #25b4cf;
                  }

                  .itempost {
                    flex: 1;
                    background: var(--tertiary-low);
                    padding: 15px 15px;
                    border-radius: 10px;
                    font-size: 15px;
                    word-break: break-all;
                    // color: #666;

                    pre code {
                      max-width: 620px;
                    }

                    img {
                      max-width: 100%;
                      max-height: 100%;
                      height: auto;
                    }

                    .itemname {
                      font-size: 16px;
                      color: #8f3a3a;
                      display: flex;
                      justify-content: space-between;
                      align-items: center;

                      span {
                        color: #9e9e9e;
                        margin-left: 20px;
                      }
                    }

                    .itemdate {
                      color: #b9b9b9;
                      font-size: 16px;
                      margin-left: auto;
                    }
                  }
                }
              }
            }
          }

          .topicpreview-opacity {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            opacity: 1;
            background: rgba(0, 0, 0, .6);
            z-index: 9;
          }
      `);

    // 添加样式
    GM_addStyle(`
    .filter-settings-dialog {
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0,0,0,0.5);
        z-index: 10000;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    .filter-settings-content {
        background: var(--header_background);
        padding: 20px;
        border-radius: 8px;
        min-width: 400px;
    }

    .filter-section {
        margin: 15px 0;
    }

    .filter-section label {
        display: block;
        margin-bottom: 5px;
    }

    .filter-section textarea {
        width: 100%;
        height: 100px;
        margin-bottom: 10px;
    }

    .filter-buttons {
        text-align: right;
    }

    .filter-buttons button {
        margin-left: 10px;
        padding: 5px 15px;
    }
    `);
})();

可以配合这个一起用

2 Likes

不知道为啥加载首贴不可用,清除预览缓存按钮倒是出来了

长贴太占地方了,页面布局也会变乱,我试试能不能调到更好点

1 Like

我也有了
本来是前30字预览的 感觉好像出了点问题 缓存现在 刷新就清除
动画修复了一下
我感觉可以改为弹窗

// ==UserScript==
// @name         Linux.do 话题自动预览(v3)
// @namespace    https://tampermonkey.net/
// @version      3.0
// @description  展示话题首段或前30字摘要,SVG加载动画,点击展开全文,无缓存策略!
// @author       星缘
// @match        https://linux.do/*
// @grant        GM_xmlhttpRequest
// @connect      linux.do
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    // 提取首段或前30字
    function getShortPreview(cookedHTML, maxLen = 30) {
        const tempDiv = document.createElement('div');
        tempDiv.innerHTML = cookedHTML;
        const firstP = tempDiv.querySelector('p');
        const raw = firstP?.textContent?.trim() || tempDiv.textContent?.trim() || '';
        return raw.slice(0, maxLen) + '...';
    }

    // SVG 加载动画
    function createLoadingSpinner() {
        const wrap = document.createElement('div');
        wrap.innerHTML = `
        <div style="text-align:center; padding:8px;">
          <svg width="24" height="24" viewBox="0 0 100 100">
            <circle cx="50" cy="50" r="35" stroke="#999" stroke-width="10" fill="none" stroke-linecap="round">
              <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite"
                dur="1s" values="0 50 50;360 50 50" keyTimes="0;1"/>
            </circle>
          </svg>
        </div>
        `;
        return wrap;
    }

    // 插入预览(初始摘要 + 展开按钮)
    function insertShortPreview(row, cookedHTML) {
        if (row.querySelector('.topic-preview')) return;

        const preview = document.createElement('div');
        preview.className = 'topic-preview';
        preview.style.cssText = `
            background:#f7f7f7; border:1px solid #ddd; border-radius:5px;
            margin-top:8px; padding:10px; font-size:14px; line-height:1.5;
        `;

        const summary = getShortPreview(cookedHTML);
        const shortP = document.createElement('div');
        shortP.textContent = summary;

        const expandBtn = document.createElement('button');
        expandBtn.textContent = '展开全文';
        expandBtn.style.cssText = `
            display:inline-block; margin-top:8px; font-size:12px;
            background:#eee; border:1px solid #aaa; border-radius:3px; cursor:pointer;
        `;
        expandBtn.onclick = () => {
            preview.innerHTML = cookedHTML;
        };

        preview.appendChild(shortP);
        preview.appendChild(expandBtn);
        row.appendChild(preview);
    }

    // 异步加载 JSON 并插入
    function fetchAndInsert(row, topicId) {
        const spinner = createLoadingSpinner();
        row.appendChild(spinner);

        const url = `https://linux.do/t/${topicId}.json`;
        GM_xmlhttpRequest({
            method: 'GET',
            url,
            onload: (res) => {
                row.removeChild(spinner);
                try {
                    const json = JSON.parse(res.responseText);
                    const cooked = json.post_stream.posts[0].cooked;
                    insertShortPreview(row, cooked);
                } catch (e) {
                    console.error('解析失败', e);
                }
            },
            onerror: () => {
                row.removeChild(spinner);
                console.error('加载失败');
            }
        });
    }

    // 获取话题ID
    function getTopicId(href) {
        const m = href.match(/\/t\/[^/]+\/(\d+)/);
        return m ? m[1] : null;
    }

    // 懒加载
    const observer = new IntersectionObserver(entries => {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                const row = entry.target;
                const link = row.querySelector('a.title');
                if (link && link.href) {
                    const id = getTopicId(link.href);
                    if (id) {
                        observer.unobserve(row);
                        fetchAndInsert(row, id);
                    }
                }
            }
        }
    }, {
        rootMargin: '100px',
        threshold: 0.1
    });

    // 扫描话题列表
    function scan() {
        const rows = document.querySelectorAll('.topic-list tr:not(.preview-bound)');
        rows.forEach(row => {
            const link = row.querySelector('a.title');
            if (!link) return;
            row.classList.add('preview-bound');
            observer.observe(row);
        });
    }

    const mo = new MutationObserver(scan);
    mo.observe(document.body, { childList: true, subtree: true });

    scan(); // 初始执行
})();
1 Like

我能看到那个加载的圈圈动画,不过还是不显示摘要内容,应该是我浏览器这里有问题(之前还有其他 bug 还没修复),我再琢磨下

弹窗式来了 应该算是全网首创(反正我没搜索 :rofl:)
我的测试浏览器是x浏览器
但是还是强调我是手机w

// ==UserScript==
// @name         Linux.do 话题预览卡片 v4.2
// @namespace    https://tampermonkey.net/
// @version      4.2
// @description  精准50字摘要,保留HTML格式,支持暗色主题,视觉美化与动画优化!
// @author       星缘
// @match        https://linux.do/*
// @grant        GM_xmlhttpRequest
// @connect      linux.do
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    const PREVIEW_LENGTH = 50;

    const style = document.createElement('style');
    style.textContent = `
    .topic-preview-card {
        position: relative;
        margin-top: 8px;
        padding: 12px;
        border-radius: 8px;
        border: 1px solid;
        box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        font-size: 14px;
        line-height: 1.6;
        max-width: 600px;
        width: 90%;
        transition: all 0.3s ease;
        opacity: 0;
        transform: translateY(10px);
        animation: fadeInUp 0.4s forwards;
        z-index: 99;
    }
    @keyframes fadeInUp {
        to {
            opacity: 1;
            transform: translateY(0);
        }
    }

    .topic-preview-dark {
        background: #1e1e1e;
        color: #ddd;
        border-color: #444;
    }

    .topic-preview-dark a { color: #80bfff; }
    .topic-preview-light a { color: #0074d9; }

    .topic-preview-light {
        background: #fff;
        color: #333;
        border-color: #ccc;
    }

    .topic-preview-meta {
        font-size: 12px;
        margin-bottom: 6px;
        opacity: 0.8;
    }

    .topic-preview-btn {
        display: inline-block;
        margin-top: 10px;
        padding: 4px 8px;
        font-size: 12px;
        border: 1px solid #999;
        background: transparent;
        border-radius: 4px;
        cursor: pointer;
    }

    .topic-loading-spinner {
        text-align: center;
        padding: 8px;
    }

    .topic-loading-spinner svg {
        width: 24px;
        height: 24px;
        animation: rotate 1s linear infinite;
    }

    @keyframes rotate {
        0% {transform: rotate(0deg);}
        100% {transform: rotate(360deg);}
    }
    `;
    document.head.appendChild(style);

    function isDarkMode() {
        return document.documentElement.getAttribute('data-theme') === 'dark'
            || document.body.className.includes('dark');
    }

    const darkClass = () => isDarkMode() ? 'topic-preview-dark' : 'topic-preview-light';

    function createSpinner() {
        const div = document.createElement('div');
        div.className = 'topic-loading-spinner';
        div.innerHTML = `
        <svg viewBox="0 0 100 100">
            <circle cx="50" cy="50" r="35" stroke="#888" stroke-width="10" fill="none" stroke-linecap="round"/>
        </svg>`;
        return div;
    }

    function truncateHtmlToTextLimit(html, maxLength) {
        const div = document.createElement('div');
        div.innerHTML = html;

        let charCount = 0;
        let truncated = '';

        function walk(node) {
            if (charCount >= maxLength) return;

            if (node.nodeType === Node.TEXT_NODE) {
                const remaining = maxLength - charCount;
                const text = node.textContent.slice(0, remaining);
                charCount += text.length;
                truncated += text;
            } else if (node.nodeType === Node.ELEMENT_NODE) {
                const tag = node.tagName.toLowerCase();
                truncated += `<${tag}${[...node.attributes].map(attr => ` ${attr.name}="${attr.value}"`).join('')}>`;
                node.childNodes.forEach(walk);
                truncated += `</${tag}>`;
            }
        }

        div.childNodes.forEach(walk);
        return truncated + (charCount >= maxLength ? '...' : '');
    }

    function insertPreviewCard(row, cooked, meta) {
        if (row.querySelector('.topic-preview-card')) return;

        const card = document.createElement('div');
        card.className = `topic-preview-card ${darkClass()}`;

        const metaLine = document.createElement('div');
        metaLine.className = 'topic-preview-meta';
        metaLine.textContent = `分类: ${meta.category}|标签: ${meta.tags.join(', ') || '无'}|回复: ${meta.replyCount}`;
        card.appendChild(metaLine);

        const summaryDiv = document.createElement('div');
        summaryDiv.innerHTML = truncateHtmlToTextLimit(cooked, PREVIEW_LENGTH);
        card.appendChild(summaryDiv);

        const expandBtn = document.createElement('button');
        expandBtn.className = 'topic-preview-btn';
        expandBtn.textContent = '展开全文';
        expandBtn.onclick = () => {
            card.innerHTML = cooked;
        };

        card.appendChild(expandBtn);
        row.appendChild(card);
    }

    function fetchTopic(row, topicId) {
        const loading = createSpinner();
        row.appendChild(loading);

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://linux.do/t/${topicId}.json`,
            onload: (res) => {
                row.removeChild(loading);
                try {
                    const data = JSON.parse(res.responseText);
                    const cooked = data.post_stream.posts[0].cooked;
                    const meta = {
                        category: data.category_slug || '未知',
                        tags: data.tags || [],
                        replyCount: data.posts_count || 0
                    };
                    insertPreviewCard(row, cooked, meta);
                } catch (err) {
                    console.error('解析失败', err);
                }
            },
            onerror: () => {
                row.removeChild(loading);
                console.error('加载失败');
            }
        });
    }

    const observer = new IntersectionObserver(entries => {
        for (const entry of entries) {
            if (entry.isIntersecting) {
                const row = entry.target;
                const link = row.querySelector('a.title');
                if (link && link.href) {
                    const id = link.href.match(/\/t\/[^/]+\/(\d+)/)?.[1];
                    if (id) {
                        observer.unobserve(row);
                        fetchTopic(row, id);
                    }
                }
            }
        }
    }, { rootMargin: '100px', threshold: 0.1 });

    function scan() {
        const rows = document.querySelectorAll('.topic-list tr:not(.preview-bound)');
        rows.forEach(row => {
            const link = row.querySelector('a.title');
            if (link) {
                row.classList.add('preview-bound');
                observer.observe(row);
            }
        });
    }

    const themeObs = new MutationObserver(() => {
        document.querySelectorAll('.topic-preview-card').forEach(card => {
            card.classList.remove('topic-preview-dark', 'topic-preview-light');
            card.classList.add(darkClass());
        });
    });

    themeObs.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
    themeObs.observe(document.body, { attributes: true });

    const mo = new MutationObserver(scan);
    mo.observe(document.body, { childList: true, subtree: true });
    scan();
})();
1 Like