cug.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. // ============================================================================
  2. // 中国地质大学(武汉) 正方教务系统 拾光课程表适配脚本
  3. // ----------------------------------------------------------------------------
  4. // 适用学校:中国地质大学(南望山校区 / 未来城校区)
  5. // 系统类型:正方教务管理系统(jwglxt)
  6. // 获取策略:优先通过正方标准 API 请求结构化数据,失败时自动回退 DOM 页面解析
  7. // 特性支持:
  8. // 1. 只要登录教务系统任意页面即可触发导入,无需手动定位课表页
  9. // 2. 弹窗选择目标学年与学期(支持跨学年/往期课表导入)
  10. // 3. 自动识别校区并弹窗提供三套作息方案:
  11. // - 南望山校区 · 秋冬季(12节,下午 14:00 上课)
  12. // - 南望山校区 · 春夏季(12节,5月1日后下午 14:30 上课)
  13. // - 未来城校区 · 标准作息(12节,上午 08:30 上课)
  14. // 4. 自动调用周次接口获取准确开学日期
  15. // ============================================================================
  16. /** 南望山校区 · 秋冬季作息(共12节) */
  17. const CUG_NANWANGSHAN_AUTUMN = [
  18. { number: 1, startTime: "08:00", endTime: "08:45" },
  19. { number: 2, startTime: "08:50", endTime: "09:35" },
  20. { number: 3, startTime: "10:05", endTime: "10:50" },
  21. { number: 4, startTime: "10:55", endTime: "11:40" },
  22. { number: 5, startTime: "14:00", endTime: "14:45" },
  23. { number: 6, startTime: "14:50", endTime: "15:35" },
  24. { number: 7, startTime: "16:00", endTime: "16:45" },
  25. { number: 8, startTime: "16:50", endTime: "17:35" },
  26. { number: 9, startTime: "19:00", endTime: "19:45" },
  27. { number: 10, startTime: "19:50", endTime: "20:35" },
  28. { number: 11, startTime: "20:40", endTime: "21:25" },
  29. { number: 12, startTime: "21:30", endTime: "22:15" }
  30. ];
  31. /** 南望山校区 · 春夏季作息(5月1日后执行,下午推迟30分钟,共12节) */
  32. const CUG_NANWANGSHAN_SUMMER = [
  33. { number: 1, startTime: "08:00", endTime: "08:45" },
  34. { number: 2, startTime: "08:50", endTime: "09:35" },
  35. { number: 3, startTime: "10:05", endTime: "10:50" },
  36. { number: 4, startTime: "10:55", endTime: "11:40" },
  37. { number: 5, startTime: "14:30", endTime: "15:15" },
  38. { number: 6, startTime: "15:20", endTime: "16:05" },
  39. { number: 7, startTime: "16:35", endTime: "17:20" },
  40. { number: 8, startTime: "17:25", endTime: "18:10" },
  41. { number: 9, startTime: "19:30", endTime: "20:15" },
  42. { number: 10, startTime: "20:20", endTime: "21:05" },
  43. { number: 11, startTime: "21:10", endTime: "21:55" },
  44. { number: 12, startTime: "22:00", endTime: "22:45" }
  45. ];
  46. /** 未来城校区 · 标准作息(全年统一,共12节) */
  47. const CUG_FUTURE_CITY = [
  48. { number: 1, startTime: "08:30", endTime: "09:15" },
  49. { number: 2, startTime: "09:20", endTime: "10:05" },
  50. { number: 3, startTime: "10:15", endTime: "11:00" },
  51. { number: 4, startTime: "11:05", endTime: "11:50" },
  52. { number: 5, startTime: "14:00", endTime: "14:45" },
  53. { number: 6, startTime: "14:50", endTime: "15:35" },
  54. { number: 7, startTime: "15:45", endTime: "16:30" },
  55. { number: 8, startTime: "16:35", endTime: "17:20" },
  56. { number: 9, startTime: "18:30", endTime: "19:15" },
  57. { number: 10, startTime: "19:20", endTime: "20:05" },
  58. { number: 11, startTime: "20:15", endTime: "21:00" },
  59. { number: 12, startTime: "21:05", endTime: "21:50" }
  60. ];
  61. /** 作息方案选项 */
  62. const SCHEDULE_OPTIONS = [
  63. { name: "南望山校区 · 秋冬季作息(下午 14:00 上课)", slots: CUG_NANWANGSHAN_AUTUMN },
  64. { name: "南望山校区 · 春夏季作息(5月1日后,下午 14:30 上课)", slots: CUG_NANWANGSHAN_SUMMER },
  65. { name: "未来城校区 · 标准作息(上午 08:30 上课,全天12节)", slots: CUG_FUTURE_CITY }
  66. ];
  67. /**
  68. * 解析周次字符串,处理单双周和多周次区间
  69. * 示例:"1-2周,4-5周,7-11周" / "1-16周(单)" / "7周"
  70. */
  71. function parseWeeks(weekStr) {
  72. if (!weekStr) return [];
  73. const weeks = [];
  74. String(weekStr).split(/[,,]/).forEach(part => {
  75. let p = (part || "").replace(/周/g, "").trim();
  76. if (!p) return;
  77. const single = p.includes("(单)");
  78. const double = p.includes("(双)");
  79. p = p.replace(/\(单\)|\(双\)/g, "").trim();
  80. let s, e;
  81. const range = p.match(/(\d+)\s*-\s*(\d+)/);
  82. if (range) {
  83. s = +range[1];
  84. e = +range[2];
  85. } else if (/^\d+$/.test(p)) {
  86. s = e = +p;
  87. } else {
  88. return;
  89. }
  90. for (let w = s; w <= e; w++) {
  91. if (single && w % 2 === 0) continue;
  92. if (double && w % 2 !== 0) continue;
  93. weeks.push(w);
  94. }
  95. });
  96. return [...new Set(weeks)].sort((a, b) => a - b);
  97. }
  98. /**
  99. * 课程合并与去重:同名同师同地同天同节次合并周次
  100. */
  101. function mergeAndDistinctCourses(courses) {
  102. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  103. const out = [];
  104. courses.forEach(c => {
  105. const weeks = Array.isArray(c.weeks) ? [...c.weeks] : [];
  106. const idx = out.findIndex(x =>
  107. x.name === c.name && x.teacher === c.teacher && x.position === c.position &&
  108. x.day === c.day && x.startSection === c.startSection && x.endSection === c.endSection
  109. );
  110. if (idx >= 0) {
  111. out[idx].weeks = [...new Set([...out[idx].weeks, ...weeks])].sort((a, b) => a - b);
  112. } else {
  113. out.push({ ...c, weeks: weeks });
  114. }
  115. });
  116. return out;
  117. }
  118. /**
  119. * 解析正方 API 返回的 JSON (kbList)
  120. */
  121. function parseJsonData(jsonData) {
  122. if (!jsonData || !Array.isArray(jsonData.kbList)) {
  123. return [];
  124. }
  125. const courses = [];
  126. for (const item of jsonData.kbList) {
  127. if (!item.kcmc || !item.jcs || !item.xqj || !item.zcd) continue;
  128. const weeks = parseWeeks(item.zcd);
  129. if (!weeks.length) continue;
  130. const jcParts = String(item.jcs).split("-");
  131. const startSection = parseInt(jcParts[0], 10);
  132. const endSection = parseInt(jcParts[jcParts.length - 1], 10);
  133. const day = parseInt(item.xqj, 10);
  134. if (isNaN(startSection) || isNaN(endSection) || isNaN(day) || day < 1 || day > 7) {
  135. continue;
  136. }
  137. const campus = (item.xqmc || "").trim();
  138. const classroom = (item.cdmc || "").trim();
  139. let position = "";
  140. if (campus && classroom) {
  141. position = classroom.includes(campus) ? classroom : `${campus} ${classroom}`;
  142. } else {
  143. position = classroom || campus;
  144. }
  145. courses.push({
  146. name: (item.kcmc || "").trim(),
  147. teacher: (item.xm || "").trim(),
  148. position: position.trim(),
  149. day: day,
  150. startSection: startSection,
  151. endSection: endSection,
  152. weeks: weeks,
  153. isCustomTime: false
  154. });
  155. }
  156. return mergeAndDistinctCourses(courses);
  157. }
  158. /**
  159. * 从教务系统读取学年和学期选项
  160. */
  161. async function fetchAcademicOptions() {
  162. const url = window.location.origin + "/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151";
  163. try {
  164. const resp = await fetch(url, { method: "GET", credentials: "include" });
  165. if (!resp.ok) return null;
  166. const html = await resp.text();
  167. const parser = new DOMParser();
  168. const doc = parser.parseFromString(html, "text/html");
  169. const yearOptions = Array.from(doc.querySelectorAll("#xnm option"))
  170. .filter(opt => opt.value !== "")
  171. .map(opt => ({
  172. value: opt.value.trim(),
  173. text: opt.textContent.trim(),
  174. selected: opt.selected
  175. }));
  176. const semesterOptions = Array.from(doc.querySelectorAll("#xqm option"))
  177. .filter(opt => opt.value !== "")
  178. .map(opt => {
  179. const text = opt.textContent.trim();
  180. const semName = text === "3" || text === "1" ? "第1学期" :
  181. text === "12" || text === "2" ? "第2学期" :
  182. text === "16" || text === "3" ? "第3学期(小学期)" : `第${text}学期`;
  183. return {
  184. value: opt.value.trim(),
  185. text: semName,
  186. selected: opt.selected
  187. };
  188. });
  189. if (!yearOptions.length || !semesterOptions.length) return null;
  190. let defYearIdx = yearOptions.findIndex(o => o.selected);
  191. if (defYearIdx === -1) defYearIdx = 0;
  192. let defSemIdx = semesterOptions.findIndex(o => o.selected);
  193. if (defSemIdx === -1) defSemIdx = 0;
  194. return {
  195. yearOptions,
  196. semesterOptions,
  197. defaultYearIndex: defYearIdx,
  198. defaultSemesterIndex: defSemIdx
  199. };
  200. } catch (e) {
  201. console.warn("读取学年学期选项失败:", e);
  202. return null;
  203. }
  204. }
  205. /**
  206. * 提示用户选择学年和学期
  207. */
  208. async function selectAcademicYearAndSemester() {
  209. const options = await fetchAcademicOptions();
  210. // 若无法通过后台请求获取选项,尝试从当前页面已有元素提取
  211. if (!options) {
  212. const xnmEl = document.querySelector("#xnm");
  213. const xqmEl = document.querySelector("#xqm");
  214. if (xnmEl && xqmEl && xnmEl.value && xqmEl.value) {
  215. return { academicYear: xnmEl.value.trim(), semesterCode: xqmEl.value.trim(), label: "当前选中学期" };
  216. }
  217. return null;
  218. }
  219. const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = options;
  220. // 1. 选择学年
  221. const yearTexts = yearOptions.map(o => o.text);
  222. const selectedYearIdx = await window.shiguangBridgePromise.showSingleSelection(
  223. "请选择导入学年",
  224. JSON.stringify(yearTexts),
  225. defaultYearIndex
  226. );
  227. if (selectedYearIdx === null || selectedYearIdx === undefined || selectedYearIdx < 0) return null;
  228. const chosenYear = yearOptions[selectedYearIdx];
  229. // 2. 选择学期
  230. const semTexts = semesterOptions.map(o => o.text);
  231. const selectedSemIdx = await window.shiguangBridgePromise.showSingleSelection(
  232. `学年:${chosenYear.text}\n请选择导入学期`,
  233. JSON.stringify(semTexts),
  234. defaultSemesterIndex
  235. );
  236. if (selectedSemIdx === null || selectedSemIdx === undefined || selectedSemIdx < 0) return null;
  237. const chosenSem = semesterOptions[selectedSemIdx];
  238. return {
  239. academicYear: chosenYear.value,
  240. semesterCode: chosenSem.value,
  241. label: `${chosenYear.text}学年 ${chosenSem.text}`
  242. };
  243. }
  244. /**
  245. * 调用正方开学周次接口获取第1周周一日期
  246. */
  247. async function fetchSemesterStartDate(academicYear, semesterCode) {
  248. if (!academicYear || !semesterCode) return null;
  249. const url = window.location.origin + "/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154";
  250. try {
  251. const resp = await fetch(url, {
  252. method: "POST",
  253. headers: {
  254. "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
  255. "x-requested-with": "XMLHttpRequest"
  256. },
  257. body: `xnm=${encodeURIComponent(academicYear)}&xqm=${encodeURIComponent(semesterCode)}`,
  258. credentials: "include"
  259. });
  260. if (resp.ok) {
  261. const json = await resp.json();
  262. if (Array.isArray(json) && json.length > 0) {
  263. const first = json.find(i => String(i.zs) === "1" || String(i.zsmc) === "1") || json[0];
  264. const raw = first && (first.rq || first.zcrq || first.ksrq);
  265. const m = raw && String(raw).match(/(\d{4}-\d{2}-\d{2})/);
  266. if (m) return m[1];
  267. }
  268. }
  269. } catch (e) {
  270. console.warn("获取开学日期失败:", e);
  271. }
  272. return null;
  273. }
  274. /**
  275. * 调用正方 API 获取课程数据
  276. */
  277. async function fetchCoursesByApi(academicYear, semesterCode) {
  278. const url = window.location.origin + "/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151";
  279. const body = `xnm=${encodeURIComponent(academicYear)}&xqm=${encodeURIComponent(semesterCode)}&kzlx=ck&xsdm=&kclbdm=`;
  280. const resp = await fetch(url, {
  281. method: "POST",
  282. headers: {
  283. "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
  284. "x-requested-with": "XMLHttpRequest"
  285. },
  286. body: body,
  287. credentials: "include"
  288. });
  289. if (!resp.ok) return null;
  290. const jsonData = await resp.json();
  291. return parseJsonData(jsonData);
  292. }
  293. /** 智能推测默认作息索引 */
  294. function inferDefaultScheduleIndex(courses) {
  295. if (!courses || !courses.length) return 0;
  296. let fcCount = 0, nwsCount = 0;
  297. courses.forEach(c => {
  298. if (c.position.includes("未来城")) fcCount++;
  299. if (c.position.includes("南望山")) nwsCount++;
  300. });
  301. if (fcCount > nwsCount) {
  302. return 2; // 未来城校区
  303. }
  304. const currentMonth = new Date().getMonth() + 1;
  305. if (currentMonth >= 5 && currentMonth <= 9) {
  306. return 1; // 南望山春夏季
  307. }
  308. return 0; // 南望山秋冬季
  309. }
  310. /**
  311. * 主导入流程
  312. */
  313. async function runImportFlow() {
  314. console.log("正在启动中国地质大学课表导入流程...");
  315. // 1. 登录与域名检测
  316. const host = window.location.host;
  317. if (!host.includes("cug.edu.cn")) {
  318. await window.shiguangBridgePromise.showAlert(
  319. "请先登录教务系统",
  320. "您当前未在学校教务系统域名下。\n\n请在统一身份认证登录并进入教务系统后,再次点击导入。",
  321. "好的"
  322. );
  323. return;
  324. }
  325. window.shiguangBridge.showToast("正在读取学年学期数据...");
  326. // 2. 交互选择学年与学期
  327. const termSelection = await selectAcademicYearAndSemester();
  328. if (!termSelection) {
  329. window.shiguangBridge.showToast("未完成学年学期选择,导入流程已取消。");
  330. return;
  331. }
  332. const { academicYear, semesterCode, label } = termSelection;
  333. window.shiguangBridge.showToast(`正在获取 ${label} 课程数据...`);
  334. // 3. 并行获取课程数据与开学日期
  335. let courses = null;
  336. let startDate = null;
  337. try {
  338. const [fetchedCourses, fetchedStartDate] = await Promise.all([
  339. fetchCoursesByApi(academicYear, semesterCode),
  340. fetchSemesterStartDate(academicYear, semesterCode)
  341. ]);
  342. courses = fetchedCourses;
  343. startDate = fetchedStartDate;
  344. } catch (err) {
  345. console.error("API请求异常:", err);
  346. }
  347. // 4. 若 API 未返回数据,提示用户
  348. if (!courses || courses.length === 0) {
  349. await window.shiguangBridgePromise.showAlert(
  350. "未获取到课程数据",
  351. `在 [${label}] 中未查询到任何课程记录。\n\n可能原因:\n1. 该学期尚未排课或您在该学期无选课记录;\n2. 登录状态可能已失效,请尝试刷新页面重新登录。`,
  352. "我知道了"
  353. );
  354. return;
  355. }
  356. console.log(`成功解析到 ${courses.length} 条有效课程。`, courses);
  357. // 5. 用户确认学期并选择校区作息
  358. const defaultIdx = inferDefaultScheduleIndex(courses);
  359. const optionsLabels = SCHEDULE_OPTIONS.map(opt => opt.name);
  360. let selectedIdx = await window.shiguangBridgePromise.showSingleSelection(
  361. `识别到 ${courses.length} 门课程 (${label})\n请选择您的校区与作息时间:`,
  362. JSON.stringify(optionsLabels),
  363. defaultIdx
  364. );
  365. if (selectedIdx === null || selectedIdx === undefined || selectedIdx < 0) {
  366. selectedIdx = defaultIdx;
  367. window.shiguangBridge.showToast("已使用推荐作息: " + SCHEDULE_OPTIONS[selectedIdx].name);
  368. }
  369. const chosenSchedule = SCHEDULE_OPTIONS[selectedIdx];
  370. // 6. 保存课程
  371. try {
  372. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  373. } catch (error) {
  374. console.error("保存课程失败:", error);
  375. window.shiguangBridge.showToast("课程保存失败: " + error.message);
  376. return;
  377. }
  378. // 7. 保存作息时间段
  379. try {
  380. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(chosenSchedule.slots));
  381. } catch (error) {
  382. console.error("保存作息时间段失败:", error);
  383. }
  384. // 8. 保存课表配置
  385. const courseConfig = {
  386. semesterTotalWeeks: 20,
  387. firstDayOfWeek: 1
  388. };
  389. if (startDate) {
  390. courseConfig.semesterStartDate = startDate;
  391. console.log("开学日期设定为:", startDate);
  392. }
  393. try {
  394. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(courseConfig));
  395. } catch (error) {
  396. console.error("保存课表配置失败:", error);
  397. }
  398. // 9. 完成提示
  399. const dateTip = startDate ? `\n开学日期已自动设定为: ${startDate}` : "\n未自动识别到开学日期,可在App「课表设置」中核对。";
  400. await window.shiguangBridgePromise.showAlert(
  401. "导入成功",
  402. `已成功导入 ${courses.length} 门课程! (${label})\n作息已设为:${chosenSchedule.name}${dateTip}`,
  403. "完成"
  404. );
  405. window.shiguangBridge.showToast(`导入成功,共导入 ${courses.length} 门课程!`);
  406. window.shiguangBridge.notifyTaskCompletion();
  407. }
  408. // 启动导入流程
  409. runImportFlow();