新版正方教务系统导出课程表

通过对新版正方教务系统的课表页面的解析,实现导出一个适用于大部分ics日历的文件,理论使用于所有使用新版正方教务系统(可对 ``include`` 进行一定的修改以适用不同的学校的链接)

当前为 2020-06-02 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         新版正方教务系统导出课程表
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  通过对新版正方教务系统的课表页面的解析,实现导出一个适用于大部分ics日历的文件,理论使用于所有使用新版正方教务系统(可对 ``include`` 进行一定的修改以适用不同的学校的链接)
// @author       31415926535x
// @supportURL   https://github.com/31415926535x/CollegeProjectBackup/blob/master/ZhengfangClassScheduleToICS/Readme.md
// @compatible   chrome
// @compatible   firefox
// @license      MIT
// @include      *://jwgl.*.edu.cn/*
// @run-at       document-start
// ==/UserScript==



// 根据自己学校教务系统的网址修改,应该对于新版教务系统的地址都是一样的,故只需修改上面 include中的教务系统的地址即可
var ClassScheduleToICSURL = "kbcx/xskbcx_cxXskbcxIndex.html";   // 学生课表查询页面,将该学期的课程信息导出为ics
var StudentEvalutionURL = "xspjgl/xspj_cxXspjIndex.html";        // 学生评教页面

var setTimeout_ = 4000;                                          // 设置脚本实际运行的开始时间,网络不好建议时间稍长,1000等于1s
(function (){
    
    'use strict';
    
    console.log("Script running.....");
    unsafeWindow.addEventListener("load", main);
    
})();

function main(){
    var windowURL = window.location.href;
    if(windowURL.indexOf(ClassScheduleToICSURL) != -1){
        ClassScheduleToICS();
    }
    else if(windowURL.indexOf(StudentEvalutionURL) != -1){
        // StudentEvalution();
        // unsafeWindow.addEventListener("load", StudentEvalution);
        document.getElementById("btn_yd").onclick = function(){
            window.setTimeout(StudentEvalution, setTimeout_);
        }
    }
}

