qut.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. // 青岛理工大学 - 正方教务系统 课程表适配脚本
  2. /**
  3. * 解析周次字符串,处理单双周和周次范围。
  4. */
  5. function parseWeeks(zcd) {
  6. if (!zcd) return [];
  7. var weekSets = String(zcd).replace(/,/g, ',').split(',');
  8. var weeks = [];
  9. for (var i = 0; i < weekSets.length; i++) {
  10. var trimmedSet = weekSets[i].trim();
  11. var rangeMatch = trimmedSet.match(/(\d+)\s*-\s*(\d+)\s*周?/);
  12. var singleMatch = trimmedSet.match(/^(\d+)\s*周?/);
  13. var start = 0;
  14. var end = 0;
  15. var processed = false;
  16. if (rangeMatch) {
  17. start = Number(rangeMatch[1]);
  18. end = Number(rangeMatch[2]);
  19. processed = true;
  20. } else if (singleMatch) {
  21. start = end = Number(singleMatch[1]);
  22. processed = true;
  23. }
  24. if (processed && start >= 1 && end >= start) {
  25. var isSingle = trimmedSet.indexOf('(单)') !== -1;
  26. var isDouble = trimmedSet.indexOf('(双)') !== -1;
  27. for (var w = start; w <= end; w++) {
  28. if (isSingle && w % 2 === 0) continue;
  29. if (isDouble && w % 2 !== 0) continue;
  30. weeks.push(w);
  31. }
  32. }
  33. }
  34. var uniqueWeeks = [];
  35. var seen = {};
  36. for (var j = 0; j < weeks.length; j++) {
  37. if (!seen[weeks[j]]) {
  38. seen[weeks[j]] = true;
  39. uniqueWeeks.push(weeks[j]);
  40. }
  41. }
  42. uniqueWeeks.sort(function(a, b) { return a - b; });
  43. return uniqueWeeks;
  44. }
  45. /**
  46. * 清洗课程名称中的特殊字符
  47. */
  48. function cleanCourseName(name) {
  49. return name.replace(/[★○●◇◆]/g, '').trim();
  50. }
  51. function mergeAndDistinctCourses(courses) {
  52. var sorted = courses.slice().sort(function(a, b) {
  53. return a.name.localeCompare(b.name) ||
  54. a.teacher.localeCompare(b.teacher) ||
  55. a.position.localeCompare(b.position) ||
  56. a.day - b.day ||
  57. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  58. a.startSection - b.startSection;
  59. });
  60. var merged = [];
  61. sorted.forEach(function(course) {
  62. var previous = merged[merged.length - 1];
  63. var sameCourse = previous && previous.name === course.name &&
  64. previous.teacher === course.teacher && previous.position === course.position &&
  65. previous.day === course.day && previous.weeks.join(',') === course.weeks.join(',');
  66. if (sameCourse && previous.endSection + 1 >= course.startSection) {
  67. previous.endSection = Math.max(previous.endSection, course.endSection);
  68. } else if (sameCourse && previous.startSection === course.startSection && previous.endSection === course.endSection) {
  69. return;
  70. } else {
  71. merged.push(course);
  72. }
  73. });
  74. var sameSection = new Map();
  75. merged.forEach(function(course) {
  76. var key = [course.name, course.teacher, course.position, course.day,
  77. course.startSection, course.endSection].join('|');
  78. if (sameSection.has(key)) {
  79. var existing = sameSection.get(key);
  80. existing.weeks = Array.from(new Set(existing.weeks.concat(course.weeks))).sort(function(a, b) { return a - b; });
  81. } else {
  82. sameSection.set(key, course);
  83. }
  84. });
  85. return Array.from(sameSection.values()).sort(function(a, b) {
  86. return a.day - b.day || a.startSection - b.startSection || a.name.localeCompare(b.name);
  87. });
  88. }
  89. /**
  90. * 解析 API 返回的 JSON 数据。
  91. */
  92. function parseJsonData(jsonData) {
  93. console.log("JS: parseJsonData 正在解析 JSON 数据...");
  94. if (!jsonData || !Array.isArray(jsonData.kbList)) {
  95. console.warn("JS: JSON 数据结构错误或缺少 kbList 字段。");
  96. return [];
  97. }
  98. var rawCourseList = jsonData.kbList;
  99. var finalCourseList = [];
  100. for (var i = 0; i < rawCourseList.length; i++) {
  101. var rawCourse = rawCourseList[i];
  102. if (!rawCourse || typeof rawCourse !== "object" || !rawCourse.kcmc ||
  103. rawCourse.xqj == null || rawCourse.jcs == null || rawCourse.zcd == null) {
  104. continue;
  105. }
  106. var weeksArray = parseWeeks(rawCourse.zcd);
  107. if (weeksArray.length === 0) {
  108. continue;
  109. }
  110. var sectionMatch = String(rawCourse.jcs).match(/^\s*(?:第)?(\d+)\s*(?:-\s*(\d+))?\s*节?\s*$/);
  111. if (!sectionMatch) {
  112. console.warn("JS: 跳过无法解析节次的课程:" + rawCourse.kcmc + ",节次=" + rawCourse.jcs);
  113. continue;
  114. }
  115. var startSection = Number(sectionMatch[1]);
  116. var endSection = Number(sectionMatch[2] || sectionMatch[1]);
  117. var day = Number(rawCourse.xqj);
  118. if (!Number.isInteger(day) || !Number.isInteger(startSection) || !Number.isInteger(endSection) ||
  119. day < 1 || day > 7 || startSection < 1 || endSection < startSection) {
  120. continue;
  121. }
  122. finalCourseList.push({
  123. name: cleanCourseName(String(rawCourse.kcmc)),
  124. teacher: rawCourse.xm == null ? "" : String(rawCourse.xm).trim(),
  125. position: rawCourse.cdmc == null ? "" : String(rawCourse.cdmc).trim(),
  126. day: day,
  127. startSection: startSection,
  128. endSection: endSection,
  129. weeks: weeksArray
  130. });
  131. }
  132. finalCourseList = mergeAndDistinctCourses(finalCourseList);
  133. console.log("JS: JSON 数据解析完成,共找到 " + finalCourseList.length + " 门课程。");
  134. return finalCourseList;
  135. }
  136. /**
  137. * 构建课表配置,从课程数据中推断最大周次。
  138. */
  139. function buildCourseConfig(courses) {
  140. var maxWeek = 0;
  141. for (var i = 0; i < courses.length; i++) {
  142. var course = courses[i];
  143. for (var j = 0; j < course.weeks.length; j++) {
  144. if (course.weeks[j] > maxWeek) {
  145. maxWeek = course.weeks[j];
  146. }
  147. }
  148. }
  149. return {
  150. semesterStartDate: null,
  151. semesterTotalWeeks: maxWeek || 20,
  152. firstDayOfWeek: 1
  153. };
  154. }
  155. /**
  156. * 检查是否在登录页面。
  157. */
  158. function isLoginPage() {
  159. var url = window.location.href;
  160. return url.indexOf("/jwglxt/xtgl/login_slogin.html") !== -1;
  161. }
  162. async function promptUserToStart() {
  163. console.log("JS: 流程开始:显示公告。");
  164. return await window.shiguangBridgePromise.showAlert(
  165. "青岛理工大学课表导入",
  166. "导入前请确保您已在浏览器中成功登录青岛理工大学教务系统。\n脚本将通过正方教务接口读取课表。",
  167. "好的,开始导入"
  168. );
  169. }
  170. function parseSelectOptions(selectElement) {
  171. if (!selectElement) return { options: [], defaultIndex: 0 };
  172. var options = [];
  173. var defaultIndex = 0;
  174. Array.from(selectElement.querySelectorAll("option")).forEach(function(option) {
  175. var value = String(option.value || "").trim();
  176. if (!value) return;
  177. var text = String(option.textContent || "").trim() || value;
  178. if (option.selected) defaultIndex = options.length;
  179. options.push({ value: value, text: text });
  180. });
  181. return { options: options, defaultIndex: defaultIndex };
  182. }
  183. function parseTermOptions(doc) {
  184. var yearData = parseSelectOptions(doc.querySelector("#xnm"));
  185. var semesterData = parseSelectOptions(doc.querySelector("#xqm"));
  186. if (!yearData.options.length || !semesterData.options.length) {
  187. throw new Error("课表页面未找到有效的学年或学期选项");
  188. }
  189. return { yearData: yearData, semesterData: semesterData };
  190. }
  191. function isOnTimetablePage() {
  192. return window.location.pathname.indexOf("/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html") !== -1;
  193. }
  194. function readCurrentTerm() {
  195. var yearElement = document.querySelector("#xnm");
  196. var semesterElement = document.querySelector("#xqm");
  197. var academicYear = yearElement ? String(yearElement.value || "").trim() : "";
  198. var semesterCode = semesterElement ? String(semesterElement.value || "").trim() : "";
  199. if (!academicYear || !semesterCode) {
  200. throw new Error("当前课表页面未读取到学年学期,请先在教务系统页面选择学年和学期");
  201. }
  202. return { academicYear: academicYear, semesterCode: semesterCode };
  203. }
  204. async function fetchTermPage() {
  205. var url = window.location.origin + "/jwglxt/kbcx/xskbcx_cxXskbcxIndex.html?gnmkdm=N253508&layout=default";
  206. var response = await fetch(url, { method: "GET", credentials: "include" });
  207. if (!response.ok) throw new Error("读取课表页面失败:HTTP " + response.status);
  208. return new DOMParser().parseFromString(await response.text(), "text/html");
  209. }
  210. async function selectTermFromPage(doc) {
  211. var termData = parseTermOptions(doc);
  212. var yearIndex = await window.shiguangBridgePromise.showSingleSelection(
  213. "选择学年", JSON.stringify(termData.yearData.options.map(function(item) { return item.text; })), termData.yearData.defaultIndex
  214. );
  215. if (yearIndex === null || yearIndex === -1 || !termData.yearData.options[yearIndex]) throw new Error("已取消或无法识别学年选择");
  216. var semesterIndex = await window.shiguangBridgePromise.showSingleSelection(
  217. "选择学期", JSON.stringify(termData.semesterData.options.map(function(item) { return item.text; })), termData.semesterData.defaultIndex
  218. );
  219. if (semesterIndex === null || semesterIndex === -1 || !termData.semesterData.options[semesterIndex]) throw new Error("已取消或无法识别学期选择");
  220. return {
  221. academicYear: termData.yearData.options[yearIndex].value,
  222. semesterCode: termData.semesterData.options[semesterIndex].value
  223. };
  224. }
  225. async function resolveTerm() {
  226. if (isOnTimetablePage()) return readCurrentTerm();
  227. return await selectTermFromPage(await fetchTermPage());
  228. }
  229. function normalizeStartDate(value) {
  230. var match = String(value || "").match(/(\d{4})[-\/.年](\d{1,2})[-\/.月](\d{1,2})/);
  231. if (!match) return null;
  232. return match[1] + "-" + match[2].padStart(2, "0") + "-" + match[3].padStart(2, "0");
  233. }
  234. function findSemesterStartDate(value) {
  235. if (value == null) return null;
  236. if (typeof value !== "object") return normalizeStartDate(value);
  237. if (Array.isArray(value)) {
  238. var firstWeek = value.find(function(item) {
  239. return item && (String(item.zs) === "1" || String(item.zsmc) === "1");
  240. }) || value[0];
  241. var firstWeekDate = findSemesterStartDate(firstWeek);
  242. if (firstWeekDate) return firstWeekDate;
  243. for (var i = 0; i < value.length; i++) {
  244. var date = findSemesterStartDate(value[i]);
  245. if (date) return date;
  246. }
  247. return null;
  248. }
  249. var fields = ["zrq", "zcrq", "rq", "ksrq"];
  250. for (var j = 0; j < fields.length; j++) {
  251. var fieldDate = normalizeStartDate(value[fields[j]]);
  252. if (fieldDate) return fieldDate;
  253. }
  254. var values = Object.values(value);
  255. for (var k = 0; k < values.length; k++) {
  256. var nestedDate = findSemesterStartDate(values[k]);
  257. if (nestedDate) return nestedDate;
  258. }
  259. return null;
  260. }
  261. async function fetchSemesterStartDate(academicYear, semesterCode) {
  262. var url = window.location.origin + "/jwglxt/kbcx/xskbcxZccx_cxZcByXnxq.html?gnmkdm=N2154";
  263. var requestBody = "xnm=" + encodeURIComponent(academicYear) + "&xqm=" + encodeURIComponent(semesterCode);
  264. try {
  265. var response = await fetch(url, {
  266. method: "POST",
  267. headers: {
  268. "accept": "application/json, text/javascript, */*; q=0.01",
  269. "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
  270. "x-requested-with": "XMLHttpRequest"
  271. },
  272. body: requestBody,
  273. credentials: "include"
  274. });
  275. if (!response.ok) {
  276. console.warn("JS: 开学日期接口请求失败:HTTP " + response.status);
  277. return null;
  278. }
  279. var responseText = await response.text();
  280. var data;
  281. try {
  282. data = JSON.parse(responseText);
  283. } catch (error) {
  284. console.warn("JS: 开学日期接口未返回 JSON,可能登录已过期。", error);
  285. return null;
  286. }
  287. var startDate = findSemesterStartDate(data);
  288. if (!startDate) console.warn("JS: 校历响应中未找到第 1 周开学日期。");
  289. else console.log("JS: 获取到开学日期:" + startDate);
  290. return startDate;
  291. } catch (error) {
  292. console.warn("JS: 获取学期开学日期失败:", error);
  293. return null;
  294. }
  295. }
  296. /**
  297. * 请求和解析课程数据。
  298. */
  299. async function fetchAndParseCourses(academicYear, semesterCode) {
  300. window.shiguangBridge.showToast("正在获取课表数据...");
  301. var requestBody = "xnm=" + encodeURIComponent(academicYear) +
  302. "&xqm=" + encodeURIComponent(semesterCode) +
  303. "&kzlx=ck&xsdm=&kclbdm=&kclxdm=";
  304. var url = "https://jxgl.qut.edu.cn/jwglxt/kbcx/xskbcx_cxXsgrkb.html?gnmkdm=N253508";
  305. console.log("JS: 发送请求到 " + url + ", body: " + requestBody);
  306. var requestOptions = {
  307. "headers": {
  308. "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
  309. "X-Requested-With": "XMLHttpRequest",
  310. },
  311. "body": requestBody,
  312. "method": "POST",
  313. "credentials": "include"
  314. };
  315. try {
  316. var requests = await Promise.all([fetch(url, requestOptions), fetchSemesterStartDate(academicYear, semesterCode)]);
  317. var response = requests[0];
  318. var semesterStartDate = requests[1];
  319. var jsonText = await response.text();
  320. var responseType = response.headers && response.headers.get
  321. ? response.headers.get("content-type") || "未知"
  322. : "未知";
  323. var responsePreview = jsonText.replace(/\s+/g, " ").trim().slice(0, 200) || "<空响应>";
  324. if (!response.ok) {
  325. var sessionHint = response.status === 901
  326. ? ";QUT 教务系统通常用 901 表示登录会话无效或已过期,请重新登录后停留在教务系统页面再测试"
  327. : "";
  328. throw new Error(
  329. "QUT 课表接口请求失败:HTTP " + response.status +
  330. (response.statusText ? " " + response.statusText : "") +
  331. sessionHint +
  332. ";响应类型=" + responseType +
  333. ";最终地址=" + (response.url || url) +
  334. ";响应摘要=" + responsePreview
  335. );
  336. }
  337. if (jsonText.indexOf("登录") !== -1 && jsonText.indexOf("密码") !== -1) {
  338. throw new Error(
  339. "QUT 课表接口返回了登录页面,当前登录会话已失效。" +
  340. "请重新登录教务系统后再导入;最终地址=" + (response.url || url) +
  341. ";响应类型=" + responseType
  342. );
  343. }
  344. var jsonData;
  345. try {
  346. jsonData = JSON.parse(jsonText);
  347. } catch (e) {
  348. throw new Error(
  349. "QUT 课表接口未返回有效 JSON:" + e.message +
  350. ";响应类型=" + responseType +
  351. ";最终地址=" + (response.url || url) +
  352. ";响应摘要=" + responsePreview
  353. );
  354. }
  355. var courses = parseJsonData(jsonData);
  356. if (courses.length === 0) {
  357. window.shiguangBridge.showToast("未找到任何课程数据,请检查所选学年学期是否正确或本学期无课。");
  358. return null;
  359. }
  360. console.log("JS: 课程数据解析成功,共找到 " + courses.length + " 门课程。");
  361. var config = buildCourseConfig(courses);
  362. config.semesterStartDate = semesterStartDate;
  363. return { courses: courses, config: config };
  364. } catch (error) {
  365. window.shiguangBridge.showToast("请求或解析失败: " + error.message);
  366. console.error('JS: Fetch/Parse Error:', error);
  367. return null;
  368. }
  369. }
  370. async function saveCourses(parsedCourses) {
  371. window.shiguangBridge.showToast("正在保存 " + parsedCourses.length + " 门课程...");
  372. console.log("JS: 尝试保存 " + parsedCourses.length + " 门课程...");
  373. try {
  374. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  375. console.log("JS: 课程保存成功!");
  376. return true;
  377. } catch (error) {
  378. window.shiguangBridge.showToast("课程保存失败: " + error.message);
  379. console.error('JS: Save Courses Error:', error);
  380. return false;
  381. }
  382. }
  383. // 青岛理工大学统一作息时间
  384. var TimeSlots = [
  385. { number: 1, startTime: "08:00", endTime: "08:45" },
  386. { number: 2, startTime: "08:50", endTime: "09:35" },
  387. { number: 3, startTime: "09:55", endTime: "10:40" },
  388. { number: 4, startTime: "10:45", endTime: "11:30" },
  389. { number: 5, startTime: "11:35", endTime: "12:20" },
  390. { number: 6, startTime: "14:00", endTime: "14:45" },
  391. { number: 7, startTime: "14:50", endTime: "15:35" },
  392. { number: 8, startTime: "15:55", endTime: "16:40" },
  393. { number: 9, startTime: "16:45", endTime: "17:30" },
  394. { number: 10, startTime: "19:00", endTime: "19:45" }
  395. ];
  396. async function importPresetTimeSlots(timeSlots) {
  397. console.log("JS: 准备导入 " + timeSlots.length + " 个预设时间段。");
  398. if (timeSlots.length > 0) {
  399. window.shiguangBridge.showToast("正在导入 " + timeSlots.length + " 个预设时间段...");
  400. try {
  401. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  402. window.shiguangBridge.showToast("预设时间段导入成功!");
  403. console.log("JS: 预设时间段导入成功。");
  404. } catch (error) {
  405. window.shiguangBridge.showToast("导入时间段失败: " + error.message);
  406. console.error('JS: Save Time Slots Error:', error);
  407. }
  408. } else {
  409. window.shiguangBridge.showToast("警告:时间段为空,未导入时间段信息。");
  410. console.warn("JS: 警告:传入时间段为空,未导入时间段信息。");
  411. }
  412. }
  413. async function runImportFlow() {
  414. try {
  415. if (isLoginPage()) {
  416. throw new Error("当前是登录页面,请先登录青岛理工大学教务系统");
  417. }
  418. window.shiguangBridge.showToast("拾光课程表 - 青岛理工大学适配");
  419. var alertConfirmed = await promptUserToStart();
  420. if (!alertConfirmed) {
  421. window.shiguangBridge.showToast("用户取消了导入。");
  422. console.log("JS: 用户取消了导入流程。");
  423. return;
  424. }
  425. var term = await resolveTerm();
  426. console.log("JS: 已选择学年学期: " + term.academicYear + ", " + term.semesterCode);
  427. var result = await fetchAndParseCourses(term.academicYear, term.semesterCode);
  428. if (result === null) {
  429. console.log("JS: 课程获取或解析失败,流程终止。");
  430. return;
  431. }
  432. var courses = result.courses;
  433. var config = result.config;
  434. var saveResult = await saveCourses(courses);
  435. if (!saveResult) {
  436. console.log("JS: 课程保存失败,流程终止。");
  437. return;
  438. }
  439. try {
  440. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  441. window.shiguangBridge.showToast("课表配置更新成功!总周数:" + config.semesterTotalWeeks + "周。");
  442. } catch (error) {
  443. window.shiguangBridge.showToast("课表配置保存失败: " + error.message);
  444. console.error('JS: Save Config Error:', error);
  445. }
  446. await importPresetTimeSlots(TimeSlots);
  447. window.shiguangBridge.showToast("成功导入 " + courses.length + " 门课程!");
  448. console.log("JS: 整个导入流程执行完毕并成功。");
  449. window.shiguangBridge.notifyTaskCompletion();
  450. } catch (error) {
  451. window.shiguangBridge.showToast("导入失败: " + error.message);
  452. console.error("JS: 导入流程失败:", error);
  453. }
  454. }
  455. runImportFlow();