hlju.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. function findHljuTimetableRequest() {
  2. const resources = performance.getEntriesByType("resource");
  3. const matches = resources
  4. .map(item => item.name)
  5. .filter(url =>
  6. url.includes("TimeTableNewService/GetTimeTableByStudent")
  7. );
  8. if (matches.length === 0) {
  9. return null;
  10. }
  11. return matches[matches.length - 1];
  12. }
  13. async function fetchHljuTimetable() {
  14. const url = findHljuTimetableRequest();
  15. if (!url) {
  16. throw new Error(
  17. "没有找到课表请求。\n" +
  18. "请先在黑龙江大学教务系统中打开“我的课程表”," +
  19. "选择学期并点击“查询”。"
  20. );
  21. }
  22. console.log("找到黑龙江大学课表请求。");
  23. const response = await fetch(url, {
  24. method: "GET",
  25. credentials: "include",
  26. headers: {
  27. "Accept": "application/json, text/javascript, */*; q=0.01",
  28. "X-Requested-With": "XMLHttpRequest"
  29. }
  30. });
  31. console.log("课表 API HTTP Status:", response.status);
  32. if (!response.ok) {
  33. throw new Error(
  34. `课表请求失败:HTTP ${response.status}`
  35. );
  36. }
  37. const data = await response.json();
  38. if (!Array.isArray(data)) {
  39. throw new Error("课表接口返回的数据不是数组。");
  40. }
  41. return data;
  42. }
  43. function parseSections(sectionText) {
  44. if (!sectionText) {
  45. return null;
  46. }
  47. const match = sectionText.match(
  48. /(\d+)\s*(?:,|,|-|~|~|至)\s*(\d+)/
  49. );
  50. if (!match) {
  51. return null;
  52. }
  53. return {
  54. start: parseInt(match[1], 10),
  55. end: parseInt(match[2], 10)
  56. };
  57. }
  58. function parseWeeks(weekText) {
  59. if (!weekText) {
  60. return [];
  61. }
  62. let text = weekText
  63. .replace(/^周次\s*[::]\s*/, "")
  64. .replace(/周/g, "")
  65. .trim();
  66. if (!text) {
  67. return [];
  68. }
  69. const weeks = new Set();
  70. const rangeRegex = /(\d+)\s*[-~~至]\s*(\d+)/g;
  71. let rangeMatch;
  72. while ((rangeMatch = rangeRegex.exec(text)) !== null) {
  73. const start = parseInt(rangeMatch[1], 10);
  74. const end = parseInt(rangeMatch[2], 10);
  75. if (start <= end) {
  76. for (let i = start; i <= end; i++) {
  77. weeks.add(i);
  78. }
  79. } else {
  80. for (let i = start; i >= end; i--) {
  81. weeks.add(i);
  82. }
  83. }
  84. }
  85. const remainingText = text.replace(
  86. /(\d+)\s*[-~~至]\s*(\d+)/g,
  87. ""
  88. );
  89. const numberMatches = remainingText.match(/\d+/g);
  90. if (numberMatches) {
  91. for (const number of numberMatches) {
  92. weeks.add(parseInt(number, 10));
  93. }
  94. }
  95. return Array.from(weeks).sort((a, b) => a - b);
  96. }
  97. function parseCourseBlock(block, weekday, sections) {
  98. const lines = block
  99. .split(/\r?\n/)
  100. .map(line => line.trim())
  101. .filter(line => line.length > 0);
  102. if (lines.length < 2) {
  103. return null;
  104. }
  105. const name = lines[0];
  106. if (!name) {
  107. return null;
  108. }
  109. const teacherLine = lines[1] || "";
  110. let teacher = teacherLine
  111. .split(/\s+/)[0]
  112. .trim();
  113. // 查找地点
  114. let position = "";
  115. for (const line of lines) {
  116. const match = line.match(/^地点\s*[::]\s*(.*)$/);
  117. if (match) {
  118. position = match[1].trim();
  119. break;
  120. }
  121. }
  122. // 查找周次
  123. let weeks = [];
  124. for (const line of lines) {
  125. const match = line.match(/^周次\s*[::]\s*(.*)$/);
  126. if (match) {
  127. weeks = parseWeeks(match[1]);
  128. break;
  129. }
  130. }
  131. let courseSections = sections;
  132. for (const line of lines) {
  133. const match = line.match(/^节次\s*[::]\s*(.*)$/);
  134. if (match) {
  135. const parsed = parseSections(match[1]);
  136. if (parsed) {
  137. courseSections = parsed;
  138. }
  139. break;
  140. }
  141. }
  142. if (!courseSections) {
  143. console.warn("无法解析课程节次:", block);
  144. return null;
  145. }
  146. if (weeks.length === 0) {
  147. console.warn("无法解析课程周次:", block);
  148. return null;
  149. }
  150. return {
  151. name: name,
  152. teacher: teacher,
  153. position: position,
  154. day: weekday,
  155. startSection: courseSections.start,
  156. endSection: courseSections.end,
  157. weeks: weeks
  158. };
  159. }
  160. function parseCourseCell(content, weekday, sections) {
  161. if (
  162. content === null ||
  163. content === undefined ||
  164. typeof content !== "string"
  165. ) {
  166. return [];
  167. }
  168. const text = content
  169. .replace(/\r\n/g, "\n")
  170. .replace(/\r/g, "\n")
  171. .trim();
  172. if (!text) {
  173. return [];
  174. }
  175. const blocks = text
  176. .split(/\n\s*\n+/)
  177. .map(block => block.trim())
  178. .filter(block => block.length > 0);
  179. const courses = [];
  180. for (const block of blocks) {
  181. const course = parseCourseBlock(
  182. block,
  183. weekday,
  184. sections
  185. );
  186. if (course) {
  187. courses.push(course);
  188. }
  189. }
  190. return courses;
  191. }
  192. function parseHljuTimetable(data) {
  193. const weekdayFields = [
  194. { field: "Monday", day: 1 },
  195. { field: "Tuesday", day: 2 },
  196. { field: "Wednesday", day: 3 },
  197. { field: "Thursday", day: 4 },
  198. { field: "Friday", day: 5 },
  199. { field: "Saturday", day: 6 },
  200. { field: "Sunday", day: 7 }
  201. ];
  202. const courses = [];
  203. for (const row of data) {
  204. if (!row || typeof row !== "object") {
  205. continue;
  206. }
  207. // 当前这一行代表的节次,例如 "5,6"
  208. const sections = parseSections(row.JieCi);
  209. if (!sections) {
  210. console.warn("无法解析节次:", row.JieCi);
  211. continue;
  212. }
  213. for (const weekday of weekdayFields) {
  214. const content = row[weekday.field];
  215. if (
  216. content === null ||
  217. content === undefined ||
  218. content === ""
  219. ) {
  220. continue;
  221. }
  222. const cellCourses = parseCourseCell(
  223. content,
  224. weekday.day,
  225. sections
  226. );
  227. courses.push(...cellCourses);
  228. }
  229. }
  230. return courses;
  231. }
  232. function validateCourses(courses) {
  233. if (!Array.isArray(courses)) {
  234. throw new Error("解析结果不是课程数组。");
  235. }
  236. if (courses.length === 0) {
  237. throw new Error(
  238. "没有解析到任何课程。\n" +
  239. "请确认当前学期已经查询成功,并且课表中有课程。"
  240. );
  241. }
  242. for (const course of courses) {
  243. if (!course.name) {
  244. throw new Error("存在课程名称为空的课程。");
  245. }
  246. if (!course.teacher) {
  247. console.warn("课程没有解析出教师:", course.name);
  248. }
  249. if (!course.position) {
  250. console.warn("课程没有解析出地点:", course.name);
  251. }
  252. if (
  253. !Number.isInteger(course.day) ||
  254. course.day < 1 ||
  255. course.day > 7
  256. ) {
  257. throw new Error(
  258. `课程“${course.name}”的星期数据非法。`
  259. );
  260. }
  261. if (
  262. !Number.isInteger(course.startSection) ||
  263. !Number.isInteger(course.endSection)
  264. ) {
  265. throw new Error(
  266. `课程“${course.name}”的节次数据非法。`
  267. );
  268. }
  269. if (
  270. !Array.isArray(course.weeks) ||
  271. course.weeks.length === 0
  272. ) {
  273. throw new Error(
  274. `课程“${course.name}”没有有效周次。`
  275. );
  276. }
  277. }
  278. }
  279. function printCourses(courses) {
  280. console.log(
  281. "========== 黑龙江大学解析后的拾光课程 =========="
  282. );
  283. console.table(
  284. courses.map(course => ({
  285. 课程: course.name,
  286. 教师: course.teacher,
  287. 地点: course.position,
  288. 星期: course.day,
  289. 开始节次: course.startSection,
  290. 结束节次: course.endSection,
  291. 周次: course.weeks.join(",")
  292. }))
  293. );
  294. console.log(
  295. "完整 CourseJsonModel:",
  296. courses
  297. );
  298. console.log(
  299. "================================================"
  300. );
  301. }
  302. async function saveHljuCourses(courses) {
  303. try {
  304. await window.AndroidBridgePromise.saveImportedCourses(
  305. JSON.stringify(courses)
  306. );
  307. AndroidBridge.showToast(
  308. `成功导入 ${courses.length} 个课程时段!`
  309. );
  310. console.log(
  311. `成功导入 ${courses.length} 个课程时段。`
  312. );
  313. return true;
  314. } catch (error) {
  315. console.error("保存课程失败:", error);
  316. AndroidBridge.showToast(
  317. "课程保存失败:" + error.message
  318. );
  319. return false;
  320. }
  321. }
  322. async function runImportFlow() {
  323. try {
  324. AndroidBridge.showToast(
  325. "正在获取黑龙江大学课表..."
  326. );
  327. const timetable = await fetchHljuTimetable();
  328. console.log(
  329. "黑龙江大学原始课表数据:",
  330. timetable
  331. );
  332. const courses = parseHljuTimetable(
  333. timetable
  334. );
  335. validateCourses(courses);
  336. printCourses(courses);
  337. AndroidBridge.showToast(
  338. `解析成功,共 ${courses.length} 个课程时段`
  339. );
  340. const saveSuccess = await saveHljuCourses(
  341. courses
  342. );
  343. if (!saveSuccess) {
  344. return;
  345. }
  346. AndroidBridge.showToast(
  347. "黑龙江大学课表导入成功!"
  348. );
  349. console.log(
  350. "黑龙江大学课程导入全部完成。"
  351. );
  352. // 只有全部成功后才发送结束信号
  353. AndroidBridge.notifyTaskCompletion();
  354. } catch (error) {
  355. console.error(
  356. "========== 黑龙江大学课表导入失败 ==========",
  357. error
  358. );
  359. AndroidBridge.showToast(
  360. "课表导入失败:" + error.message
  361. );
  362. }
  363. }
  364. runImportFlow();