Quellcode durchsuchen

Merge pull request #566 from XingHeYuZhuan/pending

星河欲转 vor 3 Wochen
Ursprung
Commit
168a9a4a4f
5 geänderte Dateien mit 626 neuen und 0 gelöschten Zeilen
  1. 10 0
      index/root_index.yaml
  2. 9 0
      resources/GDUT/adapters.yaml
  3. 362 0
      resources/GDUT/gdut.js
  4. 11 0
      resources/ZZU/adapters.yaml
  5. 234 0
      resources/ZZU/zzu.js

+ 10 - 0
index/root_index.yaml

@@ -218,6 +218,11 @@ schools:
     initial: "G"
     resource_folder: "GDPU"
 
+  - id: "GDUT"
+    name: "广东工业大学"
+    initial: "G"
+    resource_folder: "GDUT"
+
   - id: "HNUST"
     name: "湖南科技大学"
     initial: "H"
@@ -812,4 +817,9 @@ schools:
     name: "西南大学"
     initial: "X"
     resource_folder: "SWU"
+
+  - id: "ZZU"
+    name: "郑州大学"
+    initial: "Z"
+    resource_folder: "ZZU"
     

+ 9 - 0
resources/GDUT/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/GDUT/adapters.yaml
+adapters:
+  - adapter_id: "GDUT_01"
+    adapter_name: "广东工业大学教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "gdut.js"
+    import_url: "https://authserver.gdut.edu.cn/authserver/login?service=https%3A%2F%2Fjxfw.gdut.edu.cn%2Fnew%2FssoLogin"
+    maintainer: "forer4433"
+    description: "广东工业大学教务系统课程导入适配。本校开发者适配,如有问题请提交issues。"

+ 362 - 0
resources/GDUT/gdut.js