function ClassScheduleToICS(){
    console.log("ClassScheduleToICS");
    // 在课表上方创建一个点击按钮
    // --------------------------------------------------------------------------
    // unsafeWindow.addEventListener ("load", pageFullyLoaded);
    pageFullyLoaded();
    //加载完成后运行
    function pageFullyLoaded(){
        console.log("Fucking ZhengFang...");
        let div = document.getElementsByClassName("btn-toolbar pull-right")[0];
        let btn = document.createElement("button");
        btn.className = "btn btn-default";
        btn.id = "exportbtn";
        let sp = document.createElement("span");
        sp.innerText = "生成课表";
        sp.className = "bigger-120 glyphicon glyphicon-file";
        btn.append(sp);

        let dwnbtn = document.createElement("button");
        dwnbtn.className = "btn btn-default";
        sp = document.createElement("span");
        sp.innerText = "选择本学期第一个星期一:";
        sp.className = "bigger-120 glyphicon glyphicon-time";
        dwnbtn.appendChild(sp);
        let StartDate = document.createElement("input");
        StartDate.type = "date";
        StartDate.value = "2020-01-01";
        div.appendChild(btn);
        dwnbtn.appendChild(StartDate);
        
        let a = document.createElement("a");
        a.innerHTML = "下载ics文件";
        a.style.visibility = "hidden";
        a.className = "bigger-120 glyphicon glyphicon-download-alt";
        dwnbtn.appendChild(a);
        div.appendChild(dwnbtn);


        btn.onclick = function(){
            // 获取本学期的第一个星期一
            Calendar.StartDate = StartDate.value;

            // 获取课表
            // let parseTableData = parseTable();
            // console.log(parseTableData);
            // let parseCoursesData = parseCourses(parseTableData);
            // console.log(parseCoursesData);
            // let generateCalendarData = generateCalendar(parseCoursesData);
            
            // let getFixedIcsData = getFixedIcs(generateCalendarData);
            // console.log(getFixedIcsData);
            // exportIcs(getFixedIcsData, a);
            exportIcs(getFixedIcs(generateCalendar(parseCourses(parseTable()))), a);    // 嘿嘿。。
            alert("ics文件已经生成,请点击下载后导入到您所使用的日历文件;Google Calendar需要自行设置课程的颜色。。。");
        }
    }
    // --------------------------------------------------------------------------


    // 全局变量Week的双引射
    // --------------------------------------------------------------------------
    var Week;
    (function (Week) {
        Week[Week["Monday"] = 1] = "Monday";
        Week[Week["TuesDay"] = 2] = "TuesDay";
        Week[Week["Wednesday"] = 3] = "Wednesday";
        Week[Week["ThursDay"] = 4] = "ThursDay";
        Week[Week["Friday"] = 5] = "Friday";
        Week[Week["Saturday"] = 6] = "Saturday";
        Week[Week["Sunday"] = 7] = "Sunday";
    })(Week || (Week = {}));
    // --------------------------------------------------------------------------

    


    // 从页面中获取课程的 div, 返回对应的星期以及div数组
    // --------------------------------------------------------------------------
    function parseTable(){
        let table = document.getElementById("kbgrid_table_0");
        // console.log(table);
        // let tds = table.getElementsByTagName("td");  // 因为 getElementsByTagName() 方法没有foreach 故使用 queryselectorall()
        let tds = table.querySelectorAll("td");
        // console.log(tds);
        let week = new Array();
        let divs = new Array();
        tds.forEach(element => {
            if(element.hasAttribute("id")){
                if(element.hasChildNodes()){
                    let div = Array.from(element.getElementsByTagName("div"));
                    // let div = element.querySelectorAll("div");
                    divs = divs.concat(div);
                    let wk = Week[element.getAttribute("id")[0]];
                    for(let i = 0; i < div.length; ++i){
                        week.push(wk);
                    }
                }
            }
        });
        return {week: week, divs: divs};        
    }
    // --------------------------------------------------------------------------




    // --------------------------------------------------------------------------
    // 全局变量 课程信息类
    class Course{
        constructor(course){
            if(course){
                this.name = course.name;                // 课程名称
                this.week = course.week;                // 该课程具体是星期几
                this.info = course.info;                // 该课程的一个这个时段的信息
                this.startTime = course.startTime;      // 该课程的开始上课时间
                this.endTime = course.endTime;
                this.startWeek = course.startWeek;      // 该课程的持续上课的起始周,为一个数组
                this.endWeek = course.endWeek;
                this.isSingleOrDouble = course.isSingleOrDouble;
                                                        // 改课程是否是间隔上课,间隔为2
                this.location = course.location;        // 该课程的上课地点
                this.teacher = course.teacher;          // 该课程的任课老师
            }
        }
    }
    // --------------------------------------------------------------------------


    // 获取所有的课程信息,存放到一个 courses 数组中
    // --------------------------------------------------------------------------
    function parseCourses(data){
        var courses = new Array();
        for(let i = 0; i < data.divs.length; ++i){
            let course = new Course();
            course.week = data.week[i];
            course.name = data.divs[i].getElementsByTagName("span")[0].getElementsByTagName("font")[0].innerText;
            course.name = course.name.substr(0, course.name.length - 1);
            data.divs[i].querySelectorAll("p").forEach(p => {
                if(p.getElementsByTagName("span")[0].getAttribute("title") == "节/周"){
                    // 进行起始周数以及持续时间的解析
                    (function(str = p.getElementsByTagName("font")[1].innerText){
                        course.info = str;
                        // console.log(str);
                        let time = str.substring(str.indexOf("(") + 1, str.indexOf(")") + 1 - 1);
                        let wk = str.substring(str.indexOf(")") + 1, str.length).split(",");
                        // console.log(time);
                        // console.log(wk);

                        course.startTime = parseInt(time.substring(0, time.indexOf("-")));
                        course.endTime = parseInt(time.substring(time.indexOf("-") + 1, time.indexOf("节")));

                        
                        course.isSingleOrDouble = new Array();
                        course.startWeek = new Array();
                        course.endWeek = new Array();
                        wk.forEach(w => {
                            if(w.indexOf("单") != -1 || w.indexOf("双") != -1){
                                course.isSingleOrDouble.push(2);
                            }
                            else{
                                course.isSingleOrDouble.push(1);
                            }

                            let startWeek, endWeek;
                            if(w.indexOf("-") == -1){
                                startWeek = endWeek = parseInt(w.substring(0, w.indexOf("周")));
                            }
                            else{
                                startWeek = parseInt(w.substring(0, w.indexOf("-")));
                                endWeek = parseInt(w.substring(w.indexOf("-") + 1, w.indexOf("周")));
                            }
                            course.startWeek.push(startWeek);
                            course.endWeek.push(endWeek);
                        });
                    })();
                }
                else if(p.getElementsByTagName("span")[0].getAttribute("title") == "上课地点"){
                    course.location = p.getElementsByTagName("font")[1].innerText;
                }
                else if(p.getElementsByTagName("span")[0].getAttribute("title") == "教师"){
                    course.teacher = p.getElementsByTagName("font")[1].innerText;
                }
            });
            // console.log(course);
            courses.push(course);
        }
        return courses;
    }
    // --------------------------------------------------------------------------


    // --------------------------------------------------------------------------
    // 日历的一些主要参数,如PRODID、VERSION、CALSCALE、是否提醒以及提醒的时间
    var Calendar;
    (function(Calendar){
        Calendar.PRODID = "-//31415926535x//ICalendar Exporter v1.0//CN";
        Calendar.VERSION = "2.0";
        Calendar.CALSCALE = "GREGORIAN";        // 历法,默认是公历
        Calendar.TIMEZONE = "Asia/Shanghai"     // 时区,默认是上海
        Calendar.ISVALARM = true;               // 提醒,默认是开启
        Calendar.VALARM = "-P0DT0H30M0S";       // 提醒,默认半小时
        Calendar.WKST = "SU";                   // 一周开始,默认是周日

        Calendar.StartDate;                     // 这学期开始日期
    })(Calendar || (Calendar = {}));

    // --------------------------------------------------------------------------


    // --------------------------------------------------------------------------
    // 通过节次确定时间, 默认每天上午8点上课,每节课两小时(无休息时间),下午2点上课
    function getTime(num, StartOrEnd){
        if(num <= 4){
            num = 7 + num + StartOrEnd;
        }
        else{
            num = 9 + num + StartOrEnd;
        }

        return "" + getFixedLen("" + num, 2) + "0000";
    }
    function getFixedLen(s, len){
        if(s.length < len){
            return getFixedLen("0" + s, len);
        }
        else if(s.length > len){
            return s.slice(0, len);
        }
        else{
            return s;
        }
    }
    // 通过周数获得具体的日期(相对学期开始的那一周)
    function getDate(num, wk){
        let date = new Date(Calendar.StartDate.toString());
        date.setDate(date.getDate() + (num - 1) * 7 + Week[wk] - 1);
        let res = "";
        res += getFixedLen(date.getUTCFullYear().toString(), 4);
        res += getFixedLen((date.getUTCMonth() + 1).toString(), 2);
        res += getFixedLen(date.getUTCDate().toString(), 2);
        res += "T";
        return res;
    }
    // --------------------------------------------------------------------------

    // --------------------------------------------------------------------------
    // 日历的生成,由处理过的课程信心来得到一个没有处理行长的ics
    var CRLF = "\n";
    var SPACE = " ";
    function generateCalendar(courses){
        let res = new Array();
        // 首先添加日历的头 VCALDENDAR
        res.push("BEGIN:VCALENDAR");
        res.push("VERSION:" + Calendar.VERSION);
        res.push("PRODID:" + Calendar.PRODID);
        res.push("CALSCALE:" + Calendar.CALSCALE);

        // 将每一个课程信息转化为事件 VEVENT 并添加一个提醒
        console.log(courses);
        courses.forEach(course => {
            for(let i = 0; i < course.isSingleOrDouble.length; ++i){
                res.push("BEGIN:VEVENT");
                res.push("DTSTART:" + getDate(course.startWeek[i], course.week) + getTime(course.startTime, 0));
                res.push("DTEND:" + getDate(course.startWeek[i], course.week) + getTime(course.endTime, 1));
                // res.push("DTSTART;TZID=" + Calendar.TIMEZONE + ":" + getDate(course.startWeek[i], course.week) + getTime(course.startTime, 0));
                // res.push("DTEND;TZID="   + Calendar.TIMEZONE + ":" + getDate(course.startWeek[i], course.week) + getTime(course.endTime, 1));
                res.push("RRULE:FREQ=WEEKLY;WKST=" + Calendar.WKST + ";COUNT=" + (course.endWeek[i] - course.startWeek[i] + course.isSingleOrDouble[i]) / course.isSingleOrDouble[i] + ";INTERVAL=" + course.isSingleOrDouble[i] + ";BYDAY=" + course.week.substr(0, 2).toUpperCase());
                res.push("SUMMARY:" + course.name + " " + course.location + " " + course.teacher + " " + course.info);
                
                if(Calendar.ISVALARM){
                    res.push("BEGIN:VALARM");
                    res.push("ACTION:DISPLAY");
                    res.push("DESCRIPTION:This is an event reminder");
                    res.push("TRIGGER:" + Calendar.VALARM);
                    res.push("END:VALARM");
                }

                res.push("END:VEVENT");
                res.push(CRLF)
            }
        });

        // 建立一个周数事件,持续20周
        (function(){
            for(let i = 1; i < 20; ++i){
                res.push("BEGIN:VEVENT");
                res.push("DTSTART:" + getDate(i, Week[1]) + getTime(-1, 0));
                res.push("DTEND:" + getDate(i, Week[1]) + getTime(-1, 1));
                res.push("SUMMARY:" + "第" + i + "周");
                res.push("END:VEVENT");
                res.push(CRLF)
            }
        })();
        
        res.push("END:VCALENDAR");
        // console.log(res);
        return res;
    }
    // --------------------------------------------------------------------------



    // --------------------------------------------------------------------------
    // 对ics进行格式的处理,每行不超过75个字节,换行用CRLF,对于超出的进行换行,下一行行首用空格
    function getFixedIcs(data){
        let res = "";
        data.forEach(line => {
            if(line.length > 60){
                let len = line.length;
                let index = 0;
                while(len > 0){
                    for(let i = 0; i < index; ++i){
                        res += SPACE;
                    }
                    res += line.slice(0, 60) + CRLF;
                    line = line.slice(61);
                    len -= 60;
                    ++index;
                }
                line = line.slice(0, 60);
            }
            res += line + CRLF;
        });
        return res;
    }
    // --------------------------------------------------------------------------


    // --------------------------------------------------------------------------
    // 导出ics
    function exportIcs(ics, a){
        let link = window.URL.createObjectURL(new Blob([ics], {
            type: "text/x-vCalendar"
        }));
        a.style.visibility = "visible";
        a.setAttribute("href", link);
        a.setAttribute("download", "courses.ics");
    }
    // --------------------------------------------------------------------------
}

