// 文件: 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 {
// 尝试多种选择器找到 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 extractStartDate() {
const iframdate = getIframeDocument();
if (!iframdate) return null;
try {
const dayElement = iframdate.querySelector('.kbappTimeZCText'); //
第1周(3/9 ~ 3/15)
const semesterElement = iframdate.querySelector('.kbappTimeXQText'); //2025-2026学年 第2学期
if (!dayElement || !semesterElement) {
return null;
}
const dayText = dayElement.textContent.trim(); // 第1周(3/9 ~ 3/15)
const semesterText = semesterElement.textContent.trim(); // 2025-2026学年 第2学期
const startDate = parseStartDate(dayText, semesterText);
// 要判断是第几学期来选择开学年
return {startDate}; //传入解析后数据
}
catch (error) {
console.error('解析开学时间时出错:', error);
AndroidBridge.showToast(`解析开学时间失败: ${error.message}`);
return null;
}
}
/**
* 解析开学时间
* @param {string} weekText - 周次文本,如 "第1周(3/9 ~ 3/15)"
* @param {string} semesterText - 学期文本,如 "2025-2026学年 第2学期"
* @returns {string} 开学日期 YYYY-MM-DD
*/
function parseStartDate(weekText, semesterText) {
// 1. 解析学期信息,获取学年和学期
const semesterMatch = semesterText.match(/(\d{4})-(\d{4})学年\s*第(\d)学期/);
if (!semesterMatch) {
throw new Error('无法解析学期信息');
}
const startYear = parseInt(semesterMatch[1]); // 2025
const endYear = parseInt(semesterMatch[2]); // 2026
const semester = parseInt(semesterMatch[3]); // 1 或 2
// 2. 解析周次信息,获取月份和日期范围
const weekMatch = weekText.match(/第(\d+)周\((\d{1,2})\/(\d{1,2})\s*~\s*(\d{1,2})\/(\d{1,2})\)/);
if (!weekMatch) {
throw new Error('无法解析周次信息');
}
const weekNumber = parseInt(weekMatch[1]); // 周数
const startMonth = parseInt(weekMatch[2]); // 开始月份
const startDay = parseInt(weekMatch[3]); // 开始日期
const endMonth = parseInt(weekMatch[4]); // 结束月份
const endDay = parseInt(weekMatch[5]); // 结束日期
console.log(`解析结果: 第${weekNumber}周, ${startMonth}/${startDay} ~ ${endMonth}/${endDay}`);
// 3. 根据学期判断开学年份
let startYearForDate;
if (semester === 1) {
// 第一学期:开学在 startYear 年
startYearForDate = startYear;
} else {
// 第二学期:开学在 endYear 年(通常跨年)
startYearForDate = endYear;
}
// 特殊情况处理:如果开始月份小于当前月份,可能需要调整年份
// 比如 1月开学应该是 endYear 年
const currentMonth = new Date().getMonth() + 1;
if (startMonth < 6 && semester === 2) {
// 第二学期如果在1-6月开学,应该用 endYear
startYearForDate = endYear;
}
// 4. 构建开学日期(假设是第1周的周一,或者就用开始日期)
// 这里用开始日期作为参考
const startDateStr = `${startYearForDate}-${String(startMonth).padStart(2, '0')}-${String(startDay).padStart(2, '0')}`;
// 5. 如果是第1周,直接返回开始日期
if (weekNumber === 1) {
console.log(`开学日期: ${startDateStr}`);
return startDateStr;
}
// 6. 如果不是第1周,需要往前推算
// 计算第1周的日期
const startDate = new Date(startYearForDate, startMonth - 1, startDay);
const daysToSubtract = (weekNumber - 1) * 7;
startDate.setDate(startDate.getDate() - daysToSubtract);
const firstWeekStartDate = formatDate(startDate);
return firstWeekStartDate;
}
/**
* 格式化日期为 YYYY-MM-DD
*/
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
/**
* 计算每天课程节数
**/
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;
// 这里的的start和end都加了午餐晚餐 午餐晚餐修正节数
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');
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 courseMap = new Map();
courses.forEach(course => {
// 生成唯一键(不包括周次)
// 可以根据需要调整组合字段
const key = `${course.day}-${course.startSection}-${course.endSection}-${course.name}-${course.position}`;
if (courseMap.has(key)) {
// 已存在:合并周次
const existing = courseMap.get(key);
// 合并并去重
const combinedWeeks = [...existing.weeks, ...course.weeks];
const uniqueWeeks = [...new Set(combinedWeeks)];
// 排序
existing.weeks = uniqueWeeks.sort((a, b) => a - b);
// 如果需要,可以保留最早出现的教师(如果教师不同的话)
// 但这里保持原有逻辑,不更新教师
} else {
// 不存在:添加新记录
courseMap.set(key, {...course, weeks: [...course.weeks]});
}
});
// 转换回数组
return Array.from(courseMap.values());
}
/**
* 保存课程数据
*/
async function saveCourses(parsedData) {
const { courses, timeSlots } = parsedData;
// 解析开学时间
try {
const startDateInfo = extractStartDate();
if (!startDateInfo) {
AndroidBridge.showToast("获取开学时间失败");
}
const configData = {
semesterStartDate: startDateInfo?.startDate || null, // 如果获取失败就传 null
}
AndroidBridge.showToast(`准备保存开学时间 ${startDateInfo.startDate}`);
const courseSaveResult = await window.AndroidBridgePromise.saveCourseConfig (
JSON.stringify(configData)
);
if (!courseSaveResult) {
AndroidBridge.showToast("保存开学时间失败,请自行设定");
}
AndroidBridge.showToast(`准备保存 ${courses.length} 门课程...`);
// 保存课程数据
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 fitTimes() {
}
/**
* 运行主函数
*/
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();