capadap.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  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 extractStartDate() {
  65. const iframdate = getIframeDocument();
  66. if (!iframdate) return null;
  67. try {
  68. const dayElement = iframdate.querySelector('.kbappTimeZCText'); //<div class="kbappTimeZCText">第1周(3/9 ~ 3/15)</div>
  69. const semesterElement = iframdate.querySelector('.kbappTimeXQText'); //<div class="kbappTimeXQText">2025-2026学年 第2学期</div>
  70. if (!dayElement || !semesterElement) {
  71. return null;
  72. }
  73. const dayText = dayElement.textContent.trim(); // 第1周(3/9 ~ 3/15)
  74. const semesterText = semesterElement.textContent.trim(); // 2025-2026学年 第2学期
  75. const startDate = parseStartDate(dayText, semesterText);
  76. // 要判断是第几学期来选择开学年
  77. return {startDate}; //传入解析后数据
  78. }
  79. catch (error) {
  80. console.error('解析开学时间时出错:', error);
  81. AndroidBridge.showToast(`解析开学时间失败: ${error.message}`);
  82. return null;
  83. }
  84. }
  85. /**
  86. * 解析开学时间
  87. * @param {string} weekText - 周次文本,如 "第1周(3/9 ~ 3/15)"
  88. * @param {string} semesterText - 学期文本,如 "2025-2026学年 第2学期"
  89. * @returns {string} 开学日期 YYYY-MM-DD
  90. */
  91. function parseStartDate(weekText, semesterText) {
  92. // 1. 解析学期信息,获取学年和学期
  93. const semesterMatch = semesterText.match(/(\d{4})-(\d{4})学年\s*第(\d)学期/);
  94. if (!semesterMatch) {
  95. throw new Error('无法解析学期信息');
  96. }
  97. const startYear = parseInt(semesterMatch[1]); // 2025
  98. const endYear = parseInt(semesterMatch[2]); // 2026
  99. const semester = parseInt(semesterMatch[3]); // 1 或 2
  100. // 2. 解析周次信息,获取月份和日期范围
  101. const weekMatch = weekText.match(/第(\d+)周\((\d{1,2})\/(\d{1,2})\s*~\s*(\d{1,2})\/(\d{1,2})\)/);
  102. if (!weekMatch) {
  103. throw new Error('无法解析周次信息');
  104. }
  105. const weekNumber = parseInt(weekMatch[1]); // 周数
  106. const startMonth = parseInt(weekMatch[2]); // 开始月份
  107. const startDay = parseInt(weekMatch[3]); // 开始日期
  108. const endMonth = parseInt(weekMatch[4]); // 结束月份
  109. const endDay = parseInt(weekMatch[5]); // 结束日期
  110. console.log(`解析结果: 第${weekNumber}周, ${startMonth}/${startDay} ~ ${endMonth}/${endDay}`);
  111. // 3. 根据学期判断开学年份
  112. let startYearForDate;
  113. if (semester === 1) {
  114. // 第一学期:开学在 startYear 年
  115. startYearForDate = startYear;
  116. } else {
  117. // 第二学期:开学在 endYear 年(通常跨年)
  118. startYearForDate = endYear;
  119. }
  120. // 特殊情况处理:如果开始月份小于当前月份,可能需要调整年份
  121. // 比如 1月开学应该是 endYear 年
  122. const currentMonth = new Date().getMonth() + 1;
  123. if (startMonth < 6 && semester === 2) {
  124. // 第二学期如果在1-6月开学,应该用 endYear
  125. startYearForDate = endYear;
  126. }
  127. // 4. 构建开学日期(假设是第1周的周一,或者就用开始日期)
  128. // 这里用开始日期作为参考
  129. const startDateStr = `${startYearForDate}-${String(startMonth).padStart(2, '0')}-${String(startDay).padStart(2, '0')}`;
  130. // 5. 如果是第1周,直接返回开始日期
  131. if (weekNumber === 1) {
  132. console.log(`开学日期: ${startDateStr}`);
  133. return startDateStr;
  134. }
  135. // 6. 如果不是第1周,需要往前推算
  136. // 计算第1周的日期
  137. const startDate = new Date(startYearForDate, startMonth - 1, startDay);
  138. const daysToSubtract = (weekNumber - 1) * 7;
  139. startDate.setDate(startDate.getDate() - daysToSubtract);
  140. const firstWeekStartDate = formatDate(startDate);
  141. return firstWeekStartDate;
  142. }
  143. /**
  144. * 格式化日期为 YYYY-MM-DD
  145. */
  146. function formatDate(date) {
  147. const year = date.getFullYear();
  148. const month = String(date.getMonth() + 1).padStart(2, '0');
  149. const day = String(date.getDate()).padStart(2, '0');
  150. return `${year}-${month}-${day}`;
  151. }
  152. /**
  153. * 计算每天课程节数
  154. **/
  155. function getSectionByPosition(element) {
  156. const dayColumn = element.closest('.kbappTimetableDayColumnRoot');
  157. const dayCols = Array.from(dayColumn.parentNode.children);
  158. const day = dayCols.indexOf(dayColumn) + 1;
  159. let slotBlock = element;
  160. while (slotBlock.parentElement && slotBlock.parentElement !== dayColumn) {
  161. slotBlock = slotBlock.parentElement;
  162. }
  163. const win = element.ownerDocument.defaultView || window;
  164. const getFlex = (el) => {
  165. const fg = win.getComputedStyle(el).flexGrow;
  166. return Math.round(parseFloat(fg || 0));
  167. };
  168. let previousFlexSum = 0;
  169. let curr = slotBlock.previousElementSibling;
  170. while (curr) {
  171. previousFlexSum += getFlex(curr);
  172. curr = curr.previousElementSibling;
  173. }
  174. const currentFlex = getFlex(slotBlock);
  175. // 换算
  176. let start = previousFlexSum + 1;
  177. let end = start + Math.max(1, currentFlex) - 1;
  178. // 这里的的start和end都加了午餐晚餐 午餐晚餐修正节数
  179. if (start >= 10) {
  180. start -= 2;
  181. end -= 2;
  182. } else if (start > 5) {
  183. start -= 1;
  184. end -= 1;
  185. }
  186. return { day, start, end };
  187. }
  188. /**
  189. * 解析时间段数据
  190. */
  191. function parseTimeSlots(iframeDoc) {
  192. const timeSlots = [];
  193. // 查找时间段列
  194. const timeColumn = iframeDoc.querySelector('.kbappTimetableJcColumn');
  195. const timeItems = timeColumn.querySelectorAll('.kbappTimetableJcItem');
  196. timeItems.forEach((item, index) => {
  197. const textElements = item.querySelectorAll('.kbappTimetableJcItemText');
  198. if (textElements.length >= 2) {
  199. const sectionName = textElements[0]?.textContent?.trim() || `第${index + 1}节`;
  200. const timeRange = textElements[1]?.textContent?.trim() || '';
  201. // 解析时间范围
  202. const timeMatch = timeRange.match(/(\d{2}:\d{2})[~-](\d{2}:\d{2})/);
  203. if (timeMatch) {
  204. const startTime = timeMatch[1];
  205. const endTime = timeMatch[2];
  206. // 提取节次数字
  207. const sectionMatch = sectionName.match(/第(\d+)节/);
  208. const sectionNumber = sectionMatch ? parseInt(sectionMatch[1]) : index + 1;
  209. timeSlots.push({
  210. number: sectionNumber,
  211. startTime: startTime,
  212. endTime: endTime
  213. });
  214. // console.log(`时间段 ${sectionNumber}: ${startTime} ~ ${endTime}`);
  215. }
  216. }
  217. });
  218. return timeSlots;
  219. }
  220. /**
  221. * 解析周次信息
  222. */
  223. function parseWeeks(text) {
  224. const weeks = [];
  225. // 匹配如 1-16, 1, 3, 5-7 等模式
  226. const patterns = text.match(/(\d+)-(\d+)周|(\d+)周/g);
  227. if (!patterns) return weeks;
  228. const isSingle = text.includes('(单)');
  229. const isDouble = text.includes('(双)');
  230. patterns.forEach(p => {
  231. const range = p.match(/(\d+)-(\d+)/);
  232. if (range) {
  233. const start = parseInt(range[1]);
  234. const end = parseInt(range[2]);
  235. for (let i = start; i <= end; i++) {
  236. if (isSingle && i % 2 === 0) continue;
  237. if (isDouble && i % 2 !== 0) continue;
  238. weeks.push(i);
  239. }
  240. } else {
  241. const single = p.match(/(\d+)/);
  242. if (single) weeks.push(parseInt(single[1]));
  243. }
  244. });
  245. return weeks;
  246. }
  247. /**
  248. * 解析单个课程信息
  249. */
  250. // 这里源数据使用了el - popover 和el - popover__reference两种模式 一种是弹窗还要一种是课程块
  251. // 我这里解析就只用了第一种popover 因为显示的数据精简 直接可以使用
  252. function parseSingleCourse(courseElement, day, timeSlots) {
  253. try {
  254. const infoTexts = courseElement.querySelectorAll('.kbappTimetableCourseRenderCourseItemInfoText');
  255. if (infoTexts.length < 2) return null;
  256. // 课程名称
  257. let nameElement = courseElement.querySelector('.kbappTimetableCourseRenderCourseItemName');
  258. let rawName = nameElement ? nameElement.innerText.trim() : courseElement.innerText.split('\n')[0].trim();
  259. let name = rawName.replace(/\[.*?\]/g, "").replace(/\s+\d+$/, "").trim();
  260. if (name === "未知课程" || !name) return;
  261. // 获取持续时间
  262. const duration = parseInt(courseElement.getAttribute('data-scales-span') || '1');
  263. // 计算起始节次
  264. let startSection = 1;
  265. const parent = courseElement.closest('.kbappTimetableCourseRenderColumn');
  266. if (parent) {
  267. const containers = parent.querySelectorAll('.kbappTimetableCourseRenderCourseItemContainer');
  268. for (let i = 0; i < containers.length; i++) {
  269. const container = containers[i];
  270. const courseInContainer = container.querySelector('.kbappTimetableCourseRenderCourseItem');
  271. if (courseInContainer === courseElement) {
  272. const flexMatch = container.style.flex?.match(/(\d+)/);
  273. if (flexMatch) {
  274. let totalPrevSpan = 0;
  275. for (let j = 0; j < i; j++) {
  276. const prevFlex = containers[j].style.flex?.match(/(\d+)/);
  277. if (prevFlex) {
  278. totalPrevSpan += parseInt(prevFlex[1]);
  279. }
  280. }
  281. startSection = totalPrevSpan + 1;
  282. }
  283. break;
  284. }
  285. }
  286. }
  287. // 计算结束节次
  288. const endSection = startSection + duration - 1;
  289. // 验证范围
  290. const validStart = Math.max(1, Math.min(startSection, timeSlots?.length || 12));
  291. const validEnd = Math.max(validStart, Math.min(endSection, timeSlots?.length || 12));
  292. return {
  293. name: name,
  294. teacher: '未知教师', // 暂时用默认值
  295. position: '未知教室', // 暂时用默认值
  296. day: day,
  297. startSection: validStart,
  298. endSection: validEnd,
  299. weeks: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18], // 暂时用默认值
  300. isCustomTime: false
  301. };
  302. } catch (error) {
  303. // console.error('解析出错:', error);
  304. return null;
  305. }
  306. }
  307. /**
  308. * 解析课程数据
  309. */
  310. function parseCourses(iframeDoc, timeSlots) {
  311. const courses = [];
  312. // 获取所有星期列
  313. const dayColumns = iframeDoc.querySelectorAll('.kbappTimetableDayColumnRoot');
  314. // console.log('找到课表列数量:', dayColumns.length);
  315. // 遍历每一天的列
  316. for (let dayIndex = 0; dayIndex < dayColumns.length; dayIndex++) {
  317. const dayColumn = dayColumns[dayIndex];
  318. // 获取当天的所有课程
  319. const dayCourses = dayColumn.querySelectorAll('.kbappTimetableCourseRenderCourseItem');
  320. // console.log(`星期${dayIndex + 1} 课程数量:`, dayCourses.length);
  321. dayCourses.forEach(courseElement => {
  322. const courseInfo = parseSingleCourse(courseElement, dayIndex + 1, timeSlots);
  323. if (courseInfo) {
  324. courses.push(courseInfo);
  325. }
  326. });
  327. }
  328. return courses;
  329. }
  330. /**
  331. * 解析所有数据
  332. */
  333. function parseAllData(iframeDoc) {
  334. const timeSlots = parseTimeSlots(iframeDoc);
  335. const courses = [];
  336. const courseElements = iframeDoc.querySelectorAll('.kbappTimetableCourseRenderCourseItem');
  337. courseElements.forEach(element => {
  338. try {
  339. const popoverId = element.getAttribute('aria-describedby');
  340. const popover = iframeDoc.getElementById(popoverId);
  341. if (!popover) return;
  342. const nameElement = popover.querySelector('.kbappTimetableCourseRenderCourseItemInfoPopperInfo');
  343. const name = nameElement ? nameElement.textContent.trim().replace(/\[.*?\]/g, "") : "";
  344. if (!name) return;
  345. // 获取位置信息
  346. const sectionInfo = getSectionByPosition(element);
  347. // --- 关键修正:获取所有信息行 (处理单双周不同行的情况) ---
  348. const infoItems = Array.from(popover.querySelectorAll('.kbappTimetableCourseRenderCourseItemInfoPopperInfo')).slice(1);
  349. infoItems.forEach(item => {
  350. const detailStr = item.textContent.trim();
  351. if (!detailStr) return;
  352. const parts = detailStr.split(/\s+/).filter(p => p.length > 0);
  353. let teacher = "未知教师";
  354. let posParts = [];
  355. let currentWeeks = parseWeeks(detailStr);
  356. parts.forEach(p => {
  357. if (p.includes('周')) return;
  358. // 老师判定:2-4个字且不含地点特征词
  359. if (/^[\u4e00-\u9fa5]{2,4}$/.test(p) && !/(楼|校区|室|场|馆|中心)/.test(p)) {
  360. teacher = p;
  361. } else {
  362. posParts.push(p);
  363. }
  364. });
  365. // 地点去重:选最长的描述
  366. let position = posParts.sort((a, b) => b.length - a.length)[0] || "未知教室";
  367. courses.push({
  368. name: name,
  369. teacher: teacher,
  370. position: position,
  371. day: sectionInfo.day,
  372. startSection: sectionInfo.start,
  373. endSection: sectionInfo.end,
  374. weeks: currentWeeks
  375. });
  376. });
  377. } catch (e) { console.error("解析单条课程失败:", e); }
  378. });
  379. return { courses: removeDuplicates(courses), timeSlots };
  380. }
  381. /**
  382. * 课程去重 后期这里可能会出现问题
  383. */
  384. function removeDuplicates(courses) {
  385. const courseMap = new Map();
  386. courses.forEach(course => {
  387. // 生成唯一键(不包括周次)
  388. // 可以根据需要调整组合字段
  389. const key = `${course.day}-${course.startSection}-${course.endSection}-${course.name}-${course.position}`;
  390. if (courseMap.has(key)) {
  391. // 已存在:合并周次
  392. const existing = courseMap.get(key);
  393. // 合并并去重
  394. const combinedWeeks = [...existing.weeks, ...course.weeks];
  395. const uniqueWeeks = [...new Set(combinedWeeks)];
  396. // 排序
  397. existing.weeks = uniqueWeeks.sort((a, b) => a - b);
  398. // 如果需要,可以保留最早出现的教师(如果教师不同的话)
  399. // 但这里保持原有逻辑,不更新教师
  400. } else {
  401. // 不存在:添加新记录
  402. courseMap.set(key, {...course, weeks: [...course.weeks]});
  403. }
  404. });
  405. // 转换回数组
  406. return Array.from(courseMap.values());
  407. }
  408. /**
  409. * 保存课程数据
  410. */
  411. async function saveCourses(parsedData) {
  412. const { courses, timeSlots } = parsedData;
  413. // 解析开学时间
  414. try {
  415. const startDateInfo = extractStartDate();
  416. if (!startDateInfo) {
  417. AndroidBridge.showToast("获取开学时间失败");
  418. }
  419. const configData = {
  420. semesterStartDate: startDateInfo?.startDate || null, // 如果获取失败就传 null
  421. }
  422. AndroidBridge.showToast(`准备保存开学时间 ${startDateInfo.startDate}`);
  423. let courseSaveResult = await window.AndroidBridgePromise.saveCourseConfig (
  424. JSON.stringify(configData)
  425. );
  426. if (!courseSaveResult) {
  427. AndroidBridge.showToast("保存开学时间失败,请自行设定");
  428. }
  429. AndroidBridge.showToast(`准备保存 ${courses.length} 门课程...`);
  430. // 保存课程数据
  431. courseSaveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  432. if (!courseSaveResult) {
  433. AndroidBridge.showToast("保存课程失败");
  434. return false;
  435. }
  436. AndroidBridge.showToast(`成功导入 ${courses.length} 门课程`);
  437. // 保存时间段数据
  438. if (timeSlots && timeSlots.length > 0) {
  439. const timeSlotSaveResult = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  440. if (timeSlotSaveResult) {
  441. AndroidBridge.showToast(`成功导入 ${timeSlots.length} 个时间段`);
  442. } else {
  443. AndroidBridge.showToast("时间段导入失败,课程仍可使用");
  444. }
  445. }
  446. return true;
  447. } catch (error) {
  448. console.error("保存课程数据时出错:", error);
  449. AndroidBridge.showToast(`保存失败: ${error.message}`);
  450. return false;
  451. }
  452. }
  453. async function fitTimes() {
  454. }
  455. /**
  456. * 运行主函数
  457. */
  458. async function runImportFlow() {
  459. try {
  460. AndroidBridge.showToast("课表导入工具启动...");
  461. // 1. 显示导入提示
  462. const shouldProceed = await promptUserToStart();
  463. if (!shouldProceed) return;
  464. // 2. 等待一下确保页面加载
  465. await new Promise(resolve => setTimeout(resolve, 1000));
  466. // 3. 获取 iframe 内容
  467. const iframeDoc = getIframeDocument();
  468. if (!iframeDoc) return;
  469. // 4. 解析数据
  470. AndroidBridge.showToast("正在解析课表数据...");
  471. const parsedData = parseAllData(iframeDoc);
  472. if (parsedData.courses.length === 0) {
  473. await window.AndroidBridgePromise.showAlert(
  474. "解析失败",
  475. "未找到任何课程数据,请确认:\n1. 已在课表查询页面\n2. 课表已完全加载\n3. 当前学期有课程",
  476. "知道了"
  477. );
  478. return;
  479. }
  480. // 5. 显示预览
  481. const previewMsg = `找到 ${parsedData.courses.length} 门课程\n${parsedData.timeSlots.length} 个时间段\n\n是否继续导入?`;
  482. const confirmed = await window.AndroidBridgePromise.showAlert(
  483. "导入确认",
  484. previewMsg,
  485. "确认导入"
  486. );
  487. if (!confirmed) {
  488. AndroidBridge.showToast("已取消导入");
  489. return;
  490. }
  491. // 6. 保存数据
  492. const saveSuccess = await saveCourses(parsedData);
  493. if (!saveSuccess) return;
  494. // 7. 完成
  495. AndroidBridge.showToast("课表导入完成!");
  496. AndroidBridge.notifyTaskCompletion();
  497. } catch (error) {
  498. console.error("导入流程出错:", error);
  499. AndroidBridge.showToast(`导入失败: ${error.message}`);
  500. }
  501. }
  502. // 启动导入流程
  503. runImportFlow();