hbmzu.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. // 湖北民族大学教务系统(jwgl.hbmzu.edu.cn/edu)拾光课表导入适配脚本
  2. // 理论课表:课表查询页(/edu/report/mySchedule.mvc)直接渲染 DOM:
  3. // #container 内每行 tr:首 td 为节次("上午1-2节"…"晚上11-12节"),后续 7 个 td 对应周一~周日
  4. // 每格 .course-block 含 span:课程名、教室、周次、教师(无教室时仅 3 个)
  5. // 实验课表:/edu/report/myLabSchedule.mvc 页面内声明学号/学年/学期,按周请求:
  6. // /edu/report/stuLabCourseTable/{学号}/{学年}/{学期}/{周}.mvc
  7. // 导入时把理论与实验课表合并;无开学日期接口,不写课表配置
  8. // 预设作息时间(12 节,含午间/晚间)
  9. const HBMZU_TIME_SLOTS = [
  10. { number: 1, startTime: "08:00", endTime: "08:45" },
  11. { number: 2, startTime: "08:50", endTime: "09:35" },
  12. { number: 3, startTime: "10:05", endTime: "10:50" },
  13. { number: 4, startTime: "10:55", endTime: "11:40" },
  14. { number: 5, startTime: "14:00", endTime: "14:45" },
  15. { number: 6, startTime: "14:50", endTime: "15:35" },
  16. { number: 7, startTime: "16:05", endTime: "16:50" },
  17. { number: 8, startTime: "16:55", endTime: "17:40" },
  18. { number: 9, startTime: "18:30", endTime: "19:15" },
  19. { number: 10, startTime: "19:20", endTime: "20:05" },
  20. { number: 11, startTime: "20:15", endTime: "21:00" },
  21. { number: 12, startTime: "21:05", endTime: "21:50" }
  22. ];
  23. function getErrorMessage(error) {
  24. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  25. if (typeof error === "string" && error.trim()) return error;
  26. try {
  27. const serialized = JSON.stringify(error);
  28. if (serialized && serialized !== "{}") return serialized;
  29. } catch (_) {
  30. // Ignore serialization failures.
  31. }
  32. return "未知错误";
  33. }
  34. // 收集可访问的 document(frameset 用 <frame>,普通页面用 <iframe>)
  35. function collectDocs() {
  36. const docs = [document];
  37. const frames = document.querySelectorAll("iframe, frame");
  38. for (const f of frames) {
  39. try {
  40. if (f.contentDocument) docs.push(f.contentDocument);
  41. } catch (_) {
  42. // 跨域 frame 无法访问,忽略。
  43. }
  44. }
  45. return docs;
  46. }
  47. // 从各 frame 中查找理论课表表格(#container 内含 .course-block)
  48. // 注入时机早于 frame 加载完成,故轮询等待 frame 就绪(最长 6 秒)
  49. async function findCourseTable() {
  50. const deadline = Date.now() + 6000;
  51. while (Date.now() < deadline) {
  52. for (const doc of collectDocs()) {
  53. const container = doc.getElementById("container");
  54. if (container && container.querySelector(".course-block")) {
  55. return container;
  56. }
  57. }
  58. await new Promise(resolve => setTimeout(resolve, 300));
  59. }
  60. return null;
  61. }
  62. // 解析周次文本:"3-14周"→3..14;"3-13单周"→3,5,7,9,11,13;"4-14双周"→4,6..14;"14-15周"→14,15
  63. function parseWeeksText(weekStr) {
  64. const text = String(weekStr || "").replace(/\s+/g, "");
  65. const m = text.match(/^(\d+)-(\d+)(单|双)?周$/);
  66. if (!m) return [];
  67. const start = Number(m[1]);
  68. const end = Number(m[2]);
  69. const parity = m[3]; // "单" / "双" / undefined
  70. const weeks = [];
  71. for (let w = start; w <= end; w++) {
  72. if (parity === "单" && w % 2 === 0) continue;
  73. if (parity === "双" && w % 2 === 1) continue;
  74. weeks.push(w);
  75. }
  76. return weeks;
  77. }
  78. // 是否为周次文本("3-14周" / "3-13单周" / "4-14双周" / "12-12周")
  79. // 用于在 3 个 span 时区分"无教室(名-周次-教师)"和"无教师(名-教室-周次)"两种结构
  80. function isWeeksText(text) {
  81. return /^\d+-\d+(单|双)?周$/.test(String(text || "").replace(/\s+/g, ""));
  82. }
  83. // 解析理论课表表格
  84. // 每行首 td 为节次文本("上午1-2节"→1-2),其后 7 个 td 依次为周一~周日
  85. function parseCourseTable(container) {
  86. const courses = [];
  87. container.querySelectorAll("tr").forEach(row => {
  88. const tds = row.querySelectorAll("td");
  89. if (tds.length < 2) return;
  90. const sectionText = (tds[0].textContent || "").replace(/\s+/g, "");
  91. const sectionMatch = sectionText.match(/(\d+)-(\d+)节/);
  92. if (!sectionMatch) return;
  93. const startSection = Number(sectionMatch[1]);
  94. const endSection = Number(sectionMatch[2]);
  95. for (let i = 0; i < 7; i++) {
  96. const dayTd = tds[i + 1];
  97. if (!dayTd) continue;
  98. const blocks = dayTd.querySelectorAll(".course-block");
  99. for (const block of blocks) {
  100. const cells = [];
  101. block.querySelectorAll("span").forEach(function (s) {
  102. cells.push((s.textContent || "").trim());
  103. });
  104. if (cells.length < 3) continue;
  105. const name = cells[0];
  106. if (!name) continue;
  107. // span 结构:[名称, 教室?, 周次, 教师],教室或教师可能缺失:
  108. // 3 个 span 可能是 [名称, 周次, 教师](无教室),也可能是 [名称, 教室, 周次](无教师)
  109. // 先找出长得像周次的 span:它前面的第一个 span 是教室,后面的第一个 span 是教师
  110. let weekIndex = -1;
  111. let weekStr = "";
  112. for (let j = 1; j < cells.length; j++) {
  113. if (isWeeksText(cells[j])) {
  114. weekIndex = j;
  115. weekStr = cells[j];
  116. break;
  117. }
  118. }
  119. if (weekIndex < 0) continue;
  120. const position = weekIndex >= 2 ? cells[weekIndex - 1] : "";
  121. const teacher = weekIndex + 1 < cells.length ? cells[weekIndex + 1] : "";
  122. const weeks = parseWeeksText(weekStr);
  123. if (weeks.length === 0) continue;
  124. courses.push({
  125. name,
  126. teacher: teacher || "未知",
  127. position: position || "待定",
  128. day: i + 1,
  129. startSection,
  130. endSection,
  131. weeks
  132. });
  133. }
  134. }
  135. });
  136. return courses;
  137. }
  138. // 应用上下文路径(通常为 /edu),兼容不同的部署目录
  139. function getContextPath() {
  140. const m = window.location.pathname.match(/^(.*?)\/(?:report|mainpage)\//);
  141. return m ? m[1] : "/edu";
  142. }
  143. // 学年兜底:9 月及以后算当年,1~8 月算上一年
  144. function fallbackAcademicYear() {
  145. const now = new Date();
  146. return String(now.getMonth() >= 8 ? now.getFullYear() : now.getFullYear() - 1);
  147. }
  148. // 从实验课表页读取学号、学年、学期(页面内声明:const id=…; const openYear=…; const openTerm=…;)
  149. async function fetchLabBase() {
  150. const ctx = getContextPath();
  151. const resp = await fetch(ctx + "/report/myLabSchedule.mvc", { credentials: "include" });
  152. if (!resp.ok) return null;
  153. const html = await resp.text();
  154. const idMatch = html.match(/(?:const|let|var)\s+id\s*=\s*["']?(\d+)["']?\s*;/);
  155. const yearMatch = html.match(/(?:const|let|var)\s+openYear\s*=\s*["']?(\d{4})["']?\s*;/);
  156. const termMatch = html.match(/(?:const|let|var)\s+openTerm\s*=\s*["']?(\d+)["']?\s*;/);
  157. if (!idMatch || !termMatch) return null;
  158. return {
  159. ctx,
  160. sid: idMatch[1],
  161. year: yearMatch ? yearMatch[1] : fallbackAcademicYear(),
  162. term: termMatch[1]
  163. };
  164. }
  165. // 解析单周实验课表:sheduleTable 为「大节 × 星期」二维数组,单元格内 lessons 为课程
  166. function parseLabTable(data, fallbackWeek) {
  167. const courses = [];
  168. if (!data || data.code !== 0 || !Array.isArray(data.sheduleTable)) return courses;
  169. for (const row of data.sheduleTable) {
  170. if (!Array.isArray(row)) continue;
  171. for (const cell of row) {
  172. if (!cell || !Array.isArray(cell.lessons)) continue;
  173. for (const lesson of cell.lessons) {
  174. if (!lesson || !lesson.courseName) continue;
  175. const day = Number(lesson.weekday);
  176. const startSection = Number(lesson.startSection);
  177. const period = Number(lesson.period) > 0 ? Number(lesson.period) : 1;
  178. if (!(day >= 1 && day <= 7) || !(startSection >= 1)) continue;
  179. const week = Number(lesson.week);
  180. courses.push({
  181. name: String(lesson.courseName).trim(),
  182. teacher: String(lesson.teacherName || "").trim(),
  183. position: String(lesson.roomName || "").trim(),
  184. day,
  185. startSection,
  186. endSection: startSection + period - 1,
  187. weeks: [Number.isFinite(week) && week > 0 ? week : fallbackWeek]
  188. });
  189. }
  190. }
  191. }
  192. return courses;
  193. }
  194. async function fetchLabWeek(base, week) {
  195. const url = base.ctx + "/report/stuLabCourseTable/" + base.sid + "/" + base.year + "/" + base.term + "/" + week + ".mvc";
  196. const resp = await fetch(url, { credentials: "include" });
  197. if (!resp.ok) throw new Error("HTTP " + resp.status);
  198. return resp.json();
  199. }
  200. // 拉取整学期实验课表:先取第 1 周拿到总周数,再按周补齐
  201. async function fetchLabCourses() {
  202. let base;
  203. try {
  204. base = await fetchLabBase();
  205. } catch (error) {
  206. console.error("JS: 读取实验课表入口失败", error);
  207. return { courses: [], warning: "实验课表入口读取失败" };
  208. }
  209. if (!base) return { courses: [], warning: "未找到实验课表入口" };
  210. let first;
  211. try {
  212. first = await fetchLabWeek(base, 1);
  213. } catch (error) {
  214. console.error("JS: 第 1 周实验课表请求失败", error);
  215. return { courses: [], warning: "第 1 周实验课表请求失败" };
  216. }
  217. const totalWeeks = Math.max(1, Number(first.totalWeeks) || 20);
  218. const courses = parseLabTable(first, 1);
  219. const pending = [];
  220. for (let w = 2; w <= totalWeeks; w++) pending.push(w);
  221. // 分批并发,避免一次性打满
  222. const batchSize = 5;
  223. for (let i = 0; i < pending.length; i += batchSize) {
  224. const batch = pending.slice(i, i + batchSize);
  225. const results = await Promise.all(batch.map(function (w) {
  226. return fetchLabWeek(base, w).then(function (data) {
  227. return { week: w, data };
  228. }).catch(function () {
  229. return null;
  230. });
  231. }));
  232. for (const r of results) {
  233. if (!r) continue;
  234. courses.push(...parseLabTable(r.data, r.week));
  235. }
  236. }
  237. return { courses, warning: "" };
  238. }
  239. // 合并同课程同时间同教室的条目(跨周合并 weeks;单双周分教室的保持两条)
  240. function mergeCourses(courses) {
  241. const merged = new Map();
  242. for (const c of courses) {
  243. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}`;
  244. const holder = merged.get(key);
  245. if (holder) {
  246. holder.weeks = Array.from(new Set([...holder.weeks, ...c.weeks])).sort((a, b) => a - b);
  247. } else {
  248. merged.set(key, { ...c, weeks: [...c.weeks].sort((a, b) => a - b) });
  249. }
  250. }
  251. return Array.from(merged.values()).sort(
  252. (a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name)
  253. );
  254. }
  255. // 保存作息时间
  256. async function saveTimeSlots(timeSlots) {
  257. try {
  258. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  259. } catch (error) {
  260. console.error("JS: 作息时间保存失败", error);
  261. }
  262. }
  263. async function runImportFlow() {
  264. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  265. "课表导入",
  266. "导入前请确保已登录并打开课表查询页面,将同时导入理论课表与实验课表。",
  267. "好的,开始导入"
  268. );
  269. if (!alertConfirmed) {
  270. window.shiguangBridge.showToast("用户取消了导入。");
  271. return;
  272. }
  273. window.shiguangBridge.showToast("正在获取理论课表...");
  274. try {
  275. let theoryCourses = [];
  276. const container = await findCourseTable();
  277. if (container) {
  278. theoryCourses = parseCourseTable(container);
  279. }
  280. window.shiguangBridge.showToast("正在获取实验课表...");
  281. const labResult = await fetchLabCourses();
  282. const merged = mergeCourses([...theoryCourses, ...labResult.courses]);
  283. if (merged.length === 0) throw new Error("未解析到有效课程,请确认已登录并打开课表查询页面。");
  284. window.shiguangBridge.showToast(`正在保存 ${merged.length} 条课程...`);
  285. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(merged, null, 2));
  286. await saveTimeSlots(HBMZU_TIME_SLOTS);
  287. window.shiguangBridge.showToast(
  288. `课程导入成功,共 ${merged.length} 条(理论 ${theoryCourses.length} 条 · 实验 ${labResult.courses.length} 条)!`
  289. );
  290. if (labResult.warning) {
  291. window.shiguangBridge.showToast("实验课表未导入:" + labResult.warning);
  292. }
  293. window.shiguangBridge.notifyTaskCompletion();
  294. } catch (error) {
  295. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  296. console.error("JS: Import Error", error);
  297. }
  298. }
  299. runImportFlow();