您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
解析惠州学院教务系统(正方教务系统)的课程表页面,导出为标准 .ics 日历文件,供第三方日历使用。核心代码改自 31415926535x 。
// ==UserScript== // @name 惠州学院 HZU | 教务系统课程表导出助手 // @namespace http://tampermonkey.net/ // @version 0.7.1 // @description 解析惠州学院教务系统(正方教务系统)的课程表页面,导出为标准 .ics 日历文件,供第三方日历使用。核心代码改自 31415926535x 。 // @author Ckrvxr, 31415926535x // @homepage https://greasyfork.org/en/scripts/537439 // @homepage https://github.com/Ckrvxr // @compatible chrome // @license Apache-2.0 license // @include *://jwxt.hzu.edu.cn/* // @run-at document-start // ==/UserScript== (function () { 'use strict'; const ClassScheduleURL = "kbcx/xskbcx_cxXskbcxIndex.html"; unsafeWindow.addEventListener("load", main); function main() { const windowURL = window.location.href; if (windowURL.indexOf(ClassScheduleURL) !== -1) { ClassScheduleToICS(); } } function ClassScheduleToICS() { function pageFullyLoaded() { let div = document.getElementsByClassName("btn-toolbar pull-right")[0]; if (!div) return; let btn = document.createElement("button"); btn.className = "btn btn-default"; btn.id = "exportbtn"; let sp = document.createElement("span"); sp.innerText = "将导出课表为.ics"; sp.className = "bigger-120 glyphicon glyphicon-file"; btn.appendChild(sp); div.appendChild(btn); btn.onclick = function () { let input = document.createElement("input"); input.type = "date"; input.value = "2025-02-17"; let dialog = document.createElement("div"); dialog.style.position = "fixed"; dialog.style.top = "40%"; dialog.style.left = "50%"; dialog.style.transform = "translate(-50%, -50%)"; dialog.style.padding = "20px"; dialog.style.backgroundColor = "#fff"; dialog.style.border = "1px solid #ccc"; dialog.style.zIndex = "9999"; dialog.style.boxShadow = "0 0 10px rgba(0,0,0,0.3)"; dialog.innerHTML = "<p>请选择要导出的学期第一个星期一:</p>"; dialog.appendChild(input); let confirmBtn = document.createElement("button"); confirmBtn.textContent = "确定"; confirmBtn.style.marginLeft = "10px"; confirmBtn.className = "btn btn-primary btn-sm"; confirmBtn.onclick = function () { startDate = input.value; document.body.removeChild(dialog); generateCalendar(parseCourses(parseTable(startDate))); alert("请自行一一核对,以免漏课旷课!\n出现以上问题,概不负责!"); }; let cancelBtn = document.createElement("button"); cancelBtn.textContent = "取消"; cancelBtn.className = "btn btn-secondary btn-sm"; cancelBtn.onclick = function () { document.body.removeChild(dialog); }; dialog.appendChild(confirmBtn); dialog.appendChild(cancelBtn); document.body.appendChild(dialog); }; } var startDate; 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 = {})); function parseTable() { let table = document.getElementById("kbgrid_table_0"); let tds = table.querySelectorAll("td"); let week = []; let divs = []; tds.forEach(element => { if (element.hasAttribute("id")) { if (element.hasChildNodes()) { let div = Array.from(element.getElementsByTagName("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; this.location = course.location; this.teacher = course.teacher; this.exam = course.exam; } } } function parseCourses(data) { var courses = []; 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; data.divs[i].querySelectorAll("p").forEach(p => { if (p.getElementsByTagName("span")[0]?.getAttribute("title") === "节/周") { (function (str = p.getElementsByTagName("font")[1]?.innerText || "") { course.info = str; let time = str.substring(str.indexOf("(") + 1, str.indexOf(")") + 1 - 1); let wk = str.substring(str.indexOf(")") + 1, str.length).split(","); course.startTime = parseInt(time.substring(0, time.indexOf("-"))); course.endTime = parseInt(time.substring(time.indexOf("-") + 1, time.indexOf("节"))); course.isSingleOrDouble = []; course.startWeek = []; course.endWeek = []; 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 || "").replace(/\s*本校区\s*/g, ''); } else if (p.getElementsByTagName("span")[0]?.getAttribute("title") === "教师 ") { course.teacher = p.getElementsByTagName("font")[1]?.innerText || ""; } else if (p.getElementsByTagName("span")[0]?.getAttribute("title") === "考核方式") { course.exam = p.getElementsByTagName("font")[1]?.innerText || ""; } }); courses.push(course); } return courses; } function getTime(num, StartOrEnd) { const periodMap = [ { start: "080000", end: "084500" }, { start: "085000", end: "093500" }, { start: "095500", end: "104000" }, { start: "104500", end: "113000" }, { start: "143000", end: "151500" }, { start: "152000", end: "160500" }, { start: "162000", end: "170500" }, { start: "171000", end: "175500" }, { start: "193000", end: "201500" }, { start: "202000", end: "210500" }, { start: "211000", end: "215500" } ]; if (num < 1 || num > periodMap.length) { console.error("无效的节次编号:" + num); return "000000"; } const period = periodMap[num - 1]; return StartOrEnd === 0 ? period.start : period.end; } 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; } function generateCalendar(courses) { let res = new ICS(); 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 ); e.setDescription(course.name, course.teacher, course.info, course.location, course.exam); 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 < 20; ++i) { let e = new ICSEvent("" + getDate(i, Week[1]) + "060000", "" + getDate(i, Week[1]) + "070000", "" + "第" + i + "周", "" ); res.pushEvent(e); } })(); res.pushCalendarEnd(); res.exportIcs(startDate); } const CRLF = "\n"; const SPACE = " "; class ICS { constructor() { this.Calendar = { PRODID: "-//ckrvxr//HZU Calendar Exporter v0.7.1//CN", VERSION: "2.0", CALSCALE: "GREGORIAN", TIMEZONE: "Asia/Shanghai", ISVALARM: true, VALARM: "-P0DT0H15M0S", //提醒时间 WKST: "SU" }; this.ics = [ "BEGIN:VCALENDAR", "VERSION:" + this.Calendar.VERSION, "PRODID:" + this.Calendar.PRODID, "CALSCALE:" + this.Calendar.CALSCALE ]; } pushEvent(e) { this.ics.push("BEGIN:VEVENT"); this.ics.push(e.getDTSTART()); this.ics.push(e.getDTEND()); if (e.isrrule) this.ics.push(e.getRRULE()); this.ics.push(e.getSUMMARY()); this.ics.push(e.getDESCRIPTION()); if (e.LOCATION) this.ics.push(e.getLOCATION()); if (this.Calendar.ISVALARM) this.pushAlarm(); 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"); } getFixedIcs() { const MAX_LINE_LENGTH = 75; const FOLD_SPACE = " "; this.res = ""; this.ics.forEach(line => { let pos = 0; while (pos < line.length) { let end = Math.min(pos + MAX_LINE_LENGTH, line.length); this.res += line.substring(pos, end) + CRLF; pos = end; if (pos < line.length) { this.res += FOLD_SPACE; } } }); return this.res; } exportIcs(startDate) { this.getFixedIcs(); let link = window.URL.createObjectURL(new Blob([this.res], { type: "text/x-vCalendar" })); const fileName = startDate + ".ics"; let a = document.createElement("a"); a.setAttribute("href", link); a.setAttribute("download", fileName); a.click(); } } class ICSEvent { constructor(DTSTART, DTEND, SUMMARY, LOCATION) { this.DTSTART = DTSTART; this.DTEND = DTEND; this.SUMMARY = SUMMARY; this.LOCATION = LOCATION || null; this.DESCRIPTION = ""; } isrrule = false; RRULE; LOCATION; 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; } getDESCRIPTION() { return this.DESCRIPTION ? "DESCRIPTION:" + this.DESCRIPTION : ""; } setDescription(name, teacher, info, location, exam) { this.DESCRIPTION = `课程名称:${name.trim()}\\n时间安排:${info.trim()}\\n上课地点:${location.trim()}\\n授课教师:${teacher.trim()}\\n考核方式:${exam.trim()}`; } getLOCATION() { return this.LOCATION ? "LOCATION:" + this.LOCATION : ""; } } pageFullyLoaded(); } })();