|
|
@@ -1,322 +0,0 @@
|
|
|
-// 西南交通大学 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();
|
|
|
-})();
|