gdut.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  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. // 此处假设总课程数量总是小于 1000,并设置一页的最大课程数量为 1000。
  140. const pageSize = 1000;
  141. const formData = new URLSearchParams();
  142. formData.append('zc', '');
  143. formData.append('xnxqdm', semesterId);
  144. formData.append('page', '1');
  145. formData.append('rows', String(pageSize));
  146. formData.append('sort', 'kxh');
  147. formData.append('order', 'asc');
  148. const 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. const rawData = await response.json();
  161. if (rawData.total === 0 || rawData.rows.length === 0) {
  162. console.log(`学期 ${semesterId} 没有找到课程数据。`);
  163. if (await checkSemesterIsOpened(semesterId)) {
  164. throw new Error('该学期没有找到课程!请确认选择了正确的学期。');
  165. }
  166. throw new Error('学期未开放课表查询!');
  167. }
  168. const rawCourses = rawData.rows;
  169. console.log(`成功获取学期 ${semesterId} 的课程数据,共 ${rawCourses.length} 条记录。`);
  170. const courses = parseCourses(rawCourses);
  171. return courses;
  172. } catch (error) {
  173. console.error('添加课程表失败:', error);
  174. window.shiguangBridge.showToast(`添加课程失败: ${error.message}`);
  175. return null;
  176. }
  177. }
  178. /**
  179. * 检查指定学期的课表查询是否已开放
  180. * @param {string} semesterId 学期代码
  181. * @returns {Promise<boolean>} 已开放时返回 true,未开放时返回 false
  182. */
  183. async function checkSemesterIsOpened(semesterId) {
  184. console.log(`正在检查学期 ${semesterId} 是否已开放课表查询...`);
  185. const url = `${url_strings.GET_ALL_COURSES_HTML_URL}?xnxqdm=${semesterId}`;
  186. const response = await fetch(url, {
  187. method: 'GET',
  188. headers: {
  189. 'Referer': url_strings.GET_ALL_COURSES_HTML_URL_REFERRER
  190. },
  191. credentials: 'include'
  192. });
  193. const html = await response.text();
  194. // 如果不包含"未开放"文字,说明已开放
  195. return !html.includes("本学期课表还未开放,请稍后查询!");
  196. }
  197. /**
  198. * 将原始课程数据转换为标准课程对象列表
  199. * @param {Array<Object>} rawCourses 原始课程数据列表
  200. * @returns {Array<Object>} 转换后的课程对象列表
  201. */
  202. function parseCourses(rawCourses){
  203. console.log(`正在转换原始课程数据...`);
  204. const courses = [];
  205. for (const raw of rawCourses) {
  206. const sectionMatch = raw.jcdm.match(/\d{2}/g);
  207. if (!sectionMatch) {
  208. console.error(`课程节次解析失败,原始数据:${raw.jcdm}。`);
  209. throw new Error(`课程 ${raw.kcmc} 节次解析失败,原始数据:${raw.jcdm}。联系开发者解决此问题。`);
  210. }
  211. const sections = sectionMatch.map(Number);
  212. const startSection = sections[0];
  213. const endSection = sections[sections.length - 1];
  214. // 周次
  215. const week = Number(raw.zc);
  216. if (isNaN(week)) {
  217. console.error(`课程周次解析失败,原始数据:${raw.zc}。`);
  218. throw new Error(`课程 ${raw.kcmc} 周次解析失败,原始数据:${raw.zc}。联系开发者解决此问题。`);
  219. }
  220. const course = {
  221. name: decodeHtmlEntities(raw.kcmc).trim(),
  222. teacher: decodeHtmlEntities(raw.teaxms || "").trim(),
  223. position: decodeHtmlEntities(raw.jxcdmc || "").trim(),
  224. day: Number(raw.xq),
  225. startSection: startSection,
  226. endSection: endSection,
  227. weeks: [week],
  228. isCustomTime: false
  229. };
  230. courses.push(course);
  231. }
  232. return courses;
  233. }
  234. /**
  235. * 解码 HTML 实体字符为普通文本
  236. * @param {string} text 需要解码的文本
  237. * @returns {string} 解码后的文本,输入为空时返回空字符串
  238. */
  239. function decodeHtmlEntities(text) {
  240. if (!text) return '';
  241. const div = document.createElement('div');
  242. div.innerHTML = text;
  243. return div.textContent || div.innerText || '';
  244. }
  245. /**
  246. * 将课程列表导入到应用
  247. * @param {Array<Object>} courses 课程对象列表
  248. */
  249. async function saveCourses(courses){
  250. try {
  251. console.log("正在尝试导入课程...");
  252. const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  253. if (result === true) {
  254. console.log("课程导入成功!");
  255. } else {
  256. console.log("课程导入未成功,结果:" + result);
  257. window.shiguangBridge.showToast("课程导入失败,请查看日志。");
  258. }
  259. } catch (error) {
  260. console.error("导入课程时发生错误:", error);
  261. window.shiguangBridge.showToast("导入课程失败: " + error.message);
  262. }
  263. }
  264. /**
  265. * 导入预设时间段
  266. */
  267. async function setPresetTimeSlots() {
  268. const presetTimeSlots = [
  269. { "number": 1, "startTime": "08:30", "endTime": "09:15" },
  270. { "number": 2, "startTime": "09:20", "endTime": "10:05" },
  271. { "number": 3, "startTime": "10:25", "endTime": "11:10" },
  272. { "number": 4, "startTime": "11:15", "endTime": "12:00" },
  273. { "number": 5, "startTime": "13:50", "endTime": "14:35" },
  274. { "number": 6, "startTime": "14:40", "endTime": "15:25" },
  275. { "number": 7, "startTime": "15:30", "endTime": "16:15" },
  276. { "number": 8, "startTime": "16:30", "endTime": "17:15" },
  277. { "number": 9, "startTime": "17:20", "endTime": "18:05" },
  278. { "number": 10, "startTime": "18:30", "endTime": "19:15" },
  279. { "number": 11, "startTime": "19:20", "endTime": "20:05" },
  280. { "number": 12, "startTime": "20:10", "endTime": "20:55" },
  281. { "number": 13, "startTime": "21:00", "endTime": "21:45" },
  282. { "number": 14, "startTime": "21:50", "endTime": "22:35" }
  283. ];
  284. try {
  285. console.log("正在尝试导入预设时间段...");
  286. const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  287. if (result === true) {
  288. console.log("预设时间段导入成功!");
  289. } else {
  290. console.log("预设时间段导入未成功,结果:" + result);
  291. window.shiguangBridge.showToast("预设时间段导入失败,请查看日志。");
  292. }
  293. } catch (error) {
  294. console.error("导入时间段时发生错误:", error);
  295. window.shiguangBridge.showToast("导入时间段失败: " + error.message);
  296. }
  297. }
  298. /**
  299. * 导入课表配置
  300. * @param {Object} config 课表配置对象
  301. */
  302. async function saveConfig(config) {
  303. try {
  304. console.log("正在尝试导入课表配置...");
  305. const configJsonString = JSON.stringify(config);
  306. const result = await window.shiguangBridgePromise.saveCourseConfig(configJsonString);
  307. if (result === true) {
  308. console.log("课表配置导入成功!");
  309. } else {
  310. console.log("课表配置导入未成功,结果:" + result);
  311. window.shiguangBridge.showToast("课表配置导入失败,请查看日志。");
  312. }
  313. } catch (error) {
  314. console.error("导入课表配置时发生错误:", error);
  315. window.shiguangBridge.showToast("导入课表配置失败: " + error.message);
  316. }
  317. }
  318. /**
  319. * 编排这些异步操作,并在用户取消时停止后续执行。
  320. */
  321. async function runImportFlow() {
  322. if (window.location.hostname === "authserver.gdut.edu.cn"){
  323. window.shiguangBridge.showToast("执行导入课表操作前,必须先登录到教务系统!");
  324. return;
  325. }
  326. if (window.location.hostname !== "jxfw.gdut.edu.cn") {
  327. window.shiguangBridge.showToast("当前页面不是教务系统页面!");
  328. return;
  329. }
  330. const result = await stepDescriptionAlert();
  331. if (!result) {
  332. console.log("用户取消了操作,停止后续执行。");
  333. return; // 用户取消,立即退出函数
  334. }
  335. const semesterId = await selectSemesterSelection();
  336. if (!semesterId) {
  337. console.log("用户取消了学期选择,停止后续执行。");
  338. return; // 用户取消,立即退出函数
  339. }
  340. const startDate = await fetchStartDate(semesterId);
  341. const courses = await fetchCourses(semesterId);
  342. if (!courses) {
  343. console.log(`未能获取课程数据,停止后续执行。`);
  344. return; // 获取课程失败,立即退出函数
  345. }
  346. const config = {
  347. semesterStartDate: startDate.toISOString().split('T')[0], // 转换为 YYYY-MM-DD 格式
  348. semesterTotalWeeks: 20,
  349. defaultClassDuration: 45,
  350. defaultBreakDuration: 5,
  351. firstDayOfWeek: 1
  352. }
  353. await saveConfig(config);
  354. await saveCourses(courses);
  355. await setPresetTimeSlots();
  356. window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
  357. // 发送最终的生命周期完成信号
  358. window.shiguangBridge.notifyTaskCompletion();
  359. }
  360. // 入口函数,开始执行导入流程
  361. runImportFlow();