// 广州松田职业技术学院(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('---------------------
');
courseBlocks.forEach(blockHtml => {
if (blockHtml.trim() === '') return;
const cleanedBlock = blockHtml.replace(/
/gi, '\n').trim();
// 提取课程名 (第一行)
const nameMatch = cleanedBlock.match(/^(.*?)(?:\)?\n/i);
let name = (nameMatch && nameMatch[1].trim()) || "未知课程";
// 移除课程名中的 span 或其他标签
name = name.replace(/]*>.*?<\/span>|<\/?[a-z]+[^>]*>/ig, '').trim();
// 提取教师
const teacherMatch = cleanedBlock.match(/([^<]+?)<\/font>/i);
const teacher = (teacherMatch && teacherMatch[1].trim()) || "暂无教师";
// 提取地点
const positionMatch = cleanedBlock.match(/([^<]+?)<\/font>/i);
const position = (positionMatch && positionMatch[1].trim()) || "暂无教室";
// 提取周次和节次字符串
const weeksSectionMatch = cleanedBlock.match(/([^<]+?)<\/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();