cup_02.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. // resources/CUP/cup_02.js
  2. // 1. 显示一个公告信息弹窗
  3. async function promptUserToStart() {
  4. try {
  5. console.log("即将显示公告弹窗...");
  6. const confirmed = await window.AndroidBridgePromise.showAlert(
  7. "重要通知",
  8. "导入前请确保您已成功登录教务系统,并选定正确的学期。",
  9. "好的,开始"
  10. );
  11. if (confirmed) {
  12. console.log("用户点击了确认按钮。Alert Promise Resolved: " + confirmed);
  13. AndroidBridge.showToast("Alert:用户点击了确认!");
  14. return true; // 成功时返回 true
  15. } else {
  16. console.log("用户点击了取消按钮或关闭了弹窗。Alert Promise Resolved: " + confirmed);
  17. AndroidBridge.showToast("Alert:用户取消了!");
  18. return false; // 用户取消时返回 false
  19. }
  20. } catch (error) {
  21. console.error("显示公告弹窗时发生错误:", error);
  22. AndroidBridge.showToast("Alert:显示弹窗出错!" + error.message);
  23. return false; // 出现错误时也返回 false
  24. }
  25. }
  26. // 2. 选择校区
  27. async function selectCampus() {
  28. try {
  29. const campuses = ["本校", "克拉玛依校区"];
  30. // 呼叫安卓原生弹窗
  31. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  32. "选择所在校区",
  33. JSON.stringify(campuses),
  34. 0 // 默认选中第一个(本校)
  35. );
  36. if (selectedIndex !== null && selectedIndex >= 0) {
  37. const selectedCampus = campuses[selectedIndex];
  38. if (typeof AndroidBridge !== 'undefined' && AndroidBridge.showToast) {
  39. AndroidBridge.showToast("已选择: " + selectedCampus);
  40. }
  41. // 返回 true 代表是克拉玛依校区,返回 false 代表是本校
  42. return selectedIndex === 1;
  43. } else {
  44. // 用户取消了选择
  45. if (typeof AndroidBridge !== 'undefined' && AndroidBridge.showToast) {
  46. AndroidBridge.showToast("取消导入:未选择校区。");
  47. }
  48. return null;
  49. }
  50. } catch (error) {
  51. console.error("选择校区时发生错误:", error);
  52. return null;
  53. }
  54. }
  55. // 3. 获取学期信息
  56. async function getTermCode() {
  57. try {
  58. if (typeof AndroidBridge !== 'undefined') AndroidBridge.showToast("正在获取学期列表...");
  59. // 检查环境是否支持 jQuery
  60. if (typeof $ === 'undefined' || !$.ajax) {
  61. throw new Error("未检测到 jQuery 环境,请确保在正确的课表页面执行。");
  62. }
  63. // 1. 请求学期列表数据
  64. const termData = await new Promise((resolve, reject) => {
  65. $.ajax({
  66. type: 'get',
  67. dataType: 'json',
  68. url: '/gmis/default/bindterm',
  69. cache: false, // 自动附加时间戳防止缓存
  70. success: function (data) {
  71. resolve(data);
  72. },
  73. error: function (xhr, status, error) {
  74. reject(new Error(`网络请求失败,状态码: ${xhr.status} ${error}`));
  75. }
  76. });
  77. });
  78. if (!termData || termData.length === 0) {
  79. throw new Error("未能获取到有效的学期列表数据。");
  80. }
  81. // 2. 提取文本、值,并寻找当前默认学期的索引
  82. const semesterTexts = [];
  83. const semesterValues = [];
  84. let defaultSelectedIndex = 0; // 默认选中第一项
  85. termData.forEach((item, index) => {
  86. semesterTexts.push(item.termname);
  87. semesterValues.push(item.termcode);
  88. // 如果数据中带有 selected: true,则将其设为默认选中
  89. if (item.selected) {
  90. defaultSelectedIndex = index;
  91. }
  92. });
  93. // 3. 呼叫安卓原生弹窗
  94. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  95. "选择导入学期",
  96. JSON.stringify(semesterTexts),
  97. defaultSelectedIndex
  98. );
  99. // 4. 判断用户选择结果
  100. if (selectedIndex !== null && selectedIndex >= 0) {
  101. const selectedValue = semesterValues[selectedIndex];
  102. if (typeof AndroidBridge !== 'undefined' && AndroidBridge.showToast) {
  103. AndroidBridge.showToast("已选择学期: " + semesterTexts[selectedIndex]);
  104. }
  105. return selectedValue;
  106. } else {
  107. // 用户取消了选择
  108. if (typeof AndroidBridge !== 'undefined' && AndroidBridge.showToast) {
  109. AndroidBridge.showToast("取消导入:未选择学期。");
  110. }
  111. return null;
  112. }
  113. } catch (error) {
  114. console.error("读取学期信息时发生错误:", error);
  115. if (typeof AndroidBridge !== 'undefined' && AndroidBridge.showToast) {
  116. AndroidBridge.showToast("Alert:读取学期信息出错!" + error.message);
  117. }
  118. return null;
  119. }
  120. }
  121. // 4. 获取课程数据
  122. async function fetchData(termCode) {
  123. try {
  124. // 检查环境是否支持 jQuery 拦截解密
  125. if (typeof $ === 'undefined' || !$.ajax) {
  126. throw new Error("未检测到 jQuery 环境,请确保在正确的课表页面执行。");
  127. }
  128. // 将 $.ajax 包装成标准的 Promise,无缝融入 async/await 流程
  129. const response = await new Promise((resolve, reject) => {
  130. $.ajax({
  131. type: 'post',
  132. dataType: 'json',
  133. url: "../pygl/py_kbcx_ew",
  134. data: { 'kblx': 'xs', 'termcode': termCode },
  135. cache: false,
  136. success: function (data) {
  137. resolve(data);
  138. },
  139. error: function (xhr, status, error) {
  140. reject(new Error(`网络请求失败,状态码: ${xhr.status} ${error}`));
  141. }
  142. });
  143. });
  144. // 校验返回的数据结构
  145. if (!response || !response.rows) {
  146. throw new Error("接口返回数据为空或解密后格式不正确");
  147. }
  148. return response.rows;
  149. } catch (error) {
  150. console.error("获取数据时发生错误:", error);
  151. AndroidBridge.showToast("Alert:获取数据出错!" + error.message);
  152. return null;
  153. }
  154. }
  155. // 5. 导入课程数据
  156. async function parseCourses(py_kbcx_ew, isKaramayCampus) {
  157. console.log("正在解析研究生课程数据...");
  158. // 用于存放每一小节课的临时数组
  159. let allCourseBlocks = [];
  160. // 辅助函数 1:将 jcid 转换为标准的拾光节次 (1~12节)
  161. // 根据数据:上午11-15 -> 1-5节,下午21-24 -> 6-9节,晚上31-33 -> 10-12节
  162. function getStandardSection(jcid) {
  163. if (jcid >= 11 && jcid <= 15) return jcid - 10;
  164. if (jcid >= 21 && jcid <= 24) return jcid - 20 + 5;
  165. if (jcid >= 31 && jcid <= 33) return jcid - 30 + 9;
  166. return 1; // 默认兜底
  167. }
  168. // 辅助函数 2:解析类似 "连续周 1-12周" 或 "单周 1-11周" 的字符串,返回数字数组
  169. function parseWeeks(weekStr) {
  170. let weeks = [];
  171. let isSingle = weekStr.includes('单');
  172. let isDouble = weekStr.includes('双');
  173. // 匹配字符串里的所有数字或数字范围 (如 "1", "1-12")
  174. let matches = weekStr.match(/\d+-\d+|\d+/g);
  175. if (matches) {
  176. matches.forEach(m => {
  177. if (m.includes('-')) {
  178. let [start, end] = m.split('-').map(Number);
  179. for (let i = start; i <= end; i++) {
  180. if (isSingle && i % 2 === 0) continue;
  181. if (isDouble && i % 2 !== 0) continue;
  182. weeks.push(i);
  183. }
  184. } else {
  185. let w = Number(m);
  186. if (isSingle && w % 2 === 0) return;
  187. if (isDouble && w % 2 !== 0) return;
  188. weeks.push(w);
  189. }
  190. });
  191. }
  192. return [...new Set(weeks)].sort((a, b) => a - b);
  193. }
  194. // --- 第一步:将按“行”排列的数据,拆解提取出每一小节课 ---
  195. py_kbcx_ew.forEach(row => {
  196. if (!isKaramayCampus && row.jcid === 15) {
  197. return;
  198. }
  199. let currentSection = getStandardSection(row.jcid);
  200. // 遍历星期一 (z1) 到星期日 (z7)
  201. for (let day = 1; day <= 7; day++) {
  202. let zVal = row['z' + day];
  203. if (zVal) {
  204. // 如果同一个时间有两门课(比如单双周不同),按 <br/> 拆分
  205. let classParts = zVal.split(/<br\s*\/?>/i);
  206. classParts.forEach(part => {
  207. // 核心正则表达式:匹配 "课程名[周次]老师[地点]"
  208. // 兼容没有老师或没有地点的情况
  209. let match = part.match(/(.*?)\[(.*?)\]([^\[]*)(?:\[(.*?)\])?$/);
  210. if (match) {
  211. allCourseBlocks.push({
  212. name: match[1].trim(), // 提取:课程名
  213. weekStr: match[2].trim(), // 提取:原始周次字符串 (用于后续比对)
  214. weeks: parseWeeks(match[2]), // 解析:纯数字周次数组
  215. teacher: match[3] ? match[3].trim() : "",// 提取:老师
  216. position: match[4] ? match[4].trim() : "未知地点", // 提取:上课地点
  217. day: day, // 星期几
  218. section: currentSection // 当前是第几节
  219. });
  220. }
  221. });
  222. }
  223. }
  224. });
  225. // --- 第二步:将连续的小节课“合并”成一门完整的课 ---
  226. let mergedCourses = [];
  227. allCourseBlocks.forEach(block => {
  228. // 寻找是否已经有相邻的课可以合并 (同星期、同课名、同老师、同地点、同周次,且节次刚好挨着)
  229. let existingCourse = mergedCourses.find(c =>
  230. c.day === block.day &&
  231. c.name === block.name &&
  232. c.teacher === block.teacher &&
  233. c.position === block.position &&
  234. c.weekStr === block.weekStr &&
  235. c.endSection === block.section - 1 // 核心:判断是否紧挨着上一节
  236. );
  237. if (existingCourse) {
  238. // 如果可以合并,就把结束节次往后延
  239. existingCourse.endSection = block.section;
  240. } else {
  241. // 如果不能合并,就作为一门新课加入
  242. mergedCourses.push({
  243. name: block.name,
  244. teacher: block.teacher,
  245. position: block.position,
  246. day: block.day,
  247. startSection: block.section,
  248. endSection: block.section,
  249. weeks: block.weeks,
  250. weekStr: block.weekStr // 保留用于比对合并
  251. });
  252. }
  253. });
  254. // 清理掉多余的辅助比对字段,输出最终给拾光 App 的标准格式
  255. const finalCourses = mergedCourses.map(c => {
  256. delete c.weekStr;
  257. return c;
  258. });
  259. console.log("最终生成的标准课表数据:", finalCourses);
  260. try {
  261. console.log("正在尝试导入课程...");
  262. const result = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
  263. if (result === true) {
  264. console.log("课程导入成功!");
  265. AndroidBridge.showToast("测试课程导入成功!");
  266. } else {
  267. console.log("课程导入未成功,结果:" + result);
  268. AndroidBridge.showToast("测试课程导入失败,请查看日志。");
  269. }
  270. } catch (error) {
  271. console.error("导入课程时发生错误:", error);
  272. AndroidBridge.showToast("导入课程失败: " + error.message);
  273. }
  274. }
  275. // 6. 导入预设时间段
  276. async function importPresetTimeSlots(campusIsKaramay) {
  277. console.log("正在准备预设时间段数据...");
  278. const presetTimeSlots = [
  279. { "number": 1, "startTime": "08:00", "endTime": "08:45" },
  280. { "number": 2, "startTime": "08:50", "endTime": "09:35" },
  281. { "number": 3, "startTime": "10:05", "endTime": "10:50" },
  282. { "number": 4, "startTime": "10:55", "endTime": "11:40" },
  283. { "number": 5, "startTime": "13:30", "endTime": "14:15" },
  284. { "number": 6, "startTime": "14:20", "endTime": "15:05" },
  285. { "number": 7, "startTime": "15:35", "endTime": "16:20" },
  286. { "number": 8, "startTime": "16:25", "endTime": "17:10" },
  287. { "number": 9, "startTime": "18:30", "endTime": "19:15" },
  288. { "number": 10, "startTime": "19:20", "endTime": "20:05" },
  289. { "number": 11, "startTime": "20:10", "endTime": "20:55" },
  290. { "number": 12, "startTime": "21:00", "endTime": "21:45" }
  291. ];
  292. const presetTimeSlotsKaramay = [
  293. { "number": 1, "startTime": "08:00", "endTime": "08:45" },
  294. { "number": 2, "startTime": "08:50", "endTime": "09:35" },
  295. { "number": 3, "startTime": "10:05", "endTime": "10:50" },
  296. { "number": 4, "startTime": "10:55", "endTime": "11:40" },
  297. { "number": 5, "startTime": "12:00", "endTime": "12:45" },
  298. { "number": 6, "startTime": "13:30", "endTime": "14:15" },
  299. { "number": 7, "startTime": "14:20", "endTime": "15:05" },
  300. { "number": 8, "startTime": "15:35", "endTime": "16:20" },
  301. { "number": 9, "startTime": "16:25", "endTime": "17:10" },
  302. { "number": 10, "startTime": "18:30", "endTime": "19:15" },
  303. { "number": 11, "startTime": "19:20", "endTime": "20:05" },
  304. { "number": 12, "startTime": "20:10", "endTime": "20:55" },
  305. { "number": 13, "startTime": "21:00", "endTime": "21:45" }
  306. ];
  307. try {
  308. console.log("正在尝试导入预设时间段...");
  309. const result = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(campusIsKaramay ? presetTimeSlotsKaramay : presetTimeSlots));
  310. if (result === true) {
  311. console.log("预设时间段导入成功!");
  312. window.AndroidBridge.showToast("测试时间段导入成功!");
  313. } else {
  314. console.log("预设时间段导入未成功,结果:" + result);
  315. window.AndroidBridge.showToast("测试时间段导入失败,请查看日志。");
  316. }
  317. } catch (error) {
  318. console.error("导入时间段时发生错误:", error);
  319. window.AndroidBridge.showToast("导入时间段失败: " + error.message);
  320. }
  321. }
  322. /**
  323. * 验证开学日期的输入格式
  324. * 规范:验证通过返回 false,验证失败返回 错误信息字符串
  325. */
  326. function validateDateInput(input) {
  327. // 匹配 YYYY-MM-DD, YYYY/MM/DD, YYYY.MM.DD 格式
  328. if (/^\d{4}[-\/\.]\d{2}[-\/\.]\d{2}$/.test(input)) {
  329. return false; // 验证通过
  330. } else {
  331. return "请输入正确的日期格式,例如: 2025-09-01"; // 验证失败,原生 UI 会显示此提示
  332. }
  333. }
  334. // 7. 导入课表配置
  335. async function saveConfig() {
  336. console.log("正在准备配置数据...");
  337. let startDate = await window.AndroidBridgePromise.showPrompt(
  338. "输入开学日期",
  339. "请输入本学期开学日期 (格式: YYYY-MM-DD):",
  340. "2025-09-01", // 默认文本,给用户一个参考
  341. "validateDateInput" // 传入我们上面定义的全局验证函数名
  342. );
  343. // 如果返回 null,说明用户点击了取消
  344. if (startDate === null) {
  345. if (typeof AndroidBridge !== 'undefined') {
  346. AndroidBridge.showToast("已取消开学日期设置,将使用默认配置。");
  347. }
  348. startDate = "2025-09-01"; // 兜底默认值,保证流程继续
  349. } else {
  350. // 容错处理:验证函数放过了 / 和 . ,我们在保存前把它统一替换成标准的横杠 -
  351. startDate = startDate.trim().replace(/[\/\.]/g, '-');
  352. }
  353. // 注意:只传入要修改的字段,其他字段(如 semesterTotalWeeks)会使用 Kotlin 模型中的默认值
  354. const courseConfigData = {
  355. "semesterStartDate": startDate,
  356. "semesterTotalWeeks": 25,
  357. "defaultClassDuration": 45,
  358. "defaultBreakDuration": 5,
  359. "firstDayOfWeek": 1
  360. };
  361. try {
  362. console.log("正在尝试导入课表配置...");
  363. const configJsonString = JSON.stringify(courseConfigData);
  364. const result = await window.AndroidBridgePromise.saveCourseConfig(configJsonString);
  365. if (result === true) {
  366. console.log("课表配置导入成功!");
  367. AndroidBridge.showToast("测试配置导入成功!开学日期: " + startDate);
  368. } else {
  369. console.log("课表配置导入未成功,结果:" + result);
  370. AndroidBridge.showToast("测试配置导入失败,请查看日志。");
  371. }
  372. } catch (error) {
  373. console.error("导入配置时发生错误:", error);
  374. AndroidBridge.showToast("导入配置失败: " + error.message);
  375. }
  376. }
  377. /**
  378. * 编排整个课程导入流程。
  379. * 在任何一步用户取消或发生错误时,都会立即退出,AndroidBridge.notifyTaskCompletion()应该只在成功后调用
  380. */
  381. async function runImportFlow() {
  382. AndroidBridge.showToast("课程导入流程即将开始...");
  383. // 1. 公告和前置检查。
  384. const alertConfirmed = await promptUserToStart();
  385. if (!alertConfirmed) {
  386. return; // 用户取消,立即退出函数
  387. }
  388. // 2. 选择校区。
  389. const isKaramayCampus = await selectCampus();
  390. if (isKaramayCampus === null) return;
  391. // 3. 获取学期。
  392. const termCode = await getTermCode();
  393. if (termCode === null) {
  394. AndroidBridge.showToast("导入已取消。");
  395. // 用户取消,直接退出
  396. return;
  397. }
  398. // 4. 获取课程数据
  399. const py_kbcx_ew = await fetchData (termCode);
  400. if (py_kbcx_ew === null) {
  401. AndroidBridge.showToast("导入已取消。");
  402. // 请求失败或无数据,直接退出
  403. return;
  404. }
  405. // 5. 解析课程信息。
  406. await parseCourses(py_kbcx_ew, isKaramayCampus);
  407. // 6. 导入时间段数据。
  408. await importPresetTimeSlots(isKaramayCampus);
  409. // 7. 保存配置数据 (例如学期开始日期)
  410. await saveConfig();
  411. // 8. 流程**完全成功**,发送结束信号。
  412. AndroidBridge.showToast("所有任务已完成!");
  413. AndroidBridge.notifyTaskCompletion();
  414. }
  415. // 启动所有演示
  416. runImportFlow();