function StudentEvalution(){
    // SetBtnZero();
    console.log("done......");
    // let trs = document.getElementById("tempGrid").getElementsByTagName("tr");
    // console.log(trs);
    // for(let i = 0; i < trs.length; ++i){
    //     console.log("2333");
    //     trs[i].onclick = function(){
    //         console.log("???????????");
    //         // ModifyHTML();
    //         setTimeout(ModifyHTML(), 4000);
    //         console.log("!!!!!!!!");
    //     }
    // }
    ModifyHTML();

    function ModifyHTML(){
        console.log("modify...")
        // 添加一个选择要批量打分的选择框
        let panel_body1 = document.getElementsByClassName("panel panel-default")[1];
        let panel_body2 = document.getElementsByClassName("panel-body")[3];
        let blockquote = panel_body2.getElementsByTagName("blockquote")[0].cloneNode(true);
        blockquote.getElementsByTagName("p")[0].innerText = "一键评价";
        let table = panel_body2.getElementsByTagName("table")[0].cloneNode(true);
        table.removeAttribute("data-pjzbxm_id"); table.removeAttribute("data-qzz");
        let tbody = table.getElementsByTagName("tbody")[0];
        let tr = tbody.getElementsByTagName("tr")[0];
        while(tbody.getElementsByTagName("tr").length > 1){
            tbody.removeChild(tbody.getElementsByTagName("tr")[1]);
        }
        tr.removeAttribute("data-zsmbmcb_id"); tr.removeAttribute("data-pjzbxm_id"); tr.removeAttribute("data-pfdjdmb_id");
        tr.getElementsByTagName("td")[0].innerText = "选择的最高分:";
        let inputs = tr.getElementsByClassName("radio-pjf");
        for(let i = 0; i < 5; ++i){
            // tds[i].getElementsByTagName("div")[0].getElementsByTagName("div")[0].getElementsByTagName("label")[0].getElementsByTagName("")
            inputs[i].removeAttribute("name");
            inputs[i].removeAttribute("data-pfdjdmxmb_id");
            inputs[i].setAttribute("name", "StudentEvalution");
        }
        inputs[0].setAttribute("checked", "checked");
        
        
        // let btn = document.getElementsByClassName("btn-group")[1];
        let btn = document.createElement("button");
        btn.className = "btn btn-default";
        let sp = document.createElement("span");
        sp.innerText = "一键评价";
        sp.className = "bigger-120 glyphicon glyphicon-ok";
        btn.append(sp);
        btn.setAttribute("id", "btn_StudentEvalution");
        btn.onclick = function(){
            let score = 5;
            let checked = document.getElementsByName("StudentEvalution");
            for(let i = 0; i < checked.length; ++i){
                if(checked[i].checked){
                    score = checked[i].getAttribute("data-dyf")
                }
            }
            console.log("设置的最高分数为: " + score);
            score = 5 - score;
            let inputs = document.getElementsByClassName("panel-body")[3].getElementsByTagName("input");
            let flag = Math.round(Math.random() * (inputs.length / 5));
            console.log(flag);
            for(let i = score; i < inputs.length; i += 5){
                if(Math.round(i / 5) == flag){
                    inputs[i + 1].setAttribute("checked", "checked");
                }
                else{
                    inputs[i].setAttribute("checked", "checked");
                }
            }
        }
        let td = document.createElement("td");
        td.appendChild(btn);
        
        tr.appendChild(td);
        panel_body1.prepend(table);
        panel_body1.prepend(blockquote);
        

        }
              
}

function SetBtnZero(){
    // 没用,,,
    let btn = document.getElementById("btn_yd");
    btn.className = "btn btn-default btn-primary";
    btn.removeAttribute("disabled");    
}