hunnu.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. /**
  2. * 湖南师范大学 (hunnu.edu.cn) 拾光课程表适配脚本
  3. *
  4. * 适配方式:从 courseTableForStd.action 返回的 HTML 中
  5. * 解析 TaskActivity JavaScript 数据,提取精确的课程信息。
  6. * 课程数据以 new TaskActivity(...) 调用 + index 赋值的形式嵌入页面。
  7. */
  8. "use strict";
  9. // ===== 常量 =====
  10. const UNIT_COUNT = 13;
  11. const TIME_SLOTS = [
  12. { number: 1, startTime: "08:00", endTime: "08:45" },
  13. { number: 2, startTime: "08:55", endTime: "09:40" },
  14. { number: 3, startTime: "10:00", endTime: "10:45" },
  15. { number: 4, startTime: "10:55", endTime: "11:40" },
  16. { number: 5, startTime: "12:45", endTime: "13:30" },
  17. { number: 6, startTime: "13:30", endTime: "14:15" },
  18. { number: 7, startTime: "14:30", endTime: "15:15" },
  19. { number: 8, startTime: "15:25", endTime: "16:10" },
  20. { number: 9, startTime: "16:30", endTime: "17:15" },
  21. { number: 10, startTime: "17:25", endTime: "18:10" },
  22. { number: 11, startTime: "19:00", endTime: "19:45" },
  23. { number: 12, startTime: "19:55", endTime: "20:40" },
  24. { number: 13, startTime: "20:50", endTime: "21:35" }
  25. ];
  26. // ===== 工具函数 =====
  27. /**
  28. * 将周次位图转为周次数组
  29. * 位图格式:50位0/1字符串,位置i(1-indexed)对应第i周
  30. * 位置0固定为0(占位符),位置1=第1周,位置2=第2周...
  31. */
  32. function parseWeeksFromBitmap(bitmap) {
  33. const weeks = [];
  34. if (!bitmap) return weeks;
  35. for (let i = 1; i < bitmap.length; i++) {
  36. if (bitmap[i] === "1") weeks.push(i);
  37. }
  38. return weeks;
  39. }
  40. /**
  41. * 智能分割 TaskActivity 参数(按逗号,保留引号内内容)
  42. */
  43. function splitArgs(str) {
  44. const result = [];
  45. let current = "";
  46. let depth = 0;
  47. let inQuote = false;
  48. for (const ch of str) {
  49. if (ch === '"') { inQuote = !inQuote; current += ch; }
  50. else if (ch === "(" && !inQuote) { depth++; current += ch; }
  51. else if (ch === ")" && !inQuote) { depth--; current += ch; }
  52. else if (ch === "," && !inQuote && depth === 0) {
  53. result.push(current.trim());
  54. current = "";
  55. } else { current += ch; }
  56. }
  57. if (current.trim()) result.push(current.trim());
  58. return result;
  59. }
  60. /**
  61. * 从完整 HTML 中解析所有 TaskActivity 课程
  62. *
  63. * 匹配模式:actTeachers + 任意代码 + TaskActivity + index 赋值
  64. * 使用单个正则避免跨块获取错误的 actTeachers。
  65. */
  66. function parseTaskActivities(html) {
  67. const courses = [];
  68. const blockRe = /var\s+actTeachers\s*=\s*\[[^\]]*?name\s*:\s*"([^"]+)"[^\]]*?\]\s*;[\s\S]*?activity\s*=\s*new\s+TaskActivity\s*\(([\s\S]*?)\)\s*;([\s\S]*?)(?=var\s+(?:actTeachers|teachers)|table0\.marshalTable|$)/g;
  69. let match;
  70. while ((match = blockRe.exec(html)) !== null) {
  71. const teacher = match[1];
  72. const argsStr = match[2];
  73. const tail = match[3];
  74. const parts = splitArgs(argsStr);
  75. if (parts.length < 7) continue;
  76. const courseFull = (parts[3] || "").replace(/^"|"$/g, "");
  77. const location = (parts[5] || "").replace(/^"|"$/g, "");
  78. const weekBitmap = (parts[6] || "").replace(/^"|"$/g, "");
  79. const nameMatch = courseFull.match(/^(.+?)\(/);
  80. const name = nameMatch ? nameMatch[1].trim() : courseFull;
  81. if (!name) continue;
  82. const weeks = parseWeeksFromBitmap(weekBitmap);
  83. if (weeks.length === 0) continue;
  84. const idxRe = /index\s*=\s*(\d+)\s*\*\s*(?:unitCount|\d+)\s*\+\s*(\d+)\s*;/g;
  85. let idxMatch;
  86. while ((idxMatch = idxRe.exec(tail)) !== null) {
  87. const day = parseInt(idxMatch[1]);
  88. const section = parseInt(idxMatch[2]);
  89. courses.push({
  90. name, teacher, position: location,
  91. day: day + 1, startSection: section + 1, endSection: section + 1,
  92. weeks: [...weeks]
  93. });
  94. }
  95. }
  96. return courses;
  97. }
  98. /**
  99. * 合并同一课程在相邻节次的条目
  100. */
  101. function mergeCourses(courses) {
  102. // 第一步:按 name+teacher+position+day 分组
  103. const groups = new Map();
  104. for (const c of courses) {
  105. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}`;
  106. if (!groups.has(key)) groups.set(key, []);
  107. groups.get(key).push(c);
  108. }
  109. const merged = [];
  110. for (const [, group] of groups) {
  111. // 按 startSection 排序
  112. group.sort((a, b) => a.startSection - b.startSection);
  113. let current = null;
  114. for (const c of group) {
  115. if (!current) {
  116. current = { ...c, weeks: [...c.weeks] };
  117. } else if (c.startSection === current.endSection + 1) {
  118. // 相邻节次,扩展 endSection 并合并周次
  119. current.endSection = c.endSection;
  120. current.weeks = [...new Set([...current.weeks, ...c.weeks])].sort((a, b) => a - b);
  121. } else {
  122. merged.push(current);
  123. current = { ...c, weeks: [...c.weeks] };
  124. }
  125. }
  126. if (current) merged.push(current);
  127. }
  128. return merged;
  129. }
  130. // ===== 主流程 =====
  131. /**
  132. * 等待指定毫秒
  133. */
  134. function sleep(ms) {
  135. return new Promise(resolve => setTimeout(resolve, ms));
  136. }
  137. /**
  138. * 获取课表页面的完整 HTML
  139. *
  140. * 策略:
  141. * 1. 当前已经是课表页(URL 含 courseTableForStd)→ 直接取 document
  142. * 2. 在外层页面,课表 iframe 已存在 → 取 iframe 内容
  143. * 3. 在外层页面,课表 iframe 未创建 → 自动点击"我的课表"链接,等待 iframe 加载后取内容
  144. */
  145. async function getCourseTableHtml() {
  146. const url = window.location.href;
  147. if (url.includes("courseTableForStd")) {
  148. return document.documentElement.outerHTML;
  149. }
  150. let iframe = Array.from(document.querySelectorAll("iframe.eams-iframe")).find(
  151. f => (f.src || f.getAttribute("src") || "").includes("courseTableForStd")
  152. );
  153. if (!iframe) {
  154. const link = document.querySelector('a[href*="courseTableForStd"][target*="eams-iframe"]');
  155. if (link) {
  156. link.click();
  157. for (let i = 0; i < 30; i++) {
  158. iframe = Array.from(document.querySelectorAll("iframe.eams-iframe")).find(
  159. f => (f.src || f.getAttribute("src") || "").includes("courseTableForStd")
  160. );
  161. if (iframe) break;
  162. await sleep(200);
  163. }
  164. }
  165. }
  166. if (iframe) {
  167. const srcdoc = iframe.getAttribute("srcdoc");
  168. if (srcdoc) return srcdoc;
  169. if (!iframe.contentDocument) {
  170. await new Promise(resolve => {
  171. iframe.addEventListener("load", resolve, { once: true });
  172. });
  173. }
  174. return iframe.contentDocument.documentElement.outerHTML;
  175. }
  176. await window.AndroidBridgePromise.showAlert(
  177. "未找到课表",
  178. "请先点击「我的课表」打开课表页面,然后重新运行导入。",
  179. "确定"
  180. );
  181. throw new Error("course table not found");
  182. }
  183. async function runImportFlow() {
  184. AndroidBridge.showToast("湖南师范大学课程导入启动...");
  185. const confirmed = await window.AndroidBridgePromise.showAlert(
  186. "湖南师范大学课表导入",
  187. "导入前请确保:\n1. 您已登录教务系统\n2. 课表页面已打开且课程数据正常显示",
  188. "好的,开始导入"
  189. );
  190. if (!confirmed) { AndroidBridge.showToast("导入已取消"); return; }
  191. AndroidBridge.showToast("正在获取课表数据...");
  192. const html = await getCourseTableHtml();
  193. // 解析课程
  194. const rawCourses = parseTaskActivities(html);
  195. if (rawCourses.length === 0) {
  196. await window.AndroidBridgePromise.showAlert(
  197. "解析失败",
  198. "未能从页面中识别到课程数据。\n请确认课表页面已正确加载。",
  199. "确定"
  200. );
  201. return;
  202. }
  203. const courses = mergeCourses(rawCourses);
  204. // 导入时间段
  205. try {
  206. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(TIME_SLOTS));
  207. } catch (e) {
  208. AndroidBridge.showToast("导入时间段失败: " + e.message);
  209. }
  210. // 保存配置
  211. try {
  212. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({ semesterTotalWeeks: 20 }));
  213. } catch (e) {
  214. AndroidBridge.showToast("保存配置失败: " + e.message);
  215. }
  216. // 保存课程
  217. try {
  218. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  219. AndroidBridge.showToast(`成功导入 ${courses.length} 门课程!`);
  220. } catch (e) {
  221. AndroidBridge.showToast("保存课程数据失败: " + e.message);
  222. return;
  223. }
  224. AndroidBridge.showToast("课表导入完成!");
  225. AndroidBridge.notifyTaskCompletion();
  226. }
  227. runImportFlow();