Browse Source

add:中山大学教务适配 (#501)

J-X-C-Z 3 weeks ago
parent
commit
54666a0559
3 changed files with 380 additions and 0 deletions
  1. 5 0
      index/root_index.yaml
  2. 8 0
      resources/SYSU/adapters.yaml
  3. 367 0
      resources/SYSU/sysu.js

+ 5 - 0
index/root_index.yaml

@@ -752,3 +752,8 @@ schools:
     name: "兰州理工大学"
     initial: "L"
     resource_folder: "LUT"  
+
+  - id: "SYSU"
+    name: "中山大学"
+    initial: "Z"
+    resource_folder: "SYSU"

+ 8 - 0
resources/SYSU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters: 
+  - adapter_id: "SYSU"
+    adapter_name: "中山大学" 
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "sysu.js" 
+    import_url: "https://jwxt.sysu.edu.cn/"
+    maintainer: "JXCZ"
+    description: "中山大学NetID版本教务系统课程表导入"

+ 367 - 0
resources/SYSU/sysu.js

@@ -0,0 +1,367 @@
+// 中山大学教务系统课表导入器
+// 功能:获取教务系统课表 -> 转换为目标 JSON -> 自动下载
+
+const API_URL = "/jwxt/timetable-search/stuTimeTabPrint/studentQuery";
+let ACAD_YEAR = "2026-1";
+
+const AVAILABLE_YEARS = [2026, 2027];
+const AVAILABLE_SEMESTERS = [1, 2];
+
+// 中山大学各学期实际开课日期。
+// 日期以中山大学官方校历为准;尚未公布的学期暂不填写,避免错误导入。
+const SEMESTER_START_DATES = {
+    "2025-1": "2025-09-08",
+    "2025-2": "2026-03-02",
+    "2026-1": "2026-09-07"
+};
+
+function validateAcademicYear(input) {
+    if (/^(2026|2027)$/.test(String(input).trim())) {
+        return false;
+    }
+    return "请输入 2026 或 2027。";
+}
+
+async function selectSemester() {
+    if (!window.AndroidBridgePromise || typeof window.AndroidBridgePromise.showPrompt !== "function") {
+        throw new Error("AndroidBridgePromise.showPrompt 不可用,请在时光课程表 App 内运行此适配器。");
+    }
+
+    const [defaultYear, defaultSemester] = ACAD_YEAR.split("-");
+
+    const yearInput = await window.AndroidBridgePromise.showPrompt(
+        "选择学年",
+        "请输入学年,例如:2026",
+        defaultYear || "2026",
+        "validateAcademicYear"
+    );
+
+    if (yearInput === null) {
+        return null;
+    }
+
+    const year = String(yearInput).trim();
+
+    if (typeof window.AndroidBridgePromise.showSingleSelection !== "function") {
+        throw new Error("AndroidBridgePromise.showSingleSelection 不可用,请在时光课程表 App 内运行此适配器。");
+    }
+
+    const semesters = ["1(第一学期)", "2(第二学期)"];
+    const defaultSemesterIndex = defaultSemester === "2" ? 1 : 0;
+
+    const semesterIndex = await window.AndroidBridgePromise.showSingleSelection(
+        "选择学期",
+        JSON.stringify(semesters),
+        defaultSemesterIndex
+    );
+
+    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
+};
+
+function getSemesterStartDate() {
+    const startDate = SEMESTER_START_DATES[ACAD_YEAR];
+
+    if (!startDate) {
+        throw new Error(`暂未配置 ${ACAD_YEAR} 的实际开课日期,请根据中山大学官方校历更新 SEMESTER_START_DATES。`);
+    }
+
+    return startDate;
+}
+
+function buildCourseConfig() {
+    return {
+        semesterStartDate: getSemesterStartDate(),
+        ...CONFIG_BASE
+    };
+}
+
+async function fetchCourses() {
+    console.log("========================================");
+    console.log("开始请求中山大学教务系统 API...");
+    console.log("API:", API_URL);
+    console.log("学期:", ACAD_YEAR);
+    console.log("========================================");
+
+    const response = await fetch(`${API_URL}?_t=${Date.now()}`, {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/json"
+        },
+        credentials: "include",
+        body: JSON.stringify({
+            acadYear: ACAD_YEAR,
+            submitFlag: "1",
+            nothroughCourseFlag: "1"
+        })
+    });
+
+    console.log("HTTP 状态码:", response.status);
+
+    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 (error) {
+        console.error("响应不是合法 JSON:", error);
+        console.error("原始响应:", responseText);
+        return [];
+    }
+
+    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 = [];
+
+    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 result = {
+        courses,
+        timeSlots: TIME_SLOTS,
+        config: buildCourseConfig()
+    };
+
+    console.log("========================================");
+    console.log(`课程转换完成,共 ${courses.length} 条记录`);
+    console.log("最终 JSON:");
+    console.log(result);
+    console.table(courses);
+    console.log("========================================");
+
+    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,
+        color: getCourseColor(name),
+        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;
+}
+
+// 根据课程名称生成稳定的颜色编号,避免同一课程每次导出颜色变化。
+function getCourseColor(name) {
+    const colorCount = 8;
+    let hash = 0;
+
+    for (let i = 0; i < name.length; i++) {
+        hash = ((hash << 5) - hash) + name.charCodeAt(i);
+        hash |= 0;
+    }
+
+    return Math.abs(hash) % colorCount + 1;
+}
+
+async function runImportFlow() {
+    try {
+        if (!window.AndroidBridgePromise || typeof window.AndroidBridgePromise.showAlert !== "function") {
+            throw new Error("AndroidBridgePromise.showAlert 不可用,请在时光课程表 App 内运行此适配器。");
+        }
+
+        if (!confirmed) {
+            if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
+                window.AndroidBridge.showToast("已取消导入。");
+            }
+            return;
+        }
+
+        const selectedSemester = await selectSemester();
+        if (!selectedSemester) {
+            if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
+                window.AndroidBridge.showToast("已取消导入。");
+            }
+            return;
+        }
+
+        console.log(`已选择学期:${ACAD_YEAR}`);
+        console.log(`学期实际开课日期:${getSemesterStartDate()}`);
+
+        const data = await fetchCourses();
+        if (!data || !Array.isArray(data.courses)) {
+            throw new Error("课程数据为空或格式不正确。");
+        }
+
+        window.__SYSU_COURSE_JSON__ = data;
+        window.__SYSU_COURSES__ = data.courses;
+
+        console.log("window.__SYSU_COURSE_JSON__ 已更新。");
+        console.log("准备通过 AndroidBridgePromise 向应用提交数据。");
+
+        if (!window.AndroidBridgePromise) {
+            throw new Error("AndroidBridgePromise 不可用,请在时光课程表 App 内运行此适配器。");
+        }
+
+        if (typeof window.AndroidBridgePromise.saveImportedCourses !== "function") {
+            throw new Error("saveImportedCourses API 不可用。");
+        }
+        if (typeof window.AndroidBridgePromise.savePresetTimeSlots !== "function") {
+            throw new Error("savePresetTimeSlots API 不可用。");
+        }
+        if (typeof window.AndroidBridgePromise.saveCourseConfig !== "function") {
+            throw new Error("saveCourseConfig API 不可用。");
+        }
+
+        if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
+            window.AndroidBridge.showToast(`正在导入 ${data.courses.length} 条课程记录...`);
+        }
+
+        await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(data.courses));
+        console.log("课程数据提交成功。");
+
+        await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(data.timeSlots));
+        console.log("时间段数据提交成功。");
+
+        await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(data.config));
+        console.log("课表配置提交成功。");
+
+        if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
+            window.AndroidBridge.showToast(`成功导入 ${data.courses.length} 条课程记录!`);
+        }
+
+        if (window.AndroidBridge && typeof window.AndroidBridge.notifyTaskCompletion === "function") {
+            window.AndroidBridge.notifyTaskCompletion();
+        }
+    } catch (error) {
+        console.error("========================================");
+        console.error("中山大学课表导入失败:", error);
+        console.error(error.stack);
+        console.error("========================================");
+
+        if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
+            window.AndroidBridge.showToast(`导入失败:${error.message}`);
+        }
+    }
+}
+
+runImportFlow();