| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317 |
- /**
- * 成都信息工程大学(树维教务)课表导入适配脚本 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);
- }
- })();
- })();
|