// 内蒙古工业大学教务系统课程导入脚本 // 根据教务处网站内容解析课程表数据 // 2025.11.25 // ============生成时间段配置============== /** * 默认时段配置,来源于学校官网(2025.11.23) */ const defaultTimeSlots = [ { "number": 1, "startTime": "08:00", "endTime": "08:45" }, { "number": 2, "startTime": "08:55", "endTime": "09:35" }, { "number": 3, "startTime": "10:05", "endTime": "10:50" }, { "number": 4, "startTime": "11:00", "endTime": "11:40" }, { "number": 5, "startTime": "13:30", "endTime": "14:15" }, { "number": 6, "startTime": "14:25", "endTime": "15:05" }, { "number": 7, "startTime": "15:35", "endTime": "16:20" }, { "number": 8, "startTime": "16:30", "endTime": "17:10" }, { "number": 9, "startTime": "18:00", "endTime": "18:45" }, { "number": 10, "startTime": "18:45", "endTime": "19:35" }, { "number": 11, "startTime": "19:45", "endTime": "20:30" }, { "number": 12, "startTime": "20:30", "endTime": "21:20" } ]; /** * 从HTML文本中解析时段信息 * @param {string} doc - DOM 文档对象 * @returns {Object} 时段信息对象 */ function parseTimeSlotsFromHTML(doc) { const timeSlots = {}; const timetable = doc.querySelector('table#timetable'); if (timetable) { const rows = timetable.querySelectorAll('tr'); for (let i = 1; i < rows.length; i++) { // 跳过表头行 const th = rows[i].querySelector('th'); if (th) { const sectionText = th.textContent.trim(); // 解析格式如:"第1节\n08:20\n┆\n09:05" const sectionMatch = sectionText.match(/第(\d+)节/); const timeMatch = sectionText.match(/(\d{2}:\d{2})/g); if (sectionMatch && timeMatch && timeMatch.length >= 2) { const section = parseInt(sectionMatch[1]); timeSlots[section] = { section: section, startTime: timeMatch[0], endTime: timeMatch[1] }; } } } } if (Object.keys(timeSlots).length === 0) { throw new Error('未找到时段信息表格'); } return timeSlots; } /** * 从指定网页地址异步获取HTML并解析时段信息,如果解析失败则返回默认时段 * @param {string} url - 网页地址 * @returns {Promise>} 时段信息数组,按节次排序 * @returns {number} .number 节次编号 * @returns {string} .startTime 开始时间 * @returns {string} .endTime 结束时间 */ async function getTimeSlotsArray(url) { try { const doc = await fetchAndParseHTML(url, 'gbk'); // 解析时段信息 const timeSlots = parseTimeSlotsFromHTML(doc); const hasValidData = Object.keys(timeSlots).length > 0 && timeSlots[1] && timeSlots[1].startTime; if (hasValidData) { // 转换为目标格式 return Object.values(timeSlots).map(slot => ({ number: slot.section, startTime: slot.startTime, endTime: slot.endTime })).sort((a, b) => a.number - b.number); } else { throw new Error('解析到的时段数据不完整'); } } catch (error) { console.error('从HTML解析时段信息失败,使用默认时段:', error.message); // 使用默认时段 return defaultTimeSlots; } } // ============解析课程表数据============== /** * 解析周数字符串 * @param {string} weeksText - 周数字符串,支持格式:"11周"、"1-13周"、"1-10周,11-18周" * @returns {number[]} 解析后的周数数组,按升序排列 */ function parseWeeks(weeksText) { if (!weeksText) return []; const weeks = []; const text = weeksText.replace('周', '').trim(); // 处理单个周数 "11周" -> [11] if (/^\d+$/.test(text)) { return [parseInt(text)]; } // 处理范围 "1-13周" -> [1,2,3,...,13] const rangeMatch = text.match(/^(\d+)-(\d+)$/); if (rangeMatch) { const start = parseInt(rangeMatch[1]); const end = parseInt(rangeMatch[2]); for (let i = start; i <= end; i++) { weeks.push(i); } return weeks; } // 处理多个范围 "1-10周,11-18周" const ranges = text.split(','); ranges.forEach(range => { const singleMatch = range.match(/^(\d+)$/); if (singleMatch) { weeks.push(parseInt(singleMatch[1])); } else { const rangeMatch = range.match(/(\d+)-(\d+)/); if (rangeMatch) { const start = parseInt(rangeMatch[1]); const end = parseInt(rangeMatch[2]); for (let i = start; i <= end; i++) { weeks.push(i); } } } }); return weeks; } /** * 解析课程名称(去除<<>>) * @param {*} courseText - 原始课程名称文本 * @returns {string} 解析后的课程名称 */ function parseCourseName(courseText) { let name = courseText .replace(/<>/g, '') // 直接移除 >> .split(';')[0]; return name.trim(); } function parseSingleCourse(lines, day, timeSlot) { const courseNameMatch = lines[0].match(/<<(.*?)>>/); if (!courseNameMatch) { return null; } let courseData = { name: parseCourseName(courseNameMatch[1]), position: lines[1] || '', day: day, startSection: timeSlot, endSection: timeSlot, weeks: [] }; // 单门课程示例 // ['<<离散数学导论>>;1', '教C', '贾老师', '1-15周', '讲课'] // 无教师名课程示例: // ['<<体育选项课(一)>>;11', '操 场', '2-18周', '讲课'] if (lines.length > 4) { // 有教师名课程 courseData.teacher = lines[2].replace(/,$/, ''); courseData.weeks = parseWeeks(lines[3]); } else { // 无教师名课程 courseData.teacher = ''; courseData.weeks = parseWeeks(lines[2]); } return courseData; } /** * 解析包含多个课程的课程信息块。 * * @param {Array} lines - 包含课程信息的字符串数组,每个元素表示一行数据。 * @param {string} day - 表示课程所在的星期几。 * @param {string} timeSlot - 表示课程所在的时间段。 * @returns {Array} 返回一个包含课程信息的数组,每个课程信息是一个对象。 */ function parseMultipleCourses(lines, day, timeSlot) { const courses = []; let currentCourseLines = []; // 示例: // ['<<工程训练C>>;11', '格物楼D', '刘老师', '1-10周', '讲课', '<<数据结构与算法>>;1', '教C', '秦老师', '11-18周', '讲课'] for (let i = 0; i < lines.length; i++) { if (lines[i].includes('<<') && currentCourseLines.length > 0) { const courseData = parseSingleCourse(currentCourseLines, day, timeSlot); if (courseData) { courses.push(courseData); } currentCourseLines = []; } currentCourseLines.push(lines[i]); } if (currentCourseLines.length > 0) { const courseData = parseSingleCourse(currentCourseLines, day, timeSlot); if (courseData) { courses.push(courseData); } } return courses; } /** * 处理课程区块信息,解析出课程的详细信息。 * * @param {string} block - 包含课程信息的HTML字符串,使用`
`分隔每行。 * @param {string} day - 表示课程所在的星期几。 * @param {string} timeSlot - 表示课程所在的时间段。 * @returns {Array} 返回一个包含课程信息的数组,每个课程信息是一个对象。 */ function processCourseBlock(block, day, timeSlot) { const lines = block.split('
').map(line => line.replace(/</g, '<').replace(/>/g, '>').trim() ).filter(line => line); const courses = []; const courseCount = lines.filter(line => line.includes('<<')).length; if (courseCount > 1) { courses.push(...parseMultipleCourses(lines, day, timeSlot)); } else if (lines.length >= 4) { const courseData = parseSingleCourse(lines, day, timeSlot); if (courseData) { courses.push(courseData); } } return courses; } /** * 将HTML课程表转换为标准格式的课程数据 * @param {string} url - 网页地址 * @returns {Promise>} 课程表数据数组 * @returns {string} .name 课程名称 * @returns {string} .teacher 授课教师 * @returns {string} .position 上课地点 * @returns {number} .day 星期几 (1=周一, 7=周日) * @returns {number} .startSection 开始节次 * @returns {number} .endSection 结束节次 * @returns {number[]} .weeks 上课周次数组 */ async function convertToTargetFormat(url) { try { const doc = await fetchAndParseHTML(url, 'gbk'); const timetable = []; const rows = doc.querySelectorAll('#timetable tr'); // 跳过表头行 for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) { const row = rows[rowIndex]; const timeSlot = rowIndex; // 第1-13节对应rowIndex 1-13 const cells = row.querySelectorAll('td'); for (let day = 1; day <= cells.length; day++) { const cell = cells[day - 1]; const content = cell.innerHTML.trim(); if (content && content !== ' ') { // 分割每个课程块(一个单元格可能有多个课程) const courseBlocks = content.split(/
\s*
/); courseBlocks.forEach(block => { if (block.trim()) { const courses = processCourseBlock(block, day, timeSlot); for (const course of courses) { timetable.push(course); } } }); } } } return timetable; } catch (error) { return []; // 返回空数组作为错误回退 } } /** * 合并连续的课程信息。 * 合并条件:同一天、同一课程名称、同一位置、同一教师、同一周次且时间连续 * * @param {Array} courses - 课程信息数组 * @returns {Array} 返回合并后的课程信息数组 */ function mergeContinuousCourses(courses) { // 按所有关键属性进行分组 const grouped = {}; courses.forEach(course => { // 使用周次数组的字符串表示作为分组键的一部分 const weeksKey = JSON.stringify(course.weeks.sort((a, b) => a - b)); const key = `${course.day}-${course.name}-${course.position}-${course.teacher || '未知'}-${weeksKey}`; if (!grouped[key]) { grouped[key] = []; } grouped[key].push(course); }); const result = []; // 处理每个分组 Object.values(grouped).forEach(group => { // 按开始节次排序 group.sort((a, b) => a.startSection - b.startSection); let currentCourse = null; group.forEach(course => { if (!currentCourse) { // 第一个课程 currentCourse = { ...course }; } else if (currentCourse.endSection + 1 === course.startSection) { // 时间连续,合并 currentCourse.endSection = course.endSection; } else { // 时间不连续,将当前课程加入结果,开始新的课程 result.push(currentCourse); currentCourse = { ...course }; } }); // 将最后一个课程加入结果 if (currentCourse) { result.push(currentCourse); } }); return result; } // ============配置获取============== /* * 异步获取学年学期信息 * @returns {Promise} 包含 studentid, year, term的对象 * studentid: 标识ID * year: 学年,例如 45 (2025-1980) * term: 学期,1=春季,2=夏季,3=秋季 */ async function getSemesterInfo(url) { try { const doc = await fetchAndParseHTML(url, 'gbk'); // 查找 CTRT 元素 const ctrtElement = doc.querySelector('eduaffair\\:CTRT'); if (!ctrtElement) { throw new Error('未找到 CTRT 元素'); } // 提取参数 const params = { studentid: ctrtElement.getAttribute('studentid'), year: ctrtElement.getAttribute('year'), term: ctrtElement.getAttribute('term'), }; return params; } catch (error) { console.error('提取参数时出错:', error); return null; } } /** * 获取指定学年和学期的最大周数值。 * * @param {string} yearid - 学年的ID,例如 "2023"。 * @param {string} termid - 学期的ID,例如 "1" 或 "2"。 * @returns {Promise} 返回一个Promise,解析为最大周数值。 */ async function getMaxWeekValue(yearid, termid) { const url = `http://jw.imut.edu.cn/academic/manager/coursearrange/studentWeeklyTimetable.do?yearid=${yearid}&termid=${termid}`; try { const doc = await fetchAndParseHTML(url, 'gbk'); // 查找whichWeek选择框 const weekSelect = doc.querySelector('select[name="whichWeek"]'); if (!weekSelect) { throw new Error('未找到周次选择框'); } // 获取所有option的value并转换为数字 const weekOptions = Array.from(weekSelect.querySelectorAll('option')); const weekValues = weekOptions .map(option => parseInt(option.value)) .filter(value => !isNaN(value) && value !== 0); // 过滤掉非数字和空值 if (weekValues.length === 0) { throw new Error('未找到有效的周数值'); } const maxWeek = Math.max(...weekValues); return maxWeek; } catch (error) { console.error('获取最大周数时出错:', error); throw error; } } /* * 异步获取第一个课程日期 * @param {string} yearid - 学年ID * @param {string} termid - 学期ID * @returns {Promise} 第一个课程日期字符串,格式如 "2025-09-01" **/ async function getFirstCourseDate(yearid, termid) { const url = `http://jw.imut.edu.cn/academic/manager/coursearrange/studentWeeklyTimetable.do?yearid=${yearid}&termid=${termid}&whichWeek=1`; try { const doc = await fetchAndParseHTML(url, 'gbk'); // 查找第一个课程日期 const firstDateTd = doc.querySelector('td[name="td0"]'); if (firstDateTd) { const firstCourseDate = firstDateTd.textContent.trim(); return firstCourseDate; } else { return null; } } catch (error) { console.error('获取数据失败:', error); return null; } } // ====================== 辅助函数 ====================== // 请求与解析HTML的通用函数 async function fetchAndParseHTML(url, encoding = 'gbk') { const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP错误! 状态码: ${response.status}`); } const buffer = await response.arrayBuffer(); const decoder = new TextDecoder(encoding); const htmlText = decoder.decode(buffer); const parser = new DOMParser(); return parser.parseFromString(htmlText, 'text/html'); } // 日期格式验证函数 function validateDateFormat(dateString) { const regex = /^\d{4}-\d{2}-\d{2}$/; if (regex.test(dateString)) { return false; } else { return "请输入正确的日期格式,示例:2025-09-01"; } } // 弹出日期确认对话框 async function setStartDate(suggestedDate) { const dateSelection = await window.AndroidBridgePromise.showPrompt( "请确认学期起始日期", `此日期来自您本学期第一节课日期,如有误,请修改(格式:YYYY-MM-DD):`, suggestedDate || "", "validateDateFormat" ); return dateSelection; } // ====================== 导入课程主流程 ====================== async function runImportFlow() { AndroidBridge.showToast("即将开始导入课表,请稍候..."); // 获取学年学期信息 const semesterInfo = await getSemesterInfo("http://jw.imut.edu.cn/academic/student/currcourse/currcourse.jsdo"); if (!semesterInfo) { AndroidBridge.showToast("获取学生信息失败,请重试!"); return; } currentYear = semesterInfo.year; // 当前年份 - 1980 currentTerm = semesterInfo.term; // 当前学期 // 构造课程表URL const timetableUrl = `http://jw.imut.edu.cn/academic/manager/coursearrange/showTimetable.do?id=${semesterInfo.studentid}&yearid=${semesterInfo.year}&termid=${semesterInfo.term}&timetableType=STUDENT§ionType=BASE`; // 获取时段数据 const timeSlots = await getTimeSlotsArray(timetableUrl); if (!timeSlots || timeSlots.length === 0) { AndroidBridge.showToast("获取时间段信息失败,使用默认时间段!"); } // 获取并转换课程表数据 let courses = await convertToTargetFormat(timetableUrl); if (courses.length === 0) { AndroidBridge.showToast("获取课程表数据失败,请重试!"); return; } // 合并连续课程 courses = mergeContinuousCourses(courses) // 获取第一个课程日期 let firstCourseDate = null; try { firstCourseDate = await getFirstCourseDate(semesterInfo.year, semesterInfo.term); } catch (err) { console.warn("获取第一个课程日期失败:", err); } // 用户确认起始日期 try { firstCourseDate = await setStartDate(firstCourseDate); } catch (err) { console.error("用户取消了日期输入:", err); AndroidBridge.showToast("未输入起始日期。"); } // 获取最大周数 let maxWeeks = 20; // 默认最大周数 try { maxWeeks = await getMaxWeekValue(semesterInfo.year, semesterInfo.term); } catch (err) { console.warn("获取最大周数失败,使用默认值 20"); } // 配置课表配置 const coursesConfig = { semesterStartDate: firstCourseDate, semesterTotalWeeks: maxWeeks, }; // 将数据传递给Android端 // 提交课程数据 try { await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses)); const coursesCount = courses.length; AndroidBridge.showToast(`课程导入成功,共导入 ${coursesCount} 门课程!`); } catch (err) { console.error("课程导入失败:", err); AndroidBridge.showToast("课程导入失败:" + err.message); return; } // 提交时间段数据 try { await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots)); AndroidBridge.showToast("时间段导入成功!"); } catch (err) { console.error("时间段导入失败:", err); AndroidBridge.showToast("时间段导入失败:" + err.message); return; } // 提交课表配置 try { await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(coursesConfig)); AndroidBridge.showToast("课表配置保存成功!"); } catch (err) { console.error("课表配置保存失败:", err); AndroidBridge.showToast("课表配置保存失败:" + err.message); return; } // 通知任务完成 console.log("JS:整个导入流程执行完毕并成功。"); AndroidBridge.notifyTaskCompletion(); } runImportFlow();