hiit_01.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. (function () {
  2. function showToast(message) {
  3. if (typeof AndroidBridge !== "undefined" && AndroidBridge.showToast) {
  4. AndroidBridge.showToast(String(message || ""));
  5. } else {
  6. console.log(message);
  7. }
  8. }
  9. function getBaseOrigin() {
  10. return window.location.origin;
  11. }
  12. async function requestText(url, options) {
  13. const response = await fetch(url, {
  14. credentials: "include",
  15. ...(options || {})
  16. });
  17. if (!response.ok) {
  18. throw new Error(`请求失败(${response.status}):${url}`);
  19. }
  20. return response.text();
  21. }
  22. function parseEntryParams(entryHtml) {
  23. const html = String(entryHtml || "");
  24. const idsMatch = html.match(/bg\.form\.addInput\(form,"ids","(\d+)"\)/);
  25. const tagIdMatch = html.match(/id="(semesterBar\d+Semester)"/);
  26. return {
  27. studentId: idsMatch ? idsMatch[1] : "",
  28. tagId: tagIdMatch ? tagIdMatch[1] : ""
  29. };
  30. }
  31. function formatSemesterName(schoolYear, termName) {
  32. const suffixMap = {
  33. "1": "第一学期",
  34. "2": "第二学期"
  35. };
  36. const suffix = suffixMap[String(termName || "").trim()] || `第${String(termName || "").trim()}学期`;
  37. return `${String(schoolYear || "").trim()}学年${suffix}`;
  38. }
  39. function parseSemesterResponse(rawText) {
  40. let data;
  41. try {
  42. data = Function(`return (${String(rawText || "").trim()});`)();
  43. } catch (_) {
  44. throw new Error("学期数据解析失败。");
  45. }
  46. const semesters = [];
  47. if (!data || !data.semesters || typeof data.semesters !== "object") {
  48. return semesters;
  49. }
  50. Object.keys(data.semesters).forEach((key) => {
  51. const list = data.semesters[key];
  52. if (!Array.isArray(list)) return;
  53. list.forEach((semester) => {
  54. if (!semester || !semester.id) return;
  55. const schoolYear = String(semester.schoolYear || "").trim();
  56. const termName = String(semester.name || "").trim();
  57. semesters.push({
  58. id: String(semester.id),
  59. schoolYear,
  60. termName,
  61. name: formatSemesterName(schoolYear, termName)
  62. });
  63. });
  64. });
  65. return semesters;
  66. }
  67. function parseStudentProfile(htmlText) {
  68. const html = String(htmlText || "");
  69. const allDates = html.match(/\d{4}-\d{2}-\d{2}/g) || [];
  70. const enrollmentDate = allDates[0] || "";
  71. return {
  72. enrollmentDate,
  73. enrollmentYear: enrollmentDate ? Number(enrollmentDate.slice(0, 4)) : 0
  74. };
  75. }
  76. function filterSemestersByEnrollmentYear(semesters, enrollmentYear) {
  77. if (!enrollmentYear) return semesters;
  78. const filtered = semesters.filter((semester) => {
  79. const startYear = Number(String(semester.schoolYear || "").split("-")[0]);
  80. return startYear >= enrollmentYear;
  81. });
  82. return filtered.length ? filtered : semesters;
  83. }
  84. function normalizeEnglishDate(dateText) {
  85. const parsed = new Date(String(dateText || ""));
  86. if (Number.isNaN(parsed.getTime())) return "";
  87. const year = parsed.getFullYear();
  88. const month = String(parsed.getMonth() + 1).padStart(2, "0");
  89. const day = String(parsed.getDate()).padStart(2, "0");
  90. return `${year}-${month}-${day}`;
  91. }
  92. function parseCalendarInfo(htmlText) {
  93. const html = String(htmlText || "");
  94. 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+)\)/);
  95. if (!match) {
  96. return {
  97. semesterStartDate: "",
  98. semesterTotalWeeks: 0
  99. };
  100. }
  101. return {
  102. semesterStartDate: normalizeEnglishDate(match[1]),
  103. semesterTotalWeeks: Number(match[3] || 0)
  104. };
  105. }
  106. function chineseSectionToNumber(text) {
  107. const mapping = {
  108. "一": 1,
  109. "二": 2,
  110. "三": 3,
  111. "四": 4,
  112. "五": 5,
  113. "六": 6,
  114. "七": 7,
  115. "八": 8,
  116. "九": 9,
  117. "十": 10,
  118. "十一": 11
  119. };
  120. return mapping[String(text || "").trim()] || 0;
  121. }
  122. function parseTimeSlotsFromHtml(htmlText) {
  123. const doc = new DOMParser().parseFromString(String(htmlText || ""), "text/html");
  124. const slots = [];
  125. doc.querySelectorAll("#manualArrangeCourseTable tbody tr").forEach((row) => {
  126. const cells = Array.from(row.querySelectorAll("td"));
  127. const sectionCell = cells.find((cell) => /第.+节/.test(cell.textContent || ""));
  128. if (!sectionCell) return;
  129. const text = sectionCell.textContent.replace(/\s+/g, " ").trim();
  130. const match = text.match(/第([一二三四五六七八九十十一]+)节\s*(\d{2}:\d{2})\s*-\s*(\d{2}:\d{2})/);
  131. if (!match) return;
  132. const sectionNumber = chineseSectionToNumber(match[1]);
  133. if (!sectionNumber) return;
  134. slots.push({
  135. number: sectionNumber,
  136. startTime: match[2],
  137. endTime: match[3]
  138. });
  139. });
  140. return slots.sort((a, b) => a.number - b.number);
  141. }
  142. function splitJsArgs(argsText) {
  143. const args = [];
  144. let current = "";
  145. let quote = "";
  146. let escaped = false;
  147. for (let i = 0; i < argsText.length; i++) {
  148. const ch = argsText[i];
  149. if (escaped) {
  150. current += ch;
  151. escaped = false;
  152. continue;
  153. }
  154. if (ch === "\\") {
  155. current += ch;
  156. escaped = true;
  157. continue;
  158. }
  159. if (quote) {
  160. current += ch;
  161. if (ch === quote) quote = "";
  162. continue;
  163. }
  164. if (ch === "'" || ch === "\"") {
  165. current += ch;
  166. quote = ch;
  167. continue;
  168. }
  169. if (ch === ",") {
  170. args.push(current.trim());
  171. current = "";
  172. continue;
  173. }
  174. current += ch;
  175. }
  176. if (current.trim()) {
  177. args.push(current.trim());
  178. }
  179. return args;
  180. }
  181. function unquoteJsLiteral(token) {
  182. const text = String(token || "").trim();
  183. if (!text || text === "null" || text === "undefined") return "";
  184. if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) {
  185. const quote = text[0];
  186. return text.slice(1, -1)
  187. .replace(/\\\\/g, "\\")
  188. .replace(new RegExp(`\\\\${quote}`, "g"), quote)
  189. .replace(/\\n/g, "\n")
  190. .replace(/\\r/g, "\r")
  191. .replace(/\\t/g, "\t");
  192. }
  193. return text;
  194. }
  195. function parseValidWeeksBitmap(bitmap) {
  196. const weeks = [];
  197. const text = String(bitmap || "");
  198. for (let i = 0; i < text.length; i++) {
  199. if (text[i] === "1" && i >= 1) {
  200. weeks.push(i);
  201. }
  202. }
  203. return weeks;
  204. }
  205. function normalizeWeeks(weeks) {
  206. return Array.from(new Set((weeks || []).filter((week) => Number.isInteger(week) && week > 0))).sort((a, b) => a - b);
  207. }
  208. function cleanCourseName(name) {
  209. return String(name || "")
  210. .replace(/\s*\([^()]*\)\s*$/, "")
  211. .trim();
  212. }
  213. function cleanPosition(position) {
  214. return String(position || "")
  215. .replace(/鹤壁工程技术学院/g, "")
  216. .replace(/\s+/g, " ")
  217. .trim();
  218. }
  219. function resolveTeachersForTaskActivityBlock(fullText, blockStartIndex) {
  220. const start = Math.max(0, blockStartIndex - 2500);
  221. const segment = fullText.slice(start, blockStartIndex);
  222. const teachersRegex = /var\s+teachers\s*=\s*\[([^]*?)\];/g;
  223. let lastTeachersBlock = "";
  224. let match;
  225. while ((match = teachersRegex.exec(segment)) !== null) {
  226. lastTeachersBlock = match[1] || "";
  227. }
  228. if (!lastTeachersBlock) return "";
  229. const names = [];
  230. const nameRegex = /name\s*:\s*(?:"([^"]*)"|'([^']*)')/g;
  231. let nameMatch;
  232. while ((nameMatch = nameRegex.exec(lastTeachersBlock)) !== null) {
  233. const name = (nameMatch[1] || nameMatch[2] || "").trim();
  234. if (name) names.push(name);
  235. }
  236. return Array.from(new Set(names)).join(",");
  237. }
  238. function mergeContiguousSections(courses) {
  239. const normalized = (courses || []).map((course) => ({
  240. ...course,
  241. weeks: normalizeWeeks(course.weeks)
  242. }));
  243. normalized.sort((a, b) => {
  244. const keyA = `${a.name}|${a.teacher}|${a.position}|${a.day}|${a.weeks.join(",")}`;
  245. const keyB = `${b.name}|${b.teacher}|${b.position}|${b.day}|${b.weeks.join(",")}`;
  246. if (keyA < keyB) return -1;
  247. if (keyA > keyB) return 1;
  248. return a.startSection - b.startSection;
  249. });
  250. const merged = [];
  251. normalized.forEach((course) => {
  252. const previous = merged[merged.length - 1];
  253. const canMerge = previous
  254. && previous.name === course.name
  255. && previous.teacher === course.teacher
  256. && previous.position === course.position
  257. && previous.day === course.day
  258. && previous.weeks.join(",") === course.weeks.join(",")
  259. && previous.endSection + 1 >= course.startSection;
  260. if (canMerge) {
  261. previous.endSection = Math.max(previous.endSection, course.endSection);
  262. } else {
  263. merged.push({ ...course });
  264. }
  265. });
  266. return merged;
  267. }
  268. function parseCoursesFromTaskActivityScript(htmlText) {
  269. const text = String(htmlText || "");
  270. const unitCountMatch = text.match(/\bvar\s+unitCount\s*=\s*(\d+)\s*;/);
  271. const unitCount = unitCountMatch ? Number(unitCountMatch[1]) : 0;
  272. if (!unitCount) return [];
  273. const courses = [];
  274. const blockRegex = /activity\s*=\s*new\s+TaskActivity\(([^]*?)\)\s*;([\s\S]*?)(?=activity\s*=\s*new\s+TaskActivity\(|table\d+\.marshalTable|$)/g;
  275. let match;
  276. while ((match = blockRegex.exec(text)) !== null) {
  277. const args = splitJsArgs(match[1] || "");
  278. if (args.length < 7) continue;
  279. let teacher = unquoteJsLiteral(args[1]);
  280. if (/join\s*\(/.test(String(args[1] || ""))) {
  281. teacher = resolveTeachersForTaskActivityBlock(text, match.index) || teacher;
  282. }
  283. const name = cleanCourseName(unquoteJsLiteral(args[3]));
  284. const position = cleanPosition(unquoteJsLiteral(args[5]));
  285. const weeks = normalizeWeeks(parseValidWeeksBitmap(unquoteJsLiteral(args[6])));
  286. if (!name) continue;
  287. const indexBlock = match[2] || "";
  288. const indexRegex = /index\s*=\s*(?:(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)|(\d+))\s*;\s*table\d+\.activities\[index\]/g;
  289. let indexMatch;
  290. while ((indexMatch = indexRegex.exec(indexBlock)) !== null) {
  291. let linearIndex = -1;
  292. if (indexMatch[1] != null && indexMatch[2] != null) {
  293. linearIndex = Number(indexMatch[1]) * unitCount + Number(indexMatch[2]);
  294. } else if (indexMatch[3] != null) {
  295. linearIndex = Number(indexMatch[3]);
  296. }
  297. if (linearIndex < 0) continue;
  298. const day = Math.floor(linearIndex / unitCount) + 1;
  299. const section = (linearIndex % unitCount) + 1;
  300. if (day < 1 || day > 7) continue;
  301. courses.push({
  302. name,
  303. teacher: teacher || "未知教师",
  304. position: position || "待定",
  305. day,
  306. startSection: section,
  307. endSection: section,
  308. weeks
  309. });
  310. }
  311. }
  312. return mergeContiguousSections(courses);
  313. }
  314. async function fetchEntryParams() {
  315. const entryHtml = await requestText(`${getBaseOrigin()}/eams/courseTableForStd.action?&sf_request_type=ajax`, {
  316. method: "GET",
  317. headers: {
  318. "x-requested-with": "XMLHttpRequest"
  319. }
  320. });
  321. return parseEntryParams(entryHtml);
  322. }
  323. async function fetchStudentProfile() {
  324. const profileHtml = await requestText(`${getBaseOrigin()}/eams/stdInfoApply!stdInfoCheck.action?_=${Date.now()}`, {
  325. method: "GET",
  326. headers: {
  327. accept: "text/html, */*; q=0.01",
  328. "x-requested-with": "XMLHttpRequest"
  329. }
  330. });
  331. return parseStudentProfile(profileHtml);
  332. }
  333. async function fetchSemesters(tagId) {
  334. const semesterRaw = await requestText(`${getBaseOrigin()}/eams/dataQuery.action?sf_request_type=ajax`, {
  335. method: "POST",
  336. headers: {
  337. "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
  338. },
  339. body: `tagId=${encodeURIComponent(tagId)}&dataType=semesterCalendar&empty=false`
  340. });
  341. return parseSemesterResponse(semesterRaw);
  342. }
  343. async function fetchCourseHtml(studentId, semesterId) {
  344. return requestText(`${getBaseOrigin()}/eams/courseTableForStd!courseTable.action?sf_request_type=ajax`, {
  345. method: "POST",
  346. headers: {
  347. accept: "*/*",
  348. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  349. "x-requested-with": "XMLHttpRequest"
  350. },
  351. body: [
  352. "ignoreHead=1",
  353. "setting.kind=std",
  354. "startWeek=",
  355. `semester.id=${encodeURIComponent(semesterId)}`,
  356. `ids=${encodeURIComponent(studentId)}`
  357. ].join("&")
  358. });
  359. }
  360. async function fetchCalendarInfo(semesterId) {
  361. const calendarHtml = await requestText(`${getBaseOrigin()}/eams/base/calendar-info.action`, {
  362. method: "POST",
  363. headers: {
  364. accept: "text/html, */*; q=0.01",
  365. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  366. "x-requested-with": "XMLHttpRequest"
  367. },
  368. body: `version=1&semesterId=${encodeURIComponent(semesterId)}`
  369. });
  370. return parseCalendarInfo(calendarHtml);
  371. }
  372. async function selectSemester(semesters) {
  373. const recent = semesters.slice(-8);
  374. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  375. "选择要导入的学期",
  376. JSON.stringify(recent.map((semester) => semester.name || semester.id)),
  377. recent.length - 1
  378. );
  379. if (selectedIndex === null || selectedIndex === -1) {
  380. throw new Error("已取消导入。");
  381. }
  382. return recent[selectedIndex];
  383. }
  384. async function runImportFlow() {
  385. showToast("正在识别课表参数...");
  386. const params = await fetchEntryParams();
  387. if (!params.studentId || !params.tagId) {
  388. throw new Error("未能自动识别学生ID或学期参数");
  389. }
  390. showToast("正在获取学籍信息...");
  391. const studentProfile = await fetchStudentProfile();
  392. showToast("正在获取学期列表...");
  393. const semesters = filterSemestersByEnrollmentYear(
  394. await fetchSemesters(params.tagId),
  395. studentProfile.enrollmentYear
  396. );
  397. if (!semesters.length) {
  398. throw new Error("未获取到学期列表。");
  399. }
  400. const selectedSemester = await selectSemester(semesters);
  401. showToast(`正在获取 ${selectedSemester.name} 课表...`);
  402. const courseHtml = await fetchCourseHtml(params.studentId, selectedSemester.id);
  403. const timeSlots = parseTimeSlotsFromHtml(courseHtml);
  404. const courses = parseCoursesFromTaskActivityScript(courseHtml);
  405. const calendarInfo = await fetchCalendarInfo(selectedSemester.id);
  406. if (!courses.length) {
  407. console.log(courseHtml);
  408. throw new Error("未解析到课程数据,请确认当前学期有课表。");
  409. }
  410. const config = {
  411. firstDayOfWeek: 1
  412. };
  413. if (calendarInfo.semesterStartDate) {
  414. config.semesterStartDate = calendarInfo.semesterStartDate;
  415. }
  416. if (calendarInfo.semesterTotalWeeks) {
  417. config.semesterTotalWeeks = calendarInfo.semesterTotalWeeks;
  418. }
  419. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  420. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  421. if (timeSlots.length) {
  422. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  423. }
  424. showToast(`导入完成,共 ${courses.length} 门课程`);
  425. if (typeof AndroidBridge !== "undefined" && AndroidBridge.notifyTaskCompletion) {
  426. AndroidBridge.notifyTaskCompletion();
  427. }
  428. }
  429. (async function bootstrap() {
  430. try {
  431. await runImportFlow();
  432. } catch (error) {
  433. console.error(error);
  434. showToast(`导入失败:${error.message || error}`);
  435. }
  436. })();
  437. })();