| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- // 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();
|