当前位置: 首页 > news >正文

某平台增强排序脚本

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

点击查看代码
// ==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">&times;</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, '&quot;')}">${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();})();
http://www.hskmm.com/?act=detail&tid=26867

相关文章:

  • 印度乡村AI计划:用JAN AI打造人工智能优先村庄
  • # Java方法学习:动手动脑与课后实验整理
  • CF2155D Batteries
  • JAVA语法基础》动手动脑与实验问题全整理
  • 崩铁壁纸
  • PotPlayer 播放器
  • 10.8动手动孬
  • [迷宫寻路 Round 3] 七连击
  • 《程序员修炼之道:从小工到专家》阅读笔记
  • [笔记]树论笔记+做题记录
  • 云服务器部署大数据组件
  • 规模化网站SSL证书终极方案
  • 详细介绍:录制mp4
  • 10月8日
  • 【OpenGL ES】光栅化插值原理和射线拾取原理
  • HTML 速查列表 - 教程
  • Exp1
  • 20_uv_wsl_installation
  • 学习问题日记-4
  • Codeforces Round 1042 (CF2131) 补题笔记(A-E)
  • 在AI技术唾手可得的时代,挖掘新需求成为核心竞争力——某知名AI编程助手框架需求探索
  • 表格数据自动机器学习技术解析
  • 10/8
  • 2025.10.8
  • 【QT】QString 与QString区别 - 教程
  • 连通分量tarjan学习笔记
  • [Python/地图] 基于Python绘制地图
  • 实验任务1——8
  • 一款专门为 WPF 打造的开源 Office 风格用户界面控件库
  • dockercontainerd代理设置脚本