guit_01.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  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. if (course.startSection >= 5 && course.endSection <= 11) {
  148. return course;
  149. }
  150. const timeSlotMap = getTimeSlotMap(scheduleType);
  151. const startSlot = timeSlotMap.get(course.startSection);
  152. const endSlot = timeSlotMap.get(course.endSection);
  153. if (!startSlot || !endSlot) {
  154. return course;
  155. }
  156. const primaryTimeSlotMap = new Map(PRIMARY_TIME_SLOTS.map((slot) => [slot.number, slot]));
  157. const primaryStartSlot = primaryTimeSlotMap.get(course.startSection);
  158. const primaryEndSlot = primaryTimeSlotMap.get(course.endSection);
  159. if (
  160. primaryStartSlot &&
  161. primaryEndSlot &&
  162. primaryStartSlot.startTime === startSlot.startTime &&
  163. primaryEndSlot.endTime === endSlot.endTime
  164. ) {
  165. return course;
  166. }
  167. return {
  168. ...course,
  169. isCustomTime: true,
  170. customStartTime: startSlot.startTime,
  171. customEndTime: endSlot.endTime
  172. };
  173. }
  174. function parseCellCourses(cellHtml, day, row, totalWeeks) {
  175. const lines = normalizeHtmlLines(cellHtml);
  176. if (!lines.length) return [];
  177. const courses = [];
  178. for (let index = 0; index < lines.length; index += 2) {
  179. const locationLine = lines[index];
  180. const weekLine = lines[index + 1] || "";
  181. const sectionInfo = parseSectionAndRoom(locationLine);
  182. if (!sectionInfo) continue;
  183. const weeks = parseWeeks(weekLine);
  184. courses.push(fillCustomTime({
  185. name: String(row.cname || "").trim(),
  186. teacher: String(row.TeacherName || row.assteachername || "").trim() || "未知教师",
  187. position: sectionInfo.position || "待定",
  188. day,
  189. startSection: sectionInfo.startSection,
  190. endSection: sectionInfo.endSection,
  191. weeks: weeks.length ? weeks : Array.from({ length: totalWeeks }, (_, i) => i + 1)
  192. }));
  193. }
  194. return courses;
  195. }
  196. function deduplicateCourses(courses) {
  197. const seen = new Map();
  198. courses.forEach((course) => {
  199. const key = [
  200. course.name,
  201. course.teacher,
  202. course.position,
  203. course.day,
  204. course.startSection,
  205. course.endSection,
  206. course.weeks.join(",")
  207. ].join("|");
  208. if (!seen.has(key)) {
  209. seen.set(key, course);
  210. }
  211. });
  212. return Array.from(seen.values());
  213. }
  214. async function requestJson(path, options = {}) {
  215. const response = await fetch(`${getBaseOrigin()}${path}`, {
  216. credentials: "include",
  217. ...options
  218. });
  219. const text = await response.text();
  220. let data;
  221. try {
  222. data = JSON.parse(text);
  223. } catch (error) {
  224. throw new Error(`接口 ${path} 返回了非 JSON 内容,请确认已登录并位于正确页面。`);
  225. }
  226. if (!response.ok) {
  227. throw new Error(`接口 ${path} 请求失败,HTTP ${response.status}`);
  228. }
  229. return data;
  230. }
  231. async function requestText(path, options = {}) {
  232. const response = await fetch(`${getBaseOrigin()}${path}`, {
  233. credentials: "include",
  234. ...options
  235. });
  236. if (!response.ok) {
  237. throw new Error(`接口 ${path} 请求失败,HTTP ${response.status}`);
  238. }
  239. return response.text();
  240. }
  241. function parseStudentIdFromHtml(html) {
  242. const idMatch = String(html || "").match(/name=["']stid["'][^>]*value=["']([^"']+)["']/i);
  243. return idMatch ? idMatch[1].trim() : "";
  244. }
  245. async function fetchStudentProfile() {
  246. const html = await requestText("/Admin_Areas/StInfo/studentInfo");
  247. const studentId = parseStudentIdFromHtml(html);
  248. if (!studentId) {
  249. throw new Error("未能从个人信息页面解析出学号。");
  250. }
  251. const body = new URLSearchParams({ stid: studentId });
  252. const profile = await requestJson("/Admin_Areas/StInfo/getStInfo", {
  253. method: "POST",
  254. headers: {
  255. Accept: "application/json, text/javascript, */*; q=0.01",
  256. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  257. "X-Requested-With": "XMLHttpRequest"
  258. },
  259. body: body.toString()
  260. });
  261. return {
  262. studentId,
  263. enrolldate: String(profile?.enrolldate || "").trim(),
  264. grade: String(profile?.grade || "").trim()
  265. };
  266. }
  267. function getEnrollmentThreshold(profile) {
  268. const enrollmentDate = String(profile?.enrolldate || "").trim();
  269. if (enrollmentDate) {
  270. return enrollmentDate;
  271. }
  272. const grade = Number(profile?.grade || 0);
  273. if (grade >= 1900 && grade <= 2100) {
  274. return `${grade}-01-01`;
  275. }
  276. return "";
  277. }
  278. async function fetchTermList() {
  279. const terms = await requestJson("/Admin_Areas/Res/GetTermInfoAll", {
  280. method: "POST",
  281. headers: {
  282. Accept: "application/json, text/javascript, */*; q=0.01",
  283. "X-Requested-With": "XMLHttpRequest"
  284. }
  285. });
  286. if (!Array.isArray(terms) || !terms.length) {
  287. throw new Error("未获取到学期信息。");
  288. }
  289. const filteredTerms = terms.filter((term) => {
  290. if (!term || !term.term || !term.startdate) return false;
  291. const name = String(term.termname || "");
  292. return /学年第[一二三四五六七八九十]+学期/.test(name);
  293. });
  294. return filteredTerms.length ? filteredTerms : terms.filter((term) => term && term.term && term.startdate);
  295. }
  296. function filterTermsByEnrollment(terms, enrollmentThreshold) {
  297. if (!enrollmentThreshold) return terms;
  298. const filtered = terms.filter((term) => {
  299. return String(term.enddate || term.startdate || "") >= enrollmentThreshold;
  300. });
  301. return filtered.length ? filtered : terms;
  302. }
  303. function getDefaultTermIndex(terms) {
  304. const today = getTodayString();
  305. const currentIndex = terms.findIndex((term) => {
  306. return term.startdate <= today && today <= String(term.enddate || "9999-12-31");
  307. });
  308. if (currentIndex >= 0) return currentIndex;
  309. const regularIndex = terms.findIndex((term) => /学期/.test(String(term.termname || "")));
  310. return regularIndex >= 0 ? regularIndex : 0;
  311. }
  312. async function selectTerm(terms) {
  313. const items = terms.map((term) => {
  314. return String(term.termname || term.term);
  315. });
  316. const defaultIndex = getDefaultTermIndex(terms);
  317. if (typeof window.AndroidBridgePromise === "undefined") {
  318. return terms[defaultIndex];
  319. }
  320. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  321. "选择要导入的学期",
  322. JSON.stringify(items),
  323. defaultIndex
  324. );
  325. if (selectedIndex === null || selectedIndex === -1) {
  326. throw new Error("已取消导入。");
  327. }
  328. return terms[selectedIndex];
  329. }
  330. async function fetchCoursePage(termCode, page, rowsPerPage) {
  331. const body = new URLSearchParams({
  332. term: termCode,
  333. page: String(page),
  334. rows: String(rowsPerPage)
  335. });
  336. const data = await requestJson("/Admin_Areas/StInfo/GetCourseQuery", {
  337. method: "POST",
  338. headers: {
  339. Accept: "application/json, text/javascript, */*; q=0.01",
  340. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  341. "X-Requested-With": "XMLHttpRequest"
  342. },
  343. body: body.toString()
  344. });
  345. if (!data || !Array.isArray(data.rows)) {
  346. throw new Error("课表接口未返回有效数据。");
  347. }
  348. return data;
  349. }
  350. async function fetchAllCourseRows(termCode) {
  351. const rowsPerPage = 50;
  352. const firstPage = await fetchCoursePage(termCode, 1, rowsPerPage);
  353. const allRows = [...firstPage.rows];
  354. const total = Number(firstPage.total || allRows.length);
  355. const totalPages = Math.max(1, Math.ceil(total / rowsPerPage));
  356. for (let page = 2; page <= totalPages; page++) {
  357. const pageData = await fetchCoursePage(termCode, page, rowsPerPage);
  358. allRows.push(...pageData.rows);
  359. }
  360. return allRows;
  361. }
  362. function buildCourses(rows, totalWeeks) {
  363. const courses = [];
  364. rows.forEach((row) => {
  365. Object.entries(DAY_FIELD_MAP).forEach(([field, day]) => {
  366. const cellValue = row[field];
  367. if (!cellValue) return;
  368. courses.push(...parseCellCourses(cellValue, day, row, totalWeeks));
  369. });
  370. });
  371. return deduplicateCourses(courses);
  372. }
  373. async function saveConfig(term) {
  374. const config = {
  375. semesterStartDate: String(term.startdate),
  376. semesterTotalWeeks: Number(term.weeknum || 20),
  377. firstDayOfWeek: 1
  378. };
  379. await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
  380. }
  381. async function saveTimeSlots() {
  382. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(PRIMARY_TIME_SLOTS));
  383. }
  384. async function saveCourses(courses) {
  385. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  386. }
  387. async function runImportFlow() {
  388. try {
  389. showToast("正在获取个人信息...");
  390. const studentProfile = await fetchStudentProfile();
  391. const enrollmentThreshold = getEnrollmentThreshold(studentProfile);
  392. showToast("正在获取学期信息...");
  393. const terms = filterTermsByEnrollment(await fetchTermList(), enrollmentThreshold);
  394. const selectedTerm = await selectTerm(terms);
  395. showToast(`正在获取 ${selectedTerm.termname || selectedTerm.term} 课表...`);
  396. const rows = await fetchAllCourseRows(String(selectedTerm.term));
  397. const totalWeeks = Number(selectedTerm.weeknum || 20);
  398. const courses = buildCourses(rows, totalWeeks);
  399. if (!courses.length) {
  400. throw new Error("未解析到课程,请确认当前账号已在教务系统中可查看课表。");
  401. }
  402. if (typeof window.AndroidBridgePromise === "undefined") {
  403. console.log("Selected term:", selectedTerm);
  404. console.log("Courses:", courses);
  405. console.log("Time slots:", PRIMARY_TIME_SLOTS);
  406. alert(`解析完成,共 ${courses.length} 门课程。请查看控制台输出。`);
  407. return;
  408. }
  409. await saveConfig(selectedTerm);
  410. await saveTimeSlots();
  411. await saveCourses(courses);
  412. showToast(`导入完成,共 ${courses.length} 门课程`);
  413. if (typeof AndroidBridge !== "undefined" && AndroidBridge.notifyTaskCompletion) {
  414. AndroidBridge.notifyTaskCompletion();
  415. }
  416. } catch (error) {
  417. console.error(error);
  418. showToast(`导入失败: ${error.message}`);
  419. }
  420. }
  421. runImportFlow();