| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- // 文件: hpu.js
- // 河南理工大学 树维教务系统课程表导入脚本
- // 适配目标: zhjw.hpu.edu.cn (树维教务系统, Struts2 .action, /eams/ 路径) + uia.hpu.edu.cn 统一身份认证(CAS)
- // 数据链路(已对 HPU 线上服务器实测):
- // 1. GET courseTableForStd.action?sf_request_type=ajax → 解析 ids(学生) + tagId(学期栏)
- // 2. POST dataQuery.action (dataType=semesterCalendar) → 学期列表
- // 3. POST courseTableForStd!courseTable.action (semester.id) → 课表 HTML
- // 课表 HTML 内课程以 TaskActivity JS 块内嵌(unitCount=11, bitmap周次), 表格骨架含节次/时间
- // 4. 解析 TaskActivity → 课程; 解析 #manualArrangeCourseTable 节次格 → 时间段
- // 兜底: 若当前页面已是渲染后的课表页(#manualArrangeCourseTable + .infoTitle), 直接解析 DOM
- // 解析参考: HIIT/hiit_01.js (同款 eams 架构系统)
- (function () {
- function showToast(message) {
- if (typeof window.shiguangBridge !== "undefined" && window.shiguangBridge.showToast) {
- window.shiguangBridge.showToast(String(message || ""));
- } else {
- console.log("[HPU适配]", message);
- }
- }
- async function request(url, options) {
- const res = await fetch(url, { credentials: "include", ...(options || {}) });
- if (!res.ok) throw new Error("网络请求失败(" + res.status + "): " + url);
- return await res.text();
- }
- // ================= 通用小工具 =================
- const CN_DIGIT = { "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "七": 7, "八": 8, "九": 9 };
- function cnDigit(c) { return CN_DIGIT[c] || 0; }
- function cnToInt(s) {
- if (!s) return 0;
- if (s === "十") return 10;
- const i = s.indexOf("十");
- if (i >= 0) {
- const tens = i > 0 ? cnDigit(s.charAt(0)) : 1;
- const ones = s.length > i + 1 ? cnDigit(s.charAt(i + 1)) : 0;
- return tens * 10 + ones;
- }
- return cnDigit(s);
- }
- function splitJsArgs(argsText) {
- const args = [];
- let current = "";
- let quote = "";
- let escaped = false;
- for (let i = 0; i < argsText.length; i++) {
- const ch = argsText[i];
- if (escaped) { current += ch; escaped = false; continue; }
- if (ch === "\\") { current += ch; escaped = true; continue; }
- if (quote) { current += ch; if (ch === quote) quote = ""; continue; }
- if (ch === "'" || ch === "\"") { current += ch; quote = ch; continue; }
- if (ch === ",") { args.push(current.trim()); current = ""; continue; }
- current += ch;
- }
- if (current.trim()) args.push(current.trim());
- return args;
- }
- function unquoteJsLiteral(token) {
- const text = String(token || "").trim();
- if (!text || text === "null" || text === "undefined") return "";
- if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) {
- const quote = text[0];
- return text.slice(1, -1)
- .replace(/\\\\/g, "\\")
- .replace(new RegExp("\\\\" + quote, "g"), quote)
- .replace(/\\n/g, "\n").replace(/\\r/g, "\r").replace(/\\t/g, "\t");
- }
- return text;
- }
- function parseValidWeeksBitmap(bitmap) {
- const weeks = [];
- const text = String(bitmap || "");
- for (let i = 0; i < text.length; i++) {
- if (text[i] === "1" && i >= 1) weeks.push(i);
- }
- return weeks;
- }
- function normalizeWeeks(weeks) {
- return Array.from(new Set((weeks || []).filter(function (w) { return Number.isInteger(w) && w > 0; }))).sort(function (a, b) { return a - b; });
- }
- function cleanCourseName(name) {
- return String(name || "").replace(/\s*\([^()]*\)\s*$/, "").trim();
- }
- function resolveTeachersForTaskActivityBlock(fullText, blockStartIndex) {
- const start = Math.max(0, blockStartIndex - 2500);
- const segment = fullText.slice(start, blockStartIndex);
- const teachersRegex = /var\s+teachers\s*=\s*\[([^]*?)\];/g;
- let lastTeachersBlock = "";
- let match;
- while ((match = teachersRegex.exec(segment)) !== null) lastTeachersBlock = match[1] || "";
- if (!lastTeachersBlock) return "";
- const names = [];
- const nameRegex = /name\s*:\s*(?:"([^"]*)"|'([^']*)')/g;
- let nameMatch;
- while ((nameMatch = nameRegex.exec(lastTeachersBlock)) !== null) {
- const name = (nameMatch[1] || nameMatch[2] || "").trim();
- if (name) names.push(name);
- }
- return Array.from(new Set(names)).join(",");
- }
- // ================= 课程解析 (TaskActivity, 从 AJAX HTML) =================
- function parseCoursesFromTaskActivityScript(htmlText) {
- const text = String(htmlText || "");
- const unitCountMatch = text.match(/\bvar\s+unitCount\s*=\s*(\d+)\s*;/);
- const unitCount = unitCountMatch ? Number(unitCountMatch[1]) : 0;
- if (!unitCount) return [];
- const courses = [];
- const blockRegex = /activity\s*=\s*new\s+TaskActivity\(([^]*?)\)\s*;([\s\S]*?)(?=activity\s*=\s*new\s+TaskActivity\(|table\d+\.marshalTable|$)/g;
- let match;
- while ((match = blockRegex.exec(text)) !== null) {
- const args = splitJsArgs(match[1] || "");
- if (args.length < 7) continue;
- let teacher = unquoteJsLiteral(args[1]);
- if (/join\s*\(/.test(String(args[1] || ""))) {
- teacher = resolveTeachersForTaskActivityBlock(text, match.index) || teacher;
- }
- const name = cleanCourseName(unquoteJsLiteral(args[3]));
- const position = String(unquoteJsLiteral(args[5]) || "").replace(/\s+/g, " ").trim();
- const weeks = normalizeWeeks(parseValidWeeksBitmap(unquoteJsLiteral(args[6])));
- if (!name) continue;
- const indexBlock = match[2] || "";
- const indexRegex = /index\s*=\s*(?:(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)|(\d+))\s*;/g;
- let indexMatch;
- while ((indexMatch = indexRegex.exec(indexBlock)) !== null) {
- let linearIndex = -1;
- if (indexMatch[1] != null && indexMatch[2] != null) {
- linearIndex = Number(indexMatch[1]) * unitCount + Number(indexMatch[2]);
- } else if (indexMatch[3] != null) {
- linearIndex = Number(indexMatch[3]);
- }
- if (linearIndex < 0) continue;
- const day = Math.floor(linearIndex / unitCount) + 1;
- const section = (linearIndex % unitCount) + 1;
- if (day < 1 || day > 7) continue;
- courses.push({
- name: name,
- teacher: teacher || "未知教师",
- position: position || "待定",
- day: day,
- startSection: section,
- endSection: section,
- weeks: weeks
- });
- }
- }
- return mergeContiguousSections(courses);
- }
- function mergeContiguousSections(courses) {
- const normalized = (courses || []).map(function (c) {
- return { ...c, weeks: normalizeWeeks(c.weeks) };
- });
- normalized.sort(function (a, b) {
- const keyA = a.name + "|" + a.teacher + "|" + a.position + "|" + a.day + "|" + a.weeks.join(",");
- const keyB = b.name + "|" + b.teacher + "|" + b.position + "|" + b.day + "|" + b.weeks.join(",");
- if (keyA < keyB) return -1;
- if (keyA > keyB) return 1;
- return a.startSection - b.startSection;
- });
- const merged = [];
- normalized.forEach(function (course) {
- const previous = merged[merged.length - 1];
- const canMerge = previous
- && previous.name === course.name
- && previous.teacher === course.teacher
- && previous.position === course.position
- && previous.day === course.day
- && previous.weeks.join(",") === course.weeks.join(",")
- && previous.endSection + 1 >= course.startSection;
- if (canMerge) {
- previous.endSection = Math.max(previous.endSection, course.endSection);
- } else {
- merged.push({ ...course });
- }
- });
- return merged;
- }
- // ================= 时间段解析 (从 HTML 表格骨架) =================
- function parseTimeSlotsFromHtml(htmlText) {
- const doc = new DOMParser().parseFromString(String(htmlText || ""), "text/html");
- const slots = [];
- const slotMap = {};
- const table = doc.querySelector("#manualArrangeCourseTable");
- if (!table) return slots;
- table.querySelectorAll("tbody tr").forEach(function (row) {
- const cells = Array.from(row.querySelectorAll("td"));
- cells.forEach(function (cell) {
- const txt = (cell.textContent || "").replace(/\s+/g, "");
- const m = txt.match(/第([一二三四五六七八九十]+)节/);
- if (!m) return;
- const tm = txt.match(/(\d{1,2}:\d{2})-(\d{1,2}:\d{2})/);
- if (!tm) return;
- const num = cnToInt(m[1]);
- if (num && !slotMap[num]) {
- slotMap[num] = { number: num, startTime: tm[1], endTime: tm[2] };
- }
- });
- });
- Object.keys(slotMap).map(Number).sort(function (a, b) { return a - b; })
- .forEach(function (k) { slots.push(slotMap[k]); });
- return slots;
- }
- // ================= 兜底: DOM 解析(已渲染的课表页) =================
- function parseCourseCellFromInfoTitle(raw) {
- const clean = String(raw || "").replace(/;;;/g, " ")
- .replace(/[\t\r\n]+/g, " ").replace(/\s+/g, " ").trim();
- const m = clean.match(/^(.+?)\(\d[\d.]*\)\s*\(([^)]*)\)\s*\(([^)]+)\)\s*$/);
- if (!m) return null;
- const info = m[3];
- const comma = info.indexOf(",");
- if (comma < 0) return null;
- // 周次字符串(可能含"单3-17"式样)直接转为 bitmap 语义
- const weekStr = info.substring(0, comma).trim();
- const weeks = [];
- const segs = weekStr.replace(/周/g, "").split(/[,,;;]/);
- segs.forEach(function (seg) {
- const isEven = seg.indexOf("双") >= 0;
- const isOdd = seg.indexOf("单") >= 0;
- const cleanSeg = seg.replace(/[单双]/g, "").trim();
- if (cleanSeg.indexOf("-") >= 0) {
- const p = cleanSeg.split("-");
- const s = parseInt(p[0], 10), e = parseInt(p[1], 10);
- if (!isNaN(s) && !isNaN(e)) {
- for (let w = s; w <= e; w++) {
- if (isEven && w % 2 !== 0) continue;
- if (isOdd && w % 2 === 0) continue;
- weeks.push(w);
- }
- }
- } else {
- const n = parseInt(cleanSeg, 10);
- if (!isNaN(n) && n > 0) weeks.push(n);
- }
- });
- const finalWeeks = Array.from(new Set(weeks)).sort(function (a, b) { return a - b; });
- if (!finalWeeks.length) return null;
- return {
- name: m[1].trim(),
- teacher: m[2].trim() || "未知教师",
- position: info.substring(comma + 1).replace(/\s+/g, " ").trim() || "待定",
- weeks: finalWeeks
- };
- }
- function parseCoursesFromCurrentDom() {
- const table = document.querySelector("#manualArrangeCourseTable");
- if (!table) return [];
- const courses = [];
- const seen = {};
- table.querySelectorAll("tbody tr").forEach(function (tr) {
- const tds = Array.from(tr.querySelectorAll("td"));
- let labelIdx = -1, section = 0;
- tds.forEach(function (td, i) {
- const txt = (td.textContent || "").replace(/\s+/g, "");
- const m = txt.match(/第([一二三四五六七八九十]+)节/);
- if (m) { labelIdx = i; section = cnToInt(m[1]); }
- });
- if (labelIdx < 0) return;
- tds.forEach(function (td, i) {
- if (i <= labelIdx) return;
- if ((td.className || "").indexOf("infoTitle") < 0) return;
- const raw = td.getAttribute("title") || td.textContent || "";
- if (!raw.trim()) return;
- const day = i - labelIdx - 1;
- if (day < 0 || day > 6) return;
- const parsed = parseCourseCellFromInfoTitle(raw);
- if (!parsed) return;
- const rowspan = td.rowSpan > 1 ? td.rowSpan : 1;
- const key = [day, section, rowspan, parsed.name, parsed.teacher, parsed.position, parsed.weeks.join(",")].join("|");
- if (seen[key]) return;
- seen[key] = 1;
- courses.push({
- name: parsed.name, teacher: parsed.teacher, position: parsed.position,
- day: day + 1, startSection: section, endSection: section + rowspan - 1,
- weeks: parsed.weeks
- });
- });
- });
- return courses;
- }
- // ================= AJAX 链路 =================
- async function detectParams() {
- if (/cas\/login|loginExt/i.test(window.location.href)) throw new Error("请先登录教务系统后再执行导入");
- const html = await request(window.location.origin + "/eams/courseTableForStd.action?sf_request_type=ajax", {
- headers: { "x-requested-with": "XMLHttpRequest" }
- });
- const idsM = html.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
- const tagM = html.match(/id="(semesterBar\d+Semester)"/);
- return (idsM && tagM) ? { ids: idsM[1], tagId: tagM[1] } : null;
- }
- async function fetchSemesters(tagId) {
- const raw = await request(window.location.origin + "/eams/dataQuery.action?sf_request_type=ajax", {
- method: "POST",
- headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
- body: "tagId=" + encodeURIComponent(tagId) + "&dataType=semesterCalendar"
- });
- const data = Function("return (" + String(raw).trim() + ");")();
- const list = [];
- if (data && data.semesters && typeof data.semesters === "object") {
- Object.keys(data.semesters).forEach(function (k) {
- (data.semesters[k] || []).forEach(function (s) {
- if (!s || !s.id) return;
- const term = String(s.name || "").trim();
- const termName = term === "1" ? "第一学期" : term === "2" ? "第二学期" : "第" + term + "学期";
- list.push({
- id: String(s.id),
- schoolYear: String(s.schoolYear || "").trim(),
- term: term,
- name: (String(s.schoolYear || "") + "学年" + termName).trim()
- });
- });
- });
- }
- return list;
- }
- // 按当前日期猜测所在学期(用于默认选中): 8-12月→该学年第一学期; 2-7月→上学年第二学期; 1月→上学年第一学期
- function guessCurrentSemesterIndex(semesters) {
- const now = new Date();
- const y = now.getFullYear();
- const m = now.getMonth() + 1;
- let targetYear, targetTerm;
- if (m >= 8) { targetYear = y; targetTerm = "1"; }
- else if (m >= 2) { targetYear = y - 1; targetTerm = "2"; }
- else { targetYear = y - 1; targetTerm = "1"; }
- for (let i = 0; i < semesters.length; i++) {
- const s = semesters[i];
- if (s.schoolYear === targetYear + "-" + (targetYear + 1) && s.term === targetTerm) return i;
- }
- return semesters.length - 1;
- }
- async function fetchCourseHtml(params, semesterId) {
- return await request(window.location.origin + "/eams/courseTableForStd!courseTable.action?sf_request_type=ajax", {
- method: "POST",
- headers: {
- "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
- "x-requested-with": "XMLHttpRequest"
- },
- body: "ignoreHead=1&setting.kind=std&startWeek=&semester.id=" + encodeURIComponent(semesterId) + "&ids=" + encodeURIComponent(params.ids)
- });
- }
- // ================= 保存 =================
- async function save(courses, timeSlots) {
- const okCourses = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
- if (okCourses !== true) throw new Error("课程保存失败: " + String(okCourses));
- if (timeSlots && timeSlots.length) {
- await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
- }
- }
- // ================= 主流程 =================
- async function runImportFlow() {
- // ① 已在渲染后的课表页 → DOM 直解
- let courses = parseCoursesFromCurrentDom();
- let timeSlots = [];
- if (courses.length) {
- showToast("已从当前课表页解析到 " + courses.length + " 门课");
- } else {
- // ② AJAX(可自选学期)
- showToast("正在识别课表参数...");
- const params = await detectParams();
- if (!params) throw new Error("未能自动识别课表参数,请登录后在任意教务页面重试");
- showToast("正在获取学期列表...");
- const semesters = await fetchSemesters(params.tagId);
- if (!semesters.length) throw new Error("未获取到学期列表");
- const showList = semesters.slice(-12); // 只展示最近6年, 避免列表过长
- const defaultIdx = Math.min(guessCurrentSemesterIndex(semesters), semesters.length - 1);
- const showDefault = Math.max(0, defaultIdx - (semesters.length - showList.length));
- const picked = await window.shiguangBridgePromise.showSingleSelection(
- "选择要导入的学期",
- JSON.stringify(showList.map(function (s) { return s.name; })),
- showDefault
- );
- if (picked === null || picked < 0 || picked >= showList.length) throw new Error("已取消导入");
- showToast("正在获取 " + showList[picked].name + " 课表...");
- const html = await fetchCourseHtml(params, showList[picked].id);
- courses = parseCoursesFromTaskActivityScript(html);
- timeSlots = parseTimeSlotsFromHtml(html);
- if (!courses.length) throw new Error("该学期未解析到课程,请确认有课或尝试其他学期");
- }
- await save(courses, timeSlots);
- showToast("导入成功,共 " + courses.length + " 门课程");
- if (typeof window.shiguangBridge !== "undefined" && window.shiguangBridge.notifyTaskCompletion) {
- window.shiguangBridge.notifyTaskCompletion();
- }
- }
- (async function bootstrap() {
- try {
- await runImportFlow();
- } catch (e) {
- console.error("[HPU适配]", e);
- showToast("导入失败:" + (e && e.message ? e.message : String(e)));
- }
- })();
- })();
|