capadap.js 16 KB

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