@@ -0,0 +1,362 @@
+// 文件: gdut.js
+
+if (typeof Strings === 'undefined') {
+    var Strings = {
+        BASE_URL: "https://jxfw.gdut.edu.cn",
+        GET_WEEK_COURSES_URL: "https://jxfw.gdut.edu.cn/xsgrkbcx!getKbRq.action",
+        GET_ALL_COURSES_URL: "https://jxfw.gdut.edu.cn/xsgrkbcx!getDataList.action",
+        GET_ALL_COURSES_HTML_URL: "https://jxfw.gdut.edu.cn/xsgrkbcx!xsAllKbList.action",
+        GET_ALL_COURSES_HTML_URL_REFERRER: "https://jxfw.gdut.edu.cn/xsgrkbcx!getXsgrbkList.action"
+    };
+}
+
+async function stepDescriptionAlert() {
+    try {
+        const confirmed = await window.shiguangBridgePromise.showAlert(
+            "提示",
+            "即将执行导入课程操作。请确保你已处于登录状态,无需打开课程表页面。",
+            "确认"
+        );
+        
+        return confirmed;
+    } catch (error) {
+        console.error("显示弹窗时发生错误:", error);
+        return false;
+    }
+}
+
+async function selectSemesterSelection(){
+    const now = new Date();
+    const currentYear = now.getFullYear();
+    const currentMonth = now.getMonth() + 1;
+    const currentSemester = currentMonth >= 9 || currentMonth <= 2 ? 1 : 1;
+    const nextSemester = currentSemester === 1 ? 2 : 1;
+
+    const presetSemeters = [];
+    const presetSemestersName = [];
+
+    for (let year = currentYear; year >= currentYear - 3; year--){
+        for (let semester = nextSemester; semester >= 1; semester--){
+            const semesterName = `${year}-${year + 1}学年 ${semester === 1 ? "秋季" : "春季"}(第${semester}学期)`;
+            presetSemeters.push({ name: semesterName, semesterId: `${year}0${semester}` });
+            presetSemestersName.push(semesterName);
+        }
+    }
+
+    try {
+        const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+            "选择要导入的学期",
+            JSON.stringify(presetSemestersName),
+            2
+        );
+        if (selectedIndex !== null && selectedIndex >= 0 && selectedIndex < presetSemeters.length) {
+            const selecedSemester = presetSemeters[selectedIndex];
+            console.log("用户选择了: " + selecedSemester.name + " (索引: " + selectedIndex + ")");
+            return selecedSemester;
+        } else {
+            console.log("用户取消了选择。");
+            return null;
+        }
+    } catch (error) {
+        console.error("显示单选列表弹窗时发生错误:", error);
+        window.shiguangBridge.showToast("Single Selection:显示列表出错!" + error.message);
+        return null;
+    }
+}
+
+function extractFirstDay(dateInfoJsonData) {
+    try {
+        const jsonArray = JSON.parse(dateInfoJsonData);
+
+        const dateInfoArray = jsonArray[1];
+        if (!Array.isArray(dateInfoArray)) {
+            console.error('JSON 数据格式异常:索引 [1] 不是数组');
+            return null;
+        }
+
+        // 遍历查找 xqmc === "1"(周一)的项
+        for (const dateInfo of dateInfoArray) {
+            if (dateInfo.xqmc === "1" && dateInfo.rq) {
+                return dateInfo.rq;
+            }
+        }
+
+        console.warn('未找到 xqmc=1 的日期项');
+        return null;
+    } catch (error) {
+        console.error('解析 JSON 失败:', error);
+        return null;
+    }
+}
+
+async function fetchStartDate(semesterId) {
+    const url = `${"https://jxfw.gdut.edu.cn"}/xsgrkbcx!getKbRq.action?xnxqdm=${semesterId}&zc=1`;
+    
+    try {
+
+        console.log(`正在获取学期开始日期。学期代码:${semesterId}}`);
+        const response = await fetch(url, {
+            method: 'GET',
+            headers: {
+                'Referer': url
+            },
+            credentials: 'include'
+        });
+
+        const data = await response.text();
+        
+        const startDateString = extractFirstDay(data);
+
+        // 如果提取失败,返回当前日期
+        if (startDateString === null) {
+            // 使用当前日期
+            return new Date();
+        }
+
+        // 解析日期字符串为 Date 对象
+        const date = new Date(startDateString);
+        if (isNaN(date.getTime())) {
+            console.warn(`日期解析失败: ${startDateString},使用当前日期`);
+            return new Date();
+        }
+
+        console.log(`成功获取学期开始日期: ${date.toISOString().split('T')[0]}`);
+
+        return date;
+    } catch (error) {
+        console.error('获取学期开始日期失败,使用当前日期。错误信息:', error);
+        return new Date();
+    }
+}
+
+async function fetchCourses(semesterId){
+    try {
+        console.log(`正在获取学期 ${semesterId} 的课程数据...`);
+
+        const rawCourses = [];
+
+        const pageSize = 100;
+        let pageIndex = 1;
+
+        while (true) {
+            const formData = new URLSearchParams();
+            formData.append('xnxqdm', semesterId);
+            formData.append('zc', '');
+            formData.append('page', String(pageIndex));
+            formData.append('rows', String(pageSize));
+            formData.append('sort', 'kxh');
+            formData.append('order', 'asc');
+
+            const response = await fetch(Strings.GET_ALL_COURSES_URL, {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/x-www-form-urlencoded',
+                    'Referer': Strings.BASE_URL
+                },
+                body: formData.toString(),
+                credentials: 'include'
+            });
+
+            if (!response.ok) {
+                throw new Error(`请求失败: ${response.status}`);
+            }
+
+            const rawData = await response.json();
+
+            if (rawData.total === 0 || rawData.rows.length === 0) {
+                console.log(`学期 ${semesterId} 没有找到课程数据。`);
+                if (await checkSemesterIsOpened(semesterId)) {
+                    throw new Error('该学期没有找到课程,请确认选择了正确的学期');
+                }
+                throw new Error('学期未开放课表查询!');
+            }
+
+            for (const row of rawData.rows) {
+                rawCourses.push(row);
+            }
+
+            if (rawCourses.length >= rawData.total) {
+                break;
+            }
+
+            pageIndex++;
+
+            await new Promise(resolve => setTimeout(resolve, 100));
+        }
+
+        const courses = parseCourses(rawCourses);
+
+        return courses;
+
+    } catch (error) {
+        console.error('添加课程表失败:', error);
+        return null;
+    }
+}
+
+async function checkSemesterIsOpened(semesterId) {
+    console.log(`正在检查学期 ${semesterId} 是否已开放课表查询...`);
+
+    const url = `${Strings.GET_ALL_COURSES_HTML_URL}?xnxqdm=${semesterId}`;
+    
+    const response = await fetch(url, {
+        method: 'GET',
+        headers: {
+            'Referer': Strings.GET_ALL_COURSES_HTML_URL_REFERRER
+        },
+        credentials: 'include'
+    });
+    
+    const html = await response.text();
+
+    // 如果不包含"未开放"文字,说明已开放
+    return !html.includes("本学期课表还未开放,请稍后查询!");
+}
+
+function parseCourses(rawCourses){
+    console.log(`正在解析课程数据,共 ${rawCourses.length} 条原始记录...`);
+
+    const courses = [];
+    for (const raw of rawCourses) {
+        const sectionMatch = raw.jcdm.match(/\d{2}/g);
+        if (!sectionMatch) continue;
+        
+        const sections = sectionMatch.map(Number);
+        const startSection = sections[0];
+        const endSection = sections[sections.length - 1];
+        
+        // 周次
+        const week = Number(raw.zc);
+        if (isNaN(week)) continue;
+
+        courses.push({
+            name: raw.kcmc.trim(),
+            teacher: (raw.teaxms || "").trim(),
+            position: (raw.jxcdmc || "").trim(),
+            day: Number(raw.xq),
+            startSection: startSection,
+            endSection: endSection,
+            weeks: [week],
+            isCustomTime: false
+        });
+    }
+
+    return courses;
+}
+
+async function saveCourses(courses){
+    try {
+        console.log("正在尝试导入课程...");
+        const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        if (result === true) {
+            console.log("课程导入成功!");
+        } else {
+            console.log("课程导入未成功,结果:" + result);
+            window.shiguangBridge.showToast("课程导入失败,请查看日志。");
+        }
+    } catch (error) {
+        console.error("导入课程时发生错误:", error);
+        window.shiguangBridge.showToast("导入课程失败: " + error.message);
+    }
+}
+
+async function setPresetTimeSlots() {
+    const presetTimeSlots = [
+        { "number": 1, "startTime": "08:30", "endTime": "09:15" },
+        { "number": 2, "startTime": "09:20", "endTime": "10:05" },
+        { "number": 3, "startTime": "10:25", "endTime": "11:10" },
+        { "number": 4, "startTime": "11:15", "endTime": "12:00" },
+        { "number": 5, "startTime": "13:50", "endTime": "14:35" },
+        { "number": 6, "startTime": "14:40", "endTime": "15:25" },
+        { "number": 7, "startTime": "15:30", "endTime": "16:15" },
+        { "number": 8, "startTime": "16:30", "endTime": "17:15" },
+        { "number": 9, "startTime": "17:20", "endTime": "18:05" },
+        { "number": 10, "startTime": "18:30", "endTime": "19:15" },
+        { "number": 11, "startTime": "19:20", "endTime": "20:05" },
+        { "number": 12, "startTime": "20:10", "endTime": "20:55" },
+        { "number": 13, "startTime": "21:00", "endTime": "21:45" },
+        { "number": 14, "startTime": "21:50", "endTime": "22:35" }
+    ];
+
+    try {
+        console.log("正在尝试导入预设时间段...");
+        const result = await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
+        if (result === true) {
+            console.log("预设时间段导入成功!");
+        } else {
+            console.log("预设时间段导入未成功,结果:" + result);
+            window.shiguangBridge.showToast("测试时间段导入失败,请查看日志。");
+        }
+    } catch (error) {
+        console.error("导入时间段时发生错误:", error);
+        window.shiguangBridge.showToast("导入时间段失败: " + error.message);
+    }
+}
+
+async function saveConfig(config) {
+    try {
+        console.log("正在尝试导入课表配置...");
+        const configJsonString = JSON.stringify(config);
+
+        const result = await window.shiguangBridgePromise.saveCourseConfig(configJsonString);
+
+        if (result === true) {
+            console.log("课表配置导入成功!");
+        } else {
+            console.log("课表配置导入未成功,结果:" + result);
+            window.shiguangBridge.showToast("测试配置导入失败,请查看日志。");
+        }
+    } catch (error) {
+        console.error("导入配置时发生错误:", error);
+        window.shiguangBridge.showToast("导入配置失败: " + error.message);
+    }
+}
+
+/**
+ * 编排这些异步操作,并在用户取消时停止后续执行。
+ */
+async function runImportFlow() {
+    var result = await stepDescriptionAlert();
+
+    if (!result) {
+        console.log("用户取消了操作,停止后续执行。");
+        return; // 用户取消,立即退出函数
+    }
+
+    const semester = await selectSemesterSelection();
+
+    if (!semester) {
+        console.log("用户取消了学期选择,停止后续执行。");
+        return; // 用户取消,立即退出函数
+    }
+
+    const startDate = await fetchStartDate(semester.semesterId);
+
+    const config = {
+        semesterStartDate: startDate.toISOString().split('T')[0], // 转换为 YYYY-MM-DD 格式
+        semesterTotalWeeks: 20,
+        defaultClassDuration: 45,
+        defaultBreakDuration: 5,
+        firstDayOfWeek: 1
+    }
+
+    await saveConfig(config);
+
+    const courses = await fetchCourses(semester.semesterId);
+
+    if (!courses) {
+        console.log(`未能获取课程数据,停止后续执行。`);
+        return; // 获取课程失败,立即退出函数
+    }
+
+    await saveCourses(courses);
+    await setPresetTimeSlots();
+
+    window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
+
+    // 发送最终的生命周期完成信号
+    window.shiguangBridge.notifyTaskCompletion();
+}
+
+// 入口函数,开始执行导入流程
+runImportFlow();

