Просмотр исходного кода

Merge pull request #575 from XingHeYuZhuan/pending

星河欲转 2 недель назад
Родитель
Сommit
daaa3508b9
4 измененных файлов с 457 добавлено и 36 удалено
  1. 5 0
      index/root_index.yaml
  2. 163 36
      resources/CZIMT/czimt.js
  3. 8 0
      resources/XJTU/adapters.yaml
  4. 281 0
      resources/XJTU/xjtu.js

+ 5 - 0
index/root_index.yaml

@@ -882,3 +882,8 @@ schools:
     name: "南昌科技职业大学"
     initial: "N"
     resource_folder: "THDM"
+
+  - id: "XJTU"
+    name: "西安交通大学"
+    initial: "X"
+    resource_folder: "XJTU"

+ 163 - 36
resources/CZIMT/czimt.js

@@ -103,11 +103,6 @@ function parseJsonData(jsonData) {
     return finalCourseList;
 }
 
-function validateYearInput(input) {
-    if (/^[0-9]{4}$/.test(input)) return false;
-    return "请输入四位数字的学年!";
-}
-
 async function promptUserToStart() {
     return await window.shiguangBridgePromise.showAlert(
         "常机电学生课表导入",
@@ -116,34 +111,167 @@ async function promptUserToStart() {
     );
 }
 
-async function getAcademicYear() {
-    const currentYear = new Date().getFullYear().toString();
-    return await window.shiguangBridgePromise.showPrompt(
+/**
+ * 从教务系统获取学年学期选项
+ * 学年:以选中项为中心,取前2年+后2年,共5个选项
+ */
+async function fetchAcademicOptions() {
+    const targetUrls = [
+        "https://webapp.czimt.edu.cn/http/77726476706e69737468656265737421fae042d2242a65557d468aa2/kbcx/xskbcx_cxXskbcxIndex.html?vpn-12-o1-jwc.czmec.cn&gnmkdm=N2151",
+        "http://jwc.czmec.cn/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151"
+    ];
+
+    for (const url of targetUrls) {
+        try {
+            const response = await fetch(url, {
+                method: "GET",
+                credentials: "include"
+            });
+
+            if (!response.ok) continue;
+
+            const htmlText = await response.text();
+            const parser = new DOMParser();
+            const doc = parser.parseFromString(htmlText, "text/html");
+
+            const allYearOptions = Array.from(doc.querySelectorAll("#xnm option"))
+                .filter(opt => opt.value !== "")
+                .map(opt => ({
+                    value: opt.value,
+                    text: opt.textContent.trim(),
+                    selected: opt.selected
+                }));
+
+            const semesterOptions = Array.from(doc.querySelectorAll("#xqm option"))
+                .filter(opt => opt.value !== "")
+                .map(opt => ({
+                    value: opt.value,
+                    text: opt.textContent.trim(),
+                    selected: opt.selected
+                }));
+
+            if (allYearOptions.length === 0 || semesterOptions.length === 0) continue;
+
+            const selectedIndex = allYearOptions.findIndex(opt => opt.selected);
+
+            if (selectedIndex === -1) {
+                return {
+                    yearOptions: allYearOptions.slice(0, 5),
+                    semesterOptions,
+                    defaultYearIndex: 0,
+                    defaultSemesterIndex: Math.max(0, semesterOptions.findIndex(opt => opt.selected))
+                };
+            }
+
+            const start = Math.max(0, selectedIndex - 2);
+            const end = Math.min(allYearOptions.length, selectedIndex + 3);
+            const defaultSemesterIndex = semesterOptions.findIndex(opt => opt.selected);
+
+            return {
+                yearOptions: allYearOptions.slice(start, end),
+                semesterOptions,
+                defaultYearIndex: selectedIndex - start,
+                defaultSemesterIndex: defaultSemesterIndex !== -1 ? defaultSemesterIndex : 0
+            };
+        } catch (e) {
+            console.error(`JS: 获取学年学期选项失败: ${url}`);
+        }
+    }
+    return null;
+}
+
+/**
+ * 提示用户选择学年和学期
+ */
+async function selectAcademicYearAndSemester() {
+    const optionsData = await fetchAcademicOptions();
+
+    if (!optionsData) {
+        window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
+        return null;
+    }
+
+    const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
+
+    const yearTexts = yearOptions.map(item => item.text);
+    const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
         "选择学年",
-        "请输入要导入课程的起始学年(例如 2026-2027 应输入 2026):",
-        currentYear,
-        "validateYearInput"
+        JSON.stringify(yearTexts),
+        defaultYearIndex
     );
-}
 
-async function selectSemester() {
-    const semesters = ["第一学期", "第二学期", "第三学期(短学期)"];
+    if (yearIndex === null || yearIndex === -1) return null;
+    const selectedYearCode = yearOptions[yearIndex].value;
+
+    const semesterTexts = semesterOptions.map(item => item.text);
     const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
         "选择学期",
-        JSON.stringify(semesters),
-        0
+        JSON.stringify(semesterTexts),
+        defaultSemesterIndex
     );
-    return semesterIndex;
+
+    if (semesterIndex === null || semesterIndex === -1) return null;
+    const selectedSemesterCode = semesterOptions[semesterIndex].value;
+
+    return {
+        academicYear: selectedYearCode,
+        semesterCode: selectedSemesterCode
+    };
 }
 
-function getSemesterCode(semesterIndex) {
-    const codes = ["3", "12", "16"];
-    return codes[semesterIndex] || "3";
+/**
+ * 获取学期开学日期
+ * 优先找第1周的记录,从 rq / zcrq 字段中提取日期
+ */
+async function fetchSemesterStartDate(academicYear, semesterCode) {
+    const targetUrls = [
+        "https://webapp.czimt.edu.cn/http/77726476706e69737468656265737421fae042d2242a65557d468aa2/kbcx/xskbcxZccx_cxZcByXnxq.html?vpn-12-o1-jwc.czmec.cn&gnmkdm=N2154",
+        "http://jwc.czmec.cn/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154"
+    ];
+    const requestBody = `xnm=${academicYear}&xqm=${semesterCode}`;
+
+    for (const url of targetUrls) {
+        try {
+            const response = await fetch(url, {
+                method: "POST",
+                headers: {
+                    "accept": "application/json, text/javascript, */*; q=0.01",
+                    "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
+                    "x-requested-with": "XMLHttpRequest"
+                },
+                body: requestBody,
+                credentials: "include"
+            });
+
+            if (!response.ok) continue;
+
+            const json = await response.json();
+            if (!Array.isArray(json) || json.length === 0) continue;
+
+            // 优先找第1周,否则取第一项
+            const firstWeekObj = json.find(item => String(item.zs) === "1" || String(item.zsmc) === "1") || json[0];
+
+            // rq 是该周每天的日期列表(逗号或斜杠分隔),取第一个即周一
+            if (firstWeekObj.rq) {
+                const match = String(firstWeekObj.rq).match(/(\d{4}-\d{2}-\d{2})/);
+                if (match) return match[1];
+            }
+
+            if (firstWeekObj.zcrq) {
+                const match = String(firstWeekObj.zcrq).match(/(\d{4}-\d{2}-\d{2})/);
+                if (match) return match[1];
+            }
+        } catch (e) {
+            console.error(`JS: 获取开学日期失败: ${url}`);
+        }
+    }
+    return null;
 }
 
-async function fetchAndParseCourses(academicYear, semesterIndex) {
-    const semesterCode = getSemesterCode(semesterIndex);
+async function fetchAndParseCourses(academicYear, semesterCode) {
     const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=&kclxdm=`;
+    // 与课程请求并行获取开学日期
+    const semesterStartDatePromise = fetchSemesterStartDate(academicYear, semesterCode);
 
     const targetUrls = [
         "https://webapp.czimt.edu.cn/http/77726476706e69737468656265737421fae042d2242a65557d468aa2/kbcx/xskbcx_cxXsgrkb.html?vpn-12-o1-jwc.czmec.cn&gnmkdm=N2151",
@@ -170,7 +298,7 @@ async function fetchAndParseCourses(academicYear, semesterIndex) {
                         return {
                             courses: parsedCourses,
                             config: {
-                                semesterStartDate: null,
+                                semesterStartDate: await semesterStartDatePromise,
                                 semesterTotalWeeks: 20
                             }
                         };
@@ -194,7 +322,7 @@ async function saveCourses(parsedCourses) {
         return true;
     } catch (error) {
         window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
-        console.error('JS: Save Courses Error:', error);A
+        console.error('JS: Save Courses Error:', error);
         return false;
     }
 }
@@ -240,21 +368,16 @@ async function runImportFlow() {
         return;
     }
 
-    const academicYear = await getAcademicYear();
-    if (academicYear === null) {
-        window.shiguangBridge.showToast("导入已取消。");
+    const selection = await selectAcademicYearAndSemester();
+    if (!selection) {
+        window.shiguangBridge.showToast("未选择学年学期,导入流程终止。");
         return;
     }
-    console.log(`JS: 已选择学年: ${academicYear}`);
 
-    const semesterIndex = await selectSemester();
-    if (semesterIndex === null || semesterIndex === -1) {
-        window.shiguangBridge.showToast("导入已取消。");
-        return;
-    }
-    console.log(`JS: 已选择学期索引: ${semesterIndex}`);
+    const { academicYear, semesterCode } = selection;
+    console.log(`JS: 已选择学年: ${academicYear}, 学期代码: ${semesterCode}`);
 
-    const result = await fetchAndParseCourses(academicYear, semesterIndex);
+    const result = await fetchAndParseCourses(academicYear, semesterCode);
     if (result === null) {
         console.log("JS: 课程获取或解析失败,流程终止。");
         return;
@@ -276,7 +399,11 @@ async function runImportFlow() {
 
     try {
         await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
-        window.shiguangBridge.showToast(`课表配置更新成功!总周数:${config.semesterTotalWeeks}周。`);
+        let configMsg = `课表配置更新成功!总周数:${config.semesterTotalWeeks}周。`;
+        if (config.semesterStartDate) {
+            configMsg += ` 开学日期:${config.semesterStartDate}`;
+        }
+        window.shiguangBridge.showToast(configMsg);
     } catch (error) {
         window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
     }

+ 8 - 0
resources/XJTU/adapters.yaml

@@ -0,0 +1,8 @@
+adapters:
+  - adapter_id: "XJTU_01"
+    adapter_name: "西安交通大学-本科教务"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "xjtu.js"
+    import_url: "https://ehall.xjtu.edu.cn"
+    maintainer: "SDPD"
+    description: "适配西安交通大学教务系统(wdkb)本科课表导入"

+ 281 - 0
resources/XJTU/xjtu.js

@@ -0,0 +1,281 @@
+/**
+ * 西安交通大学教务系统课表适配脚本 (API版)
+ * 适配系统: e-hall 教务系统 (wdkb)
+ * API端点: xskcb.do (课程数据), dqxnxq.do (当前学期)
+ */
+
+const BASE_URL = 'https://ehall.xjtu.edu.cn/jwapp/sys/wdkb/modules';
+
+// ==================== 周次解析 ====================
+
+/**
+ * 将 SKZC 二进制位串解析为周次数组
+ * 输入: "000001" → [6], "00000000000000001111" → [1,2,3,4]
+ * SKZC 是二进制字符串,从左到右依次表示第1-20周,'1'表示上课
+ */
+function parseWeeksFromBinary(skzc) {
+    if (!skzc) return [];
+    const weeks = [];
+    for (let i = 0; i < skzc.length; i++) {
+        if (skzc[i] === '1') {
+            weeks.push(i + 1);
+        }
+    }
+    return weeks;
+}
+
+// ==================== 学期选择 ====================
+
+/**
+ * 生成学期选项列表
+ * 格式: ["2025-2026 秋季(1)", "2025-2026 春季(2)", "2025-2026 暑假(4)"]
+ */
+function generateSemesterOptions(currentYear) {
+    const options = [];
+    const semesterNames = { '1': '秋季', '2': '春季', '4': '暑假' };
+    const semesterCodes = ['1', '2', '4'];
+
+    // 从当前学年前2年到后1年
+    const startYear = parseInt(currentYear.split('-')[0], 10) - 1;
+    const endYear = parseInt(currentYear.split('-')[0], 10) + 1;
+
+    for (let y = endYear; y >= startYear; y--) {
+        const yearCode = `${y}-${y + 1}`;
+        for (const semCode of semesterCodes) {
+            const xnxqdm = `${yearCode}-${semCode}`;
+            const displayName = `${yearCode}学年 ${semesterNames[semCode]}`;
+            options.push({ xnxqdm, displayName });
+        }
+    }
+    return options;
+}
+
+/**
+ * 获取当前学期信息
+ */
+async function fetchCurrentSemester() {
+    try {
+        const response = await fetch(`${BASE_URL}/jshkcb/dqxnxq.do`, {
+            method: 'POST',
+            headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
+            credentials: 'include'
+        });
+        if (!response.ok) return null;
+        const data = await response.json();
+        const row = data?.datas?.dqxnxq?.rows?.[0];
+        return row ? row.DM : null;
+    } catch (e) {
+        console.warn('获取当前学期失败:', e);
+        return null;
+    }
+}
+
+/**
+ * 让用户选择学年学期
+ */
+async function selectSemester() {
+    // 获取当前学期
+    const currentSemester = await fetchCurrentSemester();
+    if (!currentSemester) {
+        shiguangBridge.showToast('获取学期信息失败,请确保已登录');
+        return null;
+    }
+
+    // 从当前学期提取学年
+    const parts = currentSemester.split('-');
+    const currentYear = `${parts[0]}-${parts[1]}`;
+
+    // 生成选项
+    const options = generateSemesterOptions(currentYear);
+    const displayNames = options.map(o => o.displayName);
+
+    // 找到当前学期的默认索引
+    const defaultIndex = options.findIndex(o => o.xnxqdm === currentSemester);
+    const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+        '选择学期',
+        JSON.stringify(displayNames),
+        defaultIndex >= 0 ? defaultIndex : 0
+    );
+
+    if (selectedIndex === null || selectedIndex < 0) return null;
+    return options[selectedIndex].xnxqdm;
+}
+
+// ==================== 课程数据获取与解析 ====================
+
+/**
+ * 从API获取课程数据
+ */
+async function fetchCourses(xnxqdm) {
+    const response = await fetch(`${BASE_URL}/xskcb/xskcb.do`, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
+        body: `XNXQDM=${xnxqdm}`,
+        credentials: 'include'
+    });
+    if (!response.ok) throw new Error('请求课程数据失败');
+    const data = await response.json();
+    if (data.code !== '0') throw new Error('课程数据返回异常');
+    return data?.datas?.xskcb?.rows || [];
+}
+
+/**
+ * 按课程代码分组,合并同课程不同天的记录
+ * API返回的每条记录是一门课在某一天的信息
+ * 需要将同一KCH的记录合并,将多天信息聚合到一起
+ */
+function groupCoursesByCode(rows) {
+    const grouped = {};
+    for (const row of rows) {
+        const key = `${row.KCH}_${row.KSJC}_${row.JASMC}`;
+        if (!grouped[key]) {
+            grouped[key] = {
+                name: row.KCM || '',
+                teacher: row.SKJS || '',
+                position: row.JASMC || '',
+                startSection: parseInt(row.KSJC, 10),
+                endSection: parseInt(row.JSJC, 10),
+                days: [],
+                weeks: []
+            };
+        }
+        const day = parseInt(row.SKXQ, 10);
+        if (day >= 1 && day <= 7 && !grouped[key].days.includes(day)) {
+            grouped[key].days.push(day);
+        }
+        const weeks = parseWeeksFromBinary(row.SKZC);
+        grouped[key].weeks = [...new Set([...grouped[key].weeks, ...weeks])].sort((a, b) => a - b);
+    }
+    return Object.values(grouped);
+}
+
+/**
+ * 将分组后的课程数据展开为拾光课程表格式
+ * 每个课程 × 每天 生成一条独立记录
+ */
+function buildCourseList(groupedCourses) {
+    const courses = [];
+    for (const g of groupedCourses) {
+        if (!g.name || g.days.length === 0 || g.weeks.length === 0) continue;
+        for (const day of g.days) {
+            courses.push({
+                name: g.name,
+                teacher: g.teacher,
+                position: g.position,
+                day: day,
+                startSection: g.startSection,
+                endSection: g.endSection,
+                weeks: g.weeks
+            });
+        }
+    }
+    return courses;
+}
+
+// ==================== 时间段配置 ====================
+
+// 每节课50分钟,相邻节间休息10分钟,每两节之间休息20分钟
+// 早上8:00,下午夏令时14:30/冬令时14:00,晚上夏令时19:40/冬令时19:10
+const TIME_SLOTS_SUMMER = [
+    { "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:40", "endTime": "20:30" },
+    { "number": 10, "startTime": "20:40", "endTime": "21:30" },
+    { "number": 11, "startTime": "21:40", "endTime": "22:30" }
+];
+
+const TIME_SLOTS_WINTER = [
+    { "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:00", "endTime": "14:50" },
+    { "number": 6,  "startTime": "15:00", "endTime": "15:50" },
+    { "number": 7,  "startTime": "16:10", "endTime": "17:00" },
+    { "number": 8,  "startTime": "17:10", "endTime": "18:00" },
+    { "number": 9,  "startTime": "19:10", "endTime": "20:00" },
+    { "number": 10, "startTime": "20:10", "endTime": "21:00" },
+    { "number": 11, "startTime": "21:10", "endTime": "22:00" }
+];
+
+async function importTimeSlots() {
+    try {
+        const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
+            '选择作息时间',
+            JSON.stringify(['夏令时 (下午14:30 晚上19:40)', '冬令时 (下午14:00 晚上19:10)']),
+            0
+        );
+        if (selectedIndex === null) return;
+        const timeSlots = selectedIndex === 0 ? TIME_SLOTS_SUMMER : TIME_SLOTS_WINTER;
+        await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
+    } catch (e) {
+        console.warn('时间段导入失败:', e);
+    }
+}
+
+// ==================== 主流程 ====================
+
+async function importXJTUCourses() {
+    try {
+        // 1. 提示用户确认已登录
+        const confirmed = await window.shiguangBridgePromise.showAlert(
+            '课表导入',
+            '导入前请确保已在浏览器中成功登录西安交通大学教务系统',
+            '好的,开始导入'
+        );
+        if (!confirmed) {
+            shiguangBridge.showToast('用户取消了导入');
+            return;
+        }
+
+        // 2. 选择学年学期
+        const xnxqdm = await selectSemester();
+        if (!xnxqdm) {
+            shiguangBridge.showToast('未选择学期,导入终止');
+            return;
+        }
+
+        // 3. 获取课程数据
+        shiguangBridge.showToast('正在获取课程数据...');
+        const rows = await fetchCourses(xnxqdm);
+        if (rows.length === 0) {
+            shiguangBridge.showToast('该学期暂无课程数据');
+            return;
+        }
+
+        // 4. 解析并构建课程列表
+        const grouped = groupCoursesByCode(rows);
+        const courses = buildCourseList(grouped);
+        if (courses.length === 0) {
+            shiguangBridge.showToast('课程数据解析失败');
+            return;
+        }
+
+        // 5. 导入课程
+        const result = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        if (result !== true) {
+            shiguangBridge.showToast('课程导入失败,请重试');
+            return;
+        }
+
+        // 6. 导入时间段配置
+        await importTimeSlots();
+
+        // 7. 完成
+        shiguangBridge.showToast(`成功导入 ${courses.length} 门课程`);
+        shiguangBridge.notifyTaskCompletion();
+
+    } catch (error) {
+        console.error('XJTU导入错误:', error);
+        shiguangBridge.showToast('导入出错: ' + error.message);
+    }
+}
+
+// 启动导入
+importXJTUCourses();