Bläddra i källkod

feat: 新增内蒙古工业大学(IMUT)教务系统适配

wuko233 1 vecka sedan
förälder
incheckning
d9c4169e19
3 ändrade filer med 627 tillägg och 0 borttagningar
  1. 5 0
      index/root_index.yaml
  2. 613 0
      resources/IMUT/IMUT_01.js
  3. 9 0
      resources/IMUT/adapters.yaml

+ 5 - 0
index/root_index.yaml

@@ -72,3 +72,8 @@ schools:
     name: "湖南商务职业技术学院"
     initial: "H"
     resource_folder: "HNVCC"    
+  
+  - id: "IMUT"
+    name: "内蒙古工业大学"
+    initial: "N"
+    resource_folder: "IMUT"

+ 613 - 0
resources/IMUT/IMUT_01.js

@@ -0,0 +1,613 @@
+// 内蒙古工业大学教务系统课程导入脚本
+// 根据教务处网站内容解析课程表数据
+// 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(/&lt;/g, '<').replace(/&gt;/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 !== '&nbsp;') {
+
+                    // 分割每个课程块(一个单元格可能有多个课程)
+                    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&sectionType=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();

+ 9 - 0
resources/IMUT/adapters.yaml

@@ -0,0 +1,9 @@
+# resource/IMUT/adapters.yaml
+adapters:
+  - adapter_id: "IMUT_01"
+    adapter_name: "内蒙古工业大学教务处适配"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "IMUT_01.js"
+    import_url: "http://jw.imut.edu.cn/academic/login/imut/loginIds6Valid.jsp"
+    maintainer: "wuko233"
+    description: "内蒙古工业大学教务处适配,采用新版统一身份认证登录页面"