hrbeu.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. // 哈尔滨工程大学(hrbeu.edu.cn) 本科课表适配脚本
  2. // 金智教务(Wisedu EMAP)平台,参考官方金智适配案例
  3. // 支持:学期选择、实验课、调课/停课、API作息时间、开学日期
  4. var HEU_API = {
  5. semesterList: "/jwapp/sys/wdkb/modules/jshkcb/xnxqcx.do",
  6. currentTerm: "/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do",
  7. course: "/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do",
  8. experiment: "/jwapp/sys/wdkb/modules/syjxkcb/cxsyjxxskb.do",
  9. change: "/jwapp/sys/wdkb/modules/xskcb/xsdkkc.do",
  10. timeSlots: "/jwapp/sys/wdkb/modules/jshkcb/jc.do",
  11. config: "/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do"
  12. };
  13. var DEFAULT_TIME_SLOTS = [
  14. { number: 1, startTime: "08:00", endTime: "08:45" },
  15. { number: 2, startTime: "08:50", endTime: "09:35" },
  16. { number: 3, startTime: "09:55", endTime: "10:40" },
  17. { number: 4, startTime: "10:45", endTime: "11:30" },
  18. { number: 5, startTime: "11:35", endTime: "12:20" },
  19. { number: 6, startTime: "13:30", endTime: "14:15" },
  20. { number: 7, startTime: "14:20", endTime: "15:05" },
  21. { number: 8, startTime: "15:25", endTime: "16:10" },
  22. { number: 9, startTime: "16:15", endTime: "17:00" },
  23. { number: 10, startTime: "17:05", endTime: "17:50" },
  24. { number: 11, startTime: "18:30", endTime: "19:15" },
  25. { number: 12, startTime: "19:20", endTime: "20:05" },
  26. { number: 13, startTime: "20:10", endTime: "20:55" }
  27. ];
  28. var HEADERS = {
  29. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  30. "x-requested-with": "XMLHttpRequest"
  31. };
  32. async function postApi(url, body) {
  33. var options = { headers: HEADERS, method: "POST", credentials: "include" };
  34. if (body) options.body = body;
  35. var resp = await fetch(url, options);
  36. if (!resp.ok) throw new Error("请求失败: " + resp.status);
  37. return await resp.json();
  38. }
  39. function getRows(data, key) {
  40. return (data && data.datas && data.datas[key] && Array.isArray(data.datas[key].rows))
  41. ? data.datas[key].rows : [];
  42. }
  43. function parseWeeksBitmap(skzc) {
  44. var weeks = [];
  45. var bits = String(skzc || "");
  46. for (var i = 0; i < bits.length; i++) {
  47. if (bits[i] === "1") weeks.push(i + 1);
  48. }
  49. return weeks;
  50. }
  51. function parseSingleCourse(raw) {
  52. var isExperiment = raw.SYXMMC !== undefined && raw.SYXMMC !== null;
  53. var name = isExperiment ? raw.KCM + " - " + raw.SYXMMC : raw.KCM;
  54. if (!isExperiment && raw.TYXMDM_DISPLAY) name += "(" + raw.TYXMDM_DISPLAY + ")";
  55. var day = parseInt(raw.SKXQ);
  56. var startSection = parseInt(raw.KSJC);
  57. var endSection = parseInt(raw.JSJC);
  58. var weeks = parseWeeksBitmap(raw.SKZC);
  59. if (!name || !day || !startSection || !endSection || weeks.length === 0) return null;
  60. return {
  61. name: name,
  62. teacher: raw.SKJS ? raw.SKJS.split("/")[0] : (raw.JSM || ""),
  63. position: raw.JASMC || "待定",
  64. day: day,
  65. startSection: startSection,
  66. endSection: endSection,
  67. weeks: weeks,
  68. _kbId: raw.KBID,
  69. _day: day,
  70. _startSection: startSection,
  71. _endSection: endSection
  72. };
  73. }
  74. function applyCourseChanges(courses, changes) {
  75. var count = 0;
  76. for (var i = 0; i < changes.length; i++) {
  77. var ch = changes[i];
  78. var kbId = ch.KBID;
  79. var weeksToRemove = parseWeeksBitmap(ch.SKZC);
  80. var applied = false;
  81. var affected = courses.filter(function(c) {
  82. return c._kbId === kbId &&
  83. c._day === parseInt(ch.SKXQ) &&
  84. c._startSection === parseInt(ch.KSJC) &&
  85. c._endSection === parseInt(ch.JSJC);
  86. });
  87. if (affected.length === 0) continue;
  88. if (weeksToRemove.length > 0) {
  89. affected.forEach(function(c) {
  90. var before = c.weeks.length;
  91. c.weeks = c.weeks.filter(function(w) { return weeksToRemove.indexOf(w) === -1; });
  92. if (c.weeks.length < before) applied = true;
  93. });
  94. }
  95. var isTimeChange = ch.TKLXDM === "01" || ch.TKLXDM === "03";
  96. if (isTimeChange && ch.XSKZC && ch.XSKXQ && ch.XKSJC && ch.XJSJC) {
  97. var newWeeks = parseWeeksBitmap(ch.XSKZC);
  98. if (newWeeks.length > 0) {
  99. var origTeacher = ch.YSKJS ? ch.YSKJS.split("/")[0] : "";
  100. courses.push({
  101. name: ch.KCM,
  102. teacher: ch.XSKJS ? ch.XSKJS.split("/")[0] : origTeacher,
  103. position: ch.XJASMC || ch.JASMC || "待定",
  104. day: parseInt(ch.XSKXQ),
  105. startSection: parseInt(ch.XKSJC),
  106. endSection: parseInt(ch.XJSJC),
  107. weeks: newWeeks,
  108. _kbId: kbId,
  109. _day: parseInt(ch.XSKXQ),
  110. _startSection: parseInt(ch.XKSJC),
  111. _endSection: parseInt(ch.XJSJC)
  112. });
  113. applied = true;
  114. }
  115. }
  116. if (applied) count++;
  117. }
  118. if (count > 0) window.shiguangBridge.showToast("已应用 " + count + " 条调课/停课变更。");
  119. return courses;
  120. }
  121. function cleanAndMerge(courses) {
  122. var list = courses.map(function(c) {
  123. return {
  124. name: c.name || "",
  125. teacher: c.teacher || "",
  126. position: c.position || "",
  127. day: c.day,
  128. startSection: c.startSection,
  129. endSection: c.endSection,
  130. weeks: Array.isArray(c.weeks) ? c.weeks.slice().sort(function(a,b){return a-b;}) : []
  131. };
  132. }).filter(function(c) { return c.weeks.length > 0; });
  133. // 阶段1: 合并连续节次
  134. list.sort(function(a, b) {
  135. return a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
  136. a.position.localeCompare(b.position) || (a.day||0)-(b.day||0) ||
  137. a.weeks.join(",").localeCompare(b.weeks.join(",")) ||
  138. (a.startSection||0)-(b.startSection||0);
  139. });
  140. var step1 = [];
  141. var cur = list[0];
  142. for (var i = 1; i < list.length; i++) {
  143. var nxt = list[i];
  144. var same = cur.name === nxt.name && cur.teacher === nxt.teacher &&
  145. cur.position === nxt.position && cur.day === nxt.day &&
  146. cur.weeks.join(",") === nxt.weeks.join(",");
  147. if (same && cur.endSection + 1 === nxt.startSection) {
  148. cur.endSection = nxt.endSection;
  149. } else if (same && cur.startSection === nxt.startSection && cur.endSection === nxt.endSection) {
  150. continue;
  151. } else {
  152. step1.push(cur);
  153. cur = nxt;
  154. }
  155. }
  156. step1.push(cur);
  157. // 阶段2: 合并同节次的周次
  158. step1.sort(function(a, b) {
  159. return a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
  160. a.position.localeCompare(b.position) || (a.day||0)-(b.day||0) ||
  161. (a.startSection||0)-(b.startSection||0) || (a.endSection||0)-(b.endSection||0);
  162. });
  163. var step2 = [];
  164. cur = step1[0];
  165. for (var j = 1; j < step1.length; j++) {
  166. var n = step1[j];
  167. if (cur.name === n.name && cur.teacher === n.teacher && cur.position === n.position &&
  168. cur.day === n.day && cur.startSection === n.startSection && cur.endSection === n.endSection) {
  169. var set = {};
  170. cur.weeks.concat(n.weeks).forEach(function(w) { set[w] = true; });
  171. cur.weeks = Object.keys(set).map(Number).sort(function(a,b){return a-b;});
  172. } else {
  173. step2.push(cur);
  174. cur = n;
  175. }
  176. }
  177. step2.push(cur);
  178. return step2;
  179. }
  180. async function selectSemester() {
  181. var semesterList = [];
  182. try {
  183. var data = await postApi(HEU_API.semesterList, "*order=-DM");
  184. semesterList = getRows(data, "xnxqcx");
  185. } catch (e) {
  186. window.shiguangBridge.showToast("获取学期列表失败,请检查登录状态。");
  187. return null;
  188. }
  189. if (semesterList.length === 0) {
  190. window.shiguangBridge.showToast("未查询到学期数据。");
  191. return null;
  192. }
  193. var defaultDM = null;
  194. try {
  195. var dqData = await postApi(HEU_API.currentTerm);
  196. var dqRows = getRows(dqData, "dqxnxq");
  197. if (dqRows.length > 0) defaultDM = dqRows[0].DM;
  198. } catch (e) {}
  199. var top = semesterList.slice(0, 10);
  200. var names = top.map(function(s) { return s.MC || s.DM; });
  201. var defaultIdx = -1;
  202. if (defaultDM) {
  203. for (var i = 0; i < top.length; i++) {
  204. if (top[i].DM === defaultDM) { defaultIdx = i; break; }
  205. }
  206. }
  207. var idx = await window.shiguangBridgePromise.showSingleSelection(
  208. "请选择学期", JSON.stringify(names), defaultIdx
  209. );
  210. if (idx === null || idx === -1) return null;
  211. return top[idx];
  212. }
  213. async function importTimeSlots() {
  214. var slots = null;
  215. try {
  216. var data = await postApi(HEU_API.timeSlots);
  217. var rows = getRows(data, "jc");
  218. if (rows.length > 0) {
  219. slots = rows.map(function(r) {
  220. return { number: parseInt(r.DM), startTime: r.KSSJ, endTime: r.JSSJ };
  221. });
  222. }
  223. } catch (e) {}
  224. if (!slots || slots.length === 0) {
  225. slots = DEFAULT_TIME_SLOTS;
  226. }
  227. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
  228. }
  229. async function fetchSemesterConfig(xn, xq) {
  230. try {
  231. var data = await postApi(HEU_API.config, "XN=" + xn + "&XQ=" + xq);
  232. var row = getRows(data, "cxjcs")[0];
  233. if (row) {
  234. var rawDate = row.XQKSRQ;
  235. return {
  236. semesterStartDate: rawDate ? rawDate.split(" ")[0] : null,
  237. semesterTotalWeeks: parseInt(row.ZZC) || 20
  238. };
  239. }
  240. } catch (e) {}
  241. return { semesterStartDate: null, semesterTotalWeeks: 20 };
  242. }
  243. async function runImportFlow() {
  244. var confirmed = await window.shiguangBridgePromise.showAlert(
  245. "哈尔滨工程大学课表导入",
  246. "导入前请确保已登录教务系统。",
  247. "好的,开始导入"
  248. );
  249. if (!confirmed) {
  250. window.shiguangBridge.showToast("已取消导入");
  251. return;
  252. }
  253. var semester = await selectSemester();
  254. if (!semester) {
  255. window.shiguangBridge.showToast("导入已取消。");
  256. return;
  257. }
  258. var XNXQDM = semester.DM;
  259. window.shiguangBridge.showToast("正在获取作息时间...");
  260. await importTimeSlots();
  261. window.shiguangBridge.showToast("正在获取课表数据...");
  262. var courseData = await postApi(HEU_API.course, "XNXQDM=" + XNXQDM);
  263. var rawCourses = getRows(courseData, "cxxszhxqkb");
  264. // 获取实验课
  265. var rawExpCourses = [];
  266. try {
  267. var expData = await postApi(HEU_API.experiment, "XNXQDM=" + XNXQDM);
  268. rawExpCourses = getRows(expData, "cxsyjxxskb");
  269. } catch (e) {
  270. console.warn("HEU: 获取实验课失败:", e);
  271. }
  272. var allRaw = rawCourses.concat(rawExpCourses);
  273. if (allRaw.length === 0) {
  274. await window.shiguangBridgePromise.showAlert("导入提示", "该学期未查到课程数据。", "知道了");
  275. return;
  276. }
  277. if (rawExpCourses.length > 0) {
  278. window.shiguangBridge.showToast("获取到 " + rawCourses.length + " 门理论课和 " + rawExpCourses.length + " 门实验课");
  279. }
  280. var parsed = allRaw.map(parseSingleCourse).filter(function(c) { return c !== null; });
  281. // 获取调课数据
  282. try {
  283. var chData = await postApi(HEU_API.change, "XNXQDM=" + XNXQDM + "&*order=-SQSJ");
  284. var rawChanges = getRows(chData, "xsdkkc");
  285. if (rawChanges.length > 0) {
  286. parsed = applyCourseChanges(parsed, rawChanges);
  287. }
  288. } catch (e) {
  289. console.warn("HEU: 获取调课数据失败:", e);
  290. }
  291. var courses = cleanAndMerge(parsed);
  292. if (courses.length === 0) {
  293. window.shiguangBridge.showToast("课表解析结果为空");
  294. return;
  295. }
  296. var config = await fetchSemesterConfig(semester.XNDM, semester.XQDM);
  297. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  298. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  299. window.shiguangBridge.showToast(XNXQDM + " 课表导入成功,共 " + courses.length + " 门课程");
  300. window.shiguangBridge.notifyTaskCompletion();
  301. }
  302. (async function() {
  303. try {
  304. await runImportFlow();
  305. } catch (error) {
  306. console.error("HEU课表导入失败:", error);
  307. if (window.shiguangBridgePromise) {
  308. await window.shiguangBridgePromise.showAlert(
  309. "导入失败",
  310. "错误: " + error.message + "\n\n请确认:\n1. 已登录教务系统\n2. 处于校园网或VPN环境",
  311. "确定"
  312. );
  313. }
  314. }
  315. })();