某平台增强排序脚本
通过左下角悬浮按钮,在弹窗中展示某乎内容的赞同数降序排序结果
点击查看代码
// ==UserScript==
// @name         知乎排序增强
// @namespace    https://github.com/
// @version      1.0
// @description  通过左下角悬浮按钮,在弹窗中展示知乎内容的赞同数降序排序结果。
// @author       User
// @match        https://www.zhihu.com/
// @match        https://www.zhihu.com/search*
// @match        https://www.zhihu.com/question/*
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==
// 本脚本仅供个人学习和技术交流使用,请勿用于商业目的。所有数据的版权归原作者和知乎所有。因使用本脚本产生的一切后果由使用者自行承担。
(function () {
    'use strict'; // 启用 JavaScript 的严格模式,这是一种更安全、更规范的编码方式。
    // 定义一个日志前缀,方便在浏览器的开发者控制台(F12)中过滤和识别本脚本的输出信息。
    const LOG_PREFIX = "知乎排序增强 v0.9.0:";
    console.log(`${LOG_PREFIX} 脚本已启动。`);
    /**
     * @grant GM_addStyle
     * 使用油猴提供的 GM_addStyle 函数向页面注入CSS样式。
     * 这样做的好处是样式代码和逻辑代码分离,并且能确保样式被正确应用。
     * 这里定义了排序结果弹窗的所有外观,包括遮罩层、弹窗主体、标题、列表项、按钮等。
     */
    GM_addStyle(`
        /* 半透明的黑色背景遮罩层 */
        .sorter-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); z-index: 9998; display: flex; align-items: center; justify-content: center; }
        /* 弹窗主体内容框 */
        .sorter-modal-content { background-color: #fff; color: #121212; border-radius: 8px; width: 80%; max-width: 750px; height: 80%; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 5px 15px rgba(0,0,0,0.3); }
        /* 弹窗头部 */
        .sorter-modal-header { padding: 12px 16px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
        .sorter-modal-title { font-size: 16px; font-weight: 600; }
        /* 关闭按钮 */
        .sorter-modal-close { font-size: 24px; font-weight: bold; cursor: pointer; border: none; background: none; padding: 0 8px; }
        /* 可滚动的结果列表区域 */
        .sorter-modal-body { overflow-y: auto; padding: 8px 16px; }
        /* 每一个排序结果条目 */
        .sorted-item { display: flex; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; }
        /* 赞同数样式 */
        .sorted-item-votes { font-size: 14px; font-weight: bold; color: #1772F6; flex-shrink: 0; width: 90px; }
        /* 标题和按钮的容器 */
        .sorted-item-details { flex-grow: 1; min-width: 0; }
        /* 标题链接样式,超出部分会显示省略号 */
        .sorted-item-title { font-size: 15px; color: #121212; text-decoration: none; display: block; margin-bottom: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .sorted-item-title:hover { color: #0084ff; }
        /* 按钮区域 */
        .sorted-item-actions { margin-top: 5px; }
        /* 按钮通用样式 */
        .sorted-item-button { font-size: 12px; padding: 3px 8px; margin-right: 8px; border: 1px solid #ccc; border-radius: 3px; background: #f9f9f9; cursor: pointer; text-decoration: none; color: #333; }
        .sorted-item-button:hover { background: #eee; border-color: #bbb; }
    `);
    /**
     * [核心配置]
     * voteSelector: 这是整个脚本最关键的修正。我们不再使用易变的CSS类名,而是使用属性选择器。
     * 'button[aria-label^="赞同"]' 的意思是:找到一个 <button> 元素,它的 "aria-label" 属性值是以 "赞同" 这两个字开头的。
     * 这种方法非常稳定,因为 aria-label 是为辅助功能服务的,其内容通常不会轻易改变。
     */
    const voteSelector = 'button[aria-label^="赞同"]';
    /**
     * 页面配置中心。
     * 存储不同页面上“内容项”的独特选择器。脚本通过这些选择器来识别需要抓取和排序的目标。
     */
    const pageConfigs = {
        // 问题回答页
        question: {
            itemSelector: '.Question-main .List-item', // 每个回答的最外层包裹元素
            voteSelector: voteSelector,
            titleSelector: null, // 问题页的所有回答都属于同一个问题,标题是固定的,所以在此特殊处理为null
        },
        // 搜索结果页
        search: {
            itemSelector: '.SearchResult-Card', // 每个搜索结果卡片
            voteSelector: voteSelector,
            titleSelector: 'h2.ContentItem-title a', // 结果卡片中的标题链接
        },
        // 首页推荐流
        feed: {
            itemSelector: '.TopstoryItem', // 首页推荐流中的每个内容项
            voteSelector: voteSelector,
            titleSelector: '.ContentItem-title a', // 内容项中的标题链接
        }
    };
    /**
     * 检测当前页面属于哪种类型(问题、搜索、首页)。
     * @returns {string|null} 返回页面类型字符串或null。
     */
    function detectPageType() {
        const { hostname, pathname } = window.location;
        if (hostname === 'www.zhihu.com') {
            if (pathname.startsWith('/question/')) return 'question';
            if (pathname.startsWith('/search')) return 'search';
            if (pathname === '/' || pathname.startsWith('/follow')) return 'feed';
        }
        return null;
    }
    /**
     * 解析赞同数字符串 (例如 "1.2 万", "5,432", "3k") 为纯数字。
     * @param {string} voteText - 包含赞同数的文本。
     * @returns {number} - 解析后的数字。
     */
    function parseVoteCount(voteText) {
        if (!voteText || typeof voteText !== 'string') return 0;
        // 正则表达式匹配数字(可能带逗号)和单位(k, w, 万)
        const match = voteText.replace(/,/g, '').match(/([\d.]+)\s*([kKwW万]?)/);
        if (!match) return 0;
        let num = parseFloat(match[1]);
        const unit = match[2] ? match[2].toLowerCase() : '';
        if (unit === 'k') num *= 1000;
        else if (unit === 'w' || unit === '万') num *= 10000;
        return isNaN(num) ? 0 : Math.round(num);
    }
    /**
     * 辅助函数:关闭并从页面上移除弹窗。
     */
    function closeModal() {
        const modal = document.getElementById('sorter-modal');
        if (modal) { document.body.removeChild(modal); }
    }
    /**
     * 在弹窗中动态生成并显示排序结果列表。
     * @param {Array} sortedItems - 已排序的项目数据数组。
     * @param {HTMLElement} button - 主排序按钮,用于更新其状态。
     */
    function displayResultsInModal(sortedItems, button) {
        closeModal(); // 如果已存在弹窗,先关闭
        const overlay = document.createElement('div');
        overlay.id = 'sorter-modal';
        overlay.className = 'sorter-modal-overlay';
        // 使用模板字符串构建弹窗的HTML结构
        let modalHtml = `
            <div class="sorter-modal-content">
                <div class="sorter-modal-header">
                    <span class="sorter-modal-title">排序结果 (${sortedItems.length} 条)</span>
                    <button class="sorter-modal-close">×</button>
                </div>
                <div class="sorter-modal-body">
        `;
        // 如果没有找到任何可排序内容,显示提示信息
        if (sortedItems.length === 0) {
            modalHtml += '<p style="text-align: center; padding: 20px;">未能找到任何可排序的内容。请尝试向下滚动页面加载更多内容后,再点击排序。</p>';
        } else {
            // 遍历排序后的数据,生成每一行列表项
            sortedItems.forEach((item, index) => {
                modalHtml += `
                    <div class="sorted-item">
                        <div class="sorted-item-votes">👍 ${item.votesText}</div>
                        <div class="sorted-item-details">
                            <a class="sorted-item-title" href="${item.url}" target="_blank" title="${item.title.replace(/"/g, '"')}">${item.title}</a>
                            <div class="sorted-item-actions">
                                 <a href="${item.url}" target="_blank" class="sorted-item-button">新窗口打开</a>
                                 <button class="sorted-item-button scroll-to" data-item-id="${index}">滚动到原文</button>
                            </div>
                        </div>
                    </div>
                `;
            });
        }
        modalHtml += `</div></div>`;
        // 将HTML注入弹窗并添加到页面
        overlay.innerHTML = modalHtml;
        document.body.appendChild(overlay);
        // 为所有“滚动到原文”按钮绑定点击事件
        overlay.querySelectorAll('.scroll-to').forEach(btn => {
            btn.addEventListener('click', () => {
                const itemId = parseInt(btn.dataset.itemId, 10);
                const originalElement = sortedItems[itemId].element;
                closeModal();
                // 使用 scrollIntoView 实现平滑滚动定位
                originalElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
                // 给原文添加一个短暂的黄色高亮背景,方便用户识别
                originalElement.style.transition = 'all 0.3s ease-in-out';
                originalElement.style.backgroundColor = 'rgba(255, 255, 0, 0.5)';
                setTimeout(() => { originalElement.style.backgroundColor = ''; }, 1500);
            });
        });
        // 绑定关闭事件
        overlay.querySelector('.sorter-modal-close').addEventListener('click', closeModal);
        overlay.addEventListener('click', (e) => { if (e.target === overlay) closeModal(); }); // 点击遮罩层关闭
        // 更新主按钮状态
        button.textContent = '排序完成!';
        setTimeout(() => { button.textContent = '排序'; button.disabled = false; }, 2000);
    }
    /**
     * 主流程函数:当用户点击排序按钮时执行。
     * @param {object} config - 当前页面的配置对象。
     * @param {HTMLElement} button - 主排序按钮。
     */
    function processAndShowSortedList(config, button) {
        button.textContent = '抓取中...';
        button.disabled = true;
        console.log(`${LOG_PREFIX} [全局搜索模式] 寻找项目: ${config.itemSelector}`);
        // [核心改动] 直接在整个 document 对象上进行全局搜索,绕开容器查找失败的问题。
        const items = Array.from(document.querySelectorAll(config.itemSelector));
        if (items.length === 0) {
            console.warn(`${LOG_PREFIX} 全局搜索未能找到任何可排序的项目。`);
            displayResultsInModal([], button);
            return;
        }
        console.log(`${LOG_PREFIX} 全局搜索成功找到 ${items.length} 个项目。`);
        const itemsData = [];
        items.forEach(item => {
            // 关键过滤步骤:在每个找到的项目内部,再次用精确的属性选择器寻找赞同按钮
            const voteElement = item.querySelector(config.voteSelector);
            if (!voteElement) return; // 如果找不到赞同按钮(比如广告),就跳过这个项目
            // 解析赞同数和显示的文本
            const votes = parseVoteCount(voteElement.getAttribute('aria-label') || voteElement.innerText);
            const votesText = (voteElement.innerText.replace('赞同', '').trim() || '0');
            let title = '无标题';
            let url = '#';
            // 对问题回答页进行特殊处理,因为它的标题是固定的问题标题
            if (config.titleSelector === null) {
                const questionTitleEl = document.querySelector('.QuestionHeader-title');
                title = questionTitleEl ? `回答: ${questionTitleEl.innerText}` : '回答';
                const answerLinkEl = item.querySelector('meta[itemprop="url"]');
                url = answerLinkEl ? answerLinkEl.content : item.querySelector('a[data-za-detail-view-element_name="Title"]')?.href || '#';
            } else { // 处理首页和搜索页
                const titleElement = item.querySelector(config.titleSelector);
                if (titleElement) {
                    title = titleElement.innerText.trim();
                    url = titleElement.href;
                }
            }
            // 将解析好的数据存入数组
            itemsData.push({ element: item, votes, votesText, title, url });
        });
        console.log(`${LOG_PREFIX} 成功解析 ${itemsData.length} 个有效项目。`);
        // 按赞同数(votes)进行降序排序
        itemsData.sort((a, b) => b.votes - a.votes);
        // 调用函数显示结果
        displayResultsInModal(itemsData, button);
    }
    /**
     * 创建并向页面添加左下角的悬浮排序按钮。
     * @param {object} config - 当前页面的配置对象。
     */
    function createFixedButton(config) {
        if (document.getElementById('zhihu-sort-enhancer-btn')) return;
        const button = document.createElement('button');
        button.id = 'zhihu-sort-enhancer-btn';
        button.textContent = '排序';
        // 设置按钮的CSS样式,使其固定在左下角
        Object.assign(button.style, {
            position: 'fixed', bottom: '20px', left: '20px', zIndex: '9999',
            padding: '10px 15px', fontSize: '14px', color: '#fff',
            backgroundColor: '#0084ff', border: 'none', borderRadius: '5px',
            cursor: 'pointer', boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
            transition: 'all 0.2s'
        });
        // 绑定点击事件,触发排序流程
        button.addEventListener('click', () => processAndShowSortedList(config, button));
        document.body.appendChild(button);
        console.log(`${LOG_PREFIX} 悬浮按钮创建成功。`);
    }
    /**
     * [最终优化] 脚本的启动入口函数,使用 MutationObserver 智能等待内容加载。
     */
    function initialize() {
        const pageType = detectPageType();
        if (!pageType) return;
        const config = pageConfigs[pageType];
        console.log(`${LOG_PREFIX} 已识别页面为 [${pageType}]`);
        console.log(`${LOG_PREFIX} 正在等待第一个内容项出现: ${config.itemSelector}`);
        // 创建一个DOM变更观察器
        const observer = new MutationObserver((mutations, obs) => {
            // 每次页面DOM变化时,都检查第一个内容项是否已出现
            if (document.querySelector(config.itemSelector)) {
                console.log(`${LOG_PREFIX} 第一个内容项已出现, 正在创建按钮...`);
                // 一旦出现,立刻创建按钮
                createFixedButton(config);
                // 停止观察,避免不必要的性能消耗
                obs.disconnect();
            }
        });
        // 启动观察器,监视整个文档的变化
        observer.observe(document.body, {
            childList: true, // 观察子节点的添加或删除
            subtree: true    // 观察所有后代节点
        });
    }
    // 运行启动函数
    initialize();
})();
本文来自博客园,作者:舟清颺,转载请注明原文链接:https://www.cnblogs.com/zqingyang/p/19129941

 通过左下角悬浮按钮,在弹窗中展示某乎内容的赞同数降序排序结果
        通过左下角悬浮按钮,在弹窗中展示某乎内容的赞同数降序排序结果
     
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号