DGUT Ulearning Tool

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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);

})();