nuist.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. // 南京信息工程大学教务系统(jwxt.nuist.edu.cn)拾光课程表适配脚本
  2. function parseWeeks(skzc) {
  3. const value = String(skzc || "");
  4. const weeks = [];
  5. for (let i = 0; i < value.length; i++) {
  6. if (value[i] === "1") weeks.push(i + 1);
  7. }
  8. return weeks;
  9. }
  10. // jc.do 不可用时的 NUIST 默认作息(已由接口确认)。
  11. const DEFAULT_TIME_SLOTS = [
  12. { number: 1, startTime: "08:00", endTime: "08:45" },
  13. { number: 2, startTime: "08:55", endTime: "09:40" },
  14. { number: 3, startTime: "10:10", endTime: "10:55" },
  15. { number: 4, startTime: "11:05", endTime: "11:50" },
  16. { number: 5, startTime: "13:45", endTime: "14:30" },
  17. { number: 6, startTime: "14:40", endTime: "15:25" },
  18. { number: 7, startTime: "15:55", endTime: "16:40" },
  19. { number: 8, startTime: "16:50", endTime: "17:35" },
  20. { number: 9, startTime: "18:45", endTime: "19:30" },
  21. { number: 10, startTime: "19:40", endTime: "20:25" },
  22. { number: 11, startTime: "20:35", endTime: "21:20" },
  23. { number: 12, startTime: "21:25", endTime: "22:00" }
  24. ];
  25. function getErrorMessage(error) {
  26. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  27. if (typeof error === "string" && error.trim()) return error;
  28. try {
  29. const serialized = JSON.stringify(error);
  30. if (serialized && serialized !== "{}") return serialized;
  31. } catch (_) {
  32. // Ignore serialization failures and use the generic fallback below.
  33. }
  34. return "未知错误";
  35. }
  36. function parsePosition(row) {
  37. const campus = String(row.XXXQDM_DISPLAY || "").trim();
  38. const room = String(row.JASMC || "").trim();
  39. if (campus && room) return `${room}(${campus})`;
  40. return room || campus || "待定";
  41. }
  42. function parseCourse(row) {
  43. const day = Number(row.SKXQ);
  44. const startSection = Number(row.KSJC);
  45. const endSection = Number(row.JSJC);
  46. const weeks = parseWeeks(row.SKZC);
  47. if (!row.KCM || !day || day < 1 || day > 7 || !startSection || !endSection ||
  48. startSection > endSection || weeks.length === 0) return null;
  49. return {
  50. name: String(row.KCM).trim(),
  51. teacher: String(row.SKJS || "").split(/[\\/、,,]/)[0].trim() || "未知",
  52. position: parsePosition(row),
  53. day,
  54. startSection,
  55. endSection,
  56. weeks
  57. };
  58. }
  59. async function fetchCourses(xnxqdm) {
  60. const response = await fetch("/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do", {
  61. method: "POST",
  62. headers: {
  63. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  64. "X-Requested-With": "XMLHttpRequest"
  65. },
  66. body: `XNXQDM=${encodeURIComponent(xnxqdm)}`,
  67. credentials: "include"
  68. });
  69. if (!response.ok) throw new Error(`课表接口请求失败(HTTP ${response.status})`);
  70. const payload = await response.json();
  71. const table = payload && payload.datas && payload.datas.cxxszhxqkb;
  72. if (!table) throw new Error("教务系统返回数据格式异常。");
  73. if (table.extParams && Number(table.extParams.code) !== 1) {
  74. throw new Error(table.extParams.msg || "教务系统未发布该学期课表。");
  75. }
  76. const rows = Array.isArray(table.rows) ? table.rows : [];
  77. const courses = rows.map(parseCourse).filter(Boolean);
  78. if (courses.length === 0) throw new Error("未找到包含有效时间的课程。");
  79. courses.sort((a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name));
  80. return { courses, xnxqdm };
  81. }
  82. async function fetchCurrentSemester() {
  83. const response = await fetch("/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do", {
  84. method: "POST",
  85. headers: {
  86. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  87. "X-Requested-With": "XMLHttpRequest"
  88. },
  89. body: "",
  90. credentials: "include"
  91. });
  92. if (!response.ok) throw new Error(`当前学期接口请求失败(HTTP ${response.status})`);
  93. const payload = await response.json();
  94. const rows = payload && payload.datas && payload.datas.dqxnxq && payload.datas.dqxnxq.rows;
  95. const term = Array.isArray(rows) && rows.length > 0 ? rows[0] : null;
  96. if (!term || !term.DM) throw new Error("未获取到当前学期信息。");
  97. return {
  98. code: String(term.DM),
  99. name: String(term.MC || term.DM)
  100. };
  101. }
  102. async function fetchSemesterList() {
  103. const response = await fetch("/jwapp/sys/wdkb/modules/jshkcb/xnxqcx.do", {
  104. method: "POST",
  105. headers: {
  106. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  107. "X-Requested-With": "XMLHttpRequest"
  108. },
  109. body: "*order=-DM",
  110. credentials: "include"
  111. });
  112. if (!response.ok) throw new Error(`学期列表接口请求失败(HTTP ${response.status})`);
  113. const payload = await response.json();
  114. const rows = payload && payload.datas && payload.datas.xnxqcx && payload.datas.xnxqcx.rows;
  115. if (!Array.isArray(rows)) throw new Error("学期列表返回数据格式异常。");
  116. return rows.filter(item => item && item.DM).map(item => ({
  117. code: String(item.DM),
  118. name: String(item.MC || item.DM),
  119. year: item.XNDM,
  120. term: item.XQDM
  121. }));
  122. }
  123. async function selectSemester() {
  124. let semesters;
  125. try {
  126. semesters = await fetchSemesterList();
  127. } catch (error) {
  128. window.shiguangBridge.showToast(`获取学期列表失败:${getErrorMessage(error)}`);
  129. return null;
  130. }
  131. if (semesters.length === 0) {
  132. window.shiguangBridge.showToast("未查询到可用学期。");
  133. return null;
  134. }
  135. let currentCode = null;
  136. try {
  137. currentCode = (await fetchCurrentSemester()).code;
  138. } catch (error) {
  139. console.warn("获取当前学期失败,将不设置默认选项", error);
  140. }
  141. const visibleSemesters = semesters.slice(0, 10);
  142. const defaultIndex = currentCode
  143. ? visibleSemesters.findIndex(item => item.code === currentCode)
  144. : -1;
  145. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  146. "请选择学期",
  147. JSON.stringify(visibleSemesters.map(item => item.name)),
  148. defaultIndex
  149. );
  150. if (selectedIndex === null || selectedIndex < 0 || !visibleSemesters[selectedIndex]) return null;
  151. return visibleSemesters[selectedIndex];
  152. }
  153. async function fetchTimeSlots() {
  154. const response = await fetch("/jwapp/sys/wdkb/modules/jshkcb/jc.do", {
  155. method: "POST",
  156. headers: {
  157. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  158. "X-Requested-With": "XMLHttpRequest"
  159. },
  160. body: "",
  161. credentials: "include"
  162. });
  163. if (!response.ok) throw new Error(`节次时间接口请求失败(HTTP ${response.status})`);
  164. const payload = await response.json();
  165. const rows = payload && payload.datas && payload.datas.jc && payload.datas.jc.rows;
  166. if (!Array.isArray(rows) || rows.length === 0) return [];
  167. return rows.map(row => ({
  168. number: Number(row.DM),
  169. startTime: String(row.KSSJ || ""),
  170. endTime: String(row.JSSJ || "")
  171. })).filter(slot => slot.number > 0 && slot.startTime && slot.endTime)
  172. .sort((a, b) => a.number - b.number);
  173. }
  174. async function fetchSemesterConfig(xnxqdm) {
  175. const parts = String(xnxqdm || "").split("-");
  176. if (parts.length < 3) return { semesterStartDate: null, semesterTotalWeeks: 20 };
  177. const response = await fetch("/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do", {
  178. method: "POST",
  179. headers: {
  180. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
  181. "X-Requested-With": "XMLHttpRequest"
  182. },
  183. body: `XN=${encodeURIComponent(`${parts[0]}-${parts[1]}`)}&XQ=${encodeURIComponent(parts[2])}`,
  184. credentials: "include"
  185. });
  186. if (!response.ok) throw new Error(`学期配置接口请求失败(HTTP ${response.status})`);
  187. const payload = await response.json();
  188. const rows = payload && payload.datas && payload.datas.cxjcs && payload.datas.cxjcs.rows;
  189. const row = Array.isArray(rows) ? rows[0] : null;
  190. if (!row) return { semesterStartDate: null, semesterTotalWeeks: 20 };
  191. const rawDate = row.XQKSRQ || row.XQKSRQ_DISPLAY || row.KSRQ;
  192. const startDate = rawDate ? String(rawDate).split(/[ T]/)[0] : null;
  193. const totalWeeks = Number.parseInt(row.ZZC || row.ZCZ || row.ZC, 10);
  194. return {
  195. semesterStartDate: /^\d{4}-\d{2}-\d{2}$/.test(startDate || "") ? startDate : null,
  196. semesterTotalWeeks: Number.isFinite(totalWeeks) && totalWeeks > 0 ? totalWeeks : 20
  197. };
  198. }
  199. async function runImportFlow() {
  200. try {
  201. const confirmed = await window.shiguangBridgePromise.showAlert(
  202. "南京信息工程大学课表导入",
  203. "请先在当前教务页面完成登录,再开始导入。",
  204. "开始导入"
  205. );
  206. if (!confirmed) return;
  207. const semester = await selectSemester();
  208. if (!semester) {
  209. window.shiguangBridge.showToast("导入已取消。");
  210. return;
  211. }
  212. window.shiguangBridge.showToast(`已选择学期:${semester.name}`);
  213. const result = await fetchCourses(semester.code);
  214. let semesterConfig = { semesterStartDate: null, semesterTotalWeeks: 20 };
  215. try {
  216. semesterConfig = await fetchSemesterConfig(semester.code);
  217. } catch (error) {
  218. console.warn("获取学期配置失败,将使用默认配置", error);
  219. window.shiguangBridge.showToast("开学日期获取失败,已使用默认配置继续导入。");
  220. }
  221. let timeSlots = DEFAULT_TIME_SLOTS;
  222. try {
  223. const fetchedTimeSlots = await fetchTimeSlots();
  224. if (fetchedTimeSlots.length > 0) timeSlots = fetchedTimeSlots;
  225. } catch (error) {
  226. console.warn("NUIST time slot request failed, using defaults", error);
  227. window.shiguangBridge.showToast("作息时间获取失败,已使用默认时间继续导入。");
  228. }
  229. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
  230. ...semesterConfig,
  231. defaultClassDuration: 45,
  232. defaultBreakDuration: 10,
  233. firstDayOfWeek: 1
  234. }));
  235. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(result.courses));
  236. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  237. window.shiguangBridge.showToast(`成功导入 ${result.courses.length} 条课程记录(${result.xnxqdm})。`);
  238. window.shiguangBridge.notifyTaskCompletion();
  239. } catch (error) {
  240. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  241. console.error("NUIST adapter error", error);
  242. }
  243. }
  244. runImportFlow();