Selaa lähdekoodia

feat: 添加西北工业大学(NWPU)本科教务系统适配 (#356)

* feat: 添加西北工业大学(NWPU)本科教务系统适配

* docs: 完善 NWPU 适配器描述

* fix: 更新 NWPU 适配器配置(import_url、maintainer、description)
zengzoxiong 1 kuukausi sitten
vanhempi
sitoutus
d8c0aa9700
3 muutettua tiedostoa jossa 442 lisäystä ja 3 poistoa
  1. 9 3
      index/root_index.yaml
  2. 8 0
      resources/NWPU/adapters.yaml
  3. 425 0
      resources/NWPU/nwpu_01.js

+ 9 - 3
index/root_index.yaml

@@ -382,6 +382,7 @@ schools:
     name: "江苏大学"
     initial: "J"
     resource_folder: "UJS"
+
   - id: "CQUET"
     name: "重庆电子科技职业大学"
     initial: "C"
@@ -512,6 +513,11 @@ schools:
     initial: "X"
     resource_folder: "XATU"
 
+  - id: "NWPU"
+    name: "西北工业大学"
+    initial: "X"
+    resource_folder: "NWPU"
+
   - id: "NJTECH"
     name: "南京工业大学"
     initial: "N"
@@ -541,7 +547,7 @@ schools:
     name: "浙江工业大学"
     initial: "Z"
     resource_folder: "ZJUT"
-  
+
   - id: "HFNU"
     name: "合肥师范学院"
     initial: "H"
@@ -560,12 +566,12 @@ schools:
   - id: "IMU"
     name: "内蒙古大学"
     initial: "N"
-    resource_folder: "IMU"    
+    resource_folder: "IMU"
 
   - id: "QQHRU"
     name: "齐齐哈尔大学"
     initial: "Q"
-    resource_folder: "QQHRU"  
+    resource_folder: "QQHRU"
 
   - id: "SICNU"
     name: "四川师范大学"

+ 8 - 0
resources/NWPU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "NWPU_01"
+    adapter_name: "西北工业大学"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "nwpu_01.js"
+    import_url: "https://ecampus.nwpu.edu.cn/"
+    maintainer: "zengzoxiong"
+    description: "西北工业大学本科教务系统适配,请先登录翱翔门户,手动点击进入翱翔教务后执行脚本自动获取课程信息"

+ 425 - 0
resources/NWPU/nwpu_01.js

@@ -0,0 +1,425 @@
+// 西北工业大学(NWPU) 拾光课程表适配脚本
+// 适配系统:EAMS 教务系统(jwxt.nwpu.edu.cn)
+// 适配范围:本科(BACHELOR_AND_ASSOCIATE)
+// 维护者:用户7227
+
+/**
+ * 将 HHmm 整数格式转换为 HH:mm 字符串
+ * @param {number} hhmm - 如 830, 915, 1400
+ * @returns {string} - 如 "08:30", "09:15", "14:00"
+ */
+function formatTime(hhmm) {
+    var h = String(Math.floor(hhmm / 100));
+    var m = String(hhmm % 100);
+    if (h.length < 2) h = '0' + h;
+    if (m.length < 2) m = '0' + m;
+    return h + ':' + m;
+}
+
+/**
+ * 发起网络请求
+ * @param {string} url - 请求地址
+ * @returns {Promise<string>} - 响应文本
+ */
+async function request(url) {
+    var res = await fetch(url, { credentials: "include" });
+    if (!res.ok) throw new Error("网络请求失败: " + res.status);
+    return await res.text();
+}
+
+/**
+ * 获取学生 ID
+ * 调用 student-portrait API 获取当前登录学生的数据库 ID
+ * @returns {Promise<number|null>} - 学生 ID
+ */
+async function getStudentId() {
+    console.log("[NWPU] 正在获取学生 ID...");
+    try {
+        var data = await request("/student/for-std/student-portrait/getStdInfo?bizTypeAssoc=2&cultivateTypeAssoc=1");
+        var json = JSON.parse(data);
+        if (json && json.student && json.student.id) {
+            console.log("[NWPU] 学生 ID: " + json.student.id);
+            return json.student.id;
+        }
+        console.error("[NWPU] API 响应中未找到 student.id");
+        return null;
+    } catch (e) {
+        console.error("[NWPU] 获取学生 ID 失败: " + e.message);
+        return null;
+    }
+}
+
+/**
+ * 获取学期列表
+ * 请求课表页面 HTML,解析 #allSemesters 下拉框
+ * @returns {Promise<Array<{id: number, name: string}>>} - 学期列表
+ */
+async function getSemesters() {
+    console.log("[NWPU] 正在获取学期列表...");
+    try {
+        var html = await request("/student/for-std/course-table");
+        var parser = new DOMParser();
+        var doc = parser.parseFromString(html, "text/html");
+        var select = doc.querySelector("#allSemesters");
+        if (!select) {
+            console.error("[NWPU] 未找到 #allSemesters 元素");
+            return [];
+        }
+        var options = select.querySelectorAll("option");
+        var semesters = [];
+        for (var i = 0; i < options.length; i++) {
+            var opt = options[i];
+            var id = parseInt(opt.getAttribute("value"));
+            var name = opt.textContent.trim();
+            if (!isNaN(id) && name) {
+                semesters.push({ id: id, name: name });
+            }
+        }
+        console.log("[NWPU] 获取到 " + semesters.length + " 个学期");
+        return semesters;
+    } catch (e) {
+        console.error("[NWPU] 获取学期列表失败: " + e.message);
+        return [];
+    }
+}
+
+/**
+ * 获取课程数据
+ * 调用 print-data API 获取指定学期的课程和时间表信息
+ * @param {number} semesterId - 学期 ID
+ * @param {number} studentId - 学生 ID
+ * @returns {Promise<object|null>} - API 响应 JSON
+ */
+async function getCourseData(semesterId, studentId) {
+    console.log("[NWPU] 正在获取课程数据...");
+    AndroidBridge.showToast("正在同步课表...");
+    try {
+        var url = "/student/for-std/course-table/semester/" + semesterId + "/print-data/" + studentId;
+        var data = await request(url);
+        return JSON.parse(data);
+    } catch (e) {
+        console.error("[NWPU] 获取课程数据失败: " + e.message);
+        AndroidBridge.showToast("获取课程数据失败: " + e.message);
+        return null;
+    }
+}
+
+/**
+ * NWPU 默认作息时间(长安校区)
+ * 当 API 响应中不包含 timeTableLayout 时使用
+ */
+function getDefaultTimeSlots() {
+    return [
+        { number: 1,  startTime: "08:30", endTime: "09:15" },
+        { number: 2,  startTime: "09:25", endTime: "10:10" },
+        { number: 3,  startTime: "10:30", endTime: "11:15" },
+        { number: 4,  startTime: "11:25", endTime: "12:10" },
+        { number: 5,  startTime: "12:20", endTime: "13:05" },
+        { number: 6,  startTime: "13:05", endTime: "13:50" },
+        { number: 7,  startTime: "14:00", endTime: "14:45" },
+        { number: 8,  startTime: "14:55", endTime: "15:40" },
+        { number: 9,  startTime: "16:00", endTime: "16:45" },
+        { number: 10, startTime: "16:55", endTime: "17:40" },
+        { number: 11, startTime: "19:00", endTime: "19:45" },
+        { number: 12, startTime: "19:55", endTime: "20:40" },
+        { number: 13, startTime: "20:40", endTime: "21:25" }
+    ];
+}
+
+/**
+ * 从 API 响应中提取时间段
+ * 优先从 timeTableLayout.courseUnitList 动态获取,否则使用默认时间段
+ * @param {object} apiData - API 响应
+ * @returns {Array<{number: number, startTime: string, endTime: string}>}
+ */
+function extractTimeSlots(apiData) {
+    var layout = apiData.timeTableLayout;
+    if (layout && layout.courseUnitList) {
+        var slots = [];
+        for (var i = 0; i < layout.courseUnitList.length; i++) {
+            var unit = layout.courseUnitList[i];
+            slots.push({
+                number: unit.indexNo,
+                startTime: formatTime(unit.startTime),
+                endTime: formatTime(unit.endTime)
+            });
+        }
+        console.log("[NWPU] 从 API 提取到 " + slots.length + " 个时间段");
+        return slots;
+    }
+    console.log("[NWPU] API 未返回时间段,使用默认作息时间");
+    return getDefaultTimeSlots();
+}
+
+/**
+ * 获取学期信息(开始日期、结束日期)
+ * 调用 ws/semester/get API
+ * @param {number} semesterId - 学期 ID
+ * @returns {Promise<{startDate: string|null, endDate: string|null}>}
+ */
+async function getSemesterInfo(semesterId) {
+    try {
+        var data = await request("/student/ws/semester/get/" + semesterId);
+        var json = JSON.parse(data);
+        return {
+            startDate: json.startDate || null,
+            endDate: json.endDate || null
+        };
+    } catch (e) {
+        console.warn("[NWPU] 获取学期信息失败: " + e.message);
+        return { startDate: null, endDate: null };
+    }
+}
+
+/**
+ * 计算两个日期之间的周数
+ * @param {string} startDate - YYYY-MM-DD
+ * @param {string} endDate - YYYY-MM-DD
+ * @returns {number}
+ */
+function calculateTotalWeeks(startDate, endDate) {
+    if (!startDate || !endDate) return 20;
+    var start = new Date(startDate);
+    var end = new Date(endDate);
+    var diffMs = end.getTime() - start.getTime();
+    var diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
+    return Math.ceil(diffDays / 7);
+}
+
+/**
+ * 从 API 响应和学期信息中提取学期配置
+ * @param {object} semesterInfo - 学期信息 {startDate, endDate}
+ * @param {Array} activities - 课程活动列表
+ * @returns {object} - CourseConfigJsonModel
+ */
+function extractCourseConfig(semesterInfo, activities) {
+    var semesterStartDate = semesterInfo.startDate;
+    var semesterTotalWeeks = 20;
+
+    // 优先从学期日期计算总周数
+    if (semesterInfo.startDate && semesterInfo.endDate) {
+        semesterTotalWeeks = calculateTotalWeeks(semesterInfo.startDate, semesterInfo.endDate);
+    } else if (activities.length > 0) {
+        // 回退:从 weekIndexes 推算
+        var maxWeek = 0;
+        for (var i = 0; i < activities.length; i++) {
+            var weeks = activities[i].weekIndexes;
+            if (weeks && weeks.length > 0) {
+                for (var j = 0; j < weeks.length; j++) {
+                    if (weeks[j] > maxWeek) maxWeek = weeks[j];
+                }
+            }
+        }
+        if (maxWeek > 0) semesterTotalWeeks = maxWeek;
+    }
+
+    console.log("[NWPU] 学期开始日期: " + (semesterStartDate || "未知") + ", 总周数: " + semesterTotalWeeks);
+    return {
+        semesterStartDate: semesterStartDate,
+        semesterTotalWeeks: semesterTotalWeeks,
+        defaultClassDuration: 45,
+        defaultBreakDuration: 10,
+        firstDayOfWeek: 1
+    };
+}
+
+/**
+ * 将 API 活动数据转换为 ImportCourseJsonModel 格式
+ * @param {Array} activities - API activities 数组
+ * @returns {Array<object>} - 课程列表
+ */
+function convertCourses(activities) {
+    var courses = [];
+    for (var i = 0; i < activities.length; i++) {
+        var act = activities[i];
+
+        // 拼接教室信息:优先 room,辅以 building
+        var position = act.room || act.building || "未知地点";
+
+        // 取第一个教师
+        var teacher = "未知教师";
+        if (act.teachers && act.teachers.length > 0) {
+            teacher = act.teachers[0];
+        }
+
+        // 排序周次
+        var weeks = (act.weekIndexes || []).slice().sort(function(a, b) { return a - b; });
+
+        if (act.courseName && weeks.length > 0) {
+            courses.push({
+                name: act.courseName,
+                teacher: teacher,
+                position: position,
+                day: act.weekday,
+                startSection: act.startUnit,
+                endSection: act.endUnit,
+                weeks: weeks
+            });
+        }
+    }
+    console.log("[NWPU] 转换完成,共 " + courses.length + " 个课程条目");
+    return courses;
+}
+
+/**
+ * 第一步:保存时间段
+ */
+async function saveTimeSlots(timeSlots) {
+    if (timeSlots.length === 0) {
+        console.warn("[NWPU] 无时间段数据,跳过");
+        return true;
+    }
+    try {
+        console.log("[NWPU] 正在保存时间段...");
+        await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+        AndroidBridge.showToast("成功导入 " + timeSlots.length + " 个时间段");
+        return true;
+    } catch (e) {
+        console.error("[NWPU] 保存时间段失败: " + e.message);
+        AndroidBridge.showToast("保存时间段失败: " + e.message);
+        return false;
+    }
+}
+
+/**
+ * 第二步:保存课表配置
+ */
+async function saveConfig(config) {
+    try {
+        console.log("[NWPU] 正在保存课表配置...");
+        await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
+        AndroidBridge.showToast("课表配置导入成功");
+        return true;
+    } catch (e) {
+        console.error("[NWPU] 保存配置失败: " + e.message);
+        AndroidBridge.showToast("保存配置失败: " + e.message);
+        return false;
+    }
+}
+
+/**
+ * 第三步:保存课程数据
+ */
+async function saveCourses(courses) {
+    if (courses.length === 0) {
+        AndroidBridge.showToast("没有课程数据需要导入");
+        return true;
+    }
+    try {
+        console.log("[NWPU] 正在保存课程数据...");
+        await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        AndroidBridge.showToast("成功导入 " + courses.length + " 个课程条目");
+        return true;
+    } catch (e) {
+        console.error("[NWPU] 保存课程失败: " + e.message);
+        AndroidBridge.showToast("保存课程失败: " + e.message);
+        return false;
+    }
+}
+
+/**
+ * 主导入流程
+ */
+async function runImportFlow() {
+    try {
+        // 1. 显示导入说明
+        var confirmed = await window.AndroidBridgePromise.showAlert(
+            "西北工业大学课表导入",
+            "导入前请确保您已在浏览器中登录教务系统(jwxt.nwpu.edu.cn)。\n\n" +
+            "本适配将自动获取学期信息、时间段和课程数据。",
+            "开始导入"
+        );
+        if (!confirmed) {
+            AndroidBridge.showToast("导入已取消");
+            return;
+        }
+
+        // 2. 获取学生 ID
+        AndroidBridge.showToast("正在获取学生信息...");
+        var studentId = await getStudentId();
+        if (!studentId) {
+            await window.AndroidBridgePromise.showAlert(
+                "导入失败",
+                "未能获取学生 ID,请确认您已登录教务系统。",
+                "确定"
+            );
+            return;
+        }
+
+        // 3. 获取学期列表
+        AndroidBridge.showToast("正在获取学期列表...");
+        var semesters = await getSemesters();
+        if (semesters.length === 0) {
+            await window.AndroidBridgePromise.showAlert(
+                "导入失败",
+                "未能获取学期列表,请刷新页面后重试。",
+                "确定"
+            );
+            return;
+        }
+
+        // 4. 用户选择学期
+        var semesterNames = [];
+        for (var i = 0; i < semesters.length; i++) {
+            semesterNames.push(semesters[i].name);
+        }
+        var selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
+            "选择学期",
+            JSON.stringify(semesterNames),
+            0
+        );
+        if (selectedIndex === null || selectedIndex < 0 || selectedIndex >= semesters.length) {
+            AndroidBridge.showToast("导入已取消");
+            return;
+        }
+        var selectedSemester = semesters[selectedIndex];
+        console.log("[NWPU] 选择学期: " + selectedSemester.name + " (ID: " + selectedSemester.id + ")");
+
+        // 5. 获取课程数据
+        var apiData = await getCourseData(selectedSemester.id, studentId);
+        if (!apiData) return;
+
+        var tableVm = apiData.studentTableVm;
+        if (!tableVm || !tableVm.activities || tableVm.activities.length === 0) {
+            AndroidBridge.showToast("该学期没有课程数据");
+            return;
+        }
+        var activities = tableVm.activities;
+        console.log("[NWPU] 获取到 " + activities.length + " 条课程活动");
+
+        // 6. 获取学期信息(开始日期、结束日期)
+        AndroidBridge.showToast("正在获取学期信息...");
+        var semesterInfo = await getSemesterInfo(selectedSemester.id);
+        console.log("[NWPU] 学期开始: " + (semesterInfo.startDate || "未知") + ", 结束: " + (semesterInfo.endDate || "未知"));
+
+        // 7. 提取时间段
+        var timeSlots = extractTimeSlots(apiData);
+
+        // 8. 提取课表配置
+        var courseConfig = extractCourseConfig(semesterInfo, activities);
+
+        // 9. 转换课程数据
+        var courses = convertCourses(activities);
+
+        // 10. 三步导入:时间段 → 配置 → 课程
+        var timeSlotResult = await saveTimeSlots(timeSlots);
+        if (!timeSlotResult) return;
+
+        var configResult = await saveConfig(courseConfig);
+        if (!configResult) return;
+
+        var courseResult = await saveCourses(courses);
+        if (!courseResult) return;
+
+        // 完成
+        AndroidBridge.showToast("课表导入完成!");
+        AndroidBridge.notifyTaskCompletion();
+
+    } catch (e) {
+        console.error("[NWPU] 导入异常: " + e.message);
+        AndroidBridge.showToast("导入异常: " + e.message);
+    }
+}
+
+// 启动导入流程
+runImportFlow();