qau_01.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /**
  2. * 青岛农业大学综合教务管理系统(强智科技)
  3. * by ReGoMark, 2026.07.04
  4. *
  5. */
  6. // ─────────────────────────────────────────────
  7. // 工具函数
  8. // ─────────────────────────────────────────────
  9. /**
  10. * 解析周次字符串
  11. * 例如 "1-4,6,11-13" → [1, 2, 3, 4, 6, 11, 12, 13]
  12. */
  13. function parseWeeks(weekStr) {
  14. let weeks = [];
  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. */
  36. function parseSectionFromThHeader(thText) {
  37. let match = thText.match(/第([\d,]+)节/);
  38. if (!match) return null;
  39. let nums = match[1].split(',').map(n => parseInt(n)).filter(n => !isNaN(n));
  40. if (nums.length === 0) return null;
  41. return { start: nums[0], end: nums[nums.length - 1] };
  42. }
  43. // ─────────────────────────────────────────────
  44. // 核心解析
  45. // ─────────────────────────────────────────────
  46. /**
  47. * 从课表页面的 Document 中提取并去重、合并课程数据
  48. */
  49. function extractCoursesFromDoc(doc) {
  50. let parsedCourses = [];
  51. const table = doc.getElementById('kbtable');
  52. if (!table) throw new Error("未找到课表表格(#kbtable),请确认已登录教务系统且当前学期有排课。");
  53. const rows = table.getElementsByTagName('tr');
  54. for (let i = 1; i < rows.length - 1; i++) {
  55. const row = rows[i];
  56. const th = row.querySelector('th');
  57. if (!th) continue;
  58. const sectionInfo = parseSectionFromThHeader(th.innerText || th.textContent);
  59. if (!sectionInfo) continue;
  60. const cells = row.getElementsByTagName('td');
  61. for (let j = 0; j < cells.length; j++) {
  62. const dayOfWeek = j + 1;
  63. const cell = cells[j];
  64. const detailDivs = cell.querySelectorAll('div.kbcontent');
  65. if (detailDivs.length === 0) continue;
  66. detailDivs.forEach(div => {
  67. let htmlContent = div.innerHTML;
  68. if (!htmlContent.trim() || htmlContent.trim() === '&nbsp;') return;
  69. let courseBlocks = htmlContent.split(/-{5,}\s*<br\s*\/?>/i);
  70. courseBlocks.forEach(block => {
  71. if (!block.trim() || block.trim() === '&nbsp;') return;
  72. let tempDiv = doc.createElement('div');
  73. tempDiv.innerHTML = block;
  74. let courseObj = {
  75. day: dayOfWeek,
  76. isCustomTime: false,
  77. startSection: sectionInfo.start,
  78. endSection: sectionInfo.end
  79. };
  80. // 1. 课程名
  81. let lines = tempDiv.innerHTML.split(/<br\s*\/?>/i);
  82. for (let line of lines) {
  83. let cleanLine = line.replace(/<[^>]+>/g, '').trim();
  84. if (cleanLine && cleanLine !== '&nbsp;') {
  85. courseObj.name = cleanLine;
  86. break;
  87. }
  88. }
  89. // 2. 教师(QAU title="老师")
  90. let teacherFont = tempDiv.querySelector('font[title="老师"]');
  91. courseObj.teacher = teacherFont
  92. ? (teacherFont.innerText || teacherFont.textContent).trim()
  93. : "未知";
  94. // 3. 教室
  95. let positionFont = tempDiv.querySelector('font[title="教室"]');
  96. courseObj.position = positionFont
  97. ? (positionFont.innerText || positionFont.textContent).trim()
  98. : "待定";
  99. // 4. 周次
  100. let timeFont = tempDiv.querySelector('font[title="周次(节次)"]');
  101. if (timeFont) {
  102. let timeText = (timeFont.innerText || timeFont.textContent).trim();
  103. let weekMatch = timeText.match(/^(.+?)\(周\)/);
  104. if (weekMatch) {
  105. courseObj.weeks = parseWeeks(weekMatch[1]);
  106. }
  107. }
  108. if (!courseObj.weeks || courseObj.weeks.length === 0) return;
  109. if (!courseObj.name) return;
  110. parsedCourses.push(courseObj);
  111. });
  112. });
  113. }
  114. }
  115. // ── 去重 ──
  116. let uniqueCourses = [];
  117. let courseSet = new Set();
  118. parsedCourses.forEach(course => {
  119. let key = `${course.day}-${course.startSection}-${course.endSection}-${course.name}-${course.weeks.join(',')}`;
  120. if (!courseSet.has(key)) {
  121. courseSet.add(key);
  122. uniqueCourses.push(course);
  123. }
  124. });
  125. // ── 合并相邻节次(最多合并两个大节,即 1-4 节)──
  126. // 条件:同天、同名、同教师、同教室、同周次,节次紧邻,且合并后跨度不超过 4 节
  127. const sorted = uniqueCourses.sort((a, b) => {
  128. if (a.day !== b.day) return a.day - b.day;
  129. if (a.name !== b.name) return a.name.localeCompare(b.name);
  130. const wa = a.weeks.join(','), wb = b.weeks.join(',');
  131. if (wa !== wb) return wa.localeCompare(wb);
  132. return a.startSection - b.startSection;
  133. });
  134. const merged = [];
  135. for (const cur of sorted) {
  136. const prev = merged[merged.length - 1];
  137. const canMerge = prev
  138. && prev.day === cur.day
  139. && prev.name === cur.name
  140. && prev.teacher === cur.teacher
  141. && prev.position === cur.position
  142. && prev.weeks.join(',') === cur.weeks.join(',')
  143. && prev.endSection + 1 === cur.startSection
  144. && (cur.endSection - prev.startSection) <= 3;
  145. if (canMerge) {
  146. prev.endSection = cur.endSection;
  147. } else {
  148. merged.push({ ...cur });
  149. }
  150. }
  151. return merged;
  152. }
  153. // ─────────────────────────────────────────────
  154. // 学校定制数据
  155. // ─────────────────────────────────────────────
  156. /**
  157. * 三个校区的作息时间表
  158. */
  159. const CAMPUS_TIME_SLOTS = {
  160. "青岛校区": [
  161. { "number": 1, "startTime": "08:00", "endTime": "08:45" },
  162. { "number": 2, "startTime": "08:55", "endTime": "09:40" },
  163. { "number": 3, "startTime": "09:55", "endTime": "10:40" },
  164. { "number": 4, "startTime": "10:50", "endTime": "11:35" },
  165. { "number": 5, "startTime": "11:35", "endTime": "12:00" },
  166. { "number": 6, "startTime": "14:00", "endTime": "14:45" },
  167. { "number": 7, "startTime": "14:55", "endTime": "15:40" },
  168. { "number": 8, "startTime": "15:55", "endTime": "16:40" },
  169. { "number": 9, "startTime": "16:50", "endTime": "17:35" },
  170. { "number": 10, "startTime": "18:50", "endTime": "19:35" },
  171. { "number": 11, "startTime": "19:45", "endTime": "20:30" }
  172. ],
  173. "平度校区": [
  174. { "number": 1, "startTime": "08:30", "endTime": "09:15" },
  175. { "number": 2, "startTime": "09:25", "endTime": "10:10" },
  176. { "number": 3, "startTime": "10:20", "endTime": "11:05" },
  177. { "number": 4, "startTime": "11:15", "endTime": "12:00" },
  178. { "number": 5, "startTime": "12:00", "endTime": "12:25" },
  179. { "number": 6, "startTime": "14:00", "endTime": "14:45" },
  180. { "number": 7, "startTime": "14:55", "endTime": "15:40" },
  181. { "number": 8, "startTime": "15:50", "endTime": "16:35" },
  182. { "number": 9, "startTime": "16:45", "endTime": "17:30" },
  183. { "number": 10, "startTime": "18:50", "endTime": "19:35" },
  184. { "number": 11, "startTime": "19:45", "endTime": "20:30" }
  185. ],
  186. "蓝谷校区": [
  187. { "number": 1, "startTime": "08:30", "endTime": "09:15" },
  188. { "number": 2, "startTime": "09:20", "endTime": "10:05" },
  189. { "number": 3, "startTime": "10:15", "endTime": "11:00" },
  190. { "number": 4, "startTime": "11:05", "endTime": "11:50" },
  191. { "number": 5, "startTime": "13:10", "endTime": "13:55" },
  192. { "number": 6, "startTime": "14:00", "endTime": "14:45" },
  193. { "number": 7, "startTime": "14:55", "endTime": "15:40" },
  194. { "number": 8, "startTime": "15:45", "endTime": "16:30" },
  195. { "number": 9, "startTime": "16:35", "endTime": "17:20" },
  196. { "number": 10, "startTime": "18:30", "endTime": "19:15" },
  197. { "number": 11, "startTime": "19:25", "endTime": "20:15" }
  198. ]
  199. };
  200. function getCourseConfig() {
  201. return {
  202. "defaultClassDuration": 45,
  203. "defaultBreakDuration": 5
  204. };
  205. }
  206. // ─────────────────────────────────────────────
  207. // 主流程
  208. // ─────────────────────────────────────────────
  209. async function runImportFlow() {
  210. try {
  211. AndroidBridge.showToast("正在获取课表数据,请稍候...");
  212. const response = await fetch('/jsxsd/xskb/xskb_list.do', { method: 'GET' });
  213. const htmlText = await response.text();
  214. const parser = new DOMParser();
  215. let doc = parser.parseFromString(htmlText, 'text/html');
  216. // 解析学期列表
  217. const selectElem = doc.getElementById('xnxq01id');
  218. let semesters = [];
  219. let semesterValues = [];
  220. let defaultIndex = 0;
  221. if (selectElem) {
  222. const options = selectElem.querySelectorAll('option');
  223. options.forEach((opt, index) => {
  224. semesters.push((opt.innerText || opt.textContent).trim());
  225. semesterValues.push(opt.value);
  226. if (opt.hasAttribute('selected')) {
  227. defaultIndex = index;
  228. }
  229. });
  230. }
  231. // 选择学期
  232. if (semesters.length > 0) {
  233. let selectedIdx = await window.AndroidBridgePromise.showSingleSelection(
  234. "请选择要导入的学期",
  235. JSON.stringify(semesters),
  236. defaultIndex
  237. );
  238. if (selectedIdx === null) {
  239. AndroidBridge.showToast("已取消导入");
  240. return;
  241. }
  242. if (selectedIdx !== defaultIndex) {
  243. AndroidBridge.showToast(`正在获取 [${semesters[selectedIdx]}] 课表...`);
  244. let formData = new URLSearchParams();
  245. formData.append('xnxq01id', semesterValues[selectedIdx]);
  246. const postResponse = await fetch('/jsxsd/xskb/xskb_list.do', {
  247. method: 'POST',
  248. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  249. body: formData.toString()
  250. });
  251. const postHtml = await postResponse.text();
  252. doc = parser.parseFromString(postHtml, 'text/html');
  253. }
  254. }
  255. // 选择校区
  256. const campusNames = Object.keys(CAMPUS_TIME_SLOTS);
  257. const campusIdx = await window.AndroidBridgePromise.showSingleSelection(
  258. "请选择您所在的校区",
  259. JSON.stringify(campusNames),
  260. 0
  261. );
  262. if (campusIdx === null) {
  263. AndroidBridge.showToast("已取消导入");
  264. return;
  265. }
  266. const selectedCampus = campusNames[campusIdx];
  267. const timeSlots = CAMPUS_TIME_SLOTS[selectedCampus];
  268. // 解析课程
  269. const courses = extractCoursesFromDoc(doc);
  270. if (courses.length === 0) {
  271. await window.AndroidBridgePromise.showAlert(
  272. "提示",
  273. "未能解析到任何课程,请检查当前学期是否有排课,或尝试切换学期。",
  274. "好的"
  275. );
  276. return;
  277. }
  278. // 保存
  279. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(getCourseConfig()));
  280. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  281. const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  282. if (!saveResult) {
  283. AndroidBridge.showToast("课程保存失败,请重试!");
  284. return;
  285. }
  286. AndroidBridge.showToast(`成功导入 ${courses.length} 节课程(${selectedCampus}作息)!`);
  287. AndroidBridge.notifyTaskCompletion();
  288. } catch (error) {
  289. AndroidBridge.showToast("导入发生异常: " + error.message);
  290. }
  291. }
  292. runImportFlow();