LNU_01.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  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 readSemesterOptions() {
  178. const select = document.getElementById('planCode');
  179. if (!select || !select.options || select.options.length === 0) {
  180. return null;
  181. }
  182. const texts = [];
  183. const values = [];
  184. for (let i = 0; i < select.options.length; i++) {
  185. texts.push(select.options[i].text.replace(/\s+/g, ' ').trim());
  186. values.push(select.options[i].value);
  187. }
  188. return {
  189. texts: texts,
  190. values: values,
  191. defaultIndex: select.selectedIndex >= 0 ? select.selectedIndex : 0
  192. };
  193. }
  194. async function fetchCourseConfig() {
  195. try {
  196. const res = await fetch("/indexCalendar");
  197. if (!res.ok) return null;
  198. const html = await res.text();
  199. const config = {};
  200. const rqMatch = html.match(/var\s+rq\s*=\s*["'](\d{8})["']/);
  201. if (rqMatch && rqMatch[1]) {
  202. const dateStr = rqMatch[1];
  203. // 转换为 YYYY-MM-DD
  204. config.semesterStartDate = `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`;
  205. }
  206. const skzcMatch = html.match(/var\s+skzc\s*=\s*["'](\d+)["']/);
  207. if (skzcMatch && skzcMatch[1]) {
  208. config.semesterTotalWeeks = parseInt(skzcMatch[1], 10);
  209. }
  210. return Object.keys(config).length > 0 ? config : null;
  211. } catch (e) {
  212. window.shiguangBridge.showToast("获取课程配置失败: " + e.message);
  213. return null;
  214. }
  215. }
  216. async function runImportFlow() {
  217. try {
  218. let method = "GET";
  219. let body = undefined;
  220. let idx = -1;
  221. let options = null;
  222. const apiPath = "/student/courseSelect/thisSemesterCurriculum/ajaxStudentSchedule/callback";
  223. const headers = {
  224. "Accept": "*/*",
  225. "X-Requested-With": "XMLHttpRequest"
  226. };
  227. const confirmed = await window.shiguangBridgePromise.showAlert(
  228. "辽宁大学课表导入",
  229. "请确认已登录教务系统,并处于课表页面。",
  230. "确定"
  231. );
  232. if (!confirmed) return;
  233. options = readSemesterOptions();
  234. if (options) {
  235. idx = await window.shiguangBridgePromise.showSingleSelection(
  236. "选择学期",
  237. JSON.stringify(options.texts),
  238. options.defaultIndex
  239. );
  240. if (idx === null || idx < 0) {
  241. window.shiguangBridge.showToast("导入已取消");
  242. return;
  243. }
  244. }
  245. if (options && idx >= 0) {
  246. method = "POST";
  247. headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8";
  248. headers["Accept"] = "application/json, text/javascript, */*; q=0.01";
  249. body = "planCode=" + encodeURIComponent(options.values[idx]);
  250. }
  251. window.shiguangBridge.showToast("正在获取教务数据...");
  252. const res = await fetch(apiPath, {
  253. method: method,
  254. headers: headers,
  255. body: body,
  256. credentials: "include"
  257. });
  258. if (!res.ok) {
  259. window.shiguangBridge.showToast("网络请求失败,状态码: " + res.status);
  260. return;
  261. }
  262. const data = await res.json();
  263. const courses = parseCourses(data);
  264. if (courses.length === 0) {
  265. window.shiguangBridge.showToast("导入失败: 未解析到课程数据,请确认所选学期有课");
  266. return;
  267. }
  268. const campus = guessCampus(data);
  269. let defaultIndex = TIME_SLOT_OPTION_EXTRACT;
  270. if (campus === CAMPUS_CHONGSHAN) {
  271. defaultIndex = TIME_SLOT_OPTION_CHONGSHAN;
  272. } else if (campus === CAMPUS_PUHE) {
  273. defaultIndex = TIME_SLOT_OPTION_PUHE;
  274. } else if (campus === CAMPUS_WUSHENG) {
  275. defaultIndex = TIME_SLOT_OPTION_WUSHENG;
  276. }
  277. const choice = await window.shiguangBridgePromise.showSingleSelection(
  278. "作息时间来源",
  279. JSON.stringify(TIME_SLOT_OPTIONS),
  280. defaultIndex
  281. );
  282. if (choice === null || choice < 0) {
  283. window.shiguangBridge.showToast("导入已取消");
  284. return;
  285. }
  286. let timeSlots;
  287. if (choice === TIME_SLOT_OPTION_CHONGSHAN) {
  288. timeSlots = CHONGSHAN_TIME_SLOTS;
  289. } else if (choice === TIME_SLOT_OPTION_PUHE) {
  290. timeSlots = PUHE_TIME_SLOTS;
  291. } else if (choice === TIME_SLOT_OPTION_WUSHENG) {
  292. timeSlots = WUSHENG_TIME_SLOTS;
  293. } else {
  294. timeSlots = parseTimeSlots(data);
  295. }
  296. const configData = await fetchCourseConfig();
  297. const saved = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  298. if (!saved) {
  299. window.shiguangBridge.showToast("课程保存失败");
  300. return;
  301. }
  302. try {
  303. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  304. if (configData) {
  305. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(configData));
  306. }
  307. } catch (error) {
  308. window.shiguangBridge.showToast("作息时间或配置保存失败: " + error.message);
  309. }
  310. window.shiguangBridge.showToast("成功导入 " + courses.length + " 个课程时段");
  311. window.shiguangBridge.notifyTaskCompletion();
  312. } catch (e) {
  313. window.shiguangBridge.showToast("导入失败: " + e.message);
  314. }
  315. }
  316. runImportFlow();