hlju_bachelor.js 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. // 黑龙江大学本科选课系统(xsxk.hlju.edu.cn)拾光课表导入适配脚本
  2. // 课表接口:/xszykb/queryxszykbzong(JSON 数组)
  3. // 字段:SKSJ 课程文本、KEY(xqN_jcM 星期_大节)、KSJC/JSJC 起止节次、ZC 周次位图
  4. // 课程文本格式:课程名【001】\n[教师]\n[周次][地点]\n第X-Y节(节次行可能缺失)
  5. // 作息:12 节(接口 /component/queryxqkjsj 获取)
  6. // 黑龙江大学作息时间(2026-2027 第一学期)
  7. const HLJU_BACHELOR_TIME_SLOTS = [
  8. { number: 1, startTime: "08:00", endTime: "08:45" },
  9. { number: 2, startTime: "08:50", endTime: "09:35" },
  10. { number: 3, startTime: "10:00", endTime: "10:45" },
  11. { number: 4, startTime: "10:50", endTime: "11:35" },
  12. { number: 5, startTime: "13:30", endTime: "14:15" },
  13. { number: 6, startTime: "14:20", endTime: "15:05" },
  14. { number: 7, startTime: "15:30", endTime: "16:15" },
  15. { number: 8, startTime: "16:20", endTime: "17:05" },
  16. { number: 9, startTime: "18:30", endTime: "19:15" },
  17. { number: 10, startTime: "19:20", endTime: "20:05" },
  18. { number: 11, startTime: "20:10", endTime: "20:55" },
  19. { number: 12, startTime: "21:00", endTime: "21:45" }
  20. ];
  21. function getErrorMessage(error) {
  22. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  23. if (typeof error === "string" && error.trim()) return error;
  24. try {
  25. const serialized = JSON.stringify(error);
  26. if (serialized && serialized !== "{}") return serialized;
  27. } catch (_) {
  28. // Ignore serialization failures.
  29. }
  30. return "未知错误";
  31. }
  32. // 从 performance 资源中查找课表请求 URL(queryxszykbzong)
  33. function findScheduleRequest() {
  34. const resources = performance.getEntriesByType("resource");
  35. const matches = resources.map(item => item.name).filter(url => url.indexOf("queryxszykbzong") !== -1);
  36. return matches.length ? matches[matches.length - 1] : null;
  37. }
  38. // 获取当前学期(xn=2026-2027&xq=1),从 queryxnxqdata 接口取 SFDQXQ=1 的学期
  39. async function fetchCurrentSemester() {
  40. const response = await fetch("/component/queryxnxqdata", {
  41. method: "POST",
  42. credentials: "include",
  43. headers: {
  44. "Accept": "application/json, text/javascript, */*; q=0.01",
  45. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  46. "X-Requested-With": "XMLHttpRequest"
  47. }
  48. });
  49. if (!response.ok) throw new Error(`学期接口请求失败(HTTP ${response.status})`);
  50. const data = await response.json();
  51. const list = Array.isArray(data) ? data : (Array.isArray(data.content) ? data.content : []);
  52. const current = list.find(item => String(item.SFDQXQ) === "1");
  53. if (!current || !current.XN || !current.XQ) {
  54. throw new Error("未获取到当前学期信息");
  55. }
  56. return { xn: current.XN, xq: String(current.XQ) };
  57. }
  58. async function fetchSchedule() {
  59. const semester = await fetchCurrentSemester();
  60. const url = findScheduleRequest() || "/xszykb/queryxszykbzong";
  61. const response = await fetch(url, {
  62. method: "POST",
  63. credentials: "include",
  64. headers: {
  65. "Accept": "application/json, text/javascript, */*; q=0.01",
  66. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  67. "X-Requested-With": "XMLHttpRequest"
  68. },
  69. body: `xn=${encodeURIComponent(semester.xn)}&xq=${encodeURIComponent(semester.xq)}`
  70. });
  71. if (!response.ok) throw new Error(`课表接口请求失败(HTTP ${response.status})`);
  72. const data = await response.json();
  73. if (!data || typeof data !== "object") throw new Error("课表接口返回格式异常");
  74. const list = Array.isArray(data) ? data : (Array.isArray(data.content) ? data.content : null);
  75. if (!list) throw new Error("课表接口返回格式异常");
  76. return list;
  77. }
  78. // 解析周次文本:支持带/不带方括号
  79. // "1-17周" 或 "[1-17周]" → 1..17
  80. // "1,4-8双,9,12-16双周" → 1,4,6,8,9,12,14,16
  81. function parseWeeksText(weekStr) {
  82. const text = String(weekStr || "").replace(/\s+/g, "").replace(/^\[|\]$/g, "");
  83. if (!text) return [];
  84. const body = text.replace(/周/g, "");
  85. const weeks = [];
  86. const parts = body.split(",");
  87. for (const part of parts) {
  88. const pm = part.match(/^(\d+)(?:-(\d+))?(单|双)?$/);
  89. if (!pm) continue;
  90. const start = Number(pm[1]);
  91. const end = pm[2] ? Number(pm[2]) : start;
  92. const parity = pm[3]; // "单" / "双" / undefined
  93. for (let w = start; w <= end; w++) {
  94. if (parity === "单" && w % 2 === 0) continue;
  95. if (parity === "双" && w % 2 === 1) continue;
  96. weeks.push(w);
  97. }
  98. }
  99. return [...new Set(weeks)].sort((a, b) => a - b);
  100. }
  101. // 解析课程文本:课程名【001】\n[教师]\n[周次][地点]\n第X-Y节(节次行可能缺失)
  102. function parseCourseText(sksj) {
  103. const lines = String(sksj || "").split(/\r?\n/).map(l => l.trim()).filter(l => l);
  104. if (lines.length < 2) return null;
  105. // 考试记录(以【...考试】开头)跳过
  106. if (lines[0].indexOf("考试】") !== -1) return null;
  107. const name = lines[0];
  108. // 教师:第二行固定为 [教师名]
  109. let teacher = "";
  110. if (/^\[[^\]]+\]$/.test(lines[1])) {
  111. teacher = lines[1].slice(1, -1);
  112. }
  113. // 周次与地点:从第 3 行起,含"周"的 [..] 为周次,其后 [..] 为地点
  114. // 地点可能含嵌套方括号(如 [4号楼-[617]计算机5机房分室2]),用平衡括号匹配
  115. let weekStr = "";
  116. let position = "";
  117. for (let i = 2; i < lines.length; i++) {
  118. const line = lines[i];
  119. const m = line.match(/\[([^\]]*周[^\]]*)\]/);
  120. if (m) {
  121. if (!weekStr) weekStr = m[1];
  122. const rest = line.slice(line.indexOf(m[0]) + m[0].length).trim();
  123. if (rest.startsWith("[") && !position) {
  124. position = rest.slice(1, -1);
  125. }
  126. continue;
  127. }
  128. const posM = line.match(/^\[(.+)\]$/);
  129. if (posM && !position && posM[1].indexOf("周") === -1) {
  130. position = posM[1];
  131. }
  132. }
  133. const weeks = parseWeeksText(weekStr);
  134. if (weeks.length === 0) return null;
  135. return { name, teacher: teacher || "未知", position: position || "待定", weeks };
  136. }
  137. function parseSchedule(data) {
  138. const courses = [];
  139. for (const row of data) {
  140. if (!row || typeof row !== "object") continue;
  141. const key = String(row.KEY || "");
  142. // 劳动教育等无位置排布的记录(KEY=bz 或无节次)跳过
  143. if (key === "bz" || !key) continue;
  144. const dayMatch = key.match(/xq(\d+)/);
  145. if (!dayMatch) continue;
  146. const day = Number(dayMatch[1]);
  147. if (!(day >= 1 && day <= 7)) continue;
  148. // 起止节次:KSJC/JSJC 字段优先
  149. const startSection = Number(row.KSJC);
  150. const endSection = Number(row.JSJC);
  151. if (!startSection || !endSection || startSection > endSection) continue;
  152. const parsed = parseCourseText(row.SKSJ);
  153. if (!parsed) continue;
  154. courses.push({
  155. name: parsed.name,
  156. teacher: parsed.teacher,
  157. position: parsed.position,
  158. day,
  159. startSection,
  160. endSection,
  161. weeks: parsed.weeks
  162. });
  163. }
  164. return courses;
  165. }
  166. // 合并同课程同时间同教室的条目
  167. function mergeCourses(courses) {
  168. const merged = new Map();
  169. for (const c of courses) {
  170. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}`;
  171. const holder = merged.get(key);
  172. if (holder) {
  173. holder.weeks = Array.from(new Set([...holder.weeks, ...c.weeks])).sort((a, b) => a - b);
  174. } else {
  175. merged.set(key, { ...c, weeks: [...c.weeks].sort((a, b) => a - b) });
  176. }
  177. }
  178. return Array.from(merged.values()).sort(
  179. (a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name)
  180. );
  181. }
  182. // 保存作息时间
  183. async function saveTimeSlots(timeSlots) {
  184. try {
  185. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  186. } catch (error) {
  187. console.error("JS: 作息时间保存失败", error);
  188. }
  189. }
  190. async function runImportFlow() {
  191. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  192. "课表导入",
  193. "导入前请确保已登录选课系统并打开课表查询页面。",
  194. "好的,开始导入"
  195. );
  196. if (!alertConfirmed) {
  197. window.shiguangBridge.showToast("用户取消了导入。");
  198. return;
  199. }
  200. window.shiguangBridge.showToast("正在获取课表数据...");
  201. try {
  202. const data = await fetchSchedule();
  203. const courses = parseSchedule(data);
  204. if (courses.length === 0) throw new Error("课表中未解析到有效课程,请确认当前学期有课。");
  205. const merged = mergeCourses(courses);
  206. window.shiguangBridge.showToast(`正在保存 ${merged.length} 门课程...`);
  207. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(merged, null, 2));
  208. await saveTimeSlots(HLJU_BACHELOR_TIME_SLOTS);
  209. window.shiguangBridge.showToast(`课程导入成功,共导入 ${merged.length} 门课程!`);
  210. window.shiguangBridge.notifyTaskCompletion();
  211. } catch (error) {
  212. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  213. console.error("JS: Import Error", error);
  214. }
  215. }
  216. runImportFlow();