📚谷歌学术高级排序工具箱 (按年份、引用数)

👍为谷歌学术搜索结果页面添加一个功能强大的悬浮排序工具箱。你可以通过它轻松地对【当前页面】的文献进行排序:1.【按年份排序 (新→旧 / 旧→新)】:同年份的文献会自动按引用数从高到低排列。2.【按引用数排序 (高→低 / 低→高)】。3.【直观数据显示】:排序后,每条结果左侧将清晰地显示其年份和引用数。4.【状态高亮】:当前有效的排序按钮会高亮显示,且脚本能完美保留谷歌学术的原有页面布局。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Google Scholar Advanced Sorting Toolbox(year、cite)
// @name:zh-CN   📚谷歌学术高级排序工具箱 (按年份、引用数)
// @namespace    http://tampermonkey.net/
// @version      2025.07.10.101
// @description  👍Supercharge your Google Scholar experience with a powerful, floating sorting toolbox. This script allows you to instantly sort results on the **current page**: 1. Sort by Year (Newest ↔ Oldest) 2. Sort by Citations (Highest ↔ Lowest)3. Data at a Glance 4. Intuitive UI
// @description:zh-CN  👍为谷歌学术搜索结果页面添加一个功能强大的悬浮排序工具箱。你可以通过它轻松地对【当前页面】的文献进行排序:1.【按年份排序 (新→旧 / 旧→新)】:同年份的文献会自动按引用数从高到低排列。2.【按引用数排序 (高→低 / 低→高)】。3.【直观数据显示】:排序后,每条结果左侧将清晰地显示其年份和引用数。4.【状态高亮】:当前有效的排序按钮会高亮显示,且脚本能完美保留谷歌学术的原有页面布局。
// @author       heyue
// @match        https://scholar.google.com/scholar?*
// @match        https://scholar.google.com.hk/scholar?*
// @match        https://sc.panda985.com/scholar?*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=google.com
// @grant        none
// @license      MIT
// ==/UserScript==



