|
|
@@ -0,0 +1,357 @@
|
|
|
+// 哈尔滨工程大学(hrbeu.edu.cn) 本科课表适配脚本
|
|
|
+// 金智教务(Wisedu EMAP)平台,参考官方金智适配案例
|
|
|
+// 支持:学期选择、实验课、调课/停课、API作息时间、开学日期
|
|
|
+
|
|
|
+var HEU_API = {
|
|
|
+ semesterList: "/jwapp/sys/wdkb/modules/jshkcb/xnxqcx.do",
|
|
|
+ currentTerm: "/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do",
|
|
|
+ course: "/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do",
|
|
|
+ experiment: "/jwapp/sys/wdkb/modules/syjxkcb/cxsyjxxskb.do",
|
|
|
+ change: "/jwapp/sys/wdkb/modules/xskcb/xsdkkc.do",
|
|
|
+ timeSlots: "/jwapp/sys/wdkb/modules/jshkcb/jc.do",
|
|
|
+ config: "/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do"
|
|
|
+};
|
|
|
+
|
|
|
+var DEFAULT_TIME_SLOTS = [
|
|
|
+ { number: 1, startTime: "08:00", endTime: "08:45" },
|
|
|
+ { number: 2, startTime: "08:50", endTime: "09:35" },
|
|
|
+ { number: 3, startTime: "09:55", endTime: "10:40" },
|
|
|
+ { number: 4, startTime: "10:45", endTime: "11:30" },
|
|
|
+ { number: 5, startTime: "11:35", endTime: "12:20" },
|
|
|
+ { number: 6, startTime: "13:30", endTime: "14:15" },
|
|
|
+ { number: 7, startTime: "14:20", endTime: "15:05" },
|
|
|
+ { number: 8, startTime: "15:25", endTime: "16:10" },
|
|
|
+ { number: 9, startTime: "16:15", endTime: "17:00" },
|
|
|
+ { number: 10, startTime: "17:05", endTime: "17:50" },
|
|
|
+ { number: 11, startTime: "18:30", endTime: "19:15" },
|
|
|
+ { number: 12, startTime: "19:20", endTime: "20:05" },
|
|
|
+ { number: 13, startTime: "20:10", endTime: "20:55" }
|
|
|
+];
|
|
|
+
|
|
|
+var HEADERS = {
|
|
|
+ "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
|
+ "x-requested-with": "XMLHttpRequest"
|
|
|
+};
|
|
|
+
|
|
|
+async function postApi(url, body) {
|
|
|
+ var options = { headers: HEADERS, method: "POST", credentials: "include" };
|
|
|
+ if (body) options.body = body;
|
|
|
+ var resp = await fetch(url, options);
|
|
|
+ if (!resp.ok) throw new Error("请求失败: " + resp.status);
|
|
|
+ return await resp.json();
|
|
|
+}
|
|
|
+
|
|
|
+function getRows(data, key) {
|
|
|
+ return (data && data.datas && data.datas[key] && Array.isArray(data.datas[key].rows))
|
|
|
+ ? data.datas[key].rows : [];
|
|
|
+}
|
|
|
+
|
|
|
+function parseWeeksBitmap(skzc) {
|
|
|
+ var weeks = [];
|
|
|
+ var bits = String(skzc || "");
|
|
|
+ for (var i = 0; i < bits.length; i++) {
|
|
|
+ if (bits[i] === "1") weeks.push(i + 1);
|
|
|
+ }
|
|
|
+ return weeks;
|
|
|
+}
|
|
|
+
|
|
|
+function parseSingleCourse(raw) {
|
|
|
+ var isExperiment = raw.SYXMMC !== undefined && raw.SYXMMC !== null;
|
|
|
+ var name = isExperiment ? raw.KCM + " - " + raw.SYXMMC : raw.KCM;
|
|
|
+ if (!isExperiment && raw.TYXMDM_DISPLAY) name += "(" + raw.TYXMDM_DISPLAY + ")";
|
|
|
+
|
|
|
+ var day = parseInt(raw.SKXQ);
|
|
|
+ var startSection = parseInt(raw.KSJC);
|
|
|
+ var endSection = parseInt(raw.JSJC);
|
|
|
+ var weeks = parseWeeksBitmap(raw.SKZC);
|
|
|
+
|
|
|
+ if (!name || !day || !startSection || !endSection || weeks.length === 0) return null;
|
|
|
+
|
|
|
+ return {
|
|
|
+ name: name,
|
|
|
+ teacher: raw.SKJS ? raw.SKJS.split("/")[0] : (raw.JSM || ""),
|
|
|
+ position: raw.JASMC || "待定",
|
|
|
+ day: day,
|
|
|
+ startSection: startSection,
|
|
|
+ endSection: endSection,
|
|
|
+ weeks: weeks,
|
|
|
+ _kbId: raw.KBID,
|
|
|
+ _day: day,
|
|
|
+ _startSection: startSection,
|
|
|
+ _endSection: endSection
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function applyCourseChanges(courses, changes) {
|
|
|
+ var count = 0;
|
|
|
+ for (var i = 0; i < changes.length; i++) {
|
|
|
+ var ch = changes[i];
|
|
|
+ var kbId = ch.KBID;
|
|
|
+ var weeksToRemove = parseWeeksBitmap(ch.SKZC);
|
|
|
+ var applied = false;
|
|
|
+
|
|
|
+ var affected = courses.filter(function(c) {
|
|
|
+ return c._kbId === kbId &&
|
|
|
+ c._day === parseInt(ch.SKXQ) &&
|
|
|
+ c._startSection === parseInt(ch.KSJC) &&
|
|
|
+ c._endSection === parseInt(ch.JSJC);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (affected.length === 0) continue;
|
|
|
+
|
|
|
+ if (weeksToRemove.length > 0) {
|
|
|
+ affected.forEach(function(c) {
|
|
|
+ var before = c.weeks.length;
|
|
|
+ c.weeks = c.weeks.filter(function(w) { return weeksToRemove.indexOf(w) === -1; });
|
|
|
+ if (c.weeks.length < before) applied = true;
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ var isTimeChange = ch.TKLXDM === "01" || ch.TKLXDM === "03";
|
|
|
+ if (isTimeChange && ch.XSKZC && ch.XSKXQ && ch.XKSJC && ch.XJSJC) {
|
|
|
+ var newWeeks = parseWeeksBitmap(ch.XSKZC);
|
|
|
+ if (newWeeks.length > 0) {
|
|
|
+ var origTeacher = ch.YSKJS ? ch.YSKJS.split("/")[0] : "";
|
|
|
+ courses.push({
|
|
|
+ name: ch.KCM,
|
|
|
+ teacher: ch.XSKJS ? ch.XSKJS.split("/")[0] : origTeacher,
|
|
|
+ position: ch.XJASMC || ch.JASMC || "待定",
|
|
|
+ day: parseInt(ch.XSKXQ),
|
|
|
+ startSection: parseInt(ch.XKSJC),
|
|
|
+ endSection: parseInt(ch.XJSJC),
|
|
|
+ weeks: newWeeks,
|
|
|
+ _kbId: kbId,
|
|
|
+ _day: parseInt(ch.XSKXQ),
|
|
|
+ _startSection: parseInt(ch.XKSJC),
|
|
|
+ _endSection: parseInt(ch.XJSJC)
|
|
|
+ });
|
|
|
+ applied = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (applied) count++;
|
|
|
+ }
|
|
|
+ if (count > 0) window.shiguangBridge.showToast("已应用 " + count + " 条调课/停课变更。");
|
|
|
+ return courses;
|
|
|
+}
|
|
|
+
|
|
|
+function cleanAndMerge(courses) {
|
|
|
+ var list = courses.map(function(c) {
|
|
|
+ return {
|
|
|
+ name: c.name || "",
|
|
|
+ teacher: c.teacher || "",
|
|
|
+ position: c.position || "",
|
|
|
+ day: c.day,
|
|
|
+ startSection: c.startSection,
|
|
|
+ endSection: c.endSection,
|
|
|
+ weeks: Array.isArray(c.weeks) ? c.weeks.slice().sort(function(a,b){return a-b;}) : []
|
|
|
+ };
|
|
|
+ }).filter(function(c) { return c.weeks.length > 0; });
|
|
|
+
|
|
|
+ // 阶段1: 合并连续节次
|
|
|
+ list.sort(function(a, b) {
|
|
|
+ return a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
|
|
|
+ a.position.localeCompare(b.position) || (a.day||0)-(b.day||0) ||
|
|
|
+ a.weeks.join(",").localeCompare(b.weeks.join(",")) ||
|
|
|
+ (a.startSection||0)-(b.startSection||0);
|
|
|
+ });
|
|
|
+
|
|
|
+ var step1 = [];
|
|
|
+ var cur = list[0];
|
|
|
+ for (var i = 1; i < list.length; i++) {
|
|
|
+ var nxt = list[i];
|
|
|
+ var same = cur.name === nxt.name && cur.teacher === nxt.teacher &&
|
|
|
+ cur.position === nxt.position && cur.day === nxt.day &&
|
|
|
+ cur.weeks.join(",") === nxt.weeks.join(",");
|
|
|
+
|
|
|
+ if (same && cur.endSection + 1 === nxt.startSection) {
|
|
|
+ cur.endSection = nxt.endSection;
|
|
|
+ } else if (same && cur.startSection === nxt.startSection && cur.endSection === nxt.endSection) {
|
|
|
+ continue;
|
|
|
+ } else {
|
|
|
+ step1.push(cur);
|
|
|
+ cur = nxt;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ step1.push(cur);
|
|
|
+
|
|
|
+ // 阶段2: 合并同节次的周次
|
|
|
+ step1.sort(function(a, b) {
|
|
|
+ return a.name.localeCompare(b.name) || a.teacher.localeCompare(b.teacher) ||
|
|
|
+ a.position.localeCompare(b.position) || (a.day||0)-(b.day||0) ||
|
|
|
+ (a.startSection||0)-(b.startSection||0) || (a.endSection||0)-(b.endSection||0);
|
|
|
+ });
|
|
|
+
|
|
|
+ var step2 = [];
|
|
|
+ cur = step1[0];
|
|
|
+ for (var j = 1; j < step1.length; j++) {
|
|
|
+ var n = step1[j];
|
|
|
+ if (cur.name === n.name && cur.teacher === n.teacher && cur.position === n.position &&
|
|
|
+ cur.day === n.day && cur.startSection === n.startSection && cur.endSection === n.endSection) {
|
|
|
+ var set = {};
|
|
|
+ cur.weeks.concat(n.weeks).forEach(function(w) { set[w] = true; });
|
|
|
+ cur.weeks = Object.keys(set).map(Number).sort(function(a,b){return a-b;});
|
|
|
+ } else {
|
|
|
+ step2.push(cur);
|
|
|
+ cur = n;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ step2.push(cur);
|
|
|
+ return step2;
|
|
|
+}
|
|
|
+
|
|
|
+async function selectSemester() {
|
|
|
+ var semesterList = [];
|
|
|
+ try {
|
|
|
+ var data = await postApi(HEU_API.semesterList, "*order=-DM");
|
|
|
+ semesterList = getRows(data, "xnxqcx");
|
|
|
+ } catch (e) {
|
|
|
+ window.shiguangBridge.showToast("获取学期列表失败,请检查登录状态。");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (semesterList.length === 0) {
|
|
|
+ window.shiguangBridge.showToast("未查询到学期数据。");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ var defaultDM = null;
|
|
|
+ try {
|
|
|
+ var dqData = await postApi(HEU_API.currentTerm);
|
|
|
+ var dqRows = getRows(dqData, "dqxnxq");
|
|
|
+ if (dqRows.length > 0) defaultDM = dqRows[0].DM;
|
|
|
+ } catch (e) {}
|
|
|
+
|
|
|
+ var top = semesterList.slice(0, 10);
|
|
|
+ var names = top.map(function(s) { return s.MC || s.DM; });
|
|
|
+ var defaultIdx = -1;
|
|
|
+ if (defaultDM) {
|
|
|
+ for (var i = 0; i < top.length; i++) {
|
|
|
+ if (top[i].DM === defaultDM) { defaultIdx = i; break; }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var idx = await window.shiguangBridgePromise.showSingleSelection(
|
|
|
+ "请选择学期", JSON.stringify(names), defaultIdx
|
|
|
+ );
|
|
|
+ if (idx === null || idx === -1) return null;
|
|
|
+ return top[idx];
|
|
|
+}
|
|
|
+
|
|
|
+async function importTimeSlots() {
|
|
|
+ var slots = null;
|
|
|
+ try {
|
|
|
+ var data = await postApi(HEU_API.timeSlots);
|
|
|
+ var rows = getRows(data, "jc");
|
|
|
+ if (rows.length > 0) {
|
|
|
+ slots = rows.map(function(r) {
|
|
|
+ return { number: parseInt(r.DM), startTime: r.KSSJ, endTime: r.JSSJ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+
|
|
|
+ if (!slots || slots.length === 0) {
|
|
|
+ slots = DEFAULT_TIME_SLOTS;
|
|
|
+ }
|
|
|
+ await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(slots));
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchSemesterConfig(xn, xq) {
|
|
|
+ try {
|
|
|
+ var data = await postApi(HEU_API.config, "XN=" + xn + "&XQ=" + xq);
|
|
|
+ var row = getRows(data, "cxjcs")[0];
|
|
|
+ if (row) {
|
|
|
+ var rawDate = row.XQKSRQ;
|
|
|
+ return {
|
|
|
+ semesterStartDate: rawDate ? rawDate.split(" ")[0] : null,
|
|
|
+ semesterTotalWeeks: parseInt(row.ZZC) || 20
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } catch (e) {}
|
|
|
+ return { semesterStartDate: null, semesterTotalWeeks: 20 };
|
|
|
+}
|
|
|
+
|
|
|
+async function runImportFlow() {
|
|
|
+ var confirmed = await window.shiguangBridgePromise.showAlert(
|
|
|
+ "哈尔滨工程大学课表导入",
|
|
|
+ "导入前请确保已登录教务系统。",
|
|
|
+ "好的,开始导入"
|
|
|
+ );
|
|
|
+ if (!confirmed) {
|
|
|
+ window.shiguangBridge.showToast("已取消导入");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var semester = await selectSemester();
|
|
|
+ if (!semester) {
|
|
|
+ window.shiguangBridge.showToast("导入已取消。");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var XNXQDM = semester.DM;
|
|
|
+
|
|
|
+ window.shiguangBridge.showToast("正在获取作息时间...");
|
|
|
+ await importTimeSlots();
|
|
|
+
|
|
|
+ window.shiguangBridge.showToast("正在获取课表数据...");
|
|
|
+ var courseData = await postApi(HEU_API.course, "XNXQDM=" + XNXQDM);
|
|
|
+ var rawCourses = getRows(courseData, "cxxszhxqkb");
|
|
|
+
|
|
|
+ // 获取实验课
|
|
|
+ var rawExpCourses = [];
|
|
|
+ try {
|
|
|
+ var expData = await postApi(HEU_API.experiment, "XNXQDM=" + XNXQDM);
|
|
|
+ rawExpCourses = getRows(expData, "cxsyjxxskb");
|
|
|
+ } catch (e) {
|
|
|
+ console.warn("HEU: 获取实验课失败:", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ var allRaw = rawCourses.concat(rawExpCourses);
|
|
|
+ if (allRaw.length === 0) {
|
|
|
+ await window.shiguangBridgePromise.showAlert("导入提示", "该学期未查到课程数据。", "知道了");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rawExpCourses.length > 0) {
|
|
|
+ window.shiguangBridge.showToast("获取到 " + rawCourses.length + " 门理论课和 " + rawExpCourses.length + " 门实验课");
|
|
|
+ }
|
|
|
+
|
|
|
+ var parsed = allRaw.map(parseSingleCourse).filter(function(c) { return c !== null; });
|
|
|
+
|
|
|
+ // 获取调课数据
|
|
|
+ try {
|
|
|
+ var chData = await postApi(HEU_API.change, "XNXQDM=" + XNXQDM + "&*order=-SQSJ");
|
|
|
+ var rawChanges = getRows(chData, "xsdkkc");
|
|
|
+ if (rawChanges.length > 0) {
|
|
|
+ parsed = applyCourseChanges(parsed, rawChanges);
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn("HEU: 获取调课数据失败:", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ var courses = cleanAndMerge(parsed);
|
|
|
+
|
|
|
+ if (courses.length === 0) {
|
|
|
+ window.shiguangBridge.showToast("课表解析结果为空");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var config = await fetchSemesterConfig(semester.XNDM, semester.XQDM);
|
|
|
+ await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
+ await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
+
|
|
|
+ window.shiguangBridge.showToast(XNXQDM + " 课表导入成功,共 " + courses.length + " 门课程");
|
|
|
+ window.shiguangBridge.notifyTaskCompletion();
|
|
|
+}
|
|
|
+
|
|
|
+(async function() {
|
|
|
+ try {
|
|
|
+ await runImportFlow();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("HEU课表导入失败:", error);
|
|
|
+ if (window.shiguangBridgePromise) {
|
|
|
+ await window.shiguangBridgePromise.showAlert(
|
|
|
+ "导入失败",
|
|
|
+ "错误: " + error.message + "\n\n请确认:\n1. 已登录教务系统\n2. 处于校园网或VPN环境",
|
|
|
+ "确定"
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+})();
|