Greasy Fork

Greasy Fork is available in English.

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

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

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