sany.js 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. // 湖南三一工业职业技术学院(ehall.sanyedu.com)拾光课表导入适配脚本
  2. // 金智教务(jwapp/sys/wdkb 模块):
  3. // xskcb.do 学生课表:POST XNXQDM=学年学期,返回 datas.xskcb.rows[]
  4. // cxjcs.do 学期配置:返回当前学期开学日期(XQKSRQ)与总周数(ZZC)
  5. // 每日 10 节(无午休拆分的独立小节,晚饭 17:30-18:30 休息)
  6. // 预设作息时间(10 节)
  7. const SANY_TIME_SLOTS = [
  8. { number: 1, startTime: "08:20", endTime: "09:05" },
  9. { number: 2, startTime: "09:15", endTime: "10:00" },
  10. { number: 3, startTime: "10:20", endTime: "11:05" },
  11. { number: 4, startTime: "11:15", endTime: "12:00" },
  12. { number: 5, startTime: "14:00", endTime: "14:45" },
  13. { number: 6, startTime: "14:55", endTime: "15:40" },
  14. { number: 7, startTime: "15:50", endTime: "16:35" },
  15. { number: 8, startTime: "16:45", endTime: "17:30" },
  16. { number: 9, startTime: "19:00", endTime: "19:45" },
  17. { number: 10, startTime: "19:55", endTime: "20:40" }
  18. ];
  19. function getErrorMessage(error) {
  20. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  21. if (typeof error === "string" && error.trim()) return error;
  22. try {
  23. const serialized = JSON.stringify(error);
  24. if (serialized && serialized !== "{}") return serialized;
  25. } catch (_) {
  26. // Ignore serialization failures.
  27. }
  28. return "未知错误";
  29. }
  30. // 从当前页面 URL 提取 /jwapp 前缀(含 ehall 域名与可选哈希路径)
  31. function getApiBase() {
  32. const m = window.location.href.match(/^(https?:\/\/[^/]+(?:[^?#]*)?)\/jwapp/);
  33. return m ? m[1] : window.location.origin;
  34. }
  35. const API_BASE = getApiBase();
  36. const MODULES = {
  37. table: `${API_BASE}/jwapp/sys/wdkb/modules/xskcb/xskcb.do?enlink-vpn`,
  38. semester: `${API_BASE}/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do?enlink-vpn`
  39. };
  40. // 从页面存储中提取 JWT 鉴权 token(金智常见 key + 全量扫描兜底)
  41. function getAuthToken() {
  42. const keys = ["Authorization", "access_token", "token", "X-Access-Token", "x-token", "auth_token", "jwt", "user_token"];
  43. for (const k of keys) {
  44. try {
  45. const v = window.localStorage.getItem(k) || window.sessionStorage.getItem(k);
  46. if (v && v.includes("eyJ")) return v;
  47. } catch (_) {
  48. // Ignore storage access errors.
  49. }
  50. }
  51. const stores = [window.localStorage, window.sessionStorage];
  52. for (const store of stores) {
  53. try {
  54. for (let i = 0; i < store.length; i++) {
  55. const raw = store.getItem(store.key(i));
  56. if (!raw) continue;
  57. if (raw.startsWith("eyJ")) return raw;
  58. const m = raw.match(/"([^"]*eyJ[^"]*)"/);
  59. if (m) return m[1];
  60. }
  61. } catch (_) {
  62. // Ignore storage access errors.
  63. }
  64. }
  65. return null;
  66. }
  67. async function postForm(url, params) {
  68. const token = getAuthToken();
  69. const headers = { "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8" };
  70. if (token) headers["Authorization"] = token.startsWith("Bearer ") ? token : `Bearer ${token}`;
  71. const response = await fetch(url, {
  72. method: "POST",
  73. credentials: "include",
  74. headers,
  75. body: new URLSearchParams(params || {}).toString()
  76. });
  77. if (!response.ok) throw new Error(`接口请求失败(HTTP ${response.status})`);
  78. return response.json();
  79. }
  80. // 解析 SKZC 周次位图:"00001111111111111111" 从第5位起为1 → 5..20
  81. function parseWeekBitmap(bitmap) {
  82. const weeks = [];
  83. const s = String(bitmap || "");
  84. for (let i = 0; i < s.length; i++) {
  85. if (s.charAt(i) === "1") weeks.push(i + 1);
  86. }
  87. return [...new Set(weeks)].sort((a, b) => a - b);
  88. }
  89. // 获取当前学期配置:cxjcs.do 返回全部学期(无 SFSY 等可区分字段),
  90. // 按开学日期(XQKSRQ)取"最近开学且不晚于今天"的学期,得 XN/XQ/XQKSRQ/ZZC
  91. async function fetchSemesterConfig() {
  92. const payload = await postForm(MODULES.semester);
  93. const rows = payload && payload.datas && payload.datas.cxjcs && payload.datas.cxjcs.rows;
  94. if (!Array.isArray(rows) || rows.length === 0) {
  95. throw new Error("未获取到学期配置信息");
  96. }
  97. const today = new Date();
  98. let best = null;
  99. let bestDate = -Infinity;
  100. for (const s of rows) {
  101. const start = new Date(String(s.XQKSRQ || "").slice(0, 10) + "T00:00:00");
  102. if (isNaN(start.getTime()) || start > today) continue;
  103. const t = start.getTime();
  104. if (t > bestDate) {
  105. bestDate = t;
  106. best = s;
  107. }
  108. }
  109. if (!best) {
  110. // 兜底:全部开学日期都晚于今天(如学期刚开始数据未刷新),取可用的最近一条
  111. best = rows[0];
  112. }
  113. const xn = String(best.XN || "").trim();
  114. const xq = String(best.XQ || "").trim();
  115. if (!xn || !xq) throw new Error("学期配置缺少学年学期");
  116. const semesterStart = String(best.XQKSRQ || "").slice(0, 10);
  117. const totalWeeks = Number(best.ZZC) || Number(best.ZJXZC) || 0;
  118. return { xnxqdm: `${xn}-${xq}`, semesterStartDate: semesterStart, totalWeeks };
  119. }
  120. async function fetchCourses(xnxqdm) {
  121. const payload = await postForm(MODULES.table, { XNXQDM: xnxqdm });
  122. const rows = payload && payload.datas && payload.datas.xskcb && payload.datas.xskcb.rows;
  123. if (!Array.isArray(rows)) return [];
  124. return rows;
  125. }
  126. function parseSchedule(rows) {
  127. const courses = [];
  128. for (const row of rows) {
  129. const name = String(row.KCM || "").trim();
  130. if (!name) continue;
  131. const day = Number(row.SKXQ);
  132. const startSection = Number(row.KSJC);
  133. const endSection = Number(row.JSJC);
  134. if (!(day >= 1 && day <= 7) || !startSection || !endSection || endSection < startSection) continue;
  135. const weeks = parseWeekBitmap(row.SKZC);
  136. if (weeks.length === 0) continue;
  137. courses.push({
  138. name,
  139. teacher: String(row.SKJS || "").trim() || "未知",
  140. position: String(row.JASMC || "").trim() || "待定",
  141. day,
  142. startSection,
  143. endSection,
  144. weeks
  145. });
  146. }
  147. return courses;
  148. }
  149. // 合并同课程同时间同教室的条目
  150. function mergeCourses(courses) {
  151. const merged = new Map();
  152. for (const c of courses) {
  153. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}`;
  154. const holder = merged.get(key);
  155. if (holder) {
  156. holder.weeks = Array.from(new Set([...holder.weeks, ...c.weeks])).sort((a, b) => a - b);
  157. } else {
  158. merged.set(key, { ...c, weeks: [...c.weeks].sort((a, b) => a - b) });
  159. }
  160. }
  161. return Array.from(merged.values()).sort(
  162. (a, b) => a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name)
  163. );
  164. }
  165. // 保存作息时间
  166. async function saveTimeSlots(timeSlots) {
  167. try {
  168. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  169. } catch (error) {
  170. console.error("JS: 作息时间保存失败", error);
  171. }
  172. }
  173. async function runImportFlow() {
  174. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  175. "课表导入",
  176. "导入前请确保已登录并打开课表页面。",
  177. "好的,开始导入"
  178. );
  179. if (!alertConfirmed) {
  180. window.shiguangBridge.showToast("用户取消了导入。");
  181. return;
  182. }
  183. window.shiguangBridge.showToast("正在获取课表数据...");
  184. try {
  185. const config = await fetchSemesterConfig();
  186. const rows = await fetchCourses(config.xnxqdm);
  187. const courses = parseSchedule(rows);
  188. if (courses.length === 0) throw new Error("课表中未解析到有效课程,请确认本学期有课。");
  189. const merged = mergeCourses(courses);
  190. window.shiguangBridge.showToast(`正在保存 ${merged.length} 门课程...`);
  191. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(merged, null, 2));
  192. // 保存学期配置(开学日期 + 总周数)
  193. const courseConfig = { semesterTotalWeeks: config.totalWeeks };
  194. if (config.semesterStartDate) courseConfig.semesterStartDate = config.semesterStartDate;
  195. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(courseConfig));
  196. await saveTimeSlots(SANY_TIME_SLOTS);
  197. window.shiguangBridge.showToast(`课程导入成功,共导入 ${merged.length} 门课程!`);
  198. window.shiguangBridge.notifyTaskCompletion();
  199. } catch (error) {
  200. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  201. console.error("JS: Import Error", error);
  202. }
  203. }
  204. runImportFlow();