LNU_01.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. // 辽宁大学程表适配脚本
  2. // 基于 URP 教务系统 JSON 接口适配
  3. // 解析 classWeek 二进制串,如 "111111111111111100000000" -> [1..16]
  4. function parseWeeks(binaryStr) {
  5. const weeks = [];
  6. if (!binaryStr) return weeks;
  7. for (let i = 0; i < binaryStr.length; i++) {
  8. if (binaryStr[i] === '1') weeks.push(i + 1);
  9. }
  10. return weeks;
  11. }
  12. function cleanTeacher(teacher) {
  13. return (teacher || "").replace(/\*/g, " ").replace(/\s+/g, " ").trim();
  14. }
  15. function formatTime(timeStr) {
  16. if (timeStr && timeStr.length === 4) {
  17. return timeStr.substring(0, 2) + ":" + timeStr.substring(2);
  18. }
  19. return timeStr || "";
  20. }
  21. function buildPosition(campus, building, room) {
  22. const campusPart = (campus || "").trim();
  23. let buildingPart = (building || "").trim();
  24. let roomPart = (room || "").trim();
  25. if (campusPart && buildingPart.startsWith(campusPart)) {
  26. buildingPart = buildingPart.slice(campusPart.length);
  27. }
  28. if (campusPart && roomPart.startsWith(campusPart)) {
  29. roomPart = roomPart.slice(campusPart.length);
  30. }
  31. const result = (campusPart + buildingPart + roomPart).trim();
  32. return result || "待定";
  33. }
  34. function parseCourses(data) {
  35. const courses = [];
  36. const seen = new Set();
  37. const list = (data && data.xkxx) || [];
  38. for (let i = 0; i < list.length; i++) {
  39. const map = list[i];
  40. if (!map) continue;
  41. for (const key in map) {
  42. const course = map[key];
  43. if (!course || !course.courseName || !course.timeAndPlaceList) continue;
  44. const teacher = cleanTeacher(course.attendClassTeacher);
  45. for (const timePlace of course.timeAndPlaceList) {
  46. const day = parseInt(timePlace.classDay);
  47. const start = parseInt(timePlace.classSessions);
  48. const continuing = parseInt(timePlace.continuingSession) || 2;
  49. const end = start + continuing - 1;
  50. const weeks = parseWeeks(timePlace.classWeek);
  51. if (!day || !start || weeks.length === 0) continue;
  52. const position = buildPosition(timePlace.campusName, timePlace.teachingBuildingName, timePlace.classroomName);
  53. const dedupKey = course.courseName + '|' + teacher + '|' + day + '|' + start + '|' + end + '|' + weeks.join(',') + '|' + position;
  54. if (seen.has(dedupKey)) continue;
  55. seen.add(dedupKey);
  56. courses.push({
  57. name: course.courseName,
  58. teacher: teacher,
  59. position: position,
  60. day: day,
  61. startSection: start,
  62. endSection: end,
  63. weeks: weeks,
  64. isCustomTime: false
  65. });
  66. }
  67. }
  68. }
  69. return courses;
  70. }
  71. function parseTimeSlots(data) {
  72. const slots = [];
  73. const list = (data && data.jcsjbs) || [];
  74. for (const slot of list) {
  75. const num = parseInt(slot.jc, 10);
  76. if (isNaN(num)) continue;
  77. slots.push({
  78. number: num,
  79. startTime: formatTime(slot.kssj),
  80. endTime: formatTime(slot.jssj)
  81. });
  82. }
  83. return slots.sort((a, b) => a.number - b.number);
  84. }
  85. // 教务系统提供的时间可能把校区搞混,所以提供各校区的作息时间表,供用户选择
  86. const CHONGSHAN_TIME_SLOTS = [
  87. { number: 1, startTime: "08:00", endTime: "08:50" },
  88. { number: 2, startTime: "09:00", endTime: "09:50" },
  89. { number: 3, startTime: "10:10", endTime: "11:00" },
  90. { number: 4, startTime: "11:10", endTime: "12:00" },
  91. { number: 5, startTime: "13:30", endTime: "14:20" },
  92. { number: 6, startTime: "14:30", endTime: "15:20" },
  93. { number: 7, startTime: "15:40", endTime: "16:30" },
  94. { number: 8, startTime: "16:40", endTime: "17:30" },
  95. { number: 9, startTime: "18:00", endTime: "18:50" },
  96. { number: 10, startTime: "19:00", endTime: "19:50" },
  97. { number: 11, startTime: "20:10", endTime: "21:00" },
  98. { number: 12, startTime: "21:10", endTime: "22:00" }
  99. ];
  100. const PUHE_TIME_SLOTS = [
  101. { number: 1, startTime: "08:30", endTime: "09:15" },
  102. { number: 2, startTime: "09:20", endTime: "10:05" },
  103. { number: 3, startTime: "10:25", endTime: "11:10" },
  104. { number: 4, startTime: "11:15", endTime: "12:00" },
  105. { number: 5, startTime: "13:30", endTime: "14:15" },
  106. { number: 6, startTime: "14:20", endTime: "15:05" },
  107. { number: 7, startTime: "15:25", endTime: "16:10" },
  108. { number: 8, startTime: "16:15", endTime: "17:00" },
  109. { number: 9, startTime: "17:30", endTime: "18:15" },
  110. { number: 10, startTime: "18:20", endTime: "19:05" },
  111. { number: 11, startTime: "19:25", endTime: "20:10" },
  112. { number: 12, startTime: "20:15", endTime: "21:00" }
  113. ];
  114. const WUSHENG_TIME_SLOTS = [
  115. { number: 1, startTime: "08:00", endTime: "08:50" },
  116. { number: 2, startTime: "09:00", endTime: "09:50" },
  117. { number: 3, startTime: "10:10", endTime: "11:00" },
  118. { number: 4, startTime: "11:10", endTime: "12:00" },
  119. { number: 5, startTime: "13:30", endTime: "14:20" },
  120. { number: 6, startTime: "14:30", endTime: "15:20" },
  121. { number: 7, startTime: "15:30", endTime: "16:20" },
  122. { number: 8, startTime: "16:30", endTime: "17:20" },
  123. { number: 9, startTime: "18:00", endTime: "18:50" },
  124. { number: 10, startTime: "19:00", endTime: "19:50" },
  125. { number: 11, startTime: "20:10", endTime: "21:00" },
  126. { number: 12, startTime: "21:10", endTime: "22:00" }
  127. ];
  128. const CAMPUS_CHONGSHAN = "崇山";
  129. const CAMPUS_PUHE = "蒲河";
  130. const CAMPUS_WUSHENG = "武圣";
  131. const TIME_SLOT_OPTIONS = [
  132. "从教务提取",
  133. "内置" + CAMPUS_CHONGSHAN + "校区作息",
  134. "内置" + CAMPUS_PUHE + "校区作息",
  135. "内置" + CAMPUS_WUSHENG + "校区作息"
  136. ];
  137. const TIME_SLOT_OPTION_EXTRACT = 0;
  138. const TIME_SLOT_OPTION_CHONGSHAN = 1;
  139. const TIME_SLOT_OPTION_PUHE = 2;
  140. const TIME_SLOT_OPTION_WUSHENG = 3;
  141. // 根据课程数据的 campusName 字段猜测所在校区
  142. function guessCampus(data) {
  143. const counts = {};
  144. const list = (data && data.xkxx) || [];
  145. for (const map of list) {
  146. if (!map) continue;
  147. for (const key in map) {
  148. const course = map[key];
  149. if (!course || !course.timeAndPlaceList) continue;
  150. for (const timePlace of course.timeAndPlaceList) {
  151. const campus = (timePlace.campusName || "").trim();
  152. if (campus) {
  153. counts[campus] = (counts[campus] || 0) + 1;
  154. }
  155. }
  156. }
  157. }
  158. let best = null;
  159. let bestCount = 0;
  160. for (const name in counts) {
  161. if (counts[name] > bestCount) {
  162. best = name;
  163. bestCount = counts[name];
  164. }
  165. }
  166. if (best && best.includes(CAMPUS_CHONGSHAN)) {
  167. return CAMPUS_CHONGSHAN;
  168. }
  169. if (best && best.includes(CAMPUS_PUHE)) {
  170. return CAMPUS_PUHE;
  171. }
  172. if (best && best.includes(CAMPUS_WUSHENG)) {
  173. return CAMPUS_WUSHENG;
  174. }
  175. return null;
  176. }
  177. function computeTotalWeeks(courses) {
  178. let max = 0;
  179. for (const course of courses) {
  180. for (const week of course.weeks) {
  181. if (week > max) {
  182. max = week;
  183. }
  184. }
  185. }
  186. return max > 0 ? max : 20;
  187. }
  188. function readSemesterOptions() {
  189. const select = document.getElementById('planCode');
  190. if (!select || !select.options || select.options.length === 0) {
  191. return null;
  192. }
  193. const texts = [];
  194. const values = [];
  195. for (let i = 0; i < select.options.length; i++) {
  196. texts.push(select.options[i].text.replace(/\s+/g, ' ').trim());
  197. values.push(select.options[i].value);
  198. }
  199. return {
  200. texts: texts,
  201. values: values,
  202. defaultIndex: select.selectedIndex >= 0 ? select.selectedIndex : 0
  203. };
  204. }
  205. async function runImportFlow() {
  206. try {
  207. let method = "GET";
  208. let body = undefined;
  209. let idx = -1;
  210. let options = null;
  211. const apiPath = "/student/courseSelect/thisSemesterCurriculum/ajaxStudentSchedule/callback";
  212. const headers = {
  213. "Accept": "*/*",
  214. "X-Requested-With": "XMLHttpRequest"
  215. };
  216. const confirmed = await window.shiguangBridgePromise.showAlert(
  217. "辽宁大学课表导入",
  218. "请确认已登录教务系统,并处于课表页面。",
  219. "确定"
  220. );
  221. if (!confirmed) return;
  222. options = readSemesterOptions();
  223. if (options) {
  224. idx = await window.shiguangBridgePromise.showSingleSelection(
  225. "选择学期",
  226. JSON.stringify(options.texts),
  227. options.defaultIndex
  228. );
  229. if (idx === null || idx < 0) {
  230. window.shiguangBridge.showToast("导入已取消");
  231. return;
  232. }
  233. }
  234. if (options && idx >= 0) {
  235. method = "POST";
  236. headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
  237. headers["Accept"] = "application/json, text/javascript, */*; q=0.01";
  238. body = "planCode=" + encodeURIComponent(options.values[idx]);
  239. }
  240. window.shiguangBridge.showToast("正在获取教务数据...");
  241. const res = await fetch(apiPath, {
  242. method: method,
  243. headers: headers,
  244. body: body,
  245. credentials: "include"
  246. });
  247. if (!res.ok) {
  248. window.shiguangBridge.showToast("网络请求失败,状态码: " + res.status);
  249. return;
  250. }
  251. const data = await res.json();
  252. const courses = parseCourses(data);
  253. if (courses.length === 0) {
  254. window.shiguangBridge.showToast("导入失败: 未解析到课程数据,请确认所选学期有课");
  255. return;
  256. }
  257. const totalWeeks = computeTotalWeeks(courses);
  258. const campus = guessCampus(data);
  259. let defaultIndex = TIME_SLOT_OPTION_EXTRACT;
  260. if (campus === CAMPUS_CHONGSHAN) {
  261. defaultIndex = TIME_SLOT_OPTION_CHONGSHAN;
  262. } else if (campus === CAMPUS_PUHE) {
  263. defaultIndex = TIME_SLOT_OPTION_PUHE;
  264. } else if (campus === CAMPUS_WUSHENG) {
  265. defaultIndex = TIME_SLOT_OPTION_WUSHENG;
  266. }
  267. const choice = await window.shiguangBridgePromise.showSingleSelection(
  268. "作息时间来源",
  269. JSON.stringify(TIME_SLOT_OPTIONS),
  270. defaultIndex
  271. );
  272. if (choice === null || choice < 0) {
  273. window.shiguangBridge.showToast("导入已取消");
  274. return;
  275. }
  276. let timeSlots;
  277. if (choice === TIME_SLOT_OPTION_CHONGSHAN) {
  278. timeSlots = CHONGSHAN_TIME_SLOTS;
  279. } else if (choice === TIME_SLOT_OPTION_PUHE) {
  280. timeSlots = PUHE_TIME_SLOTS;
  281. } else if (choice === TIME_SLOT_OPTION_WUSHENG) {
  282. timeSlots = WUSHENG_TIME_SLOTS;
  283. } else {
  284. timeSlots = parseTimeSlots(data);
  285. }
  286. const saved = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  287. if (!saved) {
  288. window.shiguangBridge.showToast("课程保存失败");
  289. return;
  290. }
  291. try {
  292. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  293. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({ semesterTotalWeeks: totalWeeks }));
  294. } catch (error) {
  295. window.shiguangBridge.showToast("作息时间或配置保存失败: " + error.message);
  296. }
  297. window.shiguangBridge.showToast("成功导入 " + courses.length + " 个课程时段");
  298. window.shiguangBridge.notifyTaskCompletion();
  299. } catch (e) {
  300. window.shiguangBridge.showToast("导入失败: " + e.message);
  301. }
  302. }
  303. runImportFlow();