| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- /**
- * 拾光课程表适配脚本 - 南方医科大学 (smu.edu.cn)
- * 乘方教务系统适配
- * 出现问题请提issues或者提交pr更改,这更加快速
- */
- // 基础配置
- const BASE_URL = "https://zhjw.smu.edu.cn/new";
- // 校区与作息时间映射
- const CAMPUS_CONFIG = {
- // 校本部 (00001)
- "00001": {
- name: "校本部",
- timeSlots: [
- { number: 1, startTime: "08:00", endTime: "08:40" },
- { number: 2, startTime: "08:45", endTime: "09:25" },
- { number: 3, startTime: "09:50", endTime: "10:30" },
- { number: 4, startTime: "10:35", endTime: "11:15" },
- { number: 5, startTime: "11:20", endTime: "12:00" },
- { number: 6, startTime: "14:30", endTime: "15:10" },
- { number: 7, startTime: "15:15", endTime: "15:55" },
- { number: 8, startTime: "16:15", endTime: "16:55" },
- { number: 9, startTime: "17:00", endTime: "17:40" },
- { number: 10, startTime: "19:30", endTime: "20:10" },
- { number: 11, startTime: "20:15", endTime: "20:55" },
- { number: 12, startTime: "21:00", endTime: "21:40" },
- { number: 13, startTime: "21:45", endTime: "22:25" }
- ]
- },
- // 顺德校区 (00009)
- "00009": {
- name: "顺德校区",
- timeSlots: [
- { number: 1, startTime: "08:30", endTime: "09:10" },
- { number: 2, startTime: "09:15", endTime: "09:55" },
- { number: 3, startTime: "10:20", endTime: "11:00" },
- { number: 4, startTime: "11:05", endTime: "11:45" },
- { number: 5, startTime: "11:50", endTime: "12:30" },
- { number: 6, startTime: "14:00", endTime: "14:40" },
- { number: 7, startTime: "14:45", endTime: "15:25" },
- { number: 8, startTime: "15:45", endTime: "16:25" },
- { number: 9, startTime: "16:30", endTime: "17:10" },
- { number: 10, startTime: "19:30", endTime: "20:10" },
- { number: 11, startTime: "20:15", endTime: "20:55" },
- { number: 12, startTime: "21:00", endTime: "21:40" },
- { number: 13, startTime: "21:45", endTime: "22:25" }
- ]
- }
- };
- // 周次解析
- /**
- * 解析周次字符串,例如 "1,2,3,4,5" -> [1, 2, 3, 4, 5]
- */
- function parseWeeks(weekStr) {
- if (!weekStr) return [];
- const weeks = weekStr.split(',')
- .map(w => Number(w.trim()))
- .filter(w => !isNaN(w) && w > 0);
- return [...new Set(weeks)].sort((a, b) => a - b);
- }
- // 合并去重函数
- /**
- * 节次与周次合并去重函数
- */
- function mergeAndDistinctCourses(courses) {
- if (!Array.isArray(courses) || courses.length <= 1) return courses;
- const grouped = {};
- courses.forEach(c => {
- const name = c.name || '';
- const teacher = c.teacher || '';
- const position = c.position || '';
- const day = c.day || 0;
- const isCustom = c.isCustomTime || false;
- const cst = c.customStartTime || '';
- const cet = c.customEndTime || '';
- const key = `${day}|${name}|${teacher}|${position}|${isCustom}|${cst}|${cet}`;
- if (!grouped[key]) {
- grouped[key] = {
- name, teacher, position, day,
- isCustomTime: isCustom,
- customStartTime: cst,
- customEndTime: cet,
- weeksMap: {}
- };
- }
- const weeksArr = Array.isArray(c.weeks) ? [...new Set(c.weeks)] : [];
- weeksArr.forEach(w => {
- if (!grouped[key].weeksMap[w]) grouped[key].weeksMap[w] = [];
- grouped[key].weeksMap[w].push({ start: c.startSection, end: c.endSection });
- });
- });
- const finalResult = [];
- Object.values(grouped).forEach(group => {
- const sortedWeeks = Object.keys(group.weeksMap).map(Number).sort((a, b) => a - b);
- const processedWeeksMap = {};
- sortedWeeks.forEach(w => {
- let parts = group.weeksMap[w];
- parts.sort((a, b) => a.start - b.start || a.end - b.end);
- const mergedParts = [];
- let cur = parts[0];
- for (let i = 1; i < parts.length; i++) {
- if (cur.end + 1 >= parts[i].start) {
- cur.end = Math.max(cur.end, parts[i].end);
- } else {
- mergedParts.push(cur);
- cur = parts[i];
- }
- }
- mergedParts.push(cur);
- processedWeeksMap[w] = mergedParts;
- });
- const sectionsMap = {};
- sortedWeeks.forEach(w => {
- const parts = processedWeeksMap[w];
- const sig = parts.map(p => `${p.start}-${p.end}`).join('|');
- if (!sectionsMap[sig]) sectionsMap[sig] = { sections: parts, weeks: [] };
- sectionsMap[sig].weeks.push(w);
- });
- Object.values(sectionsMap).forEach(secGroup => {
- secGroup.sections.forEach(sec => {
- const baseCourse = {
- name: group.name,
- teacher: group.teacher,
- position: group.position,
- day: group.day,
- startSection: sec.start,
- endSection: sec.end,
- weeks: secGroup.weeks
- };
- if (group.isCustomTime) {
- baseCourse.isCustomTime = true;
- baseCourse.customStartTime = group.customStartTime;
- baseCourse.customEndTime = group.customEndTime;
- }
- finalResult.push(baseCourse);
- });
- });
- });
- finalResult.sort((a, b) => {
- return (a.day || 0) - (b.day || 0) ||
- (a.startSection || 0) - (b.startSection || 0) ||
- a.name.localeCompare(b.name) ||
- a.weeks[0] - b.weeks[0];
- });
- return finalResult;
- }
- // JSON 解析
- /**
- * 将教务系统返回的 JSON 转换为拾光标准的 CourseJsonModel 数组
- */
- function parseJsonData(jsonData) {
- if (!jsonData || jsonData.code !== 0 || !Array.isArray(jsonData.data)) {
- return { courses: [], campusCodes: [] };
- }
- let allProcessedCourses = [];
- let detectedCampusCodes = new Set();
- jsonData.data.forEach(raw => {
- if (!(raw.kcmc && raw.xq && raw.ps && raw.pe)) return;
- if (raw.kkxqdm) {
- detectedCampusCodes.add(String(raw.kkxqdm).trim());
- }
- const startSec = Number(raw.ps);
- const endSec = Number(raw.pe);
- let isCustomTime = false;
- let customStartTime = "";
- let customEndTime = "";
- // 【特殊逻辑说明】
- // 南方医科大学教务系统从第10节开始(晚间)的起止时间,与常规的“40分钟一节、课间5分钟”基础作息规则不一致。
- // 为了保证课表显示的实际上下课时间与教务系统完全吻合,遇到 >=10 节的课程,
- // 强制启用 isCustomTime,并直接使用接口返回的真实起止时间(qssj/jssj),不再依赖预设的基础节次时间。
- if (startSec >= 10) {
- isCustomTime = true;
- customStartTime = raw.qssj ? raw.qssj.substring(0, 5) : "";
- customEndTime = raw.jssj ? raw.jssj.substring(0, 5) : "";
- }
- const locationMap = new Map();
- if (raw.jxcdmc2) {
- const parts = raw.jxcdmc2.split(',');
- parts.forEach(part => {
- const lastDashIndex = part.lastIndexOf('-');
- if (lastDashIndex !== -1) {
- const room = part.substring(0, lastDashIndex).trim();
- const week = Number(part.substring(lastDashIndex + 1));
- if (!isNaN(week)) {
- if (!locationMap.has(room)) locationMap.set(room, []);
- locationMap.get(room).push(week);
- }
- }
- });
- }
- if (locationMap.size > 0) {
- locationMap.forEach((weeks, room) => {
- const courseObj = {
- name: raw.kcmc.trim(),
- teacher: (raw.teaxms || "未知教师").trim(),
- position: room || "不用场地",
- day: Number(raw.xq),
- startSection: startSec,
- endSection: endSec,
- weeks: weeks.sort((a, b) => a - b)
- };
- if (isCustomTime) {
- courseObj.isCustomTime = true;
- courseObj.customStartTime = customStartTime;
- courseObj.customEndTime = customEndTime;
- }
- allProcessedCourses.push(courseObj);
- });
- } else if (raw.zc) {
- const courseObj = {
- name: raw.kcmc.trim(),
- teacher: (raw.teaxms || "未知教师").trim(),
- position: (raw.jxcdmc || "").trim() || "不用场地",
- day: Number(raw.xq),
- startSection: startSec,
- endSection: endSec,
- weeks: parseWeeks(raw.zc)
- };
- if (isCustomTime) {
- courseObj.isCustomTime = true;
- courseObj.customStartTime = customStartTime;
- courseObj.customEndTime = customEndTime;
- }
- allProcessedCourses.push(courseObj);
- }
- });
- const filtered = allProcessedCourses.filter(course => {
- return course.weeks.length > 0 && course.startSection <= course.endSection;
- });
- return {
- courses: mergeAndDistinctCourses(filtered),
- campusCodes: Array.from(detectedCampusCodes)
- };
- }
- // 学年学期选项获取
- async function fetchAcademicOptions() {
- const url = `${BASE_URL}/student/xlxx/xlxxlist.page`;
- try {
- const response = await fetch(url, {
- method: "GET",
- credentials: "include"
- });
- if (!response.ok) return null;
- const htmlText = await response.text();
- const parser = new DOMParser();
- const doc = parser.parseFromString(htmlText, "text/html");
- const allOptions = Array.from(doc.querySelectorAll("#xnxqdm option"))
- .filter(opt => opt.value !== "")
- .map(opt => ({
- value: opt.value,
- text: opt.textContent.trim(),
- selected: opt.selected
- }));
- if (allOptions.length === 0) return null;
- const selectedIndex = allOptions.findIndex(opt => opt.selected);
- if (selectedIndex === -1) {
- return {
- options: allOptions.slice(0, 7),
- defaultIndex: 0
- };
- }
- const start = Math.max(0, selectedIndex - 3);
- const end = Math.min(allOptions.length, selectedIndex + 4);
- const options = allOptions.slice(start, end);
- const newDefaultIndex = selectedIndex - start;
- return { options, defaultIndex: newDefaultIndex };
- } catch (e) {
- return null;
- }
- }
- // 校历数据获取
- async function fetchSemesterCalendar(xnxqdm) {
- const url = `${BASE_URL}/xlxx/data`;
- const requestBody = `xnxqdm=${xnxqdm}`;
- try {
- const response = await fetch(url, {
- method: "POST",
- headers: {
- "accept": "application/json, text/javascript, */*; q=0.01",
- "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
- "x-requested-with": "XMLHttpRequest"
- },
- body: requestBody,
- credentials: "include"
- });
- if (!response.ok) return { startDate: null, totalWeeks: null };
- const json = await response.json();
- if (!json || json.code !== 0 || !Array.isArray(json.data)) {
- return { startDate: null, totalWeeks: null };
- }
- let earliestDate = null;
- let earliestWeek = Infinity;
- let maxWeek = 0;
- json.data.forEach(monthBlock => {
- if (!Array.isArray(monthBlock)) return;
- monthBlock.forEach(row => {
- if (!Array.isArray(row) || row.length === 0) return;
- const weekHead = row[0];
- if (!weekHead || weekHead.type !== "zc") return;
- const weekNum = Number(weekHead.value);
- if (isNaN(weekNum)) return;
- if (weekNum > maxWeek) maxWeek = weekNum;
- for (let i = 1; i < row.length; i++) {
- const cell = row[i];
- if (!cell || cell.type !== "day") continue;
- if (!cell.fullText) continue;
- if (!/^\d{4}-\d{2}-\d{2}$/.test(cell.fullText)) continue;
- if (weekNum < earliestWeek ||
- (weekNum === earliestWeek && cell.fullText < earliestDate)) {
- earliestWeek = weekNum;
- earliestDate = cell.fullText;
- }
- }
- });
- });
- return {
- startDate: earliestDate,
- totalWeeks: maxWeek > 0 ? maxWeek : null
- };
- } catch (e) {
- return { startDate: null, totalWeeks: null };
- }
- }
- // 保存逻辑模块
- async function saveCampusTimeSlotsAndConfig(campusCodes, calendar) {
- let finalConfig = CAMPUS_CONFIG["00001"];
-
- let matchedCode = campusCodes.find(code => CAMPUS_CONFIG[code]);
-
- if (matchedCode) {
- finalConfig = CAMPUS_CONFIG[matchedCode];
- } else if (campusCodes.length > 0) {
- const unknownStr = campusCodes.join(", ");
- await window.shiguangBridgePromise.showAlert(
- "未记录校区提醒",
- `检测到课表中包含未记录的校区代码(${unknownStr})。\n系统将默认使用【校本部】作息时间。`,
- "好的,继续导入"
- );
- }
- try {
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(finalConfig.timeSlots));
- if (calendar && (calendar.startDate || calendar.totalWeeks)) {
- const config = {};
- if (calendar.startDate) config.semesterStartDate = calendar.startDate;
- if (calendar.totalWeeks) config.semesterTotalWeeks = calendar.totalWeeks;
- await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
-
- let msg = `配置更新成功!(使用 ${finalConfig.name} 作息)`;
- if (calendar.startDate) msg += ` 开学:${calendar.startDate}`;
- if (calendar.totalWeeks) msg += ` 总周数:${calendar.totalWeeks}`;
- window.shiguangBridge.showToast(msg);
- } else {
- window.shiguangBridge.showToast(`作息已更新为 ${finalConfig.name},但未获取到校历信息。`);
- }
- } catch (e) {
- window.shiguangBridge.showToast("保存配置或作息失败: " + e.message);
- }
- }
- // 用户交互
- async function promptUserToStart() {
- return await window.shiguangBridgePromise.showAlert(
- "教务导入说明",
- "1. 请确保已在浏览器中成功登录教务系统\n2. 导入过程中请勿关闭页面",
- "好的,开始导入"
- );
- }
- async function selectAcademicYearAndSemester() {
- const optionsData = await fetchAcademicOptions();
- if (!optionsData) {
- window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
- return null;
- }
- const { options, defaultIndex } = optionsData;
- const texts = options.map(item => item.text);
- const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
- "选择学年学期",
- JSON.stringify(texts),
- defaultIndex
- );
- if (selectedIndex === null || selectedIndex === -1) return null;
- return {
- xnxqdm: options[selectedIndex].value,
- xnxqmc: options[selectedIndex].text
- };
- }
- // 网络请求
- async function fetchCourses(xnxqdm) {
- window.shiguangBridge.showToast("正在请求教务数据...");
- const body = `xnxqdm=${xnxqdm}`;
- const url = `${BASE_URL}/student/xsgrkb/getCalendarWeekDatas`;
- try {
- const response = await fetch(url, {
- method: "POST",
- headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
- body: body,
- credentials: "include"
- });
- if (!response.ok) throw new Error(`HTTP Error: ${response.status}`);
- const jsonData = await response.json();
- return parseJsonData(jsonData);
- } catch (e) {
- window.shiguangBridge.showToast("网络请求失败,请检查登录状态");
- return null;
- }
- }
- // 主流程编排
- async function runImportFlow() {
- const isReady = await promptUserToStart();
- if (!isReady) return;
- const selection = await selectAcademicYearAndSemester();
- if (!selection) {
- window.shiguangBridge.showToast("未选择学年学期,导入终止。");
- return;
- }
- const { xnxqdm } = selection;
- const [courseData, calendar] = await Promise.all([
- fetchCourses(xnxqdm),
- fetchSemesterCalendar(xnxqdm)
- ]);
- if (!courseData || !courseData.courses || courseData.courses.length === 0) {
- if (courseData && courseData.courses && courseData.courses.length === 0) {
- window.shiguangBridge.showToast("该学期暂无课程数据");
- }
- return;
- }
- const { courses, campusCodes } = courseData;
- try {
- await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
- await saveCampusTimeSlotsAndConfig(campusCodes, calendar);
- window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
- window.shiguangBridge.notifyTaskCompletion();
- } catch (e) {
- window.shiguangBridge.showToast("保存失败: " + e.message);
- }
- }
- // 启动入口
- runImportFlow();
|