|
@@ -0,0 +1,522 @@
|
|
|
|
|
+(function () {
|
|
|
|
|
+ function showToast(message) {
|
|
|
|
|
+ if (typeof AndroidBridge !== "undefined" && AndroidBridge.showToast) {
|
|
|
|
|
+ AndroidBridge.showToast(String(message || ""));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log(message);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function getBaseOrigin() {
|
|
|
|
|
+ return window.location.origin;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function requestText(url, options) {
|
|
|
|
|
+ const response = await fetch(url, {
|
|
|
|
|
+ credentials: "include",
|
|
|
|
|
+ ...(options || {})
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!response.ok) {
|
|
|
|
|
+ throw new Error(`请求失败(${response.status}):${url}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return response.text();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseEntryParams(entryHtml) {
|
|
|
|
|
+ const html = String(entryHtml || "");
|
|
|
|
|
+ const idsMatch = html.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
|
|
|
|
|
+ const tagIdMatch = html.match(/id="(semesterBar\d+Semester)"/);
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ studentId: idsMatch ? idsMatch[1] : "",
|
|
|
|
|
+ tagId: tagIdMatch ? tagIdMatch[1] : ""
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function formatSemesterName(schoolYear, termName) {
|
|
|
|
|
+ const suffixMap = {
|
|
|
|
|
+ "1": "第一学期",
|
|
|
|
|
+ "2": "第二学期"
|
|
|
|
|
+ };
|
|
|
|
|
+ const suffix = suffixMap[String(termName || "").trim()] || `第${String(termName || "").trim()}学期`;
|
|
|
|
|
+ return `${String(schoolYear || "").trim()}学年${suffix}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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((key) => {
|
|
|
|
|
+ const list = data.semesters[key];
|
|
|
|
|
+ if (!Array.isArray(list)) return;
|
|
|
|
|
+
|
|
|
|
|
+ list.forEach((semester) => {
|
|
|
|
|
+ if (!semester || !semester.id) return;
|
|
|
|
|
+ const schoolYear = String(semester.schoolYear || "").trim();
|
|
|
|
|
+ const termName = String(semester.name || "").trim();
|
|
|
|
|
+ semesters.push({
|
|
|
|
|
+ id: String(semester.id),
|
|
|
|
|
+ schoolYear,
|
|
|
|
|
+ termName,
|
|
|
|
|
+ name: formatSemesterName(schoolYear, termName)
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return semesters;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseStudentProfile(htmlText) {
|
|
|
|
|
+ const html = String(htmlText || "");
|
|
|
|
|
+ const allDates = html.match(/\d{4}-\d{2}-\d{2}/g) || [];
|
|
|
|
|
+ const enrollmentDate = allDates[0] || "";
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ enrollmentDate,
|
|
|
|
|
+ enrollmentYear: enrollmentDate ? Number(enrollmentDate.slice(0, 4)) : 0
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function filterSemestersByEnrollmentYear(semesters, enrollmentYear) {
|
|
|
|
|
+ if (!enrollmentYear) return semesters;
|
|
|
|
|
+
|
|
|
|
|
+ const filtered = semesters.filter((semester) => {
|
|
|
|
|
+ const startYear = Number(String(semester.schoolYear || "").split("-")[0]);
|
|
|
|
|
+ return startYear >= enrollmentYear;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return filtered.length ? filtered : semesters;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function normalizeEnglishDate(dateText) {
|
|
|
|
|
+ const parsed = new Date(String(dateText || ""));
|
|
|
|
|
+ if (Number.isNaN(parsed.getTime())) return "";
|
|
|
|
|
+ const year = parsed.getFullYear();
|
|
|
|
|
+ const month = String(parsed.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
+ const day = String(parsed.getDate()).padStart(2, "0");
|
|
|
|
|
+ return `${year}-${month}-${day}`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseCalendarInfo(htmlText) {
|
|
|
|
|
+ const html = String(htmlText || "");
|
|
|
|
|
+ const match = html.match(/([A-Za-z]{3}\s+\d{1,2},\s+\d{4})~([A-Za-z]{3}\s+\d{1,2},\s+\d{4})\s*\((\d+)\)/);
|
|
|
|
|
+ if (!match) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ semesterStartDate: "",
|
|
|
|
|
+ semesterTotalWeeks: 0
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ semesterStartDate: normalizeEnglishDate(match[1]),
|
|
|
|
|
+ semesterTotalWeeks: Number(match[3] || 0)
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function chineseSectionToNumber(text) {
|
|
|
|
|
+ const mapping = {
|
|
|
|
|
+ "一": 1,
|
|
|
|
|
+ "二": 2,
|
|
|
|
|
+ "三": 3,
|
|
|
|
|
+ "四": 4,
|
|
|
|
|
+ "五": 5,
|
|
|
|
|
+ "六": 6,
|
|
|
|
|
+ "七": 7,
|
|
|
|
|
+ "八": 8,
|
|
|
|
|
+ "九": 9,
|
|
|
|
|
+ "十": 10,
|
|
|
|
|
+ "十一": 11
|
|
|
|
|
+ };
|
|
|
|
|
+ return mapping[String(text || "").trim()] || 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function parseTimeSlotsFromHtml(htmlText) {
|
|
|
|
|
+ const doc = new DOMParser().parseFromString(String(htmlText || ""), "text/html");
|
|
|
|
|
+ const slots = [];
|
|
|
|
|
+
|
|
|
|
|
+ doc.querySelectorAll("#manualArrangeCourseTable tbody tr").forEach((row) => {
|
|
|
|
|
+ const cells = Array.from(row.querySelectorAll("td"));
|
|
|
|
|
+ const sectionCell = cells.find((cell) => /第.+节/.test(cell.textContent || ""));
|
|
|
|
|
+ if (!sectionCell) return;
|
|
|
|
|
+
|
|
|
|
|
+ const text = sectionCell.textContent.replace(/\s+/g, " ").trim();
|
|
|
|
|
+ const match = text.match(/第([一二三四五六七八九十十一]+)节\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
|
|
|
|
|
+ if (!match) return;
|
|
|
|
|
+
|
|
|
|
|
+ const sectionNumber = chineseSectionToNumber(match[1]);
|
|
|
|
|
+ if (!sectionNumber) return;
|
|
|
|
|
+
|
|
|
|
|
+ slots.push({
|
|
|
|
|
+ number: sectionNumber,
|
|
|
|
|
+ startTime: match[2],
|
|
|
|
|
+ endTime: match[3]
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return slots.sort((a, b) => a.number - b.number);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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((week) => Number.isInteger(week) && week > 0))).sort((a, b) => a - b);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function cleanCourseName(name) {
|
|
|
|
|
+ return String(name || "")
|
|
|
|
|
+ .replace(/\s*\([^()]*\)\s*$/, "")
|
|
|
|
|
+ .trim();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function cleanPosition(position) {
|
|
|
|
|
+ return String(position || "")
|
|
|
|
|
+ .replace(/鹤壁工程技术学院/g, "")
|
|
|
|
|
+ .replace(/\s+/g, " ")
|
|
|
|
|
+ .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(",");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function mergeContiguousSections(courses) {
|
|
|
|
|
+ const normalized = (courses || []).map((course) => ({
|
|
|
|
|
+ ...course,
|
|
|
|
|
+ weeks: normalizeWeeks(course.weeks)
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ normalized.sort((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((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;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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 = cleanPosition(unquoteJsLiteral(args[5]));
|
|
|
|
|
+ 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*;\s*table\d+\.activities\[index\]/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,
|
|
|
|
|
+ teacher: teacher || "未知教师",
|
|
|
|
|
+ position: position || "待定",
|
|
|
|
|
+ day,
|
|
|
|
|
+ startSection: section,
|
|
|
|
|
+ endSection: section,
|
|
|
|
|
+ weeks
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return mergeContiguousSections(courses);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchEntryParams() {
|
|
|
|
|
+ const entryHtml = await requestText(`${getBaseOrigin()}/eams/courseTableForStd.action?&sf_request_type=ajax`, {
|
|
|
|
|
+ method: "GET",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ "x-requested-with": "XMLHttpRequest"
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return parseEntryParams(entryHtml);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchStudentProfile() {
|
|
|
|
|
+ const profileHtml = await requestText(`${getBaseOrigin()}/eams/stdInfoApply!stdInfoCheck.action?_=${Date.now()}`, {
|
|
|
|
|
+ method: "GET",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ accept: "text/html, */*; q=0.01",
|
|
|
|
|
+ "x-requested-with": "XMLHttpRequest"
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return parseStudentProfile(profileHtml);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchSemesters(tagId) {
|
|
|
|
|
+ const semesterRaw = await requestText(`${getBaseOrigin()}/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&empty=false`
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return parseSemesterResponse(semesterRaw);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchCourseHtml(studentId, semesterId) {
|
|
|
|
|
+ return requestText(`${getBaseOrigin()}/eams/courseTableForStd!courseTable.action?sf_request_type=ajax`, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ accept: "*/*",
|
|
|
|
|
+ "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(studentId)}`
|
|
|
|
|
+ ].join("&")
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function fetchCalendarInfo(semesterId) {
|
|
|
|
|
+ const calendarHtml = await requestText(`${getBaseOrigin()}/eams/base/calendar-info.action`, {
|
|
|
|
|
+ method: "POST",
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ accept: "text/html, */*; q=0.01",
|
|
|
|
|
+ "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
|
|
|
+ "x-requested-with": "XMLHttpRequest"
|
|
|
|
|
+ },
|
|
|
|
|
+ body: `version=1&semesterId=${encodeURIComponent(semesterId)}`
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return parseCalendarInfo(calendarHtml);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function selectSemester(semesters) {
|
|
|
|
|
+ const recent = semesters.slice(-8);
|
|
|
|
|
+ const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
|
|
|
|
|
+ "选择要导入的学期",
|
|
|
|
|
+ JSON.stringify(recent.map((semester) => semester.name || semester.id)),
|
|
|
|
|
+ recent.length - 1
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (selectedIndex === null || selectedIndex === -1) {
|
|
|
|
|
+ throw new Error("已取消导入。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return recent[selectedIndex];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ async function runImportFlow() {
|
|
|
|
|
+ showToast("正在识别课表参数...");
|
|
|
|
|
+ const params = await fetchEntryParams();
|
|
|
|
|
+ if (!params.studentId || !params.tagId) {
|
|
|
|
|
+ throw new Error("未能自动识别学生ID或学期参数");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ showToast("正在获取学籍信息...");
|
|
|
|
|
+ const studentProfile = await fetchStudentProfile();
|
|
|
|
|
+
|
|
|
|
|
+ showToast("正在获取学期列表...");
|
|
|
|
|
+ const semesters = filterSemestersByEnrollmentYear(
|
|
|
|
|
+ await fetchSemesters(params.tagId),
|
|
|
|
|
+ studentProfile.enrollmentYear
|
|
|
|
|
+ );
|
|
|
|
|
+ if (!semesters.length) {
|
|
|
|
|
+ throw new Error("未获取到学期列表。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const selectedSemester = await selectSemester(semesters);
|
|
|
|
|
+
|
|
|
|
|
+ showToast(`正在获取 ${selectedSemester.name} 课表...`);
|
|
|
|
|
+ const courseHtml = await fetchCourseHtml(params.studentId, selectedSemester.id);
|
|
|
|
|
+ const timeSlots = parseTimeSlotsFromHtml(courseHtml);
|
|
|
|
|
+ const courses = parseCoursesFromTaskActivityScript(courseHtml);
|
|
|
|
|
+ const calendarInfo = await fetchCalendarInfo(selectedSemester.id);
|
|
|
|
|
+
|
|
|
|
|
+ if (!courses.length) {
|
|
|
|
|
+ console.log(courseHtml);
|
|
|
|
|
+ throw new Error("未解析到课程数据,请确认当前学期有课表。");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const config = {
|
|
|
|
|
+ firstDayOfWeek: 1
|
|
|
|
|
+ };
|
|
|
|
|
+ if (calendarInfo.semesterStartDate) {
|
|
|
|
|
+ config.semesterStartDate = calendarInfo.semesterStartDate;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (calendarInfo.semesterTotalWeeks) {
|
|
|
|
|
+ config.semesterTotalWeeks = calendarInfo.semesterTotalWeeks;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
|
|
|
|
|
+ await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
|
|
|
|
|
+ if (timeSlots.length) {
|
|
|
|
|
+ await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ showToast(`导入完成,共 ${courses.length} 门课程`);
|
|
|
|
|
+ if (typeof AndroidBridge !== "undefined" && AndroidBridge.notifyTaskCompletion) {
|
|
|
|
|
+ AndroidBridge.notifyTaskCompletion();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ (async function bootstrap() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await runImportFlow();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(error);
|
|
|
|
|
+ showToast(`导入失败:${error.message || error}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ })();
|
|
|
|
|
+})();
|