Linux.do 富可敌国评分展示

每日自动缓存商户数据,并在用户卡片弹出时,为富可敌国显示其评分信息,并适配网站暗色模式。

目前為 2025-09-29 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Linux.do 富可敌国评分展示
// @namespace    http://tampermonkey.net/
// @version      1.0.2
// @license      GNU GPLv3
// @description  每日自动缓存商户数据,并在用户卡片弹出时,为富可敌国显示其评分信息,并适配网站暗色模式。
// @author       haorwen
// @match        *://linux.do/*
// @connect      rate.linux.do
// @grant        GM_xmlhttpRequest
// @grant        GM_log
// ==/UserScript==

(function() {
    'use strict';

    // --- 全局变量与工具函数 ---
    const LAST_FETCH_DATE_KEY = 'ld_merchant_last_fetch_date';
    const MERCHANT_DATA_KEY = 'ld_merchant_ratings_data';
    let premiumTopicAuthor = null;

    /**
     * 获取指定名称的cookie值
     * @param {string} name cookie名称
     * @returns {string|null} cookie值或null
     */
    function getCookie(name) {
        const value = `; ${document.cookie}`;
        const parts = value.split(`; ${name}=`);
        if (parts.length === 2) return parts.pop().split(';').shift();
        return null;
    }

    /**
     * 检测当前是否为暗色模式
     * @returns {boolean}
     */
    function isDarkModeDetected() {
        // 条件1: 系统偏好设置
        const systemPrefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
        // 条件2: 网站强制设置的cookie
        const cookieForceDark = getCookie('forced_color_mode') === 'dark';
        return systemPrefersDark || cookieForceDark;
    }

    // --- Part 1: 数据获取与缓存 ---
    // ... (这部分代码没有变化)
    function getTodayDateString() {
        return new Date().toISOString().split('T')[0];
    }
    function extractUsernameFromAvatar(avatarUrl) {
        if (!avatarUrl || typeof avatarUrl !== 'string') return null;
        try {
            const parts = avatarUrl.split('/');
            return parts.length > 3 ? parts[parts.length - 3] : null;
        } catch (error) { console.error('从头像URL提取用户名失败:', avatarUrl, error); return null; }
    }
    function fetchAndStoreMerchantData() {
        GM_log('开始获取商户评价数据...');
        const apiUrl = 'https://rate.linux.do/api/merchant?page=1&size=100&order_by=average_rating&order_direction=desc';
        GM_xmlhttpRequest({
            method: 'GET', url: apiUrl, headers: { "Accept": "application/json, text/plain, */*" },
            onload: function(response) {
                if (response.status === 200) {
                    try {
                        const result = JSON.parse(response.responseText);
                        if (result.success && result.data && Array.isArray(result.data.data)) {
                            const merchants = result.data.data;
                            const merchantDataToStore = {};
                            merchants.forEach(merchant => {
                                const username = extractUsernameFromAvatar(merchant.avatar_url);
                                if (username) {
                                    merchantDataToStore[username.toLowerCase()] = {
                                        id: merchant.id, name: merchant.name, like_count: merchant.like_count,
                                        dislike_count: merchant.dislike_count, average_rating: merchant.average_rating, rating_count: merchant.rating_count,
                                    };
                                }
                            });
                            localStorage.setItem(MERCHANT_DATA_KEY, JSON.stringify(merchantDataToStore));
                            localStorage.setItem(LAST_FETCH_DATE_KEY, getTodayDateString());
                            GM_log(`成功缓存了 ${Object.keys(merchantDataToStore).length} 条商户数据 (键已小写化)。`);
                        } else { GM_log('API响应格式不正确:', result.message); }
                    } catch (error) { GM_log('解析API响应时出错:', error); }
                } else { GM_log(`获取数据失败,HTTP状态码: ${response.status}`); }
            },
            onerror: function(error) { GM_log('网络请求失败:', error); }
        });
    }
    function dailyCheckAndFetch() {
        if (localStorage.getItem(LAST_FETCH_DATE_KEY) !== getTodayDateString()) {
            GM_log(`首次加载,准备更新商户数据。`);
            fetchAndStoreMerchantData();
        } else { GM_log(`今日已缓存商户数据。`); }
    }

    // --- Part 2: 页面监控与信息注入 ---

    function checkForPremiumTag() {
        const premiumTag = document.querySelector('a[data-tag-name="高级推广"]');
        if (premiumTag) {
            const authorElement = document.querySelector('.topic-post.regular:first-of-type a[data-user-card]');
            if (authorElement) {
                const authorUsername = authorElement.getAttribute('data-user-card');
                if (authorUsername) {
                    const lowerCaseAuthor = authorUsername.toLowerCase();
                    if (premiumTopicAuthor !== lowerCaseAuthor) {
                        premiumTopicAuthor = lowerCaseAuthor;
                        GM_log(`[高级推广] 已记录作者 (小写): ${premiumTopicAuthor}`);
                    }
                }
                return;
            }
        }
        if (premiumTopicAuthor && !premiumTag) { premiumTopicAuthor = null; }
    }

    function handleUserCard(cardElement) {
        if (!cardElement) return;

        const oldInfo = cardElement.querySelector('.merchant-rating-info');
        if (oldInfo) oldInfo.remove();

        const usernameElement = cardElement.querySelector('.names__secondary.username');
        if (!usernameElement) return;
        const username = usernameElement.textContent.trim().toLowerCase();

        const isMerchant = cardElement.classList.contains('group-g-merchant');
        const isRichTitle = Array.from(cardElement.querySelectorAll('.names__secondary')).some(el => el.textContent.trim() === '富可敌国');
        const isPremiumAuthor = !!(username && premiumTopicAuthor && username === premiumTopicAuthor);

        if (!isMerchant && !isRichTitle && !isPremiumAuthor) return;

        GM_log(`检测到目标用户 [${username}] 的卡片。`);

        const allMerchantData = JSON.parse(localStorage.getItem(MERCHANT_DATA_KEY) || '{}');
        const merchantInfo = allMerchantData[username];

        if (!merchantInfo) {
            GM_log(`本地缓存中未找到 [${username}] 的评分数据。`);
            return;
        }

        // [!!!] 核心变更:根据暗色模式设置不同的样式
        const isDark = isDarkModeDetected();
        GM_log(`暗色模式检测: ${isDark}`);

        const bgColor = isDark ? '#3a3a3a' : '#f9f9f9';
        const borderColor = isDark ? '#555555' : '#e9e9e9';
        const textColor = isDark ? '#e0e0e0' : '#222';
        const linkStyle = `color: ${textColor}; text-decoration: none; display:contents;`;

        const ratingDiv = document.createElement('div');
        ratingDiv.className = 'card-row merchant-rating-info';
        ratingDiv.style.cssText = `
            padding: 8px 12px; margin: 10px 18px 0; border: 1px solid ${borderColor}; border-radius: 5px;
            background-color: ${bgColor}; font-size: 0.9em; display: flex; justify-content: space-around;
            flex-wrap: wrap; gap: 10px; text-align: center;
        `;
        ratingDiv.innerHTML = `
            <a href="https://rate.linux.do/merchant/${merchantInfo.id}" target="_blank" title="点击查看详情" style="${linkStyle}">
                <span>⭐ <strong>${merchantInfo.average_rating.toFixed(1)}</strong> (${merchantInfo.rating_count}人)</span>
                <span>👍 <strong>${merchantInfo.like_count}</strong></span>
                <span>👎 <strong>${merchantInfo.dislike_count}</strong></span>
            </a>
        `;

        const targetRow = cardElement.querySelector('.card-row.metadata-row');
        if (targetRow) {
            targetRow.insertAdjacentElement('beforebegin', ratingDiv);
            GM_log(`已为 [${username}] 成功注入评分信息。`);
        }
    }

    // --- Part 3: 主逻辑与启动 ---

    function main() {
        dailyCheckAndFetch();
        const waitForElement = (selector, callback) => {
            const el = document.querySelector(selector);
            if (el) { callback(el); return; }
            const obs = new MutationObserver((mutations, observer) => {
                const targetEl = document.querySelector(selector);
                if (targetEl) { observer.disconnect(); callback(targetEl); }
            });
            obs.observe(document.body, { childList: true, subtree: true });
        };
        waitForElement('#d-menu-portals', (portalElement) => {
            GM_log("商户信息增强脚本已启动 (v1.0.2 by haorwen)");
            checkForPremiumTag();
            const observer = new MutationObserver((mutationsList) => {
                checkForPremiumTag();
                for (const mutation of mutationsList) {
                    if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
                        const target = mutation.target.closest('#user-card');
                        if (target && target.classList.contains('show')) {
                           queueMicrotask(() => handleUserCard(target));
                        }
                    } else if (mutation.type === 'childList') {
                         for (const node of mutationsList) {
                            if (node.nodeType === 1 && node.querySelector) {
                                const card = node.querySelector('#user-card.show');
                                if (card) { queueMicrotask(() => handleUserCard(card)); }
                            }
                        }
                    }
                }
            });
            observer.observe(portalElement, {
                childList: true, subtree: true, attributes: true, attributeFilter: ['class']
            });
        });
    }
    main();
})();