qau_01.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. /**
  2. * 青岛农业大学综合教务管理系统(强智科技)
  3. * by ReGoMark, 2026.07.03
  4. */
  5. // ─────────────────────────────────────────────
  6. // 工具函数
  7. // ─────────────────────────────────────────────
  8. /**
  9. * 解析周次字符串
  10. * 例如 "1-4,6,11-13" → [1, 2, 3, 4, 6, 11, 12, 13]
  11. */
  12. function parseWeeks(weekStr) {
  13. let weeks = [];
  14. // 去掉括号内的修饰文字,如 "(限选)" "(必修)"
  15. weekStr = weekStr.replace(/\(.*?\)/g, '').trim();
  16. let parts = weekStr.split(',');
  17. for (let part of parts) {
  18. part = part.trim();
  19. if (part.includes('-')) {
  20. let [start, end] = part.split('-');
  21. for (let i = parseInt(start); i <= parseInt(end); i++) {
  22. if (!weeks.includes(i)) weeks.push(i);
  23. }
  24. } else {
  25. let w = parseInt(part);
  26. if (!isNaN(w) && !weeks.includes(w)) weeks.push(w);
  27. }
  28. }
  29. return weeks.sort((a, b) => a - b);
  30. }
  31. /**
  32. * 解析行标题中的节次范围
  33. * 例如 "第1,2节" → { start: 1, end: 2 }
  34. * "第5节" → { start: 5, end: 5 }
  35. * "第8,9节" → { start: 8, end: 9 }
  36. */
  37. function parseSectionFromThHeader(thText) {
  38. // 匹配形如 "第1,2节" 或 "第5节" 或 "第10,11节"
  39. let match = thText.match(/第([\d,]+)节/);
  40. if (!match) return null;
  41. let nums = match[1].split(',').map(n => parseInt(n)).filter(n => !isNaN(n));
  42. if (nums.length === 0) return null;
  43. return { start: nums[0], end: nums[nums.length - 1] };
  44. }
  45. // ─────────────────────────────────────────────
  46. // 核心解析
  47. // ─────────────────────────────────────────────
  48. /**
  49. * 从课表页面的 Document 中提取并去重课程数据
  50. */
  51. function extractCoursesFromDoc(doc) {
  52. let parsedCourses = [];
  53. const table = doc.getElementById('kbtable');
  54. if (!table) throw new Error("未找到课表表格(#kbtable),请确认已登录教务系统且当前学期有排课。");
  55. const rows = table.getElementsByTagName('tr');
  56. // 跳过表头行(0),从第1行开始(节次数据行)
  57. // 最后一行是备注行(colspan=7),也跳过
  58. for (let i = 1; i < rows.length - 1; i++) {
  59. const row = rows[i];
  60. // 从该行的 <th> 解析节次范围
  61. const th = row.querySelector('th');
  62. if (!th) continue;
  63. const sectionInfo = parseSectionFromThHeader(th.innerText || th.textContent);
  64. if (!sectionInfo) continue;
  65. // 遍历该行的 7 个 <td>(周一到周日)
  66. const cells = row.getElementsByTagName('td');
  67. for (let j = 0; j < cells.length; j++) {
  68. const dayOfWeek = j + 1; // 1=周一 … 7=周日
  69. const cell = cells[j];
  70. // 读取详版 div(class="kbcontent",含教师信息)
  71. // 注意:不选 kbcontent1(简版,无教师)
  72. const detailDivs = cell.querySelectorAll('div.kbcontent');
  73. if (detailDivs.length === 0) continue;
  74. detailDivs.forEach(div => {
  75. let htmlContent = div.innerHTML;
  76. if (!htmlContent.trim() || htmlContent.trim() === '&nbsp;') return;
  77. // 同一格多门课之间用连续破折号 + <br> 分隔
  78. let courseBlocks = htmlContent.split(/-{5,}\s*<br\s*\/?>/i);
  79. courseBlocks.forEach(block => {
  80. if (!block.trim() || block.trim() === '&nbsp;') return;
  81. let tempDiv = doc.createElement('div');
  82. tempDiv.innerHTML = block;
  83. let courseObj = {
  84. day: dayOfWeek,
  85. isCustomTime: false,
  86. startSection: sectionInfo.start,
  87. endSection: sectionInfo.end
  88. };
  89. // 1. 提取课程名:取第一行非空纯文本
  90. let lines = tempDiv.innerHTML.split(/<br\s*\/?>/i);
  91. for (let line of lines) {
  92. let cleanLine = line.replace(/<[^>]+>/g, '').trim();
  93. if (cleanLine && cleanLine !== '&nbsp;') {
  94. courseObj.name = cleanLine;
  95. break;
  96. }
  97. }
  98. // 2. 提取教师(QAU 的 title 是 "老师",不是 "教师")
  99. let teacherFont = tempDiv.querySelector('font[title="老师"]');
  100. courseObj.teacher = teacherFont
  101. ? (teacherFont.innerText || teacherFont.textContent).trim()
  102. : "未知";
  103. // 3. 提取教室
  104. let positionFont = tempDiv.querySelector('font[title="教室"]');
  105. courseObj.position = positionFont
  106. ? (positionFont.innerText || positionFont.textContent).trim()
  107. : "待定";
  108. // 4. 提取周次
  109. // QAU 格式:<font title='周次(节次)'>1-4,6,11-13(周)(限选)</font>
  110. let timeFont = tempDiv.querySelector('font[title="周次(节次)"]');
  111. if (timeFont) {
  112. let timeText = (timeFont.innerText || timeFont.textContent).trim();
  113. // 提取 "(周)" 之前的部分作为周次
  114. let weekMatch = timeText.match(/^(.+?)\(周\)/);
  115. if (weekMatch) {
  116. courseObj.weeks = parseWeeks(weekMatch[1]);
  117. }
  118. }
  119. if (!courseObj.weeks || courseObj.weeks.length === 0) return;
  120. if (!courseObj.name) return;
  121. parsedCourses.push(courseObj);
  122. });
  123. });
  124. }
  125. }
  126. // ── 去重 ──
  127. let uniqueCourses = [];
  128. let courseSet = new Set();
  129. parsedCourses.forEach(course => {
  130. let key = `${course.day}-${course.startSection}-${course.endSection}-${course.name}-${course.weeks.join(',')}`;
  131. if (!courseSet.has(key)) {
  132. courseSet.add(key);
  133. uniqueCourses.push(course);
  134. }
  135. });
  136. return uniqueCourses;
  137. }
  138. // ─────────────────────────────────────────────
  139. // 学校定制数据
  140. // ─────────────────────────────────────────────
  141. /**
  142. * 青岛农业大学作息时间表
  143. */
  144. function getPresetTimeSlots() {
  145. return [
  146. { "number": 1, "startTime": "08:00", "endTime": "08:45" },
  147. { "number": 2, "startTime": "08:55", "endTime": "09:40" },
  148. { "number": 3, "startTime": "09:55", "endTime": "10:40" },
  149. { "number": 4, "startTime": "10:55", "endTime": "11:35" },
  150. { "number": 5, "startTime": "11:35", "endTime": "12:00" },
  151. { "number": 6, "startTime": "14:00", "endTime": "14:45" },
  152. { "number": 7, "startTime": "14:55", "endTime": "15:40" },
  153. { "number": 8, "startTime": "15:55", "endTime": "16:40" },
  154. { "number": 9, "startTime": "16:50", "endTime": "17:35" },
  155. { "number": 10, "startTime": "18:50", "endTime": "19:35" },
  156. { "number": 11, "startTime": "19:45", "endTime": "20:30" }
  157. ];
  158. }
  159. function getCourseConfig() {
  160. return {
  161. "defaultClassDuration": 45,
  162. "defaultBreakDuration": 5
  163. };
  164. }
  165. // ─────────────────────────────────────────────
  166. // 主流程
  167. // ─────────────────────────────────────────────
  168. async function runImportFlow() {
  169. try {
  170. AndroidBridge.showToast("正在获取课表数据,请稍候...");
  171. const response = await fetch('/jsxsd/xskb/xskb_list.do', { method: 'GET' });
  172. const htmlText = await response.text();
  173. const parser = new DOMParser();
  174. let doc = parser.parseFromString(htmlText, 'text/html');
  175. // 解析学期列表
  176. const selectElem = doc.getElementById('xnxq01id');
  177. let semesters = [];
  178. let semesterValues = [];
  179. let defaultIndex = 0;
  180. if (selectElem) {
  181. const options = selectElem.querySelectorAll('option');
  182. options.forEach((opt, index) => {
  183. semesters.push((opt.innerText || opt.textContent).trim());
  184. semesterValues.push(opt.value);
  185. if (opt.hasAttribute('selected')) {
  186. defaultIndex = index;
  187. }
  188. });
  189. }
  190. // 让用户选择学期
  191. if (semesters.length > 0) {
  192. let selectedIdx = await window.AndroidBridgePromise.showSingleSelection(
  193. "请选择要导入的学期",
  194. JSON.stringify(semesters),
  195. defaultIndex
  196. );
  197. if (selectedIdx === null) {
  198. AndroidBridge.showToast("已取消导入");
  199. return;
  200. }
  201. if (selectedIdx !== defaultIndex) {
  202. AndroidBridge.showToast(`正在获取 [${semesters[selectedIdx]}] 课表...`);
  203. let formData = new URLSearchParams();
  204. formData.append('xnxq01id', semesterValues[selectedIdx]);
  205. const postResponse = await fetch('/jsxsd/xskb/xskb_list.do', {
  206. method: 'POST',
  207. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  208. body: formData.toString()
  209. });
  210. const postHtml = await postResponse.text();
  211. doc = parser.parseFromString(postHtml, 'text/html');
  212. }
  213. }
  214. const courses = extractCoursesFromDoc(doc);
  215. if (courses.length === 0) {
  216. await window.AndroidBridgePromise.showAlert(
  217. "提示",
  218. "未能解析到任何课程,请检查当前学期是否有排课,或尝试切换学期。",
  219. "好的"
  220. );
  221. return;
  222. }
  223. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(getCourseConfig()));
  224. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(getPresetTimeSlots()));
  225. const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  226. if (!saveResult) {
  227. AndroidBridge.showToast("课程保存失败,请重试!");
  228. return;
  229. }
  230. AndroidBridge.showToast(`成功导入 ${courses.length} 节课程及作息时间!`);
  231. AndroidBridge.notifyTaskCompletion();
  232. } catch (error) {
  233. AndroidBridge.showToast("导入发生异常: " + error.message);
  234. }
  235. }
  236. runImportFlow();