cqtbi.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. // 重庆工商职业学院(重庆开放大学)教务系统课表导入适配脚本
  2. // 教务系统:强智 jsxsd(jwgl.cqtbi.edu.cn:81),导入页面 xsMainV.htmlx
  3. // 该校作息时间(12 节)
  4. const CQTBI_TIME_SLOTS = [
  5. { number: 1, startTime: "08:00", endTime: "08:40" },
  6. { number: 2, startTime: "08:50", endTime: "09:30" },
  7. { number: 3, startTime: "09:40", endTime: "10:20" },
  8. { number: 4, startTime: "10:40", endTime: "11:20" },
  9. { number: 5, startTime: "11:30", endTime: "12:10" },
  10. { number: 6, startTime: "14:00", endTime: "14:40" },
  11. { number: 7, startTime: "14:50", endTime: "15:30" },
  12. { number: 8, startTime: "15:40", endTime: "16:20" },
  13. { number: 9, startTime: "16:40", endTime: "17:20" },
  14. { number: 10, startTime: "17:30", endTime: "18:10" },
  15. { number: 11, startTime: "19:00", endTime: "19:40" },
  16. { number: 12, startTime: "19:50", endTime: "20:30" }
  17. ];
  18. function getErrorMessage(error) {
  19. if (error && typeof error.message === "string" && error.message.trim()) return error.message;
  20. if (typeof error === "string" && error.trim()) return error;
  21. try {
  22. const serialized = JSON.stringify(error);
  23. if (serialized && serialized !== "{}") return serialized;
  24. } catch (_) {
  25. // Ignore serialization failures and use the generic fallback below.
  26. }
  27. return "未知错误";
  28. }
  29. // 解析周次:"第2-5,7-10周(全部) 星期一" → [2,3,4,5,7,8,9,10]
  30. function parseWeeksText(weekStr) {
  31. const m = String(weekStr || "").match(/第([\d,\-、]+)周/);
  32. if (!m) return [];
  33. const weeks = new Set();
  34. m[1].split(/[,、]/).forEach(part => {
  35. const range = part.match(/^(\d+)-(\d+)$/);
  36. if (range) {
  37. const start = Number(range[1]);
  38. const end = Number(range[2]);
  39. const [lo, hi] = start < end ? [start, end] : [end, start];
  40. for (let i = lo; i <= hi; i++) weeks.add(i);
  41. } else if (/^\d+$/.test(part)) {
  42. weeks.add(Number(part));
  43. }
  44. });
  45. return Array.from(weeks).sort((a, b) => a - b);
  46. }
  47. // 解析节次:"01~02~03节" → { start: 1, end: 3 };"02~03节" → { start: 2, end: 3 }
  48. function parseSectionsText(sectionStr) {
  49. const nums = String(sectionStr || "").match(/\d+/g);
  50. if (!nums || nums.length === 0) return null;
  51. const list = nums.map(Number).filter(n => n > 0);
  52. if (list.length === 0) return null;
  53. let start = list[0];
  54. let end = list[0];
  55. for (let i = 1; i < list.length; i++) {
  56. if (list[i] === end + 1) end = list[i];
  57. else break;
  58. }
  59. return { start, end };
  60. }
  61. // 收集当前页及同域 iframe 的 document(跨域 iframe 无法访问则跳过)
  62. function collectDocs() {
  63. const docs = [document];
  64. document.querySelectorAll("iframe").forEach(f => {
  65. try {
  66. if (f.contentDocument && f.contentDocument.getElementById) docs.push(f.contentDocument);
  67. } catch (_) {
  68. // Cross-origin iframe, skip.
  69. }
  70. });
  71. return docs;
  72. }
  73. // 查找可访问的 jwgl 教务页面,取其 origin 作为接口基址
  74. function findJwglBase() {
  75. const candidates = collectDocs().map(d => {
  76. try { return d.location.href; } catch (_) { return ""; }
  77. });
  78. const jwgl = candidates.find(u => /jwgl\.cqtbi\.edu\.cn/i.test(u));
  79. if (jwgl) {
  80. try {
  81. return new URL(jwgl).origin;
  82. } catch (_) {
  83. // Fall through to window origin.
  84. }
  85. }
  86. return window.location.origin;
  87. }
  88. // 从课表页读取当前校区作息 ID(#kbjcmsid_ul 激活 tab 的 data-value)
  89. function readCampusSjms(doc) {
  90. const active = doc.querySelector("#kbjcmsid_ul li.layui-this[data-value]");
  91. if (active) return active.getAttribute("data-value");
  92. const any = doc.querySelector("#kbjcmsid_ul li[data-value]");
  93. return any ? any.getAttribute("data-value") : null;
  94. }
  95. // 从课表页读取当前学年学期(如 2026-2027-1),优先取选中项
  96. // 注意:该校下拉的 option 文本为 "2026-2027-1",value 为空,故需读文本内容
  97. function readSemesterId(doc) {
  98. for (const sel of doc.querySelectorAll("select")) {
  99. const selOpt = sel.selectedOptions[0] || sel.options[sel.selectedIndex];
  100. if (selOpt) {
  101. const text = (selOpt.textContent || selOpt.value || "").trim();
  102. if (/^\d{4}-\d{4}-\d$/.test(text)) return text;
  103. }
  104. }
  105. for (const opt of doc.querySelectorAll("select option")) {
  106. const text = (opt.textContent || opt.value || "").trim();
  107. if (/^\d{4}-\d{4}-\d$/.test(text)) return text;
  108. }
  109. return null;
  110. }
  111. // 从 #week 下拉读取学期配置:第一个日期为开学日,下拉项数即总周数
  112. function readSemesterConfig(doc) {
  113. const sel = doc.getElementById("week");
  114. if (!sel) return { startDate: null, totalWeeks: null };
  115. const dates = Array.from(sel.querySelectorAll("option"))
  116. .map(o => o.value.trim())
  117. .filter(v => /^\d{4}-\d{2}-\d{2}$/.test(v));
  118. return { startDate: dates[0] || null, totalWeeks: dates.length || null };
  119. }
  120. // 请求课表 API 并解析返回的 HTML(返回课程数组)
  121. async function fetchCoursesByApi() {
  122. const docs = collectDocs();
  123. // 参数优先从能读到校区 tab 的 document 读取
  124. let sjms = null;
  125. let xnxqid = null;
  126. for (const doc of docs) {
  127. if (!sjms) sjms = readCampusSjms(doc);
  128. if (!xnxqid) xnxqid = readSemesterId(doc);
  129. if (sjms && xnxqid) break;
  130. }
  131. if (!sjms) throw new Error("未能从页面读取校区信息(#kbjcmsid_ul),请确认在课表页面。");
  132. if (!xnxqid) throw new Error("未能从页面读取当前学年学期,请确认在课表页面。");
  133. const base = findJwglBase();
  134. const url = `${base}/jsxsd/framework/mainV_index_loadkb.htmlx` +
  135. `?rq=all&sjmsValue=${encodeURIComponent(sjms)}&xnxqid=${encodeURIComponent(xnxqid)}&xswk=false`;
  136. const response = await fetch(url, { credentials: "include" });
  137. if (!response.ok) throw new Error(`课表接口请求失败(HTTP ${response.status})`);
  138. const html = await response.text();
  139. const parsed = new DOMParser().parseFromString(html, "text/html");
  140. return parseCourseTableFromDom(parsed);
  141. }
  142. // 解析渲染后的课表表格
  143. function parseCourseTableFromDom(doc) {
  144. const table = doc.getElementById("timetable");
  145. if (!table) return [];
  146. const courses = [];
  147. table.querySelectorAll("tbody > tr").forEach(row => {
  148. if (row.querySelector("td[colspan]")) return; // 表头或分隔行
  149. const cells = row.querySelectorAll("td");
  150. if (cells.length < 2) return;
  151. for (let day = 1; day <= 7; day++) {
  152. const cell = cells[day];
  153. if (!cell) continue;
  154. cell.querySelectorAll(".item-box").forEach(box => {
  155. box.querySelectorAll(":scope > p").forEach(nameP => {
  156. try {
  157. const name = nameP.innerText.trim();
  158. if (!name) return;
  159. // 课程名 P 之后的 .tch-name(教师 + 节次)
  160. let tchName = nameP.nextElementSibling;
  161. while (tchName && (tchName.nodeType !== 1 || !tchName.classList.contains("tch-name"))) {
  162. tchName = tchName.nextElementSibling;
  163. }
  164. if (!tchName) return;
  165. const tchSpans = tchName.querySelectorAll("span");
  166. const teacher = (tchSpans[0] ? tchSpans[0].innerText.replace("教师:", "").trim() : "") || "未知";
  167. const sections = parseSectionsText(tchSpans[2] ? tchSpans[2].innerText : "");
  168. if (!sections) return;
  169. // 紧接着的 DIV:span[0] 教室、span[1] 周次
  170. let infoDiv = tchName.nextElementSibling;
  171. while (infoDiv && (infoDiv.nodeType !== 1 || infoDiv.tagName !== "DIV")) {
  172. infoDiv = infoDiv.nextElementSibling;
  173. }
  174. if (!infoDiv) return;
  175. const infoSpans = infoDiv.querySelectorAll("span");
  176. const position = infoSpans[0] ? infoSpans[0].innerText.trim() : "";
  177. const weeks = parseWeeksText(infoSpans[1] ? infoSpans[1].innerText : "");
  178. if (weeks.length === 0) return;
  179. courses.push({
  180. name,
  181. teacher,
  182. position: position || "待定",
  183. day,
  184. startSection: sections.start,
  185. endSection: sections.end,
  186. weeks
  187. });
  188. } catch (e) {
  189. console.error("JS: 解析课程失败", e);
  190. }
  191. });
  192. });
  193. }
  194. });
  195. return courses;
  196. }
  197. // 合并相同课程(同名/同师/同地/同星期/同节次)的周次
  198. function mergeCourses(courses) {
  199. const map = new Map();
  200. for (const c of courses) {
  201. const key = `${c.name}|${c.teacher}|${c.position}|${c.day}|${c.startSection}|${c.endSection}`;
  202. if (map.has(key)) {
  203. const existing = map.get(key);
  204. existing.weeks = Array.from(new Set([...existing.weeks, ...c.weeks])).sort((a, b) => a - b);
  205. } else {
  206. map.set(key, c);
  207. }
  208. }
  209. return Array.from(map.values());
  210. }
  211. // 保存作息时间(失败仅告警)
  212. async function saveTimeSlots(timeSlots) {
  213. if (!timeSlots || timeSlots.length === 0) return;
  214. try {
  215. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  216. } catch (error) {
  217. console.error("JS: 作息时间保存失败", error);
  218. }
  219. }
  220. async function runImportFlow() {
  221. const alertConfirmed = await window.shiguangBridgePromise.showAlert(
  222. "教务系统课表导入",
  223. "导入前请确保您已登录教务系统并进入课表页面",
  224. "好的,开始导入"
  225. );
  226. if (!alertConfirmed) {
  227. window.shiguangBridge.showToast("用户取消了导入。");
  228. return;
  229. }
  230. window.shiguangBridge.showToast("正在请求课表数据...");
  231. try {
  232. const courses = await fetchCoursesByApi();
  233. if (!courses || courses.length === 0) {
  234. throw new Error("接口返回的课表中未解析到课程,请确认当前学期有课且已选择全部周。");
  235. }
  236. const merged = mergeCourses(courses);
  237. // 学期配置(#week 下拉)可能在任意一个 iframe 中,遍历所有可访问 document 查找
  238. let config = { startDate: null, totalWeeks: null };
  239. for (const doc of collectDocs()) {
  240. config = readSemesterConfig(doc);
  241. if (config.startDate) break;
  242. }
  243. const { startDate, totalWeeks } = config;
  244. window.shiguangBridge.showToast(`正在保存 ${merged.length} 门课程...`);
  245. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(merged, null, 2));
  246. if (startDate && totalWeeks) {
  247. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify({
  248. semesterStartDate: startDate,
  249. semesterTotalWeeks: totalWeeks
  250. }));
  251. }
  252. await saveTimeSlots(CQTBI_TIME_SLOTS);
  253. window.shiguangBridge.showToast(`课程导入成功,共导入 ${merged.length} 门课程!`);
  254. window.shiguangBridge.notifyTaskCompletion();
  255. } catch (error) {
  256. window.shiguangBridge.showToast(`导入失败:${getErrorMessage(error)}`);
  257. console.error("JS: Import Error", error);
  258. }
  259. }
  260. runImportFlow();