您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在用户统计信息和帖子标题区域显示快问快答统计数据
// ==UserScript== // @name Linux.do 快问快答统计 // @namespace http://tampermonkey.net/ // @version 1.0.1 // @description 在用户统计信息和帖子标题区域显示快问快答统计数据 // @author Haleclipse & Claude // @license MIT // @match https://linux.do/* // @icon https://www.google.com/s2/favicons?sz=64&domain=linux.do // @grant none // ==/UserScript== (function () { 'use strict'; const CACHE_PREFIX = 'linuxdo_qa_stats_'; const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24小时缓存 const REQUEST_DELAY_MS = 300; const MAX_RETRIES_429 = 3; const RETRY_DELAY_429_MS = 5000; // --- 样式 --- const styles = ` /* 用户统计信息页面样式 */ .stats-qa-count { /* 复用现有的 stats-* 样式 */ } .stats-qa-solved { /* 复用现有的 stats-* 样式 */ } .stats-qa-rate { /* 复用现有的 stats-* 样式 */ } /* 帖子标题区域按钮样式 - 完全模仿标签样式 */ .qa-stats-btn { display: inline-block !important; vertical-align: middle !important; padding: 2px 8px !important; background: var(--primary-low) !important; border: none !important; border-radius: 0.25em !important; font-size: var(--font-down-1) !important; color: var(--primary) !important; cursor: pointer !important; text-decoration: none !important; white-space: nowrap !important; margin-right: 0.35em !important; margin-left: auto !important; flex-shrink: 0 !important; max-width: 14em !important; overflow: hidden !important; text-overflow: ellipsis !important; transition: background-color 0.2s ease !important; } .qa-stats-btn:hover { background: var(--primary-200) !important; color: var(--primary) !important; } .qa-stats-btn-icon { width: 12px !important; height: 12px !important; margin-right: 4px !important; vertical-align: middle !important; color: var(--primary) !important; } .qa-stats-text { display: inline-block !important; vertical-align: middle !important; padding: 2px 8px !important; background: var(--tertiary-low) !important; border: none !important; border-radius: 0.25em !important; font-size: var(--font-down-1) !important; color: var(--tertiary) !important; white-space: nowrap !important; margin-right: 0.35em !important; margin-left: auto !important; flex-shrink: 0 !important; /* 移除文字省略限制 */ max-width: none !important; overflow: visible !important; text-overflow: initial !important; } .qa-stats-container { display: inline-flex !important; align-items: center !important; margin-left: auto !important; flex-shrink: 0 !important; } .qa-stats-loading { display: inline !important; margin-left: 8px !important; font-size: 12px !important; color: #666 !important; } .qa-stats-error { display: inline !important; margin-left: 8px !important; font-size: 12px !important; color: #dc3545 !important; } `; // 创建并注入样式 const styleElement = document.createElement('style'); styleElement.textContent = styles; document.head.appendChild(styleElement); // --- 缓存功能 --- function getCachedData(username) { const cacheKey = `${CACHE_PREFIX}${username}`; try { const cached = localStorage.getItem(cacheKey); if (cached) { const { timestamp, data } = JSON.parse(cached); if (Date.now() - timestamp < CACHE_DURATION_MS) { return data; } } } catch (e) { console.error('快问快答统计: 读取缓存错误', e); localStorage.removeItem(cacheKey); } return null; } function setCachedData(username, data) { const cacheKey = `${CACHE_PREFIX}${username}`; const itemToCache = { timestamp: Date.now(), data: data }; try { localStorage.setItem(cacheKey, JSON.stringify(itemToCache)); } catch (e) { console.error('快问快答统计: 缓存设置错误', e); } } // --- 数据获取 --- async function fetchUserTopics(username) { const cachedData = getCachedData(username); if (cachedData) { return cachedData; } const allTopics = []; let page = 0; const maxPages = 20; // 减少页数以提高性能 while (page < maxPages) { let retries = 0; let success = false; while (retries <= MAX_RETRIES_429 && !success) { try { if (page > 0 || retries > 0) { await new Promise(resolve => setTimeout(resolve, retries > 0 ? RETRY_DELAY_429_MS : REQUEST_DELAY_MS)); } const url = page === 0 ? `https://linux.do/topics/created-by/${username}.json` : `https://linux.do/topics/created-by/${username}.json?page=${page}`; const response = await fetch(url); if (response.status === 429) { retries++; if (retries > MAX_RETRIES_429) { throw new Error(`超过最大重试次数`); } continue; } if (!response.ok) { throw new Error(`HTTP错误 ${response.status}`); } const data = await response.json(); if (data.topic_list && data.topic_list.topics && data.topic_list.topics.length > 0) { allTopics.push(...data.topic_list.topics); if (!data.topic_list.more_topics_url) { page = maxPages; } else { page++; } } else { page = maxPages; } success = true; } catch (error) { console.error('快问快答统计: 获取数据错误', error); if (retries >= MAX_RETRIES_429) { page = maxPages; break; } retries++; } } } const resultData = { topics: allTopics }; setCachedData(username, resultData); return resultData; } // --- 数据处理 --- function processQAData(data) { const allTopics = data.topics || []; const qaTopics = allTopics.filter(topic => topic.tags && topic.tags.includes('快问快答') ); const total = qaTopics.length; const solved = qaTopics.filter(topic => topic.has_accepted_answer === true).length; const solvedRate = total > 0 ? (solved / total * 100) : 0; return { total, solved, unsolved: total - solved, solvedRate: Math.round(solvedRate * 10) / 10 }; } // --- 用户统计信息页面功能 --- function isUserSummaryPage() { return window.location.pathname.match(/^\/u\/[^/]+\/summary$/); } function addStatsToUserPage(username, stats) { const statsSection = document.querySelector('.top-section.stats-section ul'); if (!statsSection) return; // 检查是否已添加 if (document.querySelector('.stats-qa-count')) return; // 创建快问快答统计项 const qaCountItem = document.createElement('li'); qaCountItem.className = 'stats-qa-count'; qaCountItem.innerHTML = ` <div class="user-stat"> <span class="value"> <span class="number">${stats.total}</span> </span> <span class="label"> 快问快答 </span> </div> `; const qaSolvedItem = document.createElement('li'); qaSolvedItem.className = 'stats-qa-solved'; qaSolvedItem.innerHTML = ` <div class="user-stat"> <span class="value"> <span class="number">${stats.solved}</span> </span> <span class="label"> 已采纳 </span> </div> `; const qaRateItem = document.createElement('li'); qaRateItem.className = 'stats-qa-rate'; qaRateItem.innerHTML = ` <div class="user-stat"> <span class="value"> <span class="number">${stats.solvedRate}%</span> </span> <span class="label"> 已采纳率 </span> </div> `; // 插入到统计列表中 statsSection.appendChild(qaCountItem); statsSection.appendChild(qaSolvedItem); statsSection.appendChild(qaRateItem); } // --- 帖子页面功能 --- function isTopicPage() { return window.location.pathname.match(/^\/t\/[^/]+\/\d+/); } function extractUsernameFromTopic() { // 从帖子页面提取发帖用户名 const firstPostUserLink = document.querySelector('.topic-post[data-post-number="1"] .names .username a'); if (firstPostUserLink) { const href = firstPostUserLink.getAttribute('href'); const match = href.match(/\/u\/([^/?]+)/); return match ? match[1] : null; } return null; } function addStatsToTopicTitle(username, stats) { // 首先检查当前帖子是否包含"快问快答"标签 const qaTag = document.querySelector('.discourse-tags a[data-tag-name="快问快答"]'); if (!qaTag) { return; // 如果帖子没有快问快答标签,直接返回 } // 查找类别标签区域 const topicCategory = document.querySelector('.topic-category, #ember38'); if (!topicCategory) return; // 检查是否已添加 if (document.querySelector('.qa-stats-btn') || document.querySelector('.qa-stats-text')) return; // 如果没有快问快答数据,不显示 if (stats.total === 0) return; // 查找或创建 topic-statuses 容器 let statusesContainer = topicCategory.querySelector('.topic-statuses'); if (!statusesContainer) { statusesContainer = document.createElement('span'); statusesContainer.className = 'topic-statuses'; statusesContainer.style.cssText = 'display: flex !important; align-items: center !important; flex-wrap: wrap !important; gap: 8px !important;'; topicCategory.appendChild(statusesContainer); } addButtonToStatusContainer(statusesContainer, stats); } function addButtonToStatusContainer(statusesContainer, stats) { // 创建按钮容器 const buttonWrapper = document.createElement('div'); buttonWrapper.className = 'qa-stats-container'; buttonWrapper.innerHTML = ` <button class="qa-stats-btn" type="button"> <svg class="qa-stats-btn-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <circle cx="12" cy="12" r="3"></circle> <path d="M12 1v6m0 6v6m11-7h-6m-6 0H1"></path> </svg> 快问快答统计 </button> `; const btn = buttonWrapper.querySelector('.qa-stats-btn'); btn.addEventListener('click', function() { // 替换按钮为统计文本 const statsText = document.createElement('span'); statsText.className = 'qa-stats-text'; statsText.textContent = `此用户共提出${stats.total}个问题,已采纳${stats.solved}个,已采纳率${stats.solvedRate}%`; buttonWrapper.replaceChild(statsText, btn); }); // 将按钮容器添加到 statuses 容器 statusesContainer.appendChild(buttonWrapper); } // --- 主处理函数 --- async function processUserStats(username) { try { const data = await fetchUserTopics(username); const stats = processQAData(data); if (isUserSummaryPage()) { addStatsToUserPage(username, stats); } else if (isTopicPage()) { addStatsToTopicTitle(username, stats); } } catch (error) { console.error('快问快答统计: 处理用户统计错误:', error); } } // --- 页面初始化 --- function init() { let username = null; if (isUserSummaryPage()) { const usernameMatch = window.location.pathname.match(/^\/u\/([^/]+)\/summary$/); username = usernameMatch ? usernameMatch[1] : null; } else if (isTopicPage()) { // 等待页面加载完成后提取用户名 setTimeout(() => { username = extractUsernameFromTopic(); if (username) { processUserStats(username); } }, 1000); return; // 提前返回,避免重复处理 } if (username) { processUserStats(username); } } // --- 页面变化监听 --- let lastUrl = location.href; const urlChangeObserver = new MutationObserver(() => { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; setTimeout(init, 500); // 延迟执行,确保页面元素加载完成 } }); urlChangeObserver.observe(document, { subtree: true, childList: true }); // 页面加载完成后初始化 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { setTimeout(init, 500); } })();