| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286 |
- // 北部湾大学智慧教务系统(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();
|