南信院-新版正方教务系统导出课程表

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

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

// 学期周数 默认为20
var weeks = 20

// 课堂时间表
var classtime = {
    '1': { 'start': '0800', 'end': '0845' },
    '2': { 'start': '0855', 'end': '0940' },
    '3': { 'start': '1010', 'end': '1055' },
    '4': { 'start': '1105', 'end': '1150' },
    '5': { 'start': '1330', 'end': '1415' },
    '6': { 'start': '1425', 'end': '1510' },
    '7': { 'start': '1530', 'end': '1615' },
    '8': { 'start': '1625', 'end': '1710' },
    '9': { 'start': '1800', 'end': '1845' },
    '10': { 'start': '1850', 'end': '1935' },
    '11': { 'start': '1945', 'end': '2030' }
};
// 根据自己学校教务系统的网址修改,应该对于新版教务系统的地址都是一样的,故只需修改上面 include中的教务系统的地址即可
var ClassScheduleToICSURL = "kbcx/xskbcx_cxXskbcxIndex.html"; // 学生课表查询页面,将该学期的课程信息导出为ics
var ExamScheduleToICSURL = "kwgl/kscx_cxXsksxxIndex.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(ExamScheduleToICSURL) != -1) {
        ExamScheduleToICS();
    } 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);
        div.appendChild(dwnbtn);


        btn.onclick = function() {
            startDate = StartDate.value;
            generateCalendar(parseCourses(parseTable())); // 嘿嘿。。
            alert("ics文件已经生成,请导入到您所使用的日历文件;(Google Calendar需要自行设置课程的颜色。。。)");
        }
    }
    // --------------------------------------------------------------------------

    // 本学期设定的开始日期
    var startDate;

    // 全局变量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 = $.trim(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 = $.trim(p.getElementsByTagName("font")[1].innerText);
                } else if (p.getElementsByTagName("span")[0].getAttribute("title") == "教师") {
                    course.teacher = $.trim(p.getElementsByTagName("font")[1].innerText);
                }
            });
            // console.log(course);
            courses.push(course);
        }
        return courses;
    }
    // --------------------------------------------------------------------------




    // --------------------------------------------------------------------------
    // 通过节次确定时间, 默认每天上午8点上课,每节课两小时(无休息时间),下午2点上课
    function getTime(num, StartOrEnd) {
        if (StartOrEnd == 0) {
            time = classtime[num]["start"]
        } else {
            time = classtime[num]["end"]
        }
        return time + '00';
    }

    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(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
    function generateCalendar(courses) {
        let res = new ICS();

        // 将每一个课程信息转化为事件 VEVENT 并添加一个提醒
        console.log(courses);
        courses.forEach(course => {
            for (let i = 0; i < course.isSingleOrDouble.length; ++i) {
                let e = new ICSEvent("" + getDate(course.startWeek[i], course.week) + getTime(course.startTime, 0),
                    "" + getDate(course.startWeek[i], course.week) + getTime(course.endTime, 1),
                    // "" + course.name + " " + course.location + " " + course.teacher + " " + course.info);
                    "" + course.name + " " + course.location.substring(5) + " " + course.info.substring(1, 5),
                    "" + course.location,
                    "" + course.info.substring(1, 5) + " " + course.teacher);
                e.setRRULE("WEEKLY", res.Calendar.WKST,
                    "" + (course.endWeek[i] - course.startWeek[i] + course.isSingleOrDouble[i]) / course.isSingleOrDouble[i],
                    "" + course.isSingleOrDouble[i],
                    "" + course.week.substr(0, 2).toUpperCase());
                res.pushEvent(e);
            }
        });

        // 建立一个周数事件,持续20周
        (function() {
            for (let i = 1; i <= weeks; ++i) {
                let e = new ICSEvent("" + getDate(i, Week[1]) + "060000",
                    "" + getDate(i, Week[1]) + "070000",
                    "" + "第" + i + "周");
                res.pushEvent(e);
            }
        })();

        res.pushCalendarEnd();
        res.exportIcs();
    }
    // --------------------------------------------------------------------------
}

