nwpu_01.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. // 西北工业大学(NWPU) 拾光课程表适配脚本
  2. // 适配系统:EAMS 教务系统(jwxt.nwpu.edu.cn)
  3. // 适配范围:本科(BACHELOR_AND_ASSOCIATE)
  4. // 维护者:用户7227
  5. /**
  6. * 将 HHmm 整数格式转换为 HH:mm 字符串
  7. * @param {number} hhmm - 如 830, 915, 1400
  8. * @returns {string} - 如 "08:30", "09:15", "14:00"
  9. */
  10. function formatTime(hhmm) {
  11. var h = String(Math.floor(hhmm / 100));
  12. var m = String(hhmm % 100);
  13. if (h.length < 2) h = '0' + h;
  14. if (m.length < 2) m = '0' + m;
  15. return h + ':' + m;
  16. }
  17. /**
  18. * 发起网络请求
  19. * @param {string} url - 请求地址
  20. * @returns {Promise<string>} - 响应文本
  21. */
  22. async function request(url) {
  23. var res = await fetch(url, { credentials: "include" });
  24. if (!res.ok) throw new Error("网络请求失败: " + res.status);
  25. return await res.text();
  26. }
  27. /**
  28. * 获取学生 ID
  29. * 调用 student-portrait API 获取当前登录学生的数据库 ID
  30. * @returns {Promise<number|null>} - 学生 ID
  31. */
  32. async function getStudentId() {
  33. console.log("[NWPU] 正在获取学生 ID...");
  34. try {
  35. var data = await request("/student/for-std/student-portrait/getStdInfo?bizTypeAssoc=2&cultivateTypeAssoc=1");
  36. var json = JSON.parse(data);
  37. if (json && json.student && json.student.id) {
  38. console.log("[NWPU] 学生 ID: " + json.student.id);
  39. return json.student.id;
  40. }
  41. console.error("[NWPU] API 响应中未找到 student.id");
  42. return null;
  43. } catch (e) {
  44. console.error("[NWPU] 获取学生 ID 失败: " + e.message);
  45. return null;
  46. }
  47. }
  48. /**
  49. * 获取学期列表
  50. * 请求课表页面 HTML,解析 #allSemesters 下拉框
  51. * @returns {Promise<Array<{id: number, name: string}>>} - 学期列表
  52. */
  53. async function getSemesters() {
  54. console.log("[NWPU] 正在获取学期列表...");
  55. try {
  56. var html = await request("/student/for-std/course-table");
  57. var parser = new DOMParser();
  58. var doc = parser.parseFromString(html, "text/html");
  59. var select = doc.querySelector("#allSemesters");
  60. if (!select) {
  61. console.error("[NWPU] 未找到 #allSemesters 元素");
  62. return [];
  63. }
  64. var options = select.querySelectorAll("option");
  65. var semesters = [];
  66. for (var i = 0; i < options.length; i++) {
  67. var opt = options[i];
  68. var id = parseInt(opt.getAttribute("value"));
  69. var name = opt.textContent.trim();
  70. if (!isNaN(id) && name) {
  71. semesters.push({ id: id, name: name });
  72. }
  73. }
  74. console.log("[NWPU] 获取到 " + semesters.length + " 个学期");
  75. return semesters;
  76. } catch (e) {
  77. console.error("[NWPU] 获取学期列表失败: " + e.message);
  78. return [];
  79. }
  80. }
  81. /**
  82. * 获取课程数据
  83. * 调用 print-data API 获取指定学期的课程和时间表信息
  84. * @param {number} semesterId - 学期 ID
  85. * @param {number} studentId - 学生 ID
  86. * @returns {Promise<object|null>} - API 响应 JSON
  87. */
  88. async function getCourseData(semesterId, studentId) {
  89. console.log("[NWPU] 正在获取课程数据...");
  90. AndroidBridge.showToast("正在同步课表...");
  91. try {
  92. var url = "/student/for-std/course-table/semester/" + semesterId + "/print-data/" + studentId;
  93. var data = await request(url);
  94. return JSON.parse(data);
  95. } catch (e) {
  96. console.error("[NWPU] 获取课程数据失败: " + e.message);
  97. AndroidBridge.showToast("获取课程数据失败: " + e.message);
  98. return null;
  99. }
  100. }
  101. /**
  102. * NWPU 默认作息时间(长安校区)
  103. * 当 API 响应中不包含 timeTableLayout 时使用
  104. */
  105. function getDefaultTimeSlots() {
  106. return [
  107. { number: 1, startTime: "08:30", endTime: "09:15" },
  108. { number: 2, startTime: "09:25", endTime: "10:10" },
  109. { number: 3, startTime: "10:30", endTime: "11:15" },
  110. { number: 4, startTime: "11:25", endTime: "12:10" },
  111. { number: 5, startTime: "12:20", endTime: "13:05" },
  112. { number: 6, startTime: "13:05", endTime: "13:50" },
  113. { number: 7, startTime: "14:00", endTime: "14:45" },
  114. { number: 8, startTime: "14:55", endTime: "15:40" },
  115. { number: 9, startTime: "16:00", endTime: "16:45" },
  116. { number: 10, startTime: "16:55", endTime: "17:40" },
  117. { number: 11, startTime: "19:00", endTime: "19:45" },
  118. { number: 12, startTime: "19:55", endTime: "20:40" },
  119. { number: 13, startTime: "20:40", endTime: "21:25" }
  120. ];
  121. }
  122. /**
  123. * 从 API 响应中提取时间段
  124. * 优先从 timeTableLayout.courseUnitList 动态获取,否则使用默认时间段
  125. * @param {object} apiData - API 响应
  126. * @returns {Array<{number: number, startTime: string, endTime: string}>}
  127. */
  128. function extractTimeSlots(apiData) {
  129. var layout = apiData.timeTableLayout;
  130. if (layout && layout.courseUnitList) {
  131. var slots = [];
  132. for (var i = 0; i < layout.courseUnitList.length; i++) {
  133. var unit = layout.courseUnitList[i];
  134. slots.push({
  135. number: unit.indexNo,
  136. startTime: formatTime(unit.startTime),
  137. endTime: formatTime(unit.endTime)
  138. });
  139. }
  140. console.log("[NWPU] 从 API 提取到 " + slots.length + " 个时间段");
  141. return slots;
  142. }
  143. console.log("[NWPU] API 未返回时间段,使用默认作息时间");
  144. return getDefaultTimeSlots();
  145. }
  146. /**
  147. * 获取学期信息(开始日期、结束日期)
  148. * 调用 ws/semester/get API
  149. * @param {number} semesterId - 学期 ID
  150. * @returns {Promise<{startDate: string|null, endDate: string|null}>}
  151. */
  152. async function getSemesterInfo(semesterId) {
  153. try {
  154. var data = await request("/student/ws/semester/get/" + semesterId);
  155. var json = JSON.parse(data);
  156. return {
  157. startDate: json.startDate || null,
  158. endDate: json.endDate || null
  159. };
  160. } catch (e) {
  161. console.warn("[NWPU] 获取学期信息失败: " + e.message);
  162. return { startDate: null, endDate: null };
  163. }
  164. }
  165. /**
  166. * 计算两个日期之间的周数
  167. * @param {string} startDate - YYYY-MM-DD
  168. * @param {string} endDate - YYYY-MM-DD
  169. * @returns {number}
  170. */
  171. function calculateTotalWeeks(startDate, endDate) {
  172. if (!startDate || !endDate) return 20;
  173. var start = new Date(startDate);
  174. var end = new Date(endDate);
  175. var diffMs = end.getTime() - start.getTime();
  176. var diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
  177. return Math.ceil(diffDays / 7);
  178. }
  179. /**
  180. * 从 API 响应和学期信息中提取学期配置
  181. * @param {object} semesterInfo - 学期信息 {startDate, endDate}
  182. * @param {Array} activities - 课程活动列表
  183. * @returns {object} - CourseConfigJsonModel
  184. */
  185. function extractCourseConfig(semesterInfo, activities) {
  186. var semesterStartDate = semesterInfo.startDate;
  187. var semesterTotalWeeks = 20;
  188. // 优先从学期日期计算总周数
  189. if (semesterInfo.startDate && semesterInfo.endDate) {
  190. semesterTotalWeeks = calculateTotalWeeks(semesterInfo.startDate, semesterInfo.endDate);
  191. } else if (activities.length > 0) {
  192. // 回退:从 weekIndexes 推算
  193. var maxWeek = 0;
  194. for (var i = 0; i < activities.length; i++) {
  195. var weeks = activities[i].weekIndexes;
  196. if (weeks && weeks.length > 0) {
  197. for (var j = 0; j < weeks.length; j++) {
  198. if (weeks[j] > maxWeek) maxWeek = weeks[j];
  199. }
  200. }
  201. }
  202. if (maxWeek > 0) semesterTotalWeeks = maxWeek;
  203. }
  204. console.log("[NWPU] 学期开始日期: " + (semesterStartDate || "未知") + ", 总周数: " + semesterTotalWeeks);
  205. return {
  206. semesterStartDate: semesterStartDate,
  207. semesterTotalWeeks: semesterTotalWeeks,
  208. defaultClassDuration: 45,
  209. defaultBreakDuration: 10,
  210. firstDayOfWeek: 1
  211. };
  212. }
  213. /**
  214. * 将 API 活动数据转换为 ImportCourseJsonModel 格式
  215. * @param {Array} activities - API activities 数组
  216. * @returns {Array<object>} - 课程列表
  217. */
  218. function convertCourses(activities) {
  219. var courses = [];
  220. for (var i = 0; i < activities.length; i++) {
  221. var act = activities[i];
  222. // 拼接教室信息:优先 room,辅以 building
  223. var position = act.room || act.building || "未知地点";
  224. // 取第一个教师
  225. var teacher = "未知教师";
  226. if (act.teachers && act.teachers.length > 0) {
  227. teacher = act.teachers[0];
  228. }
  229. // 排序周次
  230. var weeks = (act.weekIndexes || []).slice().sort(function(a, b) { return a - b; });
  231. if (act.courseName && weeks.length > 0) {
  232. courses.push({
  233. name: act.courseName,
  234. teacher: teacher,
  235. position: position,
  236. day: act.weekday,
  237. startSection: act.startUnit,
  238. endSection: act.endUnit,
  239. weeks: weeks
  240. });
  241. }
  242. }
  243. console.log("[NWPU] 转换完成,共 " + courses.length + " 个课程条目");
  244. return courses;
  245. }
  246. /**
  247. * 第一步:保存时间段
  248. */
  249. async function saveTimeSlots(timeSlots) {
  250. if (timeSlots.length === 0) {
  251. console.warn("[NWPU] 无时间段数据,跳过");
  252. return true;
  253. }
  254. try {
  255. console.log("[NWPU] 正在保存时间段...");
  256. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  257. AndroidBridge.showToast("成功导入 " + timeSlots.length + " 个时间段");
  258. return true;
  259. } catch (e) {
  260. console.error("[NWPU] 保存时间段失败: " + e.message);
  261. AndroidBridge.showToast("保存时间段失败: " + e.message);
  262. return false;
  263. }
  264. }
  265. /**
  266. * 第二步:保存课表配置
  267. */
  268. async function saveConfig(config) {
  269. try {
  270. console.log("[NWPU] 正在保存课表配置...");
  271. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  272. AndroidBridge.showToast("课表配置导入成功");
  273. return true;
  274. } catch (e) {
  275. console.error("[NWPU] 保存配置失败: " + e.message);
  276. AndroidBridge.showToast("保存配置失败: " + e.message);
  277. return false;
  278. }
  279. }
  280. /**
  281. * 第三步:保存课程数据
  282. */
  283. async function saveCourses(courses) {
  284. if (courses.length === 0) {
  285. AndroidBridge.showToast("没有课程数据需要导入");
  286. return true;
  287. }
  288. try {
  289. console.log("[NWPU] 正在保存课程数据...");
  290. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  291. AndroidBridge.showToast("成功导入 " + courses.length + " 个课程条目");
  292. return true;
  293. } catch (e) {
  294. console.error("[NWPU] 保存课程失败: " + e.message);
  295. AndroidBridge.showToast("保存课程失败: " + e.message);
  296. return false;
  297. }
  298. }
  299. /**
  300. * 主导入流程
  301. */
  302. async function runImportFlow() {
  303. try {
  304. // 1. 显示导入说明
  305. var confirmed = await window.AndroidBridgePromise.showAlert(
  306. "西北工业大学课表导入",
  307. "导入前请确保您已在浏览器中登录教务系统(jwxt.nwpu.edu.cn)。\n\n" +
  308. "本适配将自动获取学期信息、时间段和课程数据。",
  309. "开始导入"
  310. );
  311. if (!confirmed) {
  312. AndroidBridge.showToast("导入已取消");
  313. return;
  314. }
  315. // 2. 获取学生 ID
  316. AndroidBridge.showToast("正在获取学生信息...");
  317. var studentId = await getStudentId();
  318. if (!studentId) {
  319. await window.AndroidBridgePromise.showAlert(
  320. "导入失败",
  321. "未能获取学生 ID,请确认您已登录教务系统。",
  322. "确定"
  323. );
  324. return;
  325. }
  326. // 3. 获取学期列表
  327. AndroidBridge.showToast("正在获取学期列表...");
  328. var semesters = await getSemesters();
  329. if (semesters.length === 0) {
  330. await window.AndroidBridgePromise.showAlert(
  331. "导入失败",
  332. "未能获取学期列表,请刷新页面后重试。",
  333. "确定"
  334. );
  335. return;
  336. }
  337. // 4. 用户选择学期
  338. var semesterNames = [];
  339. for (var i = 0; i < semesters.length; i++) {
  340. semesterNames.push(semesters[i].name);
  341. }
  342. var selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  343. "选择学期",
  344. JSON.stringify(semesterNames),
  345. 0
  346. );
  347. if (selectedIndex === null || selectedIndex < 0 || selectedIndex >= semesters.length) {
  348. AndroidBridge.showToast("导入已取消");
  349. return;
  350. }
  351. var selectedSemester = semesters[selectedIndex];
  352. console.log("[NWPU] 选择学期: " + selectedSemester.name + " (ID: " + selectedSemester.id + ")");
  353. // 5. 获取课程数据
  354. var apiData = await getCourseData(selectedSemester.id, studentId);
  355. if (!apiData) return;
  356. var tableVm = apiData.studentTableVm;
  357. if (!tableVm || !tableVm.activities || tableVm.activities.length === 0) {
  358. AndroidBridge.showToast("该学期没有课程数据");
  359. return;
  360. }
  361. var activities = tableVm.activities;
  362. console.log("[NWPU] 获取到 " + activities.length + " 条课程活动");
  363. // 6. 获取学期信息(开始日期、结束日期)
  364. AndroidBridge.showToast("正在获取学期信息...");
  365. var semesterInfo = await getSemesterInfo(selectedSemester.id);
  366. console.log("[NWPU] 学期开始: " + (semesterInfo.startDate || "未知") + ", 结束: " + (semesterInfo.endDate || "未知"));
  367. // 7. 提取时间段
  368. var timeSlots = extractTimeSlots(apiData);
  369. // 8. 提取课表配置
  370. var courseConfig = extractCourseConfig(semesterInfo, activities);
  371. // 9. 转换课程数据
  372. var courses = convertCourses(activities);
  373. // 10. 三步导入:时间段 → 配置 → 课程
  374. var timeSlotResult = await saveTimeSlots(timeSlots);
  375. if (!timeSlotResult) return;
  376. var configResult = await saveConfig(courseConfig);
  377. if (!configResult) return;
  378. var courseResult = await saveCourses(courses);
  379. if (!courseResult) return;
  380. // 完成
  381. AndroidBridge.showToast("课表导入完成!");
  382. AndroidBridge.notifyTaskCompletion();
  383. } catch (e) {
  384. console.error("[NWPU] 导入异常: " + e.message);
  385. AndroidBridge.showToast("导入异常: " + e.message);
  386. }
  387. }
  388. // 启动导入流程
  389. runImportFlow();