// 文件: school.js /** * 显示导入提示 */ async function promptUserToStart() { const confirmed = await window.AndroidBridgePromise.showAlert( "导入提示", "导入前请确保您已进入课表页面(运行->课表查询->我的课表)并等待页面加载完成", "开始导入" ); if (!confirmed) { AndroidBridge.showToast("用户取消了导入"); return false; } AndroidBridge.showToast("开始获取课表数据..."); return true; } /** * 获取 iframe 内容 */ function getIframeDocument() { try { console.log('开始获取 iframe 内容'); // 尝试多种选择器找到 iframe const selectors = [ '.iframe___1hsk7', '[class*="iframe"]', 'iframe' ]; let iframe = null; for (const selector of selectors) { iframe = document.querySelector(selector); if (iframe) { console.log(`通过选择器 "${selector}" 找到 iframe`); break; } } if (!iframe) { console.error('未找到 iframe 元素'); AndroidBridge.showToast("未找到课表框架,请确保在课表页面"); return null; } // 获取 iframe 的 document const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (!iframeDoc) { console.error('无法访问 iframe 内容'); AndroidBridge.showToast("无法访问课表内容,可能页面未加载完成"); return null; } // 检查是否包含课表元素 const timetable = iframeDoc.querySelector('.kbappTimetableDayColumnRoot'); if (!timetable) { console.warn('iframe 中未找到课表元素,可能不在课表页面'); } return iframeDoc; } catch (error) { console.error('获取 iframe 内容时出错:', error); AndroidBridge.showToast(`获取课表失败: ${error.message}`); return null; } } function getSectionByPosition(element) { const dayColumn = element.closest('.kbappTimetableDayColumnRoot'); const dayCols = Array.from(dayColumn.parentNode.children); const day = dayCols.indexOf(dayColumn) + 1; let slotBlock = element; while (slotBlock.parentElement && slotBlock.parentElement !== dayColumn) { slotBlock = slotBlock.parentElement; } const win = element.ownerDocument.defaultView || window; const getFlex = (el) => { const fg = win.getComputedStyle(el).flexGrow; return Math.round(parseFloat(fg || 0)); }; let previousFlexSum = 0; let curr = slotBlock.previousElementSibling; while (curr) { previousFlexSum += getFlex(curr); curr = curr.previousElementSibling; } const currentFlex = getFlex(slotBlock); // 换算 let start = previousFlexSum + 1; let end = start + Math.max(1, currentFlex) - 1; // 午餐晚餐修正节数 if (start >= 10) { start -= 2; end -= 2; } else if (start > 5) { start -= 1; end -= 1; } return { day, start, end }; } /** * 解析时间段数据 */ function parseTimeSlots(iframeDoc) { const timeSlots = []; // 查找时间段列 const timeColumn = iframeDoc.querySelector('.kbappTimetableJcColumn'); const timeItems = timeColumn.querySelectorAll('.kbappTimetableJcItem'); console.log('找到时间段数量:', timeItems.length); timeItems.forEach((item, index) => { const textElements = item.querySelectorAll('.kbappTimetableJcItemText'); if (textElements.length >= 2) { const sectionName = textElements[0]?.textContent?.trim() || `第${index + 1}节`; const timeRange = textElements[1]?.textContent?.trim() || ''; // 解析时间范围 const timeMatch = timeRange.match(/(\d{2}:\d{2})[~-](\d{2}:\d{2})/); if (timeMatch) { const startTime = timeMatch[1]; const endTime = timeMatch[2]; // 提取节次数字 const sectionMatch = sectionName.match(/第(\d+)节/); const sectionNumber = sectionMatch ? parseInt(sectionMatch[1]) : index + 1; timeSlots.push({ number: sectionNumber, startTime: startTime, endTime: endTime }); console.log(`时间段 ${sectionNumber}: ${startTime} ~ ${endTime}`); } } }); return timeSlots; } /** * 解析周次信息 */ function parseWeeks(text) { const weeks = []; // 匹配如 1-16, 1, 3, 5-7 等模式 const patterns = text.match(/(\d+)-(\d+)周|(\d+)周/g); if (!patterns) return weeks; const isSingle = text.includes('(单)'); const isDouble = text.includes('(双)'); patterns.forEach(p => { const range = p.match(/(\d+)-(\d+)/); if (range) { const start = parseInt(range[1]); const end = parseInt(range[2]); for (let i = start; i <= end; i++) { if (isSingle && i % 2 === 0) continue; if (isDouble && i % 2 !== 0) continue; weeks.push(i); } } else { const single = p.match(/(\d+)/); if (single) weeks.push(parseInt(single[1])); } }); return weeks; } /** * 解析单个课程信息 */ // 这里源数据使用了el - popover 和el - popover__reference两种模式 一种是弹窗还要一种是课程块 // 我这里解析就只用了第一种popover 因为显示的数据精简 直接可以使用 function parseSingleCourse(courseElement, day, timeSlots) { try { const infoTexts = courseElement.querySelectorAll('.kbappTimetableCourseRenderCourseItemInfoText'); if (infoTexts.length < 2) return null; // 课程名称 let nameElement = courseElement.querySelector('.kbappTimetableCourseRenderCourseItemName'); let rawName = nameElement ? nameElement.innerText.trim() : courseElement.innerText.split('\n')[0].trim(); let name = rawName.replace(/\[.*?\]/g, "").replace(/\s+\d+$/, "").trim(); if (name === "未知课程" || !name) return; // 获取持续时间 const duration = parseInt(courseElement.getAttribute('data-scales-span') || '1'); // 计算起始节次 let startSection = 1; const parent = courseElement.closest('.kbappTimetableCourseRenderColumn'); if (parent) { const containers = parent.querySelectorAll('.kbappTimetableCourseRenderCourseItemContainer'); for (let i = 0; i < containers.length; i++) { const container = containers[i]; const courseInContainer = container.querySelector('.kbappTimetableCourseRenderCourseItem'); if (courseInContainer === courseElement) { const flexMatch = container.style.flex?.match(/(\d+)/); if (flexMatch) { let totalPrevSpan = 0; for (let j = 0; j < i; j++) { const prevFlex = containers[j].style.flex?.match(/(\d+)/); if (prevFlex) { totalPrevSpan += parseInt(prevFlex[1]); } } startSection = totalPrevSpan + 1; } break; } } } // 计算结束节次 const endSection = startSection + duration - 1; // 验证范围 const validStart = Math.max(1, Math.min(startSection, timeSlots?.length || 12)); const validEnd = Math.max(validStart, Math.min(endSection, timeSlots?.length || 12)); return { name: name, teacher: '未知教师', // 暂时用默认值 position: '未知教室', // 暂时用默认值 day: day, startSection: validStart, endSection: validEnd, weeks: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18], // 暂时用默认值 isCustomTime: false }; } catch (error) { console.error('解析出错:', error); return null; } } /** * 解析课程数据 */ function parseCourses(iframeDoc, timeSlots) { const courses = []; // 获取所有星期列 const dayColumns = iframeDoc.querySelectorAll('.kbappTimetableDayColumnRoot'); console.log('找到课表列数量:', dayColumns.length); // 遍历每一天的列 for (let dayIndex = 0; dayIndex < dayColumns.length; dayIndex++) { const dayColumn = dayColumns[dayIndex]; // 获取当天的所有课程 const dayCourses = dayColumn.querySelectorAll('.kbappTimetableCourseRenderCourseItem'); console.log(`星期${dayIndex + 1} 课程数量:`, dayCourses.length); dayCourses.forEach(courseElement => { const courseInfo = parseSingleCourse(courseElement, dayIndex + 1, timeSlots); if (courseInfo) { courses.push(courseInfo); } }); } return courses; } /** * 解析所有数据 */ function parseAllData(iframeDoc) { const timeSlots = parseTimeSlots(iframeDoc); const courses = []; const courseElements = iframeDoc.querySelectorAll('.kbappTimetableCourseRenderCourseItem'); courseElements.forEach(element => { try { const popoverId = element.getAttribute('aria-describedby'); const popover = iframeDoc.getElementById(popoverId); if (!popover) return; const nameElement = popover.querySelector('.kbappTimetableCourseRenderCourseItemInfoPopperInfo'); const name = nameElement ? nameElement.textContent.trim().replace(/\[.*?\]/g, "") : ""; if (!name) return; // 获取位置信息 const sectionInfo = getSectionByPosition(element); // --- 关键修正:获取所有信息行 (处理单双周不同行的情况) --- const infoItems = Array.from(popover.querySelectorAll('.kbappTimetableCourseRenderCourseItemInfoPopperInfo')).slice(1); infoItems.forEach(item => { const detailStr = item.textContent.trim(); if (!detailStr) return; const parts = detailStr.split(/\s+/).filter(p => p.length > 0); let teacher = "未知教师"; let posParts = []; let currentWeeks = parseWeeks(detailStr); parts.forEach(p => { if (p.includes('周')) return; // 老师判定:2-4个字且不含地点特征词 if (/^[\u4e00-\u9fa5]{2,4}$/.test(p) && !/(楼|校区|室|场|馆|中心)/.test(p)) { teacher = p; } else { posParts.push(p); } }); // 地点去重:选最长的描述 let position = posParts.sort((a, b) => b.length - a.length)[0] || "未知教室"; courses.push({ name: name, teacher: teacher, position: position, day: sectionInfo.day, startSection: sectionInfo.start, endSection: sectionInfo.end, weeks: currentWeeks }); }); } catch (e) { console.error("解析单条课程失败:", e); } }); return { courses: removeDuplicates(courses), timeSlots }; } /** * 课程去重 后期这里可能会出现问题 */ function removeDuplicates(courses) { const seen = new Set(); return courses.filter(course => { // 核心唯一键:星期 + 起始节次 + 课程名 + 周次 // 这样即使老师或地点写法稍有不同,只要是同一时间同一门课且周次一致,就会被去重 const key = `${course.day}-${course.startSection}-${course.name}-${course.weeks.join(',')}`; if (seen.has(key)) { return false; } seen.add(key); return true; }); } /** * 保存课程数据 */ async function saveCourses(parsedData) { const { courses, timeSlots } = parsedData; try { AndroidBridge.showToast(`准备保存 ${courses.length} 门课程...`); // 保存课程数据 const courseSaveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses)); if (!courseSaveResult) { AndroidBridge.showToast("保存课程失败"); return false; } AndroidBridge.showToast(`成功导入 ${courses.length} 门课程`); // 保存时间段数据 if (timeSlots && timeSlots.length > 0) { const timeSlotSaveResult = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots)); if (timeSlotSaveResult) { AndroidBridge.showToast(`成功导入 ${timeSlots.length} 个时间段`); } else { AndroidBridge.showToast("时间段导入失败,课程仍可使用"); } } return true; } catch (error) { console.error("保存课程数据时出错:", error); AndroidBridge.showToast(`保存失败: ${error.message}`); return false; } } /** * 运行主函数 */ async function runImportFlow() { try { AndroidBridge.showToast("课表导入工具启动..."); // 1. 显示导入提示 const shouldProceed = await promptUserToStart(); if (!shouldProceed) return; // 2. 等待一下确保页面加载 await new Promise(resolve => setTimeout(resolve, 1000)); // 3. 获取 iframe 内容 const iframeDoc = getIframeDocument(); if (!iframeDoc) return; // 4. 解析数据 AndroidBridge.showToast("正在解析课表数据..."); const parsedData = parseAllData(iframeDoc); if (parsedData.courses.length === 0) { await window.AndroidBridgePromise.showAlert( "解析失败", "未找到任何课程数据,请确认:\n1. 已在课表查询页面\n2. 课表已完全加载\n3. 当前学期有课程", "知道了" ); return; } // 5. 显示预览 const previewMsg = `找到 ${parsedData.courses.length} 门课程\n${parsedData.timeSlots.length} 个时间段\n\n是否继续导入?`; const confirmed = await window.AndroidBridgePromise.showAlert( "导入确认", previewMsg, "确认导入" ); if (!confirmed) { AndroidBridge.showToast("已取消导入"); return; } // 6. 保存数据 const saveSuccess = await saveCourses(parsedData); if (!saveSuccess) return; // 7. 完成 AndroidBridge.showToast("课表导入完成!"); AndroidBridge.notifyTaskCompletion(); } catch (error) { console.error("导入流程出错:", error); AndroidBridge.showToast(`导入失败: ${error.message}`); } } // 启动导入流程 runImportFlow();