nwpu_01.js 16 KB

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