|
|
@@ -0,0 +1,317 @@
|
|
|
+/**
|
|
|
+ * 成都信息工程大学(树维教务)课表导入适配脚本 Fetch API
|
|
|
+ */
|
|
|
+(function() {
|
|
|
+ const BASE = "http://jwgl.cuit.edu.cn";
|
|
|
+
|
|
|
+ // ==================== 工具函数 ====================
|
|
|
+ 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("'"))) {
|
|
|
+ return text.slice(1, -1);
|
|
|
+ }
|
|
|
+ return text;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ function parseValidWeeksBitmap(bitmap) {
|
|
|
+ if (!bitmap || typeof bitmap !== "string") return [];
|
|
|
+ const weeks = [];
|
|
|
+ for (let i = 0; i < bitmap.length; i++) {
|
|
|
+ if (bitmap[i] === "1") weeks.push(i + 1);
|
|
|
+ }
|
|
|
+ return weeks;
|
|
|
+ }
|
|
|
+
|
|
|
+ function cleanCourseName(name) {
|
|
|
+ return String(name || "").replace(/\(\d{10}\.\d{2}\)\s*$/, "").trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 核心解析 ====================
|
|
|
+ function parseCoursesFromHtml(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) : 12;
|
|
|
+
|
|
|
+ // 第一步:提取所有 actTeachers 定义块(用于解析教师姓名)
|
|
|
+ const teacherBlocks = [];
|
|
|
+ const teacherBlockRe = /var\s+teachers\s*=\s*\[(.*?)\];\s*var\s+actTeachers\s*=\s*\[(.*?)\];/gs;
|
|
|
+ let tbMatch;
|
|
|
+ while ((tbMatch = teacherBlockRe.exec(text)) !== null) {
|
|
|
+ const teachersArrStr = tbMatch[1];
|
|
|
+ const actTeachersArrStr = tbMatch[2];
|
|
|
+ const nameRe = /name\s*:\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
|
+ const names = [];
|
|
|
+ let nm;
|
|
|
+ const searchStr = actTeachersArrStr || teachersArrStr;
|
|
|
+ while ((nm = nameRe.exec(searchStr)) !== null) {
|
|
|
+ const name = (nm[1] || nm[2] || "").trim();
|
|
|
+ if (name) names.push(name);
|
|
|
+ }
|
|
|
+ if (names.length > 0) {
|
|
|
+ teacherBlocks.push({
|
|
|
+ startIndex: tbMatch.index,
|
|
|
+ endIndex: tbMatch.index + tbMatch[0].length,
|
|
|
+ teacherNames: names.join(',')
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第二步:解析 TaskActivity 及 index 赋值,收集每门课的所有节次
|
|
|
+ // 使用 Map 键为 "name|teacher|position|day|weeks" 值,存储节次数组
|
|
|
+ const courseSectionsMap = new Map();
|
|
|
+ const blockRe = /activity\s*=\s*new\s+TaskActivity\(([^]*?)\)\s*;([\s\S]*?)(?=activity\s*=\s*new\s+TaskActivity|$)/g;
|
|
|
+ let match;
|
|
|
+
|
|
|
+ while ((match = blockRe.exec(text)) !== null) {
|
|
|
+ const argsText = match[1];
|
|
|
+ const afterBlock = match[2];
|
|
|
+ const blockStart = match.index;
|
|
|
+
|
|
|
+ const args = splitJsArgs(argsText);
|
|
|
+ if (args.length < 7) continue;
|
|
|
+
|
|
|
+ let teacherExpr = args[1];
|
|
|
+ const courseFull = unquoteJsLiteral(args[2]);
|
|
|
+ let courseNameRaw = unquoteJsLiteral(args[3]);
|
|
|
+ const classroom = unquoteJsLiteral(args[5]);
|
|
|
+ const weekBitmap = unquoteJsLiteral(args[6]);
|
|
|
+
|
|
|
+ let courseName = courseNameRaw || courseFull.replace(/\(.*\)/, "");
|
|
|
+ courseName = cleanCourseName(courseName);
|
|
|
+ if (!courseName) continue;
|
|
|
+
|
|
|
+ const weeks = parseValidWeeksBitmap(weekBitmap);
|
|
|
+ if (weeks.length === 0) continue;
|
|
|
+
|
|
|
+ // 解析教师姓名
|
|
|
+ let teacherNames = "";
|
|
|
+ const teacherExprStr = String(teacherExpr).trim();
|
|
|
+ if (teacherExprStr.includes('join') || teacherExprStr.includes('actTeacherName')) {
|
|
|
+ for (let i = teacherBlocks.length - 1; i >= 0; i--) {
|
|
|
+ const tb = teacherBlocks[i];
|
|
|
+ if (tb.startIndex < blockStart) {
|
|
|
+ teacherNames = tb.teacherNames;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ teacherNames = unquoteJsLiteral(teacherExpr);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取该 activity 被赋值的所有 index,得到 day 和 section
|
|
|
+ const indexRe = /index\s*=\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)\s*;/g;
|
|
|
+ let idxMatch;
|
|
|
+ while ((idxMatch = indexRe.exec(afterBlock)) !== null) {
|
|
|
+ const dayIdx = parseInt(idxMatch[1], 10);
|
|
|
+ const sectionIdx = parseInt(idxMatch[2], 10);
|
|
|
+ const day = dayIdx + 1;
|
|
|
+ const section = sectionIdx + 1;
|
|
|
+
|
|
|
+ // 唯一键(不含节次)
|
|
|
+ const baseKey = `${courseName}|${teacherNames}|${classroom}|${day}|${weeks.join(',')}`;
|
|
|
+
|
|
|
+ if (!courseSectionsMap.has(baseKey)) {
|
|
|
+ courseSectionsMap.set(baseKey, {
|
|
|
+ name: courseName,
|
|
|
+ teacher: teacherNames,
|
|
|
+ position: classroom,
|
|
|
+ day: day,
|
|
|
+ weeks: weeks,
|
|
|
+ sections: new Set()
|
|
|
+ });
|
|
|
+ }
|
|
|
+ courseSectionsMap.get(baseKey).sections.add(section);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第三步:将节次 Set 转换为连续区间,生成最终课程列表
|
|
|
+ const courses = [];
|
|
|
+ for (const [_, data] of courseSectionsMap) {
|
|
|
+ const sections = Array.from(data.sections).sort((a, b) => a - b);
|
|
|
+ if (sections.length === 0) continue;
|
|
|
+
|
|
|
+ // 分组连续节次
|
|
|
+ let start = sections[0];
|
|
|
+ let end = sections[0];
|
|
|
+ for (let i = 1; i < sections.length; i++) {
|
|
|
+ if (sections[i] === end + 1) {
|
|
|
+ end = sections[i];
|
|
|
+ } else {
|
|
|
+ courses.push({
|
|
|
+ name: data.name,
|
|
|
+ teacher: data.teacher,
|
|
|
+ position: data.position,
|
|
|
+ day: data.day,
|
|
|
+ startSection: start,
|
|
|
+ endSection: end,
|
|
|
+ weeks: data.weeks,
|
|
|
+ isCustomTime: false
|
|
|
+ });
|
|
|
+ start = sections[i];
|
|
|
+ end = sections[i];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 最后一组
|
|
|
+ courses.push({
|
|
|
+ name: data.name,
|
|
|
+ teacher: data.teacher,
|
|
|
+ position: data.position,
|
|
|
+ day: data.day,
|
|
|
+ startSection: start,
|
|
|
+ endSection: end,
|
|
|
+ weeks: data.weeks,
|
|
|
+ isCustomTime: false
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return courses;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 学期与入口参数解析(同前)====================
|
|
|
+ async function requestText(url, options) {
|
|
|
+ const res = await fetch(url, { credentials: "include", ...options });
|
|
|
+ if (!res.ok) throw new Error(`请求失败: ${res.status}`);
|
|
|
+ return await res.text();
|
|
|
+ }
|
|
|
+
|
|
|
+ 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) 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 getPresetTimeSlots() {
|
|
|
+ return [
|
|
|
+ { 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: "17:40", endTime: "18:25" },
|
|
|
+ { number: 10, startTime: "19:30", endTime: "20:15" },
|
|
|
+ { number: 11, startTime: "20:25", endTime: "21:10" },
|
|
|
+ { number: 12, startTime: "21:20", endTime: "22:05" }
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 主导入流程 ====================
|
|
|
+ async function runImportFlow() {
|
|
|
+ if (!window.AndroidBridgePromise) throw new Error("AndroidBridgePromise 不可用");
|
|
|
+ AndroidBridge.showToast("正在探测教务参数...");
|
|
|
+
|
|
|
+ const entryHtml = await requestText(`${BASE}/eams/courseTableForStd.action?&sf_request_type=ajax`, {
|
|
|
+ method: "GET",
|
|
|
+ headers: { "x-requested-with": "XMLHttpRequest" }
|
|
|
+ });
|
|
|
+ const params = parseEntryParams(entryHtml);
|
|
|
+ if (!params.studentId || !params.tagId) {
|
|
|
+ await window.AndroidBridgePromise.showAlert("参数探测失败", "未能识别学生ID或学期组件", "确定");
|
|
|
+ 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 selectedSemester = recentSemesters[selectIndex];
|
|
|
+ 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 = parseCoursesFromHtml(courseHtml);
|
|
|
+ if (courses.length === 0) {
|
|
|
+ await window.AndroidBridgePromise.showAlert("解析失败", "未提取到课程数据", "确定");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
+ AndroidBridge.showToast(`成功导入 ${courses.length} 门课程`);
|
|
|
+
|
|
|
+ await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(getPresetTimeSlots()));
|
|
|
+
|
|
|
+ AndroidBridge.notifyTaskCompletion();
|
|
|
+ }
|
|
|
+
|
|
|
+ (async function bootstrap() {
|
|
|
+ try {
|
|
|
+ await runImportFlow();
|
|
|
+ } catch (error) {
|
|
|
+ console.error("导入流程失败:", error);
|
|
|
+ AndroidBridge.showToast("导入失败: " + error.message);
|
|
|
+ }
|
|
|
+ })();
|
|
|
+})();
|