|
@@ -0,0 +1,286 @@
|
|
|
|
|
+// 北部湾大学智慧教务系统(zhjw.bbgu.edu.cn)拾光课表导入适配脚本
|
|
|
|
|
+// 新一代金智 resourceapi 接口:session/list 取学期、timePattern/get-large-period 取作息、
|
|
|
|
|
+// timetable/class/timetable/student/my-table-detail 取课表
|
|
|
|
|
+
|
|
|
|
|
+// 解析周次字符串:"1-4,6-9" → [1,2,3,4,6,7,8,9]
|
|
|
|
|
+function parseWeeks(weekStr) {
|
|
|
|
|
+ const weeks = [];
|
|
|
|
|
+ String(weekStr || "").split(",").forEach(part => {
|
|
|
|
|
+ part = part.trim();
|
|
|
|
|
+ const range = part.match(/^(\d+)-(\d+)$/);
|
|
|
|
|
+ if (range) {
|
|
|
|
|
+ const start = Number(range[1]);
|
|
|
|
|
+ const end = Number(range[2]);
|
|
|
|
|
+ for (let i = start; i <= end; i++) weeks.push(i);
|
|
|
|
|
+ } else if (/^\d+$/.test(part)) {
|
|
|
|
|
+ weeks.push(Number(part));
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 解析节次字符串:"10-11" → { startSection: 10, endSection: 11 };单值 "12" → 第12节
|
|
|
|
|
+function parsePeriods(periodStr) {
|
|
|
|
|
+ const str = String(periodStr || "").trim();
|
|
|
|
|
+ const m = str.match(/^(\d+)-(\d+)$/);
|
|
|
|
|
+ if (m) {
|
|
|
|
|
+ const start = Number(m[1]);
|
|
|
|
|
+ const end = Number(m[2]);
|
|
|
|
|
+ if (!start || !end || start > end) return null;
|
|
|
|
|
+ return { startSection: start, endSection: end };
|
|
|
|
|
+ }
|
|
|
|
|
+ if (/^\d+$/.test(str)) {
|
|
|
|
|
+ const n = Number(str);
|
|
|
|
|
+ if (!n) return null;
|
|
|
|
|
+ return { startSection: n, endSection: n };
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 清洗教师名:"周文红[主讲];" → "周文红";"王柏玲[主讲];谢娟[主讲];" → "王柏玲、谢娟"
|
|
|
|
|
+function cleanTeacher(name) {
|
|
|
|
|
+ const cleaned = String(name || "")
|
|
|
|
|
+ .replace(/\[[^\]]*\]/g, "")
|
|
|
|
|
+ .replace(/[;;]+/g, "、")
|
|
|
|
|
+ .replace(/、+$/, "")
|
|
|
|
|
+ .trim();
|
|
|
|
|
+ return cleaned || "未知";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseCourse(row) {
|
|
|
|
|
+ const day = Number(row.weekDay);
|
|
|
|
|
+ const periods = parsePeriods(row.periodFormat);
|
|
|
|
|
+ const weeks = parseWeeks(row.teachingWeekFormat);
|
|
|
|
|
+ if (!row.courseName || !day || day < 1 || day > 7 || !periods || weeks.length === 0) return null;
|
|
|
|
|
+
|
|
|
|
|
+ const campus = String(row.campusName || "").trim();
|
|
|
|
|
+ const room = String(row.roomName || "").trim();
|
|
|
|
|
+ let position = room;
|
|
|
|
|
+ if (campus && room) position = `${room}(${campus})`;
|
|
|
|
|
+ if (!position) position = campus || "待定";
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ name: String(row.courseName).trim(),
|
|
|
|
|
+ teacher: cleanTeacher(row.instructorName),
|
|
|
|
|
+ position,
|
|
|
|
|
+ day,
|
|
|
|
|
+ startSection: periods.startSection,
|
|
|
|
|
+ endSection: periods.endSection,
|
|
|
|
|
+ weeks
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function getErrorMessage(error) {
|
|
|
|
|
+ if (error && typeof error.message === "string" && error.message.trim()) return error.message;
|
|
|
|
|
+ if (typeof error === "string" && error.trim()) return error;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const serialized = JSON.stringify(error);
|
|
|
|
|
+ if (serialized && serialized !== "{}") return serialized;
|
|
|
|
|
+ } catch (_) {
|
|
|
|
|
+ // Ignore serialization failures and use the generic fallback below.
|
|
|
|
|
+ }
|
|
|
|
|
+ return "未知错误";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 从页面存储中提取 JWT 鉴权 token(金智系统常见 key + 全量扫描兜底)
|
|
|
|
|
+function getAuthToken() {
|
|
|
|
|
+ const keys = ["Authorization", "access_token", "token", "X-Access-Token", "x-token", "auth_token", "jwt", "user_token"];
|
|
|
|
|
+ for (const k of keys) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const v = window.localStorage.getItem(k) || window.sessionStorage.getItem(k);
|
|
|
|
|
+ if (v && v.includes("eyJ")) return v;
|
|
|
|
|
+ } catch (_) {
|
|
|
|
|
+ // Ignore storage access errors.
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 全量扫描:找直接以 eyJ 开头(JWT)或 JSON 包装中含 eyJ 的字符串
|
|
|
|
|
+ const stores = [window.localStorage, window.sessionStorage];
|
|
|
|
|
+ for (const store of stores) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ for (let i = 0; i < store.length; i++) {
|
|
|
|
|
+ const raw = store.getItem(store.key(i));
|
|
|
|
|
+ if (!raw) continue;
|
|
|
|
|
+ if (raw.startsWith("eyJ")) return raw;
|
|
|
|
|
+ const m = raw.match(/"([^"]*eyJ[^"]*)"/);
|
|
|
|
|
+ if (m) return m[1];
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (_) {
|
|
|
|
|
+ // Ignore storage access errors.
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function requestJson(url, options) {
|
|
|
|
|
+ const token = getAuthToken();
|
|
|
|
|
+ const headers = { "X-Requested-With": "XMLHttpRequest" };
|
|
|
|
|
+ if (token) headers["Authorization"] = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
|
|
|
|
|
+ const response = await fetch(url, Object.assign({
|
|
|
|
|
+ credentials: "include",
|
|
|
|
|
+ headers
|
|
|
|
|
+ }, options));
|
|
|
|
|
+ if (!response.ok) throw new Error(`接口请求失败(HTTP ${response.status})`);
|
|
|
|
|
+ return response.json();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取学期列表,返回 { id, year, term, beginDate, endDate, active }
|
|
|
|
|
+async function fetchSemesters() {
|
|
|
|
|
+ const payload = await requestJson("https://zhjw.bbgu.edu.cn/api/resourceapi/session/list");
|
|
|
|
|
+ const list = payload && Array.isArray(payload.sessionVOList) ? payload.sessionVOList : [];
|
|
|
|
|
+ return list.filter(s => s && s.id && s.year && s.term);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取作息时间(第1-N节)
|
|
|
|
|
+async function fetchTimeSlots(sessionId) {
|
|
|
|
|
+ const payload = await requestJson(
|
|
|
|
|
+ `https://zhjw.bbgu.edu.cn/api/resourceapi/timePattern/get-large-period?sessionId=${encodeURIComponent(sessionId)}`
|
|
|
|
|
+ );
|
|
|
|
|
+ const patterns = payload && Array.isArray(payload.data) ? payload.data : [];
|
|
|
|
|
+ const slots = [];
|
|
|
|
|
+ for (const group of patterns) {
|
|
|
|
|
+ const list = Array.isArray(group.periodList) ? group.periodList : [];
|
|
|
|
|
+ for (const p of list) {
|
|
|
|
|
+ const number = Number(p.smallPeriod);
|
|
|
|
|
+ if (!number || !p.startTime || !p.endTime) continue;
|
|
|
|
|
+ slots.push({ number, startTime: p.startTime, endTime: p.endTime });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return slots.sort((a, b) => a.number - b.number);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取课表并解析
|
|
|
|
|
+async function fetchCourses(sessionId) {
|
|
|
|
|
+ window.shiguangBridge.showToast("正在请求课表数据...");
|
|
|
|
|
+ const payload = await requestJson(
|
|
|
|
|
+ `https://zhjw.bbgu.edu.cn/api/timetable/class/timetable/student/my-table-detail?sessionId=${encodeURIComponent(sessionId)}`,
|
|
|
|
|
+ { method: "POST", credentials: "include" }
|
|
|
|
|
+ );
|
|
|
|
|
+ const rows = payload && Array.isArray(payload.classTimetableVOList)
|
|
|
|
|
+ ? payload.classTimetableVOList
|
|
|
|
|
+ : (payload && payload.data && Array.isArray(payload.data.classTimetableVOList) ? payload.data.classTimetableVOList : []);
|
|
|
|
|
+ if (rows.length === 0) throw new Error("教务系统返回数据格式异常。");
|
|
|
|
|
+
|
|
|
|
|
+ const rawCourses = rows.map(parseCourse).filter(Boolean);
|
|
|
|
|
+
|
|
|
|
|
+ // 同一门课多处时段会分多条,按组合键去重
|
|
|
|
|
+ const seen = new Set();
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+ for (const c of rawCourses) {
|
|
|
|
|
+ const key = `${c.name}|${c.day}|${c.startSection}|${c.endSection}|${c.weeks.join(',')}|${c.teacher}|${c.position}`;
|
|
|
|
|
+ if (seen.has(key)) continue;
|
|
|
|
|
+ seen.add(key);
|
|
|
|
|
+ courses.push(c);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (courses.length === 0) throw new Error("未找到包含有效时间的课程。");
|
|
|
|
|
+ courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
|
|
|
|
|
+ return courses;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 判断是否处于登录页
|
|
|
|
|
+function isLoginPage() {
|
|
|
|
|
+ const url = window.location.href;
|
|
|
|
|
+ return url.includes("authserver") && url.includes("login");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 选择学期,默认定位到当前激活学期
|
|
|
|
|
+async function selectSemester() {
|
|
|
|
|
+ const list = await fetchSemesters();
|
|
|
|
|
+ if (list.length === 0) throw new Error("未获取到学期列表。");
|
|
|
|
|
+
|
|
|
|
|
+ const defaultIndex = Math.max(0, list.findIndex(s => s.active === "Y"));
|
|
|
|
|
+ const names = list.map(s => `${s.year}${s.term === "春" ? "春季" : "秋季"}学期`);
|
|
|
|
|
+ const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ "请选择学期",
|
|
|
|
|
+ JSON.stringify(names),
|
|
|
|
|
+ defaultIndex
|
|
|
|
|
+ );
|
|
|
|
|
+ if (selectedIndex === null || selectedIndex === undefined) return null;
|
|
|
|
|
+ return list[selectedIndex];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 保存预设作息时间(失败仅告警,不阻断课程导入)
|
|
|
|
|
+async function saveTimeSlots(timeSlots) {
|
|
|
|
|
+ if (!timeSlots || timeSlots.length === 0) return;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ console.log("JS: 作息时间保存成功");
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("JS: 作息时间保存失败:", error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取当前学期最大周次(失败返回 null)
|
|
|
|
|
+async function fetchMaxWeek(sessionId) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const payload = await requestJson(
|
|
|
|
|
+ `https://zhjw.bbgu.edu.cn/api/timetable/course/maxWeek/${encodeURIComponent(sessionId)}`
|
|
|
|
|
+ );
|
|
|
|
|
+ const weeks = Number(payload && payload.data);
|
|
|
|
|
+ return weeks > 0 ? weeks : null;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("JS: 获取最大周次失败:", error);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 保存课表配置(开学日期、总周数;失败仅告警,不阻断课程导入)
|
|
|
|
|
+async function saveCourseConfig(semester, courses) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const dateDiffDays = (new Date(semester.endDate) - new Date(semester.beginDate)) / 86400000;
|
|
|
|
|
+ const weeksByDate = Math.round(dateDiffDays / 7);
|
|
|
|
|
+ const maxCourseWeek = Math.max(0, ...courses.map(c => Math.max(...c.weeks)));
|
|
|
|
|
+ const maxWeek = await fetchMaxWeek(semester.id);
|
|
|
|
|
+ const config = {
|
|
|
|
|
+ semesterStartDate: semester.beginDate,
|
|
|
|
|
+ semesterTotalWeeks: maxWeek || Math.max(maxCourseWeek, weeksByDate || 20)
|
|
|
|
|
+ };
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
|
|
+ console.log(`JS: 课表配置保存成功(开学 ${config.semesterStartDate},共 ${config.semesterTotalWeeks} 周)`);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("JS: 课表配置保存失败:", error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ if (isLoginPage()) {
|
|
|
|
|
+ window.shiguangBridge.showToast("导入失败:请先登录教务系统!");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const alertConfirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "教务系统课表导入",
|
|
|
|
|
+ "导入前请确保您已登录教务系统,建议在课表页面进行导入",
|
|
|
|
|
+ "好的,开始导入"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!alertConfirmed) {
|
|
|
|
|
+ window.shiguangBridge.showToast("用户取消了导入。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast("正在获取学期信息...");
|
|
|
|
|
+ try {
|
|
|
|
|
+ const semester = await selectSemester();
|
|
|
|
|
+ if (!semester) {
|
|
|
|
|
+ window.shiguangBridge.showToast("用户取消了导入。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ console.log(`JS: 选择学期: ${semester.year}${semester.term} (${semester.id})`);
|
|
|
|
|
+
|
|
|
|
|
+ const courses = await fetchCourses(semester.id);
|
|
|
|
|
+ window.shiguangBridge.showToast(`正在保存 ${courses.length} 门课程...`);
|
|
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses, null, 2));
|
|
|
|
|
+
|
|
|
|
|
+ await saveCourseConfig(semester, courses);
|
|
|
|
|
+
|
|
|
|
|
+ const timeSlots = await fetchTimeSlots(semester.id);
|
|
|
|
|
+ await saveTimeSlots(timeSlots);
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
|
|
|
|
|
+ console.error("JS: Import Error:", error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+runImportFlow();
|