njcit.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. // 南京信息职业技术学院教务适配
  2. // 注意:本校教务页面加载的 zftal-ui 的 jquery.extends.contact-min.js
  3. // 会污染以下原型方法,本文件一律不使用:
  4. // Array.prototype.filter / some —— 回调实参变成 (下标, 元素, 数组)
  5. // Array.prototype.every —— 恒为 true
  6. // String.prototype.trim —— 删除所有空白而非仅首尾
  7. // 参考 resources/SCNU/scnu.js、resources/UJS/ujs_zhengfang_v9.0.js 的同类处理。
  8. const text = value => String(value == null ? '': value).replace(/^\s+|\s+$/g, '');
  9. async function promptUserToStart() {
  10. return await window.shiguangBridgePromise.showAlert(
  11. "教务系统课表导入",
  12. "导入前请确保您已在浏览器中成功登录教务系统(无需打开课表页面),点击开始后将弹窗选择学期。",
  13. "好的,开始导入"
  14. );
  15. }
  16. async function fetchAcademicOptions() {
  17. const url = "/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N2151&layout=default"
  18. try {
  19. const response = await fetch(url, {
  20. method : "GET",
  21. credentials: "include"
  22. })
  23. if (!response.ok) {
  24. return null;
  25. }
  26. const htmlText = await response.text();
  27. const doc = new DOMParser().parseFromString(htmlText, "text/html");
  28. const parseOptions = (selector) => {
  29. const list = [];
  30. let defaultIdx = 0;
  31. for (const opt of doc.querySelectorAll(`${selector} option`)) {
  32. const value = opt.getAttribute("value") || opt.value;
  33. if (!value) continue;
  34. const selected = opt.hasAttribute("selected") || opt.selected;
  35. if (selected) defaultIdx = list.length;
  36. list.push({value, text: text(opt.textContent), selected});
  37. }
  38. return {list, defaultIdx};
  39. }
  40. const {list: allYears, defaultIdx: yearIdx} = parseOptions("#xnm");
  41. const {list: semesterOptions, defaultIdx: defaultSemesterIndex } = parseOptions("#xqm");
  42. if (allYears.length == 0 || semesterOptions.length == 0) return null;
  43. const start = Math.max(0, yearIdx - 2);
  44. const end = Math.min(allYears.length, yearIdx + 3);
  45. const yearOptions = allYears.slice(start, end);
  46. return {
  47. yearOptions,
  48. semesterOptions,
  49. defaultYearIndex: yearIdx - start,
  50. defaultSemesterIndex
  51. }
  52. } catch(error) {
  53. console.error(error.message);
  54. return null;
  55. }
  56. }
  57. async function selectAcademicYearAndSemester() {
  58. const optionsData = await fetchAcademicOptions();
  59. if (!optionsData) {
  60. window.shiguangBridge.showToast("从教务系统读取学年学期失败,请确保登录状态。");
  61. return null;
  62. }
  63. const { yearOptions, semesterOptions, defaultYearIndex, defaultSemesterIndex } = optionsData;
  64. const yearTexts = yearOptions.map(item => item.text);
  65. const yearIndex = await window.shiguangBridgePromise.showSingleSelection(
  66. "选择学年",
  67. JSON.stringify(yearTexts),
  68. defaultYearIndex
  69. );
  70. if (yearIndex === null || yearIndex === -1) {
  71. return null;
  72. }
  73. const selectedYearCode = yearOptions[yearIndex].value;
  74. const semesterTexts = semesterOptions.map(item => item.text);
  75. const semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
  76. "选择学期",
  77. JSON.stringify(semesterTexts),
  78. defaultSemesterIndex
  79. );
  80. if (semesterIndex === null || semesterIndex === -1) return null;
  81. const selectedSemesterCode = semesterOptions[semesterIndex].value;
  82. return {
  83. academicYear: selectedYearCode,
  84. semesterCode: selectedSemesterCode
  85. };
  86. }
  87. async function fetchSemesterStartDate(academicYear, semesterCode) {
  88. const url = "/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154";
  89. const requestBody = `xnm=${academicYear}&xqm=${semesterCode}`;
  90. try {
  91. const response = await fetch(url, {
  92. method : "POST",
  93. headers : {
  94. "Content-Type" :"application/x-www-form-urlencoded;charset=UTF-8",
  95. "x-requested-with": "XMLHttpRequest"
  96. },
  97. body: requestBody,
  98. credentials: "include"
  99. });
  100. if (!response.ok) {
  101. return null;
  102. }
  103. const json = await response.json();
  104. if (Array.isArray(json) && json.length > 0) {
  105. const firstWeekObj = json.find(item => String(item.zs) == "1" || String(item.zsmc) == "1") || json[0];
  106. const totalWeeksInt = json.length;
  107. if (firstWeekObj.rq) {
  108. const startDateStr = firstWeekObj.rq.split('/')[0];
  109. if (/^\d{4}-\d{2}-\d{2}$/.test(startDateStr)) {
  110. return {
  111. semesterStartDate : startDateStr,
  112. semesterTotalWeeks: totalWeeksInt
  113. }
  114. }
  115. }
  116. }
  117. } catch(e) {
  118. console.error(e);
  119. }
  120. return null;
  121. }
  122. async function fetchTimesSlots(academicYear, semesterCode) {
  123. const url = "/jwglxt/kbcx/xskbcx_cxRjc.html?gnmkdm=N2151";
  124. const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&xqh_id=01`;
  125. try {
  126. const response = await fetch(url, {
  127. method : "POST",
  128. headers : {
  129. "Content-Type" :"application/x-www-form-urlencoded;charset=UTF-8",
  130. "x-requested-with": "XMLHttpRequest"
  131. },
  132. body: requestBody,
  133. credentials: "include"
  134. });
  135. if (!response.ok) {
  136. return null;
  137. }
  138. const json = await response.json();
  139. if (!Array.isArray(json)) {
  140. return null;
  141. }
  142. const timeSlots = [];
  143. for (const item of json) {
  144. if (!item.jcmc || !item.qssj || !item.jssj) {
  145. continue;
  146. }
  147. timeSlots.push({
  148. number: Number(item.jcmc),
  149. startTime: item.qssj.substring(0, 5),
  150. endTime: item.jssj.substring(0, 5)
  151. })
  152. }
  153. timeSlots.sort((a, b) => a.number - b.number);
  154. return timeSlots.length > 0 ? timeSlots : null;
  155. } catch (e) {
  156. console.error(e);
  157. return null;
  158. }
  159. }
  160. /**
  161. * 节次与周次合并去重函数
  162. * @param {Array<Object>} courses 原始解析课程数组
  163. * @returns {Array<Object>} 合并去重后的课程数组
  164. */
  165. function mergeAndDistinctCourses(courses) {
  166. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  167. // 1. 深拷贝并规范周次数据,过滤无效项
  168. const list = courses.map(c => ({
  169. ...c,
  170. name: c.name || '',
  171. teacher: c.teacher || '',
  172. position: c.position || '',
  173. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  174. }));
  175. // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
  176. list.sort((a, b) => {
  177. return a.name.localeCompare(b.name) ||
  178. a.teacher.localeCompare(b.teacher) ||
  179. a.position.localeCompare(b.position) ||
  180. (a.day || 0) - (b.day || 0) ||
  181. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  182. (a.startSection || 0) - (b.startSection || 0);
  183. });
  184. const step1Merged = [];
  185. let current = list[0];
  186. for (let i = 1; i < list.length; i++) {
  187. const next = list[i];
  188. const isSameCourseAndWeeks =
  189. current.name === next.name &&
  190. current.teacher === next.teacher &&
  191. current.position === next.position &&
  192. current.day === next.day &&
  193. current.weeks.join(',') === next.weeks.join(',');
  194. const isContinuous = current.endSection + 1 === next.startSection;
  195. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  196. if (isSameCourseAndWeeks && isContinuous) {
  197. // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
  198. current.endSection = next.endSection;
  199. } else if (isSameCourseAndWeeks && isDuplicate) {
  200. // 完全重复:跳过
  201. continue;
  202. } else {
  203. step1Merged.push(current);
  204. current = next;
  205. }
  206. }
  207. step1Merged.push(current);
  208. // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
  209. step1Merged.sort((a, b) => {
  210. return a.name.localeCompare(b.name) ||
  211. a.teacher.localeCompare(b.teacher) ||
  212. a.position.localeCompare(b.position) ||
  213. (a.day || 0) - (b.day || 0) ||
  214. (a.startSection || 0) - (b.startSection || 0) ||
  215. (a.endSection || 0) - (b.endSection || 0);
  216. });
  217. const step2Merged = [];
  218. let cur = step1Merged[0];
  219. for (let i = 1; i < step1Merged.length; i++) {
  220. const nxt = step1Merged[i];
  221. const isSameCourseAndSection =
  222. cur.name === nxt.name &&
  223. cur.teacher === nxt.teacher &&
  224. cur.position === nxt.position &&
  225. cur.day === nxt.day &&
  226. cur.startSection === nxt.startSection &&
  227. cur.endSection === nxt.endSection;
  228. if (isSameCourseAndSection) {
  229. // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周)
  230. cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
  231. } else {
  232. step2Merged.push(cur);
  233. cur = nxt;
  234. }
  235. }
  236. step2Merged.push(cur);
  237. return step2Merged;
  238. }
  239. function parseWeeks(weekStr) {
  240. if (!weekStr) return [];
  241. const weekSets = weekStr.split(',');
  242. let weeks = [];
  243. for (const set of weekSets) {
  244. const trimmedSet = text(set);
  245. const rangeMatch = trimmedSet.match(/(\d+)-(\d+)周/);
  246. const singleMatch = trimmedSet.match(/^(\d+)周/);
  247. let start = 0;
  248. let end = 0;
  249. let processed = false;
  250. if (rangeMatch) {
  251. start = Number(rangeMatch[1]);
  252. end = Number(rangeMatch[2]);
  253. processed = true;
  254. } else if (singleMatch) {
  255. start = end = Number(singleMatch[1]);
  256. processed = true;
  257. }
  258. if (processed) {
  259. const isSingle = trimmedSet.includes('(单)');
  260. const isDouble = trimmedSet.includes('(双)');
  261. for (let w = start; w <= end; w++) {
  262. if (isSingle && w % 2 === 0) continue;
  263. if (isDouble && w % 2 !== 0) continue;
  264. weeks.push(w);
  265. }
  266. }
  267. }
  268. return [...new Set(weeks)].sort((a, b) => a - b);
  269. }
  270. function parseCourseJsonData(jsonData) {
  271. if (!jsonData || !Array.isArray(jsonData.kbList)) {
  272. return [];
  273. }
  274. const rawCourseList = jsonData.kbList;
  275. let initalCourseList = [];
  276. for (const rawCourse of rawCourseList) {
  277. if (!rawCourse.kcmc || !rawCourse.xqj || !rawCourse.jcs || !rawCourse.zcd) {
  278. continue;
  279. }
  280. const sectionParts = rawCourse.jcs.split('-');
  281. const startSection = Number(sectionParts[0]);
  282. const endSection = Number(sectionParts[sectionParts.length - 1]);
  283. const day = Number(rawCourse.xqj);
  284. const weeks = parseWeeks(rawCourse.zcd);
  285. initalCourseList.push({
  286. name: text(rawCourse.kcmc),
  287. teacher: text(rawCourse.xm),
  288. position: text(rawCourse.cdmc || "未排地点"),
  289. day: day,
  290. startSection: startSection,
  291. endSection: endSection,
  292. weeks: weeks
  293. })
  294. }
  295. return mergeAndDistinctCourses(initalCourseList);
  296. }
  297. async function fetchAndParseCourses(academicYear, semesterCode) {
  298. const url = "/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151";
  299. const requestBody = `xnm=${academicYear}&xqm=${semesterCode}&kzlx=ck`;
  300. try {
  301. const [courseResponse, semesterStartDate, fetchedTimeSlots] = await Promise.all(
  302. [
  303. fetch(url, {
  304. method : "POST",
  305. headers : {
  306. "Content-Type" :"application/x-www-form-urlencoded;charset=UTF-8",
  307. "x-requested-with": "XMLHttpRequest"
  308. },
  309. body: requestBody,
  310. credentials: "include"
  311. }),
  312. fetchSemesterStartDate(academicYear, semesterCode),
  313. fetchTimesSlots(academicYear, semesterCode)
  314. ]
  315. );
  316. if (courseResponse.ok) {
  317. const json = await courseResponse.json();
  318. const courseData = parseCourseJsonData(json);
  319. return {
  320. courses: courseData,
  321. config: semesterStartDate,
  322. timeSlots: fetchedTimeSlots
  323. }
  324. }
  325. } catch(e) {
  326. console.error(e);
  327. }
  328. return null;
  329. }
  330. // 课程是必须的,保存失败就没有导入的意义,抛异常交给导入流程中断
  331. async function saveCourses(courses) {
  332. try {
  333. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  334. } catch (error) {
  335. throw new Error("课程保存失败:" + error.message);
  336. }
  337. }
  338. // 作息和学期配置不是必须的,失败只返回 false,由导入流程汇总提示
  339. async function saveTimeSlots(timeSlots) {
  340. try {
  341. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  342. return true;
  343. } catch (error) {
  344. console.error(error);
  345. return false;
  346. }
  347. }
  348. async function saveConfig(config) {
  349. try {
  350. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  351. return true;
  352. } catch (error) {
  353. console.error(error);
  354. return false;
  355. }
  356. }
  357. (async function runImportFlow() {
  358. const confirmed = await promptUserToStart();
  359. if (!confirmed) {
  360. window.shiguangBridge.showToast("用户取消了导入。");
  361. return;
  362. }
  363. const selection = await selectAcademicYearAndSemester();
  364. if (!selection) {
  365. window.shiguangBridge.showToast("未选择学年学期,导入流程终止。");
  366. return;
  367. }
  368. const { academicYear, semesterCode } = selection;
  369. const result = await fetchAndParseCourses(academicYear, semesterCode);
  370. if (!result || !result.courses || result.courses.length === 0) {
  371. window.shiguangBridge.showToast("未获取到有效课程,导入终止!");
  372. return;
  373. }
  374. const { courses, config, timeSlots } = result;
  375. try {
  376. await saveCourses(courses);
  377. } catch(error) {
  378. window.shiguangBridge.showToast(error.message);
  379. return;
  380. }
  381. // 记下没保存成功的部分,最后一起提示,避免提示互相覆盖
  382. const failed = [];
  383. if (config) {
  384. const saved = await saveConfig(config);
  385. if (!saved) failed.push("学期配置");
  386. }
  387. if (timeSlots) {
  388. const saved = await saveTimeSlots(timeSlots);
  389. if (!saved) failed.push("作息");
  390. }
  391. window.shiguangBridge.showToast(
  392. failed.length
  393. ? `成功导入 ${courses.length} 条排课记录,但${failed.join("、")}未保存成功`
  394. : `成功导入 ${courses.length} 条排课记录!`
  395. );
  396. window.shiguangBridge.notifyTaskCompletion();
  397. })();