在用户统计信息和帖子标题区域显示快问快答统计数据
// ==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);
}
})();