hniu_01.js 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. // 湖南信息职业技术学院(hniu.cn) 拾光课程表适配脚本
  2. // 非该大学开发者适配,开发者无法及时发现问题
  3. // 出现问题请提联系开发者或者提交pr更改,这更加快速
  4. // 验证逻辑
  5. /**
  6. * 年份输入验证函数
  7. * @param {string} input 用户输入的年份
  8. * @returns {boolean|string} 验证通过返回false,失败返回错误提示
  9. */
  10. window.validateYearInput = function(input) {
  11. return /^[0-9]{4}$/.test(input) ? false : "请输入四位数字的学年!";
  12. };
  13. // 数据解析函数
  14. /**
  15. * 将周次字符串解析为数字数组
  16. */
  17. function parseWeeks(weekStr) {
  18. const weeks = [];
  19. if (!weekStr) return weeks;
  20. // 适配 "1-9,11-17(周)[01-02节]" 或 "12-15(周)"
  21. const pureWeekData = weekStr.split('(')[0];
  22. pureWeekData.split(',').forEach(seg => {
  23. if (seg.includes('-')) {
  24. const [s, e] = seg.split('-').map(Number);
  25. if (!isNaN(s) && !isNaN(e)) {
  26. for (let i = s; i <= e; i++) weeks.push(i);
  27. }
  28. } else {
  29. const w = parseInt(seg);
  30. if (!isNaN(w)) weeks.push(w);
  31. }
  32. });
  33. return [...new Set(weeks)].sort((a, b) => a - b);
  34. }
  35. /**
  36. * 转换课程 HTML 格式为应用模型
  37. */
  38. function parseTimetableToModel(doc) {
  39. const timetable = doc.getElementById('timetable');
  40. if (!timetable) return [];
  41. const rawResults = [];
  42. const rows = Array.from(timetable.querySelectorAll('tr')).slice(1);
  43. // 1. 原始解析阶段
  44. rows.forEach(row => {
  45. const cells = row.querySelectorAll('td');
  46. if (cells.length < 7) return;
  47. cells.forEach((cell, dayIndex) => {
  48. const day = dayIndex + 1;
  49. const detailDiv = cell.querySelector('div.kbcontent[style*="none"]');
  50. if (detailDiv) {
  51. const rawHtml = detailDiv.innerHTML.trim();
  52. if (rawHtml === "" || rawHtml === "&nbsp;") return;
  53. const courseBlocks = rawHtml.split(/---------------------|----------------------/);
  54. courseBlocks.forEach(block => {
  55. if (block.replace(/&nbsp;|<br\/?>/g, '').trim() === "") return;
  56. const tempDiv = document.createElement('div');
  57. tempDiv.innerHTML = block;
  58. let name = "";
  59. for (let node of tempDiv.childNodes) {
  60. if (node.nodeType === 3 && node.textContent.trim() !== "") {
  61. name = node.textContent.trim();
  62. break;
  63. } else if (node.tagName === "BR" && name !== "") {
  64. break;
  65. } else if (node.nodeType === 3) {
  66. name += node.textContent.trim();
  67. }
  68. }
  69. const teacher = tempDiv.querySelector('font[title="教师"]')?.innerText || "未知教师";
  70. const weekStr = tempDiv.querySelector('font[title="周次(节次)"]')?.innerText || "";
  71. const position = tempDiv.querySelector('font[title="教室"]')?.innerText || "未知地点";
  72. let start = 0, end = 0;
  73. if (weekStr) {
  74. // 兼容 [01-02-03-04节] 格式
  75. const sectionPartMatch = weekStr.match(/\[(.*?)节\]/);
  76. if (sectionPartMatch && sectionPartMatch[1]) {
  77. const nums = sectionPartMatch[1].match(/\d+/g);
  78. if (nums && nums.length > 0) {
  79. const sectionNums = nums.map(Number);
  80. start = Math.min(...sectionNums);
  81. end = Math.max(...sectionNums);
  82. }
  83. }
  84. }
  85. if (name && weekStr && start > 0) {
  86. rawResults.push({
  87. "name": name,
  88. "teacher": teacher,
  89. "weeks": parseWeeks(weekStr), // 解析出的数组
  90. "position": position,
  91. "day": day,
  92. "startSection": start,
  93. "endSection": end
  94. });
  95. }
  96. });
  97. }
  98. });
  99. });
  100. // 2. 去重与合并阶段
  101. // 排序顺序:星期 > 课程名 > 教师 > 教室 > 周次 > 起始节次
  102. rawResults.sort((a, b) =>
  103. a.day - b.day ||
  104. a.name.localeCompare(b.name) ||
  105. a.teacher.localeCompare(b.teacher) ||
  106. a.position.localeCompare(b.position) ||
  107. JSON.stringify(a.weeks).localeCompare(JSON.stringify(b.weeks)) ||
  108. a.startSection - b.startSection
  109. );
  110. const mergedResults = [];
  111. rawResults.forEach(current => {
  112. if (mergedResults.length === 0) {
  113. mergedResults.push(current);
  114. return;
  115. }
  116. const last = mergedResults[mergedResults.length - 1];
  117. // 检查是否完全相同(完全相同的重复项直接跳过)
  118. const isDuplicate =
  119. last.day === current.day &&
  120. last.name === current.name &&
  121. last.teacher === current.teacher &&
  122. last.position === current.position &&
  123. last.startSection === current.startSection &&
  124. last.endSection === current.endSection &&
  125. JSON.stringify(last.weeks) === JSON.stringify(current.weeks);
  126. if (isDuplicate) return;
  127. // 检查是否可以合并(信息相同且节次连续,例如 1-2 节和 3-4 节合并为 1-4 节)
  128. const canMerge =
  129. last.day === current.day &&
  130. last.name === current.name &&
  131. last.teacher === current.teacher &&
  132. last.position === current.position &&
  133. JSON.stringify(last.weeks) === JSON.stringify(current.weeks) &&
  134. current.startSection <= last.endSection + 1; // 节次连续或重叠
  135. if (canMerge) {
  136. last.endSection = Math.max(last.endSection, current.endSection);
  137. } else {
  138. mergedResults.push(current);
  139. }
  140. });
  141. return mergedResults;
  142. }
  143. /**
  144. * 保存课表全局配置
  145. */
  146. async function saveAppConfig() {
  147. const config = {
  148. "semesterTotalWeeks": 20,
  149. "firstDayOfWeek": 1
  150. };
  151. return await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  152. }
  153. /**
  154. * 保存时间段配置
  155. */
  156. async function saveAppTimeSlots() {
  157. const timeSlots = [
  158. { "number": 1, "startTime": "08:30", "endTime": "09:10" },
  159. { "number": 2, "startTime": "09:20", "endTime": "10:00" },
  160. { "number": 3, "startTime": "10:20", "endTime": "11:00" },
  161. { "number": 4, "startTime": "11:10", "endTime": "11:50" },
  162. { "number": 5, "startTime": "14:00", "endTime": "14:40" },
  163. { "number": 6, "startTime": "14:50", "endTime": "15:30" },
  164. { "number": 7, "startTime": "15:50", "endTime": "16:30" },
  165. { "number": 8, "startTime": "16:40", "endTime": "17:20" },
  166. { "number": 9, "startTime": "18:40", "endTime": "19:20" },
  167. { "number": 10, "startTime": "19:30", "endTime": "20:10" },
  168. { "number": 11, "startTime": "20:20", "endTime": "21:00" },
  169. { "number": 12, "startTime": "21:10", "endTime": "21:50" }
  170. ];
  171. return await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  172. }
  173. /**
  174. * 获取并让用户选择学期 ID
  175. */
  176. async function getSelectedSemesterId() {
  177. const currentYear = new Date().getFullYear();
  178. // 绑定验证函数 validateYearInput
  179. const year = await window.AndroidBridgePromise.showPrompt(
  180. "选择学年", "请输入要导入课程的起始学年(例如 2025-2026 应输入2025):", String(currentYear), "validateYearInput"
  181. );
  182. if (!year) return null;
  183. const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
  184. "选择学期", JSON.stringify(["第一学期", "第二学期"]), 0
  185. );
  186. if (semesterIndex === null) return null;
  187. return `${year}-${parseInt(year) + 1}-${semesterIndex + 1}`;
  188. }
  189. // 流程控制
  190. async function runImportFlow() {
  191. try {
  192. const confirmed = await window.AndroidBridgePromise.showAlert(
  193. "公告",
  194. "请确保您已在当前页面成功登录教务系统,否则无法获取数据。是否继续?",
  195. "确认已登录"
  196. );
  197. if (!confirmed) {
  198. AndroidBridge.showToast("导入已取消");
  199. return;
  200. }
  201. const semesterId = await getSelectedSemesterId();
  202. if (!semesterId) {
  203. AndroidBridge.showToast("导入已取消");
  204. return;
  205. }
  206. AndroidBridge.showToast("正在获取教务数据...");
  207. const response = await fetch("https://jw.hniu.cn/jsxsd/xskb/xskb_list.do", {
  208. method: "POST",
  209. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  210. body: `cj0701id=&zc=&demo=&xnxq01id=${semesterId}`,
  211. credentials: "include"
  212. });
  213. const html = await response.text();
  214. const finalCourses = parseTimetableToModel(new DOMParser().parseFromString(html, "text/html"));
  215. if (finalCourses.length === 0) {
  216. AndroidBridge.showToast("未发现任何课程数据,检查是否登录或者学期选择是否正确");
  217. return;
  218. }
  219. // 保存数据
  220. AndroidBridge.showToast("正在保存配置...");
  221. await saveAppConfig();
  222. await saveAppTimeSlots();
  223. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
  224. AndroidBridge.showToast(`成功导入 ${finalCourses.length} 门课程`);
  225. AndroidBridge.notifyTaskCompletion();
  226. } catch (error) {
  227. AndroidBridge.showToast("异常: " + error.message);
  228. }
  229. }
  230. // 启动导入流程
  231. runImportFlow();