njnu.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. // 南京师范大学(njnu.edu.cn)拾光课程表适配脚本
  2. // 由本校开发者适配,可联系开发者修改
  3. // 出现问题请提联系开发者或者提交pr更改,这更加快速
  4. // 核心工具函数:数据验证
  5. function validateYearInput(input) {
  6. if (/^[0-9]{4}$/.test(input) && parseInt(input) > 2000) {
  7. return false;
  8. } else {
  9. return "请输入有效的四位数字学年(例如:2025)!";
  10. }
  11. }
  12. /**
  13. * 辅助函数:解析周次字符串 "111000..." 为数字数组 [1, 2, 3]
  14. */
  15. function parseWeeksFromSkzc(skzc) {
  16. const weeks = [];
  17. const rawSkzc = skzc || '';
  18. for (let i = 0; i < rawSkzc.length; i++) {
  19. if (rawSkzc[i] === '1') {
  20. weeks.push(Number(i + 1));
  21. }
  22. }
  23. return weeks;
  24. }
  25. /**
  26. * 将教务系统的课程数据转换成 CourseJsonModel 结构
  27. */
  28. function parseSingleCourse(rawCourse) {
  29. const courseName = rawCourse.KCM;
  30. const teacherName = rawCourse.SKJS ? rawCourse.SKJS.split('/')[0] : '';
  31. const position = rawCourse.JASMC;
  32. const day = rawCourse.SKXQ;
  33. const startSection = rawCourse.KSJC;
  34. const endSection = rawCourse.JSJC;
  35. const weeks = parseWeeksFromSkzc(rawCourse.SKZC);
  36. if (!courseName || !day || !startSection || !endSection || weeks.length === 0) {
  37. return null;
  38. }
  39. const course = {
  40. "name": courseName,
  41. "teacher": teacherName,
  42. "position": position || '待定',
  43. "day": parseInt(day),
  44. "startSection": parseInt(startSection),
  45. "endSection": parseInt(endSection),
  46. "weeks": weeks
  47. };
  48. course._kbId = rawCourse.KBID;
  49. course._day = course.day;
  50. course._startSection = course.startSection;
  51. course._endSection = course.endSection;
  52. return course;
  53. }
  54. /**
  55. * 获取学期开始日期函数
  56. */
  57. async function fetchSemesterStartDate(academicYear, semesterCode) {
  58. try {
  59. const response = await fetch("http://ehallapp.nnu.edu.cn/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do", {
  60. method: "POST",
  61. headers: {
  62. "accept": "application/json, text/javascript, */*; q=0.01",
  63. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  64. "x-requested-with": "XMLHttpRequest"
  65. },
  66. body: `XN=${academicYear}-${parseInt(academicYear) + 1}&XQ=${semesterCode}`,
  67. credentials: "include"
  68. });
  69. if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
  70. const data = await response.json();
  71. // 安全检查:确保 rows 存在
  72. if (!data?.datas?.cxjcs?.rows || data.datas.cxjcs.rows.length === 0) {
  73. return null;
  74. }
  75. const xqksrq = data.datas.cxjcs.rows[0].XQKSRQ;
  76. // 本校获取日期格式通常为 "2025-09-01 00:00:00"的形式
  77. // 直接切割字符串,不使用 new Date() 避免时区导致的日期减一
  78. const formattedDate = xqksrq.split(' ')[0];
  79. console.log('确认学期开始日期:', formattedDate);
  80. return formattedDate;
  81. } catch (error) {
  82. console.error('获取日期失败:', error);
  83. AndroidBridge.showToast("获取错误,需要手动设置开学日期");
  84. return null;
  85. }
  86. }
  87. /**
  88. * 将调课数据应用到已解析的课程列表上
  89. */
  90. function applyCourseChanges(parsedCourses, rawChanges) {
  91. let successCount = 0;
  92. for (const change of rawChanges) {
  93. const kbID = change.KBID;
  94. const originalTeacher = change.YSKJS ? change.YSKJS.split('/')[0] : '';
  95. const weeksToRemove = parseWeeksFromSkzc(change.SKZC);
  96. let changeApplied = false;
  97. const affectedOriginalCourses = parsedCourses.filter(c =>
  98. c._kbId === kbID &&
  99. c._day === parseInt(change.SKXQ) &&
  100. c._startSection === parseInt(change.KSJC) &&
  101. c._endSection === parseInt(change.JSJC)
  102. );
  103. if (affectedOriginalCourses.length === 0) {
  104. continue;
  105. }
  106. if (weeksToRemove.length > 0) {
  107. affectedOriginalCourses.forEach(originalCourse => {
  108. const beforeLength = originalCourse.weeks.length;
  109. originalCourse.weeks = originalCourse.weeks.filter(w => !weeksToRemove.includes(w));
  110. if (originalCourse.weeks.length < beforeLength) {
  111. changeApplied = true;
  112. }
  113. });
  114. }
  115. const isTimeLocationChange = (change.TKLXDM === '01' || change.TKLXDM === '03');
  116. if (isTimeLocationChange && change.XSKZC && change.XSKXQ && change.XKSJC && change.XJSJC) {
  117. const newWeeks = parseWeeksFromSkzc(change.XSKZC);
  118. if (newWeeks.length > 0) {
  119. const newCourse = {
  120. "name": change.KCM,
  121. "teacher": change.XSKJS ? change.XSKJS.split('/')[0] : originalTeacher,
  122. "position": change.XJASMC || change.JASMC || '待定',
  123. "day": parseInt(change.XSKXQ),
  124. "startSection": parseInt(change.XKSJC),
  125. "endSection": parseInt(change.XJSJC),
  126. "weeks": newWeeks,
  127. "_kbId": kbID,
  128. "_day": parseInt(change.XSKXQ),
  129. "_startSection": parseInt(change.XKSJC),
  130. "_endSection": parseInt(change.XJSJC)
  131. };
  132. parsedCourses.push(newCourse);
  133. changeApplied = true;
  134. }
  135. }
  136. if (changeApplied) {
  137. successCount++;
  138. }
  139. }
  140. if (successCount > 0) {
  141. AndroidBridge.showToast(`已应用 ${successCount} 条调课/停课变更,获得实际课表。`);
  142. }
  143. return parsedCourses.map(c => {
  144. delete c._kbId;
  145. delete c._day;
  146. delete c._startSection;
  147. delete c._endSection;
  148. return c;
  149. }).filter(c => c.weeks.length > 0);
  150. }
  151. async function promptUserToStart() {
  152. const confirmed = await window.AndroidBridgePromise.showAlert(
  153. "重要通知:南京师范大学课表导入",
  154. "本流程将通过教务系统接口获取您的个人课表。\n重要提示:\n导入前请确保您已在浏览器中成功登录教务系统,且未关闭登录窗口,确认当前页面有显示你想要获取的学期的课表,不然获取不了数据",
  155. "好的,开始导入"
  156. );
  157. if (!confirmed) {
  158. AndroidBridge.showToast("用户取消了导入。");
  159. return null;
  160. }
  161. return true;
  162. }
  163. async function getAcademicYear() {
  164. const currentYear = new Date().getFullYear();
  165. const yearSelection = await window.AndroidBridgePromise.showPrompt(
  166. "选择学年",
  167. "请输入要导入课程的学年(例如 2025-2026学年,无论你是上学期还是下学期,都请输入2025哦):",
  168. String(currentYear),
  169. "validateYearInput"
  170. );
  171. return yearSelection;
  172. }
  173. async function selectSemester() {
  174. const semesters = ["1 (秋季学期/上学期)", "2 (春季学期/下学期)"];
  175. const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
  176. "选择学期",
  177. JSON.stringify(semesters),
  178. 0
  179. );
  180. if (semesterIndex === null) return null;
  181. return String(semesterIndex + 1);
  182. }
  183. // 数据获取和解析部分
  184. async function fetchAndParseCourses(academicYear, semesterCode) {
  185. const XNXQDM = `${academicYear}-${parseInt(academicYear) + 1}-${semesterCode}`;
  186. const headers = {
  187. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  188. "x-requested-with": "XMLHttpRequest",
  189. };
  190. // 获取个人课表数据
  191. const courseUrl = "http://ehallapp.nnu.edu.cn/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do";
  192. const courseBody = `XNXQDM=${XNXQDM}&`;
  193. let rawCourseData;
  194. try {
  195. const response = await fetch(courseUrl, { "headers": headers, "body": courseBody, "method": "POST", "credentials": "include" });
  196. rawCourseData = JSON.parse(await response.text());
  197. } catch (e) {
  198. AndroidBridge.showToast("请求课表 API 失败,请检查网络和登录状态,以及是否跳转到课表页面");
  199. console.error("Fetch Course Error:", e);
  200. return null;
  201. }
  202. const rawCourses = rawCourseData?.datas?.cxxszhxqkb?.rows || [];
  203. if (rawCourses.length === 0) {
  204. AndroidBridge.showToast("该学期未查询到您的课程数据。");
  205. return null;
  206. }
  207. let parsedCourses = rawCourses.map(c => parseSingleCourse(c)).filter(c => c !== null);
  208. const changeUrl = "http://ehallapp.nnu.edu.cn/jwapp/sys/wdkb/modules/xskcb/xsdkkc.do";
  209. const changeBody = `XNXQDM=${XNXQDM}&*order=-SQSJ`;
  210. let rawChangeData;
  211. try {
  212. const response = await fetch(changeUrl, { "headers": headers, "body": changeBody, "method": "POST", "credentials": "include" });
  213. rawChangeData = JSON.parse(await response.text());
  214. } catch (e) {
  215. AndroidBridge.showToast("请求调课 API 失败,将使用未调整的课表数据。");
  216. console.error("Fetch Change Error:", e);
  217. }
  218. const rawChanges = rawChangeData?.datas?.xsdkkc?.rows || [];
  219. // 应用调课变更
  220. if (rawChanges.length > 0) {
  221. parsedCourses = applyCourseChanges(parsedCourses, rawChanges);
  222. }
  223. // 课表配置数据
  224. const semesterStartDate = await fetchSemesterStartDate(academicYear, semesterCode);
  225. const courseConfig = {
  226. semesterTotalWeeks: 20,
  227. semesterStartDate: semesterStartDate
  228. };
  229. return {
  230. courses: parsedCourses,
  231. config: courseConfig
  232. };
  233. }
  234. async function saveCourses(parsedCourses) {
  235. if (parsedCourses.length === 0) {
  236. AndroidBridge.showToast("没有有效的课程数据可供保存。");
  237. return true;
  238. }
  239. try {
  240. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  241. AndroidBridge.showToast(`成功导入 ${parsedCourses.length} 门课程!`);
  242. return true;
  243. } catch (error) {
  244. AndroidBridge.showToast(`保存课程数据失败: ${error.message}`);
  245. return false;
  246. }
  247. }
  248. /**
  249. * 导入预设时间段数据
  250. */
  251. async function importPresetTimeSlots() {
  252. AndroidBridge.showToast("正在导入预设节次时间...");
  253. const presetTimeSlots = [
  254. { "number": 1, "startTime": "08:00", "endTime": "08:40" },
  255. { "number": 2, "startTime": "08:45", "endTime": "09:25" },
  256. { "number": 3, "startTime": "09:40", "endTime": "10:20" },
  257. { "number": 4, "startTime": "10:35", "endTime": "11:15" },
  258. { "number": 5, "startTime": "11:20", "endTime": "12:00" },
  259. { "number": 6, "startTime": "13:30", "endTime": "14:10" },
  260. { "number": 7, "startTime": "14:15", "endTime": "14:55" },
  261. { "number": 8, "startTime": "15:10", "endTime": "15:50" },
  262. { "number": 9, "startTime": "15:55", "endTime": "16:35" },
  263. { "number": 10, "startTime": "18:30", "endTime": "19:10" },
  264. { "number": 11, "startTime": "19:20", "endTime": "20:00" },
  265. { "number": 12, "startTime": "20:10", "endTime": "20:50" }
  266. ];
  267. try {
  268. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  269. AndroidBridge.showToast("预设时间段导入成功!");
  270. return true;
  271. } catch (error) {
  272. AndroidBridge.showToast("导入时间段失败: " + error.message);
  273. return false;
  274. }
  275. }
  276. async function saveConfig(configData) {
  277. try {
  278. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(configData));
  279. AndroidBridge.showToast("课表配置更新成功!");
  280. return true;
  281. } catch (error) {
  282. AndroidBridge.showToast("保存配置失败: " + error.message);
  283. return false;
  284. }
  285. }
  286. // 主流程入口
  287. async function runImportFlow() {
  288. AndroidBridge.showToast("南京师范大学课程导入流程启动...");
  289. // 1. 公告和前置检查。
  290. const alertConfirmed = await promptUserToStart();
  291. if (!alertConfirmed) return;
  292. // 2. 获取用户输入参数 (学年和学期)。
  293. const academicYear = await getAcademicYear();
  294. if (academicYear === null) {
  295. AndroidBridge.showToast("导入已取消。");
  296. return;
  297. }
  298. const semesterCode = await selectSemester();
  299. if (semesterCode === null) {
  300. AndroidBridge.showToast("导入已取消。");
  301. return;
  302. }
  303. // 3. 导入预设时间段
  304. await importPresetTimeSlots();
  305. // 4. 网络请求和数据解析。
  306. const courseData = await fetchAndParseCourses(academicYear, semesterCode);
  307. if (courseData === null) return;
  308. // 5. 保存配置数据
  309. const configSaveResult = await saveConfig(courseData.config);
  310. if (!configSaveResult) return;
  311. // 6. 课程数据保存。
  312. const saveResult = await saveCourses(courseData.courses);
  313. if (!saveResult) return;
  314. // 7. 流程完全成功,发送结束信号。
  315. AndroidBridge.showToast("所有任务已完成!课表导入成功。");
  316. AndroidBridge.notifyTaskCompletion();
  317. }
  318. // 启动导入流程
  319. runImportFlow();