| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544 |
- // 文件: school.js
- // 功能:从大连海事大学教务系统获取课程表,通过桥接 API 导入到拾光课程表
- // ---------- 常量配置 ----------
- const BASE_URL = "http://jw.xpaas.dlmu.edu.cn";
- const ENDPOINTS = {
- DATA_QUERY: `${BASE_URL}/eams/dataQuery.action?sf_request_type=ajax`,
- COURSE_TABLE_FOR_STD: `${BASE_URL}/eams/courseTableForStd.action`,
- COURSE_TABLE: `${BASE_URL}/eams/courseTableForStd!courseTable.action?sf_request_type=ajax`,
- HOME_EXT: `${BASE_URL}/eams/homeExt.action`,
- };
- const UNIT_COUNT = 10; // 每天的课程节数
- // ---------- 全局验证函数 ----------
- /**
- * 验证学年输入格式
- * @param {string} input - 用户输入的年份
- * @returns {false|string} 验证通过返回 false,否则返回错误信息
- */
- function validateYearInput(input) {
- if (/^\d{4}$/.test(input)) {
- return false; // 验证通过
- }
- return "请输入四位数字的年份(例如 2024)";
- }
- // ---------- 工具函数 ----------
- /**
- * 解析课程数据
- * @param {string} jsCode - 包含课程数据的 JavaScript 代码字符串
- * @returns {Array} 课程对象数组
- */
- function parseCourses(jsCode) {
- /**
- * 默认在一个 TaskActivity 中只涉及该课程某一天的信息,且课程每节默认为连续 | 参考:(星期&节次解析器)[parseIndices()]
- */
- const unitCount = UNIT_COUNT;
- // 主正则:捕获 TaskActivity 参数 + 后续所有代码直到下一个 activity
- const mainRegex =
- /var\s*teachers\s*=\s*\[([^;]+?)\];(?:[\s\S]*?)activity\s*=\s*new\s*TaskActivity\(([^;]+?)\);([\s\S]*?)(?=(?:varteachers\s*=)|(?:<\/script>))/g;
- const courses = [];
- let match;
- while ((match = mainRegex.exec(jsCode)) !== null) {
- console.warn(match);
- // 第1组:教师信息
- const teachers = match[1].split("}").map((s) => s.trim());
- const teacherNames = extractTeacherNames(teachers);
- const teachersNameStr = teacherNames.join();
- // 第2组:TaskActivity 参数
- const args = match[2]
- .replaceAll(/\.join\(.*?\)/g, "")
- .split(",")
- .map((s) => s.trim());
- const courseName = extractCourseName(args[3]);
- const position = stripQuotes(args[5]);
- const weekStr = stripQuotes(args[6]);
- // 第3组:后续代码,提取所有 index
- const followingCode = match[3];
- const indexRegex =
- /index\s*=\s*(\d+(?:\s*\*\s*unitCount\s*\+\s*\d+)?)\s*;/g;
- const indices = [];
- let idxMatch;
- while ((idxMatch = indexRegex.exec(followingCode)) !== null) {
- indices.push(evalIndex(idxMatch[1], unitCount));
- }
- // 计算时间信息
- const timeInfo = parseIndices(indices, unitCount);
- courses.push({
- name: courseName,
- teacher: teachersNameStr,
- position: position,
- day: timeInfo.day,
- startSection: timeInfo.startSection,
- endSection: timeInfo.endSection,
- weeks: parseWeeks(weekStr),
- isCustomTime: false,
- });
- }
- return courses;
- }
- /**
- * 计算 index 表达式的值(安全版本)
- * @param {string} expr - 表达式字符串
- * @param {number} unitCount - 每天的课程节数
- * @returns {number} 计算结果
- */
- function evalIndex(expr, unitCount) {
- // 替换 unitCount 为实际值并移除空格
- const cleanExpr = expr.replace(/unitCount/g, unitCount).replace(/\s+/g, "");
- // 使用 Function 构造器替代 eval,仅允许数字和基本运算符
- try {
- const fn = new Function("return " + cleanExpr);
- return fn();
- } catch (error) {
- console.error("计算表达式失败:", expr, error);
- return 0;
- }
- }
- /**
- * 星期&节次解析器,根据 indices 计算 day 和 sections
- * @param {Array<number>} indices - 索引数组
- * @param {number} unitCount - 每天的课程节数
- * @returns {Object} 包含 day、startSection、endSection 的对象
- */
- function parseIndices(indices, unitCount) {
- /**
- *| index 范围 | 含义 |
- *| -------- | --------- |
- *| `0-9` | 周一 第1-10节 |
- *| `10-19` | 周二 第1-10节 |
- *| `20-29` | 周三 第1-10节 |
- *| ... | ... |
- */
- if (indices.length === 0) return { day: 1, startSection: 1, endSection: 1 };
- // 所有 index 应该在同一天
- const days = [...new Set(indices.map((i) => Math.floor(i / unitCount) + 1))];
- const day = days[0]; // 取第一天(理论上应该只有一天)
- const sections = indices
- .map((i) => (i % unitCount) + 1)
- .sort((a, b) => a - b);
- return {
- day: day,
- startSection: sections[0],
- endSection: sections[sections.length - 1],
- }; // 默认解析为同天连堂课,所以仅返回一个 SectionModel
- }
- /**
- * 提取教师姓名
- * @param {Array<string>} teachers - 教师信息数组
- * @returns {Array<string>} 教师姓名数组
- */
- function extractTeacherNames(teachers) {
- const teacherNames = [];
- for (const teacherMsg of teachers) {
- if (!teacherMsg || teacherMsg.trim().length === 0) continue;
- const args = teacherMsg.split(",");
- teacherNames.push(
- args[1]
- .replaceAll(/['"]/g, "")
- .replace(/name\s*:\s*/gi, "")
- .trim(),
- );
- }
- return teacherNames;
- }
- /**
- * 提取课程名称
- * @param {string} str - 包含课程名称的字符串
- * @returns {string} 清理后的课程名称
- */
- function extractCourseName(str) {
- return str.replace(/^["']|["']$/g, "").replace(/\([^)]+\)$/, "");
- }
- /**
- * 去除字符串两端的引号
- * @param {string} str - 输入字符串
- * @returns {string} 去除引号后的字符串
- */
- function stripQuotes(str) {
- return str.replace(/^["']|["']$/g, "");
- }
- /**
- * 解析周数字符串
- * @param {string} weekStr - 周数字符串(如 "011010...")
- * @returns {Array<number>} 周数数组
- */
- function parseWeeks(weekStr) {
- const weeks = [];
- for (let i = 0; i < weekStr.length; i++) {
- if (weekStr[i] === "1") weeks.push(i);
- }
- return weeks;
- }
- /**
- * 获取时间段配置
- * @returns {Array<Object>} 时间段数组
- */
- function getTimeSlots() {
- return [
- { number: 1, startTime: "08:00", endTime: "08:45" },
- { number: 2, startTime: "08:50", endTime: "09:35" },
- { number: 3, startTime: "10:00", endTime: "10:45" },
- { number: 4, startTime: "10:50", endTime: "11:35" },
- { number: 5, startTime: "13:30", endTime: "14:15" },
- { number: 6, startTime: "14:20", endTime: "15:05" },
- { number: 7, startTime: "15:30", endTime: "16:15" },
- { number: 8, startTime: "16:20", endTime: "17:05" },
- { number: 9, startTime: "18:00", endTime: "18:45" },
- { number: 10, startTime: "18:50", endTime: "19:35" },
- ];
- }
- /**
- * 从HTML中解析学期ID
- * @param {string} html - HTML字符串
- * @param {string} schoolYear - 学年,如 "2025-2026"
- * @param {string} name - 学期序号,如 "1"(上学期) , "2"(下学期) , "3"(小学期)
- * @returns {number|null} 学期ID,未找到返回null
- */
- function parseSemesterId(html, schoolYear, name) {
- if (!html || typeof html !== "string") {
- console.warn("HTML内容为空或格式错误");
- return null;
- }
- // 对输入值进行转义,防止正则注入
- const escapedSchoolYear = schoolYear.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- // 匹配: {id:数字,schoolYear:"学年",name:"学期序号"}
- const pattern = new RegExp(
- `\\{id:(\\d+),schoolYear:"${escapedSchoolYear}",name:"${escapedName}"\\}`,
- );
- const match = html.match(pattern);
- if (match) {
- const id = parseInt(match[1], 10);
- console.log(`匹配成功: 学年=${schoolYear}, 学期=${name}, ID=${id}`);
- return id;
- }
- console.warn(`未找到匹配: 学年=${schoolYear}, 学期=${name}`);
- return null;
- }
- /**
- * 从HTML解析学生ids
- * @param {string} html - HTML字符串
- * @returns {string|null} 学生ID,未找到返回null
- */
- function parseStudentIds(html) {
- if (!html || typeof html !== "string") {
- console.warn("HTML内容为空或格式错误");
- return null;
- }
- // 匹配: `bg.form.addInput(form,"ids","待捕获的数字");`
- const pattern = new RegExp(`bg\\.form\\.addInput\\(form,"ids","(\\d+)"\\);`);
- const match = html.match(pattern);
- if (match) {
- const ids = match[1];
- console.log(`匹配成功: ids=${ids}`);
- return ids;
- }
- console.warn(`未找到匹配的ids`);
- return null;
- }
- // ---------- 网络请求 ----------
- /**
- * 通用的 fetch 请求封装
- * @param {string} url - 请求 URL
- * @param {Object} options - fetch 选项
- * @returns {Promise<string>} 去除空白符后的 HTML 字符串
- */
- async function fetchWithCleanup(url, options = {}) {
- try {
- const response = await fetch(url, options);
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const html = await response.text();
- return html.replace(/\s/g, "");
- } catch (error) {
- console.error("请求失败:", url, error);
- throw error;
- }
- }
- /**
- * 获取学期课程安排HTML数据
- * @param {Object} options - 请求配置
- * @param {string} [options.tagId] - 标签ID (可选)
- * @param {string} [options.dataType='semesterCalendar'] - 数据类型
- * @param {string|number} [options.value] - 值 (可选)
- * @param {boolean} [options.empty=false] - 空标志 (可选)
- * @returns {Promise<string>} HTML字符串
- */
- async function fetchSemesterCalendar(options = {}) {
- const {
- tagId = "semesterBar20826294511Semester",
- dataType = "semesterCalendar",
- value = "223",
- empty = false,
- } = options;
- const url = ENDPOINTS.DATA_QUERY;
- // 构建表单数据
- const formData = new URLSearchParams();
- formData.append("tagId", tagId);
- formData.append("dataType", dataType);
- formData.append("value", value);
- formData.append("empty", empty);
- return fetchWithCleanup(url, {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
- "X-Requested-With": "XMLHttpRequest",
- Accept: "text/plain, */*; q=0.01",
- "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8",
- Origin: BASE_URL,
- Referer: ENDPOINTS.COURSE_TABLE_FOR_STD,
- },
- body: formData.toString(),
- credentials: "include",
- });
- }
- /**
- * 获取学生课表页面HTML
- * @returns {Promise<string>} HTML字符串
- */
- async function fetchCourseTableForStd() {
- return fetchWithCleanup(ENDPOINTS.COURSE_TABLE_FOR_STD, {
- method: "GET",
- headers: {
- Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
- "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8",
- "Accept-Encoding": "gzip, deflate",
- Referer: ENDPOINTS.HOME_EXT,
- "Upgrade-Insecure-Requests": "1",
- Priority: "u=4",
- },
- credentials: "include",
- });
- }
- /**
- * 获取课程表数据
- * @param {string} semesterId - 学期ID
- * @param {string} ids - 学生ID
- * @returns {Promise<string>} HTML字符串
- */
- async function fetchCourseTableData(semesterId, ids) {
- const formData = new URLSearchParams();
- formData.append("ignoreHead", "1");
- formData.append("setting.kind", "std");
- formData.append("startWeek", "");
- formData.append("project.id", "1");
- formData.append("semester.id", semesterId);
- formData.append("ids", ids);
- return fetchWithCleanup(ENDPOINTS.COURSE_TABLE, {
- method: "POST",
- headers: {
- Accept: "*/*",
- "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8",
- "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
- "X-Requested-With": "XMLHttpRequest",
- Origin: BASE_URL,
- Referer: ENDPOINTS.COURSE_TABLE,
- },
- body: formData.toString(),
- credentials: "include",
- });
- }
- // ---------- 用户交互 ----------
- /**
- * 提示用户确认开始导入
- * @returns {Promise<boolean>} 用户是否确认
- */
- async function promptUserToStart() {
- return await window.AndroidBridgePromise.showAlert(
- "重要提醒",
- "请确保您已登录服务大厅,并进入海大教务系统内的任意页面(不用点开课表)。\n值得注意的是,教务课表与海大在线的课表并非完全同步,若后期学校调课不规范,可能导致教务课表滞后。\n\n点击确定继续。",
- "确定",
- );
- }
- /**
- * 获取用户输入的学年
- * @returns {Promise<string|null>} 用户输入的年份,取消返回null
- */
- async function getAcademicYear() {
- return await window.AndroidBridgePromise.showPrompt(
- "学年设置",
- "请输入本学年开始的年份\n(例如 2024,代表 2024-2025 学年)",
- "2024",
- "validateYearInput", // 传入验证函数名
- );
- }
- /**
- * 让用户选择学期
- * @returns {Promise<number|null>} 选择的学期索引(0-2),取消返回null
- */
- async function selectSemester() {
- const semesterOptions = ["上学期", "下学期", "小学期"];
- const index = await window.AndroidBridgePromise.showSingleSelection(
- "选择学期",
- JSON.stringify(semesterOptions),
- 0,
- );
- if (index === null || index < 0 || index >= semesterOptions.length) {
- return null;
- }
- return index;
- }
- // ---------- 主流程 ----------
- /**
- * 主流程函数:协调整个课程表导入流程
- */
- async function run() {
- try {
- // 1. 公告
- const confirmed = await promptUserToStart();
- if (!confirmed) {
- AndroidBridge.showToast("用户取消了导入流程。");
- return;
- }
- // 2. 获取学年
- const yearInput = await getAcademicYear();
- if (yearInput === null) {
- AndroidBridge.showToast("导入已取消。");
- return;
- }
- const yearNum = parseInt(yearInput);
- if (isNaN(yearNum) || yearNum <= 2000 || yearNum > 2100) {
- await window.AndroidBridgePromise.showAlert(
- "错误",
- "学年输入无效,请输入2001-2100之间的数字。",
- "确定",
- );
- return;
- }
- const schoolYear = `${yearNum}-${yearNum + 1}`;
- // 3. 获取学期
- const semesterIndex = await selectSemester();
- if (semesterIndex === null) {
- AndroidBridge.showToast("导入已取消。");
- return;
- }
- const termCode =
- semesterIndex === 0 ? "1" : semesterIndex === 1 ? "2" : "3";
- const semesterHtml = await fetchSemesterCalendar();
- const semesterId = parseSemesterId(semesterHtml, schoolYear, termCode);
- // 4. 请求课表
- AndroidBridge.showToast("正在获取课表,请稍候...");
- let courseTableDataHtml = "";
- try {
- const idsHtml = await fetchCourseTableForStd();
- const ids = parseStudentIds(idsHtml);
- courseTableDataHtml = await fetchCourseTableData(semesterId, ids);
- } catch (fetchErr) {
- await window.AndroidBridgePromise.showAlert(
- "网络请求失败",
- `请求教务系统失败:${fetchErr.message}\n\n请检查网络连接和登录状态。`,
- "确定",
- );
- return;
- }
- if (!courseTableDataHtml.length) {
- await window.AndroidBridgePromise.showAlert(
- "提示",
- "未获取到任何课程数据。请确认已登录教务系统并选择正确的学年学期。",
- "确定",
- );
- return;
- }
- // 5. 解析并转换
- const targetCourses = parseCourses(courseTableDataHtml);
- // 6. 保存课程
- try {
- await window.AndroidBridgePromise.saveImportedCourses(
- JSON.stringify(targetCourses),
- );
- AndroidBridge.showToast(
- `课程数据已导入(共 ${targetCourses.length} 条)`,
- );
- } catch (saveErr) {
- await window.AndroidBridgePromise.showAlert(
- "保存课程失败",
- saveErr.message,
- "确定",
- );
- return;
- }
- // 7. 保存时间段
- const timeSlots = getTimeSlots();
- try {
- await window.AndroidBridgePromise.savePresetTimeSlots(
- JSON.stringify(timeSlots),
- );
- AndroidBridge.showToast("时间段数据已导入");
- } catch (slotErr) {
- // 时间段保存失败不终止流程,只提示
- AndroidBridge.showToast(`时间段保存失败:${slotErr.message}`);
- }
- // 8. 完成通知
- AndroidBridge.showToast("导入完成!");
- AndroidBridge.notifyTaskCompletion();
- } catch (err) {
- // 捕获所有未预料的错误
- console.error("run error:", err);
- await window.AndroidBridgePromise.showAlert(
- "导入失败",
- `未知错误:${err.message || err}\n\n请联系开发者。`,
- "确定",
- );
- // 仍然通知完成,但可能不会生成有效文件
- AndroidBridge.notifyTaskCompletion();
- }
- }
- // 启动
- run();
|