// 西安建筑科技大学(XAUAT)拾光课程表适配脚本 · 树维教务平台 · 本科 "use strict"; (function () { // bizTypeId=2:本科课表(接口约定,勿改) const BIZ_TYPE_ID = 2; // datum 接口要求 studentId 传字符串 "null"(接口约定,勿改) const DATUM_STUDENT_ID = "null"; const LIMITS = { timeout: 15000, minWeeks: 16, maxWeeks: 25, fallbackWeeks: 20 }; const MS_PER_DAY = 86400000; const DAYS_PER_WEEK = 7; const ENABLE_LOG = false; // release:关闭普通日志,仅保留 error const API = { courseTablePage: "/student/for-std/course-table", lessonIds: (s) => "/student/for-std/course-table/get-data?bizTypeId=" + BIZ_TYPE_ID + "&semesterId=" + encodeURIComponent(s) + "&dataId=", datum: "/student/ws/schedule-table/datum", semester: (s) => "/student/ws/semester/get/" + encodeURIComponent(s), }; const log = { info: (m) => ENABLE_LOG && console.log("[XAUAT] " + m), warn: (m) => ENABLE_LOG && console.warn("[XAUAT] " + m), error: (m) => console.error("[XAUAT] " + m), }; // 桥接调用统一封装,其余代码不直接访问 window.shiguangBridge const ui = { toast: (m) => window.shiguangBridge.showToast(m), alert: (t, m, b) => window.shiguangBridgePromise.showAlert(t, m, b), select: (t, items, d) => window.shiguangBridgePromise.showSingleSelection(t, JSON.stringify(items), d), saveConfig: (j) => window.shiguangBridgePromise.saveCourseConfig(j), saveCourses: (j) => window.shiguangBridgePromise.saveImportedCourses(j), done: () => window.shiguangBridge.notifyTaskCompletion(), }; async function requestText(url, options) { const controller = typeof AbortController !== "undefined" ? new AbortController() : null; const timer = controller ? setTimeout(() => controller.abort(), LIMITS.timeout) : null; try { const res = await fetch(url, Object.assign( { credentials: "include" }, options, controller ? { signal: controller.signal } : {} )); if (!res.ok) { if (res.status === 401 || res.status === 403) throw new Error("登录状态已失效,请重新登录教务系统后重试"); throw new Error("网络请求失败,请稍后重试"); } return await res.text(); } catch (e) { if (e.name === "AbortError") throw new Error("请求超时,请检查网络后重试"); throw e; } finally { if (timer) clearTimeout(timer); } } const requestJson = async (url, options) => JSON.parse(await requestText(url, options)); const pad2 = (n) => String(n).padStart(2, "0"); const formatTime = (hhmm) => pad2(Math.floor(hhmm / 100)) + ":" + pad2(hhmm % 100); const toDateString = (d) => d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()); const weekdayOf = (dateStr) => { const d = new Date(dateStr).getDay(); return d === 0 ? 7 : d; }; const clampWeeks = (w) => Math.min(Math.max(w, LIMITS.minWeeks), LIMITS.maxWeeks); // 周次去重升序:值域有界,用桶排序 function uniqueSortedWeeks(weeks) { const seen = new Array(LIMITS.maxWeeks + 1).fill(false); for (const w of weeks) if (w >= 1 && w <= LIMITS.maxWeeks) seen[w] = true; const out = []; for (let w = 1; w <= LIMITS.maxWeeks; w++) if (seen[w]) out.push(w); return out; } function calculateTotalWeeks(start, end) { if (!start || !end) return LIMITS.fallbackWeeks; return Math.ceil(Math.ceil((new Date(end) - new Date(start)) / MS_PER_DAY) / DAYS_PER_WEEK); } // 由最早上课日期反推开学日(所在周周一)。局限:假设最早上课日在第一周。 function deriveSemesterStartDate(scheduleList) { let minDate = null; for (const it of scheduleList) if (it.date && (minDate === null || it.date < minDate)) minDate = it.date; if (!minDate) return null; const d = new Date(minDate); d.setDate(d.getDate() - ((d.getDay() + 6) % 7)); return toDateString(d); } function getMaxCourseWeek(courses) { let max = 0; for (const c of courses) for (const w of c.weeks || []) if (w > max) max = w; return max; } function readPosition(room) { if (!room) return "未知地点"; return (typeof room === "object" ? room.nameZh : room) || "未知地点"; } function convertCourses(lessonList, scheduleList, semesterStartDate) { const names = new Map(lessonList.map((l) => [l.id, l.courseName])); const startMs = new Date(semesterStartDate).getTime(); // 预计算,避免循环内重复解析 const groups = new Map(); for (const it of scheduleList) { if (!it.date || typeof it.startTime !== "number" || typeof it.endTime !== "number") continue; const diffDays = Math.floor((new Date(it.date).getTime() - startMs) / MS_PER_DAY); if (diffDays < 0) continue; const week = Math.floor(diffDays / DAYS_PER_WEEK) + 1; const day = weekdayOf(it.date); const key = it.lessonId + "|" + day + "|" + it.startTime + "|" + it.endTime; if (!groups.has(key)) { groups.set(key, { name: names.get(it.lessonId) || "未知课程", teacher: it.personName || "未知教师", position: readPosition(it.room), day, startTime: it.startTime, endTime: it.endTime, weeks: [], }); } groups.get(key).weeks.push(week); } const courses = []; for (const g of groups.values()) { courses.push({ name: g.name, teacher: g.teacher, position: g.position, day: g.day, weeks: uniqueSortedWeeks(g.weeks), isCustomTime: true, customStartTime: formatTime(g.startTime), customEndTime: formatTime(g.endTime), }); } log.info("转换完成,共 " + courses.length + " 条课程"); return courses; } async function getSemesters() { log.info("获取学期列表..."); const select = new DOMParser().parseFromString(await requestText(API.courseTablePage), "text/html") .querySelector("#allSemesters, #semesters"); if (!select) throw new Error("未找到学期选择框,请确认已登录教务系统"); const semesters = []; for (const opt of select.querySelectorAll("option")) { const id = opt.getAttribute("value"); const name = opt.textContent.trim(); if (id && id !== "all" && name) semesters.push({ id, name }); } if (!semesters.length) throw new Error("学期列表为空"); return semesters; } async function fetchLessonIds(semesterId) { const json = await requestJson(API.lessonIds(semesterId)); if (!json || !Array.isArray(json.lessonIds)) throw new Error("课程数据异常,请稍后重试"); return json.lessonIds; } async function fetchScheduleDatum(lessonIds) { if (!lessonIds || !lessonIds.length) throw new Error("课程列表为空"); const json = await requestJson(API.datum, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ studentId: DATUM_STUDENT_ID, lessonIds }), }); if (!json || !json.result) throw new Error("课程详情数据异常,请稍后重试"); return { lessonList: json.result.lessonList || [], scheduleList: json.result.scheduleList || [] }; } // 学期日期仅作辅助,失败降级为空,不中断主流程 async function fetchSemesterInfo(semesterId) { try { const json = await requestJson(API.semester(semesterId)); return { startDate: json.startDate || null, endDate: json.endDate || null }; } catch (e) { log.warn("学期信息获取失败: " + e.message); return { startDate: null, endDate: null }; } } async function saveConfig(config) { try { await ui.saveConfig(JSON.stringify(config)); } catch (e) { throw new Error("保存配置失败,请重试"); } ui.toast("课表配置导入成功"); } async function saveCourses(courses) { try { await ui.saveCourses(JSON.stringify(courses)); } catch (e) { throw new Error("保存课程失败,请重试"); } ui.toast("成功导入 " + courses.length + " 条课程"); } async function chooseSemester(semesters) { const names = semesters.map((s) => s.name); for (;;) { const index = await ui.select("选择学期", names, 0); if (index === null || index < 0 || index >= semesters.length) return null; const semester = semesters[index]; let lessonIds = []; try { lessonIds = await fetchLessonIds(semester.id); } catch (e) { log.warn("获取 " + semester.name + " 课程失败: " + e.message); } if (lessonIds.length) return { semester, lessonIds }; if (!(await ui.alert("无课程数据", "「" + semester.name + "」没有课程数据,请选择其他学期。", "重新选择"))) return null; } } async function runImportFlow() { try { if (!(await ui.alert("西安建筑科技大学课表导入", "请确保已登录教务系统(swjw.xauat.edu.cn)。\n本适配将自动获取学期与课程数据。", "开始导入"))) { ui.toast("导入已取消"); return; } ui.toast("正在获取学期列表..."); const chosen = await chooseSemester(await getSemesters()); if (!chosen) { ui.toast("导入已取消"); return; } // 课程详情与学期信息无依赖,并行请求 ui.toast("正在获取课程数据..."); const [datum, semesterInfo] = await Promise.all([ fetchScheduleDatum(chosen.lessonIds), fetchSemesterInfo(chosen.semester.id), ]); const startDate = deriveSemesterStartDate(datum.scheduleList) || semesterInfo.startDate; if (!startDate) throw new Error("无法确定开学日期,请重试"); log.info("推算开学日期: " + startDate); const courses = convertCourses(datum.lessonList, datum.scheduleList, startDate); if (!courses.length) { await ui.alert("无课程数据", "未能转换出有效课程。", "确定"); return; } // 优先用课程实际最大周次,回退到日期差计算 const totalWeeks = clampWeeks(getMaxCourseWeek(courses) || calculateTotalWeeks(startDate, semesterInfo.endDate)); await saveConfig({ semesterStartDate: startDate, semesterTotalWeeks: totalWeeks }); await saveCourses(courses); ui.toast("课表导入完成!"); ui.done(); } catch (e) { log.error("导入异常: " + (e.stack || e.message)); await ui.alert("导入失败", e.message || "未知错误,请重试", "确定"); } } runImportFlow(); })();