| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- // 广州松田职业技术学院(gzst.edu.cn)拾光课程表适配脚本
- // 非该大学开发者适配,开发者无法及时发现问题
- // 出现问题请提联系开发者或者提交pr更改,这更加快速
- // 预设节次时间
- const TimeSlots = [
- { "number": 1, "startTime": "08:20", "endTime": "09:00" },
- { "number": 2, "startTime": "09:05", "endTime": "09:45" },
- { "number": 3, "startTime": "09:55", "endTime": "10:35" },
- { "number": 4, "startTime": "10:45", "endTime": "11:25" },
- { "number": 5, "startTime": "11:30", "endTime": "12:10" },
- { "number": 6, "startTime": "14:20", "endTime": "15:00" },
- { "number": 7, "startTime": "15:05", "endTime": "15:45" },
- { "number": 8, "startTime": "15:55", "endTime": "16:35" },
- { "number": 9, "startTime": "16:45", "endTime": "17:25" },
- { "number": 10, "startTime": "17:30", "endTime": "18:10" },
- { "number": 11, "startTime": "19:30", "endTime": "20:35" },
- { "number": 12, "startTime": "20:35", "endTime": "21:40" }
- ];
- // 课表配置
- const CourseConfig = {
- "semesterTotalWeeks": 20
- };
- /**
- * 验证周次字符串并转换为数字数组
- * 同时移除周次字符串中的节次信息,因为它可能会干扰周次解析。
- * @param {string} weeksStr 课表中的周次字符串,如 "5-15,17(周)[02-03节]"
- * @returns {number[]} 周数数组
- */
- function parseWeeks(weeksStr) {
- const weeks = [];
- if (!weeksStr) return weeks;
- // 移除括号内的内容(如 (周))、[节次] 和 HTML 标签
- const cleanedStr = weeksStr.replace(/\(周\)|\[.*?节\]|<\/?[a-z]+[^>]*>/ig, '').trim();
- if (cleanedStr === '') return weeks;
- cleanedStr.split(',').forEach(part => {
- const rangeMatch = part.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);
- }
- } else {
- const singleWeek = parseInt(part);
- if (!isNaN(singleWeek)) {
- weeks.push(singleWeek);
- }
- }
- });
- return [...new Set(weeks)].sort((a, b) => a - b);
- }
- /**
- * 从周次/节次字符串中提取节次范围
- * 修正了多节连排(如 [07-08-09-10节])只识别到中间节次的问题。
- * @param {string} weeksSectionStr 包含节次信息的字符串
- * @returns {{start: number, end: number} | null} 节次范围对象
- */
- function parseSectionsFromStr(weeksSectionStr) {
- // 匹配 [XX-XX节] 或 [XX节] 或 [XX-XX-XX节] 的完整内容
- const fullContentMatch = weeksSectionStr.match(/\[(\d+(?:-\d+)*)节\]/i);
-
- if (fullContentMatch) {
- const numberString = fullContentMatch[1]; // 例如: "07-08-09-10" 或 "09-10" 或 "10"
-
- // 分割所有数字
- const numbers = numberString.split('-').map(n => parseInt(n));
-
- // 确保数字是有效的
- if (numbers.length > 0 && !isNaN(numbers[0])) {
- const start = numbers[0];
- // 结束节次是数组中的最后一个有效数字
- const end = numbers[numbers.length - 1];
- if (start > 0 && end > 0) {
- // 确保 end >= start
- return {
- start: Math.min(start, end),
- end: Math.max(start, end)
- };
- }
- }
- }
- return null;
- }
- // 核心解析函数 (parseCourseTable)
- function parseCourseTable(htmlContent) {
- const parser = new DOMParser();
- const doc = parser.parseFromString(htmlContent, "text/html");
- const courseList = [];
- const table = doc.getElementById('kbtable');
- if (!table) {
- AndroidBridge.showToast("错误:未找到课表表格 (id=kbtable)。");
- return [];
- }
-
- const rows = table.querySelectorAll('tr');
-
- for (let i = 1; i < rows.length; i++) {
- const row = rows[i];
-
- const cells = row.querySelectorAll('td');
- for (let j = 0; j < cells.length; j++) {
- const cell = cells[j];
- const dayOfWeek = j + 1;
-
- const detailDiv = cell.querySelector('div[class*="kbcontent"][style*="display: none"]');
-
- if (!detailDiv) continue;
- const rawContent = detailDiv.innerHTML.trim();
- // 过滤空内容
- if (rawContent === '' || rawContent.replace(/ |<[^>]*>/ig, '').trim() === '') continue;
- // 多个课程块以分隔符处理
- const courseBlocks = rawContent.split('---------------------<br>');
-
- courseBlocks.forEach(blockHtml => {
- if (blockHtml.trim() === '') return;
-
- const cleanedBlock = blockHtml.replace(/<br\/?>/gi, '\n').trim();
-
- // 提取课程名 (第一行)
- const nameMatch = cleanedBlock.match(/^(.*?)(?:\<span.*?\/span\>)?\n/i);
- let name = (nameMatch && nameMatch[1].trim()) || "未知课程";
- // 移除课程名中的 span 或其他标签
- name = name.replace(/<span[^>]*>.*?<\/span>|<\/?[a-z]+[^>]*>/ig, '').trim();
-
- // 提取教师
- const teacherMatch = cleanedBlock.match(/<font title="老师">([^<]+?)<\/font>/i);
- const teacher = (teacherMatch && teacherMatch[1].trim()) || "暂无教师";
- // 提取地点
- const positionMatch = cleanedBlock.match(/<font title="教室">([^<]+?)<\/font>/i);
- const position = (positionMatch && positionMatch[1].trim()) || "暂无教室";
- // 提取周次和节次字符串
- const weeksSectionMatch = cleanedBlock.match(/<font title="周次\(节次\)">([^<]+?)<\/font>/i);
- const weeksSectionStr = (weeksSectionMatch && weeksSectionMatch[1].trim()) || "";
- const sections = parseSectionsFromStr(weeksSectionStr);
- if (!sections) {
- return;
- }
-
- // 解析周次数组
- const weeksArray = parseWeeks(weeksSectionStr);
- if (weeksArray.length === 0) {
- return; // 周次为空,跳过
- }
- const course = {
- name: name,
- teacher: teacher,
- position: position,
- day: dayOfWeek, // 周几 (1-7)
- startSection: sections.start, // 准确的开始节次
- endSection: sections.end, // 准确的结束节次
- weeks: weeksArray // 周次数组
- };
-
- courseList.push(course);
- });
- }
- }
-
- return courseList;
- }
- /**
- * 修正后的合并逻辑:先进行精确去重,再合并连续的课程节次。
- * @param {Array} courses 课程列表
- * @returns {Array} 去重并合并后的课程列表
- */
- function mergeCourses(courses) {
- if (!courses || courses.length === 0) {
- return [];
- }
-
- // 1. 排序:确保同一天、同一周次、完全相同的课程和连续的课程都排在一起
- courses.sort((a, b) => {
- if (a.day !== b.day) return a.day - b.day;
- const weekA = JSON.stringify(a.weeks);
- const weekB = JSON.stringify(b.weeks);
- if (weekA !== weekB) return weekA.localeCompare(weekB);
- return a.startSection - b.startSection;
- });
-
- // 2. 精确去重
- const uniqueCourses = [];
- const courseSet = new Set();
-
- for (const course of courses) {
- // 创建一个包含所有关键属性的唯一 Key
- const key = `${course.name}|${course.teacher}|${course.position}|${course.day}|${course.startSection}|${course.endSection}|${JSON.stringify(course.weeks)}`;
-
- if (!courseSet.has(key)) {
- courseSet.add(key);
- uniqueCourses.push(course);
- }
- }
- if (uniqueCourses.length <= 1) {
- return uniqueCourses;
- }
- // 3. 连续课程合并逻辑
- const mergedCourses = [];
- let currentMergedCourse = { ...uniqueCourses[0] };
- for (let i = 1; i < uniqueCourses.length; i++) {
- const nextCourse = uniqueCourses[i];
- const isSameDay = nextCourse.day === currentMergedCourse.day;
- const isSameWeeks = JSON.stringify(nextCourse.weeks) === JSON.stringify(currentMergedCourse.weeks);
- const isSameName = nextCourse.name === currentMergedCourse.name;
- const isSameTeacher = nextCourse.teacher === currentMergedCourse.teacher;
- const isSamePosition = nextCourse.position === currentMergedCourse.position;
-
- // 检查是否连续 (下一节的开始 = 当前节的结束 + 1)
- const isConsecutive = nextCourse.startSection === currentMergedCourse.endSection + 1;
- const canMerge = isSameDay && isSameWeeks && isSameName && isSameTeacher && isSamePosition && isConsecutive;
- if (canMerge) {
- // 合并:更新结束节次
- currentMergedCourse.endSection = nextCourse.endSection;
- } else {
- // 无法合并:推入当前合并结果,并开始新的合并
- mergedCourses.push(currentMergedCourse);
- currentMergedCourse = { ...nextCourse };
- }
- }
- // 推入最后一次合并的结果
- mergedCourses.push(currentMergedCourse);
-
- return mergedCourses;
- }
- // 网络请求函数
- async function fetchCourseHtml() {
- AndroidBridge.showToast("正在获取课表数据...");
- const URL = "https://jw.educationgroup.cn/gzstzyxy_jsxsd/xskb/xskb_list.do";
- try {
- const response = await fetch(URL, {
- "method": "GET",
- "credentials": "include"
- });
-
- if (!response.ok) {
- throw new Error(`网络请求失败,状态码: ${response.status}`);
- }
-
- const text = await response.text();
-
- AndroidBridge.showToast("课表数据获取成功,开始解析...");
- return text;
- } catch (error) {
- AndroidBridge.showToast(`网络请求异常: ${error.message}`);
- return null;
- }
- }
- async function importPresetTimeSlots() {
- try {
- await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(TimeSlots));
- AndroidBridge.showToast("预设时间段导入成功!");
- return true;
- } catch (error) {
- AndroidBridge.showToast("导入时间段失败: " + error.message);
- return false;
- }
- }
- async function saveConfig() {
- try {
- await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(CourseConfig));
- AndroidBridge.showToast("课表配置更新成功!");
- return true;
- } catch (error) {
- AndroidBridge.showToast("保存配置失败: " + error.message);
- return false;
- }
- }
- async function saveCourses(parsedCourses, originalCount, mergedCount) {
- if (parsedCourses.length === 0) {
- AndroidBridge.showToast("未解析到任何课程数据,跳过保存。");
- return true;
- }
-
- try {
- await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
- if (originalCount !== undefined && mergedCount !== undefined) {
- AndroidBridge.showToast(`课程导入成功!原始 ${originalCount} 条,去重合并 ${mergedCount} 条,最终导入 ${parsedCourses.length} 条。`);
- } else {
- AndroidBridge.showToast(`成功导入 ${parsedCourses.length} 条课程!`);
- }
- return true;
- } catch (error) {
- AndroidBridge.showToast(`保存失败: ${error.message}`);
- return false;
- }
- }
- async function runImportFlow() {
-
- const alertConfirmed = await window.AndroidBridgePromise.showAlert(
- "开始导入",
- "请确保您已登录教务系统,即将获取课表数据并进行课程去重和合并。",
- "确定"
- );
- if (!alertConfirmed) {
- AndroidBridge.showToast("用户取消了导入。");
- return;
- }
- // 获取 HTML
- const htmlContent = await fetchCourseHtml();
- if (htmlContent === null) {
- AndroidBridge.showToast("导入终止。");
- return;
- }
-
- // 解析课程数据
- let parsedCourses = parseCourseTable(htmlContent);
-
- if (parsedCourses.length === 0) {
- AndroidBridge.showToast("解析失败或未发现有效课程。导入终止。");
- return;
- }
-
- const originalCourseCount = parsedCourses.length;
- // 课程去重和合并
- parsedCourses = mergeCourses(parsedCourses);
- const mergedCount = originalCourseCount - parsedCourses.length;
- // 导入时间段数据
- await importPresetTimeSlots();
- // 导入课表配置
- if (!await saveConfig()) return;
- // 课程数据保存,并传入合并信息
- if (!await saveCourses(parsedCourses, originalCourseCount, mergedCount)) return;
- // 流程成功
- AndroidBridge.showToast("所有任务已完成!课表已导入成功!");
- AndroidBridge.notifyTaskCompletion();
- }
- // 启动导入流程
- runImportFlow();
|