|
|
@@ -0,0 +1,252 @@
|
|
|
+// 铜仁学院移动教务 (gztrc.edu.cn:17230/yjw) 拾光课程表适配脚本
|
|
|
+// 使用个人课表接口(courseSchedule)查询
|
|
|
+// 登录后需要从 localStorage 中读取用户信息,无需在脚本内登录
|
|
|
+
|
|
|
+const API = {
|
|
|
+ semesters: "/api/baseInfo/mobile/common/querySemester",
|
|
|
+ weeks: "/api/baseInfo/mobile/common/queryCurrentSemesterWeekList",
|
|
|
+ times: "/api/baseInfo/mobile/common/timeNameDetail",
|
|
|
+ schedule: "/api/arrange/mobile/courseSchedule/courseSchedule"
|
|
|
+};
|
|
|
+
|
|
|
+async function apiGet(path) {
|
|
|
+ const res = await fetch(path, { credentials: "include" });
|
|
|
+ if (!res.ok) throw new Error("请求失败: " + path);
|
|
|
+ return res.json();
|
|
|
+}
|
|
|
+
|
|
|
+async function apiPost(path, data, token) {
|
|
|
+ const res = await fetch(path, {
|
|
|
+ method: "POST",
|
|
|
+ headers: {
|
|
|
+ "Content-Type": "application/json",
|
|
|
+ "accessToken": token
|
|
|
+ },
|
|
|
+ credentials: "include",
|
|
|
+ body: JSON.stringify(data)
|
|
|
+ });
|
|
|
+ if (!res.ok) throw new Error("请求失败: " + path);
|
|
|
+ return res.json();
|
|
|
+}
|
|
|
+
|
|
|
+function parseUserFromStorage() {
|
|
|
+ try {
|
|
|
+ const raw = localStorage.getItem("user_info");
|
|
|
+ if (!raw) return null;
|
|
|
+ const data = JSON.parse(raw);
|
|
|
+ if (!data || !data.accessToken || !data.userInfo || !data.userInfo.userId) return null;
|
|
|
+ return {
|
|
|
+ token: data.accessToken,
|
|
|
+ userId: data.userInfo.userId,
|
|
|
+ userType: data.userInfo.userType || "0",
|
|
|
+ name: data.userInfo.name || ""
|
|
|
+ };
|
|
|
+ } catch (e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 解析单条课表记录(接口按"天 x 节次"逐格返回,每格一条记录)
|
|
|
+ * week 为空时使用记录自带的 weeks 字段
|
|
|
+ */
|
|
|
+function parseCourseItem(c, week) {
|
|
|
+ if (!c || !c.courseName) return null;
|
|
|
+ const section = parseInt(c.time);
|
|
|
+ if (isNaN(section) || section < 1) return null;
|
|
|
+
|
|
|
+ let w = week;
|
|
|
+ if (w === undefined || w === null) {
|
|
|
+ w = parseInt(c.weeks);
|
|
|
+ if (isNaN(w) || w < 1) return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 教务dayOfWeek: 1=周日,2=周一,3=周二,4=周三,5=周四,6=周五,7=周六
|
|
|
+ // 规范day: 1=周一,2=周二,3=周三,4=周四,5=周五,6=周六,7=周日
|
|
|
+ const dayMap = { "1": 7, "2": 1, "3": 2, "4": 3, "5": 4, "6": 5, "7": 6 };
|
|
|
+ const day = dayMap[String(c.dayOfWeek)] || parseInt(c.dayOfWeek);
|
|
|
+
|
|
|
+ let position = c.classroomName;
|
|
|
+ if (!position || position === "," || position.trim() === "") {
|
|
|
+ position = "未知地点";
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ name: c.courseName,
|
|
|
+ teacher: c.teacherName || "",
|
|
|
+ position: position,
|
|
|
+ day: day,
|
|
|
+ section: section,
|
|
|
+ week: w
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function sameWeekSet(a, b) {
|
|
|
+ if (a.length !== b.length) return false;
|
|
|
+ for (let i = 0; i < a.length; i++) {
|
|
|
+ if (a[i] !== b[i]) return false;
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+}
|
|
|
+
|
|
|
+function pushWeek(list, w) {
|
|
|
+ if (list.indexOf(w) === -1) list.push(w);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 将逐格记录合并为课程:
|
|
|
+ * 先按(天,课程,教师,地点,节次)聚合周次,再合并连续节次(且周次集合相同)
|
|
|
+ */
|
|
|
+function mergeToCourses(records) {
|
|
|
+ const groups = new Map();
|
|
|
+ for (let r of records) {
|
|
|
+ const key = [r.day, r.name, r.teacher, r.position].join("|");
|
|
|
+ let g = groups.get(key);
|
|
|
+ if (!g) {
|
|
|
+ g = { day: r.day, name: r.name, teacher: r.teacher, position: r.position, secMap: new Map() };
|
|
|
+ groups.set(key, g);
|
|
|
+ }
|
|
|
+ if (!g.secMap.has(r.section)) g.secMap.set(r.section, []);
|
|
|
+ pushWeek(g.secMap.get(r.section), r.week);
|
|
|
+ }
|
|
|
+
|
|
|
+ const courses = [];
|
|
|
+ for (let g of groups.values()) {
|
|
|
+ const sections = Array.from(g.secMap.keys()).sort((a, b) => a - b);
|
|
|
+ let cur = null;
|
|
|
+ for (let sec of sections) {
|
|
|
+ const weeks = g.secMap.get(sec).sort((a, b) => a - b);
|
|
|
+ if (cur && sec === cur.endSection + 1 && sameWeekSet(cur.weeks, weeks)) {
|
|
|
+ cur.endSection = sec;
|
|
|
+ } else {
|
|
|
+ cur = {
|
|
|
+ name: g.name,
|
|
|
+ teacher: g.teacher,
|
|
|
+ position: g.position,
|
|
|
+ day: g.day,
|
|
|
+ startSection: sec,
|
|
|
+ endSection: sec,
|
|
|
+ weeks: weeks.slice()
|
|
|
+ };
|
|
|
+ courses.push(cur);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return courses;
|
|
|
+}
|
|
|
+
|
|
|
+async function querySchedule(user, semesterId, weeks) {
|
|
|
+ const body = {
|
|
|
+ academicYearSemester: semesterId,
|
|
|
+ userId: user.userId,
|
|
|
+ userType: user.userType,
|
|
|
+ weeks: weeks
|
|
|
+ };
|
|
|
+ const json = await apiPost(API.schedule, body, user.token);
|
|
|
+ if (json.code !== 200) {
|
|
|
+ if (json.code === 53000505) {
|
|
|
+ throw new Error("登录已失效,请重新登录教务系统后重试。");
|
|
|
+ }
|
|
|
+ throw new Error(json.message || "获取课表失败");
|
|
|
+ }
|
|
|
+ if (!json.data || !json.data.course) throw new Error("课表数据为空");
|
|
|
+ return json.data.course;
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchAllCourses(user, semesterId, totalWeeks) {
|
|
|
+ // 注意:一次请求多周时,接口会把同一格子的多周数据以逗号拼接返回
|
|
|
+ // (如 courseName:"课A,课B"、teacherName:"师A,师A,..."),无法可靠拆分,
|
|
|
+ // 因此必须逐周查询,单周返回的数据是干净的
|
|
|
+ const records = [];
|
|
|
+ for (let w = 1; w <= totalWeeks; w++) {
|
|
|
+ const list = await querySchedule(user, semesterId, [w]);
|
|
|
+ records.push(...list.map(c => parseCourseItem(c, w)).filter(c => c !== null));
|
|
|
+ }
|
|
|
+ return mergeToCourses(records);
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchTimeSlots() {
|
|
|
+ const json = await apiGet(API.times);
|
|
|
+ if (json.code !== 200 || !json.data) throw new Error("获取作息时间失败");
|
|
|
+ return json.data.map(t => ({
|
|
|
+ number: parseInt(t.timeCode),
|
|
|
+ startTime: t.startTime,
|
|
|
+ endTime: t.endTime
|
|
|
+ }));
|
|
|
+}
|
|
|
+
|
|
|
+async function runImportFlow() {
|
|
|
+ try {
|
|
|
+ window.shiguangBridge.showToast("开始导入课表...");
|
|
|
+
|
|
|
+ // 1. 读取登录信息
|
|
|
+ const user = parseUserFromStorage();
|
|
|
+ if (!user) {
|
|
|
+ await window.shiguangBridgePromise.showAlert(
|
|
|
+ "请先登录",
|
|
|
+ "请在教务系统(移动教务)中完成登录,进入主页后再次点击执行导入。",
|
|
|
+ "知道了"
|
|
|
+ );
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 获取学期列表
|
|
|
+ window.shiguangBridge.showToast("正在获取学期列表...");
|
|
|
+ const semesterJson = await apiGet(API.semesters);
|
|
|
+ if (semesterJson.code !== 200) throw new Error("获取学期列表失败");
|
|
|
+ const semesters = (semesterJson.data || []).filter(s => s && s.semesterId && /^\d{4}-\d{4}-\d+$/.test(s.semesterId));
|
|
|
+ if (semesters.length === 0) throw new Error("未能获取学期列表");
|
|
|
+
|
|
|
+ // 3. 选择学期
|
|
|
+ let defaultIndex = 0;
|
|
|
+ const currentIdx = semesters.findIndex(s => s.isCurrentSemester === "1");
|
|
|
+ if (currentIdx !== -1) defaultIndex = currentIdx;
|
|
|
+ const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
+ "选择学期",
|
|
|
+ JSON.stringify(semesters.map(s => s.semesterName)),
|
|
|
+ defaultIndex
|
|
|
+ );
|
|
|
+ if (semesterIndex === null) {
|
|
|
+ window.shiguangBridge.showToast("导入已取消。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const semester = semesters[semesterIndex];
|
|
|
+
|
|
|
+ // 4. 获取本学期周数
|
|
|
+ let totalWeeks = 20;
|
|
|
+ try {
|
|
|
+ const weekJson = await apiGet(API.weeks);
|
|
|
+ if (weekJson.code === 200 && weekJson.data && weekJson.data.length > 0) {
|
|
|
+ totalWeeks = weekJson.data.length;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // 使用默认周数
|
|
|
+ }
|
|
|
+
|
|
|
+ // 5. 获取课表数据
|
|
|
+ window.shiguangBridge.showToast("正在获取课表数据,共 " + totalWeeks + " 周...");
|
|
|
+ const courses = await fetchAllCourses(user, semester.semesterId, totalWeeks);
|
|
|
+ if (courses.length === 0) throw new Error("未解析到课程数据,可能该学期暂无课表。");
|
|
|
+
|
|
|
+ // 6. 保存预设时间段
|
|
|
+ try {
|
|
|
+ const timeSlots = await fetchTimeSlots();
|
|
|
+ if (timeSlots.length > 0) {
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ window.shiguangBridge.showToast("时间段导入失败,但课程将继续导入。");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 7. 保存课程数据
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
+ window.shiguangBridge.showToast("成功导入 " + courses.length + " 条课程记录!");
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
+
|
|
|
+ } catch (e) {
|
|
|
+ console.error("[适配脚本错误] " + e.message);
|
|
|
+ window.shiguangBridge.showToast("导入失败: " + e.message);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+runImportFlow();
|