const PRIMARY_TIME_SLOTS = [ { number: 1, startTime: "09:00", endTime: "09:40" }, { number: 2, startTime: "09:45", endTime: "10:25" }, { number: 3, startTime: "11:10", endTime: "11:50" }, { number: 4, startTime: "11:55", endTime: "12:35" }, { number: 5, startTime: "14:30", endTime: "15:10" }, { number: 6, startTime: "15:15", endTime: "15:55" }, { number: 7, startTime: "16:15", endTime: "16:55" }, { number: 8, startTime: "17:00", endTime: "17:40" }, { number: 9, startTime: "19:00", endTime: "19:40" }, { number: 10, startTime: "19:45", endTime: "20:25" }, { number: 11, startTime: "20:30", endTime: "21:10" } ]; const SCHEDULE_TIME_MAP = { "一": [ { number: 1, startTime: "09:00", endTime: "09:40" }, { number: 2, startTime: "09:45", endTime: "10:25" }, { number: 3, startTime: "10:40", endTime: "11:20" }, { number: 4, startTime: "11:25", endTime: "12:05" }, { number: 5, startTime: "14:30", endTime: "15:10" }, { number: 6, startTime: "15:15", endTime: "15:55" }, { number: 7, startTime: "16:15", endTime: "16:55" }, { number: 8, startTime: "17:00", endTime: "17:40" }, { number: 9, startTime: "19:00", endTime: "19:40" }, { number: 10, startTime: "19:45", endTime: "20:25" }, { number: 11, startTime: "20:30", endTime: "21:10" } ], "二": [ { number: 1, startTime: "09:00", endTime: "09:40" }, { number: 2, startTime: "09:45", endTime: "10:25" }, { number: 3, startTime: "10:55", endTime: "11:20" }, { number: 4, startTime: "11:40", endTime: "12:20" }, { number: 5, startTime: "14:30", endTime: "15:10" }, { number: 6, startTime: "15:15", endTime: "15:55" }, { number: 7, startTime: "16:15", endTime: "16:55" }, { number: 8, startTime: "17:00", endTime: "17:40" }, { number: 9, startTime: "19:00", endTime: "19:40" }, { number: 10, startTime: "19:45", endTime: "20:25" }, { number: 11, startTime: "20:30", endTime: "21:10" } ], "三": PRIMARY_TIME_SLOTS }; const BUILDING_SCHEDULE_MAP = { C: "一", E: "一", G: "一", D: "二", F: "二", H: "二", A: "三", B: "三", J: "三", K: "三", L: "三", M: "三" }; const DAY_FIELD_MAP = { mon: 1, tu: 2, wes: 3, tur: 4, fri: 5, sat: 6, sun: 7 }; function showToast(message) { if (typeof AndroidBridge !== "undefined" && AndroidBridge.showToast) { AndroidBridge.showToast(message); } else { console.log(message); } } function getBaseOrigin() { return window.location.origin; } function getTodayString() { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, "0"); const day = String(now.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } function normalizeHtmlLines(html) { return String(html || "") .replace(//gi, "\n") .replace(/ /gi, " ") .replace(/<[^>]+>/g, "") .split("\n") .map((line) => line.trim()) .filter(Boolean); } function parseWeeks(rawText) { const text = String(rawText || "") .replace(/\s+/g, "") .replace(/周/g, "") .replace(/,/g, ",") .replace(/、/g, ","); if (!text) return []; const weeks = new Set(); text.split(",").forEach((segment) => { if (!segment) return; const isOdd = /单/.test(segment); const isEven = /双/.test(segment); const cleaned = segment.replace(/[单双]/g, ""); const match = cleaned.match(/^(\d+)(?:-(\d+))?$/); if (!match) return; const start = Number(match[1]); const end = Number(match[2] || match[1]); for (let week = start; week <= end; week++) { if (isOdd && week % 2 === 0) continue; if (isEven && week % 2 !== 0) continue; weeks.add(week); } }); return Array.from(weeks).sort((a, b) => a - b); } function parseSectionAndRoom(rawLocation) { const value = String(rawLocation || "").trim(); const match = value.match(/^(\d{2})(\d{2})(.*)$/); if (!match) return null; return { startSection: Number(match[1]), endSection: Number(match[2]), position: match[3].trim() || "待定" }; } function getBuildingCode(position) { const match = String(position || "").trim().match(/^([A-Z])/i); return match ? match[1].toUpperCase() : ""; } function getScheduleTypeByPosition(position) { const buildingCode = getBuildingCode(position); return BUILDING_SCHEDULE_MAP[buildingCode] || "三"; } function getTimeSlotMap(scheduleType) { const map = new Map(); (SCHEDULE_TIME_MAP[scheduleType] || PRIMARY_TIME_SLOTS).forEach((slot) => { map.set(slot.number, slot); }); return map; } function fillCustomTime(course) { const scheduleType = getScheduleTypeByPosition(course.position); if (scheduleType === "三") { return course; } const timeSlotMap = getTimeSlotMap(scheduleType); const startSlot = timeSlotMap.get(course.startSection); const endSlot = timeSlotMap.get(course.endSection); if (!startSlot || !endSlot) { return course; } return { ...course, isCustomTime: true, customStartTime: startSlot.startTime, customEndTime: endSlot.endTime }; } function parseCellCourses(cellHtml, day, row, totalWeeks) { const lines = normalizeHtmlLines(cellHtml); if (!lines.length) return []; const courses = []; for (let index = 0; index < lines.length; index += 2) { const locationLine = lines[index]; const weekLine = lines[index + 1] || ""; const sectionInfo = parseSectionAndRoom(locationLine); if (!sectionInfo) continue; const weeks = parseWeeks(weekLine); courses.push(fillCustomTime({ name: String(row.cname || "").trim(), teacher: String(row.TeacherName || row.assteachername || "").trim() || "未知教师", position: sectionInfo.position || "待定", day, startSection: sectionInfo.startSection, endSection: sectionInfo.endSection, weeks: weeks.length ? weeks : Array.from({ length: totalWeeks }, (_, i) => i + 1) })); } return courses; } function deduplicateCourses(courses) { const seen = new Map(); courses.forEach((course) => { const key = [ course.name, course.teacher, course.position, course.day, course.startSection, course.endSection, course.weeks.join(",") ].join("|"); if (!seen.has(key)) { seen.set(key, course); } }); return Array.from(seen.values()); } async function requestJson(path, options = {}) { const response = await fetch(`${getBaseOrigin()}${path}`, { credentials: "include", ...options }); const text = await response.text(); let data; try { data = JSON.parse(text); } catch (error) { throw new Error(`接口 ${path} 返回了非 JSON 内容,请确认已登录并位于正确页面。`); } if (!response.ok) { throw new Error(`接口 ${path} 请求失败,HTTP ${response.status}`); } return data; } async function requestText(path, options = {}) { const response = await fetch(`${getBaseOrigin()}${path}`, { credentials: "include", ...options }); if (!response.ok) { throw new Error(`接口 ${path} 请求失败,HTTP ${response.status}`); } return response.text(); } function parseStudentIdFromHtml(html) { const idMatch = String(html || "").match(/name=["']stid["'][^>]*value=["']([^"']+)["']/i); return idMatch ? idMatch[1].trim() : ""; } async function fetchStudentProfile() { const html = await requestText("/Admin_Areas/StInfo/studentInfo"); const studentId = parseStudentIdFromHtml(html); if (!studentId) { throw new Error("未能从个人信息页面解析出学号。"); } const body = new URLSearchParams({ stid: studentId }); const profile = await requestJson("/Admin_Areas/StInfo/getStInfo", { method: "POST", headers: { Accept: "application/json, text/javascript, */*; q=0.01", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest" }, body: body.toString() }); return { studentId, enrolldate: String(profile?.enrolldate || "").trim(), grade: String(profile?.grade || "").trim() }; } function getEnrollmentThreshold(profile) { const enrollmentDate = String(profile?.enrolldate || "").trim(); if (enrollmentDate) { return enrollmentDate; } const grade = Number(profile?.grade || 0); if (grade >= 1900 && grade <= 2100) { return `${grade}-01-01`; } return ""; } async function fetchTermList() { const terms = await requestJson("/Admin_Areas/Res/GetTermInfoAll", { method: "POST", headers: { Accept: "application/json, text/javascript, */*; q=0.01", "X-Requested-With": "XMLHttpRequest" } }); if (!Array.isArray(terms) || !terms.length) { throw new Error("未获取到学期信息。"); } const filteredTerms = terms.filter((term) => { if (!term || !term.term || !term.startdate) return false; const name = String(term.termname || ""); return /学年第[一二三四五六七八九十]+学期/.test(name); }); return filteredTerms.length ? filteredTerms : terms.filter((term) => term && term.term && term.startdate); } function filterTermsByEnrollment(terms, enrollmentThreshold) { if (!enrollmentThreshold) return terms; const filtered = terms.filter((term) => { return String(term.enddate || term.startdate || "") >= enrollmentThreshold; }); return filtered.length ? filtered : terms; } function getDefaultTermIndex(terms) { const today = getTodayString(); const currentIndex = terms.findIndex((term) => { return term.startdate <= today && today <= String(term.enddate || "9999-12-31"); }); if (currentIndex >= 0) return currentIndex; const regularIndex = terms.findIndex((term) => /学期/.test(String(term.termname || ""))); return regularIndex >= 0 ? regularIndex : 0; } async function selectTerm(terms) { const items = terms.map((term) => { return String(term.termname || term.term); }); const defaultIndex = getDefaultTermIndex(terms); if (typeof window.AndroidBridgePromise === "undefined") { return terms[defaultIndex]; } const selectedIndex = await window.AndroidBridgePromise.showSingleSelection( "选择要导入的学期", JSON.stringify(items), defaultIndex ); if (selectedIndex === null || selectedIndex === -1) { throw new Error("已取消导入。"); } return terms[selectedIndex]; } async function fetchCoursePage(termCode, page, rowsPerPage) { const body = new URLSearchParams({ term: termCode, page: String(page), rows: String(rowsPerPage) }); const data = await requestJson("/Admin_Areas/StInfo/GetCourseQuery", { method: "POST", headers: { Accept: "application/json, text/javascript, */*; q=0.01", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "X-Requested-With": "XMLHttpRequest" }, body: body.toString() }); if (!data || !Array.isArray(data.rows)) { throw new Error("课表接口未返回有效数据。"); } return data; } async function fetchAllCourseRows(termCode) { const rowsPerPage = 50; const firstPage = await fetchCoursePage(termCode, 1, rowsPerPage); const allRows = [...firstPage.rows]; const total = Number(firstPage.total || allRows.length); const totalPages = Math.max(1, Math.ceil(total / rowsPerPage)); for (let page = 2; page <= totalPages; page++) { const pageData = await fetchCoursePage(termCode, page, rowsPerPage); allRows.push(...pageData.rows); } return allRows; } function buildCourses(rows, totalWeeks) { const courses = []; rows.forEach((row) => { Object.entries(DAY_FIELD_MAP).forEach(([field, day]) => { const cellValue = row[field]; if (!cellValue) return; courses.push(...parseCellCourses(cellValue, day, row, totalWeeks)); }); }); return deduplicateCourses(courses); } async function saveConfig(term) { const config = { semesterStartDate: String(term.startdate), semesterTotalWeeks: Number(term.weeknum || 20), firstDayOfWeek: 1 }; await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config)); } async function saveTimeSlots() { await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(PRIMARY_TIME_SLOTS)); } async function saveCourses(courses) { await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses)); } async function runImportFlow() { try { showToast("正在获取个人信息..."); const studentProfile = await fetchStudentProfile(); const enrollmentThreshold = getEnrollmentThreshold(studentProfile); showToast("正在获取学期信息..."); const terms = filterTermsByEnrollment(await fetchTermList(), enrollmentThreshold); const selectedTerm = await selectTerm(terms); showToast(`正在获取 ${selectedTerm.termname || selectedTerm.term} 课表...`); const rows = await fetchAllCourseRows(String(selectedTerm.term)); const totalWeeks = Number(selectedTerm.weeknum || 20); const courses = buildCourses(rows, totalWeeks); if (!courses.length) { throw new Error("未解析到课程,请确认当前账号已在教务系统中可查看课表。"); } if (typeof window.AndroidBridgePromise === "undefined") { console.log("Selected term:", selectedTerm); console.log("Courses:", courses); console.log("Time slots:", PRIMARY_TIME_SLOTS); alert(`解析完成,共 ${courses.length} 门课程。请查看控制台输出。`); return; } await saveConfig(selectedTerm); await saveTimeSlots(); await saveCourses(courses); showToast(`导入完成,共 ${courses.length} 门课程`); if (typeof AndroidBridge !== "undefined" && AndroidBridge.notifyTaskCompletion) { AndroidBridge.notifyTaskCompletion(); } } catch (error) { console.error(error); showToast(`导入失败: ${error.message}`); } } runImportFlow();