ntu.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. // 南通大学拾光课程表适配脚本
  2. // 通过正方教务V9个人/班级课表接口获取课程,并转换为导入格式
  3. (function () {
  4. const WINTER_TIME_SLOTS = [
  5. { number: 1, startTime: "07:50", endTime: "08:30" },
  6. { number: 2, startTime: "08:40", endTime: "09:20" },
  7. { number: 3, startTime: "09:35", endTime: "10:15" },
  8. { number: 4, startTime: "10:30", endTime: "11:10" },
  9. { number: 5, startTime: "11:20", endTime: "12:00" },
  10. { number: 6, startTime: "13:30", endTime: "14:10" },
  11. { number: 7, startTime: "14:20", endTime: "15:00" },
  12. { number: 8, startTime: "15:20", endTime: "16:00" },
  13. { number: 9, startTime: "16:10", endTime: "16:50" },
  14. { number: 10, startTime: "18:30", endTime: "19:10" },
  15. { number: 11, startTime: "19:20", endTime: "20:00" },
  16. { number: 12, startTime: "20:10", endTime: "20:50" }
  17. ];
  18. const SUMMER_TIME_SLOTS = [
  19. { number: 1, startTime: "07:50", endTime: "08:30" },
  20. { number: 2, startTime: "08:40", endTime: "09:20" },
  21. { number: 3, startTime: "09:35", endTime: "10:15" },
  22. { number: 4, startTime: "10:30", endTime: "11:10" },
  23. { number: 5, startTime: "11:20", endTime: "12:00" },
  24. { number: 6, startTime: "14:00", endTime: "14:40" },
  25. { number: 7, startTime: "14:50", endTime: "15:30" },
  26. { number: 8, startTime: "15:50", endTime: "16:30" },
  27. { number: 9, startTime: "16:40", endTime: "17:20" },
  28. { number: 10, startTime: "19:00", endTime: "19:40" },
  29. { number: 11, startTime: "19:50", endTime: "20:30" },
  30. { number: 12, startTime: "20:40", endTime: "21:20" }
  31. ];
  32. function parseSections(value) {
  33. const numbers = String(value || "").match(/\d+/g);
  34. if (!numbers || numbers.length === 0) return null;
  35. const startSection = Number(numbers[0]);
  36. const endSection = Number(numbers[numbers.length - 1]);
  37. if (!Number.isInteger(startSection) || !Number.isInteger(endSection) || startSection < 1 || endSection < startSection) return null;
  38. return { startSection, endSection };
  39. }
  40. // 将周次文本转换为周数数组
  41. function parseWeeks(value) {
  42. const weeks = new Set();
  43. String(value || "").replace(/(/g, "(").replace(/)/g, ")").split(/[,,、;]/).forEach((part) => {
  44. const numbers = part.match(/\d+/g);
  45. if (!numbers) return;
  46. const start = Number(numbers[0]);
  47. const end = Number(numbers[numbers.length - 1]);
  48. const odd = part.includes("单");
  49. const even = part.includes("双");
  50. for (let week = start; week <= end; week += 1) {
  51. if (odd && week % 2 === 0) continue;
  52. if (even && week % 2 !== 0) continue;
  53. weeks.add(week);
  54. }
  55. });
  56. return [...weeks].sort((left, right) => left - right);
  57. }
  58. function normalizeStartDate(value) {
  59. const match = String(value || "").match(/(\d{4})[-\/.年](\d{1,2})[-\/.月](\d{1,2})/);
  60. if (!match) return null;
  61. return `${match[1]}-${match[2].padStart(2, "0")}-${match[3].padStart(2, "0")}`;
  62. }
  63. function findStartDate(value) {
  64. if (!value || typeof value !== "object") return normalizeStartDate(value);
  65. if (Array.isArray(value)) {
  66. const firstWeek = value.find((item) => String(item?.zs) === "1" || String(item?.zsmc) === "1") || value[0];
  67. return normalizeStartDate(firstWeek?.zrq || firstWeek?.zcrq || firstWeek?.rq);
  68. }
  69. const firstWeekDate = normalizeStartDate(value.zrq || value.zcrq || value.rq);
  70. if (firstWeekDate) return firstWeekDate;
  71. for (const [key, item] of Object.entries(value)) {
  72. const date = item && typeof item === "object" ? findStartDate(item) : null;
  73. if (date) return date;
  74. }
  75. return null;
  76. }
  77. async function fetchSemesterStartDate() {
  78. const xnm = document.querySelector("#xnm")?.value;
  79. const xqm = document.querySelector("#xqm")?.value;
  80. if (!xnm || !xqm) {
  81. console.warn("NTU semester start date: missing xnm or xqm", { xnm, xqm });
  82. return null;
  83. }
  84. const url = new URL("/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154", window.location.origin);
  85. try {
  86. const response = await fetch(url.href, {
  87. headers: {
  88. accept: "application/json, text/javascript, */*; q=0.01",
  89. "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
  90. "x-requested-with": "XMLHttpRequest"
  91. },
  92. body: `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}`,
  93. method: "POST",
  94. credentials: "include"
  95. });
  96. const rawText = await response.text();
  97. let data;
  98. try {
  99. data = JSON.parse(rawText);
  100. } catch {
  101. console.warn("NTU semester start date: response is not JSON", response.status, rawText);
  102. return null;
  103. }
  104. console.log("NTU semester start date response:", data);
  105. const startDate = findStartDate(data);
  106. console.log("NTU semester start date:", startDate || "not found");
  107. return startDate;
  108. } catch (error) {
  109. console.warn("NTU semester start date request failed:", error);
  110. return null;
  111. }
  112. }
  113. // 将课程字段转换为导入协议字段
  114. function normalizeCourses(rawCourses) {
  115. const uniqueCourses = new Map();
  116. rawCourses.forEach((rawCourse) => {
  117. const name = String(rawCourse.kcmc || "").replace(/[■◆▲]/g, "").trim();
  118. const teacher = String(rawCourse.xm || "未知").trim() || "未知";
  119. const position = String(rawCourse.cdmc || "待定").trim() || "待定";
  120. const day = Number(rawCourse.xqj);
  121. const sections = parseSections(rawCourse.jcs || rawCourse.jc);
  122. const weeks = parseWeeks(rawCourse.zcd);
  123. if (!name || !sections || !Number.isInteger(day) || day < 1 || day > 7 || weeks.length === 0) return;
  124. const course = { name, teacher, position, day, startSection: sections.startSection, endSection: sections.endSection, weeks };
  125. const key = [name, teacher, position, day, sections.startSection, sections.endSection, weeks.join(",")].join("|");
  126. if (!uniqueCourses.has(key)) uniqueCourses.set(key, course);
  127. });
  128. return [...uniqueCourses.values()].sort((left, right) => left.day - right.day || left.startSection - right.startSection || left.name.localeCompare(right.name));
  129. }
  130. // 合并同一课程的重复节次
  131. function mergeAndDistinctCourses(courses) {
  132. if (courses.length <= 1) return courses;
  133. const list = courses.map((course) => ({ ...course, weeks: [...new Set(course.weeks)].sort((left, right) => left - right) }));
  134. const sameBase = (left, right) => left.name === right.name && left.teacher === right.teacher && left.position === right.position && left.day === right.day;
  135. list.sort((left, right) => left.name.localeCompare(right.name) || left.teacher.localeCompare(right.teacher) || left.position.localeCompare(right.position) || left.day - right.day || left.startSection - right.startSection);
  136. const merged = [];
  137. for (const course of list) {
  138. const previous = merged[merged.length - 1];
  139. if (previous && sameBase(previous, course) && previous.startSection === course.startSection && previous.endSection === course.endSection) {
  140. previous.weeks = [...new Set([...previous.weeks, ...course.weeks])].sort((left, right) => left - right);
  141. } else if (previous && sameBase(previous, course) && previous.weeks.join(",") === course.weeks.join(",") && previous.endSection + 1 === course.startSection) {
  142. previous.endSection = course.endSection;
  143. } else {
  144. merged.push(course);
  145. }
  146. }
  147. return merged;
  148. }
  149. // 个人课表
  150. async function fetchApiCourses() {
  151. const xnm = document.querySelector("#xnm")?.value;
  152. const xqm = document.querySelector("#xqm")?.value;
  153. if (!xnm || !xqm) return null;
  154. const url = new URL("/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N2151", window.location.origin);
  155. try {
  156. const response = await fetch(url.href, {
  157. method: "POST",
  158. headers: { "content-type": "application/x-www-form-urlencoded;charset=UTF-8", "x-requested-with": "XMLHttpRequest" },
  159. body: `xnm=${encodeURIComponent(xnm)}&xqm=${encodeURIComponent(xqm)}&kzlx=ck&xsdm=&kclbdm=`,
  160. credentials: "include"
  161. });
  162. const data = await response.json();
  163. console.log("NTU course API response:", data);
  164. const rawCourses = data?.kbList || data?.datas?.kbList || data?.rows;
  165. if (!Array.isArray(rawCourses) || !rawCourses.length) return null;
  166. return mergeAndDistinctCourses(normalizeCourses(rawCourses));
  167. } catch (error) {
  168. console.warn("NTU course API request failed:", error);
  169. return null;
  170. }
  171. }
  172. // 班级课表
  173. async function fetchClassApiCourses() {
  174. const pageMap = window.api?.data?.map || {};
  175. const readValue = (key) => pageMap[key] ?? document.querySelector(`#${key}`)?.value ?? "";
  176. const requestFields = [
  177. "xnm",
  178. "xqm",
  179. "njdm_id",
  180. "zyh_id",
  181. "bh_id",
  182. "tjkbzdm",
  183. "tjkbzxsdm",
  184. "zxszjjs"
  185. ];
  186. const body = new URLSearchParams();
  187. requestFields.forEach((key) => {
  188. const value = readValue(key);
  189. if (value !== "" && value !== null && typeof value !== "undefined") body.set(key, String(value));
  190. });
  191. if (!body.get("xnm") || !body.get("xqm") || !body.get("bh_id")) {
  192. console.warn("NTU class course API parameters are incomplete", Object.fromEntries(body));
  193. return null;
  194. }
  195. const url = new URL("/jwglxt/kbdy/bjkbdy_cxBjKb.html", window.location.origin);
  196. try {
  197. const response = await fetch(url.href, {
  198. method: "POST",
  199. headers: { "content-type": "application/x-www-form-urlencoded;charset=UTF-8", "x-requested-with": "XMLHttpRequest" },
  200. body,
  201. credentials: "include"
  202. });
  203. const data = await response.json();
  204. console.log("NTU class course API response:", data);
  205. const rawCourses = data?.kbList || data?.datas?.kbList || data?.rows;
  206. if (!Array.isArray(rawCourses) || !rawCourses.length) return null;
  207. return mergeAndDistinctCourses(normalizeCourses(rawCourses));
  208. } catch (error) {
  209. console.warn("NTU class course API request failed:", error);
  210. return null;
  211. }
  212. }
  213. async function runImport() {
  214. try {
  215. if (!await window.shiguangBridgePromise.showAlert("南通大学课表导入", "导入前请确保已登录南通大学教务系统。", "开始导入")) return;
  216. const formAction = document.querySelector("#ajaxForm")?.getAttribute("action") || "";
  217. const isPersonalPage = /xskbcx_cxXskbcxIndex/i.test(formAction) || /\/kbcx\/xskbcx/i.test(window.location.pathname);
  218. const pageCourses = isPersonalPage ? await fetchApiCourses() : await fetchClassApiCourses();
  219. if (pageCourses?.length) {
  220. const pageWeeks = pageCourses.flatMap((course) => course.weeks);
  221. const semesterStartDate = await fetchSemesterStartDate();
  222. if (!semesterStartDate) throw new Error("未获取到开学日期,无法判断令时");
  223. const startMonth = Number(semesterStartDate.slice(5, 7));
  224. if (!Number.isInteger(startMonth) || startMonth < 1 || startMonth > 12) throw new Error(`开学日期格式无效:${semesterStartDate}`);
  225. const timeSlots = startMonth <= 6 ? WINTER_TIME_SLOTS : SUMMER_TIME_SLOTS;
  226. const courseConfig = { semesterTotalWeeks: Math.max(19, ...pageWeeks), defaultClassDuration: 40, firstDayOfWeek: 1 };
  227. courseConfig.semesterStartDate = semesterStartDate;
  228. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(courseConfig));
  229. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  230. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(pageCourses));
  231. window.shiguangBridge.showToast(`导入成功:${pageCourses.length} 条课程安排`);
  232. window.shiguangBridge.notifyTaskCompletion();
  233. return;
  234. }
  235. window.shiguangBridge.showToast("当前页面未找到课表,请先打开个人或班级课表页面。");
  236. } catch (error) {
  237. console.error("NTU adapter error", error);
  238. window.shiguangBridge.showToast(`导入失败:${error.message}`);
  239. }
  240. }
  241. runImport();
  242. })();