Greasy Fork

Greasy Fork is available in English.

PKU 手动抢课小助手

过滤选课列表,只显示目标课程,实时高亮课程名额状态,支持一键识别验证码

当前为 2025-02-18 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         PKU 手动抢课小助手
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  过滤选课列表,只显示目标课程,实时高亮课程名额状态,支持一键识别验证码
// @author       goudanZ1
// @license      MIT
// @match        https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/supplement.jsp*
// @match        https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/SupplyCancel.do*
// @match        https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/electSupplement.do*
// @match        https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/supplement/cancelCourse.do*
// @icon         https://www.pku.edu.cn/pku_logo_red.png
// @grant        GM_xmlhttpRequest
// @connect      api.ttshitu.com
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    // ******************** 以下 4 项内容需要填写 ********************

    // 1. 填写想抢的课程名与班号,在每页中只会显示 allowedCourses 中的课程,但如果这些课程
    //    分布在不同的页上,仍然需要手动换页来查看。同一个课程名对应的班号(无论一个还是多个)
    //    需要写在数组(方括号)中。
    const allowedCourses = {
        '摸鱼学导论': [6],
        '划水学原理': [1, 3],
        '投点理论与实践': [2],
    };

    // 2. 是否禁止表格的行在光标位于其上时变成黄绿色(true/false)
    const banColorChange = true;

    // 3. 课程有名额、无名额时在“限数/已选”栏显示的背景颜色和文本颜色,默认依次为浅绿、深绿、浅红、深红
    const underLimitStyle = 'background-color: #abebc6; color: #145a32';
    const reachLimitStyle = 'background-color: #f5b7b1; color: #7b241c';

    // 4. 填写 TT 识图账号的用户名和密码(http://www.ttshitu.com/,请确保账户有余额)
    const recognizerConfig = {
        username: 'PKUer',
        password: 'wasd1234',
    };

    // ******************** 以上 4 项内容需要填写 ********************

    const table = document.querySelector('table.datagrid');
    const rows = table.querySelectorAll('tr.datagrid-even,tr.datagrid-odd');
    const visibleRows = [];

    // Hide unnecessary courses
    rows.forEach(row => {
        const courseName = row.children[0].textContent.trim();
        const classNumber = parseInt(row.children[5].textContent.trim());
        if (courseName in allowedCourses && allowedCourses[courseName].includes(classNumber)) {
            visibleRows.push(row);
        } else {
            row.style.display = 'none';
        }
    });

    // Reset the color style for visible rows and optionally cancel color changes
    visibleRows.forEach((row, index) => {
        const newClass = index % 2 === 0 ? 'datagrid-even' : 'datagrid-odd';
        row.className = newClass;
        if (banColorChange) {
            row.onmouseover = null;
            row.onmouseout = null;
        }
        else {
            row.onmouseover = () => {
                row.className = 'datagrid-all';
            };
            row.onmouseout = () => {
                row.className = newClass;
            };
        }
    });

    // Set the color style for 'limit/elected' grids and change a grid from red to
    // green when its corresponding '刷新' becomes '补选'
    visibleRows.forEach(row => {
        const numCell = row.children[9]; // <td><span id='electedNum**'>* / *</span></td>
        const refreshCell = row.children[10].children[0];
        // <a><span>补选</span></a>, <a id='refreshLimit**'><span>刷新</span></a>

        numCell.children[0].style.fontSize = '13px';
        if (refreshCell.textContent.trim() === '补选') {
            numCell.style.cssText = underLimitStyle;
        } else {
            numCell.style.cssText = reachLimitStyle;

            // function refreshLimit() in supplement.js:
            //    var aTag = $('#refreshLimit' + index + index); aTag.html( '<span>补选</span>'); ...
            //
            //    When the latest refresh request indicates that elected < limit, the innerHTML 
            // of refreshCell changes, and results in a childList mutation. I don't want to observe
            // numCell, since every refresh request will reset the 'limit/elected' string no matter
            // it has changed or not, which would be too frequent to observe.
            const observer = new MutationObserver((mutationList, observer) => {
                numCell.style.cssText = underLimitStyle;
                observer.disconnect();
                // The text won't be changed again, since clicking '补选' won't trigger refreshLimit()
            });
            observer.observe(refreshCell, { childList: true });
        }
    });

    // Get the base64 form of the captcha image (mostly by DeepSeek)
    function getBase64Data() {
        const image = document.querySelector('#imgname');
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        canvas.width = image.naturalWidth;
        canvas.height = image.naturalHeight;
        ctx.drawImage(image, 0, 0);
        return canvas.toDataURL('image/jpeg').split(',')[1]; // 'data:image/jpeg;base64,***...'
    }

    // Write the validation code into the input box
    function setValidationCode(code) {
        const inputBox = document.querySelector('#validCode');
        inputBox.value = code.slice(0, 5);
    }

    // Send a cross-domain request to recognize the captcha image
    function recognizeImage() {
        const base64 = getBase64Data();
        GM_xmlhttpRequest({
            method: 'POST',
            url: 'http://api.ttshitu.com/predict',
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify({
                username: recognizerConfig.username,
                password: recognizerConfig.password,
                typeid: '1003',
                image: base64
            }),
            onload: (res => {
                try {
                    const response = JSON.parse(res.response);
                    if (response.success) {
                        setValidationCode(response.data.result);
                    } else {
                        alert('识别验证码失败:' + response.message);
                    }
                } catch (e) {
                    alert('识图响应解析失败:' + e);
                }
            })
        });
    }

    // Create a button for recognization and insert it before the input box
    const btn = document.createElement('button');
    btn.textContent = '识别验证码';
    btn.style.cssText = 'margin-left: 5px; margin-right: 10px; padding: 2px 8px; cursor: pointer';
    btn.onclick = recognizeImage;

    const inputBox = document.querySelector('#validCode');
    inputBox.value = ''; // Clear the cached value
    inputBox.parentNode.insertBefore(btn, inputBox);

})();