| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622 |
- // 中山大学教务系统课表导入器
- // 功能:获取教务系统课表 -> 转换为目标 JSON -> 通过时光课程表 V2 Bridge 导入
- const API_URL = "/jwxt/timetable-search/stuTimeTabPrint/studentQuery";
- const CALENDAR_API_URL = "/jwxt/base-info/school-calender";
- let ACAD_YEAR = "2026-1";
- function validateYearInput(input) {
- return /^[0-9]{4}$/.test(String(input).trim())
- ? false
- : "请输入四位数字的学年!";
- }
- async function getAcademicYear() {
- const defaultYear = ACAD_YEAR.split("-")[0];
- const yearSelection = await window.shiguangBridgePromise.showPrompt(
- "选择学年",
- "请输入要导入课程的学年(如 2026):",
- defaultYear || "2026",
- "validateYearInput"
- );
- return yearSelection;
- }
- async function selectSemester() {
- if (!window.shiguangBridgePromise) {
- throw new Error(
- "shiguangBridgePromise 不可用,请在时光课程表 App 内运行此适配器。"
- );
- }
- if (typeof window.shiguangBridgePromise.showPrompt !== "function") {
- throw new Error(
- "shiguangBridgePromise.showPrompt 不可用,请在时光课程表 App 内运行此适配器。"
- );
- }
- if (typeof window.shiguangBridgePromise.showSingleSelection !== "function") {
- throw new Error(
- "shiguangBridgePromise.showSingleSelection 不可用,请在时光课程表 App 内运行此适配器。"
- );
- }
- // 先输入学年
- const yearSelection = await getAcademicYear();
- if (yearSelection === null) {
- return null;
- }
- const year = String(yearSelection).trim();
- // 再选择学期
- const semesters = [
- "1(第一学期)",
- "2(第二学期)"
- ];
- const [, defaultSemester] = ACAD_YEAR.split("-");
- const semesterIndex =
- await window.shiguangBridgePromise.showSingleSelection(
- "选择学期",
- JSON.stringify(semesters),
- defaultSemester === "2" ? 1 : 0
- );
- if (
- semesterIndex === null ||
- semesterIndex < 0 ||
- semesterIndex >= semesters.length
- ) {
- return null;
- }
- ACAD_YEAR = `${year}-${semesterIndex + 1}`;
- return ACAD_YEAR;
- }
- // ========================================
- // 目标课表时间段
- // ========================================
- const TIME_SLOTS = [
- {
- number: 1,
- startTime: "08:00",
- endTime: "08:45"
- },
- {
- number: 2,
- startTime: "08:55",
- endTime: "09:40"
- },
- {
- number: 3,
- startTime: "10:10",
- endTime: "10:55"
- },
- {
- number: 4,
- startTime: "11:05",
- endTime: "11:50"
- },
- {
- number: 5,
- startTime: "14:20",
- endTime: "15:05"
- },
- {
- number: 6,
- startTime: "15:15",
- endTime: "16:00"
- },
- {
- number: 7,
- startTime: "16:30",
- endTime: "17:15"
- },
- {
- number: 8,
- startTime: "17:25",
- endTime: "18:10"
- },
- {
- number: 9,
- startTime: "19:00",
- endTime: "19:45"
- },
- {
- number: 10,
- startTime: "19:55",
- endTime: "20:40"
- },
- {
- number: 11,
- startTime: "20:50",
- endTime: "21:35"
- }
- ];
- // ========================================
- // 课表配置
- // ========================================
- const CONFIG_BASE = {
- semesterTotalWeeks: 20,
- defaultClassDuration: 45,
- defaultBreakDuration: 10
- };
- // ========================================
- // 自动获取学期实际开学日期
- // ========================================
- async function fetchSemesterStartDate() {
- const url =
- `${CALENDAR_API_URL}?academicYear=${encodeURIComponent(ACAD_YEAR)}` +
- `&weekly=1&_t=${Math.floor(Date.now() / 1000)}`;
- const response = await fetch(url, {
- method: "GET",
- credentials: "include"
- });
- if (!response.ok) {
- throw new Error(
- `获取学期开始日期失败:HTTP ${response.status}`
- );
- }
- const json = await response.json();
- const startDate = json?.data?.startTime;
- if (!startDate) {
- throw new Error(
- `教务系统未返回 ${ACAD_YEAR} 第 1 周的开始日期。`
- );
- }
- return startDate;
- }
- async function buildCourseConfig() {
- const semesterStartDate =
- await fetchSemesterStartDate();
- return {
- semesterStartDate,
- ...CONFIG_BASE
- };
- }
- // 请求课程数据
- async function fetchCourses() {
- const url = `${API_URL}?_t=${Date.now()}`;
- const payload = {
- acadYear: ACAD_YEAR,
- submitFlag: "1",
- nothroughCourseFlag: "1"
- };
- const response = await fetch(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- credentials: "include",
- body: JSON.stringify(payload)
- });
- const responseText = await response.text();
- if (!response.ok) {
- throw new Error(
- `API 请求失败:HTTP ${response.status}\n${responseText}`
- );
- }
- let json;
- try {
- json = JSON.parse(responseText);
- } catch {
- throw new Error("教务系统返回的课程数据不是有效的 JSON。\n");
- }
- if (json?.code !== 200) {
- throw new Error(
- `API 返回异常 code:${json?.code}`
- );
- }
- const timetable = json?.data?.timetable;
- if (
- !timetable ||
- typeof timetable !== "object" ||
- Array.isArray(timetable)
- ) {
- throw new Error(
- "API 返回中不存在有效的 data.timetable"
- );
- }
- const courses = [];
- // timetable 的 key:
- // 第一位 = 星期
- // 后两位 = 起始节次
- //
- // 例如:
- // 11 -> 周一第1节
- // 13 -> 周一第3节
- // 21 -> 周二第1节
- for (const entries of Object.values(timetable)) {
- if (!Array.isArray(entries) || entries.length === 0) {
- continue;
- }
- for (const item of entries) {
- const course = normalizeCourse(item);
- if (course) {
- courses.push(course);
- }
- }
- }
- // 自动获取学期配置
- const config = await buildCourseConfig();
- const result = {
- courses,
- timeSlots: TIME_SLOTS,
- config
- };
- return result;
- }
- // 课程数据转换
- function normalizeCourse(item) {
- if (!item || typeof item !== "object") {
- return null;
- }
- const name = cleanCourseName(item.courseName);
- const teacher = cleanText(item.teachingStaffName);
- const position = cleanText(item.classPlace);
- const day = toNumber(item.week);
- const startSection = toNumber(item.startClassTimes);
- const endSection = toNumber(item.endClassTimes);
- const startWeek = toNumber(item.startWeek);
- const weeks = parseWeeks(item.timeDetail, startWeek);
- if (
- !name ||
- !day ||
- !startSection ||
- !endSection ||
- weeks.length === 0
- ) {
- console.warn(
- "跳过字段不完整的课程记录:",
- item
- );
- return null;
- }
- return {
- id: crypto.randomUUID(),
- name,
- teacher,
- position,
- day,
- startSection,
- endSection,
- weeks
- };
- }
- // 清理课程名称
- function cleanCourseName(value) {
- const text = cleanText(value);
- // 去掉中山大学 API 返回的课程类别前缀
- //
- // 本(专必)高等数学一(I)
- // ↓
- // 高等数学一(I)
- //
- // 本(公必)劳动教育
- // ↓
- // 劳动教育
- return text
- .replace(/^本\([^)]*\)/, "")
- .trim();
- }
- // 解析上课周次
- function parseWeeks(
- timeDetail,
- startWeek = 1
- ) {
- const text =
- cleanText(timeDetail);
- if (!text) {
- return startWeek
- ? [startWeek]
- : [];
- }
- // 例如:
- // 1-17每周
- const rangeMatch =
- text.match(/(\d+)\s*-\s*(\d+)/);
- if (rangeMatch) {
- const start =
- Number(rangeMatch[1]);
- const end =
- Number(rangeMatch[2]);
- if (
- Number.isFinite(start) &&
- Number.isFinite(end) &&
- end >= start
- ) {
- return Array.from(
- {
- length:
- end - start + 1
- },
- (_, index) =>
- start + index
- );
- }
- }
- // 兼容:
- //
- // 1,3,5周
- // 1、3、5周
- const numbers =
- text
- .match(/\d+/g)
- ?.map(Number)
- .filter(Number.isFinite) || [];
- if (numbers.length > 0) {
- return [
- ...new Set(numbers)
- ].sort(
- (a, b) => a - b
- );
- }
- return startWeek
- ? [startWeek]
- : [];
- }
- // 清理字符串
- function cleanText(value) {
- if (value === null || value === undefined) {
- return "";
- }
- return String(value).replace(/\/+$/g, "").trim();
- }
- // 数字转换
- function toNumber(value) {
- const number = Number(value);
- return Number.isFinite(number) ? number : null;
- }
- // 主导入流程
- async function runImportFlow() {
- try {
- // 提示用户选择学期
- if (
- window.shiguangBridge &&
- typeof window.shiguangBridge.showToast ===
- "function"
- ) {
- window.shiguangBridge.showToast(
- "请选择要导入的学期..."
- );
- }
- const selectedSemester =
- await selectSemester();
- if (!selectedSemester) {
- if (
- window.shiguangBridge &&
- typeof window.shiguangBridge.showToast ===
- "function"
- ) {
- window.shiguangBridge.showToast(
- "已取消导入。"
- );
- }
- return;
- }
- // 获取课程
- const data =
- await fetchCourses();
- if (
- !data ||
- !Array.isArray(data.courses)
- ) {
- throw new Error(
- "课程数据为空或格式不正确。"
- );
- }
- // 检查 V2 Bridge
- if (
- !window.shiguangBridgePromise
- ) {
- throw new Error(
- "shiguangBridgePromise 不可用,请在时光课程表 App 内运行此适配器。"
- );
- }
- if (
- typeof window.shiguangBridgePromise
- .saveImportedCourses !==
- "function"
- ) {
- throw new Error(
- "saveImportedCourses API 不可用。"
- );
- }
- if (
- typeof window.shiguangBridgePromise
- .savePresetTimeSlots !==
- "function"
- ) {
- throw new Error(
- "savePresetTimeSlots API 不可用。"
- );
- }
- if (
- typeof window.shiguangBridgePromise
- .saveCourseConfig !==
- "function"
- ) {
- throw new Error(
- "saveCourseConfig API 不可用。"
- );
- }
- // 开始导入
- if (
- window.shiguangBridge &&
- typeof window.shiguangBridge.showToast ===
- "function"
- ) {
- window.shiguangBridge.showToast(
- `正在导入 ${data.courses.length} 条课程记录...`
- );
- }
- // 保存课程
- await window.shiguangBridgePromise
- .saveImportedCourses(
- JSON.stringify(
- data.courses
- )
- );
- // 保存时间段
- await window.shiguangBridgePromise
- .savePresetTimeSlots(
- JSON.stringify(
- data.timeSlots
- )
- );
- // 保存课表配置
- await window.shiguangBridgePromise
- .saveCourseConfig(
- JSON.stringify(
- data.config
- )
- );
- // 导入完成
- if (
- window.shiguangBridge &&
- typeof window.shiguangBridge.showToast ===
- "function"
- ) {
- window.shiguangBridge.showToast(
- `成功导入 ${data.courses.length} 条课程记录!`
- );
- }
- // 通知 App 导入流程结束
- if (
- window.shiguangBridge &&
- typeof window.shiguangBridge
- .notifyTaskCompletion ===
- "function"
- ) {
- window.shiguangBridge
- .notifyTaskCompletion();
- }
- } catch (error) {
- if (
- window.shiguangBridge &&
- typeof window.shiguangBridge.showToast ===
- "function"
- ) {
- window.shiguangBridge.showToast(
- `导入失败:${error.message}`
- );
- }
- }
- }
- // 启动
- runImportFlow();
|