Q学友刷课

Q学友刷课脚本,人脸识别后,点击刷课即可开始

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Q学友刷课
// @namespace    https://www.qxueyou.com/
// @version      2024-01-15
// @description  Q学友刷课脚本,人脸识别后,点击刷课即可开始
// @author       Kane Simmons
// @match        https://www.qxueyou.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=tampermonkey.net
// @license MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    let interval = null // interval定时器
    let timeout = null // setTimeout定时器
    const courses = new Map(); // 课程Map {courseDom: 课程dom, rateDom: 课程进度条dom, rate: 课程完成度, playTimes: 播放次数}
    let runningTime = 0 // 运行时长(s)
    let processing = false // 正在答题
    let currentPlayingIndex = 0 // 当前播放的课程索引
  
    /** 更新课程进度 */
    const updateCourseRate = (isInit = false) => {
        const courseDoms = document.getElementsByClassName('m-slide slipDel')
        if (isInit) {
            // 初始化课程进度
            for (let i = 0; i < courseDoms.length; i++) {
                const courseDom = courseDoms[i].querySelector('.v-list-item--link');
                const rateDom = courseDoms[i].getElementsByClassName('progress col col-3')[0]
                const rate = parseInt(rateDom.querySelector('div').getAttribute('aria-valuenow'), 10);
                courses.set(i, { index: i + 1, courseDom, rateDom, rate, playTimes: 0 });
            }
            return;
        }
        // 更新进度到courses的map中
        const rateDoms = Array.from(courses.values()).map(item => item.rateDom)
        for (let i = 0; i < rateDoms.length; i++) {
            const rate = parseInt(rateDoms[i].querySelector('div').getAttribute('aria-valuenow'), 10);
            courses.get(i).rate = rate;
        }
    }
  
    /** 更新当前播放课程索引 */
    const updateCurrentPlayingIndex = () => {
        const courseDoms = Array.from(courses.values()).map(item => item.courseDom)
        currentPlayingIndex = courseDoms.findIndex(item => item.classList.contains('grey'))
    }
  
    /** 检测视频是否异常,并切换到未完成的科目 */
    const checkVideoStatus = () => {
        // 检测视频是否异常
        const isLoadingShow = window.getComputedStyle(document.querySelector('.vjs-loading-spinner')).getPropertyValue('display') === 'block'
        if (isLoadingShow) {
            console.warn(`当前视频(第${currentPlayingIndex + 1}集)异常`)
            switchToUnwatchCourse()
        }
        // 检测当前播放课程是否完成
        if (courses.get(currentPlayingIndex).rate === 100) {
            console.log(`第${currentPlayingIndex + 1}集课程已完成`)
            switchToUnwatchCourse()
        }
    }
  
    /** 切换到未完成的科目 */
    const switchToUnwatchCourse = () => {
        let minPlays = Infinity;
        let minPlayIndex = -1;
        let arePlayTimesEqual = true;
        let firstRateUnder100Index = -1;
  
        // 找到 rate < 100 的课程中 playTimes 最少的课程
        courses.forEach((course, index) => {
            if (course.rate < 100) {
                if (firstRateUnder100Index === -1) {
                    firstRateUnder100Index = index;
                }
  
                if (course.playTimes < minPlays) {
                    minPlays = course.playTimes;
                    minPlayIndex = index;
                    arePlayTimesEqual = false;
                } else if (course.playTimes > minPlays) {
                    arePlayTimesEqual = false;
                }
            }
        });
  
        // 如果所有 rate < 100 的课程的 playTimes 都相等,则找到第一个 rate < 100 的课程
        if (arePlayTimesEqual && firstRateUnder100Index !== -1) {
            minPlayIndex = firstRateUnder100Index;
        }
  
        // 点击选定的课程并更新 playTimes
        if (minPlayIndex !== -1) {
            // 如果只剩一项 rate < 100 的课程,则先点击别的项目
            let RateUnder100Subjects = []
            RateUnder100Subjects = Array.from(courses.values()).filter(item => item.rate < 100)
            if (RateUnder100Subjects.length === 1) {
                const courseDoms = Array.from(courses.values()).map(item => item.courseDom)
                let otherIndex = minPlayIndex + 1 >= courseDoms.length ? 0 : minPlayIndex + 1;
                courseDoms[otherIndex].click();
                console.log(`只剩一项,先点击第${otherIndex}集`)
            }
  
            const selectedCourse = courses.get(minPlayIndex);
            selectedCourse.courseDom.click();
            currentPlayingIndex = minPlayIndex;
            selectedCourse.playTimes++;
  
            // 输出播放次数和切换信息
            console.table(Array.from(courses.values()).map(({ index, rate, playTimes }) => ({ '课程序号': index, '课程完成度(%)': rate, '播放次数': playTimes })));
            console.log(`已切换至第${selectedCourse.index}集`)
  
        }
    }
  
    /** 检查问题弹窗是否出现,出现则回答问题 */
    const checkQuestion = () => {
        const modelDom = document.querySelector('.overlay')
        if (!modelDom || processing) return // 未发现问题弹窗或正在处理不继续
  
        console.log('发现问题弹窗!');
        answerQuestions()
    }
  
    /** 回答弹窗的问题 */
    const answerQuestions = () => {
        processing = true
        const modelDom = document.querySelector('.overlay')
        const bodyDoms = modelDom.querySelectorAll('div')
        let randomTime = (Math.floor(Math.random() * 60) + 1); // 生成 60s 的随机数
        if(bodyDoms.length) {
            const questionDom = bodyDoms[1]
            const questionResult = eval(questionDom.innerText.split('=')[0])
            console.log(`答案是:${questionResult}, 将在${randomTime}s后选择答案`)
            timeout = setTimeout(() => {
                const answersDoms = bodyDoms[5].querySelectorAll('.v-radio')
                answersDoms.forEach((item) => {
                    const answer = item.innerText.split('.')[1] * 1
                    const answerDom = item.querySelector('.v-label')
                    if (answer === questionResult) {
                        answerDom && answerDom.click()
                        const confirmDom = document.getElementsByClassName('white--text mb-2 v-btn v-btn--block v-btn--contained theme--light v-size--small')[0]
                        confirmDom && confirmDom.click()
                        processing = false
                        console.log('已选择正确答案');
                    }
                })
            }, randomTime)
        }
    }
  
    /** 检查是否全部课程已完成 */
    const checkAllCompleted = () => {
        const isFinished = Array.from(courses.values()).findIndex(item => item.rate < 100) === -1
        if (isFinished) {
            console.log(`%c全部课程已完成!总用时:${runningTime / 60 / 60}小时`, 'color: green; font-weight: bold;');
            alert(`全部课程已完成!总用时:${(runningTime / 60 / 60).toFixed(2)}小时`)
            clearInterval(interval)
            clearTimeout(timeout)
        } else {
            console.log('正在运行...')
            const startBtn = document.querySelector('#startBtn')
            const restRate = Array.from(courses.values()).reduce((acc, item) => acc + item.rate, 0) / 8
            startBtn.innerText = `刷课中,已运行${runningTime / 60}分钟,已完成${restRate}%`;
            if (restRate > 95) {
              // 整体进度大于95%时,点击学习进度按钮,通过网络请求获取真正进度
              const updateRateDom = document.getElementsByClassName('m-slide__del m-slide__del-red')[0]
              updateRateDom && updateRateDom.click()
              const checkModel = setInterval(() => {
                  const confirmDom = document.getElementsByClassName('v-btn v-btn--contained theme--light v-size--default primary')[0]
                  if (confirmDom) {
                    confirmDom.click()
                    clearInterval(checkModel)
                  }
              }, 100)
            }
        }
    }
  
    /** 每秒执行 */
    const mainProcess = () => {
        interval = setInterval(() => {
            try {
  
                runningTime++
  
                // 每分钟
                if (runningTime % 60 ===  0) {
                    // 更新课程进度
                    updateCourseRate()
                    // 每分钟都检查下视频是否异常
                    checkVideoStatus()
                    // 检查是否全部课程已完成
                    checkAllCompleted()
                }
  
                // 每45分钟
                if ((runningTime % (60 * 45) === 0) && !processing) {
                    console.log('每45分钟切换一次课程')
                    switchToUnwatchCourse()
                }
  
                // 更新当前播放课程索引
                updateCurrentPlayingIndex()
                // 检查问题弹窗
                checkQuestion()
  
            } catch (e) {
                console.error('程序报错了!!已停止检查弹窗', e)
                clearInterval(interval)
                clearTimeout(timeout)
            }
        }, 1000)
    }
  
    /** 生成开始按钮 */
    const domInit = () => {
        // 创建一个按钮元素
        var button = document.createElement("button");
  
        // 设置按钮的文本内容
        button.innerText = "开始刷课";
  
        // 设置按钮的 ID
        button.id = "startBtn"; // 添加 ID
  
        // 设置按钮的点击事件处理程序
        function clickHandler() {
            startProgram()
            button.innerText = "刷课中";
            button.style.backgroundColor = "#409eff";
            button.style.borderColor = "#409eff";
            // 移除按钮的点击事件处理程序
            button.removeEventListener("click", clickHandler);
        };
  
        // 添加按钮的点击事件处理程序
        button.addEventListener("click", clickHandler);
  
  
        // 设置按钮的样式
        button.style.cssText = "position: fixed; bottom: 10%; left: 50%; transform: translateX(-50%); padding: 12px 23px;color: #fff; background-color: #67c23a; border-color: #67c23a;";
  
        // 将按钮直接添加到 body 元素中
        document.body.appendChild(button);
    }
  
    /** 启动程序 */
    const startProgram = () => {
  
        mainProcess()
  
        updateCourseRate(true)
  
        checkAllCompleted()
  
        switchToUnwatchCourse()
    }
  
    domInit()
  })();