cmc_01.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. // 解析周次字符串,输出拾光课程表需要的数字数组。
  2. function parseWeeks(weekStr) {
  3. if (!weekStr) return [];
  4. const weeks = new Set();
  5. String(weekStr).split(',').forEach(part => {
  6. const trimmed = part.trim();
  7. if (!trimmed) return;
  8. if (trimmed.includes('-')) {
  9. const [start, end] = trimmed.split('-').map(n => parseInt(n, 10));
  10. if (!isNaN(start) && !isNaN(end) && start <= end) {
  11. for (let i = start; i <= end; i++) {
  12. weeks.add(i);
  13. }
  14. }
  15. return;
  16. }
  17. const week = parseInt(trimmed, 10);
  18. if (!isNaN(week) && week > 0) {
  19. weeks.add(week);
  20. }
  21. });
  22. return Array.from(weeks).sort((a, b) => a - b);
  23. }
  24. // 从按周展开的场地字符串中提取去重后的教室名称。
  25. function extractLocationsFromJxcdmc2(jxcdmc2) {
  26. if (!jxcdmc2) return [];
  27. const locationSet = new Set();
  28. String(jxcdmc2).split(",").forEach(item => {
  29. const trimmed = item.trim();
  30. if (!trimmed) return;
  31. const match = trimmed.match(/^(.*?)-(\d+)$/);
  32. const location = (match ? match[1] : trimmed).trim();
  33. if (location && location !== "-1") {
  34. locationSet.add(location);
  35. }
  36. });
  37. return Array.from(locationSet);
  38. }
  39. // 按页面现有逻辑优先级生成课程地点文案。
  40. function resolvePosition(item) {
  41. const primary = String(item.jxcdmc || "").trim();
  42. if (primary) {
  43. return primary;
  44. }
  45. if (String(item.bapjxcd || "") === "1") {
  46. return "不用场地";
  47. }
  48. const fallbackLocations = extractLocationsFromJxcdmc2(item.jxcdmc2);
  49. if (fallbackLocations.length > 0) {
  50. return fallbackLocations.join("、");
  51. }
  52. return "待定";
  53. }
  54. // 解析课表接口返回的数据并转换为课程数组。
  55. function parseCourseList(apiJson) {
  56. if (!apiJson || apiJson.code !== 0 || !Array.isArray(apiJson.data)) {
  57. throw new Error("课表接口返回格式不正确");
  58. }
  59. const courseMap = new Map();
  60. apiJson.data.forEach(item => {
  61. const day = parseInt(item.xq, 10);
  62. const startSection = parseInt(item.ps, 10);
  63. const endSection = parseInt(item.pe, 10);
  64. const weeks = parseWeeks(item.zc);
  65. if (
  66. !item.kcmc ||
  67. isNaN(day) ||
  68. isNaN(startSection) ||
  69. isNaN(endSection) ||
  70. day < 1 ||
  71. day > 7 ||
  72. startSection > endSection ||
  73. weeks.length === 0
  74. ) {
  75. return;
  76. }
  77. const teacher = (item.teaxms || item.pkr || "").trim() || "未知";
  78. const position = resolvePosition(item);
  79. const key = [
  80. item.kcmc.trim(),
  81. teacher,
  82. position,
  83. day,
  84. startSection,
  85. endSection,
  86. weeks.join(',')
  87. ].join("__");
  88. if (!courseMap.has(key)) {
  89. courseMap.set(key, {
  90. name: item.kcmc.trim(),
  91. teacher,
  92. position,
  93. day,
  94. startSection,
  95. endSection,
  96. weeks
  97. });
  98. }
  99. });
  100. return Array.from(courseMap.values()).sort((a, b) =>
  101. a.day - b.day ||
  102. a.startSection - b.startSection ||
  103. a.endSection - b.endSection ||
  104. a.name.localeCompare(b.name)
  105. );
  106. }
  107. // 从 week.page 源码中提取学校真实作息时间。
  108. function parseBusinessHoursFromHtml(htmlText) {
  109. const match = htmlText.match(/var\s+businessHours\s*=\s*\$\.parseJSON\('(\[.*?\])'\);/);
  110. if (!match || !match[1]) {
  111. return [];
  112. }
  113. let rawData;
  114. try {
  115. rawData = JSON.parse(match[1]);
  116. } catch (error) {
  117. console.warn("businessHours 解析失败", error);
  118. return [];
  119. }
  120. return rawData
  121. .map(item => ({
  122. number: parseInt(item.jcdm, 10),
  123. startTime: String(item.qssj || "").slice(0, 5),
  124. endTime: String(item.jssj || "").slice(0, 5)
  125. }))
  126. .filter(item => !isNaN(item.number) && item.startTime && item.endTime)
  127. .sort((a, b) => a.number - b.number);
  128. }
  129. // 从页面脚本中识别总周数上限,作为后续配置接入的线索。
  130. function extractWeekCountFromHtml(htmlText) {
  131. const loopMatch = htmlText.match(/for\s*\(\s*var\s+i\s*=\s*0\s*;\s*i\s*<\s*(\d+)\s*;\s*i\+\+\s*\)/);
  132. if (loopMatch) {
  133. const weekCount = parseInt(loopMatch[1], 10);
  134. if (!isNaN(weekCount) && weekCount > 0) {
  135. return weekCount;
  136. }
  137. }
  138. return null;
  139. }
  140. // 读取页面中的学期下拉框选项和值。
  141. function extractSemesterOptions(doc) {
  142. const selectElem = doc.getElementById("xnxqdm");
  143. if (!selectElem) {
  144. return null;
  145. }
  146. const semesters = [];
  147. const semesterValues = [];
  148. let defaultIndex = 0;
  149. Array.from(selectElem.querySelectorAll("option")).forEach((option, index) => {
  150. const label = option.innerText.trim();
  151. const value = option.value;
  152. if (!label || !value) return;
  153. semesters.push(label);
  154. semesterValues.push(value);
  155. if (option.selected || option.hasAttribute("selected")) {
  156. defaultIndex = index;
  157. }
  158. });
  159. if (semesters.length === 0) {
  160. return null;
  161. }
  162. return { semesters, semesterValues, defaultIndex };
  163. }
  164. // 粗略判断当前是否已经处于个人课表页面。
  165. function isProbablySchedulePage() {
  166. const href = window.location.href;
  167. return /\/new\/student\/xsgrkb\/week\.page/i.test(href) || document.getElementById("xnxqdm") !== null;
  168. }
  169. // 导入开始前提示用户先进入课表页面。
  170. async function promptUserToStart() {
  171. return await window.AndroidBridgePromise.showAlert(
  172. "成都医学院教务导入",
  173. "请先确保自己已经进入教务系统的课表页面,再继续导入。",
  174. "我已进入课表页"
  175. );
  176. }
  177. // 获取课表页 HTML 和文档对象,优先复用当前页面。
  178. async function loadSchedulePageContext() {
  179. if (isProbablySchedulePage()) {
  180. return {
  181. htmlText: document.documentElement.outerHTML,
  182. doc: document,
  183. weekCount: extractWeekCountFromHtml(document.documentElement.outerHTML)
  184. };
  185. }
  186. const response = await fetch("/new/student/xsgrkb/week.page", {
  187. method: "GET",
  188. credentials: "include"
  189. });
  190. if (!response.ok) {
  191. throw new Error(`无法打开课表页面(HTTP ${response.status})`);
  192. }
  193. const htmlText = await response.text();
  194. const parser = new DOMParser();
  195. const doc = parser.parseFromString(htmlText, "text/html");
  196. return {
  197. htmlText,
  198. doc,
  199. weekCount: extractWeekCountFromHtml(htmlText)
  200. };
  201. }
  202. // 让用户从页面已有学期中选择一个目标学期。
  203. async function selectSemester(semesterOptions) {
  204. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  205. "选择学期",
  206. JSON.stringify(semesterOptions.semesters),
  207. semesterOptions.defaultIndex
  208. );
  209. if (selectedIndex === null || selectedIndex < 0) {
  210. return null;
  211. }
  212. return {
  213. label: semesterOptions.semesters[selectedIndex],
  214. value: semesterOptions.semesterValues[selectedIndex]
  215. };
  216. }
  217. // 请求指定学期的课程数据。
  218. async function fetchCourseData(xnxqdm) {
  219. const formData = new URLSearchParams();
  220. formData.append("xnxqdm", xnxqdm);
  221. formData.append("zc", "");
  222. formData.append("d1", "2020-01-01 00:00:00");
  223. formData.append("d2", "2040-01-01 00:00:00");
  224. const response = await fetch("/new/student/xsgrkb/getCalendarWeekDatas", {
  225. method: "POST",
  226. headers: {
  227. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  228. "X-Requested-With": "XMLHttpRequest"
  229. },
  230. credentials: "include",
  231. body: formData.toString()
  232. });
  233. if (!response.ok) {
  234. throw new Error(`课表请求失败(HTTP ${response.status})`);
  235. }
  236. return await response.json();
  237. }
  238. // 保存课程数据到应用。
  239. async function saveCourses(courses) {
  240. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  241. }
  242. // 保存课表页面中解析出的作息时间。
  243. async function saveTimeSlots(timeSlots) {
  244. if (!timeSlots.length) {
  245. return;
  246. }
  247. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  248. }
  249. // 编排导入流程:提示、选学期、请求课程、保存课程与作息时间。
  250. async function runImportFlow() {
  251. try {
  252. const confirmed = await promptUserToStart();
  253. if (!confirmed) {
  254. AndroidBridge.showToast("已取消导入");
  255. return;
  256. }
  257. AndroidBridge.showToast("正在读取课表页面信息...");
  258. const pageContext = await loadSchedulePageContext();
  259. const semesterOptions = extractSemesterOptions(pageContext.doc);
  260. if (!semesterOptions) {
  261. throw new Error("未找到学期列表,请先进入教务系统课表页面后再试");
  262. }
  263. const selectedSemester = await selectSemester(semesterOptions);
  264. if (!selectedSemester) {
  265. AndroidBridge.showToast("已取消导入");
  266. return;
  267. }
  268. AndroidBridge.showToast(`正在获取 ${selectedSemester.label} 的课表...`);
  269. const apiJson = await fetchCourseData(selectedSemester.value);
  270. const courses = parseCourseList(apiJson);
  271. if (courses.length === 0) {
  272. await window.AndroidBridgePromise.showAlert(
  273. "提示",
  274. "该学期没有获取到课程数据,请确认当前登录状态和所选学期是否正确。",
  275. "确定"
  276. );
  277. return;
  278. }
  279. const timeSlots = parseBusinessHoursFromHtml(pageContext.htmlText);
  280. await saveCourses(courses);
  281. try {
  282. await saveTimeSlots(timeSlots);
  283. } catch (error) {
  284. AndroidBridge.showToast(`课程已导入,作息时间导入失败:${error.message}`);
  285. }
  286. if (pageContext.weekCount) {
  287. console.log(`CMC: 从课表页识别到总周数 ${pageContext.weekCount} 周`);
  288. }
  289. AndroidBridge.showToast(`成功导入 ${courses.length} 门课程`);
  290. AndroidBridge.notifyTaskCompletion();
  291. } catch (error) {
  292. console.error("CMC import failed:", error);
  293. await window.AndroidBridgePromise.showAlert(
  294. "导入失败",
  295. error.message || String(error),
  296. "确定"
  297. );
  298. }
  299. }
  300. runImportFlow();