四川大学自动抢课简单脚本

川大自动抢课脚本 - 带有简单显示ui

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

// ==UserScript==
// @name         四川大学自动抢课简单脚本
// @namespace    http://tampermonkey.net/
// @version      1.01
// @description  川大自动抢课脚本 - 带有简单显示ui
// @author       Cloud Hypnos
// @license      GPL-3.0
// @match        http://zhjw.scu.edu.cn/student/courseSelect/courseSelect/*
// @match        https://zhjw.scu.edu.cn/student/courseSelect/courseSelect/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // 全局主题色配置
    const THEME_COLORS = {
        primary: '#ff6f91',      // 粉色主色调
        primaryLight: '#ff8fa3', // 浅粉色
        primaryDark: '#e6537c',  // 深粉色
        success: '#4caf50',      // 成功色
        error: '#f44336',        // 错误色
        warning: '#ff9800',      // 警告色
        info: '#2196f3',         // 信息色
        gray: '#6c757d'          // 灰色
    };

    let isRunning = false;
    let searchInterval;
    let isDragging = false;
    let dragOffset = { x: 0, y: 0 };
    let queryCount = 0;
    let notificationPermission = false;

    // 请求浏览器通知权限
    async function requestNotificationPermission() {
        if (!("Notification" in window)) {
            console.log("此浏览器不支持桌面通知");
            return false;
        }

        if (Notification.permission === "granted") {
            notificationPermission = true;
            return true;
        }

        if (Notification.permission !== "denied") {
            try {
                const permission = await Notification.requestPermission();
                notificationPermission = permission === "granted";
                if (notificationPermission) {
                    console.log("通知权限已获取");
                    new Notification("选课助手已就绪", {
                        body: "选课成功后将通过系统通知提醒您",
                        icon: "https://www.scu.edu.cn/favicon.ico",
                        tag: "course-assistant-ready"
                    });
                }
                return notificationPermission;
            } catch (error) {
                console.error("请求通知权限失败:", error);
                return false;
            }
        }
        return false;
    }

    // 发送系统通知
    function sendNotification(title, body, isSuccess = true) {
        if (!notificationPermission) {
            console.log("通知权限未授予");
            return;
        }

        try {
            const notification = new Notification(title, {
                body: body,
                icon: isSuccess ? "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234caf50'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z'/%3E%3C/svg%3E" :
                               "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23f44336'%3E%3Cpath d='M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z'/%3E%3C/svg%3E",
                requireInteraction: true,
                tag: `course-result-${Date.now()}`,
                vibrate: isSuccess ? [200, 100, 200] : [100, 50, 100]
            });

            notification.onclick = function() {
                window.focus();
                notification.close();
            };

            if (isSuccess) {
                playSuccessSound();
            }

        } catch (error) {
            console.error("发送通知失败:", error);
        }
    }

    // 播放成功提示音
    function playSuccessSound() {
        try {
            const audioContext = new (window.AudioContext || window.webkitAudioContext)();
            const oscillator = audioContext.createOscillator();
            const gainNode = audioContext.createGain();

            oscillator.connect(gainNode);
            gainNode.connect(audioContext.destination);

            oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
            oscillator.frequency.exponentialRampToValueAtTime(400, audioContext.currentTime + 0.3);

            gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
            gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);

            oscillator.start(audioContext.currentTime);
            oscillator.stop(audioContext.currentTime + 0.3);
        } catch (error) {
            console.log("无法播放提示音:", error);
        }
    }

    // 创建控制面板
    function createButton() {
        const panel = document.createElement('div');
        panel.id = 'autoGrabPanel';
        panel.innerHTML = `
            <div style="display: flex; border-radius: 5px; overflow: hidden; box-shadow: 0 3px 12px rgba(0,0,0,0.2);">
                <!-- 左侧拖动区域 -->
                <div id="dragArea" style="
                    width: 20px;
                    background: linear-gradient(135deg, ${THEME_COLORS.primary} 0%, ${THEME_COLORS.primaryDark} 100%);
                    cursor: move;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    padding: 6px 2px;
                    user-select: none;
                    transition: all 0.2s ease;
                " title="拖动">
                    <svg width="10" height="10" viewBox="0 0 24 24" fill="white" opacity="0.8">
                        <path d="M10 9h4V6h3l-5-5-5 5h3v3zm-1 1H6V7l-5 5 5 5v-3h3v-4zm14 2l-5-5v3h-3v4h3v3l5-5zm-9 3h-4v3H7l5 5 5-5h-3v-3z"/>
                    </svg>
                </div>

                <!-- 右侧功能区域 -->
                <div style="
                    background: white;
                    padding: 6px 8px;
                    min-width: 120px;
                    border: 1px solid #e0e0e0;
                    border-left: none;
                ">
                    <!-- 查询计数 -->
                    <div id="queryCounter" style="
                        margin-bottom: 5px;
                        font-size: 10px;
                        color: #666;
                        display: flex;
                        align-items: center;
                        justify-content: space-between;
                    ">
                        <span>查询: <span id="queryCount" style="font-weight: 700; color: ${THEME_COLORS.primary};">0</span></span>
                        <span id="timeElapsed" style="display: none;">运行: <span id="runTime">0</span>s</span>
                        <span id="notificationBadge" style="
                            font-size: 8px;
                            padding: 1px 3px;
                            background: ${THEME_COLORS.success};
                            color: white;
                            border-radius: 6px;
                            display: none;
                        ">通知</span>
                    </div>

                    <!-- 主按钮 -->
                    <button id="actionBtn" style="
                        width: 100%;
                        padding: 6px 8px;
                        background: linear-gradient(135deg, ${THEME_COLORS.primary} 0%, ${THEME_COLORS.primaryDark} 100%);
                        color: white;
                        border: none;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 12px;
                        font-weight: 700;
                        transition: all 0.2s ease;
                        box-shadow: 0 2px 6px rgba(0,0,0,0.15);
                        line-height: 1;
                    ">
                        开始抢课
                    </button>

                    <!-- 通知权限提示 -->
                    <div id="notificationHint" style="
                        margin-top: 4px;
                        padding: 4px 5px;
                        background: #fff3cd;
                        border: 1px solid #ffc107;
                        border-radius: 3px;
                        font-size: 9px;
                        color: #856404;
                        display: none;
                        line-height: 1.2;
                    ">
                        点击允许通知
                    </div>
                </div>
            </div>
        `;

        panel.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 9999;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        `;

        document.body.appendChild(panel);

        setupDragEvents(panel);
        setupButtonEvents();
        checkNotificationStatus();
    }

    // 设置拖动事件
    function setupDragEvents(panel) {
        const dragArea = document.getElementById('dragArea');

        dragArea.onmouseenter = function() {
            if (!isDragging) {
                dragArea.style.background = `linear-gradient(135deg, ${THEME_COLORS.primaryLight} 0%, ${THEME_COLORS.primary} 100%)`;
            }
        };

        dragArea.onmouseleave = function() {
            if (!isDragging) {
                dragArea.style.background = `linear-gradient(135deg, ${THEME_COLORS.primary} 0%, ${THEME_COLORS.primaryDark} 100%)`;
            }
        };

        dragArea.onmousedown = function(e) {
            isDragging = true;
            const rect = panel.getBoundingClientRect();
            dragOffset.x = e.clientX - rect.left;
            dragOffset.y = e.clientY - rect.top;

            dragArea.style.background = `linear-gradient(135deg, ${THEME_COLORS.primaryDark} 0%, ${THEME_COLORS.primary} 100%)`;
            dragArea.style.transform = 'scale(0.95)';
            document.body.style.cursor = 'move';

            const overlay = document.createElement('div');
            overlay.id = 'dragOverlay';
            overlay.style.cssText = `
                position: fixed;
                top: 0;
                left: 0;
                right: 0;
                bottom: 0;
                z-index: 9998;
                cursor: move;
            `;
            document.body.appendChild(overlay);

            e.preventDefault();
        };

        document.onmousemove = function(e) {
            if (isDragging) {
                panel.style.left = (e.clientX - dragOffset.x) + 'px';
                panel.style.top = (e.clientY - dragOffset.y) + 'px';
                panel.style.right = 'auto';
            }
        };

        document.onmouseup = function(e) {
            if (isDragging) {
                isDragging = false;
                dragArea.style.background = `linear-gradient(135deg, ${THEME_COLORS.primary} 0%, ${THEME_COLORS.primaryDark} 100%)`;
                dragArea.style.transform = 'scale(1)';
                document.body.style.cursor = 'auto';

                const overlay = document.getElementById('dragOverlay');
                if (overlay) {
                    overlay.remove();
                }
            }
        };

        document.oncontextmenu = function(e) {
            if (isDragging) {
                isDragging = false;
                dragArea.style.background = `linear-gradient(135deg, ${THEME_COLORS.primary} 0%, ${THEME_COLORS.primaryDark} 100%)`;
                dragArea.style.transform = 'scale(1)';
                document.body.style.cursor = 'auto';

                const overlay = document.getElementById('dragOverlay');
                if (overlay) {
                    overlay.remove();
                }

                e.preventDefault();
                return false;
            }
        };
    }

    // 设置按钮事件
    function setupButtonEvents() {
        const actionBtn = document.getElementById('actionBtn');

        actionBtn.onclick = function(e) {
            e.stopPropagation();
            toggleAutoGrab();
        };

        actionBtn.onmouseenter = function() {
            if (!isRunning) {
                this.style.transform = 'translateY(-1px)';
                this.style.boxShadow = '0 3px 8px rgba(0,0,0,0.15)';
            }
        };

        actionBtn.onmouseleave = function() {
            this.style.transform = 'translateY(0)';
            this.style.boxShadow = '0 2px 6px rgba(0,0,0,0.15)';
        };
    }

    // 检查通知权限状态
    function checkNotificationStatus() {
        if ("Notification" in window) {
            if (Notification.permission === "granted") {
                notificationPermission = true;
                document.getElementById('notificationBadge').style.display = 'inline-block';
            } else if (Notification.permission === "default") {
                document.getElementById('notificationHint').style.display = 'block';
                setTimeout(() => {
                    requestNotificationPermission().then(granted => {
                        if (granted) {
                            document.getElementById('notificationBadge').style.display = 'inline-block';
                            document.getElementById('notificationHint').style.display = 'none';
                        }
                    });
                }, 2000);
            }
        }
    }

    // 更新按钮
    function updateButton(text, color) {
        const actionBtn = document.getElementById('actionBtn');
        if (actionBtn) {
            actionBtn.textContent = text;
            actionBtn.style.background = color.includes('gradient') ? color : `linear-gradient(135deg, ${color} 0%, ${color} 100%)`;
        }
    }

    // 更新查询计数和运行时间
    let startTime = null;
    let timeInterval = null;

    function updateQueryCount() {
        queryCount++;
        const countElement = document.getElementById('queryCount');
        if (countElement) {
            countElement.textContent = queryCount;
        }
    }

    function startTimer() {
        startTime = Date.now();
        document.getElementById('timeElapsed').style.display = 'block';

        timeInterval = setInterval(() => {
            if (startTime) {
                const elapsed = Math.floor((Date.now() - startTime) / 1000);
                document.getElementById('runTime').textContent = elapsed;
            }
        }, 1000);
    }

    function stopTimer() {
        startTime = null;
        if (timeInterval) {
            clearInterval(timeInterval);
            timeInterval = null;
        }
        document.getElementById('timeElapsed').style.display = 'none';
    }

    function resetQueryCount() {
        queryCount = 0;
        const countElement = document.getElementById('queryCount');
        if (countElement) {
            countElement.textContent = queryCount;
        }
    }

    // 查询课程并获取结果
    function searchCourses() {
        return new Promise((resolve, reject) => {
            try {
                const iframe = document.querySelector("iframe");
                if (!iframe || !iframe.contentWindow) {
                    throw new Error('找不到iframe');
                }

                const iframeWin = iframe.contentWindow;
                if (typeof iframeWin.guolv !== 'function') {
                    throw new Error('找不到查询函数');
                }

                const original$ = iframeWin.$;
                const originalAjax = original$.ajax;

                original$.ajax = function(options) {
                    if (options.url && options.url.includes('/student/courseSelect/freeCourse/courseList')) {
                        const originalSuccess = options.success;

                        options.success = function(data) {
                            original$.ajax = originalAjax;
                            resolve(data);

                            if (originalSuccess) {
                                originalSuccess.call(this, data);
                            }
                        };

                        const originalError = options.error;
                        options.error = function(xhr, status, error) {
                            original$.ajax = originalAjax;
                            reject(new Error(`查询失败: ${error}`));

                            if (originalError) {
                                originalError.call(this, xhr, status, error);
                            }
                        };
                    }

                    return originalAjax.call(this, options);
                };

                iframeWin.guolv(1);

                setTimeout(() => {
                    original$.ajax = originalAjax;
                    reject(new Error('查询超时'));
                }, 8000);

            } catch (error) {
                reject(error);
            }
        });
    }

    // 选中课程
    function selectCourse(course) {
        try {
            const iframe = document.querySelector("iframe");
            const iframeWin = iframe.contentWindow;

            if (typeof iframeWin.dealHiddenData !== 'function') {
                throw new Error('找不到选课函数');
            }

            iframeWin.dealHiddenData(course, true);
            console.log(`选中课程: ${course.kcm} - ${course.skjs} (余量:${course.bkskyl})`);
            return true;

        } catch (error) {
            console.error('选中课程失败:', error);
            return false;
        }
    }

    // 提交选课
    function submitCourse() {
        return new Promise((resolve, reject) => {
            // TODO resovle the CheckCode msg
            const yzmArea = $("#yzm_area");
            if (yzmArea.length > 0 && yzmArea.css("display") !== "none" && !$("#submitCode").val()) {
                reject(new Error('请先输入验证码'));
                return;
            }

            const originalAjax = $.ajax;
            $.ajax = function(options) {
                if (options.url && options.url.includes('checkInputCodeAndSubmit')) {
                    const originalSuccess = options.success;
                    const originalError = options.error;

                    options.success = function(data) {
                        $.ajax = originalAjax;
                        if (originalSuccess) originalSuccess.call(this, data);

                        if (data.result === 'ok') {
                            resolve('选课成功');
                        } else {
                            reject(new Error(data.result));
                        }
                    };

                    options.error = function(xhr) {
                        $.ajax = originalAjax;
                        if (originalError) originalError.call(this, xhr);
                        reject(new Error(`提交失败: ${xhr.status}`));
                    };
                }
                return originalAjax.call(this, options);
            };

            try {
                if (typeof window.tijiao === 'function') {
                    window.tijiao();
                } else {
                    throw new Error('找不到提交函数');
                }
            } catch (error) {
                $.ajax = originalAjax;
                reject(error);
            }
        });
    }

    // 主抢课逻辑
    async function performGrab() {
        try {
            updateButton('查询中...', `linear-gradient(135deg, ${THEME_COLORS.warning} 0%, #f57c00 100%)`);
            updateQueryCount();

            const data = await searchCourses();

            if (!data || !data.rwRxkZlList || data.rwRxkZlList.length === 0) {
                updateButton('未找到课程', `linear-gradient(135deg, ${THEME_COLORS.gray} 0%, #495057 100%)`);
                return false;
            }

            const availableCourse = data.rwRxkZlList.find(course => course.bkskyl > 0);

            if (!availableCourse) {
                updateButton(`暂无余量`, `linear-gradient(135deg, ${THEME_COLORS.gray} 0%, #495057 100%)`);
                return false;
            }

            updateButton(`发现目标课程`, `linear-gradient(135deg, ${THEME_COLORS.success} 0%, #388e3c 100%)`);

            if (!selectCourse(availableCourse)) {
                updateButton('❌ 选中失败', `linear-gradient(135deg, ${THEME_COLORS.error} 0%, #d32f2f 100%)`);
                stopAutoGrab();

                sendNotification(
                    "选课失败",
                    `无法选中课程: ${availableCourse.kcm}`,
                    false
                );

                return false;
            }

            await new Promise(resolve => setTimeout(resolve, 500));
            updateButton('提交中...', `linear-gradient(135deg, ${THEME_COLORS.warning} 0%, #f57c00 100%)`);

            const result = await submitCourse();

            updateButton(`✅ 选课成功`, `linear-gradient(135deg, ${THEME_COLORS.success} 0%, #388e3c 100%)`);

            sendNotification(
                "选课成功",
                `已成功选中: ${availableCourse.kcm}\n${availableCourse.skjs}`,
                true
            );

            stopAutoGrab();
            return true;

        } catch (error) {
            if (error.message.includes('验证码')) {
                updateButton('需要验证码', `linear-gradient(135deg, ${THEME_COLORS.warning} 0%, #f57c00 100%)`);

                sendNotification(
                    "需要验证码",
                    "请在页面上输入验证码后重新开始",
                    false
                );

                stopAutoGrab();
                alert('请输入验证码后重新开始');
                return false;
            } else {
                updateButton(`❌ 出现错误`, `linear-gradient(135deg, ${THEME_COLORS.error} 0%, #d32f2f 100%)`);

                sendNotification(
                    "选课出错",
                    error.message,
                    false
                );

                stopAutoGrab();
                return false;
            }
        }
    }

    // 开始/停止抢课
    function toggleAutoGrab() {
        if (isRunning) {
            stopAutoGrab();
        } else {
            startAutoGrab();
        }
    }

    // 开始自动抢课
    function startAutoGrab() {
        isRunning = true;
        resetQueryCount();
        startTimer();
        console.log('[抢课脚本] 🚀 开始自动抢课');
        updateButton('停止抢课', `linear-gradient(135deg, ${THEME_COLORS.error} 0%, #d32f2f 100%)`);

        performGrab().then(success => {
            if (!success && isRunning) {
                scheduleNextQuery();
            }
        });

        searchInterval = setInterval(async () => {
            if (!isRunning) {
                clearInterval(searchInterval);
                return;
            }

            const success = await performGrab();

            if (!success && isRunning) {
                scheduleNextQuery();
            } else if (!isRunning) {
                clearInterval(searchInterval);
            }
        }, Math.random() * 100 + 600);
    }

    // 安排下次查询
    function scheduleNextQuery() {
        if (isRunning) {
            const delay = Math.random() * 10 + 10;
            updateButton('等待中...', `linear-gradient(135deg, ${THEME_COLORS.info} 0%, #1976d2 100%)`);
            setTimeout(() => {
                if (isRunning) {
                    updateButton('查询中...', `linear-gradient(135deg, ${THEME_COLORS.info} 0%, #1976d2 100%)`);
                }
            }, delay);
        }
    }

    // 停止自动抢课
    function stopAutoGrab() {
        isRunning = false;
        stopTimer();
        updateButton('开始抢课', `linear-gradient(135deg, ${THEME_COLORS.primary} 0%, ${THEME_COLORS.primaryDark} 100%)`);

        if (searchInterval) {
            clearInterval(searchInterval);
            searchInterval = null;
        }

        console.log('[抢课脚本] 已停止');
    }

    // 初始化
    function init() {
        const checkReady = setInterval(() => {
            const iframe = document.querySelector("iframe");
            if (iframe && iframe.contentWindow && document.querySelector('#myTab4')) {
                clearInterval(checkReady);
                setTimeout(() => {
                    createButton();
                    console.log('[抢课脚本] 已就绪');
                    requestNotificationPermission();
                }, 1000);
            }
        }, 1000);
    }

    // 启动
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();