guit_01.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. const PRIMARY_TIME_SLOTS = [
  2. { number: 1, startTime: "09:00", endTime: "09:40" },
  3. { number: 2, startTime: "09:45", endTime: "10:25" },
  4. { number: 3, startTime: "11:10", endTime: "11:50" },
  5. { number: 4, startTime: "11:55", endTime: "12:35" },
  6. { number: 5, startTime: "14:30", endTime: "15:10" },
  7. { number: 6, startTime: "15:15", endTime: "15:55" },
  8. { number: 7, startTime: "16:15", endTime: "16:55" },
  9. { number: 8, startTime: "17:00", endTime: "17:40" },
  10. { number: 9, startTime: "19:00", endTime: "19:40" },
  11. { number: 10, startTime: "19:45", endTime: "20:25" },
  12. { number: 11, startTime: "20:30", endTime: "21:10" }
  13. ];
  14. const SCHEDULE_TIME_MAP = {
  15. "一": [
  16. { number: 1, startTime: "09:00", endTime: "09:40" },
  17. { number: 2, startTime: "09:45", endTime: "10:25" },
  18. { number: 3, startTime: "10:40", endTime: "11:20" },
  19. { number: 4, startTime: "11:25", endTime: "12:05" },
  20. { number: 5, startTime: "14:30", endTime: "15:10" },
  21. { number: 6, startTime: "15:15", endTime: "15:55" },
  22. { number: 7, startTime: "16:15", endTime: "16:55" },
  23. { number: 8, startTime: "17:00", endTime: "17:40" },
  24. { number: 9, startTime: "19:00", endTime: "19:40" },
  25. { number: 10, startTime: "19:45", endTime: "20:25" },
  26. { number: 11, startTime: "20:30", endTime: "21:10" }
  27. ],
  28. "二": [
  29. { number: 1, startTime: "09:00", endTime: "09:40" },
  30. { number: 2, startTime: "09:45", endTime: "10:25" },
  31. { number: 3, startTime: "10:55", endTime: "11:20" },
  32. { number: 4, startTime: "11:40", endTime: "12:20" },
  33. { number: 5, startTime: "14:30", endTime: "15:10" },
  34. { number: 6, startTime: "15:15", endTime: "15:55" },
  35. { number: 7, startTime: "16:15", endTime: "16:55" },
  36. { number: 8, startTime: "17:00", endTime: "17:40" },
  37. { number: 9, startTime: "19:00", endTime: "19:40" },
  38. { number: 10, startTime: "19:45", endTime: "20:25" },
  39. { number: 11, startTime: "20:30", endTime: "21:10" }
  40. ],
  41. "三": PRIMARY_TIME_SLOTS
  42. };
  43. const BUILDING_SCHEDULE_MAP = {
  44. C: "一",
  45. E: "一",
  46. G: "一",
  47. D: "二",
  48. F: "二",
  49. H: "二",
  50. A: "三",
  51. B: "三",
  52. J: "三",
  53. K: "三",
  54. L: "三",
  55. M: "三"
  56. };
  57. const DAY_FIELD_MAP = {
  58. mon: 1,
  59. tu: 2,
  60. wes: 3,
  61. tur: 4,
  62. fri: 5,
  63. sat: 6,
  64. sun: 7
  65. };
  66. function showToast(message) {
  67. if (typeof AndroidBridge !== "undefined" && AndroidBridge.showToast) {
  68. AndroidBridge.showToast(message);
  69. } else {
  70. console.log(message);
  71. }
  72. }
  73. function getBaseOrigin() {
  74. return window.location.origin;
  75. }
  76. function getTodayString() {
  77. const now = new Date();
  78. const year = now.getFullYear();
  79. const month = String(now.getMonth() + 1).padStart(2, "0");
  80. const day = String(now.getDate()).padStart(2, "0");
  81. return `${year}-${month}-${day}`;
  82. }
  83. function normalizeHtmlLines(html) {
  84. return String(html || "")
  85. .replace(/<br\s*\/?>/gi, "\n")
  86. .replace(/&nbsp;/gi, " ")
  87. .replace(/<[^>]+>/g, "")
  88. .split("\n")
  89. .map((line) => line.trim())
  90. .filter(Boolean);
  91. }
  92. function parseWeeks(rawText) {
  93. const text = String(rawText || "")
  94. .replace(/\s+/g, "")
  95. .replace(/周/g, "")
  96. .replace(/,/g, ",")
  97. .replace(/、/g, ",");
  98. if (!text) return [];
  99. const weeks = new Set();
  100. text.split(",").forEach((segment) => {
  101. if (!segment) return;
  102. const isOdd = /单/.test(segment);
  103. const isEven = /双/.test(segment);
  104. const cleaned = segment.replace(/[单双]/g, "");
  105. const match = cleaned.match(/^(\d+)(?:-(\d+))?$/);
  106. if (!match) return;
  107. const start = Number(match[1]);
  108. const end = Number(match[2] || match[1]);
  109. for (let week = start; week <= end; week++) {
  110. if (isOdd && week % 2 === 0) continue;
  111. if (isEven && week % 2 !== 0) continue;
  112. weeks.add(week);
  113. }
  114. });
  115. return Array.from(weeks).sort((a, b) => a - b);
  116. }
  117. function parseSectionAndRoom(rawLocation) {
  118. const value = String(rawLocation || "").trim();
  119. const match = value.match(/^(\d{2})(\d{2})(.*)$/);
  120. if (!match) return null;
  121. return {
  122. startSection: Number(match[1]),
  123. endSection: Number(match[2]),
  124. position: match[3].trim() || "待定"
  125. };
  126. }
  127. function getBuildingCode(position) {
  128. const match = String(position || "").trim().match(/^([A-Z])/i);
  129. return match ? match[1].toUpperCase() : "";
  130. }
  131. function getScheduleTypeByPosition(position) {
  132. const buildingCode = getBuildingCode(position);
  133. return BUILDING_SCHEDULE_MAP[buildingCode] || "三";
  134. }
  135. function getTimeSlotMap(scheduleType) {
  136. const map = new Map();
  137. (SCHEDULE_TIME_MAP[scheduleType] || PRIMARY_TIME_SLOTS).forEach((slot) => {
  138. map.set(slot.number, slot);
  139. });
  140. return map;
  141. }
  142. function fillCustomTime(course) {
  143. const scheduleType = getScheduleTypeByPosition(course.position);
  144. if (scheduleType === "三") {
  145. return course;
  146. }
  147. const timeSlotMap = getTimeSlotMap(scheduleType);
  148. const startSlot = timeSlotMap.get(course.startSection);
  149. const endSlot = timeSlotMap.get(course.endSection);
  150. if (!startSlot || !endSlot) {
  151. return course;
  152. }
  153. return {
  154. ...course,
  155. isCustomTime: true,
  156. customStartTime: startSlot.startTime,
  157. customEndTime: endSlot.endTime
  158. };
  159. }
  160. function parseCellCourses(cellHtml, day, row, totalWeeks) {
  161. const lines = normalizeHtmlLines(cellHtml);
  162. if (!lines.length) return [];
  163. const courses = [];
  164. for (let index = 0; index < lines.length; index += 2) {
  165. const locationLine = lines[index];
  166. const weekLine = lines[index + 1] || "";
  167. const sectionInfo = parseSectionAndRoom(locationLine);
  168. if (!sectionInfo) continue;
  169. const weeks = parseWeeks(weekLine);
  170. courses.push(fillCustomTime({
  171. name: String(row.cname || "").trim(),
  172. teacher: String(row.TeacherName || row.assteachername || "").trim() || "未知教师",
  173. position: sectionInfo.position || "待定",
  174. day,
  175. startSection: sectionInfo.startSection,
  176. endSection: sectionInfo.endSection,
  177. weeks: weeks.length ? weeks : Array.from({ length: totalWeeks }, (_, i) => i + 1)
  178. }));
  179. }
  180. return courses;
  181. }
  182. function deduplicateCourses(courses) {
  183. const seen = new Map();
  184. courses.forEach((course) => {
  185. const key = [
  186. course.name,
  187. course.teacher,
  188. course.position,
  189. course.day,
  190. course.startSection,
  191. course.endSection,
  192. course.weeks.join(",")
  193. ].join("|");
  194. if (!seen.has(key)) {
  195. seen.set(key, course);
  196. }
  197. });
  198. return Array.from(seen.values());
  199. }
  200. async function requestJson(path, options = {}) {
  201. const response = await fetch(`${getBaseOrigin()}${path}`, {
  202. credentials: "include",
  203. ...options
  204. });
  205. const text = await response.text();
  206. let data;
  207. try {
  208. data = JSON.parse(text);
  209. } catch (error) {
  210. throw new Error(`接口 ${path} 返回了非 JSON 内容,请确认已登录并位于正确页面。`);
  211. }
  212. if (!response.ok) {
  213. throw new Error(`接口 ${path} 请求失败,HTTP ${response.status}`);
  214. }
  215. return data;
  216. }
  217. async function requestText(path, options = {}) {
  218. const response = await fetch(`${getBaseOrigin()}${path}`, {
  219. credentials: "include",
  220. ...options
  221. });
  222. if (!response.ok) {
  223. throw new Error(`接口 ${path} 请求失败,HTTP ${response.status}`);
  224. }
  225. return response.text();
  226. }
  227. function parseStudentIdFromHtml(html) {
  228. const idMatch = String(html || "").match(/name=["']stid["'][^>]*value=["']([^"']+)["']/i);
  229. return idMatch ? idMatch[1].trim() : "";
  230. }
  231. async function fetchStudentProfile() {
  232. const html = await requestText("/Admin_Areas/StInfo/studentInfo");
  233. const studentId = parseStudentIdFromHtml(html);
  234. if (!studentId) {
  235. throw new Error("未能从个人信息页面解析出学号。");
  236. }
  237. const body = new URLSearchParams({ stid: studentId });
  238. const profile = await requestJson("/Admin_Areas/StInfo/getStInfo", {
  239. method: "POST",
  240. headers: {
  241. Accept: "application/json, text/javascript, */*; q=0.01",
  242. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  243. "X-Requested-With": "XMLHttpRequest"
  244. },
  245. body: body.toString()
  246. });
  247. return {
  248. studentId,
  249. enrolldate: String(profile?.enrolldate || "").trim(),
  250. grade: String(profile?.grade || "").trim()
  251. };
  252. }
  253. function getEnrollmentThreshold(profile) {
  254. const enrollmentDate = String(profile?.enrolldate || "").trim();
  255. if (enrollmentDate) {
  256. return enrollmentDate;
  257. }
  258. const grade = Number(profile?.grade || 0);
  259. if (grade >= 1900 && grade <= 2100) {
  260. return `${grade}-01-01`;
  261. }
  262. return "";
  263. }
  264. async function fetchTermList() {
  265. const terms = await requestJson("/Admin_Areas/Res/GetTermInfoAll", {
  266. method: "POST",
  267. headers: {
  268. Accept: "application/json, text/javascript, */*; q=0.01",
  269. "X-Requested-With": "XMLHttpRequest"
  270. }
  271. });
  272. if (!Array.isArray(terms) || !terms.length) {
  273. throw new Error("未获取到学期信息。");
  274. }
  275. const filteredTerms = terms.filter((term) => {
  276. if (!term || !term.term || !term.startdate) return false;
  277. const name = String(term.termname || "");
  278. return /学年第[一二三四五六七八九十]+学期/.test(name);
  279. });
  280. return filteredTerms.length ? filteredTerms : terms.filter((term) => term && term.term && term.startdate);
  281. }
  282. function filterTermsByEnrollment(terms, enrollmentThreshold) {
  283. if (!enrollmentThreshold) return terms;
  284. const filtered = terms.filter((term) => {
  285. return String(term.enddate || term.startdate || "") >= enrollmentThreshold;
  286. });
  287. return filtered.length ? filtered : terms;
  288. }
  289. function getDefaultTermIndex(terms) {
  290. const today = getTodayString();
  291. const currentIndex = terms.findIndex((term) => {
  292. return term.startdate <= today && today <= String(term.enddate || "9999-12-31");
  293. });
  294. if (currentIndex >= 0) return currentIndex;
  295. const regularIndex = terms.findIndex((term) => /学期/.test(String(term.termname || "")));
  296. return regularIndex >= 0 ? regularIndex : 0;
  297. }
  298. async function selectTerm(terms) {
  299. const items = terms.map((term) => {
  300. return String(term.termname || term.term);
  301. });
  302. const defaultIndex = getDefaultTermIndex(terms);
  303. if (typeof window.AndroidBridgePromise === "undefined") {
  304. return terms[defaultIndex];
  305. }
  306. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  307. "选择要导入的学期",
  308. JSON.stringify(items),
  309. defaultIndex
  310. );
  311. if (selectedIndex === null || selectedIndex === -1) {
  312. throw new Error("已取消导入。");
  313. }
  314. return terms[selectedIndex];
  315. }
  316. async function fetchCoursePage(termCode, page, rowsPerPage) {
  317. const body = new URLSearchParams({
  318. term: termCode,
  319. page: String(page),
  320. rows: String(rowsPerPage)
  321. });
  322. const data = await requestJson("/Admin_Areas/StInfo/GetCourseQuery", {
  323. method: "POST",
  324. headers: {
  325. Accept: "application/json, text/javascript, */*; q=0.01",
  326. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  327. "X-Requested-With": "XMLHttpRequest"
  328. },
  329. body: body.toString()
  330. });
  331. if (!data || !Array.isArray(data.rows)) {
  332. throw new Error("课表接口未返回有效数据。");
  333. }
  334. return data;
  335. }
  336. async function fetchAllCourseRows(termCode) {
  337. const rowsPerPage = 50;
  338. const firstPage = await fetchCoursePage(termCode, 1, rowsPerPage);
  339. const allRows = [...firstPage.rows];
  340. const total = Number(firstPage.total || allRows.length);
  341. const totalPages = Math.max(1, Math.ceil(total / rowsPerPage));
  342. for (let page = 2; page <= totalPages; page++) {
  343. const pageData = await fetchCoursePage(termCode, page, rowsPerPage);
  344. allRows.push(...pageData.rows);
  345. }
  346. return allRows;
  347. }
  348. function buildCourses(rows, totalWeeks) {
  349. const courses = [];
  350. rows.forEach((row) => {
  351. Object.entries(DAY_FIELD_MAP).forEach(([field, day]) => {
  352. const cellValue = row[field];
  353. if (!cellValue) return;
  354. courses.push(...parseCellCourses(cellValue, day, row, totalWeeks));
  355. });
  356. });
  357. return deduplicateCourses(courses);
  358. }
  359. async function saveConfig(term) {
  360. const config = {
  361. semesterStartDate: String(term.startdate),
  362. semesterTotalWeeks: Number(term.weeknum || 20),
  363. firstDayOfWeek: 1
  364. };
  365. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  366. }
  367. async function saveTimeSlots() {
  368. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(PRIMARY_TIME_SLOTS));
  369. }
  370. async function saveCourses(courses) {
  371. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  372. }
  373. async function runImportFlow() {
  374. try {
  375. showToast("正在获取个人信息...");
  376. const studentProfile = await fetchStudentProfile();
  377. const enrollmentThreshold = getEnrollmentThreshold(studentProfile);
  378. showToast("正在获取学期信息...");
  379. const terms = filterTermsByEnrollment(await fetchTermList(), enrollmentThreshold);
  380. const selectedTerm = await selectTerm(terms);
  381. showToast(`正在获取 ${selectedTerm.termname || selectedTerm.term} 课表...`);
  382. const rows = await fetchAllCourseRows(String(selectedTerm.term));
  383. const totalWeeks = Number(selectedTerm.weeknum || 20);
  384. const courses = buildCourses(rows, totalWeeks);
  385. if (!courses.length) {
  386. throw new Error("未解析到课程,请确认当前账号已在教务系统中可查看课表。");
  387. }
  388. if (typeof window.AndroidBridgePromise === "undefined") {
  389. console.log("Selected term:", selectedTerm);
  390. console.log("Courses:", courses);
  391. console.log("Time slots:", PRIMARY_TIME_SLOTS);
  392. alert(`解析完成,共 ${courses.length} 门课程。请查看控制台输出。`);
  393. return;
  394. }
  395. await saveConfig(selectedTerm);
  396. await saveTimeSlots();
  397. await saveCourses(courses);
  398. showToast(`导入完成,共 ${courses.length} 门课程`);
  399. if (typeof AndroidBridge !== "undefined" && AndroidBridge.notifyTaskCompletion) {
  400. AndroidBridge.notifyTaskCompletion();
  401. }
  402. } catch (error) {
  403. console.error(error);
  404. showToast(`导入失败: ${error.message}`);
  405. }
  406. }
  407. runImportFlow();