cug_postgraduate.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. /**
  2. * 中国地质大学(武汉)研究生教务系统 拾光课程表适配脚本
  3. *
  4. * 数据来源:研究生教务课表查询页(DOM 表格 id = ctl00_contentParent_dgData)
  5. * 列:时间 | 节次 | 星期一 … 星期日
  6. * 行:上午(1-4节) 下午(5-8节) 晚上(9-12节),共 12 行
  7. * 日程格使用 rowspan 表示 “连续占据多个节次” 对应一个课程条目
  8. *
  9. * 课程单元格文案格式:
  10. * 课程名{第几周[教师:教师名,地点:教室名]},多个课程用;分隔
  11. * 周数为区间/顿号写法,例如:2-5、7-11 或 7 或 1-19
  12. *
  13. * 与本科版(cug01.js)保持一致:导入前让用户选择校区作息,
  14. * 导入后保存作息时段与课表配置,全程提供 Toast/弹窗反馈。
  15. */
  16. // ============================================================================
  17. // 作息方案(与中国地质大学本科共用三套,见 cug01.js)
  18. // ============================================================================
  19. /** 南望山校区 · 秋冬季作息(共12节) */
  20. const CUG_NANWANGSHAN_AUTUMN = [
  21. { number: 1, startTime: "08:00", endTime: "08:45" },
  22. { number: 2, startTime: "08:50", endTime: "09:35" },
  23. { number: 3, startTime: "10:05", endTime: "10:50" },
  24. { number: 4, startTime: "10:55", endTime: "11:40" },
  25. { number: 5, startTime: "14:00", endTime: "14:45" },
  26. { number: 6, startTime: "14:50", endTime: "15:35" },
  27. { number: 7, startTime: "16:00", endTime: "16:45" },
  28. { number: 8, startTime: "16:50", endTime: "17:35" },
  29. { number: 9, startTime: "19:00", endTime: "19:45" },
  30. { number: 10, startTime: "19:50", endTime: "20:35" },
  31. { number: 11, startTime: "20:40", endTime: "21:25" },
  32. { number: 12, startTime: "21:30", endTime: "22:15" }
  33. ];
  34. /** 南望山校区 · 春夏季作息(5月1日后执行,下午推迟30分钟,共12节) */
  35. const CUG_NANGSHAN_SUMMER = [
  36. { number: 1, startTime: "08:00", endTime: "08:45" },
  37. { number: 2, startTime: "08:50", endTime: "09:35" },
  38. { number: 3, startTime: "10:05", endTime: "10:50" },
  39. { number: 4, startTime: "10:55", endTime: "11:40" },
  40. { number: 5, startTime: "14:30", endTime: "15:15" },
  41. { number: 6, startTime: "15:20", endTime: "16:05" },
  42. { number: 7, startTime: "16:35", endTime: "17:20" },
  43. { number: 8, startTime: "17:25", endTime: "18:10" },
  44. { number: 9, startTime: "19:30", endTime: "20:15" },
  45. { number: 10, startTime: "20:20", endTime: "21:05" },
  46. { number: 11, startTime: "21:10", endTime: "21:55" },
  47. { number: 12, startTime: "22:00", endTime: "22:45" }
  48. ];
  49. /** 未来城校区 · 标准作息(全年统一,共12节) */
  50. const CUG_FUTURE_CITY = [
  51. { number: 1, startTime: "08:30", endTime: "09:15" },
  52. { number: 2, startTime: "09:20", endTime: "10:05" },
  53. { number: 3, startTime: "10:15", endTime: "11:00" },
  54. { number: 4, startTime: "11:05", endTime: "11:50" },
  55. { number: 5, startTime: "14:00", endTime: "14:45" },
  56. { number: 6, startTime: "14:50", endTime: "15:35" },
  57. { number: 7, startTime: "15:45", endTime: "16:30" },
  58. { number: 8, startTime: "16:35", endTime: "17:20" },
  59. { number: 9, startTime: "18:30", endTime: "19:15" },
  60. { number: 10, startTime: "19:20", endTime: "20:05" },
  61. { number: 11, startTime: "20:15", endTime: "21:00" },
  62. { number: 12, startTime: "21:05", endTime: "21:50" }
  63. ];
  64. /** 作息方案选项 */
  65. const SCHEDULE_OPTIONS = [
  66. { name: "南望山校区 · 秋冬季作息(下午 14:00 上课)", slots: CUG_NANWANGSHAN_AUTUMN },
  67. { name: "南望山校区 · 春夏季作息(5月1日后,下午 14:30 上课)", slots: CUG_NANGSHAN_SUMMER },
  68. { name: "未来城校区 · 标准作息(上午 08:30 上课,全天12节)", slots: CUG_FUTURE_CITY }
  69. ];
  70. // ============================================================================
  71. // 表格解析
  72. // ============================================================================
  73. /** 展开周数区间:"2-5、7-11" → [2,3,4,5,7,8,9,10,11] */
  74. function expandWeeks(weekText) {
  75. const weeks = new Set();
  76. weekText.split(/[、,,]/).forEach(part => {
  77. part = part.trim();
  78. if (!part) return;
  79. const m = part.match(/^(\d+)\s*-\s*(\d+)$/);
  80. if (m) {
  81. let a = parseInt(m[1], 10), b = parseInt(m[2], 10);
  82. if (a > b) { const t = a; a = b; b = t; }
  83. for (let i = a; i <= b; i++) weeks.add(i);
  84. } else if (/^\d+$/.test(part)) {
  85. weeks.add(parseInt(part, 10));
  86. }
  87. });
  88. return Array.from(weeks).sort((a, b) => a - b);
  89. }
  90. /** 解析单个课程条目文本,返回 {name, teacher, position, weeks} */
  91. function parseCourseText(text) {
  92. text = text.trim();
  93. // 兼容全角{}与半角{};课程名后可能直接跟周数,也可能带括号前缀
  94. const m = text.match(/^(.+?)[{}{}]\s*(.+?)\s*[}}]$/);
  95. if (!m) return null;
  96. const name = m[1].trim();
  97. const meta = m[2];
  98. let weeks = [];
  99. let teacher = "", position = "";
  100. const weekMatch = meta.match(/第?\s*([0-9、,,\-]+)\s*周?/);
  101. if (weekMatch) weeks = expandWeeks(weekMatch[1]);
  102. const teacherMatch = meta.match(/教师[::]\s*([^,\],;;。]+)/);
  103. if (teacherMatch) teacher = teacherMatch[1].trim();
  104. const posMatch = meta.match(/地点[::]\s*([^,\],;;。]+)/);
  105. if (posMatch) position = posMatch[1].trim();
  106. return { name, className: name, teacher, position, weeks };
  107. }
  108. /** 解析一个日程格(可能含多个课程,用分号分隔),返回课程对象数组,day 为 1-7(周一~周日) */
  109. function parseDayCell(cellText, day, startSection, endSection) {
  110. if (!cellText) return [];
  111. const result = [];
  112. cellText.split(/[;;]/).forEach(part => {
  113. part = part.trim();
  114. if (!part) return;
  115. const c = parseCourseText(part);
  116. if (c) result.push(Object.assign(c, { day, startSection, endSection }));
  117. });
  118. return result;
  119. }
  120. /**
  121. * 从标准课表表格还原二维课程格 grid[12][7](节次 × 星期)。
  122. * 关键:表格中每日格用 rowspan 表示占用连续节次;被覆盖的行不再出现该列的单元格,
  123. * 因此需要用“列占用计数器 occ[]”把 DOM 单元格映射回(节次 × 星期)网格位置。
  124. */
  125. function parseScheduleTable(table) {
  126. const grid = Array.from({ length: 12 }, () => new Array(7).fill(""));
  127. const rows = Array.from(table.rows);
  128. const occ = new Array(9).fill(0); // 时间、节次、星期一..星期日:9 列各自的剩余占用行数
  129. let lesson = 0;
  130. rows.forEach((tr, ri) => {
  131. if (ri === 0) return; // 表头
  132. lesson++;
  133. if (lesson > 12) return;
  134. let nextCol = 0;
  135. Array.from(tr.cells).forEach(cell => {
  136. // 跳过本行被上方 rowspan 覆盖而未出现的列
  137. while (nextCol < 9 && occ[nextCol] > 0) {
  138. occ[nextCol]--;
  139. nextCol++;
  140. }
  141. if (nextCol >= 9) return;
  142. const col = nextCol;
  143. const rs = cell.rowSpan || 1;
  144. const text = (cell.textContent || "").replace(/\s+/g, "").trim();
  145. if (col >= 2 && col <= 8) {
  146. const day = col - 2;
  147. for (let k = 0; k < rs && lesson - 1 + k < 12; k++) {
  148. grid[lesson - 1 + k][day] = text;
  149. }
  150. }
  151. occ[col] += rs - 1;
  152. nextCol++;
  153. });
  154. });
  155. return grid;
  156. }
  157. /** 依据还原的网格生成课程列表(相邻节走文案相同视为同一课程的连续时段) */
  158. function buildCourses(table) {
  159. const grid = parseScheduleTable(table);
  160. const courses = [];
  161. const seenSpan = Array.from({ length: 12 }, () => new Array(7).fill(false));
  162. grid.forEach((lessonRow, li) => {
  163. const section = li + 1;
  164. lessonRow.forEach((text, di) => {
  165. if (!text || seenSpan[li][di]) return;
  166. const day = di + 1;
  167. // 计算该课程在 (节走列, 星期列) 连续占用的末节
  168. let end = li;
  169. while (end + 1 < 12 && grid[end + 1][di] === text) end++;
  170. parseDayCell(text, day, section, end + 1).forEach(c => courses.push(c));
  171. for (let k = li; k <= end; k++) seenSpan[k][di] = true;
  172. });
  173. });
  174. return courses;
  175. }
  176. /** 在主体文档及 iframe 中查找课表,合并去重(沿用 fetchCourses 对 iframe 的过滤写法) */
  177. function parseAllDocuments() {
  178. const docs = [];
  179. if (document) docs.push(document);
  180. document.querySelectorAll("iframe").forEach(iframe => {
  181. if (iframe.contentDocument && iframe.contentDocument.getElementById) {
  182. const iframeDoc = iframe.contentDocument.documentURI;
  183. if (iframeDoc.includes("Course/StuCourseQuery")) {
  184. docs.push(iframe.contentDocument);
  185. }
  186. }
  187. });
  188. if (docs.length > 0) {
  189. const all = [];
  190. docs.forEach(doc => {
  191. const table =
  192. doc.querySelector("#ctl00_contentParent_dgData") ||
  193. doc.querySelector("table.Grid_Line") ||
  194. doc.querySelector("table[id*=dgData]");
  195. if (!table) return;
  196. all.push(...buildCourses(table));
  197. });
  198. console.log("获取到课程:" + all);
  199. // 去重
  200. const key = c =>
  201. [c.className, c.teacher, c.position, c.day, c.startSection, c.endSection, c.weeks.join(",")].join("|");
  202. const seen = new Set(), uniq = [];
  203. all.forEach(c => {
  204. const k = key(c);
  205. if (!seen.has(k)) { seen.add(k); uniq.push(c); }
  206. });
  207. uniq.sort((a, b) => a.day - b.day || a.startSection - b.startSection);
  208. return uniq;
  209. } else {
  210. console.log("docs为空");
  211. }
  212. }
  213. /** 智能推测默认作息索引(按教室是否含校区关键字;否则按季节) */
  214. function inferDefaultScheduleIndex(courses) {
  215. if (!courses || !courses.length) return 0;
  216. let future = 0, nwn = 0;
  217. courses.forEach(c => {
  218. const p = c.position || "";
  219. if (p.includes("未来城") || /未来城|future/.test(p)) future++;
  220. if (p.includes("南望山") || /南望山|nws/.test(p)) nwn++;
  221. });
  222. if (future > nwn) return 2;
  223. const month = new Date().getMonth() + 1;
  224. if (month >= 5 && month <= 9) return 1;
  225. return 0;
  226. }
  227. async function runImportFlow() {
  228. window.shiguangBridge && window.shiguangBridge.showToast("正在解析课表...");
  229. let courses = [];
  230. try {
  231. courses = parseAllDocuments();
  232. } catch (e) {
  233. console.error("解析课表失败:", e);
  234. window.shiguangBridge && window.shiguangBridge.showToast("解析课表失败:" + e.message);
  235. window.shiguangBridge && window.shiguangBridge.notifyTaskCompletion();
  236. return;
  237. }
  238. console.log("解析到课程数:", courses.length);
  239. console.table(courses);
  240. if (!courses.length) {
  241. await window.shiguangBridgePromise.showAlert(
  242. "未找到课表数据",
  243. "未在当前页面解析到任何课程。\n\n可能原因:\n1. 尚未打开课表查询页面(学期课表信息查询);\n2. 页面正在加载或发生异常。\n\n请刷新并进入课表查询页面后重试。",
  244. "我知道了"
  245. );
  246. window.shiguangBridge && window.shiguangBridge.notifyTaskCompletion();
  247. return;
  248. }
  249. // 选择校区与作息
  250. const defaultIdx = inferDefaultScheduleIndex(courses);
  251. const optionsLabels = SCHEDULE_OPTIONS.map(opt => opt.name);
  252. let selectedIdx = await window.shiguangBridgePromise.showSingleSelection(
  253. `识别到 ${courses.length} 门课程\n请选择您的校区与作息时间:`,
  254. JSON.stringify(optionsLabels),
  255. defaultIdx
  256. );
  257. if (selectedIdx === null || selectedIdx === undefined || selectedIdx < 0) {
  258. selectedIdx = defaultIdx;
  259. window.shiguangBridge.showToast("已使用推荐作息: " + SCHEDULE_OPTIONS[selectedIdx].name);
  260. }
  261. const chosenSchedule = SCHEDULE_OPTIONS[selectedIdx];
  262. // 2. 保存课程
  263. try {
  264. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  265. window.shiguangBridge.showToast("课程导入成功!");
  266. } catch (error) {
  267. console.error("保存课程失败:", error);
  268. await window.shiguangBridgePromise.showAlert("导入失败", "课程保存失败:" + error.message, "知道了");
  269. window.shiguangBridge && window.shiguangBridge.notifyTaskCompletion();
  270. return;
  271. }
  272. // 3. 保存作息时间段
  273. try {
  274. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(chosenSchedule.slots));
  275. } catch (error) {
  276. console.error("保存作息时间段失败:", error);
  277. }
  278. // 4. 保存课表配置
  279. const courseConfig = {
  280. semesterTotalWeeks: 20,
  281. firstDayOfWeek: 1
  282. };
  283. try {
  284. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(courseConfig));
  285. } catch (error) {
  286. console.error("保存课表配置失败:", error);
  287. }
  288. // 5. 完成提示
  289. await window.shiguangBridgePromise.showAlert(
  290. "导入成功",
  291. `已成功导入 ${courses.length} 门课程!\n作息已设为:${chosenSchedule.name}`,
  292. "完成"
  293. );
  294. window.shiguangBridge.showToast(`导入成功,共导入 ${courses.length} 门课程!`);
  295. window.shiguangBridge && window.shiguangBridge.notifyTaskCompletion();
  296. }
  297. // 启动导入流程
  298. runImportFlow();