Linux.do 快问快答统计

统计用户快问快答标签下提出问题的解答情况,评估用户的提问质量

当前为 2025-06-21 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();

})();