lit_01.js 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. // 洛阳理工学院教务(乘方教务)适配器
  2. // 流程:获取学期列表 → 选择学期 → 导入课表与教务作息
  3. // 接口:
  4. // GET /new/student/xsgrkb/week.page 课表页数据(学期下拉 + 作息表,直接请求获取,无需进入课表页面)
  5. // POST /new/student/xsgrkb/getCalendarWeekDatas 整学期课程数据
  6. // 周次字符串
  7. function parseWeeks(weekStr) {
  8. if (!weekStr) return [];
  9. const weeks = weekStr.split(",").map(w => parseInt(w.trim(), 10)).filter(w => !isNaN(w) && w > 0);
  10. return [...new Set(weeks)].sort((a, b) => a - b);
  11. }
  12. // 解析按周场地字符串
  13. function parseVenueWeeks(jxcdmc2) {
  14. const venueMap = new Map();
  15. let lastRoom = null;
  16. String(jxcdmc2 || "").split(",").forEach(part => {
  17. const match = part.trim().match(/^(.*?)-(\d+)$/);
  18. if (!match) return;
  19. const room = match[1].trim();
  20. const week = parseInt(match[2], 10);
  21. if (room) lastRoom = room;
  22. if (!lastRoom || isNaN(week)) return;
  23. if (!venueMap.has(lastRoom)) venueMap.set(lastRoom, []);
  24. venueMap.get(lastRoom).push(week);
  25. });
  26. return venueMap;
  27. }
  28. // 课程地点
  29. function resolvePosition(item) {
  30. const primary = String(item.jxcdmc || "").trim();
  31. if (primary) return primary;
  32. if (String(item.bapjxcd || "") === "1") return "不用场地";
  33. return "待定";
  34. }
  35. function cleanTeacherName(raw) {
  36. return String(raw || "").replace(/\[[^\]]*\]/g, "").trim();
  37. }
  38. // 课表接口数据
  39. function parseCourseList(apiJson, slotMap) {
  40. if (!apiJson) throw new Error("课表接口无响应");
  41. if (apiJson.code !== 0) {
  42. const message = String(apiJson.message || "").trim();
  43. throw new Error(message || `课表接口返回错误(code=${apiJson.code})`);
  44. }
  45. if (!Array.isArray(apiJson.data)) throw new Error("课表接口返回格式不正确");
  46. const courseMap = new Map();
  47. apiJson.data.forEach(item => {
  48. const day = parseInt(item.xq, 10);
  49. const startSection = parseInt(item.ps, 10);
  50. const endSection = parseInt(item.pe, 10);
  51. const allWeeks = parseWeeks(item.zc);
  52. if (!item.kcmc || !allWeeks.length || isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
  53. day < 1 || day > 7 || startSection > endSection) return;
  54. const teacher = cleanTeacherName(item.teaxms || item.pkr) || "未知";
  55. const venues = parseVenueWeeks(item.jxcdmc2);
  56. const venueEntries = venues.size > 0
  57. ? Array.from(venues.entries(), ([position, weeks]) => ({ position, weeks: [...new Set(weeks)].sort((a, b) => a - b) }))
  58. : [{ position: resolvePosition(item), weeks: allWeeks }];
  59. venueEntries.forEach(({ position, weeks }) => {
  60. const course = { name: item.kcmc.trim(), teacher, position, day, startSection, endSection, weeks };
  61. const actualStart = String(item.qssj || "").slice(0, 5);
  62. const actualEnd = String(item.jssj || "").slice(0, 5);
  63. const expectedStart = slotMap[startSection] && slotMap[startSection].start;
  64. const expectedEnd = slotMap[endSection] && slotMap[endSection].end;
  65. if (actualStart && actualEnd && (actualStart !== expectedStart || actualEnd !== expectedEnd)) {
  66. course.isCustomTime = true;
  67. course.customStartTime = actualStart;
  68. course.customEndTime = actualEnd;
  69. }
  70. const key = [course.name, teacher, position, day,
  71. course.isCustomTime ? actualStart + actualEnd : `${startSection}-${endSection}`].join("__");
  72. const existing = courseMap.get(key);
  73. if (existing) {
  74. existing.weeks = [...new Set(existing.weeks.concat(weeks))].sort((a, b) => a - b);
  75. } else {
  76. courseMap.set(key, course);
  77. }
  78. });
  79. });
  80. return Array.from(courseMap.values()).sort((a, b) =>
  81. a.day - b.day || a.startSection - b.startSection || a.endSection - b.endSection || a.name.localeCompare(b.name)
  82. );
  83. }
  84. // 从 week.page 源码提取作息表
  85. function parseBusinessHoursFromHtml(htmlText) {
  86. const match = htmlText.match(/var\s+businessHours\s*=\s*\$\.parseJSON\('(\[.*?\])'\);/);
  87. const slots = [];
  88. const map = {};
  89. if (match) {
  90. JSON.parse(match[1]).forEach(item => {
  91. const number = parseInt(item.jcdm, 10);
  92. const startTime = String(item.qssj || "").slice(0, 5);
  93. const endTime = String(item.jssj || "").slice(0, 5);
  94. if (isNaN(number) || !startTime || !endTime) return;
  95. slots.push({ number, startTime, endTime });
  96. map[number] = { start: startTime, end: endTime };
  97. });
  98. slots.sort((a, b) => a.number - b.number);
  99. }
  100. return { slots, map };
  101. }
  102. // 读取页面中的学期下拉框
  103. function extractSemesterOptions(doc) {
  104. const selectElem = doc.getElementById("xnxqdm");
  105. if (!selectElem) return null;
  106. const semesters = [];
  107. const semesterValues = [];
  108. let defaultIndex = 0;
  109. Array.from(selectElem.querySelectorAll("option")).forEach(option => {
  110. if (!option.value) return;
  111. semesters.push(option.innerText.trim());
  112. semesterValues.push(option.value);
  113. if (option.selected || option.hasAttribute("selected")) defaultIndex = semesters.length - 1;
  114. });
  115. if (semesters.length === 0) return null;
  116. const start = Math.max(0, defaultIndex - 1);
  117. const end = Math.min(semesters.length, defaultIndex + 10);
  118. return {
  119. semesters: semesters.slice(start, end),
  120. semesterValues: semesterValues.slice(start, end),
  121. defaultIndex: defaultIndex - start
  122. };
  123. }
  124. // 导入前提示用户先登录教务系统
  125. async function promptUserToStart() {
  126. return await window.shiguangBridgePromise.showAlert(
  127. "洛阳理工学院教务导入",
  128. "请先确保已登录教务系统,再继续导入。",
  129. "我已登录"
  130. );
  131. }
  132. // 从页面已有学期中选择目标学期
  133. async function selectSemester(semesterOptions) {
  134. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  135. "选择学期",
  136. JSON.stringify(semesterOptions.semesters),
  137. semesterOptions.defaultIndex
  138. );
  139. if (selectedIndex === null || selectedIndex < 0) return null;
  140. return {
  141. label: semesterOptions.semesters[selectedIndex],
  142. value: semesterOptions.semesterValues[selectedIndex]
  143. };
  144. }
  145. // 直接获取课表页数据(学期下拉与作息表)。乘方教务系统的学期列表由服务端渲染,
  146. // 不提供返回学期列表的 JSON 接口,因此直接请求 week.page 即可,无需用户进入课表页面。
  147. async function fetchSchedulePage() {
  148. const response = await fetch("/new/student/xsgrkb/week.page", { method: "GET", credentials: "include" });
  149. if (!response.ok) throw new Error(`无法获取课表数据(HTTP ${response.status})`);
  150. return response.text();
  151. }
  152. // 乘方统一表单 POST,附带 JSON 请求头与会话
  153. async function postForm(url, formData) {
  154. const response = await fetch(url, {
  155. method: "POST",
  156. headers: {
  157. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  158. "X-Requested-With": "XMLHttpRequest"
  159. },
  160. credentials: "include",
  161. body: formData.toString()
  162. });
  163. if (!response.ok) throw new Error(`请求失败(HTTP ${response.status})`);
  164. return response;
  165. }
  166. // 请求指定学期的课程数据
  167. async function fetchCourseData(xnxqdm) {
  168. const year = parseInt(xnxqdm.slice(0, 4), 10);
  169. const formData = new URLSearchParams();
  170. formData.append("xnxqdm", xnxqdm);
  171. formData.append("zc", "");
  172. formData.append("d1", `${year}-08-01 00:00:00`);
  173. formData.append("d2", `${year + 1}-08-31 23:59:59`);
  174. return (await postForm("/new/student/xsgrkb/getCalendarWeekDatas", formData)).json();
  175. }
  176. async function saveCourses(courses) {
  177. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  178. }
  179. async function saveTimeSlots(timeSlots) {
  180. if (timeSlots.length === 0) return;
  181. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  182. }
  183. // 编排导入流程:提示 → 选学期 → 请求课表 → 保存课程与作息时间
  184. async function runImportFlow() {
  185. try {
  186. const confirmed = await promptUserToStart();
  187. if (!confirmed) { window.shiguangBridge.showToast("导入已取消"); return; }
  188. const pageHtml = await fetchSchedulePage();
  189. const semesterOptions = extractSemesterOptions(new DOMParser().parseFromString(pageHtml, "text/html"));
  190. if (!semesterOptions) throw new Error("未找到学期列表,请先登录教务系统");
  191. const semester = await selectSemester(semesterOptions);
  192. if (!semester) { window.shiguangBridge.showToast("导入已取消"); return; }
  193. const { slots, map: slotMap } = parseBusinessHoursFromHtml(pageHtml);
  194. window.shiguangBridge.showToast(`正在获取 ${semester.label} 的课表...`);
  195. const courses = parseCourseList(await fetchCourseData(semester.value), slotMap);
  196. if (courses.length === 0) {
  197. await window.shiguangBridgePromise.showAlert(
  198. "提示",
  199. "该学期没有获取到课程数据,请检查登录状态和所选学期。",
  200. "确定"
  201. );
  202. return;
  203. }
  204. await saveCourses(courses);
  205. try {
  206. await saveTimeSlots(slots);
  207. } catch (error) {
  208. window.shiguangBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
  209. }
  210. window.shiguangBridge.showToast("导入完成");
  211. window.shiguangBridge.notifyTaskCompletion();
  212. } catch (error) {
  213. await window.shiguangBridgePromise.showAlert(
  214. "导入失败",
  215. error.message || String(error),
  216. "确定"
  217. );
  218. }
  219. }
  220. runImportFlow();