njfu_01.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. const STANDARD_TIME_SLOTS = [
  2. { number: 1, startTime: "08:00", endTime: "08:45" },
  3. { number: 2, startTime: "08:55", endTime: "09:40" },
  4. { number: 3, startTime: "10:00", endTime: "10:45" },
  5. { number: 4, startTime: "10:55", endTime: "11:40" },
  6. { number: 5, startTime: "14:00", endTime: "14:45" },
  7. { number: 6, startTime: "14:50", endTime: "15:35" },
  8. { number: 7, startTime: "15:55", endTime: "16:40" },
  9. { number: 8, startTime: "16:45", endTime: "17:30" },
  10. { number: 9, startTime: "18:30", endTime: "19:15" },
  11. { number: 10, startTime: "19:20", endTime: "20:05" },
  12. { number: 11, startTime: "20:10", endTime: "20:55" }
  13. ];
  14. const CAMPUS_TIME_SLOTS = {
  15. "新庄校区": STANDARD_TIME_SLOTS,
  16. "淮安校区": STANDARD_TIME_SLOTS,
  17. "白马校区": [
  18. { number: 1, startTime: "08:30", endTime: "09:15" },
  19. { number: 2, startTime: "09:20", endTime: "10:05" },
  20. { number: 3, startTime: "10:25", endTime: "11:10" },
  21. { number: 4, startTime: "11:15", endTime: "12:00" },
  22. { number: 5, startTime: "14:00", endTime: "14:45" },
  23. { number: 6, startTime: "14:50", endTime: "15:35" },
  24. { number: 7, startTime: "15:55", endTime: "16:40" },
  25. { number: 8, startTime: "16:45", endTime: "17:30" },
  26. { number: 9, startTime: "18:30", endTime: "19:15" },
  27. { number: 10, startTime: "19:20", endTime: "20:05" },
  28. { number: 11, startTime: "20:10", endTime: "20:55" }
  29. ]
  30. };
  31. const CAMPUS_KEYWORDS = [
  32. { campus: "淮安校区", keywords: ["淮安校区"] },
  33. { campus: "白马校区", keywords: ["白马校区"] },
  34. { campus: "新庄校区", keywords: ["新庄校区"] }
  35. ];
  36. function cleanPosition(position) {
  37. return String(position || "")
  38. .replace(/^(新庄校区|淮安校区|白马校区)/, "")
  39. .replace(/[((]\d+人[))]\s*$/g, "")
  40. .trim() || "待定";
  41. }
  42. function showToast(message) {
  43. if (typeof window.AndroidBridge !== "undefined") {
  44. AndroidBridge.showToast(message);
  45. } else {
  46. console.log(message);
  47. }
  48. }
  49. function parseWeeks(rawText) {
  50. if (!rawText) return [];
  51. const weekPart = String(rawText)
  52. .replace(/\s+/g, "")
  53. .replace(/\(周\).*/, "")
  54. .replace(/周次[::]?/g, "");
  55. const weeks = new Set();
  56. weekPart.split(/[,,]/).forEach((segment) => {
  57. if (!segment) return;
  58. const isOdd = segment.includes("单");
  59. const isEven = segment.includes("双");
  60. const cleaned = segment.replace(/[单双周]/g, "");
  61. const match = cleaned.match(/^(\d+)(?:-(\d+))?$/);
  62. if (!match) return;
  63. const start = Number(match[1]);
  64. const end = Number(match[2] || match[1]);
  65. for (let week = start; week <= end; week++) {
  66. if (isOdd && week % 2 === 0) continue;
  67. if (isEven && week % 2 !== 0) continue;
  68. weeks.add(week);
  69. }
  70. });
  71. return Array.from(weeks).sort((a, b) => a - b);
  72. }
  73. function detectCampusOrNull(...texts) {
  74. const text = texts
  75. .filter(Boolean)
  76. .map((item) => String(item))
  77. .join(" ");
  78. for (const item of CAMPUS_KEYWORDS) {
  79. if (item.keywords.some((keyword) => text.includes(keyword))) {
  80. return item.campus;
  81. }
  82. }
  83. return null;
  84. }
  85. function readLineTexts(div) {
  86. const cloned = div.cloneNode(true);
  87. cloned.querySelectorAll(".item-box").forEach((node) => node.remove());
  88. return cloned.innerHTML
  89. .split(/<br\s*\/?>/i)
  90. .map((line) => line.replace(/<[^>]+>/g, "").trim())
  91. .filter(Boolean);
  92. }
  93. function extractCourseName(lines) {
  94. const metadataPrefixes = ["通知单编号", "班级", "备注"];
  95. const metadataKeywords = ["周", "节", "教师", "教室", "校区"];
  96. const nameLines = [];
  97. for (const line of lines) {
  98. if (!line) continue;
  99. if (metadataPrefixes.some((prefix) => line.startsWith(prefix))) break;
  100. if (metadataKeywords.some((keyword) => line.includes(keyword))) break;
  101. nameLines.push(line);
  102. }
  103. return nameLines.join("").trim();
  104. }
  105. function parseCourseBlock(blockHtml, fallbackDay) {
  106. const tempDiv = document.createElement("div");
  107. tempDiv.innerHTML = blockHtml;
  108. const lines = readLineTexts(tempDiv);
  109. if (!lines.length) return null;
  110. let name = extractCourseName(lines);
  111. const teacher = tempDiv.querySelector('font[title="教师"]')?.innerText.trim() || "未知";
  112. if (teacher && name && name !== teacher && name.endsWith(teacher)) {
  113. name = name.slice(0, -teacher.length).trim();
  114. }
  115. const positionRaw = tempDiv.querySelector('font[title="教室"]')?.innerText.trim() || "待定";
  116. const building = tempDiv.querySelector('font[title="教学楼"]')?.innerText.trim()
  117. || tempDiv.querySelector('font[name="jxlmc"]')?.innerText.trim()
  118. || "";
  119. const position = cleanPosition(positionRaw);
  120. const timeText = tempDiv.querySelector('font[title="周次(节次)"]')?.innerText.trim() || "";
  121. if (!timeText) return null;
  122. const weekMatch = timeText.match(/^(.*?)\(周\)/);
  123. const sectionMatch = timeText.match(/\[(\d+)(?:-(\d+))?(?:-(\d+))?(?:-(\d+))?节\]/);
  124. const weeks = parseWeeks(weekMatch ? weekMatch[1] : timeText);
  125. let startSection = 0;
  126. let endSection = 0;
  127. if (sectionMatch) {
  128. const values = sectionMatch.slice(1).filter(Boolean).map(Number);
  129. startSection = values[0];
  130. endSection = values[values.length - 1];
  131. }
  132. if (!name || !weeks.length || !startSection || !endSection) return null;
  133. return {
  134. name,
  135. teacher,
  136. position,
  137. day: fallbackDay,
  138. startSection,
  139. endSection,
  140. weeks,
  141. campus: detectCampusOrNull(positionRaw, building, lines.join(" "))
  142. };
  143. }
  144. function extractCoursesFromDoc(doc) {
  145. const table = doc.getElementById("timetable");
  146. if (!table) {
  147. throw new Error("未获取到课表表格,请确认当前账号已登录教务系统。");
  148. }
  149. const rows = Array.from(table.querySelectorAll("tr")).slice(1, -1);
  150. const courses = [];
  151. rows.forEach((row) => {
  152. const cells = Array.from(row.querySelectorAll("td"));
  153. cells.forEach((cell, index) => {
  154. const day = index + 1;
  155. const detailDivs = cell.querySelectorAll("div.kbcontent");
  156. detailDivs.forEach((div) => {
  157. const html = div.innerHTML.trim();
  158. if (!html || html === "&nbsp;") return;
  159. const blocks = html.split(/-{10,}\s*<br\s*\/?>/i).filter((item) => item.trim());
  160. if (!blocks.length) blocks.push(html);
  161. blocks.forEach((block) => {
  162. const course = parseCourseBlock(block, day);
  163. if (course) {
  164. courses.push(course);
  165. }
  166. });
  167. });
  168. });
  169. });
  170. const uniqueMap = new Map();
  171. courses.forEach((course) => {
  172. const key = [
  173. course.name,
  174. course.teacher,
  175. course.position,
  176. course.day,
  177. course.startSection,
  178. course.endSection,
  179. course.weeks.join(",")
  180. ].join("|");
  181. if (!uniqueMap.has(key)) {
  182. uniqueMap.set(key, course);
  183. }
  184. });
  185. return Array.from(uniqueMap.values());
  186. }
  187. function choosePrimaryCampus(courses) {
  188. for (const course of courses) {
  189. if (course.campus) {
  190. return course.campus;
  191. }
  192. }
  193. return "新庄校区";
  194. }
  195. function normalizeCourses(courses, primaryCampus) {
  196. return courses.map((course) => {
  197. return {
  198. name: course.name,
  199. teacher: course.teacher,
  200. position: course.position,
  201. day: course.day,
  202. startSection: course.startSection,
  203. endSection: course.endSection,
  204. weeks: course.weeks,
  205. campus: course.campus || primaryCampus
  206. };
  207. });
  208. }
  209. function parseSemesterOptions(doc) {
  210. const select = doc.getElementById("xnxq01id");
  211. if (!select) return { labels: [], values: [], defaultIndex: 0 };
  212. const labels = [];
  213. const values = [];
  214. let defaultIndex = 0;
  215. Array.from(select.querySelectorAll("option")).forEach((option) => {
  216. labels.push(option.innerText.trim());
  217. values.push(option.value);
  218. if (option.selected || option.hasAttribute("selected")) {
  219. defaultIndex = labels.length - 1;
  220. }
  221. });
  222. return { labels, values, defaultIndex };
  223. }
  224. async function fetchTermDoc(termValue) {
  225. const body = new URLSearchParams();
  226. if (termValue) body.append("xnxq01id", termValue);
  227. const response = await fetch("/jsxsd/xskb/xskb_list.do", {
  228. method: termValue ? "POST" : "GET",
  229. headers: termValue ? { "Content-Type": "application/x-www-form-urlencoded" } : undefined,
  230. body: termValue ? body.toString() : undefined,
  231. credentials: "include"
  232. });
  233. const html = await response.text();
  234. return new DOMParser().parseFromString(html, "text/html");
  235. }
  236. async function pickTerm(doc) {
  237. const { labels, values, defaultIndex } = parseSemesterOptions(doc);
  238. if (!labels.length || typeof window.AndroidBridgePromise === "undefined") {
  239. return { doc, termLabel: labels[defaultIndex] || "" };
  240. }
  241. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  242. "请选择要导入的学期",
  243. JSON.stringify(labels),
  244. defaultIndex
  245. );
  246. if (selectedIndex === null || selectedIndex === -1) {
  247. throw new Error("已取消导入");
  248. }
  249. if (selectedIndex === defaultIndex) {
  250. return { doc, termLabel: labels[selectedIndex] };
  251. }
  252. const selectedDoc = await fetchTermDoc(values[selectedIndex]);
  253. return { doc: selectedDoc, termLabel: labels[selectedIndex] };
  254. }
  255. async function saveToApp(courses, primaryCampus) {
  256. const timeSlots = CAMPUS_TIME_SLOTS[primaryCampus];
  257. const allWeeks = courses.flatMap((course) => course.weeks || []);
  258. const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
  259. if (typeof window.AndroidBridgePromise === "undefined") {
  260. console.log("Primary campus:", primaryCampus);
  261. console.log("Time slots:", timeSlots);
  262. console.log("Courses:", courses);
  263. alert(`解析完成:${primaryCampus},共 ${courses.length} 门课程。请查看控制台输出。`);
  264. return;
  265. }
  266. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
  267. semesterTotalWeeks,
  268. firstDayOfWeek: 1
  269. }));
  270. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  271. const appCourses = courses.map(({ campus, ...course }) => course);
  272. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(appCourses));
  273. }
  274. async function runImportFlow() {
  275. try {
  276. showToast("正在获取 NJFU 课表数据...");
  277. const initialDoc = await fetchTermDoc("");
  278. const { doc, termLabel } = await pickTerm(initialDoc);
  279. const parsedCourses = extractCoursesFromDoc(doc);
  280. if (!parsedCourses.length) {
  281. throw new Error("未解析到课程,请确认当前账号已登录教务系统。");
  282. }
  283. const primaryCampus = choosePrimaryCampus(parsedCourses);
  284. const courses = normalizeCourses(parsedCourses, primaryCampus);
  285. await saveToApp(courses, primaryCampus);
  286. const message = `导入完成:${primaryCampus}${termLabel ? ` ${termLabel}` : ""}`;
  287. showToast(message);
  288. if (typeof window.AndroidBridge !== "undefined") {
  289. AndroidBridge.notifyTaskCompletion();
  290. }
  291. } catch (error) {
  292. console.error(error);
  293. showToast(`导入失败: ${error.message}`);
  294. }
  295. }
  296. runImportFlow();