Bläddra i källkod

feat(ZHKU): 引入快捷导入方案和更强的登录检测与提示 (#856)

LC 3 veckor sedan
förälder
incheckning
22ac6bc3ab
2 ändrade filer med 101 tillägg och 25 borttagningar
  1. 1 1
      resources/ZHKU/adapters.yaml
  2. 100 24
      resources/ZHKU/zhku_lc6464.js

+ 1 - 1
resources/ZHKU/adapters.yaml

@@ -6,4 +6,4 @@ adapters:
     asset_js_path: "zhku_lc6464.js"
     import_url: "https://edu-admin.zhku.edu.cn/"
     maintainer: "lc6464"
-    description: "登录教务系统后在左侧找到“培养管理”->“我的课表”->“学期理论课表”,选择学期后点击右下角“执行导入”即可。\n学期理论课表默认为当前学期,若导入本学期课表则无需修改。\n目前仅支持导入“学期理论课表”内任意学期的课表,暂不支持导入其它类型的课表。"
+    description: "便捷导入流程:登录教务系统后点击右下角“执行导入”,在弹窗中选择学年学期即可。\n\n传统导入流程:登录教务系统后在左侧找到“培养管理”->“我的课表”->“学期理论课表”,选择学期后点击右下角“执行导入”即可。\n\n目前仅支持导入“学期理论课表”内任意学期的课表,暂不支持导入其它类型的课表。"

+ 100 - 24
resources/ZHKU/zhku_lc6464.js

@@ -266,13 +266,7 @@ function findNextFont(fonts, startIndex, predicate) {
 	return null;
 }
 
-// 教务页面会用 display:none 隐藏辅助节点,这里只保留可见信息
-function isVisibleFont(font) {
-	const styleText = (font.getAttribute("style") ?? "").replace(/\s+/g, "").toLowerCase();
-	return !styleText.includes("display:none");
-}
-
-// 从课表 iframe 中解析课程
+// 从课表文档中解析课程
 // 输出为扁平数组,不做同名课程合并
 function parseCoursesFromIframeDocument(iframeDoc) {
 	const courses = [];
@@ -294,9 +288,6 @@ function parseCoursesFromIframeDocument(iframeDoc) {
 			if (!title.includes("周次")) {
 				return;
 			}
-			if (!isVisibleFont(font)) {
-				return;
-			}
 
 			const weekText = cleanText(font.textContent);
 			if (weekText === "") {
@@ -306,7 +297,7 @@ function parseCoursesFromIframeDocument(iframeDoc) {
 			// 以“周次(节次)”行为锚点,向前找教师和课程名,向后找教室
 			const teacherFont = findPreviousFont(fonts, idx, (candidate) => {
 				const candidateTitle = cleanText(candidate.getAttribute("title") ?? "");
-				return candidateTitle.includes("教师") && isVisibleFont(candidate);
+				return candidateTitle.includes("教师");
 			});
 
 			const teacherIndex = teacherFont == null ? idx : fonts.indexOf(teacherFont);
@@ -317,14 +308,13 @@ function parseCoursesFromIframeDocument(iframeDoc) {
 				return (
 					candidateTitle === "" &&
 					candidateNameAttr === "" &&
-					isVisibleFont(candidate) &&
 					text !== ""
 				);
 			});
 
 			const locationFont = findNextFont(fonts, idx, (candidate) => {
 				const candidateTitle = cleanText(candidate.getAttribute("title") ?? "");
-				return candidateTitle.includes("教室") && isVisibleFont(candidate);
+				return candidateTitle.includes("教室");
 			});
 
 			const courseName = cleanText(nameFont?.textContent ?? "");
@@ -384,22 +374,79 @@ function parseCoursesFromIframeDocument(iframeDoc) {
 // 获取课表 iframe 的文档对象
 function getScheduleIframeDocument() {
 	const iframe = document.querySelector("iframe[src*='/jsxsd/xskb/xskb_list.do']");
-	if (iframe == null || iframe.contentDocument == null) {
-		throw new Error("未找到课表 iframe,或 iframe 内容尚未加载完成");
+	if (iframe == null) {
+		return null;
+	}
+	if (iframe.contentDocument == null) {
+		throw new Error("课表 iframe 内容尚未加载完成");
 	}
 	return iframe.contentDocument;
 }
 
-// 获取当前学年学期 ID,例如 2025-2026-2
-function getSemesterId(iframeDoc) {
+// 获取列表中的全部以及当前选择的学年学期 ID
+function getSemesterIds(iframeDoc) {
 	const select = iframeDoc.querySelector("#xnxq01id");
 	if (select == null) {
-		throw new Error("未找到学年学期选择框 #xnxq01id");
+		throw new Error("未找到学年学期选择框 #xnxq01id,请确认已登录教务系统");
 	}
 
-	// 优先读取 option[selected],读取失败再回退到 select.value
-	const selectedOption = select.querySelector("option[selected]");
-	return cleanText(selectedOption?.value ?? select.value);
+	const options = Array.from(select.querySelectorAll("option"))
+		.map((opt) => cleanText(opt.value))
+		.filter((value) => value !== "");
+	const selectedOption = cleanText(select.value);
+
+	if (selectedOption === "") {
+		throw new Error("未找到当前激活的学年学期,请确认已登录教务系统");
+	}
+
+	return { options, selectedOption };
+}
+
+// 拉取学期理论课表并解析为文档对象
+// 未指定学年学期时,教务系统返回当前激活学期的课表
+async function fetchScheduleDocument(semesterId = "") {
+	const query = semesterId === "" ? "" : `?xnxq01id=${encodeURIComponent(semesterId)}`;
+	const url = `https://edu-admin.zhku.edu.cn/jsxsd/xskb/xskb_list.do${query}`;
+	const response = await fetch(url, {
+		method: "GET",
+		credentials: "include"
+	});
+	if (!response.ok) {
+		throw new Error(`获取学期理论课表失败:HTTP ${response.status}`);
+	}
+
+	const html = await response.text();
+	const parser = new DOMParser();
+	const doc = parser.parseFromString(html, "text/html");
+	return doc;
+}
+
+// 获取学年学期列表并让用户选择,默认选中当前激活学期
+async function chooseScheduleDocument() {
+	const cachedDoc = await fetchScheduleDocument();
+	const { options, selectedOption } = getSemesterIds(cachedDoc);
+	const selection = await window.shiguangBridgePromise.showSingleSelection(
+		"选择导入的学年学期",
+		JSON.stringify(options),
+		options.indexOf(selectedOption)
+	);
+	if (selection == null) {
+		throw new Error("已取消选择学年学期");
+	}
+
+	const selectedIndex = Number(selection);
+	if (!Number.isInteger(selectedIndex) || selectedIndex < 0 || selectedIndex >= options.length) {
+		throw new Error("未选择有效的学年学期");
+	}
+
+	const semesterId = options[selectedIndex];
+
+	// 激活学期直接复用首次获取的课表,其它学期再按 ID 拉取
+	let scheduleDoc = semesterId === selectedOption
+		? cachedDoc
+		: await fetchScheduleDocument(semesterId);
+
+	return { scheduleDoc, semesterId };
 }
 
 // 拉取教学周历并提取开学日期与总周数
@@ -441,16 +488,40 @@ async function fetchSemesterCalendarInfo(semesterId) {
 	};
 }
 
+// 单独区分未登录错误,入口处使用弹窗提示
+class LoginRequiredError extends Error {}
+
+// 通过登录表单识别登录页面
+function checkLoginDocument(doc) {
+	const loginForm = doc.querySelector('#loginForm[action="/Logon.do?method=logon"]');
+	if (loginForm != null) {
+		throw new LoginRequiredError("请先登录教务系统,再点击“执行导入”。");
+	}
+}
+
 // 主流程:读取课表 -> 选择校区 -> 拉周历 -> 生成节次 -> 调桥接导入
+// 传统流程:从页面中读取用户打开的课表 iframe
+// 便捷流程:若未打开课表,则让用户选择学年学期,直接拉取课表
 async function importSchedule() {
+	// 未登录时直接提示,不继续读取课表
+	checkLoginDocument(document);
+
 	window.shiguangBridge.showToast("开始读取教务课表……");
 
-	// 读取 iframe 并获取当前学年学期 ID
+	// 读取 iframe,未打开课表时则走便捷导入流程
 	const iframeDoc = getScheduleIframeDocument();
-	const semesterId = getSemesterId(iframeDoc);
+	let scheduleDoc = iframeDoc;
+	let semesterId;
+	if (iframeDoc == null) {
+		const selectedSchedule = await chooseScheduleDocument();
+		scheduleDoc = selectedSchedule.scheduleDoc;
+		semesterId = selectedSchedule.semesterId;
+	} else {
+		semesterId = getSemesterIds(iframeDoc).selectedOption;
+	}
 
 	// 解析课程信息
-	const courses = parseCoursesFromIframeDocument(iframeDoc);
+	const courses = parseCoursesFromIframeDocument(scheduleDoc);
 	if (courses.length === 0) {
 		throw new Error("未解析到任何课程,请确认当前课表页面已加载完成");
 	}
@@ -487,6 +558,11 @@ async function importSchedule() {
 	try {
 		await importSchedule();
 	} catch (error) {
+		if (error instanceof LoginRequiredError) {
+			// 弹窗强提醒,同时也在控制台输出并展示 Toast
+			await window.shiguangBridgePromise.showAlert("未登录教务系统", error.message, "好的");
+		}
+
 		console.error("课表导入失败:", error);
 		// 失败原因直接提示给用户,便于在移动端快速定位问题
 		window.shiguangBridge.showToast(`导入失败:${error.message}`);