Linux.do 富可敌国评分展示

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

当前为 2025-09-29 提交的版本,查看 最新版本

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

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 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();
})();