洛初 4 недель назад
Родитель
Сommit
9a8a3cff42
2 измененных файлов с 295 добавлено и 42 удалено
  1. 1 1
      resources/UJS/adapters.yaml
  2. 294 41
      resources/UJS/ujs_zhengfang_v9.0.js

+ 1 - 1
resources/UJS/adapters.yaml

@@ -5,4 +5,4 @@ adapters: #固定字段
     asset_js_path: "ujs_zhengfang_v9.0.js"
     import_url: "https://jwxt.ujs.edu.cn/sso/jziotlogin"
     maintainer: "洛初 Github@gongfuture"
-    description: "江苏大学教务系统适配,仅支持个人课表的导入,通过统一认证登录"
+    description: "江苏大学教务系统适配,仅支持个人课表的导入,通过统一认证登录  ver.2"

+ 294 - 41
resources/UJS/ujs_zhengfang_v9.0.js

@@ -5,6 +5,19 @@
 // 基于GSMC修改
 // 作者:洛初 Github@gongfuture
 
+// 2026.03.30 第一版
+// 通过正方接口 xskbcx_cxXsgrkb 拉取个人课表,解析课程名、教师、教室、星期、节次和周次(含单双周)。
+// 交互上依次询问学年、学期与作息类型(夏令时/冬令时/智能选择,智能选择按固定切令时日期判定)。
+// 按课程所在楼栋匹配上午与下午作息,写入课程的自定义时间;导入课程、课表配置与预设时间段。
+
+// 2026.08.23 第二版
+// 桥接接口升级至 v2(window.shiguangBridge / window.shiguangBridgePromise)。
+// 补充开学日期:从首页日历区块取当前学期起止日期写入 semesterStartDate,总周数改为按实际周次取值;
+// 取不到开学日期时跳过配置保存,避免覆盖用户已有设置。
+// 课程备注补充重修标记、选课备注(体育项目、微专业等)与周次原文。
+// 集中实践课(军训、毕业设计等)无星期节次无法排课,改为弹窗提示手动添加。
+// 教师与教室为空不再丢弃课程;修正京江各号楼的楼栋匹配;支持校外 WebVPN 访问。
+
 /**
  * 解析周次字符串,处理单双周和周次范围。
  */
@@ -50,6 +63,62 @@ function parseWeeks(weekStr) {
     return [...new Set(weeks)].sort((a, b) => a - b);
 }
 
+/**
+ * 拼接教务系统接口地址。
+ * 校内直连时 location.origin 就是教务域名,前缀为空;
+ * 校外经 WebVPN 访问时路径带有 /http/<hex> 前缀,必须保留,否则会变成跨域请求。
+ */
+function buildApiUrl(path) {
+    const prefixMatch = window.location.pathname.match(/^\/http\/[0-9a-f]+/i);
+    const prefix = prefixMatch ? prefixMatch[0] : "";
+    return window.location.origin + prefix + path;
+}
+
+/**
+ * 拼装课程备注。
+ * xm 只有姓名,kcmc 只有课程名,以下信息只存在于原始字段里,
+ * 放进备注方便用户核对:重修标记、选课备注(体育项目、微专业等)、周次原文。
+ */
+function buildCourseRemark(rawCourse) {
+    const parts = [];
+
+    const retakeFlag = String(rawCourse.cxbjmc || "").trim();
+    if (retakeFlag) {
+        parts.push(retakeFlag);
+    }
+
+    const selectionNote = String(rawCourse.xkbz || "").trim();
+    if (selectionNote) {
+        parts.push(selectionNote);
+    }
+
+    const weekDesc = String(rawCourse.zcd || "").trim();
+    if (weekDesc) {
+        parts.push(weekDesc);
+    }
+
+    return parts.join(" | ");
+}
+
+/**
+ * 解析集中实践课列表(sjkList)。
+ * 这类课程(军事技能训练、形势与政策等)只有课程名、教师和起止周,
+ * 没有星期和节次,无法映射到周课表,只能提示用户手动添加。
+ */
+function parsePracticeCourses(jsonData) {
+    if (!jsonData || !Array.isArray(jsonData.sjkList)) {
+        return [];
+    }
+
+    return jsonData.sjkList
+        .map((item) => ({
+            name: String(item.kcmc || "").trim(),
+            teacher: String(item.jsxm || "").trim(),
+            weekDesc: String(item.qsjsz || "").trim()
+        }))
+        .filter((item) => item.name);
+}
+
 /**
  * 解析 API 返回的 JSON 数据。
  */
