|
@@ -0,0 +1,224 @@
|
|
|
|
|
+// 湖南三一工业职业技术学院(ehall.sanyedu.com)拾光课表导入适配脚本
|
|
|
|
|
+// 金智教务(jwapp/sys/wdkb 模块):
|
|
|
|
|
+// xskcb.do 学生课表:POST XNXQDM=学年学期,返回 datas.xskcb.rows[]
|
|
|
|
|
+// cxjcs.do 学期配置:返回当前学期开学日期(XQKSRQ)与总周数(ZZC)
|
|
|
|
|
+// 每日 10 节(无午休拆分的独立小节,晚饭 17:30-18:30 休息)
|
|
|
|
|
+
|
|
|
|
|
+// 预设作息时间(10 节)
|
|
|
|
|
+const SANY_TIME_SLOTS = [
|
|
|
|
|
+ { number: 1, startTime: "08:20", endTime: "09:05" },
|
|
|
|
|
+ { number: 2, startTime: "09:15", endTime: "10:00" },
|
|
|
|
|
+ { number: 3, startTime: "10:20", endTime: "11:05" },
|
|
|
|
|
+ { number: 4, startTime: "11:15", endTime: "12:00" },
|
|
|
|
|
+ { number: 5, startTime: "14:00", endTime: "14:45" },
|
|
|
|
|
+ { number: 6, startTime: "14:55", endTime: "15:40" },
|
|
|
|
|
+ { number: 7, startTime: "15:50", endTime: "16:35" },
|
|
|
|
|
+ { number: 8, startTime: "16:45", endTime: "17:30" },
|
|
|
|
|
+ { number: 9, startTime: "19:00", endTime: "19:45" },
|
|
|
|
|
+ { number: 10, startTime: "19:55", endTime: "20:40" }
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+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.
|
|
|
|
|
+ }
|
|
|
|
|
+ return "未知错误";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 从当前页面 URL 提取 /jwapp 前缀(含 ehall 域名与可选哈希路径)
|
|
|
|
|
+function getApiBase() {
|
|
|
|
|
+ const m = window.location.href.match(/^(https?:\/\/[^/]+(?:[^?#]*)?)\/jwapp/);
|
|
|
|
|
+ return m ? m[1] : window.location.origin;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const API_BASE = getApiBase();
|
|
|
|
|
+const MODULES = {
|
|
|
|
|
+ table: `${API_BASE}/jwapp/sys/wdkb/modules/xskcb/xskcb.do?enlink-vpn`,
|
|
|
|
|
+ semester: `${API_BASE}/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do?enlink-vpn`
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 从页面存储中提取 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.
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ 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 postForm(url, params) {
|
|
|
|
|
+ const token = getAuthToken();
|
|
|
|
|
+ const headers = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" };
|
|
|
|
|
+ if (token) headers["Authorization"] = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
|
|
|
|
|
+ const response = await fetch(url, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ credentials: "include",
|
|
|
|
|
+ headers,
|
|
|
|
|
+ body: new URLSearchParams(params || {}).toString()
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!response.ok) throw new Error(`接口请求失败(HTTP ${response.status})`);
|
|
|
|
|
+ return response.json();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 解析 SKZC 周次位图:"00001111111111111111" 从第5位起为1 → 5..20
|
|
|
|
|
+function parseWeekBitmap(bitmap) {
|
|
|
|
|
+ const weeks = [];
|
|
|
|
|
+ const s = String(bitmap || "");
|
|
|
|
|
+ for (let i = 0; i < s.length; i++) {
|
|
|
|
|
+ if (s.charAt(i) === "1") weeks.push(i + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return [...new Set(weeks)].sort((a, b) => a - b);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取当前学期配置:cxjcs.do 返回全部学期(无 SFSY 等可区分字段),
|
|
|
|
|
+// 按开学日期(XQKSRQ)取"最近开学且不晚于今天"的学期,得 XN/XQ/XQKSRQ/ZZC
|
|
|
|
|
+async function fetchSemesterConfig() {
|
|
|
|
|
+ const payload = await postForm(MODULES.semester);
|
|
|
|
|
+ const rows = payload && payload.datas && payload.datas.cxjcs && payload.datas.cxjcs.rows;
|
|
|
|
|
+ if (!Array.isArray(rows) || rows.length === 0) {
|
|
|
|
|
+ throw new Error("未获取到学期配置信息");
|
|
|
|
|
+ }
|
|
|
|
|
+ const today = new Date();
|
|
|
|
|
+ let best = null;
|
|
|
|
|
+ let bestDate = -Infinity;
|
|
|
|
|
+ for (const s of rows) {
|
|
|
|
|
+ const start = new Date(String(s.XQKSRQ || "").slice(0, 10) + "T00:00:00");
|
|
|
|
|
+ if (isNaN(start.getTime()) || start > today) continue;
|
|
|
|
|
+ const t = start.getTime();
|
|
|
|
|
+ if (t > bestDate) {
|
|
|
|
|
+ bestDate = t;
|
|
|
|
|
+ best = s;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!best) {
|
|
|
|
|
+ // 兜底:全部开学日期都晚于今天(如学期刚开始数据未刷新),取可用的最近一条
|
|
|
|
|
+ best = rows[0];
|
|
|
|
|
+ }
|
|
|
|
|
+ const xn = String(best.XN || "").trim();
|
|
|
|
|
+ const xq = String(best.XQ || "").trim();
|
|
|
|
|
+ if (!xn || !xq) throw new Error("学期配置缺少学年学期");
|
|
|
|
|
+ const semesterStart = String(best.XQKSRQ || "").slice(0, 10);
|
|
|
|
|
+ const totalWeeks = Number(best.ZZC) || Number(best.ZJXZC) || 0;
|
|
|
|
|
+ return { xnxqdm: `${xn}-${xq}`, semesterStartDate: semesterStart, totalWeeks };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function fetchCourses(xnxqdm) {
|
|
|
|
|
+ const payload = await postForm(MODULES.table, { XNXQDM: xnxqdm });
|
|
|
|
|
+ const rows = payload && payload.datas && payload.datas.xskcb && payload.datas.xskcb.rows;
|
|
|
|
|
+ if (!Array.isArray(rows)) return [];
|
|
|
|
|
+ return rows;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function parseSchedule(rows) {
|
|
|
|
|
+ const courses = [];
|
|
|
|
|
+ for (const row of rows) {
|
|
|
|
|
+ const name = String(row.KCM || "").trim();
|
|
|
|
|
+ if (!name) continue;
|
|
|
|
|
+ const day = Number(row.SKXQ);
|
|
|
|
|
+ const startSection = Number(row.KSJC);
|
|
|
|
|
+ const endSection = Number(row.JSJC);
|
|
|
|
|
+ if (!(day >= 1 && day <= 7) || !startSection || !endSection || endSection < startSection) continue;
|
|
|
|
|
+ const weeks = parseWeekBitmap(row.SKZC);
|
|
|
|
|
+ if (weeks.length === 0) continue;
|
|
|
|
|
+ courses.push({
|
|
|
|
|
+ name,
|
|
|
|
|
+ teacher: String(row.SKJS || "").trim() || "未知",
|
|
|
|
|
+ position: String(row.JASMC || "").trim() || "待定",
|
|
|
|
|
+ day,
|
|
|
|
|
+ startSection,
|
|
|
|
|
+ endSection,
|
|
|
|
|
+ weeks
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ return courses;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 合并同课程同时间同教室的条目
|
|
|
|
|
+function mergeCourses(courses) {
|
|
|
|
|
+ const merged = new Map();
|
|
|
|
|
+ for (const c of courses) {
|
|
|
|
|
+ const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}`;
|
|
|
|
|
+ const holder = merged.get(key);
|
|
|
|
|
+ if (holder) {
|
|
|
|
|
+ holder.weeks = Array.from(new Set([...holder.weeks, ...c.weeks])).sort((a, b) => a - b);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ merged.set(key, { ...c, weeks: [...c.weeks].sort((a, b) => a - b) });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return Array.from(merged.values()).sort(
|
|
|
|
|
+ (a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name)
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 保存作息时间
|
|
|
|
|
+async function saveTimeSlots(timeSlots) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("JS: 作息时间保存失败", error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function runImportFlow() {
|
|
|
|
|
+ const alertConfirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
|
|
+ "课表导入",
|
|
|
|
|
+ "导入前请确保已登录并打开课表页面。",
|
|
|
|
|
+ "好的,开始导入"
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!alertConfirmed) {
|
|
|
|
|
+ window.shiguangBridge.showToast("用户取消了导入。");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast("正在获取课表数据...");
|
|
|
|
|
+ try {
|
|
|
|
|
+ const config = await fetchSemesterConfig();
|
|
|
|
|
+ const rows = await fetchCourses(config.xnxqdm);
|
|
|
|
|
+ const courses = parseSchedule(rows);
|
|
|
|
|
+ if (courses.length === 0) throw new Error("课表中未解析到有效课程,请确认本学期有课。");
|
|
|
|
|
+
|
|
|
|
|
+ const merged = mergeCourses(courses);
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast(`正在保存 ${merged.length} 门课程...`);
|
|
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(merged, null, 2));
|
|
|
|
|
+
|
|
|
|
|
+ // 保存学期配置(开学日期 + 总周数)
|
|
|
|
|
+ const courseConfig = { semesterTotalWeeks: config.totalWeeks };
|
|
|
|
|
+ if (config.semesterStartDate) courseConfig.semesterStartDate = config.semesterStartDate;
|
|
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(courseConfig));
|
|
|
|
|
+
|
|
|
|
|
+ await saveTimeSlots(SANY_TIME_SLOTS);
|
|
|
|
|
+
|
|
|
|
|
+ window.shiguangBridge.showToast(`课程导入成功,共导入 ${merged.length} 门课程!`);
|
|
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
|
|
|
|
|
+ console.error("JS: Import Error", error);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+runImportFlow();
|