gdut.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. // 文件: gdut.js
  2. if (typeof url_strings === 'undefined') {
  3. var url_strings = {
  4. BASE_URL: "https://jxfw.gdut.edu.cn",
  5. GET_WEEK_COURSES_API_URL: "https://jxfw.gdut.edu.cn/xsgrkbcx!getKbRq.action",
  6. GET_ALL_COURSES_API_URL: "https://jxfw.gdut.edu.cn/xsgrkbcx!getDataList.action",
  7. GET_ALL_COURSES_HTML_URL: "https://jxfw.gdut.edu.cn/xsgrkbcx!xsAllKbList.action",
  8. GET_ALL_COURSES_HTML_URL_REFERRER: "https://jxfw.gdut.edu.cn/xsgrkbcx!getXsgrbkList.action"
  9. };
  10. }
  11. /**
  12. * 展示导入课程确认弹窗
  13. * @returns {Promise<boolean>} 是否确认执行导入课程操作
  14. */
  15. async function stepDescriptionAlert() {
  16. try {
  17. const confirmed = await window.shiguangBridgePromise.showAlert(
  18. "提示",
  19. "即将执行导入课程操作,确保当前已登录到教务系统(无需打开课程表页面)",
  20. "确认"
  21. );
  22. return confirmed;
  23. } catch (error) {
  24. console.error("显示弹窗时发生错误:", error);
  25. return false;
  26. }
  27. }
  28. /**
  29. * 展示学期选择弹窗
  30. * @returns {Promise<string>} 选择的学期代码
  31. */
  32. async function selectSemesterSelection(){
  33. // 教务系统识别学期的规则为:学年年份 + 学期编号。
  34. // 学年年份与实际年份并不相等,例如:2025-2026 学年(2025 学年)秋季学期对应实际年份为 2025,春季学期对应实际年份为 2026;
  35. // 2026-2027 学年(2026 学年)秋季学期对应实际年份为 2026,春季学期对应实际年份为 2027。
  36. const now = new Date();
  37. const currentYear = now.getFullYear();
  38. const currentMonth = now.getMonth() + 1;
  39. const currentSemester = currentMonth >= 7 || currentMonth <= 1 ? 1 : 2;
  40. let currentSemesterYear = currentSemester === 1 ? currentYear : currentYear - 1;
  41. currentSemesterYear = currentMonth <= 1 ? currentSemesterYear - 1 : currentSemesterYear;
  42. const nextSemester = currentSemester === 1 ? 2 : 1;
  43. const nextSemesterYear = currentSemester === 1 ? currentSemesterYear : currentSemesterYear + 1;
  44. const presetSemestersIds = [];
  45. const presetSemestersNames = [];
  46. for (let semesterYear = nextSemesterYear; semesterYear >= nextSemesterYear - 6; semesterYear--){
  47. for (let semester = semesterYear === nextSemesterYear ? nextSemester : 2; semester >= 1; semester--){
  48. presetSemestersIds.push(`${semesterYear}0${semester}`);
  49. const semesterName = `${semester === 1 ? semesterYear : semesterYear + 1}年${semester === 1 ? "秋季" : "春季"} (${semesterYear}-${semesterYear + 1} 学年第${semester}学期)`;
  50. presetSemestersNames.push(semesterName);
  51. }
  52. }
  53. try {
  54. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  55. "选择要导入的学期",
  56. JSON.stringify(presetSemestersNames),
  57. 1
  58. );
  59. if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < presetSemestersIds.length) {
  60. console.log("用户选择了: " + presetSemestersNames[selectedIndex] + " (索引: " + selectedIndex + ")");
  61. return presetSemestersIds[selectedIndex];
  62. } else {
  63. console.log("用户取消了选择。");
  64. return null;
  65. }
  66. } catch (error) {
  67. console.error("显示单选列表弹窗时发生错误:", error);
  68. window.shiguangBridge.showToast("Single Selection:显示列表出错!" + error.message);
  69. return null;
  70. }
  71. }
  72. /**
  73. * 从 JSON 格式的日期信息数据中提取第一个周一的日期
  74. * @param {string} dateInfoJsonData JSON 格式的日期信息数据
  75. * @returns {string|null} 找到的第一个周一的日期字符串,未找到时返回 null
  76. */
  77. function extractFirstDay(dateInfoJsonData) {
  78. try {
  79. const jsonArray = JSON.parse(dateInfoJsonData);
  80. const dateInfoArray = jsonArray[1];
  81. // 遍历查找 xqmc === "1"(周一)的项
  82. for (const dateInfo of dateInfoArray) {
  83. if (dateInfo.xqmc === "1" && dateInfo.rq) {
  84. return dateInfo.rq;
  85. }
  86. }
  87. console.error('未找到 xqmc=1 的日期项');
  88. return null;
  89. } catch (error) {
  90. console.error('解析 JSON 失败:', error);
  91. return null;
  92. }
  93. }
  94. /**
  95. * 获取学期开始日期(第一个周一),获取失败时返回当前日期
  96. * @param {string} semesterId 学期代码
  97. * @returns {Promise<Date>} 学期开始日期,获取失败时返回当前日期
  98. */
  99. async function fetchStartDate(semesterId) {
  100. const url = `${url_strings.GET_WEEK_COURSES_API_URL}?xnxqdm=${semesterId}&zc=1`;
  101. try {
  102. console.log(`正在获取学期开始日期。学期代码:${semesterId}`);
  103. const response = await fetch(url, {
  104. method: 'GET',
  105. headers: {
  106. 'Referer': url
  107. },
  108. credentials: 'include'
  109. });
  110. const data = await response.text();
  111. const startDateString = extractFirstDay(data);
  112. // 如果提取失败,返回当前日期
  113. if (startDateString === null) {
  114. // 使用当前日期
  115. return new Date();
  116. }
  117. // 解析日期字符串为 Date 对象
  118. const date = new Date(startDateString);
  119. if (isNaN(date.getTime())) {
  120. console.warn(`日期解析失败: ${startDateString},使用当前日期`);
  121. return new Date();
  122. }
  123. console.log(`成功获取学期开始日期: ${date.toISOString().split('T')[0]}`);
  124. return date;
  125. } catch (error) {
  126. console.error('获取学期开始日期失败,使用当前日期。错误信息:', error);
  127. return new Date();
  128. }
  129. }
  130. /**
  131. * 获取指定学期的课程数据并转换为课程对象列表
  132. * @param {string} semesterId 学期代码
  133. * @returns {Promise<Array<Object>|null>} 课程对象列表,获取失败时返回 null
  134. */
  135. async function fetchCourses(semesterId){
  136. try {
  137. console.log(`正在获取学期 ${semesterId} 的课程数据...`);
  138. // 理论上此处应分页遍历处理,但分页处理将导致教务系统返回错误的数据(课程重复和缺失)。
  139. // API 返回结果包含课程总量,可以先探测一遍获取总量,再使用获取的总量去获取所有课程。
  140. let formData = new URLSearchParams({
  141. zc: '',
  142. xnxqdm: semesterId,
  143. page: '1',
  144. rows: '1',
  145. sort: 'kxh',
  146. order: 'asc'
  147. })
  148. let response = await fetch(url_strings.GET_ALL_COURSES_API_URL, {
  149. method: 'POST',
  150. headers: {
  151. 'Content-Type': 'application/x-www-form-urlencoded',
  152. 'Referer': url_strings.BASE_URL
  153. },
  154. body: formData.toString(),
  155. credentials: 'include'
  156. });
  157. if (!response.ok) {
  158. throw new Error(`请求失败: ${response.status}`);
  159. }
  160. let rawData = await response.json();
  161. const total = rawData.total;
  162. if (total === 0 || rawData.rows.length === 0) {
  163. console.log(`学期 ${semesterId} 没有找到课程数据。`);
  164. if (await checkSemesterIsOpened(semesterId)) {
  165. throw new Error('该学期没有找到课程!请确认选择了正确的学期。');
  166. }
  167. throw new Error('学期未开放课表查询!');
  168. }
  169. formData.set('rows', total);
  170. response = await fetch(url_strings.GET_ALL_COURSES_API_URL, {
  171. method: 'POST',
  172. headers: {
  173. 'Content-Type': 'application/x-www-form-urlencoded',
  174. 'Referer': url_strings.BASE_URL
  175. },
  176. body: formData.toString(),
  177. credentials: 'include'
  178. });
  179. if (!response.ok) {
  180. throw new Error(`请求失败: ${response.status}`);
  181. }
  182. rawData = await response.json();
  183. const rawCourses = rawData.rows;
  184. console.log(`成功获取学期 ${semesterId} 的课程数据,共 ${rawCourses.length} 条记录。`);
  185. const courses = parseCourses(rawCourses);
  186. return courses;
  187. } catch (error) {
  188. console.error('添加课程表失败:', error);
  189. window.shiguangBridge.showToast(`添加课程失败: ${error.message}`);
  190. return null;
  191. }
  192. }
  193. /**
  194. * 检查指定学期的课表查询是否已开放
  195. * @param {string} semesterId 学期代码
  196. * @returns {Promise<boolean>} 已开放时返回 true,未开放时返回 false
  197. */
  198. async function checkSemesterIsOpened(semesterId) {
  199. console.log(`正在检查学期 ${semesterId} 是否已开放课表查询...`);
  200. const url = `${url_strings.GET_ALL_COURSES_HTML_URL}?xnxqdm=${semesterId}`;
  201. const response = await fetch(url, {
  202. method: 'GET',
  203. headers: {
  204. 'Referer': url_strings.GET_ALL_COURSES_HTML_URL_REFERRER
  205. },
  206. credentials: 'include'
  207. });
  208. const html = await response.text();
  209. // 如果不包含"未开放"文字,说明已开放
  210. return !html.includes("本学期课表还未开放,请稍后查询!");
  211. }
  212. /**
  213. * 将原始课程数据转换为标准课程对象列表
  214. * @param {Array<Object>} rawCourses 原始课程数据列表
  215. * @returns {Array<Object>} 转换后的课程对象列表
  216. */
  217. function parseCourses(rawCourses){
  218. console.log(`正在转换原始课程数据...`);
  219. const courses = [];
  220. for (const raw of rawCourses) {
  221. const sectionMatch = raw.jcdm.match(/\d{2}/g);
  222. if (!sectionMatch) {
  223. console.error(`课程节次解析失败,原始数据:${raw.jcdm}。`);
  224. throw new Error(`课程 ${raw.kcmc} 节次解析失败,原始数据:${raw.jcdm}。联系开发者解决此问题。`);
  225. }
  226. const sections = sectionMatch.map(Number);
  227. const startSection = sections[0];
  228. const endSection = sections[sections.length - 1];
  229. // 周次
  230. const week = Number(raw.zc);
  231. if (isNaN(week)) {
  232. console.error(`课程周次解析失败,原始数据:${raw.zc}。`);
  233. throw new Error(`课程 ${raw.kcmc} 周次解析失败,原始数据:${raw.zc}。联系开发者解决此问题。`);
  234. }
  235. const course = {
  236. name: decodeHtmlEntities(raw.kcmc).trim(),
  237. teacher: decodeHtmlEntities(raw.teaxms || "").trim(),
  238. position: decodeHtmlEntities(raw.jxcdmc || "").trim(),
  239. day: Number(raw.xq),
  240. startSection: startSection,
  241. endSection: endSection,
  242. weeks: [week],
  243. isCustomTime: false
  244. };
  245. courses.push(course);
  246. }
  247. return courses;
  248. }
  249. /**
  250. * 解码 HTML 实体字符为普通文本
  251. * @param {string} text 需要解码的文本
  252. * @returns {string} 解码后的文本,输入为空时返回空字符串
  253. */
  254. function decodeHtmlEntities(text) {
  255. if (!text) return '';
  256. const div = document.createElement('div');
  257. div.innerHTML = text;
  258. return div.textContent || div.innerText || '';
  259. }
  260. /**
  261. * 将课程列表导入到应用
  262. * @param {Array<Object>} courses 课程对象列表
  263. */
  264. async function saveCourses(courses){
  265. try {
  266. console.log("正在尝试导入课程...");
  267. const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  268. if (result !== true) {
  269. console.log("课程导入未成功,结果:" + result);
  270. window.shiguangBridge.showToast("课程导入失败,请查看日志。");
  271. }
  272. } catch (error) {
  273. console.error("导入课程时发生错误:", error);
  274. window.shiguangBridge.showToast("导入课程失败: " + error.message);
  275. }
  276. }
  277. /**
  278. * 导入预设时间段
  279. */
  280. async function setPresetTimeSlots() {
  281. const presetTimeSlots = [
  282. { "number": 1, "startTime": "08:30", "endTime": "09:15" },
  283. { "number": 2, "startTime": "09:20", "endTime": "10:05" },
  284. { "number": 3, "startTime": "10:25", "endTime": "11:10" },
  285. { "number": 4, "startTime": "11:15", "endTime": "12:00" },
  286. { "number": 5, "startTime": "13:50", "endTime": "14:35" },
  287. { "number": 6, "startTime": "14:40", "endTime": "15:25" },
  288. { "number": 7, "startTime": "15:30", "endTime": "16:15" },
  289. { "number": 8, "startTime": "16:30", "endTime": "17:15" },
  290. { "number": 9, "startTime": "17:20", "endTime": "18:05" },
  291. { "number": 10, "startTime": "18:30", "endTime": "19:15" },
  292. { "number": 11, "startTime": "19:20", "endTime": "20:05" },
  293. { "number": 12, "startTime": "20:10", "endTime": "20:55" },
  294. { "number": 13, "startTime": "21:00", "endTime": "21:45" },
  295. { "number": 14, "startTime": "21:50", "endTime": "22:35" }
  296. ];
  297. try {
  298. console.log("正在尝试导入预设时间段...");
  299. const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  300. if (result !== true) {
  301. console.log("预设时间段导入未成功,结果:" + result);
  302. window.shiguangBridge.showToast("预设时间段导入失败,请查看日志。");
  303. }
  304. } catch (error) {
  305. console.error("导入时间段时发生错误:", error);
  306. window.shiguangBridge.showToast("导入时间段失败: " + error.message);
  307. }
  308. }
  309. /**
  310. * 导入课表配置
  311. * @param {Object} config 课表配置对象
  312. */
  313. async function saveConfig(config) {
  314. try {
  315. console.log("正在尝试导入课表配置...");
  316. const configJsonString = JSON.stringify(config);
  317. const result = await window.shiguangBridgePromise.saveCourseConfig(configJsonString);
  318. if (result !== true) {
  319. console.log("课表配置导入未成功,结果:" + result);
  320. window.shiguangBridge.showToast("课表配置导入失败,请查看日志。");
  321. }
  322. } catch (error) {
  323. console.error("导入课表配置时发生错误:", error);
  324. window.shiguangBridge.showToast("导入课表配置失败: " + error.message);
  325. }
  326. }
  327. /**
  328. * 编排这些异步操作,并在用户取消时停止后续执行。
  329. */
  330. async function runImportFlow() {
  331. if (window.location.hostname === "authserver.gdut.edu.cn"){
  332. window.shiguangBridge.showToast("执行导入课表操作前,必须先登录到教务系统!");
  333. return;
  334. }
  335. if (window.location.hostname !== "jxfw.gdut.edu.cn") {
  336. window.shiguangBridge.showToast("当前页面不是教务系统页面!");
  337. return;
  338. }
  339. const result = await stepDescriptionAlert();
  340. if (!result) {
  341. console.log("用户取消了操作,停止后续执行。");
  342. return; // 用户取消,立即退出函数
  343. }
  344. const semesterId = await selectSemesterSelection();
  345. if (!semesterId) {
  346. console.log("用户取消了学期选择,停止后续执行。");
  347. return; // 用户取消,立即退出函数
  348. }
  349. const startDate = await fetchStartDate(semesterId);
  350. const courses = await fetchCourses(semesterId);
  351. if (!courses) {
  352. console.log(`未能获取课程数据,停止后续执行。`);
  353. return; // 获取课程失败,立即退出函数
  354. }
  355. const config = {
  356. semesterStartDate: startDate.toISOString().split('T')[0], // 转换为 YYYY-MM-DD 格式
  357. semesterTotalWeeks: 20,
  358. defaultClassDuration: 45,
  359. defaultBreakDuration: 5,
  360. firstDayOfWeek: 1
  361. }
  362. await saveConfig(config);
  363. await saveCourses(courses);
  364. await setPresetTimeSlots();
  365. window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
  366. // 发送最终的生命周期完成信号
  367. window.shiguangBridge.notifyTaskCompletion();
  368. }
  369. // 入口函数,开始执行导入流程
  370. runImportFlow();