| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322 |
- // 西南交通大学 VATUU 为途教务系统课表导入脚本
- // 适配页面:/vatuu/CourseAction?setAction=userCourseSchedule&selectTableType=ThisTerm
- (() => {
- const SWJTU_SEMESTER_TOTAL_WEEKS = 19;
- const SWJTU_TIME_SLOTS = [
- { number: 1, startTime: "08:00", endTime: "08:45" },
- { number: 2, startTime: "08:50", endTime: "09:35" },
- { number: 3, startTime: "09:50", endTime: "10:35" },
- { number: 4, startTime: "10:40", endTime: "11:25" },
- { number: 5, startTime: "11:30", endTime: "12:15" },
- { number: 6, startTime: "14:00", endTime: "14:45" },
- { number: 7, startTime: "14:50", endTime: "15:35" },
- { number: 8, startTime: "15:40", endTime: "16:25" },
- { number: 9, startTime: "16:40", endTime: "17:25" },
- { number: 10, startTime: "17:30", endTime: "18:15" },
- { number: 11, startTime: "19:30", endTime: "20:15" },
- { number: 12, startTime: "20:20", endTime: "21:05" },
- { number: 13, startTime: "21:10", endTime: "21:55" },
- ];
- const SWJTU_COURSE_CONFIG = {
- semesterStartDate: null,
- semesterTotalWeeks: SWJTU_SEMESTER_TOTAL_WEEKS,
- defaultClassDuration: 45,
- firstDayOfWeek: 1,
- };
- function normalizeText(text) {
- return String(text || "")
- .replace(/\u00a0/g, " ")
- .replace(/ /gi, " ")
- .replace(/[\t\r]+/g, " ")
- .replace(/ +/g, " ")
- .trim();
- }
- function getCellLines(cell) {
- const cloned = cell.cloneNode(true);
- cloned.querySelectorAll("br").forEach((br) => {
- br.replaceWith("\n");
- });
- return normalizeText(cloned.textContent)
- .split(/\n+/)
- .map(normalizeText)
- .filter((line) => line && line !== " ");
- }
- function isCourseHeaderLine(line) {
- return /^[A-Z]+\d+\s+.+[((].+[))]$/.test(line);
- }
- function parseCourseHeader(line) {
- const match = normalizeText(line).match(
- /^([A-Z]+\d+)\s+(.+?)[((]([^()()]*)[))]$/,
- );
- if (!match) return null;
- return {
- courseCode: match[1],
- name: normalizeText(match[2]),
- teacher: normalizeText(match[3]),
- };
- }
- function parseWeeks(weekText) {
- const weeks = [];
- const text = normalizeText(weekText)
- .replace(/第/g, "")
- .replace(/[[【]/g, "(")
- .replace(/[\]】]/g, ")");
- const regex =
- /(\d+)(?:\s*-\s*(\d+))?\s*周?\s*(?:\((单|双)\)|([单双])周?)?/g;
- let match = regex.exec(text);
- while (match !== null) {
- const start = Number(match[1]);
- const end = match[2] ? Number(match[2]) : start;
- const parity = match[3] || match[4] || "";
- if (!Number.isFinite(start) || !Number.isFinite(end) || start > end)
- continue;
- for (let week = start; week <= end; week++) {
- if (parity === "单" && week % 2 !== 1) continue;
- if (parity === "双" && week % 2 !== 0) continue;
- if (!weeks.includes(week)) weeks.push(week);
- }
- match = regex.exec(text);
- }
- return weeks.sort((a, b) => a - b);
- }
- function parseScheduleLine(line) {
- const text = normalizeText(line);
- const match = text.match(
- /^(.+?周(?:\s*[((][单双][))])?)(?:\s+(.+))?$/,
- );
- if (!match) return null;
- const weeks = parseWeeks(match[1]);
- if (weeks.length === 0) return null;
- return {
- weeks,
- position: normalizeText(match[2] || "未指定"),
- };
- }
- function parseCoursesFromCell(cell) {
- const lines = getCellLines(cell);
- const courses = [];
- for (let i = 0; i < lines.length; i++) {
- if (!isCourseHeaderLine(lines[i])) continue;
- const header = parseCourseHeader(lines[i]);
- if (!header?.name) continue;
- let schedule = null;
- for (let j = i + 1; j < lines.length; j++) {
- if (isCourseHeaderLine(lines[j])) break;
- schedule = parseScheduleLine(lines[j]);
- if (schedule) break;
- }
- if (schedule) {
- courses.push({
- courseCode: header.courseCode,
- name: header.name,
- teacher: header.teacher || "未指定",
- position: schedule.position || "未指定",
- weeks: schedule.weeks,
- });
- }
- }
- return courses;
- }
- function getCandidateDocuments() {
- const docs = [document];
- document.querySelectorAll("iframe").forEach((frame) => {
- try {
- if (frame.contentDocument) docs.push(frame.contentDocument);
- } catch (error) {
- console.warn("跳过不可访问的 iframe:", error);
- }
- });
- return docs;
- }
- function findScheduleTable() {
- for (const doc of getCandidateDocuments()) {
- const tables = Array.from(doc.querySelectorAll("table"));
- const table = tables.find((item) => {
- const text = normalizeText(item.textContent);
- return (
- text.includes("星期一") &&
- text.includes("上课时间") &&
- text.includes("星期日")
- );
- });
- if (table) return table;
- }
- return null;
- }
- function getTableDiagnostics() {
- return getCandidateDocuments()
- .map((doc, index) => {
- const tableCount = doc.querySelectorAll("table").length;
- return `文档${index + 1}:${doc.title || "无标题"},URL:${doc.location?.href || "未知"},表格数量:${tableCount}`;
- })
- .join(";");
- }
- async function waitForScheduleTable(timeoutMs = 5000) {
- const startedAt = Date.now();
- let table = findScheduleTable();
- while (!table && Date.now() - startedAt < timeoutMs) {
- await new Promise((resolve) => setTimeout(resolve, 300));
- table = findScheduleTable();
- }
- return table;
- }
- function sameCourseForMerge(a, b) {
- return (
- a.day === b.day &&
- a.courseCode === b.courseCode &&
- a.name === b.name &&
- a.teacher === b.teacher &&
- a.position === b.position &&
- a.weeks.join(",") === b.weeks.join(",")
- );
- }
- function mergeContinuousCourses(courseRows) {
- const merged = [];
- const sorted = courseRows.slice().sort((a, b) => {
- if (a.day !== b.day) return a.day - b.day;
- if (a.startSection !== b.startSection)
- return a.startSection - b.startSection;
- return a.name.localeCompare(b.name, "zh-Hans-CN");
- });
- sorted.forEach((course) => {
- const last = merged[merged.length - 1];
- if (
- last &&
- sameCourseForMerge(last, course) &&
- course.startSection === last.endSection + 1
- ) {
- last.endSection = course.endSection;
- } else {
- merged.push({ ...course });
- }
- });
- return merged.map(({ courseCode, ...course }) => course);
- }
- async function parseScheduleTable() {
- const table = await waitForScheduleTable();
- if (!table) {
- throw new Error(
- `未找到课表表格,请确认已进入 VATUU 本学期课表页面并等待课表加载完成。${getTableDiagnostics()}`,
- );
- }
- const rows = Array.from(table.querySelectorAll("tr")).slice(1);
- const courseRows = [];
- rows.forEach((row) => {
- const cells = Array.from(row.querySelectorAll("td"));
- if (cells.length < 9) return;
- const section = Number(
- normalizeText(cells[0].textContent).match(/\d+/)?.[0],
- );
- if (!Number.isFinite(section)) return;
- for (let day = 1; day <= 7; day++) {
- const cell = cells[day + 1];
- parseCoursesFromCell(cell).forEach((course) => {
- courseRows.push({
- ...course,
- day,
- startSection: section,
- endSection: section,
- });
- });
- }
- });
- return mergeContinuousCourses(courseRows);
- }
- function collectUnscheduledCourses() {
- const marker = "以下课程由于未安排具体节次时间,无法显示";
- const text = getCandidateDocuments()
- .map((doc) => normalizeText(doc.body?.textContent || ""))
- .find((docText) => docText.includes(marker));
- if (!text) return [];
- return text
- .slice(text.indexOf(marker) + marker.length)
- .split(/(?=[A-Z]+\d+\s+)/)
- .map(normalizeText)
- .filter((line) => /^[A-Z]+\d+\s+/.test(line));
- }
- async function importSwjtuSchedule() {
- try {
- window.shiguangBridge.showToast("正在解析西南交通大学 VATUU 课表...");
- const courses = await parseScheduleTable();
- if (courses.length === 0) {
- await window.shiguangBridgePromise.showAlert(
- "导入失败",
- "未解析到课程。请确认当前页面为本学期课表,并选择“全部周次”。",
- "确定",
- );
- return false;
- }
- await window.shiguangBridgePromise.saveCourseConfig(
- JSON.stringify(SWJTU_COURSE_CONFIG),
- );
- await window.shiguangBridgePromise.savePresetTimeSlots(
- JSON.stringify(SWJTU_TIME_SLOTS),
- );
- await window.shiguangBridgePromise.saveImportedCourses(
- JSON.stringify(courses),
- );
- const unscheduledCourses = collectUnscheduledCourses();
- if (unscheduledCourses.length > 0) {
- window.shiguangBridge.showToast(
- `成功导入 ${courses.length} 条课程,另有 ${unscheduledCourses.length} 门无节次课程已跳过`,
- );
- } else {
- window.shiguangBridge.showToast(`成功导入 ${courses.length} 条课程`);
- }
- window.shiguangBridge.notifyTaskCompletion();
- return true;
- } catch (error) {
- console.error("SWJTU VATUU 课表导入失败:", error);
- await window.shiguangBridgePromise.showAlert(
- "导入失败",
- `解析或保存课表失败:${error.message}`,
- "确定",
- );
- return false;
- }
- }
- void importSwjtuSchedule();
- })();
|