DGUT Ulearning Tool

一个适用于新版本DGUT U学院的脚本,支持自动播放视频,调整倍速,并从官方API获取章节测验答案。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         DGUT Ulearning Tool
// @version 1.1.0
// @match        https://ua.dgut.edu.cn/learnCourse/learnCourse.html*
// @description  一个适用于新版本DGUT U学院的脚本,支持自动播放视频,调整倍速,并从官方API获取章节测验答案。
// @run-at       document-end
// @grant        GM_xmlhttpRequest
// @license MIT
// @namespace https://greasyfork.org/users/1537344
// ==/UserScript==


(function () {
    'use strict';

    let video_speed = 6;
    let logBox;


    class AnswerVideo {
        constructor() { }

        getType(questionNode) {
            const tag = questionNode.querySelector('.question-type-tag');
            if (!tag) return null;

            const text = tag.textContent.trim();
            if (text.includes('单选题')) return 'single';
            if (text.includes('多选题')) return 'multiple';
            if (text.includes('判断题')) return 'judge';
            if (text.includes('填空题')) return 'blank';
            return null;
        }

        answerQuestions(questionId, answerList) {

            const questionNode = document.querySelector(`#question${questionId}`);
            if (!questionNode) {
                Yukilog(`Qusetion ${questionId} called: ${type}`);
                return;
            }

            const type = this.getType(questionNode);
            if (!type) {
                Yukilog(`Unknown Type Qusetion called: ${type}`);
                return;
            }

            switch (type) {
                case 'single':
                    this.choice(questionNode, answerList);
                    break;
                case 'multiple':
                    this.choice(questionNode, answerList);
                    break;
                case 'judge':
                    this.judge(questionNode, answerList);
                    break;
                case 'blank':
                    this.blank(questionNode, answerList);
                    break;
                default:
                    Yukilog(`Unsupported question type: ${type} for question ${questionId}`);
            }

        }

        choice(questionNode, answerList) {
            const choiceItems = questionNode.querySelectorAll('.choice-item');
            choiceItems.forEach(item => {
                const optionLetter = item.querySelector('.option')?.textContent?.trim().replace('.', '');
                if (optionLetter && answerList.includes(optionLetter)) {
                    const checkbox = item.querySelector('.checkbox');
                    if (checkbox && !checkbox.classList.contains('selected')) {
                        item.click();
                        Yukilog(`已勾选选项 ${optionLetter}`);
                    }
                }
            });
            Yukilog(`选择题选择: ${answerList}`);
        }

        judge(questionNode, answerList) {
            const isCorrect = String(answerList) === 'true';
            const btnClass = isCorrect ? '.right-btn' : '.wrong-btn';
            const btn = questionNode.querySelector(btnClass);
            if (btn && !btn.classList.contains('selected')) {
                btn.click();
                Yukilog(`判断题选择: ${isCorrect ? 'true' : 'false'}`);
            }
        }

        blank(questionNode, answerList) {
            Yukilog(`填空题自动作答功能尚未实现`);
        }

    }

    function Yukilog(msg) {
        console.log(`DGUT Ulearning Tool: ${msg}`);
        if (logBox) {
            logBox.innerHTML += msg.replace(/\n/g, "<br>") + "<br>";
            logBox.scrollTop = logBox.scrollHeight;
        }
    }

    function controlPanel() {
        const controlPanel = document.createElement("div");
        controlPanel.style.cssText = `
            position: fixed;
            top: 100px;
            right: 30px;
            z-index: 999999;
            background: rgba(0,0,0,0.6);
            color: #fff;
            padding: 10px 12px;
            border-radius: 8px;
            font-size: 14px;
            width: 250px;
            cursor: move;
        `;
        controlPanel.innerHTML = `
            <div>DGUT Ulearning Tools</div>
            <div style="margin-top:6px;">视频播放倍速: <span id="speedVal">${video_speed}</span>x</div>
            <button id="speedUp" style="margin:4px;">➕</button>
            <button id="speedDown" style="margin:4px;">➖</button>
            <div style="margin-top:8px;">输出:</div>
            <div id="logBox" style="height:150px;overflow:auto;background:#111;padding:4px;border-radius:4px;font-size:12px; line-height: 1.5;"></div>
        `;

        document.body.appendChild(controlPanel);

        document.getElementById("speedUp").onclick = () => controlSpeed(1);
        document.getElementById("speedDown").onclick = () => controlSpeed(-1);
        logBox = document.getElementById("logBox");

        controlPanel.onmousedown = function (e) {
            let offsetX = e.clientX - controlPanel.offsetLeft;
            let offsetY = e.clientY - controlPanel.offsetTop;

            document.onmousemove = function (e) {
                controlPanel.style.left = e.clientX - offsetX + "px";
                controlPanel.style.top = e.clientY - offsetY + "px";
            };

            document.onmouseup = function () { document.onmousemove = null; };
        };

    }

    function controlSpeed(speed) {
        video_speed = video_speed + speed;
        if (video_speed < 1) {
            video_speed = 1;
            Yukilog("补药再减速啦 O (≧口≦)O");
        }
        document.getElementById("speedVal").innerText = video_speed;
        const video = document.querySelector("video");
        if (video) video.playbackRate = video_speed;
        Yukilog(`视频播放倍速调整为 ${video_speed}x`);
    }

    function init() {
        new MutationObserver((mutations, observer) => {
            main();
        }).observe(document.body, { childList: true, subtree: true });
        main();
    }

    function main() {
        const video = document.querySelector('video');
        if (video && !video.dataset.hooked) {
            video.dataset.hooked = 'true';
            Yukilog("(´ ∀ ` *) 找到视频,开始刷课啦!");
            video.playbackRate = video_speed;
            video.muted = true; // mute the video by default
            video.play().catch(e => Yukilog("播放失败,请手动点击一次。"));

            const intervalId = setInterval(() => {
                if (!document.contains(video)) {
                    clearInterval(intervalId);
                    return;
                }
                video.playbackRate = video_speed;
                if (video.paused) video.play();
            }, 2000);

            video.addEventListener("ended", () => {
                Yukilog("视频播放结束。");
                goNext();
            });

        }

        const quizPanel = document.querySelector(".question-setting-panel");
        if (quizPanel && !quizPanel.dataset.hooked) {
            quizPanel.dataset.hooked = 'true';
            Yukilog("检测到做题页面,开始获取答案...");

            let result = getElementsId();

            result.forEach(({ questionId, parentId }) => {
                fetchAnswers(questionId, parentId);
            });

            submitQuiz();

        }
    }

    function submitQuiz() {
        const btn = document.querySelector('.btn-submit');
        if (btn) {
            btn.click();
        } else {
            setTimeout(btn.click(), 1000);
        }
    }


    function goNext() {
        Yukilog("(* ^ ω ^) 视频播放完毕,自动跳下一节...");
        let nextBtn = document.querySelector(".next-btn,.btn-next,.nextVideoBtn,.mobile-next-page-btn");
        if (nextBtn && !nextBtn.classList.contains('disabled')) {
            nextBtn.click();
        } else {
            Yukilog("未找到或无法点击下一节按钮,可能是本章结束。");
        }
    }

    function getElementsId() {
        const result = [];
        let parentId = $('.page-name.active').parent().attr('id').substring(4);

        const questionElements = document.querySelectorAll('.question-element-node');
        questionElements.forEach(node => {
            const wrapper = node.querySelector('[id^="question"]') || node;
            if (wrapper && wrapper.id.startsWith('question')) {
                const questionId = wrapper.id.replace('question', '');
                result.push({ parentId, questionId });
            }
        });
        return result;
    }

    function getUAAuth() {
        let uaAuth = document.cookie.split(";")
            .map(c => c.trim().split("="))
            .find(([k, v]) => k === "AUTHORIZATION")?.[1] || "";
        if (!uaAuth) Yukilog("未找到 UA-AUTHORIZATION!");
        return uaAuth;
    }

    function fetchAnswers(questionId, parentId) {
        const uaAuth = getUAAuth();
        if (!uaAuth) return Yukilog("UA-AUTHORIZATION 为空,无法请求");

        GM_xmlhttpRequest({
            method: "GET",
            url: `https://ua.dgut.edu.cn/uaapi/questionAnswer/${questionId}?parentId=${parentId}`,
            headers: {
                "UA-AUTHORIZATION": uaAuth,
                "X-Requested-With": "XMLHttpRequest",
                "Referer": window.location.href
            },
            onload: function (res) {
                try {
                    const data = JSON.parse(res.responseText);
                    const answerList = data.correctAnswerList || [];
                    console.log("题目答案抓取成功:", data);

                    const answerer = new AnswerVideo();
                    if (answerList.length > 0) {
                        answerer.answerQuestions(questionId, answerList);
                    }
                } catch (e) {
                    console.error("解析答案失败", e, res.responseText);
                }
            },
            onerror: function (err) {
                console.error("拉取答案失败", err);
            }
        });
    }


    // START
    controlPanel();
    setTimeout(init, 2000);

})();