|
@@ -0,0 +1,189 @@
|
|
|
|
|
+// 武汉科技大学教务系统课程表导入脚本
|
|
|
|
|
+
|
|
|
|
|
+(function () {
|
|
|
|
|
+ "use strict";
|
|
|
|
|
+
|
|
|
|
|
+ var SCHEDULE_PATH = "/jsxsd/xskb/xskb_list.do";
|
|
|
|
|
+ function toast(message) {
|
|
|
|
|
+ if (window.shiguangBridge && window.shiguangBridge.showToast) {
|
|
|
|
|
+ window.shiguangBridge.showToast(message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function normalizeText(value) {
|
|
|
|
|
+ return (value || "").replace(/\u00a0/g, " ").replace(/\s+/g, " ").trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseWeeks(value) {
|
|
|
|
|
+ var text = normalizeText(value).replace(/\(周\)|周/g, "");
|
|
|
|
|
+ var weeks = new Set();
|
|
|
|
|
+ text.split(",").forEach(function (part) {
|
|
|
|
|
+ var range = part.trim().split("-").map(function (item) {
|
|
|
|
|
+ return parseInt(item, 10);
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!Number.isFinite(range[0])) return;
|
|
|
|
|
+ var end = Number.isFinite(range[1]) ? range[1] : range[0];
|
|
|
|
|
+ for (var week = range[0]; week <= end; week++) weeks.add(week);
|
|
|
|
|
+ });
|
|
|
|
|
+ return Array.from(weeks).sort(function (a, b) { return a - b; });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parsePeriods(value) {
|
|
|
|
|
+ var match = normalizeText(value).match(/\[([^\]]+)\]/);
|
|
|
|
|
+ if (!match) return [];
|
|
|
|
|
+ var numbers = match[1].match(/\d+/g);
|
|
|
|
|
+ if (!numbers || numbers.length === 0) return [];
|
|
|
|
|
+ var start = parseInt(numbers[0], 10);
|
|
|
|
|
+ var end = parseInt(numbers[numbers.length - 1], 10);
|
|
|
|
|
+ var periods = [];
|
|
|
|
|
+ for (var period = start; period <= end; period++) periods.push(period);
|
|
|
|
|
+ return periods;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function findScheduleDocument() {
|
|
|
|
|
+ if (document.querySelector("#kbtable")) return document;
|
|
|
|
|
+ return Array.from(document.querySelectorAll("iframe")).reduce(function (found, frame) {
|
|
|
|
|
+ if (found) return found;
|
|
|
|
|
+ try {
|
|
|
|
|
+ var frameDoc = frame.contentDocument || frame.contentWindow.document;
|
|
|
|
|
+ return frameDoc && frameDoc.querySelector("#kbtable") ? frameDoc : null;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseCourseBlock(block) {
|
|
|
|
|
+ var holder = document.createElement("div");
|
|
|
|
|
+ holder.innerHTML = block;
|
|
|
|
|
+ var lines = Array.from(holder.querySelectorAll("font[title]")).reduce(function (result, font) {
|
|
|
|
|
+ result[font.getAttribute("title")] = normalizeText(font.textContent);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }, {});
|
|
|
|
|
+ var firstLine = holder.innerHTML.split(/<br\s*\/?\s*>/i)[0];
|
|
|
|
|
+ var nameHolder = document.createElement("div");
|
|
|
|
|
+ nameHolder.innerHTML = firstLine;
|
|
|
|
|
+ var name = normalizeText(nameHolder.textContent);
|
|
|
|
|
+ var weekPeriod = lines["周次(节次)"] || "";
|
|
|
|
|
+ var periods = parsePeriods(weekPeriod);
|
|
|
|
|
+ var weeks = parseWeeks(weekPeriod.split("[")[0]);
|
|
|
|
|
+ if (!name || weeks.length === 0 || periods.length === 0) return null;
|
|
|
|
|
+ return {
|
|
|
|
|
+ name: name,
|
|
|
|
|
+ teacher: lines["老师"] || "",
|
|
|
|
|
+ position: lines["教室"] || "未指定",
|
|
|
|
|
+ weeks: weeks,
|
|
|
|
|
+ startSection: periods[0],
|
|
|
|
|
+ endSection: periods[periods.length - 1]
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseCourses(doc) {
|
|
|
|
|
+ var table = doc.querySelector("#kbtable");
|
|
|
|
|
+ if (!table) return [];
|
|
|
|
|
+ var courses = new Map();
|
|
|
|
|
+
|
|
|
|
|
+ Array.from(table.querySelectorAll("div.kbcontent")).forEach(function (div) {
|
|
|
|
|
+ var idParts = (div.id || "").split("-");
|
|
|
|
|
+ var weekday = parseInt(idParts[idParts.length - 2], 10);
|
|
|
|
|
+ if (!Number.isFinite(weekday) || weekday < 1 || weekday > 7) return;
|
|
|
|
|
+
|
|
|
|
|
+ var blocks = div.innerHTML.split(/<br\s*\/?\s*>\s*[-—]{10,}\s*<br\s*\/?\s*>/i);
|
|
|
|
|
+ blocks.forEach(function (block) {
|
|
|
|
|
+ var course = parseCourseBlock(block);
|
|
|
|
|
+ if (!course) return;
|
|
|
|
|
+ course.day = weekday;
|
|
|
|
|
+ var key = [course.name, course.teacher, course.position, weekday, course.weeks.join(",")].join("|");
|
|
|
|
|
+ if (!courses.has(key)) {
|
|
|
|
|
+ courses.set(key, course);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var existing = courses.get(key);
|
|
|
|
|
+ existing.startSection = Math.min(existing.startSection, course.startSection);
|
|
|
|
|
+ existing.endSection = Math.max(existing.endSection, course.endSection);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return Array.from(courses.values()).sort(function (a, b) {
|
|
|
|
|
+ return a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name);
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseTimeSlots(doc) {
|
|
|
|
|
+ var slots = new Map();
|
|
|
|
|
+ Array.from(doc.querySelectorAll("#kbtable tr")).forEach(function (row) {
|
|
|
|
|
+ var header = row.querySelector("th");
|
|
|
|
|
+ if (!header) return;
|
|
|
|
|
+ var match = normalizeText(header.textContent).match(/^(\d+).*?(\d{1,2}:\d{2})\s*[-~至]\s*(\d{1,2}:\d{2})/);
|
|
|
|
|
+ if (!match) return;
|
|
|
|
|
+ var number = parseInt(match[1], 10);
|
|
|
|
|
+ if (!slots.has(number)) {
|
|
|
|
|
+ slots.set(number, { number: number, startTime: match[2].padStart(5, "0"), endTime: match[3].padStart(5, "0") });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return Array.from(slots.values()).sort(function (a, b) { return a.number - b.number; });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getSemesterId(doc) {
|
|
|
|
|
+ var select = doc.querySelector("#xnxq01id");
|
|
|
|
|
+ return select ? select.value : "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchScheduleDocument(semesterId) {
|
|
|
|
|
+ var body = "cj0701id=&zc=&demo=&xnxq01id=" + encodeURIComponent(semesterId) + "&sfFD=1&wkbkc=1";
|
|
|
|
|
+ var response = await fetch(SCHEDULE_PATH, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
|
|
|
+ credentials: "include",
|
|
|
|
|
+ body: body
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) throw new Error("课表请求失败:HTTP " + response.status);
|
|
|
|
|
+ return new DOMParser().parseFromString(await response.text(), "text/html");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function saveData(courses, timeSlots) {
|
|
|
|
|
+ var maxWeek = Math.max.apply(null, courses.map(function (course) {
|
|
|
|
|
+ return Math.max.apply(null, course.weeks);
|
|
|
|
|
+ }));
|
|
|
|
|
+ var config = {
|
|
|
|
|
+ semesterTotalWeeks: Number.isFinite(maxWeek) && maxWeek > 0 ? maxWeek : 20,
|
|
|
|
|
+ firstDayOfWeek: 1,
|
|
|
|
|
+ defaultClassDuration: 45,
|
|
|
|
|
+ defaultBreakDuration: 5
|
|
|
|
|
+ };
|
|
|
|
|
+ if (window.shiguangBridgePromise.saveCourseConfig) {
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (timeSlots.length > 0 && window.shiguangBridgePromise.savePresetTimeSlots) {
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ }
|
|
|
|
|
+ return await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function runImport() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ var currentDoc = findScheduleDocument() || document;
|
|
|
|
|
+ var semesterId = getSemesterId(currentDoc);
|
|
|
|
|
+ var scheduleDoc = currentDoc.querySelector("#kbtable") ? currentDoc : await fetchScheduleDocument(semesterId);
|
|
|
|
|
+ var courses = parseCourses(scheduleDoc);
|
|
|
|
|
+ if (courses.length === 0) {
|
|
|
|
|
+ toast("未解析到课程,请确认已登录且课表页面已加载完成");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ var saved = await saveData(courses, parseTimeSlots(scheduleDoc));
|
|
|
|
|
+ if (!saved) {
|
|
|
|
|
+ toast("课程导入失败");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ toast("成功导入 " + courses.length + " 门课程");
|
|
|
|
|
+ if (window.shiguangBridge.notifyTaskCompletion) window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("WUST import failed:", error);
|
|
|
|
|
+ toast("导入失败:" + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (window.shiguangBridgePromise && window.shiguangBridgePromise.saveImportedCourses) {
|
|
|
|
|
+ runImport();
|
|
|
|
|
+ }
|
|
|
|
|
+})();
|