(function () {
    'use strict';

    let currentSortState = { key: null, descending: null };

    // --- 数据提取辅助函数 ---
    function getYear(node) {
        const authorLine = node.querySelector('.gs_a');
        if (!authorLine) return 0;
        const text = authorLine.textContent;
        const yearMatch = text.match(/\b(19|20)\d{2}\b/);
        return yearMatch ? parseInt(yearMatch[0], 10) : 0;
    }

    function getCiteCount(node) {
        const citeLink = node.querySelector('a[href*="/scholar?cites"]');
        if (!citeLink) return 0;
        const citeMatch = citeLink.textContent.match(/\d+/);
        return citeMatch ? parseInt(citeMatch[0], 10) : 0;
    }

    // --- 核心排序与渲染逻辑 ---
    function sortElements(sortKey, isDescending) {
        const gsResCclMid = document.getElementById('gs_res_ccl_mid');
        if (!gsResCclMid) return;

        const elementsWithData = [...gsResCclMid.querySelectorAll('.gs_or')]
            .map(node => ({
                node: node,
                year: getYear(node),
                cite: getCiteCount(node)
            }));

        elementsWithData.sort((a, b) => {
            if (sortKey === 'year') {
                const yearDiff = isDescending ? b.year - a.year : a.year - b.year;
                if (yearDiff !== 0) return yearDiff;
                return b.cite - a.cite;
            } else {
                return isDescending ? b.cite - a.cite : a.cite - b.cite;
            }
        });

        // 【关键改动】为每个结果项更新布局和信息
        elementsWithData.forEach(item => {
            const { node, year, cite } = item;

            // --- 布局包裹逻辑 ---
            // 检查是否已经处理过(即是否已存在包裹容器)
            if (!node.querySelector('.gs-original-content-wrapper')) {
                const contentWrapper = document.createElement('div');
                contentWrapper.className = 'gs-original-content-wrapper';
                contentWrapper.style.flexGrow = '1'; // 让包裹容器占据剩余空间

                // 将 node 的所有原始子元素移动到包裹容器中
                while (node.firstChild) {
                    contentWrapper.appendChild(node.firstChild);
                }
                // 将包裹容器加回 node
                node.appendChild(contentWrapper);
            }

            // --- 信息块创建与更新逻辑 ---
            let infoBox = node.querySelector('.gs-sort-info-box');
            if (!infoBox) {
                infoBox = document.createElement('div');
                infoBox.className = 'gs-sort-info-box';
                Object.assign(infoBox.style, {
                    display: 'flex', flexDirection: 'column', alignItems: 'center',
                    justifyContent: 'center', width: '80px', flexShrink: '0',
                    paddingRight: '15px', marginRight: '15px', borderRight: '1px solid #e0e0e0',
                    textAlign: 'center'
                });
                // 首次创建时,插入到最前面
                node.prepend(infoBox);
            }

            // 无论是否首次创建,都更新信息块的内容
            const yearText = year > 0 ? year : 'N/A';
            const yearDisplay = `<div>📅 ${yearText}</div>`;
            const citeDisplay = `<div>💬 ${cite}</div>`;
            infoBox.innerHTML = yearDisplay + citeDisplay;

            // 设置父容器为flex,使其能容纳 信息块 和 包裹容器
            node.style.display = 'flex';
            node.style.alignItems = 'center';
        });

        gsResCclMid.innerHTML = '';
        gsResCclMid.append(...elementsWithData.map(item => item.node));

        currentSortState = { key: sortKey, descending: isDescending };
        console.log(`排序完成,布局已保留。`);

        // 排序后,需要手动更新一下按钮高亮,因为我们没有在循环外调用它
        const panel = document.querySelector('.gs-sort-panel');
        if (panel) updateHighlights(panel); // 传递 panel 引用
    }

    // --- UI 创建与交互 ---
    let updateHighlights; // 将函数声明提前

    function createSortPanel() {
        const normalStyle = { backgroundColor: '#f8f9fa', color: '#202124', border: '1px solid #dadce0' };
        const activeStyle = { backgroundColor: '#4285F4', color: 'white', border: '1px solid #4285F4' };

        const panel = document.createElement('div');
        panel.className = 'gs-sort-panel'; // 添加一个 class 以便之后查找
        Object.assign(panel.style, {
            position: 'fixed', bottom: '20px', right: '20px', zIndex: '9999',
            backgroundColor: 'white', border: '1px solid #dadce0', borderRadius: '8px',
            padding: '12px 16px', boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
            display: 'flex', flexDirection: 'column', gap: '12px'
        });

        const createSortRow = (labelText, descText, ascText) => {
            const row = document.createElement('div');
            Object.assign(row.style, { display: 'flex', alignItems: 'center', gap: '8px' });
            const label = document.createElement('span');
            Object.assign(label.style, { fontSize: '14px', color: '#5f6368', minWidth: '85px', flexShrink: '0' });
            label.textContent = labelText;
            const buttonStyle = { fontSize: '13px', padding: '6px 10px', cursor: 'pointer', borderRadius: '4px', fontWeight: '500', transition: 'all 0.2s', whiteSpace: 'nowrap' };
            const descButton = document.createElement('button');
            descButton.textContent = descText;
            Object.assign(descButton.style, buttonStyle);
            const ascButton = document.createElement('button');
            ascButton.textContent = ascText;
            Object.assign(ascButton.style, buttonStyle);
            row.append(label, descButton, ascButton);
            return { row, descButton, ascButton };
        };

        const yearRow = createSortRow('按年份:', '新 → 旧', '旧 → 新');
        const citeRow = createSortRow('按引用数:', '高 → 低', '低 → 高');

        // 将高亮函数赋值
        updateHighlights = (panelRef) => {
            const buttons = [
                { btn: yearRow.descButton, key: 'year', desc: true }, { btn: yearRow.ascButton,  key: 'year', desc: false },
                { btn: citeRow.descButton, key: 'cite', desc: true }, { btn: citeRow.ascButton,  key: 'cite', desc: false },
            ];
            buttons.forEach(item => {
                const isActive = (currentSortState.key === item.key && currentSortState.descending === item.desc);
                Object.assign(item.btn.style, isActive ? activeStyle : normalStyle);
            });
        };

        yearRow.descButton.addEventListener('click', () => sortElements('year', true));
        yearRow.ascButton.addEventListener('click', () => sortElements('year', false));
        citeRow.descButton.addEventListener('click', () => sortElements('cite', true));
        citeRow.ascButton.addEventListener('click', () => sortElements('cite', false));

        panel.append(yearRow.row, citeRow.row);
        document.body.appendChild(panel);
        updateHighlights(panel);
    }

    if (document.readyState === 'complete') {
        createSortPanel();
    } else {
        window.addEventListener('load', createSortPanel);
    }
})();