Greasy Fork

Greasy Fork is available in English.

Linux.do Credit Display

显示 linux.do credit积分收入

当前为 2026-02-09 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Linux.do Credit Display
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  显示 linux.do credit积分收入
// @author       You
// @match        https://linux.do/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      credit.linux.do
// ==/UserScript==

(function() {
    'use strict';

    // API 和页面配置
    const TRANSACTIONS_API = 'https://credit.linux.do/api/v1/order/transactions';
    const LEADERBOARD_URL = 'https://linux.do/leaderboard';

    // 缓存 key
    const STORAGE_KEY = 'linux_do_credit_cache';
    const CURRENT_SCORE_KEY = 'linux_do_current_score_cache';

    // 请求配置
    const PAGE_SIZE = 20;
    const IFRAME_POLL_INTERVAL = 100;
    const IFRAME_MAX_ATTEMPTS = 50;
    const IFRAME_TIMEOUT = 10000;
    const ELEMENT_WAIT_TIMEOUT = 5000;

    // 通用容器样式
    const CONTAINER_STYLE = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        background: #fff;
        color: #333;
        padding: 10px 14px;
        border-radius: 8px;
        font-size: 14px;
        z-index: 9999;
        box-shadow: 0 2px 12px rgba(0,0,0,0.15);
    `;

    // 获取今天的时间范围(使用本地时区)
    function getTodayRange() {
        const now = new Date();
        const y = now.getFullYear();
        const m = String(now.getMonth() + 1).padStart(2, '0');
        const d = String(now.getDate()).padStart(2, '0');

        // 获取本地时区偏移,格式化为 +HH:MM 或 -HH:MM
        const offset = -now.getTimezoneOffset();
        const sign = offset >= 0 ? '+' : '-';
        const absOffset = Math.abs(offset);
        const hours = String(Math.floor(absOffset / 60)).padStart(2, '0');
        const minutes = String(absOffset % 60).padStart(2, '0');
        const tz = `${sign}${hours}:${minutes}`;

        return {
            startTime: `${y}-${m}-${d}T00:00:00${tz}`,
            endTime: `${y}-${m}-${d}T23:59:59${tz}`
        };
    }

    // 从 remark 解析积分: "社区积分从 1877 更新到 1938,变化 61" -> 1938
    function parseScore(remark) {
        const match = remark.match(/更新到\s*(\d+)/);
        if (!match) return null;
        const score = parseInt(match[1], 10);
        return isNaN(score) ? null : score;
    }

    // 安全解析数字文本(移除逗号,如 "2,003" -> 2003)
    function parseNumberText(text) {
        if (!text) return null;
        const num = parseInt(text.trim().replace(/,/g, ''), 10);
        return isNaN(num) ? null : num;
    }

    // 通用缓存读取(当天有效)
    function getCache(key) {
        const cache = GM_getValue(key, null);
        if (cache && cache.date === new Date().toDateString()) {
            return cache.score;
        }
        return null;
    }

    // 通用缓存写入
    function setCache(key, score) {
        GM_setValue(key, { score, date: new Date().toDateString() });
    }

    // 计算积分变化的显示信息
    function getDiffDisplay(currentScore, baseScore) {
        const diff = currentScore - baseScore;
        const sign = diff >= 0 ? '+' : '';
        const color = diff >= 0 ? '#22c55e' : '#ef4444';
        return { diff, sign, color };
    }

    // 获取今日基础积分
    function fetchBaseScore() {
        return new Promise((resolve, reject) => {
            const { startTime, endTime } = getTodayRange();

            GM_xmlhttpRequest({
                method: 'POST',
                url: TRANSACTIONS_API,
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify({ page: 1, page_size: PAGE_SIZE, startTime, endTime }),
                onload: (res) => {
                    try {
                        const data = JSON.parse(res.responseText);
                        const orders = data.data?.orders || [];
                        for (const item of orders) {
                            if (item.remark?.includes('社区积分')) {
                                const score = parseScore(item.remark);
                                if (score !== null) {
                                    resolve(score);
                                    return;
                                }
                            }
                        }
                        reject(new Error('未找到积分记录'));
                    } catch (e) {
                        reject(e);
                    }
                },
                onerror: reject
            });
        });
    }

    // 等待元素出现
    function waitForElement(selector, timeout = ELEMENT_WAIT_TIMEOUT) {
        return new Promise((resolve) => {
            const el = document.querySelector(selector);
            if (el) {
                resolve(el);
                return;
            }

            let resolved = false;
            const observer = new MutationObserver(() => {
                if (resolved) return;
                const el = document.querySelector(selector);
                if (el) {
                    resolved = true;
                    observer.disconnect();
                    resolve(el);
                }
            });

            observer.observe(document.body, { childList: true, subtree: true });

            setTimeout(() => {
                if (!resolved) {
                    resolved = true;
                    observer.disconnect();
                    resolve(null);
                }
            }, timeout);
        });
    }

    // 从排行榜页面 DOM 获取当前积分
    function parseScoreFromElement(userEl) {
        if (!userEl) return null;
        return parseNumberText(userEl.textContent);
    }

    // 判断是否在排行榜页面
    function isLeaderboardPage() {
        return location.pathname === '/leaderboard' || location.pathname === '/leaderboard/';
    }

    // 创建基础容器
    function createContainer() {
        const container = document.createElement('div');
        container.id = 'linux-do-credit';
        container.style.cssText = CONTAINER_STYLE;
        return container;
    }

    // 创建积分变化显示元素
    function createDiffElement(currentScore, baseScore) {
        const { sign, diff, color } = getDiffDisplay(currentScore, baseScore);
        const span = document.createElement('span');
        span.style.cssText = `font-weight: bold; color: ${color};`;
        span.textContent = `${sign}${diff}`;
        return span;
    }

    // 创建 UI(排行榜页面专用)
    async function createLeaderboardUI(baseScore) {
        const container = createContainer();
        container.textContent = '计算中...';
        document.body.appendChild(container);

        const userEl = await waitForElement('.user.-self .user__score');
        const currentScore = parseScoreFromElement(userEl);

        container.textContent = '';
        if (currentScore !== null) {
            const textNode = document.createTextNode('今日变化: ');
            container.appendChild(textNode);
            container.appendChild(createDiffElement(currentScore, baseScore));
        } else {
            container.style.color = '#f59e0b';
            container.textContent = '无法获取积分';
        }
    }

    // 通过隐藏 iframe 获取排行榜积分
    function fetchScoreViaIframe() {
        return new Promise((resolve, reject) => {
            const iframe = document.createElement('iframe');
            iframe.style.cssText = 'position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none;';
            iframe.src = LEADERBOARD_URL;

            let resolved = false;

            const cleanup = () => {
                if (iframe.parentNode) {
                    document.body.removeChild(iframe);
                }
            };

            const finish = (success, value) => {
                if (resolved) return;
                resolved = true;
                cleanup();
                success ? resolve(value) : reject(value);
            };

            iframe.onload = () => {
                let iframeDoc;
                try {
                    iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                } catch (e) {
                    finish(false, new Error('无法访问 iframe 内容(跨域限制)'));
                    return;
                }

                let attempts = 0;
                const checkElement = () => {
                    if (resolved) return;

                    const userEl = iframeDoc.querySelector('.user.-self .user__score');
                    if (userEl) {
                        const score = parseNumberText(userEl.textContent);
                        if (score !== null) {
                            finish(true, score);
                        } else {
                            finish(false, new Error('积分解析失败'));
                        }
                    } else if (attempts < IFRAME_MAX_ATTEMPTS) {
                        attempts++;
                        setTimeout(checkElement, IFRAME_POLL_INTERVAL);
                    } else {
                        finish(false, new Error('无法找到积分元素'));
                    }
                };

                setTimeout(checkElement, 500);
            };

            iframe.onerror = () => {
                finish(false, new Error('iframe 加载失败'));
            };

            setTimeout(() => {
                finish(false, new Error('获取积分超时'));
            }, IFRAME_TIMEOUT);

            document.body.appendChild(iframe);
        });
    }

    // 创建 UI(普通页面)
    function createUI(baseScore) {
        const container = createContainer();
        container.style.cssText += 'display: flex; flex-direction: column; gap: 6px;';

        const cachedCurrentScore = getCache(CURRENT_SCORE_KEY);

        // 历史积分行
        const historyRow = document.createElement('div');
        historyRow.style.cssText = 'display: flex; align-items: center; gap: 8px;';
        historyRow.appendChild(document.createTextNode('历史积分: '));
        const scoreSpan = document.createElement('span');
        scoreSpan.textContent = baseScore;
        historyRow.appendChild(scoreSpan);

        // 获取按钮
        const fetchBtn = document.createElement('div');
        fetchBtn.id = 'credit-fetch';
        fetchBtn.style.cssText = 'display: flex; align-items: center; gap: 8px; cursor: pointer; color: #3b82f6;';
        fetchBtn.textContent = cachedCurrentScore !== null ? '点击刷新' : '点击获取今日变化';

        // 结果显示区
        const resultDiv = document.createElement('div');
        resultDiv.id = 'credit-result';
        resultDiv.style.display = cachedCurrentScore !== null ? 'block' : 'none';

        container.appendChild(historyRow);
        container.appendChild(fetchBtn);
        container.appendChild(resultDiv);
        document.body.appendChild(container);

        // 如果有缓存,直接显示结果
        if (cachedCurrentScore !== null) {
            updateResultDisplay(resultDiv, cachedCurrentScore, baseScore);
        }

        // 点击获取当前积分
        fetchBtn.onclick = async () => {
            if (fetchBtn.dataset.loading === 'true') return;
            fetchBtn.dataset.loading = 'true';

            fetchBtn.textContent = '正在获取...';
            fetchBtn.style.color = '#999';
            resultDiv.style.display = 'none';

            try {
                const currentScore = await fetchScoreViaIframe();
                setCache(CURRENT_SCORE_KEY, currentScore);

                fetchBtn.textContent = '点击刷新';
                fetchBtn.style.color = '#3b82f6';
                resultDiv.style.display = 'block';
                updateResultDisplay(resultDiv, currentScore, baseScore);
            } catch (e) {
                console.error('获取积分失败:', e);
                fetchBtn.textContent = '获取失败,点击重试';
                fetchBtn.style.color = '#ef4444';
                resultDiv.style.display = 'block';
                resultDiv.textContent = '';
                const errorSpan = document.createElement('span');
                errorSpan.style.color = '#f59e0b';
                errorSpan.textContent = e.message;
                resultDiv.appendChild(errorSpan);
            }

            fetchBtn.dataset.loading = 'false';
        };
    }

    // 更新结果显示
    function updateResultDisplay(resultDiv, currentScore, baseScore) {
        resultDiv.textContent = '';
        resultDiv.appendChild(document.createTextNode('今日变化: '));
        resultDiv.appendChild(createDiffElement(currentScore, baseScore));
        resultDiv.appendChild(document.createTextNode(` (当前: ${currentScore})`));
    }

    // 显示错误状态
    function showError() {
        const container = createContainer();
        container.style.background = '#ef4444';
        container.style.color = 'white';
        container.textContent = '获取积分失败';
        document.body.appendChild(container);
    }

    // 初始化
    async function init() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
            return;
        }

        // 获取基础积分(优先缓存)
        let baseScore = getCache(STORAGE_KEY);

        if (baseScore === null) {
            try {
                baseScore = await fetchBaseScore();
                setCache(STORAGE_KEY, baseScore);
            } catch (e) {
                console.error('获取积分失败:', e);
                showError();
                return;
            }
        }

        // 根据页面类型显示不同 UI
        if (isLeaderboardPage()) {
            createLeaderboardUI(baseScore);
        } else {
            createUI(baseScore);
        }
    }

    init();
})();