// school.js - 教务系统适配 // // 适配说明: // 此脚本通过调用教务系统已有的 AJAX 接口获取课程数据, // 解析 scheduleText 文本字段提取排课信息, // 然后转换为拾光 App 所需的格式并导出。 // // 使用方式: // 1. 登录教务系统,进入课表页面 // 2. 点击扩展图标 → "开始测试" // 3. 等待数据采集完成,弹出下载 const CONTEXT_PATH = window.CONTEXT_PATH || '/student'; const BIZ_TYPE_ID = window.bizTypeId || 2; // ============================================================ // 工具函数 // ============================================================ /** * 从页面全局变量中获取当前学期信息 */ function getCurrentSemesterInfo() { if (window.currentSemester) { const cs = window.currentSemester; const startDate = cs.startDate ? `${cs.startDate.year}-${String(cs.startDate.monthOfYear).padStart(2, '0')}-${String(cs.startDate.dayOfMonth).padStart(2, '0')}` : null; const endDate = cs.endDate ? `${cs.endDate.year}-${String(cs.endDate.monthOfYear).padStart(2, '0')}-${String(cs.endDate.dayOfMonth).padStart(2, '0')}` : null; return { id: cs.id, name: cs.nameZh || cs.name, startDate: startDate, endDate: endDate, weekStartOnSunday: cs.weekStartOnSunday || false, schoolYear: cs.schoolYear, }; } if (window.semesters && window.semesters.length > 0) { const semesterId = document.getElementById('allSemesters')?.value; const sem = semesterId ? window.semesters.find(s => s.id == semesterId) : window.semesters[0]; if (sem) { return { id: sem.id, name: sem.nameZh || sem.name, startDate: sem.startDate, endDate: sem.endDate, weekStartOnSunday: sem.weekStartOnSunday || false, schoolYear: sem.schoolYear, }; } } return null; } /** * 计算学期总周数 */ function calculateTotalWeeks(startDateStr, endDateStr) { if (!startDateStr || !endDateStr) return 20; const start = new Date(startDateStr); const end = new Date(endDateStr); const diffDays = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); return Math.ceil(diffDays / 7); } // 星期映射 const DAY_MAP = { '星期一': 1, '星期二': 2, '星期三': 3, '星期四': 4, '星期五': 5, '星期六': 6, '星期日': 7, }; // ============================================================ // 核心:解析 scheduleText 文本 // ============================================================ /** * 解析周次字符串,如 "2~15" → [2,3,...,15], "1" → [1] */ function parseWeekRange(weekStr) { const weeks = []; if (weekStr.includes('~')) { const parts = weekStr.split('~'); const start = parseInt(parts[0], 10); const end = parseInt(parts[1], 10); for (let i = start; i <= end; i++) { weeks.push(i); } } else { weeks.push(parseInt(weekStr, 10)); } return weeks; } /** * 解析单条排课文本行 * * 格式示例 (有教室): * "1周 星期三 8~9节 成龙 1-A407" * "2~15周 星期三 1~2节 成龙 1-B214" * 格式示例 (无教室,如网课/讲座): * "11~12周 星期三 3~4节" */ function parseScheduleLine(line, lesson) { if (!line || !line.trim()) return []; // 正则匹配: // (\d+(?:~\d+)?)周 - 周次 (如 "1" 或 "2~15") // (星期[一二三四五六日]) - 星期几 // (\d+)~(\d+)节 - 节次范围 // (?:\s+(\S+))?(?:\s+(\S+))? - 可选的校区和教室 (有些课没有) const pattern = /(\d+(?:~\d+)?)周\s+(星期[一二三四五六日])\s+(\d+)~(\d+)节(?:\s+(\S+))?(?:\s+(\S+))?/; const match = line.match(pattern); if (!match) { console.warn('[教务适配] 无法解析排课行:', line); return []; } const weekStr = match[1]; const dayText = match[2]; const startSection = parseInt(match[3], 10); const endSection = parseInt(match[4], 10); const campus = match[5] || ''; // 可选 const classroom = match[6] || ''; // 可选 const weeks = parseWeekRange(weekStr); const day = DAY_MAP[dayText] || 1; // 课程名: lesson.course.nameZh const courseName = (lesson.course && lesson.course.nameZh) || lesson.nameZh || '未知课程'; // 教师: lesson.teacherAssignmentString const teacher = lesson.teacherAssignmentString || ''; // 教室位置 const position = [campus, classroom].filter(Boolean).join(' '); return [{ name: courseName, teacher: teacher, position: position, day: day, startSection: startSection, endSection: endSection, weeks: weeks, isCustomTime: false, }]; } /** * 从 lesson 的 scheduleText 中解析所有排课条目 */ function parseLessonScheduleText(lesson) { const courses = []; const scheduleText = lesson.scheduleText; if (!scheduleText) return courses; let rawText = ''; if (scheduleText.dateTimePlaceText && scheduleText.dateTimePlaceText.text) { rawText = scheduleText.dateTimePlaceText.text; } else if (scheduleText.dateTimePlacePersonText && scheduleText.dateTimePlacePersonText.text) { rawText = scheduleText.dateTimePlacePersonText.text; } else if (scheduleText.dateTimeText && scheduleText.dateTimeText.text) { rawText = scheduleText.dateTimeText.text; } if (!rawText) return courses; // 按 "; \n" 或 ";\n" 拆分各行 const lines = rawText.split(/;\s*\n\s*/).filter(line => line.trim()); for (const line of lines) { const parsed = parseScheduleLine(line.trim(), lesson); courses.push(...parsed); } return courses; } // ============================================================ // 合并相同课程 // ============================================================ /** * 合并相同课程的周次 */ function mergeCourseWeeks(courses) { const map = new Map(); for (const course of courses) { const key = JSON.stringify({ name: course.name, teacher: course.teacher, position: course.position, day: course.day, startSection: course.startSection, endSection: course.endSection, }); if (map.has(key)) { const existing = map.get(key); const mergedWeeks = new Set([...existing.weeks, ...course.weeks]); existing.weeks = Array.from(mergedWeeks).sort((a, b) => a - b); } else { map.set(key, { ...course }); } } return Array.from(map.values()); } // ============================================================ // 数据获取 // ============================================================ /** * 通过 AJAX 获取课程数据 */ function fetchCourseData(semesterId) { return new Promise((resolve, reject) => { const url = CONTEXT_PATH + '/for-std/course-table/get-data'; const data = { bizTypeId: BIZ_TYPE_ID, semesterId: semesterId, dataId: window.studentIds ? window.studentIds[0] : window.personId, }; console.log('[教务适配] 正在请求课程数据...', { url, data }); $.ajax({ url: url, type: 'GET', data: data, success: function (res) { console.log('[教务适配] 成功获取课程数据,包含 ' + (res.lessons?.length || 0) + ' 门课程'); resolve(res); }, error: function (jqXHR, textStatus, errorThrown) { console.error('[教务适配] 获取课程数据失败:', textStatus, errorThrown); reject(new Error('获取课程数据失败: ' + textStatus)); }, }); }); } // ============================================================ // 时间段配置 (狮子山|成龙校区课表) // ============================================================ /** * 获取上课时间段配置 */ function getDefaultTimeSlots() { return [ { number: 1, startTime: "08:10", endTime: "08:55" }, { number: 2, startTime: "09:00", endTime: "09:45" }, { number: 3, startTime: "10:10", endTime: "10:55" }, { number: 4, startTime: "11:05", endTime: "11:45" }, { number: 5, startTime: "11:50", endTime: "12:35" }, { number: 6, startTime: "12:40", endTime: "13:25" }, { number: 7, startTime: "13:30", endTime: "14:10" }, { number: 8, startTime: "14:10", endTime: "14:55" }, { number: 9, startTime: "15:00", endTime: "15:45" }, { number: 10, startTime: "16:10", endTime: "16:55" }, { number: 11, startTime: "17:00", endTime: "17:45" }, { number: 12, startTime: "18:00", endTime: "18:45" }, { number: 13, startTime: "19:10", endTime: "19:55" }, { number: 14, startTime: "20:00", endTime: "20:45" }, { number: 15, startTime: "20:50", endTime: "21:35" }, ]; } // ============================================================ // 课表配置 // ============================================================ function getCourseConfig(semesterInfo) { const totalWeeks = semesterInfo?.startDate && semesterInfo?.endDate ? calculateTotalWeeks(semesterInfo.startDate, semesterInfo.endDate) : 20; return { semesterStartDate: semesterInfo?.startDate || null, semesterTotalWeeks: totalWeeks, defaultClassDuration: 45, defaultBreakDuration: 10, firstDayOfWeek: 1, }; } // ============================================================ // 主流程 // ============================================================ async function main() { try { console.log('========================================'); console.log('[教务适配] 开始执行 SICNU 教务系统适配'); console.log('========================================'); // 1. 获取学期信息 const semesterInfo = getCurrentSemesterInfo(); if (!semesterInfo) { throw new Error('无法获取学期信息,请确认已在课表页面'); } console.log('[教务适配] 当前学期:', semesterInfo.name, '(id=' + semesterInfo.id + ')'); console.log('[教务适配] 学期日期:', semesterInfo.startDate, '~', semesterInfo.endDate); // 2. 获取课程数据 AndroidBridge.showToast('正在采集课程数据...'); const data = await fetchCourseData(semesterInfo.id); if (!data || !data.lessons || data.lessons.length === 0) { throw new Error('未获取到课程数据,请确认已登录并选择了正确的学期'); } // 3. 解析每门课的 scheduleText console.log('[教务适配] 开始解析 ' + data.lessons.length + ' 门课程...'); let allCourses = []; for (const lesson of data.lessons) { const courses = parseLessonScheduleText(lesson); if (courses.length > 0) { const courseName = (lesson.course && lesson.course.nameZh) || lesson.nameZh; console.log('[教务适配] ' + courseName + ': 解析出 ' + courses.length + ' 条排课记录'); allCourses.push(...courses); } else { const courseName = (lesson.course && lesson.course.nameZh) || lesson.nameZh || '(未知)'; console.warn('[教务适配] ' + courseName + ' (id=' + lesson.id + '): 未能解析出排课记录'); // 输出 lesson 的 scheduleText 原始内容供调试 if (lesson.scheduleText) { console.log(' scheduleText 内容:', JSON.stringify(lesson.scheduleText)); } else { console.log(' lesson 没有 scheduleText 字段, keys:', Object.keys(lesson)); } } } console.log('[教务适配] 解析得到 ' + allCourses.length + ' 条课程记录'); // 4. 合并相同课程的周次 allCourses = mergeCourseWeeks(allCourses); console.log('[教务适配] 合并后共 ' + allCourses.length + ' 条课程记录'); if (allCourses.length === 0) { throw new Error('未能解析出任何课程数据。请查看控制台日志。'); } // 输出课程样本 console.log('[教务适配] 课程样本 (前5条):'); allCourses.slice(0, 5).forEach((c, i) => { console.log(` ${i + 1}. ${c.name} | ${c.teacher} | ${c.position} | 周${c.day} | ${c.startSection}-${c.endSection}节 | 周次:${c.weeks.join(',')}`); }); // 5. 时间段配置 const timeSlots = getDefaultTimeSlots(); // 6. 课表配置 const config = getCourseConfig(semesterInfo); console.log('[教务适配] 准备导入:'); console.log(' - 课程数:', allCourses.length); console.log(' - 时间段数:', timeSlots.length); console.log(' - 学期总周数:', config.semesterTotalWeeks); console.log(' - 开学日期:', config.semesterStartDate); // 7. 导入数据 AndroidBridge.showToast(`正在导入 ${allCourses.length} 门课程...`); const courseResult = await window.AndroidBridgePromise.saveImportedCourses( JSON.stringify(allCourses) ); console.log('[教务适配] 课程导入结果:', courseResult); const timeSlotResult = await window.AndroidBridgePromise.savePresetTimeSlots( JSON.stringify(timeSlots) ); console.log('[教务适配] 时间段导入结果:', timeSlotResult); const configResult = await window.AndroidBridgePromise.saveCourseConfig( JSON.stringify(config) ); console.log('[教务适配] 配置导入结果:', configResult); // 8. 触发下载 console.log('[教务适配] ✅ 所有数据导入完成,触发下载...'); AndroidBridge.showToast('课程数据采集完成!请保存下载的文件。'); AndroidBridge.notifyTaskCompletion(); } catch (error) { console.error('[教务适配] ❌ 执行失败:', error.message); console.error(error.stack); AndroidBridge.showToast('采集失败: ' + error.message); } } // 启动 main();