Explorar o código

Merge pull request #502 from XingHeYuZhuan/pending

星河欲转 hai 3 semanas
pai
achega
df51b22c26

+ 9 - 1
index/root_index.yaml

@@ -748,4 +748,12 @@ schools:
     initial: "L"
     resource_folder: "USTL"
 
-  
+  - id: "LUT"
+    name: "兰州理工大学"
+    initial: "L"
+    resource_folder: "LUT"  
+
+  - id: "SYSU"
+    name: "中山大学"
+    initial: "Z"
+    resource_folder: "SYSU"

+ 128 - 0
resources/LUT/LUT.js

@@ -0,0 +1,128 @@
+// 兰州理工大学教务系统课程表适配
+
+const API_ROOT = "/jwapp/sys";
+const TERM_URL = `${API_ROOT}/wdkb/modules/jshkcb/dqxnxq.do`;
+const COURSE_URL = `${API_ROOT}/wdkb/modules/xskcb/cxxszhxqkb.do`;
+const LUT_TIME_SLOTS = [
+    { number: 1, startTime: "08:00", endTime: "08:50" },
+    { number: 2, startTime: "09:00", endTime: "09:50" },
+    { number: 3, startTime: "10:10", endTime: "11:00" },
+    { number: 4, startTime: "11:10", endTime: "12:00" },
+    { number: 5, startTime: "14:30", endTime: "15:20" },
+    { number: 6, startTime: "15:30", endTime: "16:20" },
+    { number: 7, startTime: "16:40", endTime: "17:30" },
+    { number: 8, startTime: "17:40", endTime: "18:30" },
+    { number: 9, startTime: "19:30", endTime: "20:20" },
+    { number: 10, startTime: "20:30", endTime: "21:20" }
+];
+
+function rowsOf(data, name) {
+    return data && data.datas && data.datas[name] && Array.isArray(data.datas[name].rows)
+        ? data.datas[name].rows : [];
+}
+
+async function requestJson(url, params) {
+    const options = { credentials: "include" };
+    if (params) {
+        options.method = "POST";
+        options.headers = { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" };
+        options.body = new URLSearchParams(params);
+    }
+    const response = await fetch(url, options);
+    if (!response.ok) throw new Error(`请求失败: ${response.status} ${url}`);
+    const data = await response.json();
+    if (data.code && data.code !== "0") throw new Error(`教务系统拒绝请求: ${url}`);
+    return data;
+}
+
+function parseWeeks(value, maxWeek) {
+    const text = String(value || "").replace(/[第周]/g, "").replace(/,/g, ",");
+    const numbers = text.match(/\d+/g) || [];
+    const odd = /单/.test(text);
+    const even = /双/.test(text);
+    const weeks = new Set();
+    const add = week => {
+        if (week > 0 && week <= maxWeek && (!odd && !even || odd && week % 2 === 1 || even && week % 2 === 0)) {
+            weeks.add(week);
+        }
+    };
+    if (!numbers.length || /全周|全部/.test(text)) {
+        for (let week = 1; week <= maxWeek; week++) add(week);
+    } else {
+        numbers.forEach(number => add(Number(number)));
+        const rangePattern = /(\d+)\s*[至到-]\s*(\d+)/g;
+        let match;
+        while ((match = rangePattern.exec(text))) {
+            for (let week = Number(match[1]); week <= Number(match[2]); week++) add(week);
+        }
+    }
+    return Array.from(weeks).sort((a, b) => a - b);
+}
+
+function parseTime(value) {
+    const match = String(value || "").match(/(\d{1,2}):(\d{2})/);
+    return match ? `${match[1].padStart(2, "0")}:${match[2]}` : null;
+}
+
+function getTimeSlots(rows) {
+    return rows.map((row, index) => ({
+        number: Number(row.DM || index + 1),
+        startTime: parseTime(row.KSSJ || row.START_TIME),
+        endTime: parseTime(row.JSSJ || row.END_TIME)
+    })).filter(slot => slot.startTime && slot.endTime).sort((a, b) => a.number - b.number);
+}
+
+async function loadLutSchedule() {
+    const termData = await requestJson(TERM_URL);
+    const term = rowsOf(termData, "dqxnxq")[0];
+    if (!term || !term.DM) throw new Error("未找到当前学期,请先登录教务系统");
+
+    const courseData = await requestJson(COURSE_URL, { XNXQDM: term.DM });
+    const courseRows = rowsOf(courseData, "cxxszhxqkb");
+    const timeSlots = LUT_TIME_SLOTS;
+    const courses = courseRows.flatMap(row => {
+        const day = Number(row.SKXQ);
+        const startSection = Number(row.KSJC);
+        const endSection = Number(row.JSJC);
+        const weeks = parseWeeks(row.ZCMC, 30);
+        if (!row.KCM || !day || !startSection || !endSection || !weeks.length) return [];
+        return [{
+            name: String(row.KCM).trim(),
+            teacher: String(row.SKJS || "").trim(),
+            position: String(row.JASMC || "").trim(),
+            day,
+            startSection,
+            endSection,
+            weeks
+        }];
+    });
+    return { term, courses, timeSlots };
+}
+
+async function importLutCourses() {
+    const result = await loadLutSchedule();
+    if (!result.courses.length) throw new Error("课表接口未返回可导入课程,请检查当前学期");
+
+    await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(result.courses));
+    if (result.timeSlots.length) {
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(result.timeSlots));
+    }
+    await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
+        semesterStartDate: null,
+        semesterTotalWeeks: Math.max(...result.courses.flatMap(course => course.weeks), 20),
+        defaultClassDuration: 45,
+        defaultBreakDuration: 10,
+        firstDayOfWeek: 1
+    }));
+    window.shiguangBridge.showToast(`兰州理工大学课表导入成功,共 ${result.courses.length} 条安排`);
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+(async function run() {
+    try {
+        await importLutCourses();
+    } catch (error) {
+        console.error("兰州理工大学课表导入失败:", error);
+        window.shiguangBridge.showToast(`课表导入失败: ${error.message}`);
+    }
+})();

+ 9 - 0
resources/LUT/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/LUT/adapters.yaml
+adapters:
+  - adapter_id: "LUT"
+    adapter_name: "兰州理工大学教务系统"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "LUT.js"
+    import_url: "https://jwxt.lut.edu.cn/"
+    maintainer: "Colton"
+    description: "兰州理工大学教务适配, 登录后点击【我的课表】,获取课程"

+ 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();