@@ -66,9 +135,10 @@ function parseJsonData(jsonData) {
     const finalCourseList = [];
 
     for (const rawCourse of rawCourseList) {
-        // 关键字段检查: kcmc(课名), xm(教师), cdmc(教室), xqj(星期), jcs(节次范围), zcd(周次描述)
-        if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
-            !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
+        // 关键字段检查:只有 kcmc(课名), xqj(星期), jcs(节次范围), zcd(周次描述) 是排课必需的。
+        // xm(教师) 与 cdmc(教室) 在实践课、线上课、未排地点的课程上可能为空,
+        // 缺这两项不影响排课,不能因此丢弃整门课程。
+        if (!rawCourse.kcmc || !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
             continue;
         }
 
@@ -92,15 +162,23 @@ function parseJsonData(jsonData) {
             continue;
         }
 
-        finalCourseList.push({
-            name: rawCourse.kcmc.trim(),
-            teacher: rawCourse.xm.trim(),
-            position: rawCourse.cdmc.trim(),
+        const remark = buildCourseRemark(rawCourse);
+
+        const course = {
+            name: String(rawCourse.kcmc).trim(),
+            teacher: String(rawCourse.xm || "").trim(),
+            position: String(rawCourse.cdmc || "").trim(),
             day: day,
             startSection: startSection,
             endSection: endSection,
             weeks: weeksArray
-        });
+        };
+
+        if (remark) {
+            course.remark = remark;
+        }
+
+        finalCourseList.push(course);
     }
 
     finalCourseList.sort((a, b) =>
@@ -177,7 +255,7 @@ async function whetherSummerTimeSlot() {
 
     // } catch (error) {
     //     console.error('JS: 获取作息时间公告失败:', error);
-    //     AndroidBridge.showToast("无法获取作息时间公告,智能选择回退到预设时间。");
+    //     window.shiguangBridge.showToast("无法获取作息时间公告,智能选择回退到预设时间。");
 
     //     // 预设日期
     //     const summerStart = new Date(new Date().getFullYear(), 3, 7); // 4月7日
@@ -192,10 +270,12 @@ async function whetherSummerTimeSlot() {
     // }
 
     // CORS 问题导致无法获取公告页,智能选择回退到预设时间。
+    // 冬令时是十一假期结束后调整,取 10 月 7 日;夏令时公告历年均为 4 月 7 日起执行。
+    // 参考教务处历年作息时间表公告:https://jwc.ujs.edu.cn/index/xl_zuo_xi_shi_jian.htm
 
     // 预设日期
     const summerStart = new Date(new Date().getFullYear(), 3, 7); // 4月7日
-    const winterStart = new Date(new Date().getFullYear(), 9, 8); // 10月8
+    const winterStart = new Date(new Date().getFullYear(), 9, 7); // 10月7
 
     const now = new Date();
     if (now >= summerStart && now < winterStart) {
@@ -205,16 +285,40 @@ async function whetherSummerTimeSlot() {
     }
 }
 
+/**
+ * 计算本次导入的作息何时失效。
+ * 作息时间是导入时一次性写死的,不会自动跟随切令时变化,
+ * 所以这里返回下一个「切到另一种令时」的日期,用于提示用户届时重新导入。
+ * 夏令时 4 月 7 日起执行,冬令时十一假期后(10 月 7 日)起执行。
+ */
+function getNextTimeSlotSwitchDate(isSummerTime) {
+    const now = new Date();
+    const year = now.getFullYear();
+    const targetLabel = isSummerTime ? "冬令时" : "夏令时";
+
+    const candidates = [
+        { date: new Date(year, 3, 7), label: "夏令时" },
+        { date: new Date(year, 9, 7), label: "冬令时" },
+        { date: new Date(year + 1, 3, 7), label: "夏令时" },
+        { date: new Date(year + 1, 9, 7), label: "冬令时" }
+    ];
+
+    const next = candidates.find((item) => item.date > now && item.label === targetLabel);
+
+    return {
+        label: next.label,
+        text: `${next.date.getFullYear()}年${next.date.getMonth() + 1}月${next.date.getDate()}日`
+    };
+}
+
 /**
  * 检查是否在登录页面。
- * 只有当 URL 精确匹配时,才返回 true。
+ * 校内直连时地址是 http://jwxt.ujs.edu.cn/sso/jziotlogin,
+ * 校外经 WebVPN 时地址是 https://webvpn.ujs.edu.cn/http/<hex>/sso/jziotlogin,
+ * 因此按路径结尾判断,两种入口都能识别。
  */
 function isLoginPage() {
-    const url = window.location.href;
-    const loginUrl = "http://jwxt.ujs.edu.cn/sso/jziotlogin";
-
-    // 如果当前 URL 与指定的登录 URL 完全一致,则返回 true (是登录页)
-    return url === loginUrl;
+    return window.location.pathname.endsWith("/sso/jziotlogin");
 }
 
 
@@ -278,7 +382,7 @@ async function selectTimeSlot() {
 
 async function reselectTimeSlot(selectedTimeSlot) {
     const options = ["对的对的,就是这个", "不对不对,应该是另外一个"];
-    const dialogTitle = "当前智能选择结果为: " + (selectedTimeSlot ? "夏令时" : "冬令时") + "是否更改选择?";
+    const dialogTitle = "当前智能选择结果为: \n  " + (selectedTimeSlot ? "夏令时" : "冬令时") + "\n是否更改选择?";
     const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
         dialogTitle,
         JSON.stringify(options),
@@ -302,6 +406,86 @@ function getSemesterCode(semesterIndex) {
 }
 
 
+/**
+ * 获取教务系统当前学期的起止日期。
+ * 首页日历区块的标题形如 "2026-2027学年1学期(2026-08-31至2027-02-21)",
+ * 其中起始日期就是第 1 周周一,正是 semesterStartDate 需要的值。
+ * 注意:该接口忽略 xnm/xqm 参数,只返回当前学期,
+ * 因此只有用户选择的学年学期与返回值一致时才能使用。
+ */
+async function fetchCurrentSemesterRange() {
+    const url = buildApiUrl("/xtgl/index_cxAreaFive.html?localeKey=zh_CN&gnmkdm=index");
+
+    try {
+        const response = await fetch(url, {
+            "headers": {
+                "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
+            },
+            "body": "",
+            "method": "POST",
+            "credentials": "include"
+        });
+
+        if (!response.ok) {
+            throw new Error(`状态码 ${response.status}`);
+        }
+
+        const html = await response.text();
+        const match = html.match(/(\d{4})-\d{4}学年(\d)学期\s*[((](\d{4}-\d{2}-\d{2})至(\d{4}-\d{2}-\d{2})[))]/);
+
+        if (!match) {
+            console.warn("JS: 未能从日历区块解析出学期起止日期。");
+            return null;
+        }
+
+        const range = {
+            academicYear: match[1],
+            semesterIndex: Number(match[2]) - 1,
+            startDate: match[3],
+            endDate: match[4]
+        };
+        console.log("JS: 教务系统当前学期:", range);
+        return range;
+
+    } catch (error) {
+        console.warn("JS: 获取学期起止日期失败:", error);
+        return null;
+    }
+}
+
+/**
+ * 计算课表配置。
+ *
+ * 应用侧的 saveCourseConfig 是整体覆盖而非字段级合并:没有传入的字段会被写成模型默认值,
+ * 其中 semesterStartDate 的默认值是 null,会把用户已经设置好的开学日期清空。
+ * 所以拿不到真实开学日期时返回 null,由调用方跳过整个配置保存,宁可不写也不要写坏。
+ *
+ * defaultClassDuration / defaultBreakDuration 不显式传入,会被重置为应用默认的 45 / 10 分钟,
+ * 与江大「45 分钟一节、课间 10 分钟」一致,因此没有副作用。
+ */
+function buildCourseConfig(courses, semesterRange, firstDayOfWeek) {
+    if (!semesterRange) {
+        return null;
+    }
+
+    let maxWeek = 0;
+    for (const course of courses) {
+        for (const week of course.weeks) {
+            if (week > maxWeek) {
+                maxWeek = week;
+            }
+        }
+    }
+
+    return {
+        semesterStartDate: semesterRange.startDate,
+        // 只增不减:默认 20 周,课表里出现更大的周次时才扩展。
+        semesterTotalWeeks: Math.max(maxWeek, 20),
+        firstDayOfWeek: firstDayOfWeek
+    };
+}
+
+
 /**
  * 请求和解析课程数据
  */
@@ -312,7 +496,7 @@ async function fetchAndParseCourses(academicYear, semesterIndex) {
 
     // API URL 和请求体
     const xnmXqmBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`;
-    const url = "http://jwxt.ujs.edu.cn/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151";
+    const url = buildApiUrl("/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151");
 
     console.log(`JS: 发送请求到 ${url}, body: ${xnmXqmBody}`);
 
@@ -353,14 +537,21 @@ async function fetchAndParseCourses(academicYear, semesterIndex) {
 
         console.log("JS: 课程列表预览:", courses.slice(0, 5)); // 预览前5门课程
 
-        // 默认总周数为 20,周一为一周第一天
-        const config = {
-            semesterTotalWeeks: 20,
-            firstDayOfWeek: 1
-        };
+        // 集中实践课(军训、形势与政策等)没有星期和节次,无法排进周课表,单独取出用于提示
+        const practiceCourses = parsePracticeCourses(jsonData);
+        if (practiceCourses.length > 0) {
+            console.log(`JS: 检测到 ${practiceCourses.length} 门集中实践课,无法自动导入。`);
+        }
 
-        // 返回课程列表和配置信息
-        return { courses: courses, config: config };
+        // qsxqj: 教务系统设置的一周起始星期几,缺失时按周一处理。
+        const rawFirstDay = Number(jsonData.qsxqj);
+        const firstDayOfWeek = (rawFirstDay >= 1 && rawFirstDay <= 7) ? rawFirstDay : 1;
+
+        return {
+            courses: courses,
+            practiceCourses: practiceCourses,
+            firstDayOfWeek: firstDayOfWeek
+        };
 
     } catch (error) {
         window.shiguangBridge.showToast(`请求或解析失败: ${error.message}`);
@@ -383,6 +574,48 @@ async function saveCourses(parsedCourses) {
     }
 }
 
+/**
+ * 只在能拿到真实开学日期时写入课表配置。
+ * 拿不到就完全不调用 saveCourseConfig —— 应用侧是整体覆盖,
+ * 传入不含 semesterStartDate 的配置会把用户已设置的开学日期清空。
+ */
+async function saveCourseConfigIfPossible(courses, academicYear, semesterIndex, firstDayOfWeek) {
+    const semesterRange = await fetchCurrentSemesterRange();
+
+    let usableRange = null;
+    if (semesterRange) {
+        const sameYear = semesterRange.academicYear === String(academicYear);
+        const sameSemester = semesterRange.semesterIndex === semesterIndex;
+
+        if (sameYear && sameSemester) {
+            usableRange = semesterRange;
+        } else {
+            console.log(
+                `JS: 所选学年学期(${academicYear}/第${semesterIndex + 1}学期)` +
+                `不是教务系统当前学期(${semesterRange.academicYear}/第${semesterRange.semesterIndex + 1}学期),跳过开学日期写入。`
+            );
+        }
+    }
+
+    const config = buildCourseConfig(courses, usableRange, firstDayOfWeek);
+
+    if (!config) {
+        window.shiguangBridge.showToast("未取到本学期开学日期,已跳过课表配置,请在应用内手动设置开学日期。");
+        console.log("JS: 无可用开学日期,跳过 saveCourseConfig 以保留用户现有配置。");
+        return;
+    }
+
+    try {
+        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
+        window.shiguangBridge.showToast(
+            `课表配置更新成功!开学日期 ${config.semesterStartDate},总周数 ${config.semesterTotalWeeks} 周。`
+        );
+    } catch (error) {
+        window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
+        console.error('JS: Save Config Error:', error);
+    }
+}
+
 // 上午作息时间
 // 北固及本部主楼、主A楼、生环楼、汽车能动楼、京江楼
 const AMorningTimeSlots = [
@@ -488,7 +721,9 @@ function getCampusTypeFromPosition(position) {
 function getMorningTypeFromPosition(position) {
     const text = String(position || "").trim();
 
-    if (text.includes("主A楼") || text.includes("京江楼")) return "A";
+    // 教务系统返回的是「京江2号楼2101」「京江3号楼3407」这类名称,没有「京江楼」这个写法,
+    // 所以这里匹配「京江」而不是「京江楼」。
+    if (text.includes("主A楼") || text.includes("京江")) return "A";
     if (text.includes("三江楼")) return "B";
     if (text.includes("三山楼") || text.includes("讲堂群")) return "C";
 
@@ -642,33 +877,51 @@ async function runImportFlow() {
         console.log("JS: 课程获取或解析失败,流程终止。");
         return;
     }
-    const { courses, config } = result;
+    const { courses, practiceCourses, firstDayOfWeek } = result;
 
     const coursesWithCustomTime = applyCustomTimeToCourses(courses, isSummerTime);
 
-    // 只匹配了主A楼、京江楼、三江楼、三山楼、讲堂群这几个教学楼的作息时间,其他课程都使用默认时间
-    const timeSlotAlert = await window.shiguangBridgePromise.showAlert(
-        "楼栋自定义时间提示",
-        "脚本已根据课程所在位置智能匹配了作息时间,部分课程可能与预设时间不符。\n" +
-        "请在课表页面核对课程时间,如有错误请手动修改课程所在位置或节次信息。\n" +
-        "(目前仅主A楼、京江楼、三江楼、三山楼、讲堂群的课程会应用自定义时间,其他课程默认时间不变)\n" +
+    // 作息时间在导入时一次性写入,不会自动跟随切令时变化,需要明确告知用户。
+    // 同时说明只有部分教学楼收录了独立作息,其余楼栋使用默认作息时间。
+    const nextSwitch = getNextTimeSlotSwitchDate(isSummerTime);
+    await window.shiguangBridgePromise.showAlert(
+        "作息时间提示",
+        `本次按${isSummerTime ? "夏令时" : "冬令时"}导入。作息时间在导入时写入,不会自动跟随学校切换令时。\n` +
+        `${nextSwitch.text}起学校切换为${nextSwitch.label},届时请重新导入课表,或在应用内手动修改时间段。\n\n` +
+        "脚本已根据课程所在位置匹配作息时间,部分课程可能与预设时间不符。\n" +
+        "请在课表页面核对课程时间,如有错误请手动修改课程所在位置或节次信息。\n\n" +
+        "已收录独立作息的楼栋:主A楼、京江各号楼、三江楼、三山楼、讲堂群。\n" +
+        "其余楼栋(各学院楼、各实验室、运动场、未排地点等)使用默认作息时间。\n\n" +
         "欢迎其他楼栋的同学提供课程时间信息以完善脚本!",
         "我知道了"
     );
 
+    if (practiceCourses.length > 0) {
+        const practiceList = practiceCourses
+            .map((item) => {
+                const teacher = item.teacher ? `(${item.teacher})` : "";
+                const weekDesc = item.weekDesc ? ` ${item.weekDesc}` : "";
+                return `· ${item.name}${teacher}${weekDesc}`;
+            })
+            .join("\n");
+
+        console.log("JS: 集中实践课列表:", practiceCourses);
+        await window.shiguangBridgePromise.showAlert(
+            "集中实践课需手动添加",
+            `本学期有 ${practiceCourses.length} 门集中实践课,教务系统未给出星期和节次,无法自动导入:\n\n` +
+            practiceList +
+            "\n\n请按实际安排在应用内手动添加。",
+            "我知道了"
+        );
+    }
+
     const saveResult = await saveCourses(coursesWithCustomTime);
     if (!saveResult) {
         console.log("JS: 课程保存失败,流程终止。");
         return;
     }
 
-    try {
-        await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
-        window.shiguangBridge.showToast(`课表配置更新成功!总周数:${config.semesterTotalWeeks}周。`);
-    } catch (error) {
-        window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
-        console.error('JS: Save Config Error:', error);
-    }
+    await saveCourseConfigIfPossible(coursesWithCustomTime, academicYear, semesterIndex, firstDayOfWeek);
 
     await importPresetTimeSlots(isSummerTime ? SummerTimeSlots : WinterTimeSlots);
 
@@ -678,4 +931,4 @@ async function runImportFlow() {
     window.shiguangBridge.notifyTaskCompletion();
 }
 
-runImportFlow();
+runImportFlow();