+ 11 - 0
resources/ZZU/adapters.yaml

@@ -0,0 +1,11 @@
+# resources/ZZU/adapters.yaml
+# 郑州大学 拾光课程表适配
+adapters:
+  - adapter_id: "ZZU_01"
+    adapter_name: "郑州大学"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "zzu.js"
+    import_url: "https://cas.s.zzu.edu.cn/cas/a/login?service=https%3A%2F%2Fjwxt.zzu.edu.cn%2Fstudent%2Fsso%2Flogin"
+    maintainer: "LilyCarry"
+    description: "登录之后向右下角滑,点击课表.课表出现后点击右下角导入即可."
+

+ 234 - 0
resources/ZZU/zzu.js

@@ -0,0 +1,234 @@
+// 郑州大学 拾光课程表适配脚本
+// 适配系统:树维新一代智慧教务系统(jwxt.zzu.edu.cn)
+
+(async function () {
+    // 郑州大学标准 12 节作息时间(第 1-4 节上午,第 5-8 节下午,第 9-12 节晚上)
+    const presetTimeSlots = [
+        { 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:00", endTime: "14:45" },
+        { number: 6, startTime: "14:55", endTime: "15:40" },
+        { number: 7, startTime: "16:10", endTime: "16:55" },
+        { number: 8, startTime: "17:05", endTime: "17:50" },
+        { number: 9, startTime: "19:00", endTime: "19:45" },
+        { number: 10, startTime: "19:55", endTime: "20:40" },
+        { number: 11, startTime: "20:50", endTime: "21:35" },
+        { number: 12, startTime: "21:40", endTime: "22:25" }
+    ];
+
+    function showToast(message) {
+        if (window.shiguangBridge && window.shiguangBridge.showToast) {
+            window.shiguangBridge.showToast(message);
+        }
+    }
+
+    /**
+     * 获取学期下拉列表
+     */
+    async function fetchSemesters() {
+        try {
+            const res = await fetch("/student/for-std/course-table", {
+                headers: {
+                    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+                    "x-requested-with": "XMLHttpRequest"
+                },
+                method: "GET",
+                credentials: "include"
+            });
+            if (!res.ok) return null;
+
+            const htmlText = await res.text();
+            const parser = new DOMParser();
+            const doc = parser.parseFromString(htmlText, "text/html");
+            const select = doc.getElementById("allSemesters");
+            if (!select) return null;
+
+            const options = Array.from(select.querySelectorAll("option")).map(opt => ({
+                label: opt.textContent.trim(),
+                value: opt.value.trim(),
+                selected: opt.hasAttribute("selected") || opt.selected
+            })).filter(o => o.value && o.label);
+
+            return options;
+        } catch (e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取学期开学日期与结束日期
+     */
+    async function fetchSemesterMetadata(semesterId) {
+        try {
+            const res = await fetch(`/student/ws/semester/get/${semesterId}`, {
+                headers: {
+                    "accept": "*/*",
+                    "x-requested-with": "XMLHttpRequest"
+                },
+                method: "GET",
+                credentials: "include"
+            });
+            if (!res.ok) return null;
+            const data = await res.json();
+            return {
+                startDate: data.startDate || null,
+                endDate: data.endDate || null
+            };
+        } catch (e) {
+            return null;
+        }
+    }
+
+    /**
+     * 获取并解析课程表数据
+     */
+    async function fetchAndParseCourses(semesterId) {
+        try {
+            const url = `/student/for-std/course-table/semester/${semesterId}/print-data?semesterId=${semesterId}&hasExperiment=true`;
+            const res = await fetch(url, {
+                headers: {
+                    "accept": "*/*",
+                    "x-requested-with": "XMLHttpRequest"
+                },
+                method: "GET",
+                credentials: "include"
+            });
+
+            if (!res.ok) return null;
+            const data = await res.json();
+            if (!data) return null;
+
+            const rawActivities = (data.studentTableVms && data.studentTableVms[0] ? data.studentTableVms[0].activities : (data.studentTableVm ? data.studentTableVm.activities : (data.activities || []))) || [];
+            if (!Array.isArray(rawActivities) || rawActivities.length === 0) return null;
+
+            const parsedCourses = [];
+            for (const act of rawActivities) {
+                if (!act.courseName || !act.weekday || !act.startUnit || !act.endUnit || !Array.isArray(act.weekIndexes)) {
+                    continue;
+                }
+
+                const teacherName = Array.isArray(act.teachers) && act.teachers.length > 0
+                    ? act.teachers.map(t => String(t).replace(/\(\d+\)/g, "").replace(/\[\d+\]/g, "").trim()).filter(Boolean).join(",")
+                    : (typeof act.teachers === "string" ? act.teachers.replace(/\(\d+\)/g, "").trim() : "");
+
+                const weeks = act.weekIndexes.map(Number).filter(w => Number.isInteger(w) && w > 0).sort((a, b) => a - b);
+                if (weeks.length === 0) continue;
+
+                const startSection = Number(act.startUnit);
+                const endSection = Number(act.endUnit);
+                const sections = [];
+                for (let s = startSection; s <= endSection; s++) sections.push(s);
+
+                parsedCourses.push({
+                    name: String(act.courseName).trim(),
+                    teacher: teacherName,
+                    position: String(act.room || act.building || "未知地点").trim(),
+                    day: Number(act.weekday),
+                    startSection: startSection,
+                    endSection: endSection,
+                    sections: sections,
+                    weeks: weeks
+                });
+            }
+
+            return parsedCourses.length > 0 ? parsedCourses : null;
+        } catch (e) {
+            return null;
+        }
+    }
+
+    /**
+     * 计算学期总周数
+     */
+    function calculateTotalWeeks(startDate, endDate) {
+        if (!startDate || !endDate) return 20;
+        const start = new Date(startDate);
+        const end = new Date(endDate);
+        const diffMs = end.getTime() - start.getTime();
+        if (diffMs <= 0) return 20;
+        const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
+        return Math.min(Math.max(Math.ceil(diffDays / 7), 16), 30);
+    }
+
+    /**
+     * 流程主入口
+     */
+    async function runImportFlow() {
+        showToast("正在拉取学期列表...");
+
+        const semesters = await fetchSemesters();
+        if (!semesters || semesters.length === 0) {
+            showToast("获取学期列表失败,请确认已进入教务课表页面");
+            return;
+        }
+
+        const labels = semesters.map(s => s.label);
+        let defaultIndex = semesters.findIndex(s => s.selected);
+        if (defaultIndex < 0) defaultIndex = 0;
+
+        const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+            "选择学期",
+            JSON.stringify(labels),
+            defaultIndex
+        );
+
+        if (selectedIndex === null || selectedIndex < 0 || selectedIndex >= semesters.length) {
+            showToast("操作已取消");
+            return;
+        }
+
+        const selectedSemester = semesters[selectedIndex];
+        showToast("正在拉取课表数据...");
+
+        const [meta, courses] = await Promise.all([
+            fetchSemesterMetadata(selectedSemester.value),
+            fetchAndParseCourses(selectedSemester.value)
+        ]);
+
+        if (!courses || courses.length === 0) {
+            showToast("未查询到有效课程数据");
+            return;
+        }
+
+        let totalWeeks = 20;
+        if (meta && meta.startDate && meta.endDate) {
+            totalWeeks = calculateTotalWeeks(meta.startDate, meta.endDate);
+        } else {
+            const allWeeks = courses.flatMap(c => c.weeks || []);
+            if (allWeeks.length > 0) totalWeeks = Math.max(...allWeeks);
+        }
+
+        const configData = {
+            semesterStartDate: meta && meta.startDate ? meta.startDate : "",
+            semesterTotalWeeks: Math.max(totalWeeks, 18),
+            firstDayOfWeek: 1,
+            defaultClassDuration: 45,
+            defaultBreakDuration: 10
+        };
+
+        if (window.shiguangBridgePromise && window.shiguangBridgePromise.saveCourseConfig) {
+            await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(configData));
+        }
+
+        if (window.shiguangBridgePromise && window.shiguangBridgePromise.savePresetTimeSlots) {
+            await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
+        }
+
+        if (window.shiguangBridgePromise && window.shiguangBridgePromise.saveImportedCourses) {
+            const saveOk = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+            if (!saveOk) {
+                showToast("课程保存失败");
+                return;
+            }
+        }
+
+        showToast(`成功导入 ${courses.length} 门课程及作息时间!`);
+        if (window.shiguangBridge && window.shiguangBridge.notifyTaskCompletion) {
+            window.shiguangBridge.notifyTaskCompletion();
+        }
+    }
+
+    runImportFlow();
+})();