| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629 |
- (async function () {
- function toast(msg) {
- try {
- if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
- window.AndroidBridge.showToast(String(msg));
- }
- } catch (_) {}
- }
- async function fail(message, error) {
- var detail = String(message || "导入失败");
- if (error) {
- detail += "\n" + (error.stack || error.message || String(error));
- }
- try {
- console.error(detail, error || "");
- } catch (_) {}
- try {
- if (window.AndroidBridgePromise && typeof window.AndroidBridgePromise.showAlert === "function") {
- await window.AndroidBridgePromise.showAlert("提示", detail, "确定");
- }
- } catch (_) {}
- throw new Error(detail);
- }
- function sleep(ms) {
- return new Promise(function (resolve) { setTimeout(resolve, ms); });
- }
- async function waitFor(cond, timeout, interval) {
- var start = Date.now();
- timeout = timeout || 15000;
- interval = interval || 300;
- while (Date.now() - start < timeout) {
- try {
- var value = await cond();
- if (value) return value;
- } catch (_) {}
- await sleep(interval);
- }
- return null;
- }
- function normalizeText(text) {
- return String(text || "")
- .replace(/\u00a0/g, " ")
- .replace(/\r/g, "\n")
- .replace(/\t/g, " ")
- .replace(/[ ]+\n/g, "\n")
- .replace(/\n[ ]+/g, "\n")
- .replace(/[ ]{2,}/g, " ")
- .replace(/\n{3,}/g, "\n\n")
- .trim();
- }
- function getAccessibleDocuments() {
- var docs = [];
- function pushDoc(doc) {
- if (doc && docs.indexOf(doc) === -1) docs.push(doc);
- }
- function scoreDoc(doc) {
- try {
- var score = 0;
- var href = String((doc.location && doc.location.href) || "");
- var title = normalizeText(doc.title || "");
- var text = normalizeText((doc.body && doc.body.innerText) || "");
- if (/\/xskb\/xskb_list\.do/i.test(href)) score += 100;
- if (/学期理论课表/.test(title)) score += 50;
- if (/学期理论课表/.test(text)) score += 30;
- if (doc.querySelector("#xnxq01id")) score += 20;
- if (doc.querySelector("#zc")) score += 10;
- if (getCourseTable(doc)) score += 10;
- return score;
- } catch (_) {
- return 0;
- }
- }
- pushDoc(document);
- Array.from(document.querySelectorAll("iframe")).forEach(function (iframe) {
- try {
- var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
- if (doc) pushDoc(doc);
- } catch (_) {}
- });
- docs.sort(function (a, b) { return scoreDoc(b) - scoreDoc(a); });
- return docs;
- }
- function getCourseTable(doc) {
- return doc.querySelector("#kbtable") ||
- doc.querySelector("#tab1") ||
- doc.querySelector("table.kb_table") ||
- doc.querySelector("table.kbtable") ||
- doc.querySelector("table");
- }
- function isScheduleDoc(doc) {
- try {
- var text = normalizeText((doc.body && doc.body.innerText) || "");
- if (/登录|用户名|密码/.test(text) && !/课表/.test(text)) return false;
- if (/学期理论课表|我的课表/.test(text) && getCourseTable(doc)) return true;
- if (doc.querySelector("#xnxq01id") && getCourseTable(doc)) return true;
- return false;
- } catch (_) {
- return false;
- }
- }
- function parseDayFromHeader(text) {
- var m = normalizeText(text).match(/星期([一二三四五六日天])/);
- if (!m) return 0;
- return { "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "日": 7, "天": 7 }[m[1]] || 0;
- }
- function parseWeekText(weekText) {
- var text = normalizeText(weekText);
- if (!text) return [];
- text = text.replace(/(/g, "(").replace(/)/g, ")");
- text = text.replace(/\s+/g, "");
- text = text.replace(/周次[::]?/g, "");
- var odd = /单/.test(text);
- var even = /双/.test(text);
- text = text.replace(/\((?:单|双)\)/g, "");
- text = text.replace(/[单双]/g, "");
- text = text.replace(/\(周\)/g, "");
- text = text.replace(/周/g, "");
- text = text.replace(/[;;]/g, ",");
- var result = [];
- var seen = {};
- text.split(/[,,]/).map(function (x) { return x.trim(); }).filter(Boolean).forEach(function (part) {
- var range = part.match(/^(\d+)-(\d+)$/);
- if (range) {
- var start = parseInt(range[1], 10);
- var end = parseInt(range[2], 10);
- if (start > end) {
- var t = start;
- start = end;
- end = t;
- }
- for (var i = start; i <= end; i++) {
- if (odd && i % 2 === 0) continue;
- if (even && i % 2 !== 0) continue;
- if (!seen[i]) {
- seen[i] = true;
- result.push(i);
- }
- }
- return;
- }
- var single = part.match(/^(\d+)$/);
- if (single) {
- var w = parseInt(single[1], 10);
- if (odd && w % 2 === 0) return;
- if (even && w % 2 !== 0) return;
- if (!seen[w]) {
- seen[w] = true;
- result.push(w);
- }
- }
- });
- result.sort(function (a, b) { return a - b; });
- return result;
- }
- function parseSectionText(text) {
- var raw = normalizeText(text).replace(/(/g, "(").replace(/)/g, ")");
- if (!raw) return null;
- var m = raw.match(/\[(\d+(?:-\d+)*)节\]/) || raw.match(/\[(\d+(?:[-,,]\d+)*)小节\]/);
- if (!m) return null;
- var nums = (m[1].match(/\d+/g) || []).map(function (x) { return parseInt(x, 10); }).filter(function (n) { return !isNaN(n); });
- if (!nums.length) return null;
- return {
- startSection: Math.min.apply(null, nums),
- endSection: Math.max.apply(null, nums)
- };
- }
- function extractWeekAndSectionLine(lines) {
- for (var i = 0; i < lines.length; i++) {
- if (/\(周\)/.test(lines[i]) && /\[\d+(?:-\d+)*节\]/.test(lines[i])) {
- return { index: i, text: lines[i] };
- }
- }
- return null;
- }
- function isMeaninglessLine(line) {
- var text = normalizeText(line);
- if (!text) return true;
- if (/^(学期理论课表|理论课表|实践课表|课表查询|筛选|放大|时间模式[::]?.*|周次[::]?.*)$/.test(text)) return true;
- return false;
- }
- function splitCoursesInCell(doc, cell) {
- function getCellLines() {
- var text = normalizeText(cell.innerText || cell.textContent || "");
- if (!text) return [];
- return text.split("\n").map(function (x) { return normalizeText(x); }).filter(Boolean).filter(function (line) {
- return !isMeaninglessLine(line) && !/^[-]{3,}$/.test(line);
- });
- }
- function splitByParagraphs() {
- var ps = Array.from(cell.querySelectorAll("p"));
- if (!ps.length) return [];
- return ps.map(function (p) {
- return normalizeText(p.innerText || p.textContent || "");
- }).filter(Boolean).filter(function (t) {
- return /\(周\)/.test(t) && /\[(?:\d+(?:[-,,]\d+)*)节\]/.test(t);
- });
- }
- function splitBySequentialLines(lines) {
- var blocks = [];
- var i = 0;
- while (i < lines.length) {
- var line = lines[i];
- if (!line) {
- i++;
- continue;
- }
- if (!/\(周\)/.test(line) || !/\[(?:\d+(?:[-,,]\d+)*)节\]/.test(line)) {
- i++;
- continue;
- }
- var parts = [];
- if (i - 2 >= 0) {
- parts.push(lines[i - 2]);
- parts.push(lines[i - 1]);
- } else if (i - 1 >= 0) {
- parts.push(lines[i - 1]);
- }
- parts.push(line);
- if (i + 1 < lines.length) parts.push(lines[i + 1]);
- var cleaned = [];
- parts.forEach(function (p) {
- p = normalizeText(p);
- if (!p) return;
- if (/^[-]{3,}$/.test(p)) return;
- cleaned.push(p);
- });
- if (cleaned.length) {
- var last = cleaned[cleaned.length - 1];
- if (/\(周\)/.test(last) && i + 1 < lines.length) cleaned.push(lines[i + 1]);
- blocks.push(cleaned.join("\n"));
- }
- i += 2;
- }
- return blocks;
- }
- function splitCompactText(text) {
- var lines = text.split("\n").map(function (x) { return normalizeText(x); }).filter(Boolean);
- if (!lines.length) return [];
- return splitBySequentialLines(lines);
- }
- var byP = splitByParagraphs();
- if (byP.length) return byP;
- var text2 = normalizeText(cell.innerText || cell.textContent || "");
- if (!text2) return [];
- var lineBlocks = splitBySequentialLines(getCellLines());
- if (lineBlocks.length) return lineBlocks;
- var normalized = text2
- .replace(/([\u4e00-\u9fa5A-Za-z0-9()()《》·,,、\-\s]+?)\s+([0-9]+(?:-[0-9]+)?(?:[,,][0-9]+(?:-[0-9]+)?)*(?:\((?:单|双)\))?\(周\)\[[0-9\-,,]+节\])/g, function (_, a, b) {
- return normalizeText(a) + "\n" + normalizeText(b);
- })
- .replace(/(\[[0-9\-,,]+节\])\s*([^\n\[]+)/g, function (_, a, b) {
- return a + "\n" + normalizeText(b);
- })
- .replace(/\s{2,}/g, "\n");
- var compactBlocks = splitCompactText(normalized);
- if (compactBlocks.length) return compactBlocks;
- return [];
- }
- function parseCourseBlock(text, day) {
- var raw = normalizeText(text);
- if (!raw) return null;
- raw = raw.replace(/(/g, "(").replace(/)/g, ")");
- if (!/\(周\)/.test(raw) || !/\[(?:\d+(?:[-,,]\d+)*)节\]/.test(raw)) return null;
- var lines = raw.split("\n").map(function (x) { return normalizeText(x); }).filter(Boolean);
- if (!lines.length) return null;
- var wsIndex = -1;
- for (var li = 0; li < lines.length; li++) {
- if (/\(周\)/.test(lines[li]) && /\[(?:\d+(?:[-,,]\d+)*)节\]/.test(lines[li])) {
- wsIndex = li;
- break;
- }
- }
- if (wsIndex <= 0) {
- for (var i = 0; i < lines.length; i++) {
- var line = lines[i];
- if (!/\(周\)/.test(line)) continue;
- var weekPartMatch = line.match(/([0-9,,\-]+(?:\((?:单|双)\))?\(周\))/);
- var sectionPartMatch = line.match(/(\[(?:\d+(?:[-,,]\d+)*)节\])/);
- if (weekPartMatch && sectionPartMatch) {
- var prefix = normalizeText(line.slice(0, line.indexOf(weekPartMatch[1])));
- var suffix = normalizeText(line.slice(line.indexOf(sectionPartMatch[1]) + sectionPartMatch[1].length));
- var rebuilt = [];
- if (prefix) rebuilt.push(prefix);
- rebuilt.push(weekPartMatch[1] + sectionPartMatch[1]);
- if (suffix) rebuilt.push(suffix);
- lines.splice.apply(lines, [i, 1].concat(rebuilt));
- wsIndex = prefix ? i + 1 : i;
- break;
- }
- }
- }
- if (wsIndex <= 0) return null;
- var wsLine = lines[wsIndex];
- var weekMatch = wsLine.match(/([0-9,,\-]+(?:\((?:单|双)\))?\(周\))/);
- if (!weekMatch) return null;
- var weeks = parseWeekText(weekMatch[1]);
- if (!weeks.length) return null;
- var section = parseSectionText(wsLine);
- if (!section) return null;
- var name = lines[0] || "";
- if (!name) return null;
- if (/^[\d,\-,()\[\]单双周节小节]+$/.test(name)) return null;
- var courseNature = "";
- var natureMatch = name.match(/\[(必修|选修)\]/);
- if (natureMatch) {
- courseNature = natureMatch[1] === "必修" ? "required" : "elective";
- name = name.replace(/\[(必修|选修)\]/g, "").trim();
- }
- name = name.replace(/\[(\d+)\]/g, "").trim();
- if (!name) return null;
- var beforeWeek = lines.slice(1, wsIndex);
- var teacher = "";
- var noteParts = [];
- if (beforeWeek.length) {
- if (beforeWeek.length === 1) {
- teacher = beforeWeek[0];
- } else {
- teacher = beforeWeek[beforeWeek.length - 1] || "";
- noteParts = beforeWeek.slice(0, -1);
- }
- }
- if (/^\[[0-9]+(?:-[0-9]+)?\]班$/.test(teacher) || /^\d+$/.test(teacher) || /\(周\)|\[(?:\d+(?:[-,,]\d+)*)节\]/.test(teacher)) {
- noteParts.push(teacher);
- teacher = "";
- }
- var position = "";
- for (var j = wsIndex + 1; j < lines.length; j++) {
- var nextLine = lines[j];
- if (!nextLine) continue;
- position = nextLine;
- break;
- }
- noteParts = noteParts.filter(Boolean).map(function (x) { return x.trim(); }).filter(Boolean);
- var course = {
- name: name,
- teacher: teacher || "",
- position: position || "",
- day: Number(day),
- startSection: section.startSection,
- endSection: section.endSection,
- weeks: weeks
- };
- course.location = course.position;
- course.dayOfWeek = course.day;
- course.startWeek = weeks[0];
- course.endWeek = weeks[weeks.length - 1];
- if (courseNature) course.courseNature = courseNature;
- if (weeks.length && weeks.every(function (w) { return w % 2 === 1; })) course.isOddWeek = true;
- if (weeks.length && weeks.every(function (w) { return w % 2 === 0; })) course.isEvenWeek = true;
- if (noteParts.length) course.note = noteParts.join(" ");
- return course;
- }
- function dedupeCourses(courses) {
- var map = {};
- var result = [];
- courses.forEach(function (c) {
- var key = [
- c.name, c.teacher, c.position, c.day, c.startSection, c.endSection, (c.weeks || []).join(",")
- ].join("||");
- if (!map[key]) {
- map[key] = true;
- result.push(c);
- }
- });
- return result;
- }
- function parseTimeSlots(doc) {
- var table = getCourseTable(doc);
- if (!table) return [];
- var rows = Array.from(table.querySelectorAll("tr"));
- var result = [];
- var seen = {};
- function toMin(v) {
- var p = String(v).split(":");
- return parseInt(p[0], 10) * 60 + parseInt(p[1], 10);
- }
- function toHHMM(mins) {
- mins = Math.round(mins);
- var h = Math.floor(mins / 60);
- var m = mins % 60;
- return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0");
- }
- rows.forEach(function (row, idx) {
- if (idx === 0) return;
- var firstCell = row.cells && row.cells[0];
- if (!firstCell) return;
- var txt = normalizeText(firstCell.innerText || firstCell.textContent || "");
- var secMatch = txt.match(/(\d+(?:,\d+)*)节/);
- var timeMatch = txt.match(/(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})/);
- if (!secMatch || !timeMatch) return;
- var nums = secMatch[1].split(",").map(function (x) { return parseInt(x, 10); }).filter(function (n) { return !isNaN(n); });
- if (!nums.length) return;
- var start = timeMatch[1].padStart(5, "0");
- var end = timeMatch[2].padStart(5, "0");
- var startMin = toMin(start);
- var endMin = toMin(end);
- var step = (endMin - startMin) / nums.length;
- nums.forEach(function (num, index) {
- if (seen[num]) return;
- var item = {
- number: num,
- section: num,
- startTime: toHHMM(startMin + step * index),
- endTime: toHHMM(index === nums.length - 1 ? endMin : (startMin + step * (index + 1)))
- };
- seen[num] = true;
- result.push(item);
- });
- });
- result.sort(function (a, b) { return a.number - b.number; });
- return result;
- }
- function parseCourseConfig(doc) {
- var config = {
- firstDayOfWeek: 1,
- semesterStartDate: null
- };
- var termSelect = doc.querySelector("#xnxq01id");
- if (termSelect) {
- var selectedOption = termSelect.options && termSelect.selectedIndex >= 0 ? termSelect.options[termSelect.selectedIndex] : null;
- var termValue = normalizeText((selectedOption && (selectedOption.value || selectedOption.text)) || termSelect.value || "");
- if (termValue) {
- config.term = termValue;
- var m = termValue.match(/^(\d{4})-(\d{4})-(\d)$/);
- if (m) {
- config.schoolYear = m[1] + "-" + m[2];
- config.termName = "第" + m[3] + "学期";
- }
- }
- }
- var weekSelect = doc.querySelector("#zc");
- if (weekSelect) {
- var maxWeek = 0;
- Array.from(weekSelect.options || []).forEach(function (opt) {
- var n = parseInt(opt.value, 10);
- if (!isNaN(n) && n > maxWeek) maxWeek = n;
- });
- if (maxWeek > 0) {
- config.totalWeeks = maxWeek;
- config.semesterTotalWeeks = maxWeek;
- }
- }
- config.defaultClassDuration = 45;
- config.defaultBreakDuration = 10;
- return config;
- }
- function parseFromTable(doc) {
- var table = getCourseTable(doc);
- if (!table) throw new Error("未找到课表表格");
- var rows = Array.from(table.querySelectorAll("tr"));
- if (rows.length < 2) throw new Error("课表表格行数不足");
- var headerRow = rows[0];
- var headerCells = Array.from((headerRow && headerRow.cells) || []);
- if (headerCells.length < 8 && rows[0].querySelectorAll("th,td").length >= 8) {
- headerCells = Array.from(rows[0].querySelectorAll("th,td"));
- }
- if (headerCells.length < 8) throw new Error("课表表头异常");
- var dayMap = {};
- for (var i = 1; i < headerCells.length; i++) {
- var day = parseDayFromHeader(headerCells[i].innerText || headerCells[i].textContent || "");
- if (day) dayMap[i] = day;
- }
- if (Object.keys(dayMap).length < 7) throw new Error("星期列识别不完整");
- var courses = [];
- for (var r = 1; r < rows.length; r++) {
- var cells = Array.from(rows[r].cells || []);
- if (cells.length < 8) continue;
- for (var c = 1; c <= 7 && c < cells.length; c++) {
- var dayNum = dayMap[c];
- if (!dayNum) continue;
- var cell = cells[c];
- var cellText = normalizeText(cell.innerText || cell.textContent || "");
- if (!cellText || isMeaninglessLine(cellText)) continue;
- var blocks = splitCoursesInCell(doc, cell);
- blocks.forEach(function (block) {
- var parsed = parseCourseBlock(block, dayNum);
- if (parsed) courses.push(parsed);
- });
- }
- }
- return {
- courses: dedupeCourses(courses),
- timeSlots: parseTimeSlots(doc),
- config: parseCourseConfig(doc)
- };
- }
- function toFinalCourse(course) {
- var weeks = (course.weeks || []).map(function (x) { return Number(x); }).filter(function (x) { return !isNaN(x); }).sort(function (a, b) { return a - b; });
- var day = Number(course.day);
- var finalCourse = {
- name: String(course.name || "").trim(),
- teacher: String(course.teacher || "").trim(),
- position: String(course.position || "").trim(),
- day: day,
- startSection: Number(course.startSection),
- endSection: Number(course.endSection),
- weeks: weeks
- };
- finalCourse.location = finalCourse.position;
- finalCourse.dayOfWeek = day;
- finalCourse.startWeek = weeks.length ? weeks[0] : 0;
- finalCourse.endWeek = weeks.length ? weeks[weeks.length - 1] : 0;
- finalCourse.customWeeks = weeks.slice();
- if (course.courseNature) finalCourse.courseNature = course.courseNature;
- if (course.note) finalCourse.note = String(course.note).trim();
- if (course.isOddWeek) finalCourse.isOddWeek = true;
- if (course.isEvenWeek) finalCourse.isEvenWeek = true;
- return finalCourse;
- }
- async function main() {
- if (!window.AndroidBridgePromise || !window.AndroidBridge || typeof window.AndroidBridge.notifyTaskCompletion !== "function") {
- throw new Error("桥接接口不可用");
- }
- var targetDoc = await waitFor(function () {
- var docs = getAccessibleDocuments();
- for (var i = 0; i < docs.length; i++) {
- var doc = docs[i];
- try {
- var href = String((doc.location && doc.location.href) || "");
- if (/\/xskb\/xskb_list\.do/i.test(href) && getCourseTable(doc)) return doc;
- } catch (_) {}
- }
- for (var j = 0; j < docs.length; j++) {
- if (isScheduleDoc(docs[j])) return docs[j];
- }
- return null;
- }, 20000, 300);
- if (!targetDoc) {
- throw new Error("当前页面不是可解析的课表页,或课表 iframe 未加载完成");
- }
- var parsed = parseFromTable(targetDoc);
- if (!parsed || !parsed.courses || !parsed.courses.length) {
- throw new Error("未解析到任何课程,请确认当前学期课表已加载且不是空课表");
- }
- var finalCourses = parsed.courses.map(toFinalCourse).filter(function (c) {
- return c.name &&
- c.day >= 1 && c.day <= 7 &&
- c.startSection > 0 &&
- c.endSection >= c.startSection &&
- Array.isArray(c.weeks) &&
- c.weeks.length > 0;
- });
- if (!finalCourses.length) {
- throw new Error("课程字段转换后为空,无法保存");
- }
- await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
- if (parsed.timeSlots && parsed.timeSlots.length) {
- await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(parsed.timeSlots));
- }
- if (parsed.config) {
- await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(parsed.config));
- }
- toast("课表导入成功,共" + finalCourses.length + "门课程");
- window.AndroidBridge.notifyTaskCompletion();
- }
- try {
- await main();
- } catch (error) {
- await fail("解析学期理论课表失败", error);
- }
- })();
|