| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613 |
- // 内蒙古工业大学教务系统课程导入脚本
- // 根据教务处网站内容解析课程表数据
- // 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<Array<Object>>} 时段信息数组,按节次排序
- * @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, '') // 直接移除 <<
- .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<string>} lines - 包含课程信息的字符串数组,每个元素表示一行数据。
- * @param {string} day - 表示课程所在的星期几。
- * @param {string} timeSlot - 表示课程所在的时间段。
- * @returns {Array<Object>} 返回一个包含课程信息的数组,每个课程信息是一个对象。
- */
- 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字符串,使用`<br>`分隔每行。
- * @param {string} day - 表示课程所在的星期几。
- * @param {string} timeSlot - 表示课程所在的时间段。
- * @returns {Array<Object>} 返回一个包含课程信息的数组,每个课程信息是一个对象。
- */
- function processCourseBlock(block, day, timeSlot) {
- const lines = block.split('<br>').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<Array<Object>>} 课程表数据数组
- * @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(/<br>\s*<br>/);
-
- 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<Object>} courses - 课程信息数组
- * @returns {Array<Object>} 返回合并后的课程信息数组
- */
- 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<Object>} 包含 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<number>} 返回一个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<string>} 第一个课程日期字符串,格式如 "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();
|