| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- // 北京邮电大学本科教务管理系统拾光课表适配脚本
- // 适配页面:https://jwgl.bupt.edu.cn/jsxsd/xskb/xskb_list.do
- // 当前版本只解析已打开的“学期理论课表”页面,不主动请求接口。
- (function () {
- function toast(message) {
- if (window.AndroidBridge && AndroidBridge.showToast) {
- AndroidBridge.showToast(message);
- } else {
- console.log(message);
- }
- }
- async function alertUser(title, message) {
- if (window.AndroidBridgePromise && window.AndroidBridgePromise.showAlert) {
- return await window.AndroidBridgePromise.showAlert(title, message, "确定");
- }
- alert(title + "\n" + message);
- return true;
- }
- function normalizeText(text) {
- return String(text || "")
- .replace(/\u00a0/g, " ")
- .replace(/ /gi, " ")
- .replace(/[0-9]/g, function (ch) {
- return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0);
- })
- .replace(/[,、]/g, ",")
- .replace(/[-–—~~至到]/g, "-")
- .replace(/[()]/g, function (ch) {
- return ch === "(" ? "(" : ")";
- })
- .replace(/\s+/g, " ")
- .trim();
- }
- function findScheduleDocument() {
- if (document.querySelector("#kbtable")) return document;
- const frames = Array.from(document.querySelectorAll("iframe"));
- for (const frame of frames) {
- try {
- const frameDoc = frame.contentDocument || frame.contentWindow.document;
- if (frameDoc && frameDoc.querySelector("#kbtable")) return frameDoc;
- } catch (e) {
- // Ignore cross-origin or inaccessible frames.
- }
- }
- return null;
- }
- function getTitleText(container, title) {
- const node = container.querySelector(
- `font[title="${title}"], span[title="${title}"], div[title="${title}"]`
- );
- return normalizeText(node ? node.textContent : "");
- }
- function extractCourseName(courseDiv) {
- const clone = courseDiv.cloneNode(true);
- Array.from(clone.querySelectorAll("font[title], span[title], div[title]")).forEach(function (node) {
- node.remove();
- });
- Array.from(clone.querySelectorAll("span")).forEach(function (node) {
- const text = normalizeText(node.textContent);
- if (/^[A-Z]$/.test(text) || /^[●★○]+$/.test(text)) node.remove();
- });
- const holder = document.createElement("div");
- holder.innerHTML = clone.innerHTML.replace(/<br\s*\/?>/gi, "\n");
- const lines = holder.textContent
- .split(/\n+/)
- .map(normalizeText)
- .filter(function (line) {
- return line && line !== "-" && !/^\(\d+\)$/.test(line);
- });
- return normalizeText((lines[0] || "").replace(/[●★○]/g, ""));
- }
- function parseDay(courseDiv, fallbackDay) {
- const id = courseDiv.getAttribute("id") || "";
- const match = id.match(/-(\d)-\d$/);
- if (match) return parseInt(match[1], 10);
- return fallbackDay || 0;
- }
- function parseWeeks(weekText) {
- const text = normalizeText(weekText)
- .replace(/\[[^\]]*\]/g, "")
- .replace(/\(周\)/g, "")
- .replace(/周/g, "")
- .replace(/\s/g, "");
- const weeks = new Set();
- text.split(/[;,;]/).forEach(function (part) {
- if (!part) return;
- const isOdd = /单/.test(part);
- const isEven = /双/.test(part);
- const ranges = part.match(/\d+(?:-\d+)?/g) || [];
- ranges.forEach(function (rangeText) {
- const range = rangeText.split("-").map(function (value) {
- return parseInt(value, 10);
- });
- const start = range[0];
- const end = range.length > 1 ? range[1] : start;
- if (!start || !end || start > end) return;
- for (let week = start; week <= end; week++) {
- if (isOdd && week % 2 === 0) continue;
- if (isEven && week % 2 !== 0) continue;
- weeks.add(week);
- }
- });
- });
- return Array.from(weeks).sort(function (a, b) { return a - b; });
- }
- function parseSections(weekText) {
- const text = normalizeText(weekText).replace(/\s/g, "");
- const match = text.match(/\[([^\]]+)\]/);
- if (!match) return [];
- const numbers = match[1].match(/\d+/g) || [];
- if (numbers.length === 0) return [];
- const start = parseInt(numbers[0], 10);
- const end = parseInt(numbers[numbers.length - 1], 10);
- if (!start || !end || start > end) return [];
- const sections = [];
- for (let section = start; section <= end; section++) {
- sections.push(section);
- }
- return sections;
- }
- function parseCourseDiv(courseDiv, fallbackDay) {
- const rawText = normalizeText(courseDiv.textContent);
- if (!rawText || rawText === " " || rawText.length < 2) return null;
- const name = extractCourseName(courseDiv);
- const teacher = getTitleText(courseDiv, "老师") || getTitleText(courseDiv, "教师");
- const weekText = getTitleText(courseDiv, "周次(节次)");
- const position = getTitleText(courseDiv, "教室") || "未知地点";
- const weeks = parseWeeks(weekText);
- const sections = parseSections(weekText);
- const day = parseDay(courseDiv, fallbackDay);
- if (!name || !day || weeks.length === 0 || sections.length === 0) return null;
- return {
- name: name,
- teacher: teacher || "未知教师",
- position: position,
- day: day,
- startSection: sections[0],
- endSection: sections[sections.length - 1],
- weeks: weeks
- };
- }
- function parseCourses(doc) {
- const table = doc.querySelector("#kbtable");
- if (!table) return [];
- const courses = [];
- Array.from(table.querySelectorAll("tr")).forEach(function (row) {
- const cells = Array.from(row.querySelectorAll("td"));
- cells.forEach(function (cell, index) {
- const fallbackDay = index + 1;
- Array.from(cell.querySelectorAll("div.kbcontent")).forEach(function (courseDiv) {
- if (courseDiv.classList.contains("sykb2")) return;
- const course = parseCourseDiv(courseDiv, fallbackDay);
- if (course) courses.push(course);
- });
- });
- });
- return mergeCourses(courses);
- }
- function mergeCourses(courses) {
- const map = new Map();
- courses.forEach(function (course) {
- const key = [
- course.name,
- course.teacher,
- course.position,
- course.day,
- course.startSection,
- course.endSection
- ].join("|");
- if (!map.has(key)) {
- map.set(key, {
- name: course.name,
- teacher: course.teacher,
- position: course.position,
- day: course.day,
- startSection: course.startSection,
- endSection: course.endSection,
- weeks: course.weeks.slice()
- });
- return;
- }
- const existing = map.get(key);
- existing.weeks = Array.from(new Set(existing.weeks.concat(course.weeks)));
- });
- return Array.from(map.values())
- .map(function (course) {
- course.weeks = course.weeks.sort(function (a, b) { return a - b; });
- return course;
- })
- .sort(function (a, b) {
- return a.day - b.day ||
- a.startSection - b.startSection ||
- a.name.localeCompare(b.name);
- });
- }
- function parseTimeSlots(doc) {
- const table = doc.querySelector("#kbtable");
- if (!table) return [];
- const map = new Map();
- Array.from(table.querySelectorAll("tr")).forEach(function (row) {
- const header = row.querySelector("th");
- if (!header) return;
- const text = normalizeText(header.textContent);
- const match = text.match(/^(\d+).*?(\d{1,2}:\d{2})-(\d{1,2}:\d{2})/);
- if (!match) return;
- const number = parseInt(match[1], 10);
- if (!number || map.has(number)) return;
- map.set(number, {
- number: number,
- startTime: match[2].padStart(5, "0"),
- endTime: match[3].padStart(5, "0")
- });
- });
- return Array.from(map.values()).sort(function (a, b) { return a.number - b.number; });
- }
- async function saveToApp(courses, timeSlots) {
- const maxWeek = Math.max.apply(null, courses.flatMap(function (course) { return course.weeks; }));
- const config = {
- semesterTotalWeeks: Number.isFinite(maxWeek) && maxWeek > 0 ? maxWeek : 20,
- firstDayOfWeek: 1,
- defaultClassDuration: 45,
- defaultBreakDuration: 5
- };
- if (window.AndroidBridgePromise && window.AndroidBridgePromise.saveCourseConfig) {
- await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
- }
- if (timeSlots.length > 0 && window.AndroidBridgePromise && window.AndroidBridgePromise.savePresetTimeSlots) {
- await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
- }
- if (window.AndroidBridgePromise && window.AndroidBridgePromise.saveImportedCourses) {
- return await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
- }
- console.log("BUPT parsed courses:", JSON.stringify(courses, null, 2));
- console.log("BUPT parsed time slots:", JSON.stringify(timeSlots, null, 2));
- return true;
- }
- async function runImportFlow() {
- try {
- const doc = findScheduleDocument();
- if (!doc) {
- await alertUser(
- "未找到课表",
- "请不要在教务系统主页直接导入。请先进入“学期理论课表”页面,并等待课表加载完成后再点击导入。"
- );
- return;
- }
- const confirmed = await alertUser(
- "北邮课表导入",
- "请确认当前不是教务系统主页,而是已经进入“学期理论课表”页面。脚本将直接解析当前页面显示的课表,请确认学期正确且页面已加载完成。"
- );
- if (!confirmed) return;
- const courses = parseCourses(doc);
- const timeSlots = parseTimeSlots(doc);
- if (courses.length === 0) {
- await alertUser(
- "未解析到课程",
- "当前页面没有解析到有效课程。请确认课表页面中存在课程块,或把一段 kbcontent HTML 发给我继续微调。"
- );
- return;
- }
- const saved = await saveToApp(courses, timeSlots);
- if (!saved) {
- toast("课程保存失败,请重试");
- return;
- }
- toast(`导入成功:${courses.length} 个课程时段${timeSlots.length ? ",已同步作息时间" : ""}`);
- if (window.AndroidBridge && AndroidBridge.notifyTaskCompletion) {
- AndroidBridge.notifyTaskCompletion();
- }
- } catch (error) {
- console.error("BUPT import failed:", error);
- await alertUser("导入失败", error && error.message ? error.message : String(error));
- }
- }
- runImportFlow();
- })();
|