|
@@ -0,0 +1,430 @@
|
|
|
|
|
+// 文件: cqie_01.js
|
|
|
|
|
+// 重庆工程学院(cqie.edu.cn)拾光课程表适配脚本
|
|
|
|
|
+// 适配新教务系统(njw.cqie.edu.cn),登录门户 i.cqie.edu.cn 后进入教务系统页面执行导入
|
|
|
|
|
+// 导入前可选择学期,默认最新学期
|
|
|
|
|
+// 出现问题请提交 pr 更改,这更加快速
|
|
|
|
|
+
|
|
|
|
|
+// 隔离所有声明,允许脚本在同一 WebView 中失败或取消后再次执行
|
|
|
|
|
+(async () => {
|
|
|
|
|
+const HOST = 'njw.cqie.edu.cn';
|
|
|
|
|
+const RESOURCE_API = 'https://njw.cqie.edu.cn/api/resourceapi';
|
|
|
|
|
+const TIMETABLE_API = 'https://njw.cqie.edu.cn/api/timetable';
|
|
|
|
|
+const USER_API = 'https://njw.cqie.edu.cn/api/userserver';
|
|
|
|
|
+const AUTH_API = 'https://njw.cqie.edu.cn/authserver';
|
|
|
|
|
+
|
|
|
|
|
+// 注意:本校新教务平台的存储前缀沿用厂商默认的 "cqu_edu_"
|
|
|
|
|
+
|
|
|
|
|
+// 读取前端存储的访问令牌(兼容 localStorage / sessionStorage,去除引号)
|
|
|
|
|
+function getAccessToken() {
|
|
|
|
|
+ const keys = ['cqu_edu_ACCESS_TOKEN', 'cqu_edu_CURRENT_TOKEN'];
|
|
|
|
|
+ for (const store of [window.localStorage, window.sessionStorage]) {
|
|
|
|
|
+ for (const key of keys) {
|
|
|
|
|
+ const value = store.getItem(key);
|
|
|
|
|
+ if (value) return value.replaceAll('"', '');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 从前端缓存的用户信息中读取学号
|
|
|
|
|
+// 缓存位置:sessionStorage 裸键 "USER_INFO"(登录服务写入)及各存储中 "*_USER_INFO" 键
|
|
|
|
|
+function getCachedStudentId() {
|
|
|
|
|
+ for (const store of [window.localStorage, window.sessionStorage]) {
|
|
|
|
|
+ for (const key of Object.keys(store)) {
|
|
|
|
|
+ if (key !== 'USER_INFO' && !key.endsWith('_USER_INFO')) continue;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const info = JSON.parse(store.getItem(key));
|
|
|
|
|
+ if (!info || typeof info !== 'object') continue;
|
|
|
|
|
+ const sid = info.code ?? info.userCode;
|
|
|
|
|
+ if (isPlausibleStudentId(sid)) return String(sid);
|
|
|
|
|
+ } catch (e) { /* 缓存内容不是合法 JSON,跳过 */ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 过滤掉疑似响应状态码的值(200/404 等),避免把接口包装层的 code 误认成学号
|
|
|
|
|
+function isPlausibleStudentId(value) {
|
|
|
|
|
+ if (value === null || value === undefined) return false;
|
|
|
|
|
+ const text = String(value).trim();
|
|
|
|
|
+ if (text.length < 4) return false;
|
|
|
|
|
+ return !['0', '200', '401', '403', '404', '500'].includes(text);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 通过登录服务的用户信息接口获取学号(与前端 simple-user 接口一致)
|
|
|
|
|
+async function fetchRemoteStudentId() {
|
|
|
|
|
+ const json = await apiFetch(`${AUTH_API}/simple-user`, {}, "获取学号");
|
|
|
|
|
+ for (const info of [json, unwrap(json)]) {
|
|
|
|
|
+ if (!info || typeof info !== 'object') continue;
|
|
|
|
|
+ const sid = info.code ?? info.userCode ?? info.account;
|
|
|
|
|
+ if (isPlausibleStudentId(sid)) return String(sid);
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 学号获取:缓存 -> 登录服务接口 -> 手动输入兜底
|
|
|
|
|
+async function getStudentId() {
|
|
|
|
|
+ const cached = getCachedStudentId();
|
|
|
|
|
+ if (cached) return cached;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const remote = await fetchRemoteStudentId();
|
|
|
|
|
+ if (remote) return remote;
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn("获取学号接口失败:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ return await promptStudentId();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 学号输入校验(供 showPrompt 按名回调使用;桥接在页面全局作用域查找函数,必须挂到 window 上)
|
|
|
|
|
+window.validateStudentId = function (studentId) {
|
|
|
|
|
+ if (studentId === null || studentId.trim().length === 0) {
|
|
|
|
|
+ return "学号不能为空!";
|
|
|
|
|
+ }
|
|
|
|
|
+ return false;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 兜底:通过弹窗让用户手动输入学号
|
|
|
|
|
+async function promptStudentId() {
|
|
|
|
|
+ const input = await window.shiguangBridgePromise.showPrompt(
|
|
|
|
|
+ "输入学号",
|
|
|
|
|
+ "未能自动识别学号,请输入你的学号:",
|
|
|
|
|
+ "",
|
|
|
|
|
+ "validateStudentId"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (input === null) throw new Error("用户取消了学号输入");
|
|
|
|
|
+ return input.trim();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 通用请求封装,携带登录令牌;响应体兼容 JSON/空体/非 JSON(如网关异常页)
|
|
|
|
|
+async function apiFetch(url, options = {}, description = "数据") {
|
|
|
|
|
+ const response = await fetch(url, {
|
|
|
|
|
+ ...options,
|
|
|
|
|
+ credentials: 'include',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ 'Authorization': `Bearer ${getAccessToken()}`,
|
|
|
|
|
+ ...(options.headers || {}),
|
|
|
|
|
+ },
|
|
|
|
|
+ });
|
|
|
|
|
+ if (response.status === 401 || response.status === 403) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`${description}失败,登录已失效,请退出重试`);
|
|
|
|
|
+ throw new Error(`${description}失败: 登录状态失效 (${response.status})`);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`${description}失败,请稍后重试`);
|
|
|
|
|
+ throw new Error(`${description}失败: ${response.status} ${response.statusText}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ const text = await response.text();
|
|
|
|
|
+ if (!text || !text.trim()) return null;
|
|
|
|
|
+ try {
|
|
|
|
|
+ return JSON.parse(text);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn(`${description}:响应不是 JSON(前 120 字符):`, text.slice(0, 120));
|
|
|
|
|
+ return { raw: text };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 兼容两种响应结构:{code, data: {...}} 或扁平结构
|
|
|
|
|
+const unwrap = (json) => (json && typeof json.data !== 'undefined' ? json.data : json);
|
|
|
|
|
+
|
|
|
|
|
+// 获取学期列表(含系统当前学期 ID)
|
|
|
|
|
+// 优先取"已发布课表"的学期(前端课表页的选择器即用此接口),避免列出全校全量学期;
|
|
|
|
|
+// 该接口失败时回退到 info-detail 的 sessionFinder 全量列表。
|
|
|
|
|
+// session/list 仅用于补充开始日期(排序用),失败不影响流程。
|
|
|
|
|
+async function getSessionInfo() {
|
|
|
|
|
+ const json = await apiFetch(`${RESOURCE_API}/session/info-detail`, {}, "获取学期信息");
|
|
|
|
|
+ const data = unwrap(json) || {};
|
|
|
|
|
+ let sessions = [];
|
|
|
|
|
+ try {
|
|
|
|
|
+ const releaseJson = await apiFetch(`${TIMETABLE_API}/optionFinder/session-release-schedule`, {}, "获取已发布课表的学期");
|
|
|
|
|
+ const release = unwrap(releaseJson) ?? [];
|
|
|
|
|
+ if (Array.isArray(release) && release.length > 0) {
|
|
|
|
|
+ sessions = release
|
|
|
|
|
+ .filter((session) => session && session.id != null)
|
|
|
|
|
+ .map((session) => ({ id: String(session.id), name: session.name ?? String(session.id), beginDate: session.beginDate ?? null }));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn("获取已发布课表学期失败,回退全量学期列表:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sessions.length === 0) {
|
|
|
|
|
+ sessions = (json.sessionFinder ?? data.sessionFinder ?? [])
|
|
|
|
|
+ .filter((session) => session && session.id != null && session.id !== '')
|
|
|
|
|
+ .map((session) => ({ id: String(session.id), name: session.name ?? String(session.id), beginDate: null }));
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ const listJson = await apiFetch(`${RESOURCE_API}/session/list`, {}, "获取学期列表");
|
|
|
|
|
+ const list = unwrap(listJson)?.sessionVOList ?? [];
|
|
|
|
|
+ if (list.length > 0) {
|
|
|
|
|
+ const byId = new Map(list.map((session) => [String(session.id), session]));
|
|
|
|
|
+ sessions = sessions.map((session) => ({ ...session, beginDate: session.beginDate ?? byId.get(session.id)?.beginDate ?? null }));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn("获取学期列表失败,将按学期名称排序:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ const curSessionId = json.curSessionId ?? data.curSessionId ?? data.id ?? json.id;
|
|
|
|
|
+ return { curSessionId: curSessionId == null ? null : String(curSessionId), sessions };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 学期排序键(数值越大越新):优先学期开始日期(兼容带时间的日期串),
|
|
|
|
|
+// 否则解析名称中的学年与学期序号(如 "2025-2026学年第二学期"、"2026秋")
|
|
|
|
|
+const TERM_NUM = { "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "春": 1, "夏": 2, "秋": 3, "冬": 4 };
|
|
|
|
|
+function sessionSortKey(session) {
|
|
|
|
|
+ const dateMatch = session.beginDate ? String(session.beginDate).match(/^(\d{4})-(\d{2})-(\d{2})/) : null;
|
|
|
|
|
+ if (dateMatch) return Number(dateMatch[1] + dateMatch[2] + dateMatch[3]);
|
|
|
|
|
+ const name = session.name || "";
|
|
|
|
|
+ const year = Number((name.match(/\d{4}/) ?? ["0"])[0]);
|
|
|
|
|
+ const termMatch = name.match(/第([一二三四五六七八\d]+)学期|([春夏秋冬])/);
|
|
|
|
|
+ let term = 0;
|
|
|
|
|
+ if (termMatch) {
|
|
|
|
|
+ term = termMatch[1] !== undefined
|
|
|
|
|
+ ? (TERM_NUM[termMatch[1]] ?? parseInt(termMatch[1], 10) ?? 0)
|
|
|
|
|
+ : (TERM_NUM[termMatch[2]] ?? 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ return year * 100 + term;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 弹窗选择学期,返回选中学期;列表已按新到旧排序,默认选中第一项(最新学期)
|
|
|
|
|
+async function selectSession(sessions, curSessionId) {
|
|
|
|
|
+ const labels = sessions.map((session) =>
|
|
|
|
|
+ session.id === curSessionId ? `${session.name}(当前学期)` : session.name
|
|
|
|
|
+ );
|
|
|
|
|
+ const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择要导入的学期",
|
|
|
|
|
+ JSON.stringify(labels),
|
|
|
|
|
+ 0
|
|
|
|
|
+ );
|
|
|
|
|
+ if (selectedIndex === null || selectedIndex < 0 || selectedIndex >= sessions.length) {
|
|
|
|
|
+ return null; // 用户取消
|
|
|
|
|
+ }
|
|
|
|
|
+ return sessions[selectedIndex];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 切换服务端当前学期:课表接口按服务端会话学期返回数据,忽略 sessionId 参数。
|
|
|
|
|
+// 注意:前端调用此接口用的是普通 post(表单格式)而非 postJson(JSON),
|
|
|
|
|
+// body 必须是 urlencoded 的 curSessionId,发 JSON 会被后端静默忽略(返回 200 但不切换)
|
|
|
|
|
+async function switchSession(sessionId) {
|
|
|
|
|
+ const json = await apiFetch(`${USER_API}/user-switch-session`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ body: `curSessionId=${encodeURIComponent(sessionId)}`,
|
|
|
|
|
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
|
|
|
+ }, "切换学期");
|
|
|
|
|
+ if (json && json.status === 'error') {
|
|
|
|
|
+ window.shiguangBridge.showToast(`切换学期失败(${json.msg ?? '未知错误'}),请重试`);
|
|
|
|
|
+ throw new Error(`切换学期失败: ${json.msg ?? '未知错误'}`);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取学期开始日期(规范化为 yyyy-MM-dd,App 无法解析带时间的日期串)
|
|
|
|
|
+async function getStartDate(sessionId) {
|
|
|
|
|
+ const json = await apiFetch(`${RESOURCE_API}/session/detail/${sessionId}`, {}, "获取学期起止信息");
|
|
|
|
|
+ const data = unwrap(json) || {};
|
|
|
|
|
+ const raw = data.beginDate ?? json.beginDate ?? null;
|
|
|
|
|
+ const matched = raw ? String(raw).match(/^(\d{4}-\d{2}-\d{2})/) : null;
|
|
|
|
|
+ return matched ? matched[1] : null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取学期最大周数
|
|
|
|
|
+async function getMaxWeek(sessionId) {
|
|
|
|
|
+ const json = await apiFetch(`${TIMETABLE_API}/course/maxWeek/${sessionId}`, {}, "获取最大周数");
|
|
|
|
|
+ const maxWeek = Number(unwrap(json));
|
|
|
|
|
+ return Number.isFinite(maxWeek) && maxWeek > 0 ? maxWeek : null;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取大节时间配置(periodList 内字段为 smallPeriod/startTime/endTime)
|
|
|
|
|
+async function getTimeSlots() {
|
|
|
|
|
+ const json = await apiFetch(`${RESOURCE_API}/timePattern/get-large-period`, {}, "获取时间段配置");
|
|
|
|
|
+ const groups = unwrap(json) || [];
|
|
|
|
|
+ let periods = (groups[0] && groups[0].periodList) || [];
|
|
|
|
|
+ if (periods.length === 0 && groups.length > 0) {
|
|
|
|
|
+ periods = groups.flatMap((group) => group.periodList || []);
|
|
|
|
|
+ }
|
|
|
|
|
+ return periods.map((period, index) => ({
|
|
|
|
|
+ number: period.smallPeriod ?? index + 1,
|
|
|
|
|
+ startTime: period.startTime ?? '',
|
|
|
|
|
+ endTime: period.endTime ?? '',
|
|
|
|
|
+ }));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取指定学期、指定学号的课表原始数据
|
|
|
|
|
+// 优先 stu/schedule-detail(我的课表页使用),为空或失败时回退 student/my-table-detail,
|
|
|
|
|
+// 两个接口参数与返回结构一致(classTimetableVOList)
|
|
|
|
|
+async function fetchScheduleList(url, studentId, description) {
|
|
|
|
|
+ const json = await apiFetch(url, { method: 'POST', body: JSON.stringify([studentId]) }, description);
|
|
|
|
|
+ const payload = unwrap(json) || {};
|
|
|
|
|
+ return payload.classTimetableVOList ?? json.classTimetableVOList ?? [];
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function getSchedule(sessionId, studentId) {
|
|
|
|
|
+ const primary = `${TIMETABLE_API}/class/timetable/stu/schedule-detail?sessionId=${sessionId}`;
|
|
|
|
|
+ const fallback = `${TIMETABLE_API}/class/timetable/student/my-table-detail?sessionId=${sessionId}`;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const list = await fetchScheduleList(primary, studentId, "获取课程表");
|
|
|
|
|
+ if (list.length > 0) return list;
|
|
|
|
|
+ console.warn("stu/schedule-detail 返回为空,尝试备用接口");
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn("stu/schedule-detail 查询失败,尝试备用接口:", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ return await fetchScheduleList(fallback, studentId, "获取课程表(备用)");
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 解析周次字符串,与前端逻辑一致:支持 "1-16"、"1,3,5"、"1-8,10-16" 等
|
|
|
|
|
+function parseWeeks(weekFormat) {
|
|
|
|
|
+ const weeks = [];
|
|
|
|
|
+ if (!weekFormat) return weeks;
|
|
|
|
|
+ const parts = String(weekFormat).replace(/,/g, ',').split(',');
|
|
|
|
|
+ for (const part of parts) {
|
|
|
|
|
+ const range = part.split('-');
|
|
|
|
|
+ const start = parseInt(range[0], 10);
|
|
|
|
|
+ const end = parseInt(range[1], 10);
|
|
|
|
|
+ if (range.length > 1) {
|
|
|
|
|
+ if (isNaN(start) || isNaN(end)) continue;
|
|
|
|
|
+ const [from, to] = start > end ? [end, start] : [start, end];
|
|
|
|
|
+ for (let week = from; week <= to; week++) {
|
|
|
|
|
+ if (week > 0) weeks.push(week);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (range[0] !== '' && !isNaN(start) && start > 0) {
|
|
|
|
|
+ weeks.push(start);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 兼容旧版二进制周次串(如 "101010...")
|
|
|
|
|
+function parseBinaryWeeks(binary) {
|
|
|
|
|
+ if (typeof binary !== 'string' || !/^[01]+$/.test(binary)) return [];
|
|
|
|
|
+ return [...binary].reduce((weeks, char, index) => {
|
|
|
|
|
+ if (char === '1') weeks.push(index + 1);
|
|
|
|
|
+ return weeks;
|
|
|
|
|
+ }, []);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 解析节次字符串(如 "1-2" 或 "3")
|
|
|
|
|
+function parsePeriodFormat(periodFormat) {
|
|
|
|
|
+ if (!periodFormat) return null;
|
|
|
|
|
+ const parts = String(periodFormat).trim().split('-');
|
|
|
|
|
+ const start = parseInt(parts[0], 10);
|
|
|
|
|
+ const end = parts.length > 1 ? parseInt(parts[1], 10) : start;
|
|
|
|
|
+ if (isNaN(start) || isNaN(end)) return null;
|
|
|
|
|
+ return { start, end };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 清理教师名:去掉工号与 "[主讲];" 等标记(如 "罗桓-03068[主讲];" -> "罗桓")
|
|
|
|
|
+function cleanTeacher(raw) {
|
|
|
|
|
+ const text = String(raw ?? '').trim();
|
|
|
|
|
+ if (!text) return '';
|
|
|
|
|
+ const names = text.split(/[;,,;]/)
|
|
|
|
|
+ .map((part) => part.replace(/\[[^\]]*\]/g, '').replace(/-\d+$/, '').trim())
|
|
|
|
|
+ .filter(Boolean);
|
|
|
|
|
+ return names.length > 0 ? names.join(',') : text;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 将课表数据转换为拾光课程表格式
|
|
|
|
|
+function parseSchedule(schedule) {
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+ for (const item of schedule) {
|
|
|
|
|
+ if (!item || !item.courseName) continue;
|
|
|
|
|
+ const period = parsePeriodFormat(item.periodFormat);
|
|
|
|
|
+ let weeks = parseWeeks(item.teachingWeekFormat);
|
|
|
|
|
+ if (weeks.length === 0) weeks = parseBinaryWeeks(item.teachingWeek);
|
|
|
|
|
+ if (!period || weeks.length === 0) continue;
|
|
|
|
|
+ const day = Number(item.weekDay) || 0;
|
|
|
|
|
+ if (day < 1 || day > 7) continue;
|
|
|
|
|
+ const [startSection, endSection] = period.start <= period.end
|
|
|
|
|
+ ? [period.start, period.end]
|
|
|
|
|
+ : [period.end, period.start];
|
|
|
|
|
+ courses.push({
|
|
|
|
|
+ name: item.courseName,
|
|
|
|
|
+ teacher: cleanTeacher(item.instructorName),
|
|
|
|
|
+ position: item.position || item.roomName || '',
|
|
|
|
|
+ day,
|
|
|
|
|
+ startSection,
|
|
|
|
|
+ endSection,
|
|
|
|
|
+ weeks,
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return courses;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 保存课表数据到拾光课程表
|
|
|
|
|
+async function saveSchedule(parsedSchedule) {
|
|
|
|
|
+ const tasks = [];
|
|
|
|
|
+ const config = {};
|
|
|
|
|
+ if (parsedSchedule.startDate) config.semesterStartDate = parsedSchedule.startDate;
|
|
|
|
|
+ if (parsedSchedule.maxWeek) config.semesterTotalWeeks = parsedSchedule.maxWeek;
|
|
|
|
|
+ if (Object.keys(config).length > 0) {
|
|
|
|
|
+ tasks.push(window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config)));
|
|
|
|
|
+ }
|
|
|
|
|
+ if (parsedSchedule.timeSlots.length > 0) {
|
|
|
|
|
+ tasks.push(window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(parsedSchedule.timeSlots)));
|
|
|
|
|
+ }
|
|
|
|
|
+ tasks.push(window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedSchedule.courses)));
|
|
|
|
|
+ const results = await Promise.allSettled(tasks);
|
|
|
|
|
+ const failed = results.filter((result) => result.status === 'rejected');
|
|
|
|
|
+ if (failed.length > 0) {
|
|
|
|
|
+ console.error("保存过程出现错误:", failed.map((result) => result.reason));
|
|
|
|
|
+ window.shiguangBridge.showToast("部分数据保存失败,请查看日志后重试");
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function main() {
|
|
|
|
|
+ if (window.location.hostname !== HOST) {
|
|
|
|
|
+ window.shiguangBridge.showToast("请先登录并进入重庆工程学院新教务系统页面,再执行导入!");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!getAccessToken()) {
|
|
|
|
|
+ window.shiguangBridge.showToast("尚未登录新教务系统,请先登录!");
|
|
|
|
|
+ throw new Error("未检测到登录状态");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取学期列表,按新到旧排序后弹窗选择,默认最新学期
|
|
|
|
|
+ const { curSessionId, sessions } = await getSessionInfo();
|
|
|
|
|
+ if (sessions.length === 0) {
|
|
|
|
|
+ window.shiguangBridge.showToast("未获取到学期列表,请稍后重试");
|
|
|
|
|
+ throw new Error("学期列表为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ sessions.sort((a, b) => sessionSortKey(b) - sessionSortKey(a));
|
|
|
|
|
+ const chosen = await selectSession(sessions, curSessionId);
|
|
|
|
|
+ if (!chosen) return;
|
|
|
|
|
+
|
|
|
|
|
+ const confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "教务系统课表导入",
|
|
|
|
|
+ `将导入「${chosen.name}」学期的课表`,
|
|
|
|
|
+ "好的,开始导入"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!confirmed) return;
|
|
|
|
|
+
|
|
|
|
|
+ // 先切换服务端当前学期,否则课表接口会返回系统默认学期的数据
|
|
|
|
|
+ await switchSession(chosen.id);
|
|
|
|
|
+
|
|
|
|
|
+ const studentId = await getStudentId();
|
|
|
|
|
+ const sessionId = chosen.id;
|
|
|
|
|
+ const [startDate, maxWeek, timeSlots, schedule] = await Promise.all([
|
|
|
|
|
+ getStartDate(sessionId),
|
|
|
|
|
+ getMaxWeek(sessionId),
|
|
|
|
|
+ getTimeSlots(),
|
|
|
|
|
+ getSchedule(sessionId, studentId),
|
|
|
|
|
+ ]);
|
|
|
|
|
+
|
|
|
|
|
+ const courses = parseSchedule(schedule);
|
|
|
|
|
+ if (courses.length === 0) {
|
|
|
|
|
+ window.shiguangBridge.showToast("未解析到课程数据,请确认所选学期有课表后再试");
|
|
|
|
|
+ throw new Error("课表数据为空");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const success = await saveSchedule({ startDate, maxWeek, timeSlots, courses });
|
|
|
|
|
+ if (success) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`「${chosen.name}」导入成功,共 ${courses.length} 条课程记录!`);
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 兜底捕获所有未处理异常,避免导入失败时无任何提示
|
|
|
|
|
+main().catch((e) => {
|
|
|
|
|
+ console.error("导入过程出现错误:", e);
|
|
|
|
|
+ try {
|
|
|
|
|
+ window.shiguangBridge.showToast(`导入失败:${e?.message ?? e}`);
|
|
|
|
|
+ } catch (_) { /* 桥不可用时仅记录日志 */ }
|
|
|
|
|
+});
|
|
|
|
|
+})();
|