dlmu_01.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. // 文件: school.js
  2. // 功能:从大连海事大学教务系统获取课程表,通过桥接 API 导入到拾光课程表
  3. // ---------- 常量配置 ----------
  4. const BASE_URL = "http://jw.xpaas.dlmu.edu.cn";
  5. const ENDPOINTS = {
  6. DATA_QUERY: `${BASE_URL}/eams/dataQuery.action?sf_request_type=ajax`,
  7. COURSE_TABLE_FOR_STD: `${BASE_URL}/eams/courseTableForStd.action`,
  8. COURSE_TABLE: `${BASE_URL}/eams/courseTableForStd!courseTable.action?sf_request_type=ajax`,
  9. HOME_EXT: `${BASE_URL}/eams/homeExt.action`,
  10. };
  11. const UNIT_COUNT = 10; // 每天的课程节数
  12. // ---------- 全局验证函数 ----------
  13. /**
  14. * 验证学年输入格式
  15. * @param {string} input - 用户输入的年份
  16. * @returns {false|string} 验证通过返回 false,否则返回错误信息
  17. */
  18. function validateYearInput(input) {
  19. if (/^\d{4}$/.test(input)) {
  20. return false; // 验证通过
  21. }
  22. return "请输入四位数字的年份(例如 2024)";
  23. }
  24. // ---------- 工具函数 ----------
  25. /**
  26. * 解析课程数据
  27. * @param {string} jsCode - 包含课程数据的 JavaScript 代码字符串
  28. * @returns {Array} 课程对象数组
  29. */
  30. function parseCourses(jsCode) {
  31. /**
  32. * 默认在一个 TaskActivity 中只涉及该课程某一天的信息,且课程每节默认为连续 | 参考:(星期&节次解析器)[parseIndices()]
  33. */
  34. const unitCount = UNIT_COUNT;
  35. // 主正则:捕获 TaskActivity 参数 + 后续所有代码直到下一个 activity
  36. const mainRegex =
  37. /var\s*teachers\s*=\s*\[([^;]+?)\];(?:[\s\S]*?)activity\s*=\s*new\s*TaskActivity\(([^;]+?)\);([\s\S]*?)(?=(?:varteachers\s*=)|(?:<\/script>))/g;
  38. const courses = [];
  39. let match;
  40. while ((match = mainRegex.exec(jsCode)) !== null) {
  41. console.warn(match);
  42. // 第1组:教师信息
  43. const teachers = match[1].split("}").map((s) => s.trim());
  44. const teacherNames = extractTeacherNames(teachers);
  45. const teachersNameStr = teacherNames.join();
  46. // 第2组:TaskActivity 参数
  47. const args = match[2]
  48. .replaceAll(/\.join\(.*?\)/g, "")
  49. .split(",")
  50. .map((s) => s.trim());
  51. const courseName = extractCourseName(args[3]);
  52. const position = stripQuotes(args[5]);
  53. const weekStr = stripQuotes(args[6]);
  54. // 第3组:后续代码,提取所有 index
  55. const followingCode = match[3];
  56. const indexRegex =
  57. /index\s*=\s*(\d+(?:\s*\*\s*unitCount\s*\+\s*\d+)?)\s*;/g;
  58. const indices = [];
  59. let idxMatch;
  60. while ((idxMatch = indexRegex.exec(followingCode)) !== null) {
  61. indices.push(evalIndex(idxMatch[1], unitCount));
  62. }
  63. // 计算时间信息
  64. const timeInfo = parseIndices(indices, unitCount);
  65. courses.push({
  66. name: courseName,
  67. teacher: teachersNameStr,
  68. position: position,
  69. day: timeInfo.day,
  70. startSection: timeInfo.startSection,
  71. endSection: timeInfo.endSection,
  72. weeks: parseWeeks(weekStr),
  73. isCustomTime: false,
  74. });
  75. }
  76. return courses;
  77. }
  78. /**
  79. * 计算 index 表达式的值(安全版本)
  80. * @param {string} expr - 表达式字符串
  81. * @param {number} unitCount - 每天的课程节数
  82. * @returns {number} 计算结果
  83. */
  84. function evalIndex(expr, unitCount) {
  85. // 替换 unitCount 为实际值并移除空格
  86. const cleanExpr = expr.replace(/unitCount/g, unitCount).replace(/\s+/g, "");
  87. // 使用 Function 构造器替代 eval,仅允许数字和基本运算符
  88. try {
  89. const fn = new Function("return " + cleanExpr);
  90. return fn();
  91. } catch (error) {
  92. console.error("计算表达式失败:", expr, error);
  93. return 0;
  94. }
  95. }
  96. /**
  97. * 星期&节次解析器,根据 indices 计算 day 和 sections
  98. * @param {Array<number>} indices - 索引数组
  99. * @param {number} unitCount - 每天的课程节数
  100. * @returns {Object} 包含 day、startSection、endSection 的对象
  101. */
  102. function parseIndices(indices, unitCount) {
  103. /**
  104. *| index 范围 | 含义 |
  105. *| -------- | --------- |
  106. *| `0-9` | 周一 第1-10节 |
  107. *| `10-19` | 周二 第1-10节 |
  108. *| `20-29` | 周三 第1-10节 |
  109. *| ... | ... |
  110. */
  111. if (indices.length === 0) return { day: 1, startSection: 1, endSection: 1 };
  112. // 所有 index 应该在同一天
  113. const days = [...new Set(indices.map((i) => Math.floor(i / unitCount) + 1))];
  114. const day = days[0]; // 取第一天(理论上应该只有一天)
  115. const sections = indices
  116. .map((i) => (i % unitCount) + 1)
  117. .sort((a, b) => a - b);
  118. return {
  119. day: day,
  120. startSection: sections[0],
  121. endSection: sections[sections.length - 1],
  122. }; // 默认解析为同天连堂课,所以仅返回一个 SectionModel
  123. }
  124. /**
  125. * 提取教师姓名
  126. * @param {Array<string>} teachers - 教师信息数组
  127. * @returns {Array<string>} 教师姓名数组
  128. */
  129. function extractTeacherNames(teachers) {
  130. const teacherNames = [];
  131. for (const teacherMsg of teachers) {
  132. if (!teacherMsg || teacherMsg.trim().length === 0) continue;
  133. const args = teacherMsg.split(",");
  134. teacherNames.push(
  135. args[1]
  136. .replaceAll(/['"]/g, "")
  137. .replace(/name\s*:\s*/gi, "")
  138. .trim(),
  139. );
  140. }
  141. return teacherNames;
  142. }
  143. /**
  144. * 提取课程名称
  145. * @param {string} str - 包含课程名称的字符串
  146. * @returns {string} 清理后的课程名称
  147. */
  148. function extractCourseName(str) {
  149. return str.replace(/^["']|["']$/g, "").replace(/\([^)]+\)$/, "");
  150. }
  151. /**
  152. * 去除字符串两端的引号
  153. * @param {string} str - 输入字符串
  154. * @returns {string} 去除引号后的字符串
  155. */
  156. function stripQuotes(str) {
  157. return str.replace(/^["']|["']$/g, "");
  158. }
  159. /**
  160. * 解析周数字符串
  161. * @param {string} weekStr - 周数字符串(如 "011010...")
  162. * @returns {Array<number>} 周数数组
  163. */
  164. function parseWeeks(weekStr) {
  165. const weeks = [];
  166. for (let i = 0; i < weekStr.length; i++) {
  167. if (weekStr[i] === "1") weeks.push(i);
  168. }
  169. return weeks;
  170. }
  171. /**
  172. * 获取时间段配置
  173. * @returns {Array<Object>} 时间段数组
  174. */
  175. function getTimeSlots() {
  176. return [
  177. { number: 1, startTime: "08:00", endTime: "08:45" },
  178. { number: 2, startTime: "08:50", endTime: "09:35" },
  179. { number: 3, startTime: "10:00", endTime: "10:45" },
  180. { number: 4, startTime: "10:50", endTime: "11:35" },
  181. { number: 5, startTime: "13:30", endTime: "14:15" },
  182. { number: 6, startTime: "14:20", endTime: "15:05" },
  183. { number: 7, startTime: "15:30", endTime: "16:15" },
  184. { number: 8, startTime: "16:20", endTime: "17:05" },
  185. { number: 9, startTime: "18:00", endTime: "18:45" },
  186. { number: 10, startTime: "18:50", endTime: "19:35" },
  187. ];
  188. }
  189. /**
  190. * 从HTML中解析学期ID
  191. * @param {string} html - HTML字符串
  192. * @param {string} schoolYear - 学年,如 "2025-2026"
  193. * @param {string} name - 学期序号,如 "1"(上学期) , "2"(下学期) , "3"(小学期)
  194. * @returns {number|null} 学期ID,未找到返回null
  195. */
  196. function parseSemesterId(html, schoolYear, name) {
  197. if (!html || typeof html !== "string") {
  198. console.warn("HTML内容为空或格式错误");
  199. return null;
  200. }
  201. // 对输入值进行转义,防止正则注入
  202. const escapedSchoolYear = schoolYear.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  203. const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  204. // 匹配: {id:数字,schoolYear:"学年",name:"学期序号"}
  205. const pattern = new RegExp(
  206. `\\{id:(\\d+),schoolYear:"${escapedSchoolYear}",name:"${escapedName}"\\}`,
  207. );
  208. const match = html.match(pattern);
  209. if (match) {
  210. const id = parseInt(match[1], 10);
  211. console.log(`匹配成功: 学年=${schoolYear}, 学期=${name}, ID=${id}`);
  212. return id;
  213. }
  214. console.warn(`未找到匹配: 学年=${schoolYear}, 学期=${name}`);
  215. return null;
  216. }
  217. /**
  218. * 从HTML解析学生ids
  219. * @param {string} html - HTML字符串
  220. * @returns {string|null} 学生ID,未找到返回null
  221. */
  222. function parseStudentIds(html) {
  223. if (!html || typeof html !== "string") {
  224. console.warn("HTML内容为空或格式错误");
  225. return null;
  226. }
  227. // 匹配: `bg.form.addInput(form,"ids","待捕获的数字");`
  228. const pattern = new RegExp(`bg\\.form\\.addInput\\(form,"ids","(\\d+)"\\);`);
  229. const match = html.match(pattern);
  230. if (match) {
  231. const ids = match[1];
  232. console.log(`匹配成功: ids=${ids}`);
  233. return ids;
  234. }
  235. console.warn(`未找到匹配的ids`);
  236. return null;
  237. }
  238. // ---------- 网络请求 ----------
  239. /**
  240. * 通用的 fetch 请求封装
  241. * @param {string} url - 请求 URL
  242. * @param {Object} options - fetch 选项
  243. * @returns {Promise<string>} 去除空白符后的 HTML 字符串
  244. */
  245. async function fetchWithCleanup(url, options = {}) {
  246. try {
  247. const response = await fetch(url, options);
  248. if (!response.ok) {
  249. throw new Error(`HTTP error! status: ${response.status}`);
  250. }
  251. const html = await response.text();
  252. return html.replace(/\s/g, "");
  253. } catch (error) {
  254. console.error("请求失败:", url, error);
  255. throw error;
  256. }
  257. }
  258. /**
  259. * 获取学期课程安排HTML数据
  260. * @param {Object} options - 请求配置
  261. * @param {string} [options.tagId] - 标签ID (可选)
  262. * @param {string} [options.dataType='semesterCalendar'] - 数据类型
  263. * @param {string|number} [options.value] - 值 (可选)
  264. * @param {boolean} [options.empty=false] - 空标志 (可选)
  265. * @returns {Promise<string>} HTML字符串
  266. */
  267. async function fetchSemesterCalendar(options = {}) {
  268. const {
  269. tagId = "semesterBar20826294511Semester",
  270. dataType = "semesterCalendar",
  271. value = "223",
  272. empty = false,
  273. } = options;
  274. const url = ENDPOINTS.DATA_QUERY;
  275. // 构建表单数据
  276. const formData = new URLSearchParams();
  277. formData.append("tagId", tagId);
  278. formData.append("dataType", dataType);
  279. formData.append("value", value);
  280. formData.append("empty", empty);
  281. return fetchWithCleanup(url, {
  282. method: "POST",
  283. headers: {
  284. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  285. "X-Requested-With": "XMLHttpRequest",
  286. Accept: "text/plain, */*; q=0.01",
  287. "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8",
  288. Origin: BASE_URL,
  289. Referer: ENDPOINTS.COURSE_TABLE_FOR_STD,
  290. },
  291. body: formData.toString(),
  292. credentials: "include",
  293. });
  294. }
  295. /**
  296. * 获取学生课表页面HTML
  297. * @returns {Promise<string>} HTML字符串
  298. */
  299. async function fetchCourseTableForStd() {
  300. return fetchWithCleanup(ENDPOINTS.COURSE_TABLE_FOR_STD, {
  301. method: "GET",
  302. headers: {
  303. Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
  304. "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8",
  305. "Accept-Encoding": "gzip, deflate",
  306. Referer: ENDPOINTS.HOME_EXT,
  307. "Upgrade-Insecure-Requests": "1",
  308. Priority: "u=4",
  309. },
  310. credentials: "include",
  311. });
  312. }
  313. /**
  314. * 获取课程表数据
  315. * @param {string} semesterId - 学期ID
  316. * @param {string} ids - 学生ID
  317. * @returns {Promise<string>} HTML字符串
  318. */
  319. async function fetchCourseTableData(semesterId, ids) {
  320. const formData = new URLSearchParams();
  321. formData.append("ignoreHead", "1");
  322. formData.append("setting.kind", "std");
  323. formData.append("startWeek", "");
  324. formData.append("project.id", "1");
  325. formData.append("semester.id", semesterId);
  326. formData.append("ids", ids);
  327. return fetchWithCleanup(ENDPOINTS.COURSE_TABLE, {
  328. method: "POST",
  329. headers: {
  330. Accept: "*/*",
  331. "Accept-Language": "zh-CN,en-US;q=0.9,en;q=0.8",
  332. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  333. "X-Requested-With": "XMLHttpRequest",
  334. Origin: BASE_URL,
  335. Referer: ENDPOINTS.COURSE_TABLE,
  336. },
  337. body: formData.toString(),
  338. credentials: "include",
  339. });
  340. }
  341. // ---------- 用户交互 ----------
  342. /**
  343. * 提示用户确认开始导入
  344. * @returns {Promise<boolean>} 用户是否确认
  345. */
  346. async function promptUserToStart() {
  347. return await window.AndroidBridgePromise.showAlert(
  348. "重要提醒",
  349. "请确保您已登录服务大厅,并进入海大教务系统内的任意页面(不用点开课表)。\n值得注意的是,教务课表与海大在线的课表并非完全同步,若后期学校调课不规范,可能导致教务课表滞后。\n\n点击确定继续。",
  350. "确定",
  351. );
  352. }
  353. /**
  354. * 获取用户输入的学年
  355. * @returns {Promise<string|null>} 用户输入的年份,取消返回null
  356. */
  357. async function getAcademicYear() {
  358. return await window.AndroidBridgePromise.showPrompt(
  359. "学年设置",
  360. "请输入本学年开始的年份\n(例如 2024,代表 2024-2025 学年)",
  361. "2024",
  362. "validateYearInput", // 传入验证函数名
  363. );
  364. }
  365. /**
  366. * 让用户选择学期
  367. * @returns {Promise<number|null>} 选择的学期索引(0-2),取消返回null
  368. */
  369. async function selectSemester() {
  370. const semesterOptions = ["上学期", "下学期", "小学期"];
  371. const index = await window.AndroidBridgePromise.showSingleSelection(
  372. "选择学期",
  373. JSON.stringify(semesterOptions),
  374. 0,
  375. );
  376. if (index === null || index < 0 || index >= semesterOptions.length) {
  377. return null;
  378. }
  379. return index;
  380. }
  381. // ---------- 主流程 ----------
  382. /**
  383. * 主流程函数:协调整个课程表导入流程
  384. */
  385. async function run() {
  386. try {
  387. // 1. 公告
  388. const confirmed = await promptUserToStart();
  389. if (!confirmed) {
  390. AndroidBridge.showToast("用户取消了导入流程。");
  391. return;
  392. }
  393. // 2. 获取学年
  394. const yearInput = await getAcademicYear();
  395. if (yearInput === null) {
  396. AndroidBridge.showToast("导入已取消。");
  397. return;
  398. }
  399. const yearNum = parseInt(yearInput);
  400. if (isNaN(yearNum) || yearNum <= 2000 || yearNum > 2100) {
  401. await window.AndroidBridgePromise.showAlert(
  402. "错误",
  403. "学年输入无效,请输入2001-2100之间的数字。",
  404. "确定",
  405. );
  406. return;
  407. }
  408. const schoolYear = `${yearNum}-${yearNum + 1}`;
  409. // 3. 获取学期
  410. const semesterIndex = await selectSemester();
  411. if (semesterIndex === null) {
  412. AndroidBridge.showToast("导入已取消。");
  413. return;
  414. }
  415. const termCode =
  416. semesterIndex === 0 ? "1" : semesterIndex === 1 ? "2" : "3";
  417. const semesterHtml = await fetchSemesterCalendar();
  418. const semesterId = parseSemesterId(semesterHtml, schoolYear, termCode);
  419. // 4. 请求课表
  420. AndroidBridge.showToast("正在获取课表,请稍候...");
  421. let courseTableDataHtml = "";
  422. try {
  423. const idsHtml = await fetchCourseTableForStd();
  424. const ids = parseStudentIds(idsHtml);
  425. courseTableDataHtml = await fetchCourseTableData(semesterId, ids);
  426. } catch (fetchErr) {
  427. await window.AndroidBridgePromise.showAlert(
  428. "网络请求失败",
  429. `请求教务系统失败:${fetchErr.message}\n\n请检查网络连接和登录状态。`,
  430. "确定",
  431. );
  432. return;
  433. }
  434. if (!courseTableDataHtml.length) {
  435. await window.AndroidBridgePromise.showAlert(
  436. "提示",
  437. "未获取到任何课程数据。请确认已登录教务系统并选择正确的学年学期。",
  438. "确定",
  439. );
  440. return;
  441. }
  442. // 5. 解析并转换
  443. const targetCourses = parseCourses(courseTableDataHtml);
  444. // 6. 保存课程
  445. try {
  446. await window.AndroidBridgePromise.saveImportedCourses(
  447. JSON.stringify(targetCourses),
  448. );
  449. AndroidBridge.showToast(
  450. `课程数据已导入(共 ${targetCourses.length} 条)`,
  451. );
  452. } catch (saveErr) {
  453. await window.AndroidBridgePromise.showAlert(
  454. "保存课程失败",
  455. saveErr.message,
  456. "确定",
  457. );
  458. return;
  459. }
  460. // 7. 保存时间段
  461. const timeSlots = getTimeSlots();
  462. try {
  463. await window.AndroidBridgePromise.savePresetTimeSlots(
  464. JSON.stringify(timeSlots),
  465. );
  466. AndroidBridge.showToast("时间段数据已导入");
  467. } catch (slotErr) {
  468. // 时间段保存失败不终止流程,只提示
  469. AndroidBridge.showToast(`时间段保存失败:${slotErr.message}`);
  470. }
  471. // 8. 完成通知
  472. AndroidBridge.showToast("导入完成!");
  473. AndroidBridge.notifyTaskCompletion();
  474. } catch (err) {
  475. // 捕获所有未预料的错误
  476. console.error("run error:", err);
  477. await window.AndroidBridgePromise.showAlert(
  478. "导入失败",
  479. `未知错误:${err.message || err}\n\n请联系开发者。`,
  480. "确定",
  481. );
  482. // 仍然通知完成,但可能不会生成有效文件
  483. AndroidBridge.notifyTaskCompletion();
  484. }
  485. }
  486. // 启动
  487. run();