|
@@ -0,0 +1,179 @@
|
|
|
|
|
+// 济宁医学院教务(乘方教务 · 旧版 .action 接口)适配器
|
|
|
|
|
+
|
|
|
|
|
+const PRESET_TIME_SLOTS = [
|
|
|
|
|
+ { number: 1, startTime: "08:00", endTime: "08:40" },
|
|
|
|
|
+ { number: 2, startTime: "08:50", endTime: "09:30" },
|
|
|
|
|
+ { number: 3, startTime: "09:50", endTime: "10:30" },
|
|
|
|
|
+ { number: 4, startTime: "10:40", endTime: "11:20" },
|
|
|
|
|
+ { number: 5, startTime: "11:30", endTime: "12:10" },
|
|
|
|
|
+ { number: 6, startTime: "14:30", endTime: "15:10" },
|
|
|
|
|
+ { number: 7, startTime: "15:20", endTime: "16:00" },
|
|
|
|
|
+ { number: 8, startTime: "16:20", endTime: "17:00" },
|
|
|
|
|
+ { number: 9, startTime: "17:10", endTime: "17:50" },
|
|
|
|
|
+ { number: 10, startTime: "18:00", endTime: "18:40" },
|
|
|
|
|
+ { number: 11, startTime: "19:30", endTime: "20:10" },
|
|
|
|
|
+ { number: 12, startTime: "20:20", endTime: "21:00" }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+// 周次字符串 "10,7,8,9" → 去重排序的周数组
|
|
|
|
|
+function parseWeeks(weekStr) {
|
|
|
|
|
+ if (!weekStr) return [];
|
|
|
|
|
+ const weeks = weekStr.split(",").map(w => parseInt(w.trim(), 10)).filter(w => !isNaN(w) && w > 0);
|
|
|
|
|
+ return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 教室可能为空、含 "\\" 分隔的多教室(如 "B205\206教室")或以 "," 分隔的多场地
|
|
|
|
|
+function resolvePosition(raw) {
|
|
|
|
|
+ const position = String(raw || "").replace(/\\/g, "/").trim();
|
|
|
|
|
+ return position || "待定";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// kbxx 课程 JSON → 拾光课程格式(合并同 key 课程的周次)
|
|
|
|
|
+function parseCourseList(kbxx) {
|
|
|
|
|
+ if (!Array.isArray(kbxx)) throw new Error("课表接口返回格式不正确");
|
|
|
|
|
+ const courseMap = new Map();
|
|
|
|
|
+ kbxx.forEach(item => {
|
|
|
|
|
+ const day = parseInt(item.xq, 10);
|
|
|
|
|
+ const sections = String(item.jcdm2 || "").split(",")
|
|
|
|
|
+ .map(s => parseInt(s.trim(), 10)).filter(s => !isNaN(s));
|
|
|
|
|
+ const allWeeks = parseWeeks(item.zcs);
|
|
|
|
|
+ if (!item.kcmc || sections.length === 0 || allWeeks.length === 0 || isNaN(day) || day < 1 || day > 7) return;
|
|
|
|
|
+
|
|
|
|
|
+ const course = {
|
|
|
|
|
+ name: item.kcmc.trim(),
|
|
|
|
|
+ teacher: String(item.teaxms || "").trim() || "未知",
|
|
|
|
|
+ position: resolvePosition(item.jxcdmcs),
|
|
|
|
|
+ day,
|
|
|
|
|
+ startSection: Math.min(...sections),
|
|
|
|
|
+ endSection: Math.max(...sections),
|
|
|
|
|
+ weeks: allWeeks
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const key = [course.name, course.teacher, course.position, course.day,
|
|
|
|
|
+ course.startSection, course.endSection].join("__");
|
|
|
|
|
+ const existing = courseMap.get(key);
|
|
|
|
|
+ if (existing) existing.weeks = [...new Set([...existing.weeks, ...course.weeks])].sort((a, b) => a - b);
|
|
|
|
|
+ else courseMap.set(key, course);
|
|
|
|
|
+ });
|
|
|
|
|
+ return Array.from(courseMap.values()).sort((a, b) =>
|
|
|
|
|
+ a.day - b.day || a.startSection - b.startSection || a.endSection - b.endSection || a.name.localeCompare(b.name)
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 读取课表页中的学期下拉框
|
|
|
|
|
+function extractSemesterOptions(doc) {
|
|
|
|
|
+ const selectElem = doc.getElementById("xnxqdm");
|
|
|
|
|
+ if (!selectElem) return null;
|
|
|
|
|
+ const semesters = [];
|
|
|
|
|
+ const semesterValues = [];
|
|
|
|
|
+ let defaultIndex = 0;
|
|
|
|
|
+ Array.from(selectElem.querySelectorAll("option")).forEach(option => {
|
|
|
|
|
+ if (!option.value) return;
|
|
|
|
|
+ semesters.push(option.innerText.trim());
|
|
|
|
|
+ semesterValues.push(option.value);
|
|
|
|
|
+ if (option.selected || option.hasAttribute("selected")) defaultIndex = semesters.length - 1;
|
|
|
|
|
+ });
|
|
|
|
|
+ if (semesters.length === 0) return null;
|
|
|
|
|
+
|
|
|
|
|
+ const start = Math.max(0, defaultIndex - 1);
|
|
|
|
|
+ const end = Math.min(semesters.length, defaultIndex + 10);
|
|
|
|
|
+ return {
|
|
|
|
|
+ semesters: semesters.slice(start, end),
|
|
|
|
|
+ semesterValues: semesterValues.slice(start, end),
|
|
|
|
|
+ defaultIndex: defaultIndex - start
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 导入前提示先登录教务系统
|
|
|
|
|
+async function promptUserToStart() {
|
|
|
|
|
+ return await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "济宁医学院教务导入",
|
|
|
|
|
+ "请先确保已登录教务系统,再继续导入。",
|
|
|
|
|
+ "我已登录"
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 选择学期
|
|
|
|
|
+async function selectSemester(semesterOptions) {
|
|
|
|
|
+ const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择学期",
|
|
|
|
|
+ JSON.stringify(semesterOptions.semesters),
|
|
|
|
|
+ semesterOptions.defaultIndex
|
|
|
|
|
+ );
|
|
|
|
|
+ if (selectedIndex === null || selectedIndex < 0) return null;
|
|
|
|
|
+ return {
|
|
|
|
|
+ label: semesterOptions.semesters[selectedIndex],
|
|
|
|
|
+ value: semesterOptions.semesterValues[selectedIndex]
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 拉取课表页 HTML(含学期列表)
|
|
|
|
|
+async function fetchSchedulePage() {
|
|
|
|
|
+ const response = await fetch("/xsgrkbcx!getXsgrbkList.action", { method: "GET", credentials: "include" });
|
|
|
|
|
+ if (!response.ok) throw new Error(`无法打开课表页面(HTTP ${response.status})`);
|
|
|
|
|
+ return response.text();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 拉取指定学期课表(HTML 内嵌 var kbxx=[...])
|
|
|
|
|
+async function fetchCourseData(xnxqdm) {
|
|
|
|
|
+ const response = await fetch(
|
|
|
|
|
+ `/xsgrkbcx!xsAllKbList.action?xnxqdm=${encodeURIComponent(xnxqdm)}`,
|
|
|
|
|
+ { method: "GET", credentials: "include" }
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!response.ok) throw new Error(`课表请求失败(HTTP ${response.status})`);
|
|
|
|
|
+ const htmlText = await response.text();
|
|
|
|
|
+ const match = htmlText.match(/var\s+kbxx\s*=\s*(\[[\s\S]*?\]);/);
|
|
|
|
|
+ if (!match) throw new Error("课表数据解析失败,请检查登录状态");
|
|
|
|
|
+ let kbxx;
|
|
|
|
|
+ try {
|
|
|
|
|
+ kbxx = JSON.parse(match[1]);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ throw new Error(`课表数据解析失败:${error.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ return kbxx;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 主流程:提示 → 选学期 → 拉课表 → 保存课程与作息
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const confirmed = await promptUserToStart();
|
|
|
|
|
+ if (!confirmed) { window.shiguangBridge.showToast("导入已取消"); return; }
|
|
|
|
|
+
|
|
|
|
|
+ const pageHtml = await fetchSchedulePage();
|
|
|
|
|
+ const semesterOptions = extractSemesterOptions(new DOMParser().parseFromString(pageHtml, "text/html"));
|
|
|
|
|
+ if (!semesterOptions) throw new Error("未找到学期列表,请先登录教务系统");
|
|
|
|
|
+
|
|
|
|
|
+ const semester = await selectSemester(semesterOptions);
|
|
|
|
|
+ if (!semester) { window.shiguangBridge.showToast("导入已取消"); return; }
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast(`正在获取 ${semester.label} 的课表...`);
|
|
|
|
|
+ const courses = parseCourseList(await fetchCourseData(semester.value));
|
|
|
|
|
+
|
|
|
|
|
+ if (courses.length === 0) {
|
|
|
|
|
+ await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "提示",
|
|
|
|
|
+ "该学期没有获取到课程数据,请检查登录状态和所选学期。",
|
|
|
|
|
+ "确定"
|
|
|
|
|
+ );
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(PRESET_TIME_SLOTS));
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!`);
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "导入失败",
|
|
|
|
|
+ error.message || String(error),
|
|
|
|
|
+ "确定"
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+runImportFlow();
|