swjtu_vatuu.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. // 西南交通大学 VATUU 为途教务系统课表导入脚本
  2. // 适配页面:/vatuu/CourseAction?setAction=userCourseSchedule&selectTableType=ThisTerm
  3. (() => {
  4. const SWJTU_SEMESTER_TOTAL_WEEKS = 19;
  5. const SWJTU_TIME_SLOTS = [
  6. { number: 1, startTime: "08:00", endTime: "08:45" },
  7. { number: 2, startTime: "08:50", endTime: "09:35" },
  8. { number: 3, startTime: "09:50", endTime: "10:35" },
  9. { number: 4, startTime: "10:40", endTime: "11:25" },
  10. { number: 5, startTime: "11:30", endTime: "12:15" },
  11. { number: 6, startTime: "14:00", endTime: "14:45" },
  12. { number: 7, startTime: "14:50", endTime: "15:35" },
  13. { number: 8, startTime: "15:40", endTime: "16:25" },
  14. { number: 9, startTime: "16:40", endTime: "17:25" },
  15. { number: 10, startTime: "17:30", endTime: "18:15" },
  16. { number: 11, startTime: "19:30", endTime: "20:15" },
  17. { number: 12, startTime: "20:20", endTime: "21:05" },
  18. { number: 13, startTime: "21:10", endTime: "21:55" },
  19. ];
  20. const SWJTU_COURSE_CONFIG = {
  21. semesterStartDate: null,
  22. semesterTotalWeeks: SWJTU_SEMESTER_TOTAL_WEEKS,
  23. defaultClassDuration: 45,
  24. firstDayOfWeek: 1,
  25. };
  26. function normalizeText(text) {
  27. return String(text || "")
  28. .replace(/\u00a0/g, " ")
  29. .replace(/ /gi, " ")
  30. .replace(/[\t\r]+/g, " ")
  31. .replace(/ +/g, " ")
  32. .trim();
  33. }
  34. function getCellLines(cell) {
  35. const cloned = cell.cloneNode(true);
  36. cloned.querySelectorAll("br").forEach((br) => {
  37. br.replaceWith("\n");
  38. });
  39. return normalizeText(cloned.textContent)
  40. .split(/\n+/)
  41. .map(normalizeText)
  42. .filter((line) => line && line !== " ");
  43. }
  44. function isCourseHeaderLine(line) {
  45. return /^[A-Z]+\d+\s+.+[((].+[))]$/.test(line);
  46. }
  47. function parseCourseHeader(line) {
  48. const match = normalizeText(line).match(
  49. /^([A-Z]+\d+)\s+(.+?)[((]([^()()]*)[))]$/,
  50. );
  51. if (!match) return null;
  52. return {
  53. courseCode: match[1],
  54. name: normalizeText(match[2]),
  55. teacher: normalizeText(match[3]),
  56. };
  57. }
  58. function parseWeeks(weekText) {
  59. const weeks = [];
  60. const text = normalizeText(weekText)
  61. .replace(/第/g, "")
  62. .replace(/[[【]/g, "(")
  63. .replace(/[\]】]/g, ")");
  64. const regex =
  65. /(\d+)(?:\s*-\s*(\d+))?\s*周?\s*(?:\((单|双)\)|([单双])周?)?/g;
  66. let match = regex.exec(text);
  67. while (match !== null) {
  68. const start = Number(match[1]);
  69. const end = match[2] ? Number(match[2]) : start;
  70. const parity = match[3] || match[4] || "";
  71. if (!Number.isFinite(start) || !Number.isFinite(end) || start > end)
  72. continue;
  73. for (let week = start; week <= end; week++) {
  74. if (parity === "单" && week % 2 !== 1) continue;
  75. if (parity === "双" && week % 2 !== 0) continue;
  76. if (!weeks.includes(week)) weeks.push(week);
  77. }
  78. match = regex.exec(text);
  79. }
  80. return weeks.sort((a, b) => a - b);
  81. }
  82. function parseScheduleLine(line) {
  83. const text = normalizeText(line);
  84. const match = text.match(
  85. /^(.+?周(?:\s*[((][单双][))])?)(?:\s+(.+))?$/,
  86. );
  87. if (!match) return null;
  88. const weeks = parseWeeks(match[1]);
  89. if (weeks.length === 0) return null;
  90. return {
  91. weeks,
  92. position: normalizeText(match[2] || "未指定"),
  93. };
  94. }
  95. function parseCoursesFromCell(cell) {
  96. const lines = getCellLines(cell);
  97. const courses = [];
  98. for (let i = 0; i < lines.length; i++) {
  99. if (!isCourseHeaderLine(lines[i])) continue;
  100. const header = parseCourseHeader(lines[i]);
  101. if (!header?.name) continue;
  102. let schedule = null;
  103. for (let j = i + 1; j < lines.length; j++) {
  104. if (isCourseHeaderLine(lines[j])) break;
  105. schedule = parseScheduleLine(lines[j]);
  106. if (schedule) break;
  107. }
  108. if (schedule) {
  109. courses.push({
  110. courseCode: header.courseCode,
  111. name: header.name,
  112. teacher: header.teacher || "未指定",
  113. position: schedule.position || "未指定",
  114. weeks: schedule.weeks,
  115. });
  116. }
  117. }
  118. return courses;
  119. }
  120. function getCandidateDocuments() {
  121. const docs = [document];
  122. document.querySelectorAll("iframe").forEach((frame) => {
  123. try {
  124. if (frame.contentDocument) docs.push(frame.contentDocument);
  125. } catch (error) {
  126. console.warn("跳过不可访问的 iframe:", error);
  127. }
  128. });
  129. return docs;
  130. }
  131. function findScheduleTable() {
  132. for (const doc of getCandidateDocuments()) {
  133. const tables = Array.from(doc.querySelectorAll("table"));
  134. const table = tables.find((item) => {
  135. const text = normalizeText(item.textContent);
  136. return (
  137. text.includes("星期一") &&
  138. text.includes("上课时间") &&
  139. text.includes("星期日")
  140. );
  141. });
  142. if (table) return table;
  143. }
  144. return null;
  145. }
  146. function getTableDiagnostics() {
  147. return getCandidateDocuments()
  148. .map((doc, index) => {
  149. const tableCount = doc.querySelectorAll("table").length;
  150. return `文档${index + 1}:${doc.title || "无标题"},URL:${doc.location?.href || "未知"},表格数量:${tableCount}`;
  151. })
  152. .join(";");
  153. }
  154. async function waitForScheduleTable(timeoutMs = 5000) {
  155. const startedAt = Date.now();
  156. let table = findScheduleTable();
  157. while (!table && Date.now() - startedAt < timeoutMs) {
  158. await new Promise((resolve) => setTimeout(resolve, 300));
  159. table = findScheduleTable();
  160. }
  161. return table;
  162. }
  163. function sameCourseForMerge(a, b) {
  164. return (
  165. a.day === b.day &&
  166. a.courseCode === b.courseCode &&
  167. a.name === b.name &&
  168. a.teacher === b.teacher &&
  169. a.position === b.position &&
  170. a.weeks.join(",") === b.weeks.join(",")
  171. );
  172. }
  173. function mergeContinuousCourses(courseRows) {
  174. const merged = [];
  175. const sorted = courseRows.slice().sort((a, b) => {
  176. if (a.day !== b.day) return a.day - b.day;
  177. if (a.startSection !== b.startSection)
  178. return a.startSection - b.startSection;
  179. return a.name.localeCompare(b.name, "zh-Hans-CN");
  180. });
  181. sorted.forEach((course) => {
  182. const last = merged[merged.length - 1];
  183. if (
  184. last &&
  185. sameCourseForMerge(last, course) &&
  186. course.startSection === last.endSection + 1
  187. ) {
  188. last.endSection = course.endSection;
  189. } else {
  190. merged.push({ ...course });
  191. }
  192. });
  193. return merged.map(({ courseCode, ...course }) => course);
  194. }
  195. async function parseScheduleTable() {
  196. const table = await waitForScheduleTable();
  197. if (!table) {
  198. throw new Error(
  199. `未找到课表表格,请确认已进入 VATUU 本学期课表页面并等待课表加载完成。${getTableDiagnostics()}`,
  200. );
  201. }
  202. const rows = Array.from(table.querySelectorAll("tr")).slice(1);
  203. const courseRows = [];
  204. rows.forEach((row) => {
  205. const cells = Array.from(row.querySelectorAll("td"));
  206. if (cells.length < 9) return;
  207. const section = Number(
  208. normalizeText(cells[0].textContent).match(/\d+/)?.[0],
  209. );
  210. if (!Number.isFinite(section)) return;
  211. for (let day = 1; day <= 7; day++) {
  212. const cell = cells[day + 1];
  213. parseCoursesFromCell(cell).forEach((course) => {
  214. courseRows.push({
  215. ...course,
  216. day,
  217. startSection: section,
  218. endSection: section,
  219. });
  220. });
  221. }
  222. });
  223. return mergeContinuousCourses(courseRows);
  224. }
  225. function collectUnscheduledCourses() {
  226. const marker = "以下课程由于未安排具体节次时间,无法显示";
  227. const text = getCandidateDocuments()
  228. .map((doc) => normalizeText(doc.body?.textContent || ""))
  229. .find((docText) => docText.includes(marker));
  230. if (!text) return [];
  231. return text
  232. .slice(text.indexOf(marker) + marker.length)
  233. .split(/(?=[A-Z]+\d+\s+)/)
  234. .map(normalizeText)
  235. .filter((line) => /^[A-Z]+\d+\s+/.test(line));
  236. }
  237. async function importSwjtuSchedule() {
  238. try {
  239. window.shiguangBridge.showToast("正在解析西南交通大学 VATUU 课表...");
  240. const courses = await parseScheduleTable();
  241. if (courses.length === 0) {
  242. await window.shiguangBridgePromise.showAlert(
  243. "导入失败",
  244. "未解析到课程。请确认当前页面为本学期课表,并选择“全部周次”。",
  245. "确定",
  246. );
  247. return false;
  248. }
  249. await window.shiguangBridgePromise.saveCourseConfig(
  250. JSON.stringify(SWJTU_COURSE_CONFIG),
  251. );
  252. await window.shiguangBridgePromise.savePresetTimeSlots(
  253. JSON.stringify(SWJTU_TIME_SLOTS),
  254. );
  255. await window.shiguangBridgePromise.saveImportedCourses(
  256. JSON.stringify(courses),
  257. );
  258. const unscheduledCourses = collectUnscheduledCourses();
  259. if (unscheduledCourses.length > 0) {
  260. window.shiguangBridge.showToast(
  261. `成功导入 ${courses.length} 条课程,另有 ${unscheduledCourses.length} 门无节次课程已跳过`,
  262. );
  263. } else {
  264. window.shiguangBridge.showToast(`成功导入 ${courses.length} 条课程`);
  265. }
  266. window.shiguangBridge.notifyTaskCompletion();
  267. return true;
  268. } catch (error) {
  269. console.error("SWJTU VATUU 课表导入失败:", error);
  270. await window.shiguangBridgePromise.showAlert(
  271. "导入失败",
  272. `解析或保存课表失败:${error.message}`,
  273. "确定",
  274. );
  275. return false;
  276. }
  277. }
  278. void importSwjtuSchedule();
  279. })();