capadap.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. // 文件: school.js
  2. /**
  3. * 显示导入提示
  4. */
  5. async function promptUserToStart() {
  6. const confirmed = await window.AndroidBridgePromise.showAlert(
  7. "导入提示",
  8. "导入前请确保您已进入课表页面(运行->课表查询->我的课表)并等待页面加载完成",
  9. "开始导入"
  10. );
  11. if (!confirmed) {
  12. AndroidBridge.showToast("用户取消了导入");
  13. return false;
  14. }
  15. AndroidBridge.showToast("开始获取课表数据...");
  16. return true;
  17. }
  18. /**
  19. * 获取 iframe 内容
  20. */
  21. function getIframeDocument() {
  22. try {
  23. console.log('开始获取 iframe 内容');
  24. // 尝试多种选择器找到 iframe
  25. const selectors = [
  26. '.iframe___1hsk7',
  27. '[class*="iframe"]',
  28. 'iframe'
  29. ];
  30. let iframe = null;
  31. for (const selector of selectors) {
  32. iframe = document.querySelector(selector);
  33. if (iframe) {
  34. console.log(`通过选择器 "${selector}" 找到 iframe`);
  35. break;
  36. }
  37. }
  38. if (!iframe) {
  39. console.error('未找到 iframe 元素');
  40. AndroidBridge.showToast("未找到课表框架,请确保在课表页面");
  41. return null;
  42. }
  43. // 获取 iframe 的 document
  44. const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
  45. if (!iframeDoc) {
  46. console.error('无法访问 iframe 内容');
  47. AndroidBridge.showToast("无法访问课表内容,可能页面未加载完成");
  48. return null;
  49. }
  50. // 检查是否包含课表元素
  51. const timetable = iframeDoc.querySelector('.kbappTimetableDayColumnRoot');
  52. if (!timetable) {
  53. console.warn('iframe 中未找到课表元素,可能不在课表页面');
  54. }
  55. return iframeDoc;
  56. } catch (error) {
  57. console.error('获取 iframe 内容时出错:', error);
  58. AndroidBridge.showToast(`获取课表失败: ${error.message}`);
  59. return null;
  60. }
  61. }
  62. function getSectionByPosition(element) {
  63. const dayColumn = element.closest('.kbappTimetableDayColumnRoot');
  64. const dayCols = Array.from(dayColumn.parentNode.children);
  65. const day = dayCols.indexOf(dayColumn) + 1;
  66. let slotBlock = element;
  67. while (slotBlock.parentElement && slotBlock.parentElement !== dayColumn) {
  68. slotBlock = slotBlock.parentElement;
  69. }
  70. const win = element.ownerDocument.defaultView || window;
  71. const getFlex = (el) => {
  72. const fg = win.getComputedStyle(el).flexGrow;
  73. return Math.round(parseFloat(fg || 0));
  74. };
  75. let previousFlexSum = 0;
  76. let curr = slotBlock.previousElementSibling;
  77. while (curr) {
  78. previousFlexSum += getFlex(curr);
  79. curr = curr.previousElementSibling;
  80. }
  81. const currentFlex = getFlex(slotBlock);
  82. // 换算
  83. let start = previousFlexSum + 1;
  84. let end = start + Math.max(1, currentFlex) - 1;
  85. // 午餐晚餐修正节数
  86. if (start >= 10) {
  87. start -= 2;
  88. end -= 2;
  89. } else if (start > 5) {
  90. start -= 1;
  91. end -= 1;
  92. }
  93. return { day, start, end };
  94. }
  95. /**
  96. * 解析时间段数据
  97. */
  98. function parseTimeSlots(iframeDoc) {
  99. const timeSlots = [];
  100. // 查找时间段列
  101. const timeColumn = iframeDoc.querySelector('.kbappTimetableJcColumn');
  102. const timeItems = timeColumn.querySelectorAll('.kbappTimetableJcItem');
  103. console.log('找到时间段数量:', timeItems.length);
  104. timeItems.forEach((item, index) => {
  105. const textElements = item.querySelectorAll('.kbappTimetableJcItemText');
  106. if (textElements.length >= 2) {
  107. const sectionName = textElements[0]?.textContent?.trim() || `第${index + 1}节`;
  108. const timeRange = textElements[1]?.textContent?.trim() || '';
  109. // 解析时间范围
  110. const timeMatch = timeRange.match(/(\d{2}:\d{2})[~-](\d{2}:\d{2})/);
  111. if (timeMatch) {
  112. const startTime = timeMatch[1];
  113. const endTime = timeMatch[2];
  114. // 提取节次数字
  115. const sectionMatch = sectionName.match(/第(\d+)节/);
  116. const sectionNumber = sectionMatch ? parseInt(sectionMatch[1]) : index + 1;
  117. timeSlots.push({
  118. number: sectionNumber,
  119. startTime: startTime,
  120. endTime: endTime
  121. });
  122. console.log(`时间段 ${sectionNumber}: ${startTime} ~ ${endTime}`);
  123. }
  124. }
  125. });
  126. return timeSlots;
  127. }
  128. /**
  129. * 解析周次信息
  130. */
  131. function parseWeeks(text) {
  132. const weeks = [];
  133. // 匹配如 1-16, 1, 3, 5-7 等模式
  134. const patterns = text.match(/(\d+)-(\d+)周|(\d+)周/g);
  135. if (!patterns) return weeks;
  136. const isSingle = text.includes('(单)');
  137. const isDouble = text.includes('(双)');
  138. patterns.forEach(p => {
  139. const range = p.match(/(\d+)-(\d+)/);
  140. if (range) {
  141. const start = parseInt(range[1]);
  142. const end = parseInt(range[2]);
  143. for (let i = start; i <= end; i++) {
  144. if (isSingle && i % 2 === 0) continue;
  145. if (isDouble && i % 2 !== 0) continue;
  146. weeks.push(i);
  147. }
  148. } else {
  149. const single = p.match(/(\d+)/);
  150. if (single) weeks.push(parseInt(single[1]));
  151. }
  152. });
  153. return weeks;
  154. }
  155. /**
  156. * 解析单个课程信息
  157. */
  158. // 这里源数据使用了el - popover 和el - popover__reference两种模式 一种是弹窗还要一种是课程块
  159. // 我这里解析就只用了第一种popover 因为显示的数据精简 直接可以使用
  160. function parseSingleCourse(courseElement, day, timeSlots) {
  161. try {
  162. const infoTexts = courseElement.querySelectorAll('.kbappTimetableCourseRenderCourseItemInfoText');
  163. if (infoTexts.length < 2) return null;
  164. // 课程名称
  165. let nameElement = courseElement.querySelector('.kbappTimetableCourseRenderCourseItemName');
  166. let rawName = nameElement ? nameElement.innerText.trim() : courseElement.innerText.split('\n')[0].trim();
  167. let name = rawName.replace(/\[.*?\]/g, "").replace(/\s+\d+$/, "").trim();
  168. if (name === "未知课程" || !name) return;
  169. // 获取持续时间
  170. const duration = parseInt(courseElement.getAttribute('data-scales-span') || '1');
  171. // 计算起始节次
  172. let startSection = 1;
  173. const parent = courseElement.closest('.kbappTimetableCourseRenderColumn');
  174. if (parent) {
  175. const containers = parent.querySelectorAll('.kbappTimetableCourseRenderCourseItemContainer');
  176. for (let i = 0; i < containers.length; i++) {
  177. const container = containers[i];
  178. const courseInContainer = container.querySelector('.kbappTimetableCourseRenderCourseItem');
  179. if (courseInContainer === courseElement) {
  180. const flexMatch = container.style.flex?.match(/(\d+)/);
  181. if (flexMatch) {
  182. let totalPrevSpan = 0;
  183. for (let j = 0; j < i; j++) {
  184. const prevFlex = containers[j].style.flex?.match(/(\d+)/);
  185. if (prevFlex) {
  186. totalPrevSpan += parseInt(prevFlex[1]);
  187. }
  188. }
  189. startSection = totalPrevSpan + 1;
  190. }
  191. break;
  192. }
  193. }
  194. }
  195. // 计算结束节次
  196. const endSection = startSection + duration - 1;
  197. // 验证范围
  198. const validStart = Math.max(1, Math.min(startSection, timeSlots?.length || 12));
  199. const validEnd = Math.max(validStart, Math.min(endSection, timeSlots?.length || 12));
  200. return {
  201. name: name,
  202. teacher: '未知教师', // 暂时用默认值
  203. position: '未知教室', // 暂时用默认值
  204. day: day,
  205. startSection: validStart,
  206. endSection: validEnd,
  207. weeks: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18], // 暂时用默认值
  208. isCustomTime: false
  209. };
  210. } catch (error) {
  211. console.error('解析出错:', error);
  212. return null;
  213. }
  214. }
  215. /**
  216. * 解析课程数据
  217. */
  218. function parseCourses(iframeDoc, timeSlots) {
  219. const courses = [];
  220. // 获取所有星期列
  221. const dayColumns = iframeDoc.querySelectorAll('.kbappTimetableDayColumnRoot');
  222. console.log('找到课表列数量:', dayColumns.length);
  223. // 遍历每一天的列
  224. for (let dayIndex = 0; dayIndex < dayColumns.length; dayIndex++) {
  225. const dayColumn = dayColumns[dayIndex];
  226. // 获取当天的所有课程
  227. const dayCourses = dayColumn.querySelectorAll('.kbappTimetableCourseRenderCourseItem');
  228. console.log(`星期${dayIndex + 1} 课程数量:`, dayCourses.length);
  229. dayCourses.forEach(courseElement => {
  230. const courseInfo = parseSingleCourse(courseElement, dayIndex + 1, timeSlots);
  231. if (courseInfo) {
  232. courses.push(courseInfo);
  233. }
  234. });
  235. }
  236. return courses;
  237. }
  238. /**
  239. * 解析所有数据
  240. */
  241. function parseAllData(iframeDoc) {
  242. const timeSlots = parseTimeSlots(iframeDoc);
  243. const courses = [];
  244. const courseElements = iframeDoc.querySelectorAll('.kbappTimetableCourseRenderCourseItem');
  245. courseElements.forEach(element => {
  246. try {
  247. const popoverId = element.getAttribute('aria-describedby');
  248. const popover = iframeDoc.getElementById(popoverId);
  249. if (!popover) return;
  250. const nameElement = popover.querySelector('.kbappTimetableCourseRenderCourseItemInfoPopperInfo');
  251. const name = nameElement ? nameElement.textContent.trim().replace(/\[.*?\]/g, "") : "";
  252. if (!name) return;
  253. // 获取位置信息
  254. const sectionInfo = getSectionByPosition(element);
  255. // --- 关键修正:获取所有信息行 (处理单双周不同行的情况) ---
  256. const infoItems = Array.from(popover.querySelectorAll('.kbappTimetableCourseRenderCourseItemInfoPopperInfo')).slice(1);
  257. infoItems.forEach(item => {
  258. const detailStr = item.textContent.trim();
  259. if (!detailStr) return;
  260. const parts = detailStr.split(/\s+/).filter(p => p.length > 0);
  261. let teacher = "未知教师";
  262. let posParts = [];
  263. let currentWeeks = parseWeeks(detailStr);
  264. parts.forEach(p => {
  265. if (p.includes('周')) return;
  266. // 老师判定:2-4个字且不含地点特征词
  267. if (/^[\u4e00-\u9fa5]{2,4}$/.test(p) && !/(楼|校区|室|场|馆|中心)/.test(p)) {
  268. teacher = p;
  269. } else {
  270. posParts.push(p);
  271. }
  272. });
  273. // 地点去重:选最长的描述
  274. let position = posParts.sort((a, b) => b.length - a.length)[0] || "未知教室";
  275. courses.push({
  276. name: name,
  277. teacher: teacher,
  278. position: position,
  279. day: sectionInfo.day,
  280. startSection: sectionInfo.start,
  281. endSection: sectionInfo.end,
  282. weeks: currentWeeks
  283. });
  284. });
  285. } catch (e) { console.error("解析单条课程失败:", e); }
  286. });
  287. return { courses: removeDuplicates(courses), timeSlots };
  288. }
  289. /**
  290. * 课程去重 后期这里可能会出现问题
  291. */
  292. function removeDuplicates(courses) {
  293. const seen = new Set();
  294. return courses.filter(course => {
  295. // 核心唯一键:星期 + 起始节次 + 课程名 + 周次
  296. // 这样即使老师或地点写法稍有不同,只要是同一时间同一门课且周次一致,就会被去重
  297. const key = `${course.day}-${course.startSection}-${course.name}-${course.weeks.join(',')}`;
  298. if (seen.has(key)) {
  299. return false;
  300. }
  301. seen.add(key);
  302. return true;
  303. });
  304. }
  305. /**
  306. * 保存课程数据
  307. */
  308. async function saveCourses(parsedData) {
  309. const { courses, timeSlots } = parsedData;
  310. try {
  311. AndroidBridge.showToast(`准备保存 ${courses.length} 门课程...`);
  312. // 保存课程数据
  313. const courseSaveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  314. if (!courseSaveResult) {
  315. AndroidBridge.showToast("保存课程失败");
  316. return false;
  317. }
  318. AndroidBridge.showToast(`成功导入 ${courses.length} 门课程`);
  319. // 保存时间段数据
  320. if (timeSlots && timeSlots.length > 0) {
  321. const timeSlotSaveResult = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  322. if (timeSlotSaveResult) {
  323. AndroidBridge.showToast(`成功导入 ${timeSlots.length} 个时间段`);
  324. } else {
  325. AndroidBridge.showToast("时间段导入失败,课程仍可使用");
  326. }
  327. }
  328. return true;
  329. } catch (error) {
  330. console.error("保存课程数据时出错:", error);
  331. AndroidBridge.showToast(`保存失败: ${error.message}`);
  332. return false;
  333. }
  334. }
  335. /**
  336. * 运行主函数
  337. */
  338. async function runImportFlow() {
  339. try {
  340. AndroidBridge.showToast("课表导入工具启动...");
  341. // 1. 显示导入提示
  342. const shouldProceed = await promptUserToStart();
  343. if (!shouldProceed) return;
  344. // 2. 等待一下确保页面加载
  345. await new Promise(resolve => setTimeout(resolve, 1000));
  346. // 3. 获取 iframe 内容
  347. const iframeDoc = getIframeDocument();
  348. if (!iframeDoc) return;
  349. // 4. 解析数据
  350. AndroidBridge.showToast("正在解析课表数据...");
  351. const parsedData = parseAllData(iframeDoc);
  352. if (parsedData.courses.length === 0) {
  353. await window.AndroidBridgePromise.showAlert(
  354. "解析失败",
  355. "未找到任何课程数据,请确认:\n1. 已在课表查询页面\n2. 课表已完全加载\n3. 当前学期有课程",
  356. "知道了"
  357. );
  358. return;
  359. }
  360. // 5. 显示预览
  361. const previewMsg = `找到 ${parsedData.courses.length} 门课程\n${parsedData.timeSlots.length} 个时间段\n\n是否继续导入?`;
  362. const confirmed = await window.AndroidBridgePromise.showAlert(
  363. "导入确认",
  364. previewMsg,
  365. "确认导入"
  366. );
  367. if (!confirmed) {
  368. AndroidBridge.showToast("已取消导入");
  369. return;
  370. }
  371. // 6. 保存数据
  372. const saveSuccess = await saveCourses(parsedData);
  373. if (!saveSuccess) return;
  374. // 7. 完成
  375. AndroidBridge.showToast("课表导入完成!");
  376. AndroidBridge.notifyTaskCompletion();
  377. } catch (error) {
  378. console.error("导入流程出错:", error);
  379. AndroidBridge.showToast(`导入失败: ${error.message}`);
  380. }
  381. }
  382. // 启动导入流程
  383. runImportFlow();