DGUT Ulearning Tool

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

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

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

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

您需要先安装一个扩展,例如 篡改猴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);

})();