// 导出考试信息
function ExamScheduleToICS() {
    console.log("ExamScheduleToICS");

    pageFullyLoaded();
    //加载完成后运行
    function pageFullyLoaded() {
        let div = document.getElementsByClassName("col-sm-12")[1];
        let btn = document.createElement("button");
        btn.className = "btn btn-primary btn-sm";
        btn.id = "exportbtn";
        btn.innerText = "导出ICS文件";

        div.appendChild(btn);


        btn.onclick = function() {
            generateCalendar();
            alert("ics文件已经生成,请导入到您所使用的日历文件;(Google Calendar需要自行设置课程的颜色。。。)");
        }

        document.getElementById("search_go").click();
    }

    function generateCalendar() {
        let table = document.getElementById("tabGrid");
        let trs = table.getElementsByTagName("tr");

        class EXAM {
            constructor(e) {
                if (e) {
                    this.course = e.course; // 课程名
                    this.teacher = e.teacher; // 教师
                    this.examNmae = e.examNmae; // 考试名称:期中还是期末
                    this.timeS = e.timeS; // 考试时间
                    this.timeT = e.timeE; // 考试时间
                    this.location = e.location; // 考试地点组成: 考试地点、考试校区、座位号
                }
            }
        }
        let exams = new Array();
        for (let i = 1; i < trs.length; ++i) {
            let tds = trs[i].getElementsByTagName("td");
            let exam = new EXAM();
            exam.loction = "";
            trs[i].querySelectorAll("td").forEach(tr => {
                let attr = tr.getAttribute("aria-describedby");
                if (attr == "tabGrid_kcmc") {
                    // 课程名称
                    exam.course = tr.innerText;
                } else if (attr == "tabGrid_jsxx") {
                    // 教师
                    exam.teacher = tr.innerText.substring(tr.innerText.indexOf("/") + 1, tr.innerText.length);
                } else if (attr == "tabGrid_ksmc") {
                    // 考试类型
                    exam.examNmae = tr.innerText;
                } else if (attr == "tabGrid_kssj") {
                    // 考试时间
                    let time = tr.innerText;
                    let date = "" + time[0] + time[1] + time[2] + time[3] + time[5] + time[6] + time[8] + time[9] + "T";
                    exam.timeS = date + time[11] + time[12] + time[14] + time[15] + "00";
                    exam.timeE = date + time[17] + time[18] + time[20] + time[21] + "00";
                } else if (attr == "tabGrid_cdmc") {
                    // 考试地点
                    exam.location = tr.innerText;
                } else if (attr == "tabGrid_cdxqmc") {
                    // 校区
                    exam.location += " " + tr.innerText;
                } else if (attr == "tabGrid_zwh") {
                    // 座位号
                    exam.location += "  " + tr.innerText;
                }
                // 可以根据自己学校和自己的喜好添加你想要的信息
            });
            exams.push(exam);
        }
        console.log(exams);
        let ics = new ICS();
        exams.forEach(ex => {
            let e = new ICSEvent("" + ex.timeS, "" + ex.timeE, "" + ex.course + " " + ex.examNmae + " " + ex.teacher + " " + ex.location);
            ics.pushEvent(e);
        });
        ics.pushCalendarEnd();
        ics.exportIcs();
    }
}


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");
}

// -------------------------------- ICS类,用于处理所有有关日历的操作 ------------------------------------------//
var CRLF = "\n";
var SPACE = " ";
class ICS {

    Calendar; // 日历参数
    ics; // ics格式的日历,
    res; // 最后格式化的结果

    constructor() {

        // --------------------------------------------------------------------------
        // 日历的一些主要参数,如PRODID、VERSION、CALSCALE、是否提醒以及提醒的时间
        (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 = "-PT5M"; // 提醒,默认半小时
            Calendar.WKST = "SU"; // 一周开始,默认是周日

        })(this.Calendar || (this.Calendar = {}));
        // --------------------------------------------------------------------------


        this.ics = new Array();
        this.ics.push("BEGIN:VCALENDAR");
        this.ics.push("VERSION:" + this.Calendar.VERSION);
        this.ics.push("PRODID:" + this.Calendar.PRODID);
        this.ics.push("CALSCALE:" + this.Calendar.CALSCALE);
    }

    // 添加事件
    pushEvent(e) {
        this.ics.push("BEGIN:VEVENT");
        this.ics.push(e.getDTSTART());
        this.ics.push(e.getDTEND());
        if (e.isrrule == true) this.ics.push(e.getRRULE());
        this.ics.push(e.getSUMMARY());
        if (this.Calendar.ISVALARM == true) this.pushAlarm();
        this.ics.push(e.getLOCATION());
        this.ics.push(e.getDESCRIPTION());
        this.ics.push("END:VEVENT");
        this.ics.push(CRLF);
    }

    // 添加提醒
    pushAlarm() {
        this.ics.push("BEGIN:VALARM");
        this.ics.push("ACTION:DISPLAY");
        this.ics.push("DESCRIPTION:This is an event reminder");
        this.ics.push("TRIGGER:" + this.Calendar.VALARM);
        this.ics.push("END:VALARM");
    }

    // 添加日历末
    pushCalendarEnd() {
        this.ics.push("END:VCALENDAR");
    }

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


    // --------------------------------------------------------------------------
    // 导出ics
    exportIcs() {
            this.getFixedIcs();
            // 使用a标签模拟下载,blob实现流文件的下载链接转化
            let link = window.URL.createObjectURL(new Blob([this.res], {
                type: "text/x-vCalendar"
            }));
            let a = document.createElement("a");
            a.setAttribute("href", link);
            a.setAttribute("download", "courses.ics");
            a.click(); // 模拟下载
        }
        // --------------------------------------------------------------------------
        // -------------------------------- ICS ------------------------------------------//


}
// -------------------------------- ICS类,用于处理所有有关日历的操作 ------------------------------------------//
class ICSEvent {
    constructor(DTSTART, DTEND, SUMMARY, LOCATION, DESCRIPTION) {
        this.DTSTART = DTSTART;
        this.DTEND = DTEND;
        this.SUMMARY = SUMMARY;
        this.LOCATION = (LOCATION == undefined) ? "" : LOCATION;
        this.DESCRIPTION = (DESCRIPTION == undefined) ? "" : DESCRIPTION;
    }
    isrrule = false;
    RRULE;
    setRRULE(FREQ, WKST, COUNT, INTERVAL, BYDAY) {
        this.isrrule = true;
        this.RRULE = "RRULE:FREQ=" + FREQ + ";WKST=" + WKST + ";COUNT=" + COUNT + ";INTERVAL=" + INTERVAL + ";BYDAY=" + BYDAY;
    }
    getRRULE() {
        return "" + this.RRULE;
    }
    getDTSTART() {
        return "DTSTART:" + this.DTSTART;
    }
    getDTEND() {
        return "DTEND:" + this.DTEND;
    }
    getSUMMARY() {
        return "SUMMARY:" + this.SUMMARY;
    }
    getLOCATION() {
        return "LOCATION:" + this.LOCATION;
    }
    getDESCRIPTION() {
        return "DESCRIPTION:" + this.DESCRIPTION;
    }
}

// -------------------------------- ICS类,用于处理所有有关日历的操作 ------------------------------------------//