| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481 |
- // 文件: cust_chaoyang.js
- // 长春理工大学朝阳校区 kbpro 课程表导入脚本
- //
- // 适用校区:朝阳校区;沿用现有第 1~12 节作息配置。
- // 导入学生个人课表,课程地点仍按接口返回的校区、教学楼和教室显示。
- // 导入流程:判断页面 → 获取学期和课程 → 合并课程 → 保存课程 → 保存每节作息 → 通知完成。
- // 数据分开保存:课程包含星期、周次和起止节次;作息表包含各节次的开始、结束时间。
- // 本脚本使用 V2 的 window.shiguangBridge / window.shiguangBridgePromise。
- // 课表页面用于建立当前登录会话;两个接口请求均使用该会话的 Cookie。
- const SCHEDULE_PAGE_URL = 'https://kbpro.cust.edu.cn/Schedule/';
- const SCHEDULE_INFO_API = 'https://kbpro.cust.edu.cn/Schedule/scheduleInfo';
- const SCHEDULE_JSON_API = 'https://kbpro.cust.edu.cn/Schedule/getSchedulejson';
- // 判断当前是否已经进入新版课表页面
- function isOnSchedulePage() {
- try {
- const url = new URL(window.location.href);
- return url.hostname.toLowerCase() === 'kbpro.cust.edu.cn'
- && url.pathname.toLowerCase().startsWith('/schedule');
- } catch (error) {
- return false;
- }
- }
- // 直接进入新版课表页面,不再进入旧教务系统 Student 页面
- // 跳转只负责打开目标页面;当前执行不会在跳转后接着请求数据,需要在新页面执行脚本。
- function redirectToSchedulePage() {
- console.log('当前不在新版课表页面,正在跳转:', SCHEDULE_PAGE_URL);
- window.shiguangBridge.showToast('正在打开课程表...');
- window.location.replace(SCHEDULE_PAGE_URL);
- }
- // 通用 GET JSON 请求。Cookie 由浏览器当前登录会话自动携带,不写死 JSESSIONID。
- async function fetchJson(url, description) {
- const response = await fetch(url, {
- method: 'GET',
- headers: {
- 'Accept': 'application/json, text/plain, */*',
- 'Cache-Control': 'no-cache',
- 'Pragma': 'no-cache'
- },
- credentials: 'include',
- cache: 'no-store'
- });
- if (!response.ok) {
- throw new Error(`${description}失败,HTTP ${response.status}`);
- }
- const contentType = response.headers.get('content-type') || '';
- // 登录失效时可能返回登录页 HTML,不能把它当作课程 JSON 继续导入。
- if (!contentType.toLowerCase().includes('application/json')) {
- throw new Error(`${description}返回的不是 JSON,可能登录状态已失效`);
- }
- return await response.json();
- }
- // 获取学期、开学日期、当前周以及周次日期映射
- // 当前实现只将这些信息输出到日志,没有调用 saveCourseConfig 保存学期开始日期或总周数。
- async function fetchScheduleInfo() {
- try {
- console.log('正在获取学期信息...');
- const data = await fetchJson(SCHEDULE_INFO_API, '获取学期信息');
- console.log('学期信息:', data);
- return data;
- } catch (error) {
- console.error('获取学期信息失败:', error);
- window.shiguangBridge.showToast('获取学期信息失败: ' + error.message);
- return null;
- }
- }
- // 获取课程明细
- // 预期返回课程数组:courseName/teacherName 为课程和教师,dayOfWeek 为星期,
- // beginSection/endSection 为节次,weekDescription 为周次位图,beginTime/endTime 为时刻。
- async function fetchScheduleData() {
- try {
- console.log('正在获取课程明细...');
- const data = await fetchJson(SCHEDULE_JSON_API, '获取课程数据');
- if (!Array.isArray(data)) {
- throw new Error('课程接口返回格式异常');
- }
- console.log(`成功获取 ${data.length} 条课程明细`);
- console.log('课程接口原始数据:', data);
- return data;
- } catch (error) {
- console.error('获取课程数据失败:', error);
- window.shiguangBridge.showToast('获取课程失败: ' + error.message);
- return null;
- }
- }
- // weekDescription 是 25 位周次位图。
- // 下标直接对应 scheduleInfo.dateList 中的 weekOrder:
- // 第 0 位 = weekOrder 0(开学前一周),正式导入时忽略;第 1 位 = 第 1 周,以此类推。
- // 例如 "0101..." 表示第 1、3 周有课;这是接口约定,不能把所有下标统一加 1。
- function parseWeekDescription(weekDescription) {
- if (typeof weekDescription !== 'string') {
- return [];
- }
- const weeks = [];
- for (let index = 1; index < weekDescription.length; index++) {
- if (weekDescription[index] === '1') {
- weeks.push(index);
- }
- }
- return weeks;
- }
- // 教师显示名称:保持接口第一次出现的顺序,只把空格统一成“、”
- // 重复姓名只保留一次,例如 "张老师 李老师 张老师" 显示为 "张老师、李老师"。
- function normalizeTeacherDisplay(teacherName) {
- if (!teacherName) {
- return '';
- }
- const names = teacherName
- .trim()
- .split(/\s+/)
- .filter(Boolean);
- return [...new Set(names)].join('、');
- }
- // 教师比较键:忽略多人教师在不同周返回时的排列顺序
- // "张老师 李老师" 与 "李老师 张老师" 可归为同一教师集合,显示名称仍保持首次顺序。
- function normalizeTeacherKey(teacherName) {
- if (!teacherName) {
- return '';
- }
- const names = teacherName
- .trim()
- .split(/\s+/)
- .filter(Boolean);
- return [...new Set(names)].sort().join('、');
- }
- // 拼接完整上课地点
- // 按校区、教学楼、教室依次拼接;空字段跳过,全部为空时显示“未指定”。
- function buildPosition(item) {
- return [item.campus, item.buildingName, item.classroomName]
- .map(value => (value || '').trim())
- .filter(Boolean)
- .join(' ') || '未指定';
- }
- // 基于主仓库教程的 mergeAndDistinctCourses:先合并节次与重复记录,再合并同节次的周次。
- // 来源:https://github.com/ShiGuangSchedule/shiguangschedule/wiki/课程合并与去重函数
- // kbpro 补充:周次先去重、处理重叠节次,并在周次合并后继续处理新形成的连续节次。
- function mergeAndDistinctCourses(courses) {
- if (!Array.isArray(courses)) {
- return [];
- }
- // 复制课程和周次数组,避免合并过程修改原始数据。
- let list = courses.map(course => ({
- ...course,
- name: course.name || '',
- teacher: course.teacher || '',
- position: course.position || '',
- weeks: Array.isArray(course.weeks)
- ? Array.from(new Set(course.weeks)).sort((a, b) => a - b)
- : []
- }));
- if (list.length <= 1) {
- return list;
- }
- let previousCount;
- do {
- previousCount = list.length;
- // 阶段 1:名称、教师、地点、星期、周次一致时合并连续节次,并去掉重复或重叠部分。
- list.sort((a, b) =>
- a.name.localeCompare(b.name)
- || a.teacher.localeCompare(b.teacher)
- || a.position.localeCompare(b.position)
- || a.day - b.day
- || a.weeks.join(',').localeCompare(b.weeks.join(','))
- || a.startSection - b.startSection
- || a.endSection - b.endSection
- );
- const step1Merged = [];
- let current = list[0];
- for (let index = 1; index < list.length; index++) {
- const next = list[index];
- const isSameCourseAndWeeks = current.name === next.name
- && current.teacher === next.teacher
- && current.position === next.position
- && current.day === next.day
- && current.weeks.join(',') === next.weeks.join(',');
- // 已按开始节次排序,因此该条件同时覆盖连续、完全重复和部分重叠的节次。
- if (isSameCourseAndWeeks && next.startSection <= current.endSection + 1) {
- current.endSection = Math.max(current.endSection, next.endSection);
- } else {
- step1Merged.push(current);
- current = next;
- }
- }
- step1Merged.push(current);
- // 阶段 2:名称、教师、地点、星期及起止节次一致时,合并并排序上课周次。
- step1Merged.sort((a, b) =>
- a.name.localeCompare(b.name)
- || a.teacher.localeCompare(b.teacher)
- || a.position.localeCompare(b.position)
- || a.day - b.day
- || a.startSection - b.startSection
- || a.endSection - b.endSection
- );
- const step2Merged = [];
- let cur = step1Merged[0];
- for (let index = 1; index < step1Merged.length; index++) {
- const next = step1Merged[index];
- const isSameCourseAndSection = cur.name === next.name
- && cur.teacher === next.teacher
- && cur.position === next.position
- && cur.day === next.day
- && cur.startSection === next.startSection
- && cur.endSection === next.endSection;
- if (isSameCourseAndSection) {
- cur.weeks = Array.from(new Set([...cur.weeks, ...next.weeks])).sort((a, b) => a - b);
- } else {
- step2Merged.push(cur);
- cur = next;
- }
- }
- step2Merged.push(cur);
- list = step2Merged;
- // 周次合并可能让相邻时段的周次变得一致;每轮有合并时再处理,记录数不再减少即停止。
- } while (list.length < previousCount);
- return list;
- }
- // 将 getSchedulejson 返回结果转换成时光课表格式
- // 返回 { courses, timeSlots }:courses 用于课程导入,timeSlots 是接口给出的课程时段边界。
- // timeSlots 不是完整的每节作息表,后续由 generateTimeSlots 补齐为第 1~12 节。
- function convertScheduleData(apiData) {
- // 先解析为教程函数要求的标准课程数组,作息边界单独收集。
- const rawCourses = [];
- const teacherDisplaysMap = new Map();
- const timeSlotsMap = new Map();
- apiData.forEach(item => {
- const courseName = (item.courseName || '').trim();
- const teacherDisplay = normalizeTeacherDisplay(item.teacherName);
- const teacherKey = normalizeTeacherKey(item.teacherName);
- const position = buildPosition(item);
- // 接口字段可能是数字字符串,转为整数后再校验;星期 1~7 分别表示周一~周日。
- const day = Number(item.dayOfWeek);
- const startSection = Number(item.beginSection);
- const endSection = Number(item.endSection);
- const weeks = parseWeekDescription(item.weekDescription);
- // 无课程名称、无正式上课周次或星期/节次不合法的记录不导入。
- if (!courseName || weeks.length === 0) {
- return;
- }
- if (!Number.isInteger(day) || day < 1 || day > 7) {
- console.warn('忽略星期字段异常的课程:', item);
- return;
- }
- if (!Number.isInteger(startSection) || !Number.isInteger(endSection)
- || startSection <= 0 || endSection < startSection) {
- console.warn('忽略节次字段异常的课程:', item);
- return;
- }
- // 教程函数直接比较 teacher 字段,先使用排序后的教师集合,防止姓名顺序不同影响合并。
- // 显示名称另外保留,合并后还原为接口首次出现的顺序。
- const teacherDisplayKey = JSON.stringify([courseName, teacherKey, position, day]);
- if (!teacherDisplaysMap.has(teacherDisplayKey)) {
- teacherDisplaysMap.set(teacherDisplayKey, teacherDisplay);
- }
- rawCourses.push({
- name: courseName,
- teacher: teacherKey,
- position,
- day,
- startSection,
- endSection,
- weeks
- });
- // 收集 API 给出的时间范围,用于覆盖默认作息时间的起止边界
- // beginTime/endTime 表示整段课程的开始和结束,不能据此推算中间每节课的时间。
- const beginTime = (item.beginTime || '').trim();
- const endTime = (item.endTime || '').trim();
- if (beginTime && endTime) {
- const timeKey = `${startSection}-${endSection}-${beginTime}-${endTime}`;
- timeSlotsMap.set(timeKey, {
- startSection,
- endSection,
- startTime: beginTime,
- endTime
- });
- }
- });
- // 调用教程的合并去重函数,再还原教师显示名称,不向 App 输出内部比较字段。
- const courses = mergeAndDistinctCourses(rawCourses).map(course => ({
- ...course,
- teacher: teacherDisplaysMap.get(JSON.stringify([
- course.name, course.teacher, course.position, course.day
- ])) ?? course.teacher
- }));
- // 最终按星期、起止节次、课程名称排序,便于检查导入结果。
- courses.sort((a, b) => {
- if (a.day !== b.day) return a.day - b.day;
- if (a.startSection !== b.startSection) return a.startSection - b.startSection;
- if (a.endSection !== b.endSection) return a.endSection - b.endSection;
- return a.name.localeCompare(b.name, 'zh-CN');
- });
- console.log(`接口原始记录: ${apiData.length} 条`);
- console.log(`合并后的课程记录: ${courses.length} 条`);
- console.log('转换后的课程:', courses);
- return {
- courses,
- timeSlots: Array.from(timeSlotsMap.values())
- };
- }
- // 生成朝阳校区时间段配置
- // 每节都有独立的 number、startTime、endTime;当前共配置 12 节,时刻格式为 HH:mm。
- // 下表为现有朝阳校区的默认作息,不表示每一节都由本次接口返回。
- // 本文件的作息只用于朝阳校区;春明湖校区需另行配置独立脚本和时间表。
- // 课程记录只保存节次,App 使用这张作息表将节次对应到具体时间,不使用课程自定义时间模式。
- function generateTimeSlots(timeSlotsFromAPI) {
- const defaultTimeSlots = [
- { number: 1, startTime: '08:00', endTime: '08:45' },
- { number: 2, startTime: '08:55', endTime: '09:35' },
- { number: 3, startTime: '10:05', endTime: '10:50' },
- { number: 4, startTime: '11:00', endTime: '11:40' },
- { number: 5, startTime: '13:30', endTime: '14:15' },
- { number: 6, startTime: '14:25', endTime: '15:05' },
- { number: 7, startTime: '15:35', endTime: '16:20' },
- { number: 8, startTime: '16:30', endTime: '17:10' },
- { number: 9, startTime: '18:00', endTime: '18:45' },
- { number: 10, startTime: '18:45', endTime: '19:35' },
- { number: 11, startTime: '19:45', endTime: '20:30' },
- { number: 12, startTime: '20:30', endTime: '21:20' }
- ];
- if (Array.isArray(timeSlotsFromAPI)) {
- timeSlotsFromAPI.forEach(slot => {
- // 只覆盖首节的开始时间、末节的结束时间,其余节次继续使用默认值。
- // 例如接口返回 5-7 节 13:30~16:20:更新第 5 节开始、第 7 节结束,
- // 第 5 节结束、第 6 节起止、第 7 节开始仍按内置作息,不均分课程总时长。
- // 同一边界有多条不同时间时,后处理的记录覆盖先处理的记录。
- // 接口节次超出 1~12 时找不到对应作息,因此不会新增节次。
- const first = defaultTimeSlots.find(item => item.number === slot.startSection);
- const last = defaultTimeSlots.find(item => item.number === slot.endSection);
- if (first && slot.startTime) {
- first.startTime = slot.startTime;
- }
- if (last && slot.endTime) {
- last.endTime = slot.endTime;
- }
- });
- }
- return defaultTimeSlots;
- }
- // 主函数:获取并导入课程
- // 所有数据保存都 await V2 Promise 接口;获取失败、没有课程或课程保存失败时退出。
- async function importCourseSchedule() {
- try {
- console.log('开始导入朝阳校区 kbpro 课程表...');
- window.shiguangBridge.showToast('正在获取课程表...');
- // 1. 获取学期信息供日志查看;此处不修改 App 的学期配置。
- const scheduleInfo = await fetchScheduleInfo();
- if (!scheduleInfo) {
- return false;
- }
- console.log(
- `当前学期: ${scheduleInfo.termName || '未知'},`
- + `开学日期: ${scheduleInfo.schoolStartTime || '未知'},`
- + `当前周: ${scheduleInfo.weekNum ?? '未知'}`
- );
- // 2. 获取当前会话对应的课程数组,并转换为 App 需要的字段。
- const scheduleData = await fetchScheduleData();
- if (!scheduleData) {
- return false;
- }
- const { courses, timeSlots } = convertScheduleData(scheduleData);
- if (courses.length === 0) {
- window.shiguangBridge.showToast('未找到可导入的课程');
- return false;
- }
- // 3. 先保存课程:name、teacher、position、day、startSection、endSection、weeks。
- // 起止时刻不直接放入课程对象,下一步单独保存每节作息。
- const coursesResult = await window.shiguangBridgePromise.saveImportedCourses(
- JSON.stringify(courses)
- );
- if (coursesResult !== true) {
- console.error('课程导入失败,返回:', coursesResult);
- window.shiguangBridge.showToast('课程导入失败');
- return false;
- }
- console.log('课程导入成功');
- window.shiguangBridge.showToast(`成功导入 ${courses.length} 条课程记录!`);
- // 4. 以朝阳校区 12 节默认作息为基础应用接口边界,再提交完整作息表。
- const finalTimeSlots = generateTimeSlots(timeSlots);
- console.log('朝阳校区时间段配置:', finalTimeSlots);
- const timeSlotsResult = await window.shiguangBridgePromise.savePresetTimeSlots(
- JSON.stringify(finalTimeSlots)
- );
- if (timeSlotsResult === true) {
- console.log('时间段导入成功');
- window.shiguangBridge.showToast('朝阳校区作息配置成功!');
- } else {
- // 返回 false 时保留已导入课程,并提示作息失败;抛出异常时则进入下面的 catch。
- console.warn('时间段导入失败,返回:', timeSlotsResult);
- window.shiguangBridge.showToast('课程已导入,但时间段配置失败');
- }
- return true;
- } catch (error) {
- console.error('导入过程出错:', error);
- window.shiguangBridge.showToast('导入失败: ' + error.message);
- return false;
- }
- }
- // ========== 主执行逻辑 ==========
- if (!isOnSchedulePage()) {
- redirectToSchedulePage();
- } else {
- console.log('已进入长春理工大学朝阳校区新版课表导入流程');
- window.shiguangBridge.showToast('正在准备导入朝阳校区课程表...');
- // 等待页面和登录态稳定后请求接口
- setTimeout(async () => {
- const success = await importCourseSchedule();
- if (success) {
- // 完成信号用于通知 App 收尾;获取或课程保存失败时不发送。
- window.shiguangBridge.notifyTaskCompletion();
- }
- }, 1000);
- }
|