capadap.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  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; // 减去中午那节课
  230. if (section > 10) realSection -= 1; // 减去晚餐那节课
  231. return realSection;
  232. }
  233. /**
  234. * 解析函数 Gemini所写
  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. // 优先使用数据里的数字字段,因为 titleDetail 有时会被截断
  244. const sSection = parseInt(item.beginSection || item.startSection);
  245. const eSection = parseInt(item.endSection);
  246. const day = parseInt(item.dayOfWeek || item.day);
  247. mergedResult.segments.forEach(seg => {
  248. if (!isNaN(sSection) && !isNaN(eSection)) {
  249. finalCourses.push({
  250. name: mergedResult.courseName,
  251. teacher: seg.teacher,
  252. position: (seg.building + " " + seg.location).trim(),
  253. day: day,
  254. startSection: sSection,
  255. endSection: eSection,
  256. weeks: seg.weeks,
  257. startTime: seg.startTime, // 记录开始时间防止后续需要
  258. endTime: seg.endTime
  259. });
  260. }
  261. });
  262. }
  263. });
  264. return finalCourses;
  265. }
  266. /**
  267. * 获取所有课程信息
  268. */
  269. async function fetchAllRawData() {
  270. try {
  271. // 获取基础环境信息 (学期、开学日期、时间表)
  272. const baseInfo = await extractCourseTime();
  273. if (!baseInfo) return null;
  274. const rawArrangedList = await getCourseData();
  275. if (!rawArrangedList || rawArrangedList.length === 0) {
  276. AndroidBridge.showToast("未检测到当前学期的课程数据");
  277. return null;
  278. }
  279. return { baseInfo, rawArrangedList };
  280. } catch (e) {
  281. console.error("抓取数据失败:", e);
  282. return null;
  283. }
  284. }
  285. /**
  286. * 保存
  287. */
  288. async function executeSaveSequence(finalCourses, baseInfo) {
  289. try {
  290. // 1. 保存基础配置 (开学日期、总周数)
  291. const configData = {
  292. semesterStartDate: baseInfo.startDate,
  293. semesterTotalWeeks: baseInfo.totalWeeks || 20,
  294. };
  295. const configSuccess = await AndroidBridge.saveCourseConfig(JSON.stringify(configData));
  296. if (!configSuccess) {
  297. AndroidBridge.showToast("学期保存失败");
  298. return false;
  299. }
  300. // 2. 保存时间段 (节次时间表
  301. const slotSuccess = await AndroidBridge.savePresetTimeSlots(JSON.stringify(baseInfo.cleanSections));
  302. if (!slotSuccess) return false;
  303. // 3. 保存课程数据
  304. const saveResult = await AndroidBridge.saveImportedCourses(JSON.stringify(finalCourses));
  305. return saveResult;
  306. } catch (e) {
  307. console.error("保存流程崩溃:", e);
  308. AndroidBridge.showToast("导入过程发生意外");
  309. return false;
  310. }
  311. }
  312. /**
  313. * 保存配置 (日期和周数)
  314. */
  315. async function saveConfig(baseInfo) {
  316. const configData = {
  317. semesterStartDate: baseInfo.startDate,
  318. semesterTotalWeeks: baseInfo.totalWeeks || 20,
  319. };
  320. try {
  321. const configSuccess = await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(configData));
  322. if (configSuccess) {
  323. return true;
  324. }
  325. return false;
  326. } catch (error) {
  327. AndroidBridge.showToast("保存配置失败: " + error.message);
  328. return false;
  329. }
  330. }
  331. /**
  332. * 主导入流
  333. */
  334. async function runImportFlow() {
  335. try {
  336. // 1. 前置确认
  337. const isReady = await promptUserToStart();
  338. if (!isReady) return;
  339. // 2. 抓取所有必要数据
  340. const dataBundle = await fetchAllRawData();
  341. if (!dataBundle) return;
  342. // 3. 解析原始数据
  343. const finalCourses = parseAllCourses(dataBundle.rawArrangedList);
  344. if (finalCourses.length === 0) {
  345. AndroidBridge.showToast("解析失败:未能提取到有效课程");
  346. return;
  347. }
  348. // 4. 保存配置数据 (存日期、周数)
  349. const configSaveResult = await saveConfig(dataBundle.baseInfo);
  350. if (!configSaveResult) return;
  351. //时间段保存
  352. try {
  353. const slotJson = JSON.stringify(dataBundle.baseInfo.cleanSections);
  354. console.log("写入时间段数据:", slotJson);
  355. await window.AndroidBridgePromise.savePresetTimeSlots(slotJson);
  356. } catch (e) {
  357. console.error("时间段写入失败:", e);
  358. // 这里可以选择跳过或报错
  359. }
  360. // 5. 课程数据保存
  361. const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
  362. if (!saveResult) {
  363. AndroidBridge.showToast("课程数据保存失败");
  364. return;
  365. }
  366. // 6. 流程成功结束
  367. AndroidBridge.showToast("Hi ~ 课表导入成功!");
  368. AndroidBridge.notifyTaskCompletion();
  369. } catch (error) {
  370. console.error("主流程异常:", error);
  371. AndroidBridge.showToast("意外错误: " + error.message);
  372. }
  373. }
  374. // 启动导入流程
  375. runImportFlow();