|
@@ -0,0 +1,405 @@
|
|
|
|
|
+// 广东轻工职业技术大学教务适配器
|
|
|
|
|
+// 适配器ID: GDIPU_01
|
|
|
|
|
+
|
|
|
|
|
+// 提示用户输入学期开始日期
|
|
|
|
|
+const promptForStartDate = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const today = new Date();
|
|
|
|
|
+ const defaultDate = today.toISOString().split('T')[0];
|
|
|
|
|
+
|
|
|
|
|
+ const startDate = await window.AndroidBridgePromise.showPrompt(
|
|
|
|
|
+ "请输入学期开始日期",
|
|
|
|
|
+ "格式:YYYY-MM-DD(例如:2026-02-24)",
|
|
|
|
|
+ defaultDate,
|
|
|
|
|
+ "validateDate"
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (startDate === null) {
|
|
|
|
|
+ throw new Error("用户取消了输入");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(startDate)) {
|
|
|
|
|
+ AndroidBridge.showToast("日期格式不正确,请使用YYYY-MM-DD格式");
|
|
|
|
|
+ throw new Error("日期格式不正确");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return startDate;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("获取开始日期失败:", error);
|
|
|
|
|
+ throw error;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 日期验证函数(供AndroidBridge调用)
|
|
|
|
|
+function validateDate(dateStr) {
|
|
|
|
|
+ if (!dateStr) {
|
|
|
|
|
+ return "日期不能为空";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
|
|
|
+ return "日期格式不正确,请使用YYYY-MM-DD格式";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const date = new Date(dateStr);
|
|
|
|
|
+ if (isNaN(date.getTime())) {
|
|
|
|
|
+ return "无效的日期";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取课程表数据
|
|
|
|
|
+const fetchTimetable = async (date) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await fetch('https://jw.gdipu.edu.cn/jsxsd/framework/main_index_loadkb.jsp', {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Accept': 'text/html, */*; q=0.01',
|
|
|
|
|
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
|
|
|
|
|
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
|
|
|
|
|
+ 'Origin': 'https://jw.gdipu.edu.cn',
|
|
|
|
|
+ 'Referer': window.location.href,
|
|
|
|
|
+ 'X-Requested-With': 'XMLHttpRequest'
|
|
|
|
|
+ },
|
|
|
|
|
+ credentials: 'include',
|
|
|
|
|
+ body: `rq=${date}`
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`HTTP错误: ${response.status}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const html = await response.text();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有课程
|
|
|
|
|
+ const hasCourses = checkIfHasCourses(html);
|
|
|
|
|
+
|
|
|
|
|
+ return { html, hasCourses };
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取课程表失败:', error);
|
|
|
|
|
+ throw error;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 检查HTML中是否有课程
|
|
|
|
|
+const checkIfHasCourses = (html) => {
|
|
|
|
|
+ const parser = new DOMParser();
|
|
|
|
|
+ const doc = parser.parseFromString(html, 'text/html');
|
|
|
|
|
+
|
|
|
|
|
+ const table = doc.querySelector('form table.kb_table');
|
|
|
|
|
+ if (!table) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const courseCells = table.querySelectorAll('td p[title]');
|
|
|
|
|
+ return courseCells.length > 0;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 从HTML字符串解析课程表
|
|
|
|
|
+const parseTimetableFromHTML = (html, date) => {
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+
|
|
|
|
|
+ const parser = new DOMParser();
|
|
|
|
|
+ const doc = parser.parseFromString(html, 'text/html');
|
|
|
|
|
+
|
|
|
|
|
+ const table = doc.querySelector('form table.kb_table');
|
|
|
|
|
+ if (!table) {
|
|
|
|
|
+ console.error('未找到课程表表格');
|
|
|
|
|
+ return courses;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const rows = table.querySelectorAll('tbody tr');
|
|
|
|
|
+
|
|
|
|
|
+ rows.forEach((row) => {
|
|
|
|
|
+ const sectionCell = row.querySelector('td:first-child');
|
|
|
|
|
+ if (!sectionCell) return;
|
|
|
|
|
+
|
|
|
|
|
+ const sectionText = sectionCell.textContent.trim().split('\n')[0];
|
|
|
|
|
+ const sectionInfo = parseSection(sectionText);
|
|
|
|
|
+
|
|
|
|
|
+ // 遍历星期几的列(从第2列到第8列)
|
|
|
|
|
+ for (let colIndex = 1; colIndex <= 7; colIndex++) {
|
|
|
|
|
+ const dayCell = row.querySelector(`td:nth-child(${colIndex + 1})`);
|
|
|
|
|
+ if (!dayCell) continue;
|
|
|
|
|
+
|
|
|
|
|
+ const courseP = dayCell.querySelector('p[title]');
|
|
|
|
|
+ if (!courseP) continue;
|
|
|
|
|
+
|
|
|
|
|
+ const title = courseP.getAttribute('title');
|
|
|
|
|
+ const courseInfo = parseCourseFromTitle(title);
|
|
|
|
|
+
|
|
|
|
|
+ const timeInfo = parseTimeInfo(courseInfo.time || '');
|
|
|
|
|
+
|
|
|
|
|
+ let dayOfWeek;
|
|
|
|
|
+ if (timeInfo.day && timeInfo.day > 0) {
|
|
|
|
|
+ dayOfWeek = timeInfo.day;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 列索引映射:1->星期一(1), 2->星期二(2), 3->星期三(3), 4->星期四(4), 5->星期五(5), 6->星期六(6), 7->星期日(7)
|
|
|
|
|
+ dayOfWeek = colIndex;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 将跨节课程拆分为多个单节课程
|
|
|
|
|
+ const startSection = sectionInfo.startSection;
|
|
|
|
|
+ const endSection = sectionInfo.endSection;
|
|
|
|
|
+
|
|
|
|
|
+ for (let section = startSection; section <= endSection; section++) {
|
|
|
|
|
+ const course = {
|
|
|
|
|
+ name: courseInfo.name || '',
|
|
|
|
|
+ teacher: '', // 这个系统似乎没有教师信息
|
|
|
|
|
+ position: courseInfo.location || '',
|
|
|
|
|
+ day: dayOfWeek,
|
|
|
|
|
+ startSection: section,
|
|
|
|
|
+ endSection: section, // 单节课,开始和结束节次相同
|
|
|
|
|
+ weeks: timeInfo.weeks.length > 0 ? timeInfo.weeks : [1] // 暂时使用第1周
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ courses.push(course);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return courses;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 从title属性解析课程信息
|
|
|
|
|
+const parseCourseFromTitle = (title) => {
|
|
|
|
|
+ const info = {};
|
|
|
|
|
+
|
|
|
|
|
+ const creditMatch = title.match(/课程学分:([\d.]+)/);
|
|
|
|
|
+ const propertyMatch = title.match(/课程属性:([^<]+)/);
|
|
|
|
|
+ const nameMatch = title.match(/课程名称:([^<]+)/);
|
|
|
|
|
+ const timeMatch = title.match(/上课时间:([^<]+)/);
|
|
|
|
|
+ const locationMatch = title.match(/上课地点:([^<]+)/);
|
|
|
|
|
+ const campusMatch = title.match(/上课校区:([^<]+)/);
|
|
|
|
|
+ const groupMatch = title.match(/分组名:([^<]+)/);
|
|
|
|
|
+
|
|
|
|
|
+ if (creditMatch) info.credit = creditMatch[1];
|
|
|
|
|
+ if (propertyMatch) info.property = propertyMatch[1].trim();
|
|
|
|
|
+ if (nameMatch) info.name = nameMatch[1].trim();
|
|
|
|
|
+ if (timeMatch) info.time = timeMatch[1].trim();
|
|
|
|
|
+ if (locationMatch) info.location = locationMatch[1].trim();
|
|
|
|
|
+ if (campusMatch) info.campus = campusMatch[1].trim();
|
|
|
|
|
+ if (groupMatch) info.group = groupMatch[1].trim();
|
|
|
|
|
+
|
|
|
|
|
+ return info;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 解析上课时间字符串
|
|
|
|
|
+const parseTimeInfo = (timeStr) => {
|
|
|
|
|
+ const result = {
|
|
|
|
|
+ weeks: [],
|
|
|
|
|
+ day: 0,
|
|
|
|
|
+ startSection: 0,
|
|
|
|
|
+ endSection: 0
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 解析周数范围
|
|
|
|
|
+ const weekMatch = timeStr.match(/第(\d+)(?:-(\d+))?周/);
|
|
|
|
|
+ if (weekMatch) {
|
|
|
|
|
+ const startWeek = parseInt(weekMatch[1], 10);
|
|
|
|
|
+ if (weekMatch[2]) {
|
|
|
|
|
+ const endWeek = parseInt(weekMatch[2], 10);
|
|
|
|
|
+ for (let week = startWeek; week <= endWeek; week++) {
|
|
|
|
|
+ result.weeks.push(week);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.weeks.push(startWeek);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析星期几
|
|
|
|
|
+ const dayMap = {
|
|
|
|
|
+ '星期一': 1,
|
|
|
|
|
+ '星期二': 2,
|
|
|
|
|
+ '星期三': 3,
|
|
|
|
|
+ '星期四': 4,
|
|
|
|
|
+ '星期五': 5,
|
|
|
|
|
+ '星期六': 6,
|
|
|
|
|
+ '星期日': 7
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ for (const [dayStr, dayNum] of Object.entries(dayMap)) {
|
|
|
|
|
+ if (timeStr.includes(dayStr)) {
|
|
|
|
|
+ result.day = dayNum;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析节次
|
|
|
|
|
+ const sectionMatch = timeStr.match(/\[(\d+)-(\d+)\]/);
|
|
|
|
|
+ if (sectionMatch) {
|
|
|
|
|
+ result.startSection = parseInt(sectionMatch[1], 10);
|
|
|
|
|
+ result.endSection = parseInt(sectionMatch[2], 10);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 解析节次字符串
|
|
|
|
|
+const parseSection = (sectionStr) => {
|
|
|
|
|
+ const result = {
|
|
|
|
|
+ startSection: 0,
|
|
|
|
|
+ endSection: 0
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (sectionStr.includes('-')) {
|
|
|
|
|
+ const parts = sectionStr.split('-');
|
|
|
|
|
+ result.startSection = parseInt(parts[0], 10);
|
|
|
|
|
+ result.endSection = parseInt(parts[1], 10);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ result.startSection = parseInt(sectionStr, 10);
|
|
|
|
|
+ result.endSection = parseInt(sectionStr, 10);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取学期配置
|
|
|
|
|
+const getSemesterConfig = (startDate, totalWeeks) => {
|
|
|
|
|
+ return {
|
|
|
|
|
+ semesterStartDate: startDate,
|
|
|
|
|
+ totalWeeks: totalWeeks
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取时间段配置
|
|
|
|
|
+const getTimeSlots = (html) => {
|
|
|
|
|
+ // 北区有14节课,使用准确时间配置
|
|
|
|
|
+ return [
|
|
|
|
|
+ { number: 1, startTime: "08:30", endTime: "09:10" },
|
|
|
|
|
+ { number: 2, startTime: "09:15", endTime: "09:55" },
|
|
|
|
|
+ { number: 3, startTime: "10:15", endTime: "10:55" },
|
|
|
|
|
+ { number: 4, startTime: "11:00", endTime: "11:40" },
|
|
|
|
|
+ { number: 5, startTime: "11:45", endTime: "12:25" },
|
|
|
|
|
+ { number: 6, startTime: "13:15", endTime: "13:55" },
|
|
|
|
|
+ { number: 7, startTime: "14:00", endTime: "14:40" },
|
|
|
|
|
+ { number: 8, startTime: "14:45", endTime: "15:25" },
|
|
|
|
|
+ { number: 9, startTime: "15:45", endTime: "16:25" },
|
|
|
|
|
+ { number: 10, startTime: "16:30", endTime: "17:10" },
|
|
|
|
|
+ { number: 11, startTime: "17:15", endTime: "17:55" },
|
|
|
|
|
+ { number: 12, startTime: "19:30", endTime: "20:10" },
|
|
|
|
|
+ { number: 13, startTime: "20:15", endTime: "20:55" },
|
|
|
|
|
+ { number: 14, startTime: "21:00", endTime: "21:40" },
|
|
|
|
|
+ ];
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 保存课程数据
|
|
|
|
|
+const saveSchedule = async (courses, courseConfig, timeSlots) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await Promise.allSettled([
|
|
|
|
|
+ window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(courseConfig)),
|
|
|
|
|
+ window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses)),
|
|
|
|
|
+ window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots))
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ AndroidBridge.showToast("课程表导入成功!");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("保存课程数据时出错:", error);
|
|
|
|
|
+ AndroidBridge.showToast("课程表导入失败:" + error.message);
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 日期增加指定天数
|
|
|
|
|
+const addDays = (dateStr, days) => {
|
|
|
|
|
+ const date = new Date(dateStr);
|
|
|
|
|
+ date.setDate(date.getDate() + days);
|
|
|
|
|
+ return date.toISOString().split('T')[0];
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 判断日期是否是周日(0=周日,1=周一,...,6=周六)
|
|
|
|
|
+const isSunday = (dateStr) => {
|
|
|
|
|
+ const date = new Date(dateStr);
|
|
|
|
|
+ return date.getDay() === 0; // 0表示周日
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 获取多周课程表
|
|
|
|
|
+const fetchMultiWeekTimetable = async (startDate) => {
|
|
|
|
|
+ const allCourses = [];
|
|
|
|
|
+
|
|
|
|
|
+ //如果起始日期是周日,请求日期就-1
|
|
|
|
|
+ let requestDate = startDate;
|
|
|
|
|
+ if (isSunday(startDate)) {
|
|
|
|
|
+ requestDate = addDays(startDate, -1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ let weekCount = 0;
|
|
|
|
|
+ let timeSlots = null;
|
|
|
|
|
+
|
|
|
|
|
+ AndroidBridge.showToast("正在获取课程表数据,请稍候...");
|
|
|
|
|
+
|
|
|
|
|
+ while (true) {
|
|
|
|
|
+ weekCount++;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const { html, hasCourses } = await fetchTimetable(requestDate);
|
|
|
|
|
+
|
|
|
|
|
+ if (weekCount === 1) {
|
|
|
|
|
+ timeSlots = getTimeSlots(html);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const weekCourses = parseTimetableFromHTML(html, requestDate);
|
|
|
|
|
+
|
|
|
|
|
+ if (weekCourses.length > 0) {
|
|
|
|
|
+ allCourses.push(...weekCourses);
|
|
|
|
|
+ console.log(`第${weekCount}周: 找到 ${weekCourses.length} 门课程`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasCourses) {
|
|
|
|
|
+ console.log(`第${weekCount}周: 没有课程,停止获取`);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ requestDate = addDays(requestDate, 7);
|
|
|
|
|
+
|
|
|
|
|
+ if (weekCount >= 20) {
|
|
|
|
|
+ console.log("达到最大周数限制(20周),停止获取");
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`获取第${weekCount}周课程表失败:`, error);
|
|
|
|
|
+ AndroidBridge.showToast(`第${weekCount}周获取失败,继续下一周`);
|
|
|
|
|
+ requestDate = addDays(requestDate, 7);
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ courses: allCourses,
|
|
|
|
|
+ totalWeeks: weekCount,
|
|
|
|
|
+ timeSlots: timeSlots || getTimeSlots('')
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 主函数
|
|
|
|
|
+(async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ AndroidBridge.showToast("正在启动GDIPU课程表导入...");
|
|
|
|
|
+
|
|
|
|
|
+ const startDate = await promptForStartDate();
|
|
|
|
|
+
|
|
|
|
|
+ const { courses, totalWeeks, timeSlots } = await fetchMultiWeekTimetable(startDate);
|
|
|
|
|
+
|
|
|
|
|
+ if (courses.length === 0) {
|
|
|
|
|
+ AndroidBridge.showToast("未找到任何课程信息");
|
|
|
|
|
+ throw new Error("未找到课程信息");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log(`总共找到 ${courses.length} 门课程,共 ${totalWeeks} 周`);
|
|
|
|
|
+
|
|
|
|
|
+ const courseConfig = getSemesterConfig(startDate, totalWeeks);
|
|
|
|
|
+
|
|
|
|
|
+ const success = await saveSchedule(courses, courseConfig, timeSlots);
|
|
|
|
|
+
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ AndroidBridge.notifyTaskCompletion();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ throw new Error("保存课程数据失败");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("导入课程表时出错:", error);
|
|
|
|
|
+ AndroidBridge.showToast("导入课程表失败:" + error.message);
|
|
|
|
|
+ }
|
|
|
|
|
+})();
|