syist_01.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509
  1. // 沈阳科技学院 (syist.edu.cn) 拾光课程表适配脚本
  2. // 非该大学开发者适配,开发者无法及时发现问题
  3. // 出现问题请提联系开发者或者提交pr更改,这更加快速
  4. // 备用默认作息时间表
  5. const DEFAULT_TIME_SLOTS = [
  6. { "number": 1, "startTime": "08:20", "endTime": "09:05" },
  7. { "number": 2, "startTime": "09:10", "endTime": "09:55" },
  8. { "number": 3, "startTime": "10:10", "endTime": "10:55" },
  9. { "number": 4, "startTime": "11:00", "endTime": "11:45" },
  10. { "number": 5, "startTime": "13:30", "endTime": "14:15" },
  11. { "number": 6, "startTime": "14:20", "endTime": "15:05" },
  12. { "number": 7, "startTime": "15:15", "endTime": "16:00" },
  13. { "number": 8, "startTime": "16:05", "endTime": "16:50" },
  14. { "number": 9, "startTime": "17:20", "endTime": "18:05" },
  15. { "number": 10, "startTime": "18:15", "endTime": "19:00" },
  16. { "number": 11, "startTime": "19:10", "endTime": "19:55" },
  17. { "number": 12, "startTime": "20:05", "endTime": "20:50" }
  18. ];
  19. /**
  20. * 辅助函数:解析周次字符串 "111000..." 为数字数组 [1, 2, 3]
  21. */
  22. function parseWeeksFromSkzc(skzc) {
  23. const weeks = [];
  24. const rawSkzc = skzc || '';
  25. for (let i = 0; i < rawSkzc.length; i++) {
  26. if (rawSkzc[i] === '1') {
  27. weeks.push(Number(i + 1));
  28. }
  29. }
  30. return weeks;
  31. }
  32. /**
  33. * 清除辅助私有属性并过滤掉无效课程
  34. */
  35. function cleanCourses(courses) {
  36. return courses.map(c => {
  37. const { _kbId, _day, _startSection, _endSection, ...cleanCourse } = c;
  38. return cleanCourse;
  39. }).filter(c => c.weeks && c.weeks.length > 0);
  40. }
  41. /**
  42. * 节次合并、周次合并与去重函数
  43. */
  44. function mergeAndDistinctCourses(courses) {
  45. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  46. // 1. 深拷贝并规范周次数据,过滤无效项
  47. const list = courses.map(c => ({
  48. ...c,
  49. name: c.name || '',
  50. teacher: c.teacher || '',
  51. position: c.position || '',
  52. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  53. }));
  54. // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
  55. list.sort((a, b) => {
  56. return a.name.localeCompare(b.name) ||
  57. a.teacher.localeCompare(b.teacher) ||
  58. a.position.localeCompare(b.position) ||
  59. (a.day || 0) - (b.day || 0) ||
  60. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  61. (a.startSection || 0) - (b.startSection || 0);
  62. });
  63. const step1Merged = [];
  64. let current = list[0];
  65. for (let i = 1; i < list.length; i++) {
  66. const next = list[i];
  67. const isSameCourseAndWeeks =
  68. current.name === next.name &&
  69. current.teacher === next.teacher &&
  70. current.position === next.position &&
  71. current.day === next.day &&
  72. current.weeks.join(',') === next.weeks.join(',');
  73. const isContinuous = current.endSection + 1 === next.startSection;
  74. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  75. if (isSameCourseAndWeeks && isContinuous) {
  76. // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
  77. current.endSection = next.endSection;
  78. } else if (isSameCourseAndWeeks && isDuplicate) {
  79. // 完全重复:跳过
  80. continue;
  81. } else {
  82. step1Merged.push(current);
  83. current = next;
  84. }
  85. }
  86. step1Merged.push(current);
  87. // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
  88. step1Merged.sort((a, b) => {
  89. return a.name.localeCompare(b.name) ||
  90. a.teacher.localeCompare(b.teacher) ||
  91. a.position.localeCompare(b.position) ||
  92. (a.day || 0) - (b.day || 0) ||
  93. (a.startSection || 0) - (b.startSection || 0) ||
  94. (a.endSection || 0) - (b.endSection || 0);
  95. });
  96. const step2Merged = [];
  97. let cur = step1Merged[0];
  98. for (let i = 1; i < step1Merged.length; i++) {
  99. const nxt = step1Merged[i];
  100. const isSameCourseAndSection =
  101. cur.name === nxt.name &&
  102. cur.teacher === nxt.teacher &&
  103. cur.position === nxt.position &&
  104. cur.day === nxt.day &&
  105. cur.startSection === nxt.startSection &&
  106. cur.endSection === nxt.endSection;
  107. if (isSameCourseAndSection) {
  108. // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周)
  109. cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
  110. } else {
  111. step2Merged.push(cur);
  112. cur = nxt;
  113. }
  114. }
  115. step2Merged.push(cur);
  116. return step2Merged;
  117. }
  118. /**
  119. * 将教务系统的课程数据转换成 CourseJsonModel 结构
  120. */
  121. function parseSingleCourse(rawCourse) {
  122. const courseName = rawCourse.KCM;
  123. const teacherName = rawCourse.SKJS ? rawCourse.SKJS.split('/')[0] : '';
  124. const position = rawCourse.JASMC;
  125. const day = rawCourse.SKXQ;
  126. const startSection = rawCourse.KSJC;
  127. const endSection = rawCourse.JSJC;
  128. const weeks = parseWeeksFromSkzc(rawCourse.SKZC);
  129. if (!courseName || !day || !startSection || !endSection || weeks.length === 0) {
  130. return null;
  131. }
  132. const course = {
  133. "name": courseName,
  134. "teacher": teacherName,
  135. "position": position || '待定',
  136. "day": parseInt(day),
  137. "startSection": parseInt(startSection),
  138. "endSection": parseInt(endSection),
  139. "weeks": weeks
  140. };
  141. // 用于匹配调课信息的私有辅助属性
  142. course._kbId = rawCourse.KBID;
  143. course._day = course.day;
  144. course._startSection = course.startSection;
  145. course._endSection = course.endSection;
  146. return course;
  147. }
  148. /**
  149. * 将调课数据应用到已解析的课程列表上
  150. */
  151. function applyCourseChanges(parsedCourses, rawChanges) {
  152. let successCount = 0;
  153. for (const change of rawChanges) {
  154. const kbID = change.KBID;
  155. const originalTeacher = change.YSKJS ? change.YSKJS.split('/')[0] : '';
  156. const weeksToRemove = parseWeeksFromSkzc(change.SKZC);
  157. let changeApplied = false;
  158. const affectedOriginalCourses = parsedCourses.filter(c =>
  159. c._kbId === kbID &&
  160. c._day === parseInt(change.SKXQ) &&
  161. c._startSection === parseInt(change.KSJC) &&
  162. c._endSection === parseInt(change.JSJC)
  163. );
  164. if (affectedOriginalCourses.length === 0) {
  165. continue;
  166. }
  167. if (weeksToRemove.length > 0) {
  168. affectedOriginalCourses.forEach(originalCourse => {
  169. const beforeLength = originalCourse.weeks.length;
  170. originalCourse.weeks = originalCourse.weeks.filter(w => !weeksToRemove.includes(w));
  171. if (originalCourse.weeks.length < beforeLength) {
  172. changeApplied = true;
  173. }
  174. });
  175. }
  176. const isTimeLocationChange = (change.TKLXDM === '01' || change.TKLXDM === '03');
  177. if (isTimeLocationChange && change.XSKZC && change.XSKXQ && change.XKSJC && change.XJSJC) {
  178. const newWeeks = parseWeeksFromSkzc(change.XSKZC);
  179. if (newWeeks.length > 0) {
  180. const newCourse = {
  181. "name": change.KCM,
  182. "teacher": change.XSKJS ? change.XSKJS.split('/')[0] : originalTeacher,
  183. "position": change.XJASMC || change.JASMC || '待定',
  184. "day": parseInt(change.XSKXQ),
  185. "startSection": parseInt(change.XKSJC),
  186. "endSection": parseInt(change.XJSJC),
  187. "weeks": newWeeks,
  188. "_kbId": kbID,
  189. "_day": parseInt(change.XSKXQ),
  190. "_startSection": parseInt(change.XKSJC),
  191. "_endSection": parseInt(change.XJSJC)
  192. };
  193. parsedCourses.push(newCourse);
  194. changeApplied = true;
  195. }
  196. }
  197. if (changeApplied) {
  198. successCount++;
  199. }
  200. }
  201. if (successCount > 0) {
  202. window.shiguangBridge.showToast(`已应用 ${successCount} 条调课/停课变更。`);
  203. }
  204. return parsedCourses;
  205. }
  206. /**
  207. * 前置提示弹窗
  208. */
  209. async function promptUserToStart() {
  210. const confirmed = await window.shiguangBridgePromise.showAlert(
  211. "注意",
  212. "导入前请确保您已在浏览器中成功登录教务系统,且当前页面显示课表系统,否则无法获取数据。",
  213. "好的,开始导入"
  214. );
  215. if (!confirmed) {
  216. window.shiguangBridge.showToast("用户取消了导入。");
  217. return null;
  218. }
  219. return true;
  220. }
  221. /**
  222. * 动态获取并选择学期
  223. */
  224. async function selectSemester() {
  225. const headers = {
  226. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  227. "x-requested-with": "XMLHttpRequest"
  228. };
  229. let semesterList = [];
  230. try {
  231. const response = await fetch("http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/jshkcb/xnxqcx.do", {
  232. headers,
  233. body: "*order=-DM",
  234. method: "POST",
  235. credentials: "include"
  236. });
  237. const resData = await response.json();
  238. semesterList = resData?.datas?.xnxqcx?.rows || [];
  239. } catch (e) {
  240. window.shiguangBridge.showToast("获取学期列表失败,请检查登录状态。");
  241. return null;
  242. }
  243. if (semesterList.length === 0) {
  244. window.shiguangBridge.showToast("未查询到学期数据。");
  245. return null;
  246. }
  247. const topSemesters = semesterList.slice(0, 10);
  248. const displayNames = topSemesters.map(item => item.MC || item.DM);
  249. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  250. "请选择学期",
  251. JSON.stringify(displayNames),
  252. -1
  253. );
  254. if (selectedIndex === null || selectedIndex === -1) {
  255. return null;
  256. }
  257. return topSemesters[selectedIndex];
  258. }
  259. /**
  260. * 获取开学日期与总周数配置
  261. */
  262. async function fetchSemesterConfig(xn, xq) {
  263. const headers = {
  264. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  265. "x-requested-with": "XMLHttpRequest"
  266. };
  267. try {
  268. const response = await fetch("http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do", {
  269. headers,
  270. body: `XN=${xn}&XQ=${xq}`,
  271. method: "POST",
  272. credentials: "include"
  273. });
  274. const resData = await response.json();
  275. const row = resData?.datas?.cxjcs?.rows?.[0];
  276. if (row) {
  277. const rawDate = row.XQKSRQ;
  278. const startDate = rawDate ? rawDate.split(' ')[0] : null;
  279. const totalWeeks = parseInt(row.ZZC) || 20;
  280. return {
  281. semesterStartDate: startDate,
  282. semesterTotalWeeks: totalWeeks
  283. };
  284. }
  285. } catch (e) {
  286. console.error("Fetch Config Error:", e);
  287. }
  288. return {
  289. semesterTotalWeeks: 20
  290. };
  291. }
  292. /**
  293. * 获取并保存节次作息时间段(含备用逻辑)
  294. */
  295. async function importPresetTimeSlots() {
  296. window.shiguangBridge.showToast("正在获取作息时间...");
  297. const headers = {
  298. "accept": "application/json, text/javascript, */*; q=0.01",
  299. "x-requested-with": "XMLHttpRequest"
  300. };
  301. let presetTimeSlots = null;
  302. try {
  303. const response = await fetch("http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/jshkcb/jc.do", {
  304. headers,
  305. method: "POST",
  306. credentials: "include"
  307. });
  308. const resData = await response.json();
  309. const rows = resData?.datas?.jc?.rows || [];
  310. if (rows.length > 0) {
  311. presetTimeSlots = rows.map(item => ({
  312. number: parseInt(item.DM),
  313. startTime: item.KSSJ,
  314. endTime: item.JSSJ
  315. }));
  316. }
  317. } catch (error) {
  318. console.warn("拉取作息时间失败,将使用备用作息时间表:", error);
  319. }
  320. if (!presetTimeSlots || presetTimeSlots.length === 0) {
  321. window.shiguangBridge.showToast("未能获取线上作息时间,已启用备用作息表。");
  322. presetTimeSlots = DEFAULT_TIME_SLOTS;
  323. }
  324. try {
  325. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  326. window.shiguangBridge.showToast("预设时间段导入成功!");
  327. return true;
  328. } catch (error) {
  329. window.shiguangBridge.showToast("保存时间段失败: " + error.message);
  330. return false;
  331. }
  332. }
  333. /**
  334. * 获取并解析课程数据
  335. */
  336. async function fetchAndParseCourses(semesterObj) {
  337. const XNXQDM = semesterObj.DM;
  338. const headers = {
  339. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  340. "x-requested-with": "XMLHttpRequest"
  341. };
  342. const courseUrl = "http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do";
  343. const courseBody = `XNXQDM=${XNXQDM}`;
  344. let rawCourseData;
  345. try {
  346. const response = await fetch(courseUrl, { headers, body: courseBody, method: "POST", credentials: "include" });
  347. rawCourseData = await response.json();
  348. } catch (e) {
  349. window.shiguangBridge.showToast("请求课表 API 失败,请检查网络或登录状态。");
  350. console.error("Fetch Course Error:", e);
  351. return null;
  352. }
  353. const rawCourses = rawCourseData?.datas?.cxxszhxqkb?.rows || [];
  354. if (rawCourses.length === 0) {
  355. window.shiguangBridge.showToast("该学期未查询到您的课程数据。");
  356. return null;
  357. }
  358. let parsedCourses = rawCourses.map(c => parseSingleCourse(c)).filter(c => c !== null);
  359. const changeUrl = "http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/xskcb/xsdkkc.do";
  360. const changeBody = `XNXQDM=${XNXQDM}&*order=-SQSJ`;
  361. let rawChangeData;
  362. try {
  363. const response = await fetch(changeUrl, { headers, body: changeBody, method: "POST", credentials: "include" });
  364. rawChangeData = await response.json();
  365. } catch (e) {
  366. window.shiguangBridge.showToast("请求调课 API 失败,将使用原始课表。");
  367. console.error("Fetch Change Error:", e);
  368. }
  369. const rawChanges = rawChangeData?.datas?.xsdkkc?.rows || [];
  370. if (rawChanges.length > 0) {
  371. parsedCourses = applyCourseChanges(parsedCourses, rawChanges);
  372. }
  373. // 1. 清理临时字段
  374. const cleanList = cleanCourses(parsedCourses);
  375. // 2. 执行两阶段合并(节次合并 + 周次合并)
  376. const finalCourses = mergeAndDistinctCourses(cleanList);
  377. const courseConfig = await fetchSemesterConfig(semesterObj.XNDM, semesterObj.XQDM);
  378. return {
  379. courses: finalCourses,
  380. config: courseConfig
  381. };
  382. }
  383. /**
  384. * 保存课程数据
  385. */
  386. async function saveCourses(parsedCourses) {
  387. if (!parsedCourses || parsedCourses.length === 0) {
  388. window.shiguangBridge.showToast("没有有效的课程数据可供保存。");
  389. return false;
  390. }
  391. try {
  392. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  393. window.shiguangBridge.showToast(`成功导入 ${parsedCourses.length} 门课程!`);
  394. return true;
  395. } catch (error) {
  396. window.shiguangBridge.showToast(`保存课程数据失败: ${error.message}`);
  397. return false;
  398. }
  399. }
  400. /**
  401. * 保存配置数据
  402. */
  403. async function saveConfig(configData) {
  404. try {
  405. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(configData));
  406. window.shiguangBridge.showToast("课表配置更新成功!");
  407. return true;
  408. } catch (error) {
  409. window.shiguangBridge.showToast("保存配置失败: " + error.message);
  410. return false;
  411. }
  412. }
  413. /**
  414. * 主流程控制
  415. */
  416. async function runImportFlow() {
  417. window.shiguangBridge.showToast("课程导入流程启动...");
  418. const alertConfirmed = await promptUserToStart();
  419. if (!alertConfirmed) return;
  420. const selectedSemester = await selectSemester();
  421. if (!selectedSemester) {
  422. window.shiguangBridge.showToast("导入已取消。");
  423. return;
  424. }
  425. await importPresetTimeSlots();
  426. const courseData = await fetchAndParseCourses(selectedSemester);
  427. if (!courseData) return;
  428. const configSaveResult = await saveConfig(courseData.config);
  429. if (!configSaveResult) return;
  430. const saveResult = await saveCourses(courseData.courses);
  431. if (!saveResult) return;
  432. window.shiguangBridge.showToast("所有任务已完成!课表导入成功。");
  433. window.shiguangBridge.notifyTaskCompletion();
  434. }
  435. // 启动导入流程
  436. runImportFlow();