capadap.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. // 文件: capadap.js
  2. //后期可加入接口-获取校区 https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduledCampus.do
  3. /**
  4. * 显示导入提示
  5. */
  6. async function promptUserToStart() {
  7. const confirmed = await window.AndroidBridgePromise.showAlert(
  8. "导入确认",
  9. "请确保您已经登录咯~",
  10. "开始导入"
  11. );
  12. if (!confirmed) {
  13. AndroidBridge.showToast("用户取消了导入");
  14. return false;
  15. }
  16. AndroidBridge.showToast("开始流程咯~");
  17. return true;
  18. }
  19. /**
  20. * 请求工具
  21. */
  22. async function api(url, options = {}) {
  23. //设置默认值
  24. const method = options.method || (options.data ? "POST" : "GET");
  25. const headers = {
  26. "fetch-api": "true",
  27. "x-requested-with": "XMLHttpRequest",
  28. "Referer": "https://jwxt.cap.edu.cn/jwapp/sys/homeapp/home/index.html",
  29. ...(options.data && { "content-type": "application/x-www-form-urlencoded; charset=UTF-8" }),
  30. ...options.headers // 允许传入自定义 header 覆盖上面这些
  31. };
  32. //发起请求
  33. const res = await fetch(url, {
  34. method: method,
  35. headers: headers,
  36. body: options.data || null,
  37. credentials: "include"
  38. });
  39. return res.json();
  40. }
  41. //共享变量
  42. const AppConfig = {
  43. currentSemester: null,
  44. postData: null,
  45. };
  46. /**
  47. * 提取上课时间 开学时间 课程周数
  48. */
  49. async function extractCourseTime() {
  50. try { //上课时间
  51. const userRes = await api("https://jwxt.cap.edu.cn/jwapp/sys/homeapp/api/home/currentUser.do");
  52. AppConfig.currentSemester = userRes.datas.welcomeInfo.xnxqdm; //获取学期
  53. console.log("检测到当前学期:", AppConfig.currentSemester);
  54. AppConfig.postData = `XNXQDM=${AppConfig.currentSemester}&XQDM=01`;
  55. //XQDM这里暂不知道有什么用,2返回的也是一个时间 不知道是不是代表不同校区 暂时用(‘龙泉’校区)替代
  56. const res = await api("https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMySectionList.do", {
  57. data: AppConfig.postData,
  58. })
  59. const rawSections = res.datas.getMySectionList;
  60. const cleanSections = rawSections
  61. .filter(item => item.name.includes("第"))
  62. .map(item => ({
  63. "number": parseInt(item.name.replace(/[^0-9]/g, "")),
  64. "startTime": item.startTime, // 必须叫 startTime
  65. "endTime": item.endTime // 必须叫 endTime
  66. }))
  67. .sort((a, b) => a.number - b.number);
  68. console.log(cleanSections)
  69. //开学时间 课程周数
  70. const weekRes = await api("https://jwxt.cap.edu.cn/jwapp/sys/homeapp/api/home/getTermWeeks.do",
  71. {
  72. data: `termCode=${AppConfig.currentSemester}`
  73. });
  74. const finalWeeks = weekRes.datas.map(item => ({
  75. "week": item.serialNumber, // 周序 (1, 2, 3...)
  76. "startTime": item.startDate.split(' ')[0], // 格式化为 YYYY-MM-DD
  77. "endTime": item.endDate.split(' ')[0], // 格式化为 YYYY-MM-DD
  78. "isCurrent": item.curWeek // 是否为当前周
  79. }));
  80. const totalWeeks = finalWeeks.length;
  81. const startDate = finalWeeks[0].startTime;
  82. console.log(AppConfig.currentSemester, totalWeeks,startDate,cleanSections)
  83. return {
  84. currentSemester: AppConfig.currentSemester,
  85. totalWeeks,
  86. startDate,
  87. cleanSections //每日课程时间 timeSlots!!!
  88. };
  89. }
  90. catch (error) {
  91. console.error('解析开学时间时出错:', error);
  92. AndroidBridge.showToast(`解析开学时间失败: ${error.message}`);
  93. return null;
  94. }
  95. }//返回 学期时间 课程周数 开始时间 时间表
  96. // 2025-2026-2 19 2026-03-09 Array
  97. /**
  98. * 获取课表数据 返回的是原始数据
  99. */
  100. async function getCourseData() {
  101. const courseRes = await api("https://jwxt.cap.edu.cn/jwapp/sys/kbapp/api/wdkbcx/getMyScheduleDetail.do", {
  102. data: AppConfig.postData,
  103. })
  104. const rawCourses = courseRes?.datas?.getMyScheduleDetail?.arrangedList || [];
  105. // console.log("获取到课程数据:", rawCourses);
  106. return rawCourses;
  107. }
  108. function parseWeeks(weekStr) {
  109. if (!weekStr) return [];
  110. const weeks = [];
  111. // 1. 处理逗号分隔的多个区间
  112. const segments = weekStr.replace(/周/g, "").split(",");
  113. segments.forEach(seg => {
  114. // 处理单双周逻辑
  115. const isOnlyOdd = seg.includes("(单)");
  116. const isOnlyEven = seg.includes("(双)");
  117. const cleanSeg = seg.replace(/\(单\)|\(双\)/g, "");
  118. if (cleanSeg.includes("-")) {
  119. // 处理范围型:1-4
  120. const [start, end] = cleanSeg.split("-").map(Number);
  121. for (let i = start; i <= end; i++) {
  122. if (isOnlyOdd && i % 2 === 0) continue;
  123. if (isOnlyEven && i % 2 !== 0) continue;
  124. weeks.push(i);
  125. }
  126. } else {
  127. // 处理单个数字
  128. const num = Number(cleanSeg);
  129. if (!isNaN(num)) weeks.push(num);
  130. }
  131. });
  132. return [...new Set(weeks)].sort((a, b) => a - b);
  133. }
  134. /**
  135. * 1. 展开周次函数:支持 1-3周(单), 7-17周(单) 等
  136. */
  137. function expandWeeks(rawStr) {
  138. const weeks = [];
  139. if (!rawStr) return weeks;
  140. const cleanStr = rawStr.replace(/\s+/g, '').replace(/,/g, ',').replace(/周/g, '');
  141. const isOdd = cleanStr.includes('(单)');
  142. const isEven = cleanStr.includes('(双)');
  143. const rangePart = cleanStr.replace(/\([单双]\)/g, '');
  144. rangePart.split(',').forEach(segment => {
  145. if (segment.includes('-')) {
  146. const [start, end] = segment.split('-').map(Number);
  147. for (let i = start; i <= end; i++) {
  148. if (isOdd && i % 2 === 0) continue;
  149. if (isEven && i % 2 !== 0) continue;
  150. weeks.push(i);
  151. }
  152. } else {
  153. const num = parseInt(segment);
  154. if (!isNaN(num)) {
  155. if (isOdd && num % 2 === 0) return;
  156. if (isEven && num % 2 !== 0) return;
  157. weeks.push(num);
  158. }
  159. }
  160. });
  161. return weeks;
  162. }
  163. /**
  164. * 2. 单行解析函数:提取核心信息
  165. */
  166. function parseDetailLine(line) {
  167. // 移除 HTML 标签
  168. const cleanLine = line.replace(/<[^>]+>/g, "").trim();
  169. const parts = cleanLine.split(/\s+/);
  170. // 假设格式为:[周次] [老师] [建筑/校区] [具体地点]
  171. const rawWeek = parts[0] || "";
  172. const teacher = parts[1] || "未知教师";
  173. const building = parts[2] || "";
  174. const location = parts[3] || "";
  175. let finalPosition = "";
  176. if (location.includes(building)) {
  177. finalPosition = location;
  178. } else {
  179. finalPosition = building + " " + location;
  180. }
  181. finalPosition = finalPosition.trim();
  182. return {
  183. rawWeek,
  184. teacher,
  185. building,
  186. location,
  187. weeks: parseWeeks(rawWeek)
  188. };
  189. }
  190. /**
  191. * 3. 智能汇总函数:处理地点变动逻辑
  192. */
  193. function extractAndMergeCourse(titleDetail) {
  194. if (!titleDetail || titleDetail.length === 0) return null;
  195. const courseName = titleDetail[0];
  196. // 过滤掉第一行课程名,解析后面所有的详情行
  197. const rawSlots = titleDetail.slice(1).map(line => parseDetailLine(line));
  198. const mergedMap = new Map();
  199. rawSlots.forEach(slot => {
  200. // 连堂课如果地点老师一样,就合并周次;如果不一样(比如一半在教室一半在实验室),会拆分成两个 segment
  201. const identifier = `${slot.teacher}|${slot.building}|${slot.location}`;
  202. if (mergedMap.has(identifier)) {
  203. const existing = mergedMap.get(identifier);
  204. // 合并周次并去重排序
  205. existing.weeks = [...new Set([...existing.weeks, ...slot.weeks])].sort((a, b) => a - b);
  206. existing.rawWeeksDesc += `, ${slot.rawWeek}`;
  207. } else {
  208. mergedMap.set(identifier, {
  209. teacher: slot.teacher,
  210. building: slot.building,
  211. location: slot.location,
  212. weeks: slot.weeks,
  213. rawWeeksDesc: slot.rawWeek
  214. });
  215. }
  216. });
  217. const segments = Array.from(mergedMap.values());
  218. // --- 修复点:先计算,再打印和返回 ---
  219. const allActiveWeeks = [...new Set(segments.flatMap(s => s.weeks))].sort((a, b) => a - b);
  220. console.log("解析课程:", courseName, "总周次:", allActiveWeeks);
  221. return {
  222. courseName,
  223. allActiveWeeks,
  224. segments
  225. };
  226. }
  227. function fixSection(section) {
  228. let realSection = section;
  229. if (section > 5) realSection -= 1; // 超过午餐,向上平移1格
  230. if (section > 10) realSection -= 1; // 超过晚餐,再向上平移1格
  231. return realSection;
  232. }
  233. /**
  234. * 解析所有课程并应用修正
  235. */
  236. function parseAllCourses(rawArrangedList) {
  237. const finalCourses = [];
  238. if (!rawArrangedList || !Array.isArray(rawArrangedList)) return [];
  239. rawArrangedList.forEach(item => {
  240. if (item.titleDetail && item.titleDetail.length > 0) {
  241. const mergedResult = extractAndMergeCourse(item.titleDetail);
  242. if (!mergedResult) return;
  243. // 1. 获取原始节次数据
  244. const rawStart = parseInt(item.beginSection || item.startSection);
  245. const rawEnd = parseInt(item.endSection);
  246. const day = parseInt(item.dayOfWeek || item.day);
  247. // 2. 调用 fixSection 解决错位
  248. const sSection = fixSection(rawStart);
  249. const eSection = fixSection(rawEnd);
  250. mergedResult.segments.forEach(seg => {
  251. if (!isNaN(sSection) && !isNaN(eSection)) {
  252. // 3. 处理地点显示问题(去重:避免出现 "励行楼 励行楼xxx")
  253. let finalPos = seg.location;
  254. if (seg.building && !seg.location.includes(seg.building)) {
  255. finalPos = seg.building + " " + seg.location;
  256. }
  257. finalCourses.push({
  258. name: mergedResult.courseName,
  259. teacher: seg.teacher,
  260. position: finalPos.trim(),
  261. day: day,
  262. startSection: sSection, // 写入修正后的开始节次
  263. endSection: eSection, // 写入修正后的结束节次
  264. weeks: seg.weeks
  265. });
  266. }
  267. });
  268. }
  269. });
  270. return finalCourses;
  271. }
  272. /**
  273. * 获取所有课程信息
  274. */
  275. async function fetchAllRawData() {
  276. try {
  277. // 获取基础环境信息 (学期、开学日期、时间表)
  278. const baseInfo = await extractCourseTime();
  279. if (!baseInfo) return null;
  280. const rawArrangedList = await getCourseData();
  281. if (!rawArrangedList || rawArrangedList.length === 0) {
  282. AndroidBridge.showToast("未检测到当前学期的课程数据");
  283. return null;
  284. }
  285. return { baseInfo, rawArrangedList };
  286. } catch (e) {
  287. console.error("抓取数据失败:", e);
  288. return null;
  289. }
  290. }
  291. /**
  292. * 保存
  293. */
  294. async function executeSaveSequence(finalCourses, baseInfo) {
  295. try {
  296. // 1. 保存基础配置 (开学日期、总周数)
  297. const configData = {
  298. semesterStartDate: baseInfo.startDate,
  299. semesterTotalWeeks: baseInfo.totalWeeks || 20,
  300. };
  301. const configSuccess = await AndroidBridge.saveCourseConfig(JSON.stringify(configData));
  302. if (!configSuccess) {
  303. AndroidBridge.showToast("学期保存失败");
  304. return false;
  305. }
  306. // 2. 保存时间段 (节次时间表
  307. const slotSuccess = await AndroidBridge.savePresetTimeSlots(JSON.stringify(baseInfo.cleanSections));
  308. if (!slotSuccess) return false;
  309. // 3. 保存课程数据
  310. const saveResult = await AndroidBridge.saveImportedCourses(JSON.stringify(finalCourses));
  311. return saveResult;
  312. } catch (e) {
  313. console.error("保存流程崩溃:", e);
  314. AndroidBridge.showToast("导入过程发生意外");
  315. return false;
  316. }
  317. }
  318. /**
  319. * 保存配置 (日期和周数)
  320. */
  321. async function saveConfig(baseInfo) {
  322. const configData = {
  323. semesterStartDate: baseInfo.startDate,
  324. semesterTotalWeeks: baseInfo.totalWeeks || 20,
  325. };
  326. try {
  327. const configSuccess = await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(configData));
  328. if (configSuccess) {
  329. return true;
  330. }
  331. return false;
  332. } catch (error) {
  333. AndroidBridge.showToast("保存配置失败: " + error.message);
  334. return false;
  335. }
  336. }
  337. /**
  338. * 主导入流
  339. */
  340. async function runImportFlow() {
  341. try {
  342. // 1. 前置确认
  343. const isReady = await promptUserToStart();
  344. if (!isReady) return;
  345. // 2. 抓取所有必要数据
  346. const dataBundle = await fetchAllRawData();
  347. if (!dataBundle) return;
  348. // 3. 解析原始数据
  349. const finalCourses = parseAllCourses(dataBundle.rawArrangedList);
  350. if (finalCourses.length === 0) {
  351. AndroidBridge.showToast("解析失败:未能提取到有效课程");
  352. return;
  353. }
  354. // 4. 保存配置数据 (存日期、周数)
  355. const configSaveResult = await saveConfig(dataBundle.baseInfo);
  356. if (!configSaveResult) return;
  357. //时间段保存
  358. try {
  359. const slotJson = JSON.stringify(dataBundle.baseInfo.cleanSections);
  360. console.log("写入时间段数据:", slotJson);
  361. await window.AndroidBridgePromise.savePresetTimeSlots(slotJson);
  362. } catch (e) {
  363. console.error("时间段写入失败:", e);
  364. // 这里可以选择跳过或报错
  365. }
  366. // 5. 课程数据保存
  367. const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
  368. if (!saveResult) {
  369. AndroidBridge.showToast("课程数据保存失败");
  370. return;
  371. }
  372. // 6. 流程成功结束
  373. AndroidBridge.showToast("Hi ~ 课表导入成功!");
  374. AndroidBridge.notifyTaskCompletion();
  375. } catch (error) {
  376. console.error("主流程异常:", error);
  377. AndroidBridge.showToast("意外错误: " + error.message);
  378. }
  379. }
  380. // 启动导入流程
  381. runImportFlow();