| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- // 课表以空 HTML 表格返回,课程数据通过 JavaScript 脚本动态注入
- // 脚本中包含 `new TaskActivity(...)` 构造函数调用来定义课程
- // 需要从脚本文本中直接提取课程信息,而不是解析 DOM
- (function () {
- const BASE = "https://jwc3-yangtzeu-edu-cn-s.atrust.yangtzeu.edu.cn";
- function extractCourseHtmlDebugInfo(courseHtml) {
- const text = String(courseHtml || "");
- const hasTaskActivity = /new\s+TaskActivity\s*\(/i.test(text);
- return {
- responseLength: text.length,
- hasTaskActivity
- };
- }
- async function requestText(url, options) {
- const requestOptions = {
- credentials: "include",
- ...options
- };
- const res = await fetch(url, requestOptions);
- const text = await res.text();
- if (!res.ok) {
- throw new Error(`网络请求失败: ${res.status}`);
- }
- return text;
- }
- // 从入口页提取学生 ID 和学期组件 tagId
- function parseEntryParams(entryHtml) {
- const idsMatch = entryHtml.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
- const tagIdMatch = entryHtml.match(/id="(semesterBar\d+Semester)"/);
- return {
- studentId: idsMatch ? idsMatch[1] : "",
- tagId: tagIdMatch ? tagIdMatch[1] : ""
- };
- }
- // 学期接口返回对象字面量,这里按脚本文本解析
- function parseSemesterResponse(rawText) {
- let data;
- try {
- data = Function(`return (${String(rawText || "").trim()});`)();
- } catch (_) {
- throw new Error("学期数据解析失败");
- }
- const semesters = [];
- if (!data || !data.semesters || typeof data.semesters !== "object") {
- return semesters;
- }
- Object.keys(data.semesters).forEach((k) => {
- const arr = data.semesters[k];
- if (!Array.isArray(arr)) return;
- arr.forEach((s) => {
- if (!s || !s.id) return;
- semesters.push({
- id: String(s.id),
- name: `${s.schoolYear || ""} 第${s.name || ""}学期`.trim()
- });
- });
- });
- return semesters;
- }
- // 清除课程名后面的课程序号
- function cleanCourseName(name) {
- return String(name || "").replace(/\(\d+\)\s*$/, "").trim();
- }
- // 解析周次位图字符串
- function parseValidWeeksBitmap(bitmap) {
- if (!bitmap || typeof bitmap !== "string") return [];
- const weeks = [];
- for (let i = 0; i < bitmap.length; i++) {
- if (bitmap[i] === "1" && i >= 1) weeks.push(i);
- }
- return weeks;
- }
- function normalizeWeeks(weeks) {
- const list = Array.from(new Set((weeks || []).filter((w) => Number.isInteger(w) && w > 0)));
- list.sort((a, b) => a - b);
- return list;
- }
- // 节次编号与 TimeSlots 编号映射
- function mapSectionToTimeSlotNumber(section) {
- const mapping = {
- 1: 1,
- 2: 2,
- 3: 4,
- 4: 5,
- 5: 7,
- 6: 8,
- 7: 3,
- 8: 6
- };
- return mapping[section] || section;
- }
- // 反引号化 JavaScript 字面量字符串,处理转义字符
- function unquoteJsLiteral(token) {
- const text = String(token || "").trim();
- if (!text) return "";
- if (text === "null" || text === "undefined") return "";
- if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) {
- const quote = text[0];
- let inner = text.slice(1, -1);
- inner = inner
- .replace(/\\\\/g, "\\")
- .replace(new RegExp(`\\\\${quote}`, "g"), quote)
- .replace(/\\n/g, "\n")
- .replace(/\\r/g, "\r")
- .replace(/\\t/g, "\t");
- return inner;
- }
- return text;
- }
- // 分割 JavaScript 函数参数字符串,正确处理引号和转义
- function splitJsArgs(argsText) {
- const args = [];
- let curr = "";
- let inQuote = "";
- let escaped = false;
- for (let i = 0; i < argsText.length; i++) {
- const ch = argsText[i];
- if (escaped) {
- curr += ch;
- escaped = false;
- continue;
- }
- if (ch === "\\") {
- curr += ch;
- escaped = true;
- continue;
- }
- if (inQuote) {
- curr += ch;
- if (ch === inQuote) inQuote = "";
- continue;
- }
- if (ch === "\"" || ch === "'") {
- curr += ch;
- inQuote = ch;
- continue;
- }
- if (ch === ",") {
- args.push(curr.trim());
- curr = "";
- continue;
- }
- curr += ch;
- }
- if (curr.trim() || argsText.endsWith(",")) {
- args.push(curr.trim());
- }
- return args;
- }
- // 从脚本文本中的 TaskActivity 还原课程
- function parseCoursesFromTaskActivityScript(htmlText) {
- const text = String(htmlText || "");
- if (!text) return [];
- const unitCountMatch = text.match(/\bvar\s+unitCount\s*=\s*(\d+)\s*;/);
- const unitCount = unitCountMatch ? parseInt(unitCountMatch[1], 10) : 0;
- if (!Number.isInteger(unitCount) || unitCount <= 0) return [];
- const courses = [];
- const stats = {
- blocks: 0,
- teacherRecovered: 0,
- teacherUnresolvedExpression: 0
- };
- const blockRe = /activity\s*=\s*new\s+TaskActivity\(([^]*?)\)\s*;\s*index\s*=\s*(?:(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)|(\d+))\s*;\s*table\d+\.activities\[index\]/g;
- let match;
- while ((match = blockRe.exec(text)) !== null) {
- stats.blocks += 1;
- const argsText = match[1] || "";
- const args = splitJsArgs(argsText);
- if (args.length < 7) continue;
- const dayPart = match[2];
- const sectionPart = match[3];
- const directIndexPart = match[4];
- let indexValue = -1;
- if (dayPart != null && sectionPart != null) {
- indexValue = parseInt(dayPart, 10) * unitCount + parseInt(sectionPart, 10);
- } else if (directIndexPart != null) {
- indexValue = parseInt(directIndexPart, 10);
- }
- if (!Number.isInteger(indexValue) || indexValue < 0) continue;
- const day = Math.floor(indexValue / unitCount) + 1;
- let section = (indexValue % unitCount) + 1;
- section = mapSectionToTimeSlotNumber(section);
- if (day < 1 || day > 7 || section < 1 || section > 16) continue;
- let teacher = unquoteJsLiteral(args[1]);
- if (teacher && !/^['"]/.test(String(args[1]).trim()) && /join\s*\(/.test(String(args[1]))) {
- const resolved = resolveTeachersForTaskActivityBlock(text, match.index);
- if (resolved) {
- teacher = resolved;
- stats.teacherRecovered += 1;
- } else {
- stats.teacherUnresolvedExpression += 1;
- }
- }
- const name = cleanCourseName(unquoteJsLiteral(args[3]));
- const position = unquoteJsLiteral(args[5]);
- const weekBitmap = unquoteJsLiteral(args[6]);
- const weeks = normalizeWeeks(parseValidWeeksBitmap(weekBitmap));
- if (!name) continue;
- courses.push({
- name,
- teacher,
- position,
- day,
- startSection: section,
- endSection: section,
- weeks
- });
- }
- console.info("[课程解析 TaskActivity]", {
- blocks: stats.blocks,
- parsedCourses: courses.length,
- teacherRecovered: stats.teacherRecovered,
- teacherUnresolvedExpression: stats.teacherUnresolvedExpression
- });
- return mergeContiguousSections(courses);
- }
- // 当教师名为表达式时,尝试在附近代码中回溯真实教师名
- function resolveTeachersForTaskActivityBlock(fullText, blockStartIndex) {
- const start = Math.max(0, blockStartIndex - 2200);
- const segment = fullText.slice(start, blockStartIndex);
- const re = /var\s+actTeachers\s*=\s*\[([^]*?)\]\s*;/g;
- let m;
- let last = null;
- while ((m = re.exec(segment)) !== null) {
- last = m[1];
- }
- if (!last) return "";
- const names = [];
- const nameRe = /name\s*:\s*(?:"([^"]*)"|'([^']*)')/g;
- let nm;
- while ((nm = nameRe.exec(last)) !== null) {
- const name = (nm[1] || nm[2] || "").trim();
- if (name) names.push(name);
- }
- if (names.length === 0) return "";
- return Array.from(new Set(names)).join(",");
- }
- // 合并同一课程的连续节次
- function mergeContiguousSections(courses) {
- const list = (courses || [])
- .filter((c) => c && c.name && Number.isInteger(c.day) && Number.isInteger(c.startSection) && Number.isInteger(c.endSection))
- .map((c) => ({
- ...c,
- weeks: normalizeWeeks(c.weeks)
- }));
- list.sort((a, b) => {
- const ak = `${a.name}|${a.teacher}|${a.position}|${a.day}|${a.weeks.join(",")}`;
- const bk = `${b.name}|${b.teacher}|${b.position}|${b.day}|${b.weeks.join(",")}`;
- if (ak < bk) return -1;
- if (ak > bk) return 1;
- return a.startSection - b.startSection;
- });
- const merged = [];
- for (const item of list) {
- const prev = merged[merged.length - 1];
- const canMerge = prev
- && prev.name === item.name
- && prev.teacher === item.teacher
- && prev.position === item.position
- && prev.day === item.day
- && prev.weeks.join(",") === item.weeks.join(",")
- && prev.endSection + 1 >= item.startSection;
- if (canMerge) {
- prev.endSection = Math.max(prev.endSection, item.endSection);
- } else {
- merged.push({ ...item });
- }
- }
- return merged;
- }
- function getPresetTimeSlots() {
- return [
- { number: 1, startTime: "08:00", endTime: "09:35" },
- { number: 2, startTime: "10:05", endTime: "11:40" },
- { number: 3, startTime: "12:00", endTime: "13:35" }, // 午间课
- { number: 4, startTime: "14:00", endTime: "15:35" },
- { number: 5, startTime: "16:05", endTime: "17:40" },
- { number: 6, startTime: "17:45", endTime: "18:30" }, // 晚间课,部分课程为 18:00-18:45
- { number: 7, startTime: "19:00", endTime: "20:35" },
- { number: 8, startTime: "20:45", endTime: "22:20" }
- ];
- }
- function validateSemesterStartDateInput(input) {
- const value = String(input || "").trim();
- if (!value) return "请输入开学日期";
- return /^\d{4}[-/.]\d{2}[-/.]\d{2}$/.test(value) ? false : "请输入 YYYY-MM-DD";
- }
- window.validateSemesterStartDateInput = validateSemesterStartDateInput;
- async function selectSemesterStartDate() {
- const picked = await window.AndroidBridgePromise.showPrompt(
- "选择开学日期",
- "请输入开学日期(YYYY-MM-DD)",
- "",
- "validateSemesterStartDateInput"
- );
- if (picked === null) return null;
- const value = String(picked || "").trim().replace(/[/.]/g, "-");
- return value || null;
- }
- async function runImportFlow() {
- if (!window.AndroidBridgePromise) {
- throw new Error("AndroidBridgePromise 不可用,无法进行导入交互。");
- }
- AndroidBridge.showToast("开始自动探测长江大学教务参数...");
- // 探测学生 ID 和学期组件
- const entryUrl = `${BASE}/eams/courseTableForStd.action?&sf_request_type=ajax`;
- const entryHtml = await requestText(entryUrl, {
- method: "GET",
- headers: { "x-requested-with": "XMLHttpRequest" }
- });
- const params = parseEntryParams(entryHtml);
- if (!params.studentId || !params.tagId) {
- await window.AndroidBridgePromise.showAlert(
- "参数探测失败",
- "未能识别学生 ID 或学期组件 tagId,请确认已登录后重试。",
- "确定"
- );
- return;
- }
- // 学期选择
- const semesterRaw = await requestText(`${BASE}/eams/dataQuery.action?sf_request_type=ajax`, {
- method: "POST",
- headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
- body: `tagId=${encodeURIComponent(params.tagId)}&dataType=semesterCalendar`
- });
- const allSemesters = parseSemesterResponse(semesterRaw);
- if (allSemesters.length === 0) {
- throw new Error("学期列表为空,无法继续导入。");
- }
- const recentSemesters = allSemesters.slice(-8);
- const selectIndex = await window.AndroidBridgePromise.showSingleSelection(
- "请选择导入学期",
- JSON.stringify(recentSemesters.map((s) => s.name || s.id)),
- recentSemesters.length - 1
- );
- if (selectIndex === null) {
- AndroidBridge.showToast("已取消导入");
- return;
- }
- const index = Number.isInteger(Number(selectIndex)) ? Number(selectIndex) : recentSemesters.length - 1;
- const selectedSemester = recentSemesters[index >= 0 && index < recentSemesters.length ? index : recentSemesters.length - 1];
- const semesterStartDate = await selectSemesterStartDate();
- if (semesterStartDate === null) {
- AndroidBridge.showToast("已取消导入");
- return;
- }
- AndroidBridge.showToast("正在获取课表数据...");
- // 拉取并解析课表
- const courseHtml = await requestText(`${BASE}/eams/courseTableForStd!courseTable.action?sf_request_type=ajax`, {
- method: "POST",
- headers: { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" },
- body: [
- "ignoreHead=1",
- "setting.kind=std",
- "startWeek=",
- `semester.id=${encodeURIComponent(selectedSemester.id)}`,
- `ids=${encodeURIComponent(params.studentId)}`
- ].join("&")
- });
- const courses = parseCoursesFromTaskActivityScript(courseHtml);
- if (courses.length === 0) {
- const debugInfo = extractCourseHtmlDebugInfo(courseHtml);
- await window.AndroidBridgePromise.showAlert(
- "解析失败",
- `未能从课表响应中识别到课程。\n响应长度: ${debugInfo.responseLength}\n包含 TaskActivity: ${debugInfo.hasTaskActivity}`,
- "确定"
- );
- return;
- }
- await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
- await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(getPresetTimeSlots()));
- try {
- await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({ semesterStartDate }));
- } catch (e) {
- console.warn("保存开学日期配置失败:", e);
- }
- AndroidBridge.showToast(`导入成功,共 ${courses.length} 条课程`);
- AndroidBridge.notifyTaskCompletion();
- }
- (async function bootstrap() {
- try {
- await runImportFlow();
- } catch (error) {
- console.error("导入流程失败:", error);
- AndroidBridge.showToast(`导入失败:${error && error.message ? error.message : "请检查教务连接"}`);
- }
- })();
- })();
|