// ==UserScript==
// @name Linux.do 快问快答统计
// @namespace http://tampermonkey.net/
// @version 2025-06-21
// @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 = `
.qa-stats-container {
margin-bottom: 20px !important;
background: white !important;
border-radius: 8px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
padding: 20px !important;
border-left: 4px solid #007bff !important;
}
.qa-stats-header {
display: flex !important;
align-items: center !important;
margin-bottom: 16px !important;
}
.qa-stats-icon {
font-size: 20px !important;
margin-right: 8px !important;
}
.qa-stats-title {
font-size: 1.3em !important;
font-weight: bold !important;
color: #333 !important;
}
.qa-stats-content {
display: grid !important;
grid-template-columns: 1fr 1fr !important;
gap: 20px !important;
margin-bottom: 16px !important;
}
.qa-stats-left {
display: flex !important;
flex-direction: column !important;
gap: 8px !important;
}
.qa-stats-item {
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
padding: 8px 0 !important;
font-size: 14px !important;
}
.qa-stats-label {
color: #666 !important;
font-weight: 500 !important;
}
.qa-stats-value {
font-weight: bold !important;
color: #333 !important;
}
.qa-stats-value.primary {
color: #007bff !important;
font-size: 16px !important;
}
.qa-stats-value.success {
color: #28a745 !important;
}
.qa-stats-value.warning {
color: #ffc107 !important;
}
.qa-stats-right {
display: flex !important;
flex-direction: column !important;
justify-content: center !important;
align-items: center !important;
}
.qa-stats-progress {
width: 100px !important;
height: 100px !important;
border-radius: 50% !important;
background: conic-gradient(#28a745 var(--progress), #e9ecef var(--progress)) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-bottom: 10px !important;
position: relative !important;
}
.qa-stats-progress::before {
content: '' !important;
position: absolute !important;
width: 70px !important;
height: 70px !important;
background: white !important;
border-radius: 50% !important;
}
.qa-stats-percentage {
position: relative !important;
z-index: 1 !important;
font-size: 18px !important;
font-weight: bold !important;
color: #333 !important;
}
.qa-stats-evaluation {
margin-top: 12px !important;
padding: 12px !important;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%) !important;
border-radius: 6px !important;
text-align: center !important;
border-left: 3px solid var(--eval-color) !important;
}
.qa-stats-evaluation-text {
font-size: 14px !important;
color: #333 !important;
font-weight: 500 !important;
}
.qa-stats-loading {
display: flex !important;
justify-content: center !important;
align-items: center !important;
padding: 30px 20px !important;
background: white !important;
border-radius: 8px !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
font-size: 16px !important;
color: #555 !important;
}
.qa-stats-loading .spinner {
border: 3px solid rgba(0,0,0,0.1) !important;
border-left-color: #007bff !important;
border-radius: 50% !important;
width: 20px !important;
height: 20px !important;
animation: spin 1s linear infinite !important;
margin-right: 10px !important;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@media (max-width: 768px) {
.qa-stats-content {
grid-template-columns: 1fr !important;
}
.qa-stats-right {
margin-top: 16px !important;
}
}
`;
// 创建并注入样式
const styleElement = document.createElement('style');
styleElement.textContent = styles;
document.head.appendChild(styleElement);
// --- 辅助函数 ---
function showLoadingIndicator(message = "正在加载快问快答统计...") {
removeLoadingIndicator();
const indicator = document.createElement('div');
indicator.className = 'qa-stats-loading';
indicator.innerHTML = `<div class="spinner"></div> <span class="loading-text">${message}</span>`;
return indicator;
}
function updateLoadingMessage(indicator, message) {
if (indicator) {
const textElement = indicator.querySelector('.loading-text');
if (textElement) textElement.textContent = message;
}
}
function removeLoadingIndicator() {
const existingIndicator = document.querySelector('.qa-stats-loading');
if (existingIndicator) {
existingIndicator.remove();
}
}
// --- 缓存功能 ---
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) {
console.log('快问快答统计: 使用缓存数据', username);
return data;
}
console.log('快问快答统计: 缓存已过期', username);
}
} 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));
console.log('快问快答统计: 数据已缓存', username);
} catch (e) {
console.error('快问快答统计: 缓存设置错误', e);
}
}
// --- 数据获取 ---
async function fetchUserTopics(username, loadingIndicator) {
const cachedData = getCachedData(username);
if (cachedData) {
return cachedData;
}
const allTopics = [];
let page = 0;
const maxPages = 50; // 最多获取50页,约1500个主题
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));
}
if (loadingIndicator) {
updateLoadingMessage(loadingIndicator, `正在获取主题数据... (第${page + 1}页${retries > 0 ? `, 重试${retries}` : ''})`);
}
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++;
console.warn(`快问快答统计: 429错误,重试 ${retries}/${MAX_RETRIES_429}`);
if (loadingIndicator) updateLoadingMessage(loadingIndicator, `服务器限流,正在重试 (${retries}/${MAX_RETRIES_429})...`);
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) {
console.log('快问快答统计: 已获取所有主题');
page = maxPages; // 跳出外层循环
} else {
page++;
}
} else {
console.log('快问快答统计: 没有更多主题');
page = maxPages; // 跳出外层循环
}
success = true;
} catch (error) {
console.error('快问快答统计: 获取数据错误', error);
if (retries >= MAX_RETRIES_429 || !error.message.includes("429")) {
if (loadingIndicator) updateLoadingMessage(loadingIndicator, `获取数据出错,显示已有结果`);
await new Promise(resolve => setTimeout(resolve, 2000));
page = maxPages; // 跳出外层循环
break;
}
retries++;
}
}
if (!success && retries > MAX_RETRIES_429) {
console.warn("快问快答统计: 达到最大重试次数,使用已获取的数据");
break;
}
}
console.log(`快问快答统计: 共获取 ${allTopics.length} 个主题`);
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 unsolved = total - solved;
const solvedRate = total > 0 ? (solved / total * 100) : 0;
return {
total,
solved,
unsolved,
solvedRate: Math.round(solvedRate * 10) / 10, // 保留一位小数
qaTopics // 返回详细数据供调试
};
}
// --- UI创建 ---
function createQAStatsWidget(stats) {
const container = document.createElement('div');
container.className = 'qa-stats-container';
// 评估用户提问质量
let evaluation = '';
let evalColor = '#6c757d';
if (stats.total === 0) {
evaluation = '🤔 暂无快问快答提问记录';
evalColor = '#6c757d';
} else if (stats.solvedRate >= 90) {
evaluation = '🌟 提问质量极高,问题描述清晰易懂';
evalColor = '#28a745';
} else if (stats.solvedRate >= 75) {
evaluation = '👍 善于提问,大部分问题都能得到解答';
evalColor = '#007bff';
} else if (stats.solvedRate >= 50) {
evaluation = '💡 提问能力不错,继续提升问题描述';
evalColor = '#ffc107';
} else if (stats.solvedRate >= 25) {
evaluation = '📝 建议优化问题描述,提供更多背景信息';
evalColor = '#fd7e14';
} else {
evaluation = '🔍 学习如何提出好问题,会更容易得到帮助';
evalColor = '#dc3545';
}
container.innerHTML = `
<div class="qa-stats-header">
<span class="qa-stats-icon">🤔</span>
<span class="qa-stats-title">快问快答统计</span>
</div>
<div class="qa-stats-content">
<div class="qa-stats-left">
<div class="qa-stats-item">
<span class="qa-stats-label">提问总数</span>
<span class="qa-stats-value primary">${stats.total}</span>
</div>
<div class="qa-stats-item">
<span class="qa-stats-label">已获解答</span>
<span class="qa-stats-value success">${stats.solved}</span>
</div>
<div class="qa-stats-item">
<span class="qa-stats-label">待解答</span>
<span class="qa-stats-value warning">${stats.unsolved}</span>
</div>
</div>
<div class="qa-stats-right">
<div class="qa-stats-progress" style="--progress: ${stats.solvedRate * 3.6}deg">
<span class="qa-stats-percentage">${stats.solvedRate}%</span>
</div>
<small style="color: #666;">解答率</small>
</div>
</div>
<div class="qa-stats-evaluation" style="--eval-color: ${evalColor}">
<div class="qa-stats-evaluation-text">${evaluation}</div>
</div>
`;
return container;
}
// --- 页面检测和集成 ---
function isUserSummaryPage() {
return window.location.pathname.match(/^\/u\/[^/]+\/summary$/);
}
function cleanupPreviousWidget() {
const existingWidget = document.querySelector('.qa-stats-container');
if (existingWidget) {
existingWidget.remove();
}
removeLoadingIndicator();
}
function waitForUserContent(callback) {
const targetNode = document.body;
const config = { childList: true, subtree: true };
let userContent = document.querySelector('#user-content');
if (userContent) {
callback(userContent);
return;
}
console.log('快问快答统计: 等待 #user-content 元素...');
const observer = new MutationObserver((mutationsList, observer) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
userContent = document.querySelector('#user-content');
if (userContent) {
console.log('快问快答统计: 找到 #user-content 元素');
observer.disconnect();
callback(userContent);
return;
}
}
}
});
observer.observe(targetNode, config);
}
// --- 主初始化函数 ---
async function init() {
if (!isUserSummaryPage()) {
cleanupPreviousWidget();
return;
}
const usernameMatch = window.location.pathname.match(/^\/u\/([^/]+)\/summary$/);
if (!usernameMatch || !usernameMatch[1]) {
console.error('快问快答统计: 无法从URL提取用户名');
return;
}
const username = usernameMatch[1];
cleanupPreviousWidget();
const loadingIndicator = showLoadingIndicator(`正在加载 ${username} 的快问快答统计...`);
waitForUserContent(async (userContent) => {
userContent.prepend(loadingIndicator);
try {
const data = await fetchUserTopics(username, loadingIndicator);
if (!data || !data.topics) {
throw new Error("获取的数据无效");
}
const stats = processQAData(data);
const widget = createQAStatsWidget(stats);
userContent.prepend(widget);
console.log('快问快答统计: 小部件创建成功', stats);
} catch (error) {
console.error('快问快答统计: 创建小部件错误:', error);
if (loadingIndicator) updateLoadingMessage(loadingIndicator, `加载失败: ${error.message}`);
const spinner = loadingIndicator.querySelector('.spinner');
if (spinner) spinner.style.display = 'none';
return;
} finally {
if (loadingIndicator && !loadingIndicator.textContent.toLowerCase().includes("失败") && !loadingIndicator.textContent.toLowerCase().includes("错误")) {
removeLoadingIndicator();
} else if (loadingIndicator) {
const spinner = loadingIndicator.querySelector('.spinner');
if (spinner) spinner.style.display = 'none';
}
}
});
}
// --- 页面变化监听 ---
let lastUrl = location.href;
const urlChangeObserver = new MutationObserver(() => {
const currentUrl = location.href;
if (currentUrl !== lastUrl) {
lastUrl = currentUrl;
console.log('快问快答统计: URL变化,重新初始化');
init();
}
});
urlChangeObserver.observe(document, { subtree: true, childList: true });
// 初始化
init();
})();