gpnu_01.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059
  1. // ===================== 工具函数 =====================
  2. /**
  3. * 节次与周次合并去重函数
  4. * @param {Array<Object>} courses 原始解析课程数组
  5. * @returns {Array<Object>} 合并去重后的课程数组
  6. */
  7. function mergeAndDistinctCourses(courses) {
  8. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  9. const list = courses.map(c => ({
  10. ...c,
  11. name: c.name || '',
  12. teacher: c.teacher || '',
  13. position: c.position || '',
  14. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  15. }));
  16. list.sort((a, b) => {
  17. return a.name.localeCompare(b.name) ||
  18. a.teacher.localeCompare(b.teacher) ||
  19. a.position.localeCompare(b.position) ||
  20. (a.day || 0) - (b.day || 0) ||
  21. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  22. (a.startSection || 0) - (b.startSection || 0);
  23. });
  24. const step1Merged = [];
  25. let current = list[0];
  26. for (let i = 1; i < list.length; i++) {
  27. const next = list[i];
  28. const isSameCourseAndWeeks =
  29. current.name === next.name &&
  30. current.teacher === next.teacher &&
  31. current.position === next.position &&
  32. current.day === next.day &&
  33. current.weeks.join(',') === next.weeks.join(',');
  34. const isContinuous = current.endSection + 1 === next.startSection;
  35. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  36. if (isSameCourseAndWeeks && isContinuous) {
  37. current.endSection = next.endSection;
  38. } else if (isSameCourseAndWeeks && isDuplicate) {
  39. continue;
  40. } else {
  41. step1Merged.push(current);
  42. current = next;
  43. }
  44. }
  45. step1Merged.push(current);
  46. step1Merged.sort((a, b) => {
  47. return a.name.localeCompare(b.name) ||
  48. a.teacher.localeCompare(b.teacher) ||
  49. a.position.localeCompare(b.position) ||
  50. (a.day || 0) - (b.day || 0) ||
  51. (a.startSection || 0) - (b.startSection || 0) ||
  52. (a.endSection || 0) - (b.endSection || 0);
  53. });
  54. const step2Merged = [];
  55. let cur = step1Merged[0];
  56. for (let i = 1; i < step1Merged.length; i++) {
  57. const nxt = step1Merged[i];
  58. const isSameCourseAndSection =
  59. cur.name === nxt.name &&
  60. cur.teacher === nxt.teacher &&
  61. cur.position === nxt.position &&
  62. cur.day === nxt.day &&
  63. cur.startSection === nxt.startSection &&
  64. cur.endSection === nxt.endSection;
  65. if (isSameCourseAndSection) {
  66. cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
  67. } else {
  68. step2Merged.push(cur);
  69. cur = nxt;
  70. }
  71. }
  72. step2Merged.push(cur);
  73. return step2Merged;
  74. }
  75. /**
  76. * 解析周次字符串,例如 "1-16周"、"1-8周,10-16周"、"1-16周(单)"、"1-16周(双)" 等。
  77. * @param {string} weekStr - 周次描述字符串
  78. * @returns {number[]} 周次数字数组(升序)
  79. */
  80. function parseWeeks(weekStr) {
  81. if (typeof weekStr !== 'string') {
  82. weekStr = String(weekStr || '');
  83. }
  84. if (!weekStr) return [];
  85. let cleanStr = weekStr.replace(/周/g, '').replace(/\s+/g, '');
  86. const weeks = new Set();
  87. const parts = cleanStr.split(',');
  88. for (let rawPart of parts) {
  89. if (!rawPart) continue;
  90. // ========== 改进点 1:先判断单双标记,再提取数字 ==========
  91. const hasDan = /单/.test(rawPart);
  92. const hasShuang = /双/.test(rawPart);
  93. // 仅含“单”不含“双” → 单周(奇数)
  94. let oddOnly = hasDan && !hasShuang;
  95. // 仅含“双”不含“单” → 双周(偶数)
  96. let evenOnly = hasShuang && !hasDan;
  97. // 若同时包含“单”和“双”,视为不限制奇偶(每周都上)
  98. // 无需额外处理,因为 oddOnly 和 evenOnly 已经为 false
  99. // 提取片段中的数字和连字符(忽略括号、单双等非数字字符)
  100. const numericPart = rawPart.replace(/[^\d\-]/g, '');
  101. if (!numericPart) {
  102. console.warn(`JS: 解析周次时忽略非法片段: ${rawPart}`);
  103. continue;
  104. }
  105. if (!/^[\d\-]+$/.test(numericPart)) {
  106. console.warn(`JS: 解析周次时忽略非法片段: ${rawPart}`);
  107. continue;
  108. }
  109. // 解析范围或单个数字
  110. if (numericPart.includes('-')) {
  111. const [startStr, endStr] = numericPart.split('-');
  112. const start = Number(startStr);
  113. const end = Number(endStr);
  114. if (!isNaN(start) && !isNaN(end) && start <= end) {
  115. for (let w = start; w <= end; w++) {
  116. if (oddOnly && w % 2 !== 1) continue; // 单周只保留奇数
  117. if (evenOnly && w % 2 !== 0) continue; // 双周只保留偶数
  118. weeks.add(w);
  119. }
  120. }
  121. } else {
  122. const w = Number(numericPart);
  123. if (!isNaN(w)) {
  124. if (oddOnly && w % 2 !== 1) continue;
  125. if (evenOnly && w % 2 !== 0) continue;
  126. weeks.add(w);
  127. }
  128. }
  129. }
  130. // 备用解析:如果正常解析为空,尝试用正则直接提取所有数字和范围
  131. if (weeks.size === 0) {
  132. // ========== 改进点 2:备用解析使用一致的奇偶判断逻辑 ==========
  133. const hasDanOverall = /单/.test(weekStr);
  134. const hasShuangOverall = /双/.test(weekStr);
  135. let oddOnlyOverall = hasDanOverall && !hasShuangOverall;
  136. let evenOnlyOverall = hasShuangOverall && !hasDanOverall;
  137. const rangePattern = /(\d+)\s*-\s*(\d+)/g;
  138. let match;
  139. let matched = false;
  140. while ((match = rangePattern.exec(weekStr)) !== null) {
  141. const start = Number(match[1]);
  142. const end = Number(match[2]);
  143. if (start <= end) {
  144. for (let w = start; w <= end; w++) {
  145. if (oddOnlyOverall && w % 2 === 0) continue; // 单周跳过偶数
  146. if (evenOnlyOverall && w % 2 !== 0) continue; // 双周跳过奇数
  147. weeks.add(w);
  148. }
  149. matched = true;
  150. }
  151. }
  152. if (!matched) {
  153. const singlePattern = /(\d+)周/g;
  154. while ((match = singlePattern.exec(weekStr)) !== null) {
  155. const w = Number(match[1]);
  156. if (!isNaN(w)) {
  157. if (oddOnlyOverall && w % 2 === 0) continue;
  158. if (evenOnlyOverall && w % 2 !== 0) continue;
  159. weeks.add(w);
  160. }
  161. }
  162. }
  163. }
  164. return Array.from(weeks).sort((a, b) => a - b);
  165. }
  166. /**
  167. * 解析 API 返回的 JSON 数据,提取课程信息。
  168. * 保持与旧版完全一致的课程解析行为。
  169. * @param {Object} jsonData - 教务系统返回的 JSON 对象
  170. * @returns {Array} 解析后的课程数组
  171. */
  172. function parseJsonData(jsonData) {
  173. console.log("JS: parseJsonData 正在解析 JSON 数据...");
  174. if (!jsonData || !Array.isArray(jsonData.kbList)) {
  175. console.warn("JS: JSON 数据结构错误或缺少 kbList 字段。");
  176. return [];
  177. }
  178. const rawCourseList = jsonData.kbList;
  179. const finalCourseList = [];
  180. for (const rawCourse of rawCourseList) {
  181. // 旧版严格非空校验,包含 cdmc
  182. if (!rawCourse.kcmc || !rawCourse.xm || !rawCourse.cdmc ||
  183. !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
  184. continue;
  185. }
  186. const weeksArray = parseWeeks(rawCourse.zcd);
  187. if (weeksArray.length === 0) {
  188. continue;
  189. }
  190. // 旧版节次解析:整体去除“节”字,按 '-' 分割取首尾
  191. const jcs = String(rawCourse.jcs).replace(/节/g, '');
  192. const sectionParts = jcs.split('-');
  193. const startSection = Number(sectionParts[0]);
  194. const endSection = Number(sectionParts[sectionParts.length - 1]);
  195. const day = Number(rawCourse.xqj);
  196. if (isNaN(day) || isNaN(startSection) || isNaN(endSection) ||
  197. day < 1 || day > 7 || startSection > endSection) {
  198. continue;
  199. }
  200. finalCourseList.push({
  201. name: rawCourse.kcmc.trim(),
  202. teacher: rawCourse.xm.trim(),
  203. position: rawCourse.cdmc.trim(),
  204. day: day,
  205. startSection: startSection,
  206. endSection: endSection,
  207. weeks: weeksArray
  208. });
  209. }
  210. // 旧版排序规则
  211. finalCourseList.sort((a, b) =>
  212. a.day - b.day ||
  213. a.startSection - b.startSection ||
  214. a.name.localeCompare(b.name)
  215. );
  216. console.log(`JS: JSON 数据解析完成,共找到 ${finalCourseList.length} 门课程。`);
  217. const merged = mergeAndDistinctCourses(finalCourseList);
  218. window.shiguangBridge.showToast(`去重前:${finalCourseList.length} 门,去重后:${merged.length} 门`);
  219. return merged;
  220. }
  221. /**
  222. * 根据课程周次数组推断学期总周数。
  223. * @param {Array} courses - 课程数组
  224. * @returns {number} 推断出的最大周次,至少为1
  225. */
  226. function inferTotalWeeks(courses) {
  227. let maxWeek = 0;
  228. for (const course of courses) {
  229. const weekNums = course.weeks;
  230. if (weekNums.length > 0) {
  231. maxWeek = Math.max(maxWeek, ...weekNums);
  232. }
  233. }
  234. // 返回至少1周,若无法推断则使用默认20
  235. return Math.max(1, maxWeek || 20);
  236. }
  237. /**
  238. * 从周次数据数组中解析第 1 周的开学日期。
  239. * @param {Array} weekList - 教务系统返回的周次数据数组
  240. * @returns {string|null} 开学日期字符串(YYYY-MM-DD),解析失败返回 null
  241. */
  242. function parseSchoolStartDate(weekList) {
  243. if (!Array.isArray(weekList) || weekList.length === 0) {
  244. return null;
  245. }
  246. // 找到第 1 周
  247. let firstWeek = weekList.find(item => String(item.zs) === '1');
  248. if (!firstWeek) {
  249. firstWeek = weekList.find(item => Number(item.zs) === 1);
  250. }
  251. // 如果还是没有,就按周次排序,取最小周次
  252. if (!firstWeek) {
  253. const sorted = [...weekList].sort((a, b) => Number(a.zs) - Number(b.zs));
  254. firstWeek = sorted[0];
  255. }
  256. if (!firstWeek) {
  257. return null;
  258. }
  259. // 优先从 rq 字段取开始日期
  260. const rq = String(firstWeek.rq || '');
  261. const rqMatch = rq.match(/\d{4}-\d{1,2}-\d{1,2}/);
  262. if (rqMatch) {
  263. return rqMatch[0];
  264. }
  265. // 兼容从 zcrq 字段取开始日期
  266. const zcrq = String(firstWeek.zcrq || '');
  267. const zcrqMatch = zcrq.match(/\d{4}-\d{1,2}-\d{1,2}/);
  268. if (zcrqMatch) {
  269. return zcrqMatch[0];
  270. }
  271. return null;
  272. }
  273. /**
  274. * 从教务系统周次数据中提取最大周次。
  275. * @param {Array} weekList - 教务系统周次数据数组
  276. * @returns {number|null} 最大周次,解析失败返回 null
  277. */
  278. function getMaxWeekFromSchoolWeekList(weekList) {
  279. if (!Array.isArray(weekList) || weekList.length === 0) {
  280. return null;
  281. }
  282. let max = 0;
  283. for (const item of weekList) {
  284. const week = Number(item.zs);
  285. if (!isNaN(week) && week > max) {
  286. max = week;
  287. }
  288. }
  289. return max > 0 ? max : null;
  290. }
  291. function getDefaultSemesterDate(academicYear, semesterIndex) {
  292. const year = Number(academicYear);
  293. if (isNaN(year)) return "2026-02-01";
  294. if (semesterIndex === 0) {
  295. return `${year}-09-01`;
  296. } else {
  297. return `${year + 1}-02-01`;
  298. }
  299. }
  300. // ===================== 全局常量 =====================
  301. const BASE_URL = 'https://jwglxt.gpnu.edu.cn';
  302. // ===================== 全局验证函数 =====================
  303. /**
  304. * 判断一个字符串是否为有效的日期(YYYY-MM-DD),并检查日期是否真实存在。
  305. * @param {string} dateStr - 日期字符串
  306. * @returns {boolean} 是否有效
  307. */
  308. function isValidDateString(dateStr) {
  309. if (!/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) return false;
  310. const [year, month, day] = dateStr.split('-').map(Number);
  311. if (year < 2000 || year > 2100) return false;
  312. if (month < 1 || month > 12) return false;
  313. const daysInMonth = new Date(year, month, 0).getDate();
  314. return day >= 1 && day <= daysInMonth;
  315. }
  316. /**
  317. * 校验学年输入:必须为四位数字,且在合理范围内(2000-2100)。
  318. * @param {string|null|undefined} input - 用户输入
  319. * @returns {string|false} 校验失败返回提示字符串,通过返回 false
  320. */
  321. function validateYearInput(input) {
  322. console.log("JS: validateYearInput被调用,输入: " + input);
  323. if (input === null || input === undefined) {
  324. return "请输入四位数字的学年喵~";
  325. }
  326. if (/^[0-9]{4}$/.test(input)) {
  327. const year = Number(input);
  328. if (year >= 2000 && year <= 2100) {
  329. return false;
  330. } else {
  331. return "学年需在 2000 到 2100 之间喵~";
  332. }
  333. } else {
  334. return "请输入四位数字的学年喵~";
  335. }
  336. }
  337. /**
  338. * 校验日期输入:允许空值,格式必须为 YYYY-MM-DD,且日期有效。
  339. * @param {string|null|undefined} input - 用户输入
  340. * @returns {string|false} 校验失败返回提示字符串,通过返回 false
  341. */
  342. function validateDateInput(input) {
  343. if (input === null || input === undefined || input.trim() === '') {
  344. return false;
  345. }
  346. if (isValidDateString(input.trim())) {
  347. return false;
  348. }
  349. return "日期格式应为 YYYY-MM-DD,且为有效日期喵~";
  350. }
  351. /**
  352. * 校验学期总周数输入:必须为 1~30 的整数。
  353. * @param {string|null|undefined} input - 用户输入
  354. * @returns {string|false} 校验失败返回提示字符串,通过返回 false
  355. */
  356. function validateWeeksInput(input) {
  357. if (input === null || input === undefined || input.trim() === '') {
  358. return "请输入学期总周数(1-30)喵~";
  359. }
  360. const num = Number(input.trim());
  361. if (Number.isInteger(num) && num >= 1 && num <= 30) {
  362. return false;
  363. }
  364. return "周数必须是 1 到 30 之间的整数喵~";
  365. }
  366. // ===================== 与原生交互的异步封装 =====================
  367. /**
  368. * 询问用户是否退出当前导入流程。
  369. * @returns {Promise<boolean>} true=退出,false=留在页面
  370. */
  371. async function promptUserToExit() {
  372. console.log("JS: 询问用户是否退出。");
  373. const options = ["退出", "留在页面"];
  374. const rawIndex = await window.shiguangBridgePromise.showSingleSelection(
  375. "导入流程已结束",
  376. JSON.stringify(options),
  377. 0 // 默认选中“退出”
  378. );
  379. // 用户取消选择(返回 null/undefined/'')时,默认不退出
  380. if (rawIndex === null || rawIndex === undefined || rawIndex === '') {
  381. console.log("JS: 用户取消退出选择,默认留在页面。");
  382. return false;
  383. }
  384. const index = Number(rawIndex);
  385. if (isNaN(index) || index < 0 || index >= options.length) {
  386. console.warn("JS: 退出选择索引无效,默认留在页面。");
  387. return false;
  388. }
  389. return index === 0;
  390. }
  391. async function promptUserToStart() {
  392. console.log("JS: 流程开始:显示公告。");
  393. return await window.shiguangBridgePromise.showAlert(
  394. "教务系统课表导入喵~",
  395. "Ciallo~ 导入前请确保您已成功登录教务系统哦喵~",
  396. "好的"
  397. );
  398. }
  399. /**
  400. * 显示课程导入成功的弹窗公告。
  401. * @param {number} length - 导入的课程数量
  402. */
  403. async function promptUserToSUCCESS(length) {
  404. console.log("JS: SUCCESS:显示公告。");
  405. return await window.shiguangBridgePromise.showAlert(
  406. "SUCCESS",
  407. `成功了喵~ 共导入 ${length} 门课程!`,
  408. "好的"
  409. );
  410. }
  411. const SCHOOL_START_MONTH = 8; // 默认
  412. async function getAcademicYear() {
  413. const now = new Date();
  414. let academicYear = now.getFullYear();
  415. if (now.getMonth() + 1 < SCHOOL_START_MONTH) {
  416. academicYear -= 1;
  417. }
  418. const defaultYear = academicYear.toString();
  419. return await window.shiguangBridgePromise.showPrompt(
  420. "选择学年喵~(Auto value)",
  421. "请输入要导入课程的起始学年喵~:",
  422. defaultYear,
  423. "validateYearInput"
  424. );
  425. }
  426. async function selectSemester() {
  427. const semesters = ["第一学期", "第二学期"];
  428. console.log("JS: 提示用户选择学期。");
  429. const rawIndex = await window.shiguangBridgePromise.showSingleSelection(
  430. "选择学期喵~",
  431. JSON.stringify(semesters),
  432. 0
  433. );
  434. if (rawIndex === null || rawIndex === undefined || rawIndex === '') {
  435. console.log("JS: 用户取消选择学期。");
  436. return null;
  437. }
  438. const index = Number(rawIndex);
  439. if (isNaN(index) || index < 0 || index >= semesters.length) {
  440. console.warn("JS: 学期索引无效,返回 null。");
  441. return null;
  442. }
  443. return index;
  444. }
  445. async function selectArea() {
  446. const areas = ["东/西/北校区", "白云校区", "河源校区"];
  447. console.log("JS: 提示用户选择校区。");
  448. const rawIndex = await window.shiguangBridgePromise.showSingleSelection(
  449. "选择校区喵~",
  450. JSON.stringify(areas),
  451. 0
  452. );
  453. if (rawIndex === null || rawIndex === undefined || rawIndex === '') {
  454. console.log("JS: 用户取消选择校区。");
  455. return null;
  456. }
  457. const index = Number(rawIndex);
  458. if (isNaN(index) || index < 0 || index >= areas.length) {
  459. console.warn("JS: 校区索引无效,返回 null。");
  460. return null;
  461. }
  462. return index;
  463. }
  464. async function selectTotalWeeksSource(courseMaxWeeks, schoolMaxWeeks) {
  465. const options = [
  466. `课程最大周数(${courseMaxWeeks}周)`,
  467. schoolMaxWeeks ? `教务系统周数(${schoolMaxWeeks}周)` : "教务系统周数(无数据)",
  468. "手动输入"
  469. ];
  470. console.log("JS: 提示用户选择周数来源。");
  471. const rawIndex = await window.shiguangBridgePromise.showSingleSelection(
  472. "选择学期总周数来源喵~(已自动解析)",
  473. JSON.stringify(options),
  474. 0
  475. );
  476. if (rawIndex === null || rawIndex === undefined || rawIndex === '') {
  477. console.log("JS: 用户取消选择周数来源。");
  478. return null;
  479. }
  480. const index = Number(rawIndex);
  481. if (isNaN(index) || index < 0 || index > 2) {
  482. console.warn("JS: 周数来源选择无效,默认使用课程最大周数。");
  483. return 0;
  484. }
  485. return index;
  486. }
  487. /**
  488. * 获取学期开始日期。
  489. * 优先使用传入的周次数据解析开学日期;若未传入或数据无效,则向教务系统请求。
  490. * 解析成功后弹出对话框让用户确认或修改,用户可留空跳过(返回 null)。
  491. * @param {string} academicYear - 学年(四位数字)
  492. * @param {number} semesterIndex - 学期索引(0 第一学期,1 第二学期)
  493. * @param {Array|null} [weekList=null] - 可选的周次数据数组,若提供且非空则直接使用
  494. * @returns {Promise<string|null>} 用户确认的日期字符串(YYYY-MM-DD),取消或留空返回 null
  495. */
  496. async function getSemesterStartDate(academicYear, semesterIndex, weekList = null) {
  497. console.log("JS: 正在获取学期周次数据以解析开学日期...");
  498. let defaultDate = getDefaultSemesterDate(academicYear, semesterIndex);
  499. // 1. 确定要使用的周次数据来源
  500. let data = weekList;
  501. if (!Array.isArray(data) || data.length === 0) {
  502. // 外部没有提供有效数据,尝试从教务系统获取
  503. try {
  504. data = await fetchSchoolWeekData(academicYear, getSemesterCode(semesterIndex));
  505. } catch (error) {
  506. console.warn("JS: 获取周次数据失败,将使用动态默认日期。", error);
  507. window.shiguangBridge.showToast("未能获取学期周次数据,开始日期可能需要手动填写。");
  508. data = null; // 确保后续不会使用无效数据
  509. }
  510. }
  511. // 2. 解析开学日期
  512. if (Array.isArray(data) && data.length > 0) {
  513. const parsedDate = parseSchoolStartDate(data);
  514. if (parsedDate) {
  515. defaultDate = parsedDate;
  516. console.log(`JS: 解析到的开学日期: ${defaultDate}`);
  517. } else {
  518. console.log("JS: 未能从周次数据解析出开学日期,使用动态默认值。");
  519. }
  520. } else {
  521. console.log("JS: 无可用周次数据,使用动态默认值。");
  522. }
  523. // 3. 弹出对话框让用户确认或修改日期(可留空跳过)
  524. console.log("JS: 提示用户输入学期开始日期(可留空跳过)。");
  525. const input = await window.shiguangBridgePromise.showPrompt(
  526. "学期开始日期喵~",
  527. "请输入学期第一天的日期喵~(YYYY-MM-DD)(已尝试自动解析)",
  528. defaultDate,
  529. "validateDateInput"
  530. );
  531. if (input === null || input === undefined) {
  532. console.log("JS: 用户取消了开始日期输入,继续流程。");
  533. return null;
  534. }
  535. if (input.trim() === '') {
  536. return null;
  537. }
  538. return input.trim();
  539. }
  540. async function getSemesterTotalWeeks(defaultWeeks) {
  541. console.log(`JS: 提示用户输入学期总周数,默认值:${defaultWeeks}`);
  542. const input = await window.shiguangBridgePromise.showPrompt(
  543. "设置学期总周数喵~",
  544. "请输入本学期的总周数喵~(1-30)",
  545. String(defaultWeeks),
  546. "validateWeeksInput"
  547. );
  548. if (input === null || input === undefined) {
  549. console.log("JS: 用户取消了总周数输入,流程继续使用默认值。");
  550. return defaultWeeks;
  551. }
  552. const weeks = Number(input.trim());
  553. if (!Number.isInteger(weeks) || weeks < 1 || weeks > 30) {
  554. console.warn("JS: 周数输入非法,使用默认值。");
  555. return defaultWeeks;
  556. }
  557. return weeks;
  558. }
  559. async function promptImportTimeSlots() {
  560. console.log("JS: 询问用户是否导入预设时间段。");
  561. return await window.shiguangBridgePromise.showAlert(
  562. "导入预设时间段喵~",
  563. "是否导入该校区对应的预设上课时间段喵~?",
  564. "导入"
  565. );
  566. }
  567. // ===================== 网络请求 =====================
  568. async function fetchCourseData(academicYear, semesterIndex) {
  569. const semesterCode = getSemesterCode(semesterIndex);
  570. const requestBody = new URLSearchParams({
  571. gnmkdm: 'N2151',
  572. xnm: academicYear,
  573. xqm: semesterCode,
  574. kzlx: 'ck',
  575. xsdm: '',
  576. kclbdm: '',
  577. kclxdm: ''
  578. }).toString();
  579. const targetUrls = [
  580. `${BASE_URL}/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151`,
  581. ];
  582. const errors = [];
  583. for (const url of targetUrls) {
  584. try {
  585. const response = await fetch(url, {
  586. method: "POST",
  587. headers: {
  588. "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
  589. "X-Requested-With": "XMLHttpRequest"
  590. },
  591. body: requestBody,
  592. credentials: "include"
  593. });
  594. if (!response.ok) {
  595. const msg = `HTTP ${response.status} ${response.statusText}`;
  596. errors.push(`${url} -> ${msg}`);
  597. console.warn(`课表数据请求失败(${msg}):${url}`);
  598. continue;
  599. }
  600. const jsonText = await response.text();
  601. if (jsonText.includes('<html')) {
  602. errors.push(`${url} -> 返回登录页面,可能登录已过期`);
  603. console.warn(`课表数据返回登录页面,可能未登录或会话过期:${url}`);
  604. continue;
  605. }
  606. let data;
  607. try {
  608. data = JSON.parse(jsonText);
  609. } catch (parseError) {
  610. errors.push(`${url} -> JSON 解析失败: ${parseError.message}`);
  611. console.warn(`课表数据 JSON 解析失败:${url}`, parseError);
  612. continue;
  613. }
  614. if (!data || !Array.isArray(data.kbList)) {
  615. errors.push(`${url} -> 返回数据缺少 kbList 数组`);
  616. console.warn(`课表数据缺少 kbList 字段:${url}`, data);
  617. continue;
  618. }
  619. return data;
  620. } catch (networkError) {
  621. errors.push(`${url} -> 网络异常: ${networkError.message}`);
  622. console.warn(`课表数据网络请求异常:${url}`, networkError);
  623. }
  624. }
  625. const errorMessage = errors.length > 0
  626. ? `课表数据获取失败,尝试了 ${targetUrls.length} 个地址:\n${errors.join('\n')}`
  627. : '课表数据获取失败,未配置任何请求地址';
  628. console.error(errorMessage);
  629. throw new Error(errorMessage);
  630. }
  631. async function fetchSchoolWeekData(xnm, xqm) {
  632. const targetUrls = [
  633. `${BASE_URL}/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154`,
  634. ];
  635. const requestBody = new URLSearchParams({
  636. xnm: xnm,
  637. xqm: xqm
  638. }).toString();
  639. const errors = [];
  640. for (const url of targetUrls) {
  641. try {
  642. const response = await fetch(url, {
  643. headers: {
  644. 'accept': 'application/json, text/javascript, */*; q=0.01',
  645. 'accept-language': 'zh-CN,zh;q=0.9',
  646. 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8',
  647. 'x-requested-with': 'XMLHttpRequest'
  648. },
  649. body: requestBody,
  650. method: 'POST',
  651. mode: 'cors',
  652. credentials: 'include'
  653. });
  654. if (!response.ok) {
  655. const msg = `HTTP ${response.status} ${response.statusText}`;
  656. errors.push(`${url} -> ${msg}`);
  657. console.warn(`周次数据请求失败(${msg}):${url}`);
  658. continue;
  659. }
  660. const text = await response.text();
  661. if (text.includes('<html')) {
  662. errors.push(`${url} -> 返回登录页面,可能登录已过期`);
  663. console.warn(`周次数据返回登录页面,可能未登录或会话过期:${url}`);
  664. continue;
  665. }
  666. let data;
  667. try {
  668. data = JSON.parse(text);
  669. } catch (parseError) {
  670. errors.push(`${url} -> JSON 解析失败: ${parseError.message}`);
  671. console.warn(`周次数据 JSON 解析失败:${url}`, parseError);
  672. continue;
  673. }
  674. if (!Array.isArray(data)) {
  675. const msg = '返回数据不是数组';
  676. errors.push(`${url} -> ${msg}`);
  677. console.warn(`周次数据格式错误(期望数组):${url}`, data);
  678. continue;
  679. }
  680. return data;
  681. } catch (networkError) {
  682. errors.push(`${url} -> 网络异常: ${networkError.message}`);
  683. console.warn(`周次数据网络请求异常:${url}`, networkError);
  684. }
  685. }
  686. const errorMessage = errors.length > 0
  687. ? `周次数据获取失败,尝试了 ${targetUrls.length} 个地址:\n${errors.join('\n')}`
  688. : '周次数据获取失败,未配置任何请求地址';
  689. console.error(errorMessage);
  690. throw new Error(errorMessage);
  691. }
  692. // ===================== 数据解析与配置构建 =====================
  693. function buildCourseDataFromRaw(rawData, startDate) {
  694. console.log("JS: buildCourseDataFromRaw 开始解析原始数据...");
  695. if (!rawData || !Array.isArray(rawData.kbList)) {
  696. throw new Error('课表数据格式错误或缺少 kbList 字段');
  697. }
  698. const courses = parseJsonData(rawData);
  699. if (courses.length === 0) {
  700. throw new Error('未解析到有效课程,请检查课表数据');
  701. }
  702. const inferredWeeks = inferTotalWeeks(courses);
  703. const config = {
  704. semesterStartDate: startDate,
  705. semesterTotalWeeks: inferredWeeks
  706. };
  707. console.log(`JS: 解析成功,课程数:${courses.length},推断总周数:${inferredWeeks}`);
  708. return { courses, config };
  709. }
  710. // ===================== 数据保存 =====================
  711. async function saveCourses(parsedCourses) {
  712. window.shiguangBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
  713. console.log(`JS: 尝试保存 ${parsedCourses.length} 门课程...`);
  714. try {
  715. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  716. console.log("JS: 课程保存成功!");
  717. return true;
  718. } catch (error) {
  719. window.shiguangBridge.showToast(`课程保存失败: ${error.message}`);
  720. console.error('JS: Save Courses Error:', error);
  721. return false;
  722. }
  723. }
  724. async function importPresetTimeSlots(timeSlots) {
  725. console.log(`JS: 准备导入 ${timeSlots.length} 个预设时间段。`);
  726. if (timeSlots.length > 0) {
  727. window.shiguangBridge.showToast(`正在导入 ${timeSlots.length} 个预设时间段...`);
  728. try {
  729. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  730. window.shiguangBridge.showToast("预设时间段导入成功!");
  731. } catch (error) {
  732. window.shiguangBridge.showToast("导入时间段失败: " + error.message);
  733. console.error('JS: Save Time Slots Error:', error);
  734. }
  735. } else {
  736. window.shiguangBridge.showToast("警告:时间段为空,未导入时间段信息。");
  737. console.warn("JS: 警告:传入时间段为空,未导入时间段信息。");
  738. }
  739. }
  740. // ===================== 三个校区的时间表 =====================
  741. const TimeSlots_one = [
  742. { number: 1, startTime: "08:20", endTime: "09:00" },
  743. { number: 2, startTime: "09:10", endTime: "09:50" },
  744. { number: 3, startTime: "10:00", endTime: "10:40" },
  745. { number: 4, startTime: "10:50", endTime: "11:30" },
  746. { number: 5, startTime: "13:30", endTime: "14:10" },
  747. { number: 6, startTime: "14:20", endTime: "15:00" },
  748. { number: 7, startTime: "15:10", endTime: "15:50" },
  749. { number: 8, startTime: "16:00", endTime: "16:40" },
  750. { number: 9, startTime: "18:40", endTime: "19:20" },
  751. { number: 10, startTime: "19:30", endTime: "20:10" },
  752. { number: 11, startTime: "20:20", endTime: "21:00" }
  753. ];
  754. const TimeSlots_two = [
  755. { number: 1, startTime: "08:30", endTime: "09:10" },
  756. { number: 2, startTime: "09:15", endTime: "09:55" },
  757. { number: 3, startTime: "10:05", endTime: "10:45" },
  758. { number: 4, startTime: "10:50", endTime: "11:30" },
  759. { number: 5, startTime: "13:30", endTime: "14:10" },
  760. { number: 6, startTime: "14:15", endTime: "14:55" },
  761. { number: 7, startTime: "15:05", endTime: "15:45" },
  762. { number: 8, startTime: "15:50", endTime: "16:30" },
  763. { number: 9, startTime: "18:40", endTime: "19:20" },
  764. { number: 10, startTime: "19:25", endTime: "20:05" },
  765. { number: 11, startTime: "20:10", endTime: "20:50" }
  766. ];
  767. const TimeSlots_three = [
  768. { number: 1, startTime: "08:20", endTime: "09:00" },
  769. { number: 2, startTime: "09:10", endTime: "09:50" },
  770. { number: 3, startTime: "10:10", endTime: "10:50" },
  771. { number: 4, startTime: "11:00", endTime: "11:40" },
  772. { number: 5, startTime: "13:50", endTime: "14:30" },
  773. { number: 6, startTime: "14:40", endTime: "15:20" },
  774. { number: 7, startTime: "15:40", endTime: "16:20" },
  775. { number: 8, startTime: "16:30", endTime: "17:10" },
  776. { number: 9, startTime: "18:40", endTime: "19:20" },
  777. { number: 10, startTime: "19:30", endTime: "20:10" },
  778. { number: 11, startTime: "20:20", endTime: "21:00" }
  779. ];
  780. // ===================== 索引 =====================
  781. function getSemesterCode(semesterIndex) {
  782. return semesterIndex === 0 ? "3" : "12";
  783. }
  784. function getTimeSlotsByAreaIndex(areaIndex) {
  785. if (areaIndex === 0) return TimeSlots_one;
  786. if (areaIndex === 1) return TimeSlots_two;
  787. if (areaIndex === 2) return TimeSlots_three;
  788. return TimeSlots_one;
  789. }
  790. // ===================== 主流程 =====================
  791. async function runImportFlow() {
  792. let shouldExit = false; // 最终是否退出(由用户选择决定)
  793. let flowEnded = false; // 标记流程是否已经走到需要询问退出的节点
  794. try {
  795. // ================= 流程开始 =================
  796. const alertConfirmed = await promptUserToStart();
  797. if (!alertConfirmed) {
  798. window.shiguangBridge.showToast("导入已取消。");
  799. console.log("JS: 用户取消了导入流程。");
  800. flowEnded = true;
  801. return; // 直接返回,但 finally 中会询问是否退出
  802. }
  803. // 获取学年
  804. const academicYear = await getAcademicYear();
  805. if (academicYear === null || academicYear === undefined) {
  806. window.shiguangBridge.showToast("导入已取消。");
  807. console.log("JS: 获取学年失败/取消,流程终止。");
  808. flowEnded = true;
  809. return;
  810. }
  811. console.log(`JS: 已选择学年: ${academicYear}`);
  812. // 选择学期
  813. const semesterIndex = await selectSemester();
  814. if (semesterIndex === null) {
  815. window.shiguangBridge.showToast("导入已取消。");
  816. console.log("JS: 选择学期失败/取消,流程终止。");
  817. flowEnded = true;
  818. return;
  819. }
  820. console.log(`JS: 已选择学期索引: ${semesterIndex}`);
  821. // 选择校区
  822. const areaIndex = await selectArea();
  823. if (areaIndex === null) {
  824. window.shiguangBridge.showToast("导入已取消。");
  825. console.log("JS: 选择校区失败/取消,流程终止。");
  826. flowEnded = true;
  827. return;
  828. }
  829. console.log(`JS: 已选择校区索引: ${areaIndex}`);
  830. // 获取周次数据(可选)
  831. let schoolWeekList = null;
  832. try {
  833. schoolWeekList = await fetchSchoolWeekData(academicYear, getSemesterCode(semesterIndex));
  834. } catch (error) {
  835. console.warn("JS: 获取周次数据失败,稍后周数来源可能缺少教务系统选项。", error);
  836. window.shiguangBridge.showToast("获取周次数据失败,将使用课程周数或手动输入。");
  837. schoolWeekList = [];
  838. }
  839. // 获取学期开始日期
  840. const startDate = await getSemesterStartDate(academicYear, semesterIndex, schoolWeekList);
  841. console.log(`JS: 学期开始日期输入结果: ${startDate}`);
  842. // 获取课表原始数据
  843. const rawData = await fetchCourseData(academicYear, semesterIndex);
  844. // 解析课程
  845. let courses, config;
  846. try {
  847. const parsedResult = buildCourseDataFromRaw(rawData, startDate);
  848. courses = parsedResult.courses;
  849. config = parsedResult.config;
  850. } catch (error) {
  851. window.shiguangBridge.showToast(error.message);
  852. console.error('JS: 课程解析失败:', error);
  853. flowEnded = true;
  854. return;
  855. }
  856. // 周数来源选择
  857. const courseMaxWeeks = config.semesterTotalWeeks;
  858. const schoolMaxWeeks = getMaxWeekFromSchoolWeekList(schoolWeekList);
  859. const weekSource = await selectTotalWeeksSource(courseMaxWeeks, schoolMaxWeeks);
  860. if (weekSource === null) {
  861. window.shiguangBridge.showToast("未选择周数来源,导入已取消。");
  862. console.log("JS: 用户取消周数来源选择,流程终止。");
  863. flowEnded = true;
  864. return;
  865. }
  866. let finalWeeks;
  867. if (weekSource === 0) {
  868. finalWeeks = courseMaxWeeks;
  869. } else if (weekSource === 1) {
  870. if (schoolMaxWeeks) {
  871. finalWeeks = schoolMaxWeeks;
  872. } else {
  873. window.shiguangBridge.showToast("教务系统周数不可用,已使用课程最大周数。");
  874. finalWeeks = courseMaxWeeks;
  875. }
  876. } else {
  877. finalWeeks = await getSemesterTotalWeeks(courseMaxWeeks);
  878. }
  879. config.semesterTotalWeeks = finalWeeks;
  880. // 保存课程
  881. const saveResult = await saveCourses(courses);
  882. if (!saveResult) {
  883. console.log("JS: 课程保存失败,流程终止。");
  884. flowEnded = true;
  885. return;
  886. }
  887. // 保存配置
  888. try {
  889. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  890. window.shiguangBridge.showToast(`课表配置更新成功!总周数:${config.semesterTotalWeeks}周。`);
  891. } catch (error) {
  892. window.shiguangBridge.showToast(`课表配置保存失败: ${error.message}`);
  893. console.error('JS: Save Config Error:', error);
  894. }
  895. // 导入预设时间段
  896. const timeSlots = getTimeSlotsByAreaIndex(areaIndex);
  897. const shouldImportTimeSlots = await promptImportTimeSlots();
  898. if (shouldImportTimeSlots) {
  899. await importPresetTimeSlots(timeSlots);
  900. } else {
  901. console.log("JS: 用户取消导入预设时间段,跳过。");
  902. window.shiguangBridge.showToast("已跳过导入时间段。");
  903. }
  904. await promptUserToSUCCESS(courses.length);
  905. console.log("JS: 整个导入流程执行完毕并成功。");
  906. flowEnded = true; // 成功结束
  907. } catch (e) {
  908. console.error('JS: 导入流程异常:', e);
  909. try {
  910. window.shiguangBridge.showToast('导入失败:' + (e && e.message ? e.message : e));
  911. } catch (_) {}
  912. flowEnded = true; // 异常结束
  913. } finally {
  914. // 无论成功、失败、取消,只要流程已经结束,就询问是否退出
  915. if (flowEnded) {
  916. shouldExit = await promptUserToExit();
  917. if (shouldExit) {
  918. console.log("JS: 用户选择退出,通知原生任务结束。");
  919. try {
  920. window.shiguangBridge.notifyTaskCompletion();
  921. } catch (error) {
  922. console.error("JS: 调用 notifyTaskCompletion 失败:", error);
  923. }
  924. } else {
  925. console.log("JS: 用户选择留在页面,不通知原生。");
  926. // 可在此处添加刷新页面或保持现状的逻辑
  927. window.location.reload();
  928. }
  929. }
  930. }
  931. }
  932. runImportFlow();