|
|
@@ -18,6 +18,33 @@
|
|
|
// 集中实践课(军训、毕业设计等)无星期节次无法排课,改为弹窗提示手动添加。
|
|
|
// 教师与教室为空不再丢弃课程;修正京江各号楼的楼栋匹配;支持校外 WebVPN 访问。
|
|
|
|
|
|
+// 2026.09.07 第三版
|
|
|
+// 开学日期改从校历接口 xskbcxZccx_cxZcByXnxq 按所选学年学期获取,历史学期同样能取到,
|
|
|
+// 总周数取校历周数,日期解析兼容 zcrq / ksrq 字段兜底;取不到校历时仍跳过配置保存。
|
|
|
+// 作息时间不随切令时自动变化,导入后弹窗提示下次切令时日期。
|
|
|
+// 新增课程合并去重:连续节次合并、同课同节次的周次合并、完全重复去重。
|
|
|
+// 学年学期改为从教务系统读取选项并默认选中当前学期,读取失败时回退手动输入。
|
|
|
+// 课程请求与校历请求并行执行,课程解析与配置计算整合进同一函数,结构对齐官方参考脚本。
|
|
|
+// 修复:教务页面 zftal 脚本会改写 Array.prototype.filter(回调实参颠倒),
|
|
|
+// 导致集中实践课被静默丢弃,改用自实现的 arrayFilter 规避;清理注释残留与冗余日志。
|
|
|
+
|
|
|
+/**
|
|
|
+ * 数组过滤(原生实现)。
|
|
|
+ * 教务页面加载的 zftal 脚本会改写 Array.prototype.filter/some/every,
|
|
|
+ * 把回调实参换成 (index, value)(原生的 value 在前),
|
|
|
+ * 原生 filter 写法在这种页面上会得到错误结果(例如过滤不掉空选项),
|
|
|
+ * 所以这里用普通循环自行实现,不依赖任何被改写过的数组方法。
|
|
|
+ */
|
|
|
+function arrayFilter(arr, predicate) {
|
|
|
+ const result = [];
|
|
|
+ for (let i = 0; i < arr.length; i++) {
|
|
|
+ if (predicate(arr[i], i, arr)) {
|
|
|
+ result.push(arr[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 解析周次字符串,处理单双周和周次范围。
|
|
|
*/
|
|
|
@@ -63,6 +90,101 @@ function parseWeeks(weekStr) {
|
|
|
return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * 节次与周次合并去重函数(参考官方 wiki 课程合并与去重函数)。
|
|
|
+ * 正方系统常把同一门课拆成多条记录:连续节次分开返回(1-2 节 + 3-4 节)、
|
|
|
+ * 单双周分开返回、或完全重复返回,这里统一合并/去重。
|
|
|
+ * @param {Array<Object>} courses 原始解析课程数组
|
|
|
+ * @returns {Array<Object>} 合并去重后的课程数组
|
|
|
+ */
|
|
|
+function mergeAndDistinctCourses(courses) {
|
|
|
+ if (!Array.isArray(courses) || courses.length <= 1) return courses;
|
|
|
+
|
|
|
+ // 1. 深拷贝并规范周次数据,过滤无效项
|
|
|
+ const list = courses.map(c => ({
|
|
|
+ ...c,
|
|
|
+ name: c.name || '',
|
|
|
+ teacher: c.teacher || '',
|
|
|
+ position: c.position || '',
|
|
|
+ weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
|
|
|
+ list.sort((a, b) => {
|
|
|
+ return a.name.localeCompare(b.name) ||
|
|
|
+ a.teacher.localeCompare(b.teacher) ||
|
|
|
+ a.position.localeCompare(b.position) ||
|
|
|
+ (a.day || 0) - (b.day || 0) ||
|
|
|
+ a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
|
|
|
+ (a.startSection || 0) - (b.startSection || 0);
|
|
|
+ });
|
|
|
+
|
|
|
+ const step1Merged = [];
|
|
|
+ let current = list[0];
|
|
|
+
|
|
|
+ for (let i = 1; i < list.length; i++) {
|
|
|
+ const next = list[i];
|
|
|
+
|
|
|
+ const isSameCourseAndWeeks =
|
|
|
+ current.name === next.name &&
|
|
|
+ current.teacher === next.teacher &&
|
|
|
+ current.position === next.position &&
|
|
|
+ current.day === next.day &&
|
|
|
+ current.weeks.join(',') === next.weeks.join(',');
|
|
|
+
|
|
|
+ const isContinuous = current.endSection + 1 === next.startSection;
|
|
|
+ const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
|
|
|
+
|
|
|
+ if (isSameCourseAndWeeks && isContinuous) {
|
|
|
+ // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
|
|
|
+ current.endSection = next.endSection;
|
|
|
+ } else if (isSameCourseAndWeeks && isDuplicate) {
|
|
|
+ // 完全重复:跳过
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ step1Merged.push(current);
|
|
|
+ current = next;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ step1Merged.push(current);
|
|
|
+
|
|
|
+ // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
|
|
|
+ step1Merged.sort((a, b) => {
|
|
|
+ return a.name.localeCompare(b.name) ||
|
|
|
+ a.teacher.localeCompare(b.teacher) ||
|
|
|
+ a.position.localeCompare(b.position) ||
|
|
|
+ (a.day || 0) - (b.day || 0) ||
|
|
|
+ (a.startSection || 0) - (b.startSection || 0) ||
|
|
|
+ (a.endSection || 0) - (b.endSection || 0);
|
|
|
+ });
|
|
|
+
|
|
|
+ const step2Merged = [];
|
|
|
+ let cur = step1Merged[0];
|
|
|
+
|
|
|
+ for (let i = 1; i < step1Merged.length; i++) {
|
|
|
+ const nxt = step1Merged[i];
|
|
|
+
|
|
|
+ const isSameCourseAndSection =
|
|
|
+ cur.name === nxt.name &&
|
|
|
+ cur.teacher === nxt.teacher &&
|
|
|
+ cur.position === nxt.position &&
|
|
|
+ cur.day === nxt.day &&
|
|
|
+ cur.startSection === nxt.startSection &&
|
|
|
+ cur.endSection === nxt.endSection;
|
|
|
+
|
|
|
+ if (isSameCourseAndSection) {
|
|
|
+ // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周,单周 + 双周 -> 每周)
|
|
|
+ cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
|
|
|
+ } else {
|
|
|
+ step2Merged.push(cur);
|
|
|
+ cur = nxt;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ step2Merged.push(cur);
|
|
|
+
|
|
|
+ return step2Merged;
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* 拼接教务系统接口地址。
|
|
|
* 校内直连时 location.origin 就是教务域名,前缀为空;
|
|
|
@@ -110,21 +232,22 @@ function parsePracticeCourses(jsonData) {
|
|
|
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);
|
|
|
+ return arrayFilter(
|
|
|
+ jsonData.sjkList
|
|
|
+ .map((item) => ({
|
|
|
+ name: String(item.kcmc || "").trim(),
|
|
|
+ teacher: String(item.jsxm || "").trim(),
|
|
|
+ weekDesc: String(item.qsjsz || "").trim()
|
|
|
+ })),
|
|
|
+ (item) => item.name
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 解析 API 返回的 JSON 数据。
|
|
|
+ * 合并去重后,按课程所在位置匹配作息,写入自定义时间。
|
|
|
*/
|
|
|
-function parseJsonData(jsonData) {
|
|
|
- console.log("JS: parseJsonData 正在解析 JSON 数据...");
|
|
|
-
|
|
|
+function parseJsonData(jsonData, isSummerTime) {
|
|
|
// 检查JSON结构:新的数据在 kbList 字段中
|
|
|
if (!jsonData || !Array.isArray(jsonData.kbList)) {
|
|
|
console.warn("JS: JSON 数据结构错误或缺少 kbList 字段。");
|
|
|
@@ -132,7 +255,7 @@ function parseJsonData(jsonData) {
|
|
|
}
|
|
|
|
|
|
const rawCourseList = jsonData.kbList;
|
|
|
- const finalCourseList = [];
|
|
|
+ const initialCourseList = [];
|
|
|
|
|
|
for (const rawCourse of rawCourseList) {
|
|
|
// 关键字段检查:只有 kcmc(课名), xqj(星期), jcs(节次范围), zcd(周次描述) 是排课必需的。
|
|
|
@@ -158,7 +281,6 @@ function parseJsonData(jsonData) {
|
|
|
|
|
|
// 数字有效性检查
|
|
|
if (isNaN(day) || isNaN(startSection) || isNaN(endSection) || day < 1 || day > 7 || startSection > endSection) {
|
|
|
- // console.warn(`JS: 课程 ${rawCourse.kcmc} 星期或节次数据无效,跳过。`);
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
@@ -178,111 +300,38 @@ function parseJsonData(jsonData) {
|
|
|
course.remark = remark;
|
|
|
}
|
|
|
|
|
|
- finalCourseList.push(course);
|
|
|
+ initialCourseList.push(course);
|
|
|
}
|
|
|
|
|
|
+ // 正方常把同一门课拆成多条记录(连续节次、单双周、重复),先合并去重,
|
|
|
+ // 再按楼栋匹配作息写入自定义时间,最后按星期节次排序。
|
|
|
+ const mergedCourses = mergeAndDistinctCourses(initialCourseList);
|
|
|
+ const finalCourseList = applyCustomTimeToCourses(mergedCourses, isSummerTime);
|
|
|
finalCourseList.sort((a, b) =>
|
|
|
a.day - b.day ||
|
|
|
a.startSection - b.startSection ||
|
|
|
a.name.localeCompare(b.name)
|
|
|
);
|
|
|
|
|
|
- console.log(`JS: JSON 数据解析完成,共找到 ${finalCourseList.length} 门课程。`);
|
|
|
+ console.log(`JS: 课程解析完成,原始 ${initialCourseList.length} 条,合并去重后 ${finalCourseList.length} 条。`);
|
|
|
return finalCourseList;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 检查当前是否处于夏令时作息时间段。
|
|
|
* @returns true 夏令时 false 冬令时
|
|
|
+ *
|
|
|
+ * 曾尝试抓取教务处作息时间公告页(https://jwc.ujs.edu.cn/index/xl_zuo_xi_shi_jian.htm),
|
|
|
+ * 但跨域(CORS)无法读取,智能选择回退到预设日期。
|
|
|
+ * 冬令时是十一假期结束后调整,取 10 月 7 日;夏令时公告历年均为 4 月 7 日起执行。
|
|
|
*/
|
|
|
-async function whetherSummerTimeSlot() {
|
|
|
-
|
|
|
- // // 教务处 校历/作息时间 公告页
|
|
|
- // const url = "https://jwc.ujs.edu.cn/index/xl_zuo_xi_shi_jian.htm";
|
|
|
- // let title = "";
|
|
|
-
|
|
|
- // try {
|
|
|
- // const response = await fetch(url);
|
|
|
- // if (!response.ok) {
|
|
|
- // throw new Error(`网络请求失败。状态码: ${response.status} (${response.statusText})`);
|
|
|
- // }
|
|
|
-
|
|
|
- // const html = await response.text();
|
|
|
- // const doc = new DOMParser().parseFromString(html, "text/html");
|
|
|
-
|
|
|
- // // 优先按页面固定 id 读取:#line_u8_0, #line_u8_1 ...
|
|
|
- // for (let i = 0; i < 30; i++) {
|
|
|
- // const a = doc.querySelector(`#line_u8_${i} > a`);
|
|
|
- // if (!a) continue;
|
|
|
- // title = (a.getAttribute("title") || a.textContent || "").trim();
|
|
|
- // if (title.includes("作息时间表")) {
|
|
|
- // break;
|
|
|
- // }
|
|
|
- // }
|
|
|
-
|
|
|
- // // 若固定 id 没取到,则扫描所有链接文本
|
|
|
- // if (title.trim().length === 0) {
|
|
|
- // const links = doc.querySelectorAll("a");
|
|
|
- // for (const link of links) {
|
|
|
- // title = (link.getAttribute("title") || link.textContent || "").trim();
|
|
|
- // if (title.includes("作息时间表")) {
|
|
|
- // break;
|
|
|
- // }
|
|
|
- // }
|
|
|
- // }
|
|
|
-
|
|
|
- // // 从公告中提取日期
|
|
|
- // if (title.trim().length === 0) {
|
|
|
- // throw new Error("未找到作息时间公告标题。");
|
|
|
- // }
|
|
|
- // const match = title.match(/[((]\s*(\d{4})年(\d{1,2})月(\d{1,2})日起执行\s*[))]/);
|
|
|
- // if (!match) {
|
|
|
- // throw new Error("公告标题格式不匹配,无法提取执行日期。");
|
|
|
- // }
|
|
|
- // const y = Number(match[1]);
|
|
|
- // const m = Number(match[2]);
|
|
|
- // const d = Number(match[3]);
|
|
|
- // const changeDate = new Date(y, m - 1, d);
|
|
|
-
|
|
|
- // const now = new Date();
|
|
|
- // if (changeDate.getMonth() === 3 && now >= changeDate ) { // 4月7日开始夏令时
|
|
|
- // return true;
|
|
|
- // } else if (changeDate.getMonth() === 9 && now < changeDate) { // 10月7日开始冬令时
|
|
|
- // return true;
|
|
|
- // } else {
|
|
|
- // return false;
|
|
|
- // }
|
|
|
-
|
|
|
- // } catch (error) {
|
|
|
- // console.error('JS: 获取作息时间公告失败:', error);
|
|
|
- // window.shiguangBridge.showToast("无法获取作息时间公告,智能选择回退到预设时间。");
|
|
|
-
|
|
|
- // // 预设日期
|
|
|
- // const summerStart = new Date(new Date().getFullYear(), 3, 7); // 4月7日
|
|
|
- // const winterStart = new Date(new Date().getFullYear(), 9, 7); // 10月7日
|
|
|
-
|
|
|
- // const now = new Date();
|
|
|
- // if (now >= summerStart && now < winterStart) {
|
|
|
- // return true; // 夏令时
|
|
|
- // } else {
|
|
|
- // return false; // 冬令时
|
|
|
- // }
|
|
|
- // }
|
|
|
-
|
|
|
- // CORS 问题导致无法获取公告页,智能选择回退到预设时间。
|
|
|
- // 冬令时是十一假期结束后调整,取 10 月 7 日;夏令时公告历年均为 4 月 7 日起执行。
|
|
|
- // 参考教务处历年作息时间表公告:https://jwc.ujs.edu.cn/index/xl_zuo_xi_shi_jian.htm
|
|
|
-
|
|
|
+function whetherSummerTimeSlot() {
|
|
|
// 预设日期
|
|
|
const summerStart = new Date(new Date().getFullYear(), 3, 7); // 4月7日
|
|
|
const winterStart = new Date(new Date().getFullYear(), 9, 7); // 10月7日
|
|
|
|
|
|
const now = new Date();
|
|
|
- if (now >= summerStart && now < winterStart) {
|
|
|
- return true; // 夏令时
|
|
|
- } else {
|
|
|
- return false; // 冬令时
|
|
|
- }
|
|
|
+ return now >= summerStart && now < winterStart;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -323,18 +372,13 @@ function isLoginPage() {
|
|
|
|
|
|
|
|
|
function validateYearInput(input) {
|
|
|
- console.log("JS: validateYearInput 被调用,输入: " + input);
|
|
|
if (/^[0-9]{4}$/.test(input)) {
|
|
|
- console.log("JS: validateYearInput 验证通过。");
|
|
|
return false;
|
|
|
- } else {
|
|
|
- console.log("JS: validateYearInput 验证失败。");
|
|
|
- return "请输入四位数字的学年!";
|
|
|
}
|
|
|
+ return "请输入四位数字的学年!";
|
|
|
}
|
|
|
|
|
|
async function promptUserToStart() {
|
|
|
- console.log("JS: 流程开始:显示公告。");
|
|
|
return await window.shiguangBridgePromise.showAlert(
|
|
|
"教务系统课表导入",
|
|
|
"导入前请确保您已在浏览器中成功登录教务系统",
|
|
|
@@ -342,36 +386,134 @@ async function promptUserToStart() {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-async function getAcademicYear() {
|
|
|
+/**
|
|
|
+ * 从教务系统课表查询页读取学年学期选项。
|
|
|
+ * 学年:以系统当前选中项为中心,取前 2 年 + 后 2 年,共 5 个选项;
|
|
|
+ * 学期:直接取页面全部选项。
|
|
|
+ * 读取失败返回 null,由调用方回退到手动输入。
|
|
|
+ */
|
|
|
+async function fetchAcademicOptions() {
|
|
|
+ const url = buildApiUrl("/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151");
|
|
|
+
|
|
|
+ try {
|
|
|
+ const response = await fetch(url, {
|
|
|
+ method: "GET",
|
|
|
+ credentials: "include"
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) return null;
|
|
|
+
|
|
|
+ const doc = new DOMParser().parseFromString(await response.text(), "text/html");
|
|
|
+
|
|
|
+ const allYearOptions = arrayFilter(
|
|
|
+ Array.from(doc.querySelectorAll("#xnm option"))
|
|
|
+ .map((opt) => ({
|
|
|
+ value: opt.value,
|
|
|
+ text: opt.textContent.trim(),
|
|
|
+ selected: opt.selected
|
|
|
+ })),
|
|
|
+ (opt) => opt.value !== ""
|
|
|
+ );
|
|
|
+
|
|
|
+ const semesterOptions = arrayFilter(
|
|
|
+ Array.from(doc.querySelectorAll("#xqm option"))
|
|
|
+ .map((opt) => ({
|
|
|
+ value: opt.value,
|
|
|
+ // 江大返回的文本只有「1」「2」,直接展示不友好,补成「第一学期」这类写法。
|
|
|
+ text: /^\d$/.test(opt.textContent.trim())
|
|
|
+ ? `第${"一二三四五六七八九十"[Number(opt.textContent.trim()) - 1] || opt.textContent.trim()}学期`
|
|
|
+ : opt.textContent.trim(),
|
|
|
+ selected: opt.selected
|
|
|
+ })),
|
|
|
+ (opt) => opt.value !== ""
|
|
|
+ );
|
|
|
+
|
|
|
+ if (allYearOptions.length === 0 || semesterOptions.length === 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const selectedIndex = allYearOptions.findIndex((opt) => opt.selected);
|
|
|
+ const start = Math.max(0, selectedIndex === -1 ? 0 : selectedIndex - 2);
|
|
|
+ const end = Math.min(allYearOptions.length, selectedIndex === -1 ? 5 : selectedIndex + 3);
|
|
|
+ const yearOptions = allYearOptions.slice(start, end);
|
|
|
+
|
|
|
+ const defaultSemesterIndex = semesterOptions.findIndex((opt) => opt.selected);
|
|
|
+
|
|
|
+ return {
|
|
|
+ yearOptions,
|
|
|
+ semesterOptions,
|
|
|
+ defaultYearIndex: selectedIndex === -1 ? 0 : selectedIndex - start,
|
|
|
+ defaultSemesterIndex: defaultSemesterIndex === -1 ? 0 : defaultSemesterIndex
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ console.warn("JS: 读取学年学期选项失败:", error);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 提示用户选择学年和学期。
|
|
|
+ * 优先用教务系统返回的选项(自动默认选中当前学期),
|
|
|
+ * 读取失败时回退为手动输入学年 + 固定的两学期选择。
|
|
|
+ * @returns {{academicYear: string, semesterCode: string}} 或 null(取消)
|
|
|
+ */
|
|
|
+async function selectAcademicYearAndSemester() {
|
|
|
+ const optionsData = await fetchAcademicOptions();
|
|
|
+
|
|
|
+ if (optionsData) {
|
|
|
+ const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
|
|
|
+
|
|
|
+ const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
+ "选择学年",
|
|
|
+ JSON.stringify(yearOptions.map((item) => item.text)),
|
|
|
+ defaultYearIndex
|
|
|
+ );
|
|
|
+ if (yearIndex === null || yearIndex === -1) return null;
|
|
|
+
|
|
|
+ const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
+ "选择学期",
|
|
|
+ JSON.stringify(semesterOptions.map((item) => item.text)),
|
|
|
+ defaultSemesterIndex
|
|
|
+ );
|
|
|
+ if (semesterIndex === null || semesterIndex === -1) return null;
|
|
|
+
|
|
|
+ return {
|
|
|
+ academicYear: yearOptions[yearIndex].value,
|
|
|
+ semesterCode: semesterOptions[semesterIndex].value
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 回退:手动输入学年。
|
|
|
const currentYear = new Date().getFullYear().toString();
|
|
|
const currentMonth = new Date().getMonth() + 1; // 月份从0开始,所以加1
|
|
|
- // 如果当前月份在8月或之后,默认学年是当前年份-下一年份,否则是上一年份-当前年份
|
|
|
- const defaultYear = currentMonth >= 8 ? currentYear : (Number(currentYear) - 1).toString();
|
|
|
- console.log("JS: 提示用户输入学年。");
|
|
|
- return await window.shiguangBridgePromise.showPrompt(
|
|
|
+ // 如果当前月份在8月或之后,默认学年是当前年份,否则是上一年份
|
|
|
+ const defaultYear = currentMonth >= 8 ? currentYear : (Number(currentYear) - 1).toString();
|
|
|
+
|
|
|
+ const academicYear = await window.shiguangBridgePromise.showPrompt(
|
|
|
"选择学年",
|
|
|
- "请输入要导入课程的起始学年(如2025-2026 应该填2025):",
|
|
|
+ "未能从教务系统读取学年学期,请手动输入要导入课程的起始学年(如2025-2026 应该填2025):",
|
|
|
defaultYear,
|
|
|
"validateYearInput"
|
|
|
);
|
|
|
-}
|
|
|
+ if (academicYear === null) return null;
|
|
|
|
|
|
-async function selectSemester() {
|
|
|
const semesters = ["第一学期", "第二学期"];
|
|
|
- const currentMonth = new Date().getMonth() + 1; // 月份从0开始,所以加1
|
|
|
- const defaultSemesterIndex = currentMonth >= 8 ? 0 : 1; // 如果当前月份在8月或之后,默认选择第一学期,否则选择第二学期
|
|
|
- console.log("JS: 提示用户选择学期。");
|
|
|
+ const defaultSemesterIndex = currentMonth >= 8 ? 0 : 1;
|
|
|
const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
"选择学期",
|
|
|
JSON.stringify(semesters),
|
|
|
defaultSemesterIndex
|
|
|
);
|
|
|
- return semesterIndex;
|
|
|
+ if (semesterIndex === null || semesterIndex === -1) return null;
|
|
|
+
|
|
|
+ return {
|
|
|
+ academicYear,
|
|
|
+ semesterCode: getSemesterCode(semesterIndex)
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
async function selectTimeSlot() {
|
|
|
const timeSlots = ["智能选择" ,"夏令时", "冬令时"];
|
|
|
- console.log("JS: 提示用户选择作息类型。");
|
|
|
const timeSlotIndex = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
"选择作息时间",
|
|
|
JSON.stringify(timeSlots),
|
|
|
@@ -407,21 +549,25 @@ function getSemesterCode(semesterIndex) {
|
|
|
|
|
|
|
|
|
/**
|
|
|
- * 获取教务系统当前学期的起止日期。
|
|
|
- * 首页日历区块的标题形如 "2026-2027学年1学期(2026-08-31至2027-02-21)",
|
|
|
- * 其中起始日期就是第 1 周周一,正是 semesterStartDate 需要的值。
|
|
|
- * 注意:该接口忽略 xnm/xqm 参数,只返回当前学期,
|
|
|
- * 因此只有用户选择的学年学期与返回值一致时才能使用。
|
|
|
+ * 获取指定学年学期的校历周次。
|
|
|
+ *
|
|
|
+ * 接口按周次顺序返回数组,每个元素代表一周:
|
|
|
+ * rq 形如 "2026-08-31/2026-09-06",zs 是周次序号。
|
|
|
+ * 第 1 周的起始日(周一)就是 semesterStartDate 需要的开学日期,
|
|
|
+ * 数组长度就是教务排的总周数。
|
|
|
+ * 与首页日历区块不同,这个接口尊重 xnm/xqm 参数,历史学期同样能取到;
|
|
|
+ * 尚未排出校历的学期返回空数组。
|
|
|
*/
|
|
|
-async function fetchCurrentSemesterRange() {
|
|
|
- const url = buildApiUrl("/xtgl/index_cxAreaFive.html?localeKey=zh_CN&gnmkdm=index");
|
|
|
+async function fetchSemesterWeeks(academicYear, semesterCode) {
|
|
|
+ const url = buildApiUrl("/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154");
|
|
|
|
|
|
try {
|
|
|
const response = await fetch(url, {
|
|
|
"headers": {
|
|
|
"content-type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
|
+ "x-requested-with": "XMLHttpRequest"
|
|
|
},
|
|
|
- "body": "",
|
|
|
+ "body": `xnm=${academicYear}&xqm=${semesterCode}`,
|
|
|
"method": "POST",
|
|
|
"credentials": "include"
|
|
|
});
|
|
|
@@ -430,25 +576,33 @@ async function fetchCurrentSemesterRange() {
|
|
|
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})[))]/);
|
|
|
+ const weekList = JSON.parse(await response.text());
|
|
|
|
|
|
- if (!match) {
|
|
|
- console.warn("JS: 未能从日历区块解析出学期起止日期。");
|
|
|
+ if (!Array.isArray(weekList) || weekList.length === 0) {
|
|
|
+ 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);
|
|
|
+ // 正常情况下数组已按周次排好,仍按 zs 找一次第 1 周,避免顺序变化时取错日期。
|
|
|
+ const firstWeek = weekList.find((item) => Number(item.zs) === 1) || weekList[0];
|
|
|
+
|
|
|
+ // rq 形如 "2026-08-31/2026-09-06",取斜杠前的周一日期;
|
|
|
+ // 部分正方部署改用 zcrq / ksrq 字段,一并兜底。
|
|
|
+ const rqDate = String(firstWeek.rq || "").split("/")[0].trim();
|
|
|
+ const fallbackMatch = String(firstWeek.zcrq || firstWeek.ksrq || "").match(/(\d{4}-\d{2}-\d{2})/);
|
|
|
+ const startDate = /^\d{4}-\d{2}-\d{2}$/.test(rqDate) ? rqDate : (fallbackMatch ? fallbackMatch[1] : "");
|
|
|
+
|
|
|
+ if (!startDate) {
|
|
|
+ console.warn("JS: 校历接口返回的日期无法识别:", firstWeek);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const range = { startDate: startDate, totalWeeks: weekList.length };
|
|
|
+ console.log("JS: 校历周次:", range);
|
|
|
return range;
|
|
|
|
|
|
} catch (error) {
|
|
|
- console.warn("JS: 获取学期起止日期失败:", error);
|
|
|
+ console.warn("JS: 获取校历周次失败:", error);
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
@@ -463,80 +617,66 @@ async function fetchCurrentSemesterRange() {
|
|
|
* defaultClassDuration / defaultBreakDuration 不显式传入,会被重置为应用默认的 45 / 10 分钟,
|
|
|
* 与江大「45 分钟一节、课间 10 分钟」一致,因此没有副作用。
|
|
|
*/
|
|
|
-function buildCourseConfig(courses, semesterRange, firstDayOfWeek) {
|
|
|
+function buildCourseConfig(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),
|
|
|
+ // 直接采用校历周数。校历含考试周与假期周,比实际上课周多几周,
|
|
|
+ // 但这个值只是课表的周次上限:多几周空白无害,少了会截断课程。
|
|
|
+ semesterTotalWeeks: semesterRange.totalWeeks,
|
|
|
firstDayOfWeek: firstDayOfWeek
|
|
|
};
|
|
|
}
|
|
|
|
|
|
|
|
|
/**
|
|
|
- * 请求和解析课程数据
|
|
|
+ * 请求和解析课程数据。
|
|
|
+ * 课程请求与校历请求并行执行,课表配置一并算好返回,
|
|
|
+ * 由调用方决定是否保存(拿不到开学日期时 config 为 null)。
|
|
|
*/
|
|
|
-async function fetchAndParseCourses(academicYear, semesterIndex) {
|
|
|
+async function fetchAndParseCourses(academicYear, semesterCode, isSummerTime) {
|
|
|
window.shiguangBridge.showToast("正在请求课表数据...");
|
|
|
|
|
|
- const semesterCode = getSemesterCode(semesterIndex);
|
|
|
-
|
|
|
- // API URL 和请求体
|
|
|
const xnmXqmBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck&xsdm=&kclbdm=`;
|
|
|
const url = buildApiUrl("/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151");
|
|
|
|
|
|
- console.log(`JS: 发送请求到 ${url}, body: ${xnmXqmBody}`);
|
|
|
-
|
|
|
- const requestOptions = {
|
|
|
- "headers": {
|
|
|
- "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
|
|
|
- },
|
|
|
- "body": xnmXqmBody,
|
|
|
- "method": "POST",
|
|
|
- "credentials": "include"
|
|
|
- };
|
|
|
+ // 并行获取课程数据和校历周次
|
|
|
+ const [courseResponse, semesterRange] = await Promise.all([
|
|
|
+ fetch(url, {
|
|
|
+ method: "POST",
|
|
|
+ headers: {
|
|
|
+ "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
|
|
|
+ },
|
|
|
+ body: xnmXqmBody,
|
|
|
+ credentials: "include"
|
|
|
+ }),
|
|
|
+ fetchSemesterWeeks(academicYear, semesterCode)
|
|
|
+ ]);
|
|
|
|
|
|
try {
|
|
|
- const response = await fetch(url, requestOptions);
|
|
|
-
|
|
|
- if (!response.ok) {
|
|
|
- throw new Error(`网络请求失败。状态码: ${response.status} (${response.statusText})`);
|
|
|
+ if (!courseResponse.ok) {
|
|
|
+ throw new Error(`网络请求失败。状态码: ${courseResponse.status} (${courseResponse.statusText})`);
|
|
|
}
|
|
|
|
|
|
- const jsonText = await response.text();
|
|
|
let jsonData;
|
|
|
try {
|
|
|
- jsonData = JSON.parse(jsonText);
|
|
|
+ jsonData = JSON.parse(await courseResponse.text());
|
|
|
} catch (e) {
|
|
|
console.error('JS: JSON 解析失败,可能是会话过期:', e);
|
|
|
window.shiguangBridge.showToast("数据返回格式错误,可能是您未成功登录或会话已过期。");
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- const courses = parseJsonData(jsonData);
|
|
|
+ const courses = parseJsonData(jsonData, isSummerTime);
|
|
|
|
|
|
if (courses.length === 0) {
|
|
|
window.shiguangBridge.showToast("未找到任何课程数据,请检查所选学年学期是否正确或本学期无课,或教务系统需要二次登录。");
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- console.log(`JS: 课程数据解析成功,共找到 ${courses.length} 门课程。`);
|
|
|
-
|
|
|
- console.log("JS: 课程列表预览:", courses.slice(0, 5)); // 预览前5门课程
|
|
|
-
|
|
|
// 集中实践课(军训、形势与政策等)没有星期和节次,无法排进周课表,单独取出用于提示。
|
|
|
const practiceCourses = parsePracticeCourses(jsonData);
|
|
|
if (practiceCourses.length > 0) {
|
|
|
@@ -550,7 +690,7 @@ async function fetchAndParseCourses(academicYear, semesterIndex) {
|
|
|
return {
|
|
|
courses: courses,
|
|
|
practiceCourses: practiceCourses,
|
|
|
- firstDayOfWeek: firstDayOfWeek
|
|
|
+ config: buildCourseConfig(semesterRange, firstDayOfWeek)
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
@@ -561,11 +701,8 @@ async function fetchAndParseCourses(academicYear, semesterIndex) {
|
|
|
}
|
|
|
|
|
|
async function saveCourses(parsedCourses) {
|
|
|
- window.shiguangBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
|
|
|
- console.log(`JS: 尝试保存 ${parsedCourses.length} 门课程...`);
|
|
|
try {
|
|
|
- await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses, null, 2));
|
|
|
- console.log("JS: 课程保存成功!");
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
|
|
|
return true;
|
|
|
} catch (error) {
|
|
|
window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
|
|
|
@@ -579,26 +716,7 @@ 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);
|
|
|
-
|
|
|
+async function saveCourseConfigIfPossible(config) {
|
|
|
if (!config) {
|
|
|
window.shiguangBridge.showToast("未取到本学期开学日期,已跳过课表配置,请在应用内手动设置开学日期。");
|
|
|
console.log("JS: 无可用开学日期,跳过 saveCourseConfig 以保留用户现有配置。");
|
|
|
@@ -641,7 +759,7 @@ const CMorningTimeSlots = [
|
|
|
{ number: 4, startTime: "11:15", endTime: "12:00" },
|
|
|
];
|
|
|
|
|
|
-// 夏令时
|
|
|
+// 夏令时
|
|
|
// 下午作息时间
|
|
|
// 北固
|
|
|
const DSummerAfternoonTimeSlots = [
|
|
|
@@ -702,20 +820,11 @@ function getCampusTypeFromPosition(position) {
|
|
|
const normalized = String(position || "").replace(/\s+/g, " ").trim();
|
|
|
if (!normalized) return null;
|
|
|
|
|
|
- // const firstPart = normalized.split(" ")[0] || "";
|
|
|
- // if (firstPart.includes("北固")) return "D";
|
|
|
- // if (firstPart.includes("本部")) return "E";
|
|
|
-
|
|
|
- // // 兜底:有些数据可能不按空格分段,补充全文匹配。
|
|
|
- // if (normalized.includes("北固")) return "D";
|
|
|
- // if (normalized.includes("本部")) return "E";
|
|
|
-
|
|
|
- // api好像不返回前缀了,我也不确定北固是怎么样的格式,只能这么写了()
|
|
|
+ // api 不再返回「本部」这类校区前缀,北固的返回格式也未确认,只能先这么写:
|
|
|
+ // 带「北固」的按北固校区处理,其余一律按本部处理。
|
|
|
const firstPart = normalized.split(" ")[0] || "";
|
|
|
if (firstPart.includes("北固") || normalized.includes("北固")) return "D";
|
|
|
- return "E"; // 其他默认本部
|
|
|
-
|
|
|
- // return null;
|
|
|
+ return "E";
|
|
|
}
|
|
|
|
|
|
function getMorningTypeFromPosition(position) {
|
|
|
@@ -790,21 +899,18 @@ function applyCustomTimeToCourses(courses, isSummerTime) {
|
|
|
|
|
|
|
|
|
async function importPresetTimeSlots(timeSlots) {
|
|
|
- console.log(`JS: 准备导入 ${timeSlots.length} 个预设时间段。`);
|
|
|
-
|
|
|
- if (timeSlots.length > 0) {
|
|
|
- window.shiguangBridge.showToast(`正在导入 ${timeSlots.length} 个预设时间段...`);
|
|
|
- try {
|
|
|
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
- window.shiguangBridge.showToast("预设时间段导入成功!");
|
|
|
- console.log("JS: 预设时间段导入成功。");
|
|
|
- } catch (error) {
|
|
|
- window.shiguangBridge.showToast("导入时间段失败: " + error.message);
|
|
|
- console.error('JS: Save Time Slots Error:', error);
|
|
|
- }
|
|
|
- } else {
|
|
|
+ if (timeSlots.length === 0) {
|
|
|
window.shiguangBridge.showToast("警告:时间段为空,未导入时间段信息。");
|
|
|
- console.warn("JS: 警告:传入时间段为空,未导入时间段信息。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ window.shiguangBridge.showToast(`正在导入 ${timeSlots.length} 个预设时间段...`);
|
|
|
+ try {
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
+ window.shiguangBridge.showToast("预设时间段导入成功!");
|
|
|
+ } catch (error) {
|
|
|
+ window.shiguangBridge.showToast("导入时间段失败: " + error.message);
|
|
|
+ console.error('JS: Save Time Slots Error:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -819,67 +925,43 @@ async function runImportFlow() {
|
|
|
const alertConfirmed = await promptUserToStart();
|
|
|
if (!alertConfirmed) {
|
|
|
window.shiguangBridge.showToast("用户取消了导入。");
|
|
|
- console.log("JS: 用户取消了导入流程。");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- // // 与后续流程并发执行,提前缓存智能选择结果。
|
|
|
- // const smartTimeSlotPromise = whetherSummerTimeSlot();
|
|
|
- // console.log("JS: 智能作息判定已并发启动。");
|
|
|
-
|
|
|
- const academicYear = await getAcademicYear();
|
|
|
- if (academicYear === null) {
|
|
|
+ const selection = await selectAcademicYearAndSemester();
|
|
|
+ if (selection === null) {
|
|
|
window.shiguangBridge.showToast("导入已取消。");
|
|
|
- console.log("JS: 获取学年失败/取消,流程终止。");
|
|
|
return;
|
|
|
}
|
|
|
- console.log(`JS: 已选择学年: ${academicYear}`);
|
|
|
-
|
|
|
-
|
|
|
- const semesterIndex = await selectSemester();
|
|
|
- if (semesterIndex === null || semesterIndex === -1) {
|
|
|
- window.shiguangBridge.showToast("导入已取消。");
|
|
|
- console.log("JS: 选择学期失败/取消,流程终止。");
|
|
|
- return;
|
|
|
- }
|
|
|
- console.log(`JS: 已选择学期索引: ${semesterIndex}`);
|
|
|
+ const { academicYear, semesterCode } = selection;
|
|
|
+ console.log(`JS: 已选择学年学期: ${academicYear} / ${semesterCode}`);
|
|
|
|
|
|
const timeSlotIndex = await selectTimeSlot();
|
|
|
if (timeSlotIndex === null || timeSlotIndex === -1) {
|
|
|
window.shiguangBridge.showToast("导入已取消。");
|
|
|
- console.log("JS: 选择作息类型失败/取消,流程终止。");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- let isSummerTime = false;
|
|
|
+ let isSummerTime;
|
|
|
if (timeSlotIndex === 1) {
|
|
|
isSummerTime = true;
|
|
|
} else if (timeSlotIndex === 2) {
|
|
|
isSummerTime = false;
|
|
|
} else {
|
|
|
- // try {
|
|
|
- // isSummerTime = await smartTimeSlotPromise;
|
|
|
- // } catch (error) {
|
|
|
- // console.error("JS: 智能作息判定异常,回退重新判定:", error);
|
|
|
- // isSummerTime = await whetherSummerTimeSlot();
|
|
|
- // }
|
|
|
- isSummerTime = await whetherSummerTimeSlot();
|
|
|
+ isSummerTime = whetherSummerTimeSlot();
|
|
|
const shouldReselect = await reselectTimeSlot(isSummerTime);
|
|
|
if (shouldReselect) {
|
|
|
isSummerTime = !isSummerTime;
|
|
|
}
|
|
|
-
|
|
|
}
|
|
|
console.log(`JS: 作息类型: ${isSummerTime ? "夏令时" : "冬令时"}`);
|
|
|
|
|
|
- const result = await fetchAndParseCourses(academicYear, semesterIndex);
|
|
|
+ const result = await fetchAndParseCourses(academicYear, semesterCode, isSummerTime);
|
|
|
if (result === null) {
|
|
|
console.log("JS: 课程获取或解析失败,流程终止。");
|
|
|
return;
|
|
|
}
|
|
|
- const { courses, practiceCourses, firstDayOfWeek } = result;
|
|
|
-
|
|
|
- const coursesWithCustomTime = applyCustomTimeToCourses(courses, isSummerTime);
|
|
|
+ const { courses, practiceCourses, config } = result;
|
|
|
|
|
|
// 作息时间在导入时一次性写入,不会自动跟随切令时变化,需要明确告知用户。
|
|
|
// 同时说明只有部分教学楼收录了独立作息,其余楼栋使用默认作息时间。
|
|
|
@@ -915,18 +997,18 @@ async function runImportFlow() {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- const saveResult = await saveCourses(coursesWithCustomTime);
|
|
|
+ const saveResult = await saveCourses(courses);
|
|
|
if (!saveResult) {
|
|
|
console.log("JS: 课程保存失败,流程终止。");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- await saveCourseConfigIfPossible(coursesWithCustomTime, academicYear, semesterIndex, firstDayOfWeek);
|
|
|
+ await saveCourseConfigIfPossible(config);
|
|
|
|
|
|
await importPresetTimeSlots(isSummerTime ? SummerTimeSlots : WinterTimeSlots);
|
|
|
|
|
|
|
|
|
- window.shiguangBridge.showToast(`课程导入成功,共导入 ${coursesWithCustomTime.length} 门课程!`);
|
|
|
+ window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
|
|
|
console.log("JS: 整个导入流程执行完毕并成功。");
|
|
|
window.shiguangBridge.notifyTaskCompletion();
|
|
|
}
|