jnmc.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. // 济宁医学院教务(乘方教务 · 旧版 .action 接口)适配器
  2. const PRESET_TIME_SLOTS = [
  3. { number: 1, startTime: "08:00", endTime: "08:40" },
  4. { number: 2, startTime: "08:50", endTime: "09:30" },
  5. { number: 3, startTime: "09:50", endTime: "10:30" },
  6. { number: 4, startTime: "10:40", endTime: "11:20" },
  7. { number: 5, startTime: "11:30", endTime: "12:10" },
  8. { number: 6, startTime: "14:30", endTime: "15:10" },
  9. { number: 7, startTime: "15:20", endTime: "16:00" },
  10. { number: 8, startTime: "16:20", endTime: "17:00" },
  11. { number: 9, startTime: "17:10", endTime: "17:50" },
  12. { number: 10, startTime: "18:00", endTime: "18:40" },
  13. { number: 11, startTime: "19:30", endTime: "20:10" },
  14. { number: 12, startTime: "20:20", endTime: "21:00" }
  15. ];
  16. // 周次字符串 "10,7,8,9" → 去重排序的周数组
  17. function parseWeeks(weekStr) {
  18. if (!weekStr) return [];
  19. const weeks = weekStr.split(",").map(w => parseInt(w.trim(), 10)).filter(w => !isNaN(w) && w > 0);
  20. return [...new Set(weeks)].sort((a, b) => a - b);
  21. }
  22. // 教室可能为空、含 "\\" 分隔的多教室(如 "B205\206教室")或以 "," 分隔的多场地
  23. function resolvePosition(raw) {
  24. const position = String(raw || "").replace(/\\/g, "/").trim();
  25. return position || "待定";
  26. }
  27. // kbxx 课程 JSON → 拾光课程格式(合并同 key 课程的周次)
  28. function parseCourseList(kbxx) {
  29. if (!Array.isArray(kbxx)) throw new Error("课表接口返回格式不正确");
  30. const courseMap = new Map();
  31. kbxx.forEach(item => {
  32. const day = parseInt(item.xq, 10);
  33. const sections = String(item.jcdm2 || "").split(",")
  34. .map(s => parseInt(s.trim(), 10)).filter(s => !isNaN(s));
  35. const allWeeks = parseWeeks(item.zcs);
  36. if (!item.kcmc || sections.length === 0 || allWeeks.length === 0 || isNaN(day) || day < 1 || day > 7) return;
  37. const course = {
  38. name: item.kcmc.trim(),
  39. teacher: String(item.teaxms || "").trim() || "未知",
  40. position: resolvePosition(item.jxcdmcs),
  41. day,
  42. startSection: Math.min(...sections),
  43. endSection: Math.max(...sections),
  44. weeks: allWeeks
  45. };
  46. const key = [course.name, course.teacher, course.position, course.day,
  47. course.startSection, course.endSection].join("__");
  48. const existing = courseMap.get(key);
  49. if (existing) existing.weeks = [...new Set([...existing.weeks, ...course.weeks])].sort((a, b) => a - b);
  50. else courseMap.set(key, course);
  51. });
  52. return Array.from(courseMap.values()).sort((a, b) =>
  53. a.day - b.day || a.startSection - b.startSection || a.endSection - b.endSection || a.name.localeCompare(b.name)
  54. );
  55. }
  56. // 读取课表页中的学期下拉框
  57. function extractSemesterOptions(doc) {
  58. const selectElem = doc.getElementById("xnxqdm");
  59. if (!selectElem) return null;
  60. const semesters = [];
  61. const semesterValues = [];
  62. let defaultIndex = 0;
  63. Array.from(selectElem.querySelectorAll("option")).forEach(option => {
  64. if (!option.value) return;
  65. semesters.push(option.innerText.trim());
  66. semesterValues.push(option.value);
  67. if (option.selected || option.hasAttribute("selected")) defaultIndex = semesters.length - 1;
  68. });
  69. if (semesters.length === 0) return null;
  70. const start = Math.max(0, defaultIndex - 1);
  71. const end = Math.min(semesters.length, defaultIndex + 10);
  72. return {
  73. semesters: semesters.slice(start, end),
  74. semesterValues: semesterValues.slice(start, end),
  75. defaultIndex: defaultIndex - start
  76. };
  77. }
  78. // 导入前提示先登录教务系统
  79. async function promptUserToStart() {
  80. return await window.shiguangBridgePromise.showAlert(
  81. "济宁医学院教务导入",
  82. "请先确保已登录教务系统,再继续导入。",
  83. "我已登录"
  84. );
  85. }
  86. // 选择学期
  87. async function selectSemester(semesterOptions) {
  88. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  89. "选择学期",
  90. JSON.stringify(semesterOptions.semesters),
  91. semesterOptions.defaultIndex
  92. );
  93. if (selectedIndex === null || selectedIndex < 0) return null;
  94. return {
  95. label: semesterOptions.semesters[selectedIndex],
  96. value: semesterOptions.semesterValues[selectedIndex]
  97. };
  98. }
  99. // 拉取课表页 HTML(含学期列表)
  100. async function fetchSchedulePage() {
  101. const response = await fetch("/xsgrkbcx!getXsgrbkList.action", { method: "GET", credentials: "include" });
  102. if (!response.ok) throw new Error(`无法打开课表页面(HTTP ${response.status})`);
  103. return response.text();
  104. }
  105. // 拉取指定学期课表(HTML 内嵌 var kbxx=[...])
  106. async function fetchCourseData(xnxqdm) {
  107. const response = await fetch(
  108. `/xsgrkbcx!xsAllKbList.action?xnxqdm=${encodeURIComponent(xnxqdm)}`,
  109. { method: "GET", credentials: "include" }
  110. );
  111. if (!response.ok) throw new Error(`课表请求失败(HTTP ${response.status})`);
  112. const htmlText = await response.text();
  113. const match = htmlText.match(/var\s+kbxx\s*=\s*(\[[\s\S]*?\]);/);
  114. if (!match) throw new Error("课表数据解析失败,请检查登录状态");
  115. let kbxx;
  116. try {
  117. kbxx = JSON.parse(match[1]);
  118. } catch (error) {
  119. throw new Error(`课表数据解析失败:${error.message}`);
  120. }
  121. return kbxx;
  122. }
  123. // 主流程:提示 → 选学期 → 拉课表 → 保存课程与作息
  124. async function runImportFlow() {
  125. try {
  126. const confirmed = await promptUserToStart();
  127. if (!confirmed) { window.shiguangBridge.showToast("导入已取消"); return; }
  128. const pageHtml = await fetchSchedulePage();
  129. const semesterOptions = extractSemesterOptions(new DOMParser().parseFromString(pageHtml, "text/html"));
  130. if (!semesterOptions) throw new Error("未找到学期列表,请先登录教务系统");
  131. const semester = await selectSemester(semesterOptions);
  132. if (!semester) { window.shiguangBridge.showToast("导入已取消"); return; }
  133. window.shiguangBridge.showToast(`正在获取 ${semester.label} 的课表...`);
  134. const courses = parseCourseList(await fetchCourseData(semester.value));
  135. if (courses.length === 0) {
  136. await window.shiguangBridgePromise.showAlert(
  137. "提示",
  138. "该学期没有获取到课程数据,请检查登录状态和所选学期。",
  139. "确定"
  140. );
  141. return;
  142. }
  143. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  144. try {
  145. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(PRESET_TIME_SLOTS));
  146. } catch (error) {
  147. window.shiguangBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
  148. }
  149. window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
  150. window.shiguangBridge.notifyTaskCompletion();
  151. } catch (error) {
  152. await window.shiguangBridgePromise.showAlert(
  153. "导入失败",
  154. error.message || String(error),
  155. "确定"
  156. );
  157. }
  158. }
  159. runImportFlow();