Greasy Fork

Linux.do 快问快答统计

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

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

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

})();