myschool.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // 西安工业大学(http://jwgl2018.xatu.edu.cn) 拾光课程表适配脚本,基于天津农学院适配脚本
  2. // 本校开发者适配,出现问题请提issues或者提交pr更改,这更加快速
  3. //感谢XingHeYuZhuan、aryunm、jursin...等的帮助,感谢trae的辅助
  4. function powerSplit(paramsRaw) {
  5. const args = [];
  6. let current = "";
  7. let depth = 0;
  8. let inQuote = false;
  9. let quoteChar = "";
  10. for (let i = 0; i < paramsRaw.length; i++) {
  11. let char = paramsRaw[i];
  12. if ((char === '"' || char === "'") && (i === 0 || paramsRaw[i - 1] !== '\\')) {
  13. if (!inQuote) { inQuote = true; quoteChar = char; }
  14. else if (char === quoteChar) { inQuote = false; }
  15. }
  16. if (!inQuote) {
  17. if (char === '(' || char === '[' || char === '{') depth++;
  18. if (char === ')' || char === ']' || char === '}') depth--;
  19. }
  20. if (char === ',' && depth === 0 && !inQuote) {
  21. args.push(cleanArg(current));
  22. current = "";
  23. } else {
  24. current += char;
  25. }
  26. }
  27. args.push(cleanArg(current));
  28. return args;
  29. }
  30. function cleanArg(s) {
  31. s = s.trim();
  32. return s.replace(/^["']|["']$/g, "");
  33. }
  34. /** 按课程、教师、地点和星期合并连续节次,保持原有50周范围。 */
  35. function mergeContinuousLessons(lessons) {
  36. const groups = new Map();
  37. for (const lesson of lessons) {
  38. const key = JSON.stringify([lesson.courseSequence, lesson.name, lesson.teacher, lesson.position, lesson.day]);
  39. if (!groups.has(key)) groups.set(key, { lesson, weeks: Array.from({ length: 50 }, () => new Set()) });
  40. const { weeks } = groups.get(key);
  41. for (const week of lesson.weeks) {
  42. if (week >= 50) continue;
  43. for (let section = lesson.startSection; section <= lesson.endSection; section++) weeks[week].add(section);
  44. }
  45. }
  46. const courses = [];
  47. for (const { lesson, weeks } of groups.values()) {
  48. const blocks = new Map();
  49. weeks.forEach((sections, week) => {
  50. const sorted = [...sections].sort((a, b) => a - b);
  51. for (let i = 0; i < sorted.length; i++) {
  52. const start = sorted[i];
  53. while (i + 1 < sorted.length && sorted[i + 1] === sorted[i] + 1) i++;
  54. const key = `${start}-${sorted[i]}`;
  55. if (!blocks.has(key)) blocks.set(key, []);
  56. blocks.get(key).push(week);
  57. }
  58. });
  59. for (const [key, courseWeeks] of blocks) {
  60. const [startSection, endSection] = key.split('-').map(Number);
  61. courses.push({
  62. name: lesson.courseSequence ? `${lesson.name} | ${lesson.courseSequence}` : lesson.name,
  63. teacher: lesson.teacher, position: lesson.position, day: lesson.day,
  64. startSection, endSection, weeks: courseWeeks,
  65. credit: lesson.credit,
  66. ...(/教3/.test(lesson.position) && startSection === 3 && endSection === 4 ? {
  67. isCustomTime: true, customStartTime: "10:10", customEndTime: "11:40"
  68. } : {})
  69. });
  70. }
  71. }
  72. return courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
  73. }
  74. /** 从同一课表响应的课程列表中读取学分,按课程序号匹配,避免同名课程混淆。 */
  75. function parseCourseCredits(html) {
  76. const doc = new DOMParser().parseFromString(html, "text/html");
  77. const credits = new Map();
  78. for (const table of doc.querySelectorAll("table")) {
  79. const header = Array.from(table.rows).find(row =>
  80. Array.from(row.cells).some(cell => cell.tagName === "TH" && cell.textContent.trim() === "学分"));
  81. if (!header) continue;
  82. const headers = Array.from(header.cells).map(cell => cell.textContent.trim());
  83. const creditIndex = headers.indexOf("学分");
  84. const sequenceIndex = headers.indexOf("课程序号");
  85. if (sequenceIndex < 0) continue;
  86. for (const row of table.rows) {
  87. const sequence = row.cells[sequenceIndex]?.textContent.trim();
  88. const text = row.cells[creditIndex]?.textContent.trim();
  89. if (!sequence || !/^\d+(?:\.\d+)?$/.test(text)) continue;
  90. credits.set(sequence, Number(text));
  91. }
  92. }
  93. return credits;
  94. }
  95. function parseTaskActivities(html) {
  96. const credits = parseCourseCredits(html);
  97. const lessons = [];
  98. for (const block of html.split(/var\s+teachers\s*=/).slice(1)) {
  99. const activities = [...block.matchAll(/new\s+TaskActivity\(([\s\S]*?)\);/g)];
  100. let teacher = "未知教师";
  101. for (let i = 0; i < activities.length; i++) {
  102. const activity = activities[i];
  103. const before = block.slice(i ? activities[i - 1].index + activities[i - 1][0].length : 0, activity.index);
  104. const teachers = before.match(/actTeachers\s*=\s*(\[[\s\S]*?\])/);
  105. if (teachers) {
  106. teacher = [...teachers[1].matchAll(/name\s*:\s*["'](.*?)["']/g)].map(m => m[1]).join("、") || "未知教师";
  107. }
  108. const args = powerSplit(activity[1]);
  109. const courseSequence = (args[2] || "").match(/\(([^()]*)\)$/)?.[1] || "";
  110. const credit = credits.get(courseSequence);
  111. const position = (args[5] || "未知地点").replace(/\(.*?\)/g, "");
  112. const weeks = [...(args[6] || "")].flatMap((bit, week) => bit === "1" ? [week] : []);
  113. const indexes = block.slice(activity.index + activity[0].length, activities[i + 1]?.index)
  114. .matchAll(/index\s*=\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+);/g);
  115. for (const index of indexes) {
  116. lessons.push({
  117. name: (args[3] || "未知课程").split('(')[0], courseSequence,
  118. teacher, position, day: Number(index[1]) + 1,
  119. startSection: Number(index[2]) + 1, endSection: Number(index[2]) + 1,
  120. weeks, credit
  121. });
  122. }
  123. }
  124. }
  125. return mergeContinuousLessons(lessons);
  126. }
  127. async function request(path, body) {
  128. const controller = new AbortController();
  129. const timer = setTimeout(() => controller.abort(), 15000);
  130. try {
  131. const res = await fetch(`http://jwgl2018.xatu.edu.cn/eams/${path}`, {
  132. credentials: "include", signal: controller.signal,
  133. ...(body !== undefined ? {
  134. method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body
  135. } : {})
  136. });
  137. if (!res.ok) throw new Error(`教务请求失败:HTTP ${res.status}`);
  138. const html = await res.text();
  139. return { html, serverTime: Date.parse(res.headers.get("Date") || "") };
  140. } catch (e) {
  141. if (controller.signal.aborted) throw new Error("教务请求超时,请稍后重试");
  142. throw e;
  143. } finally {
  144. clearTimeout(timer);
  145. }
  146. }
  147. /** 根据教务首页的当前教学周,推算第一教学周的周一(北京时间)。 */
  148. function inferCurrentSemester(html, referenceTime) {
  149. const doc = new DOMParser().parseFromString(html, "text/html");
  150. const label = Array.from(doc.querySelectorAll("strong"))
  151. .find(el => el.textContent.trim() === "当前教学周");
  152. const weekText = label?.parentElement?.querySelector("span")?.textContent.trim();
  153. const totalMatch = label?.parentElement?.querySelector("i")?.textContent.match(/\/\s*(\d+)/);
  154. if (!weekText || !/^\d+$/.test(weekText) || !totalMatch) return null;
  155. const week = Number(weekText);
  156. const totalWeeks = Number(totalMatch[1]);
  157. if (week < 1 || week > totalWeeks || totalWeeks > 53) return null;
  158. // 固定按教务的 UTC+8 日期计算第一周周一。
  159. const start = new Date(referenceTime + 8 * 3600000);
  160. start.setUTCHours(0, 0, 0, 0);
  161. start.setUTCDate(start.getUTCDate() - (start.getUTCDay() + 6) % 7 - (week - 1) * 7);
  162. const isAutumn = start.getUTCMonth() >= 6;
  163. const year = start.getUTCFullYear() - (isAutumn ? 0 : 1);
  164. return {
  165. schoolYear: `${year}-${year + 1}`, term: isAutumn ? "1" : "2",
  166. semesterStartDate: start.toISOString().slice(0, 10),
  167. semesterTotalWeeks: totalWeeks
  168. };
  169. }
  170. async function getCurrentSemesterConfig() {
  171. try {
  172. const { html, serverTime } = await request("homeExt!main.action?sf_request_type=ajax");
  173. const current = inferCurrentSemester(html, Number.isFinite(serverTime) ? serverTime : Date.now());
  174. if (!current) throw new Error("未能识别当前教学周");
  175. return current;
  176. } catch (e) {
  177. console.warn(`[开学日期推算] ${e.message}`);
  178. window.shiguangBridge.showToast(`${e.message};开学日期请手动设置`);
  179. return null;
  180. }
  181. }
  182. async function detectParameters() {
  183. const { html } = await request("courseTableForStd.action?sf_request_type=ajax");
  184. const idsMatch = html.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
  185. const tagIdMatch = html.match(/id="(semesterBar\d+Semester)"/);
  186. if (!idsMatch || !tagIdMatch) return null;
  187. return { ids: idsMatch[1], tagId: tagIdMatch[1] };
  188. }
  189. async function getSelectedSemester({ tagId }, current) {
  190. const { html } = await request("dataQuery.action?sf_request_type=ajax",
  191. `tagId=${encodeURIComponent(tagId)}&dataType=semesterCalendar`);
  192. const data = Function(`return (${html});`)();
  193. const list = Object.values(data.semesters).flat().map(s => ({
  194. id: s.id, name: `${s.schoolYear}学年 ${String(s.name) === "1" ? "秋季" : "春季"}学期`,
  195. isCurrent: s.schoolYear === current?.schoolYear && String(s.name) === current?.term
  196. }));
  197. const currentIndex = list.findIndex(s => s.isCurrent);
  198. const idx = await window.shiguangBridgePromise.showSingleSelection("选择学期",
  199. JSON.stringify(list.map(s => s.name + (s.isCurrent ? "(当前学期)" : ""))),
  200. currentIndex);
  201. return idx === null ? null : list[idx];
  202. }
  203. async function fetchAndParseCourses(semesterId, ids) {
  204. const { html } = await request("courseTableForStd!courseTable.action?sf_request_type=ajax",
  205. `ignoreHead=1&setting.kind=std&semester.id=${encodeURIComponent(semesterId)}&ids=${encodeURIComponent(ids)}`);
  206. return parseTaskActivities(html);
  207. }
  208. async function applyTimeSlots() {
  209. const slots = [
  210. ["08:20", "09:05"], ["09:15", "10:00"],
  211. ["10:20", "11:05"], ["11:15", "12:00"],
  212. ["14:00", "14:45"], ["14:55", "15:40"],
  213. ["16:00", "16:45"], ["16:55", "17:40"],
  214. ["18:10", "18:55"], ["19:05", "19:50"],
  215. ["20:00", "20:45"], ["20:55", "21:40"]
  216. ].map(([startTime, endTime], i) => ({ number: i + 1, startTime, endTime }));
  217. return await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
  218. }
  219. async function runImportFlow() {
  220. try {
  221. window.shiguangBridge.showToast("开始探测教务参数...");
  222. const params = await detectParameters();
  223. if (!params) throw new Error("未能识别教务参数,请确认已登录");
  224. const current = await getCurrentSemesterConfig();
  225. const semester = await getSelectedSemester(params, current);
  226. if (!semester) return;
  227. window.shiguangBridge.showToast("正在同步课表...");
  228. const courses = await fetchAndParseCourses(semester.id, params.ids);
  229. if (!courses.length) throw new Error("未解析到课程数据");
  230. if (current && !semester.isCurrent) window.shiguangBridge.showToast("历史或其他学期开学日期请手动设置");
  231. if (!await applyTimeSlots()) {
  232. console.error("作息时间保存失败,导入未完成");
  233. return;
  234. }
  235. if (semester.isCurrent && !await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
  236. semesterStartDate: current.semesterStartDate,
  237. semesterTotalWeeks: current.semesterTotalWeeks
  238. }))) {
  239. throw new Error("学期配置保存失败,导入未完成");
  240. }
  241. if (!await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses))) {
  242. throw new Error("课程保存失败,导入未完成");
  243. }
  244. window.shiguangBridge.showToast(`成功导入 ${courses.length} 个课程条目`);
  245. window.shiguangBridge.notifyTaskCompletion();
  246. } catch (e) {
  247. console.error(`[异常] ${e.message}`);
  248. window.shiguangBridge.showToast(e.message);
  249. }
  250. }
  251. runImportFlow();