某平台增强排序脚本
通过左下角悬浮按钮,在弹窗中展示某乎内容的赞同数降序排序结果
点击查看代码
// ==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号