syist_01.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  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. // 判断是否为实验课程(存在 SYXMMC 字段)
  123. const isExperiment = rawCourse.SYXMMC !== undefined && rawCourse.SYXMMC !== null;
  124. // 课程名称:实验课程使用 "KCM - SYXMMC" 格式
  125. const courseName = isExperiment
  126. ? `${rawCourse.KCM} - ${rawCourse.SYXMMC}`
  127. : rawCourse.KCM;
  128. const teacherName = rawCourse.SKJS ? rawCourse.SKJS.split('/')[0] : (rawCourse.JSM || '');
  129. const position = rawCourse.JASMC;
  130. const day = rawCourse.SKXQ;
  131. const startSection = rawCourse.KSJC;
  132. const endSection = rawCourse.JSJC;
  133. const weeks = parseWeeksFromSkzc(rawCourse.SKZC);
  134. if (!courseName || !day || !startSection || !endSection || weeks.length === 0) {
  135. return null;
  136. }
  137. const course = {
  138. "name": courseName,
  139. "teacher": teacherName,
  140. "position": position || '待定',
  141. "day": parseInt(day),
  142. "startSection": parseInt(startSection),
  143. "endSection": parseInt(endSection),
  144. "weeks": weeks
  145. };
  146. // 用于匹配调课信息的私有辅助属性
  147. course._kbId = rawCourse.KBID;
  148. course._day = course.day;
  149. course._startSection = course.startSection;
  150. course._endSection = course.endSection;
  151. return course;
  152. }
  153. /**
  154. * 将调课数据应用到已解析的课程列表上
  155. */
  156. function applyCourseChanges(parsedCourses, rawChanges) {
  157. let successCount = 0;
  158. for (const change of rawChanges) {
  159. const kbID = change.KBID;
  160. const originalTeacher = change.YSKJS ? change.YSKJS.split('/')[0] : '';
  161. const weeksToRemove = parseWeeksFromSkzc(change.SKZC);
  162. let changeApplied = false;
  163. const affectedOriginalCourses = parsedCourses.filter(c =>
  164. c._kbId === kbID &&
  165. c._day === parseInt(change.SKXQ) &&
  166. c._startSection === parseInt(change.KSJC) &&
  167. c._endSection === parseInt(change.JSJC)
  168. );
  169. if (affectedOriginalCourses.length === 0) {
  170. continue;
  171. }
  172. if (weeksToRemove.length > 0) {
  173. affectedOriginalCourses.forEach(originalCourse => {
  174. const beforeLength = originalCourse.weeks.length;
  175. originalCourse.weeks = originalCourse.weeks.filter(w => !weeksToRemove.includes(w));
  176. if (originalCourse.weeks.length < beforeLength) {
  177. changeApplied = true;
  178. }
  179. });
  180. }
  181. const isTimeLocationChange = (change.TKLXDM === '01' || change.TKLXDM === '03');
  182. if (isTimeLocationChange && change.XSKZC && change.XSKXQ && change.XKSJC && change.XJSJC) {
  183. const newWeeks = parseWeeksFromSkzc(change.XSKZC);
  184. if (newWeeks.length > 0) {
  185. const newCourse = {
  186. "name": change.KCM,
  187. "teacher": change.XSKJS ? change.XSKJS.split('/')[0] : originalTeacher,
  188. "position": change.XJASMC || change.JASMC || '待定',
  189. "day": parseInt(change.XSKXQ),
  190. "startSection": parseInt(change.XKSJC),
  191. "endSection": parseInt(change.XJSJC),
  192. "weeks": newWeeks,
  193. "_kbId": kbID,
  194. "_day": parseInt(change.XSKXQ),
  195. "_startSection": parseInt(change.XKSJC),
  196. "_endSection": parseInt(change.XJSJC)
  197. };
  198. parsedCourses.push(newCourse);
  199. changeApplied = true;
  200. }
  201. }
  202. if (changeApplied) {
  203. successCount++;
  204. }
  205. }
  206. if (successCount > 0) {
  207. window.shiguangBridge.showToast(`已应用 ${successCount} 条调课/停课变更。`);
  208. }
  209. return parsedCourses;
  210. }
  211. /**
  212. * 前置提示弹窗
  213. */
  214. async function promptUserToStart() {
  215. const confirmed = await window.shiguangBridgePromise.showAlert(
  216. "注意",
  217. "导入前请确保您已在浏览器中成功登录教务系统,且当前页面显示课表系统,否则无法获取数据。",
  218. "好的,开始导入"
  219. );
  220. if (!confirmed) {
  221. window.shiguangBridge.showToast("用户取消了导入。");
  222. return null;
  223. }
  224. return true;
  225. }
  226. /**
  227. * 动态获取并选择学期
  228. */
  229. async function selectSemester() {
  230. const headers = {
  231. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  232. "x-requested-with": "XMLHttpRequest"
  233. };
  234. // 获取学期列表
  235. let semesterList = [];
  236. try {
  237. const response = await fetch("http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/jshkcb/xnxqcx.do", {
  238. headers,
  239. body: "*order=-DM",
  240. method: "POST",
  241. credentials: "include"
  242. });
  243. const resData = await response.json();
  244. semesterList = resData?.datas?.xnxqcx?.rows || [];
  245. } catch (e) {
  246. window.shiguangBridge.showToast("获取学期列表失败,请检查登录状态。");
  247. return null;
  248. }
  249. if (semesterList.length === 0) {
  250. window.shiguangBridge.showToast("未查询到学期数据。");
  251. return null;
  252. }
  253. // 获取当前学期作为默认值
  254. let defaultDM = null;
  255. try {
  256. const dqResponse = await fetch("http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/jshkcb/dqxnxq.do", {
  257. headers,
  258. method: "POST",
  259. credentials: "include"
  260. });
  261. const dqData = await dqResponse.json();
  262. const currentSemester = dqData?.datas?.dqxnxq?.rows?.[0];
  263. if (currentSemester) {
  264. defaultDM = currentSemester.DM;
  265. }
  266. } catch (e) {
  267. console.warn("获取当前学期失败,将不使用默认值:", e);
  268. }
  269. const topSemesters = semesterList.slice(0, 10);
  270. const displayNames = topSemesters.map(item => item.MC || item.DM);
  271. // 查找默认学期的索引
  272. let defaultIndex = -1;
  273. if (defaultDM) {
  274. defaultIndex = topSemesters.findIndex(item => item.DM === defaultDM);
  275. }
  276. const selectedIndex = await window.shiguangBridgePromise.showSingleSelection(
  277. "请选择学期",
  278. JSON.stringify(displayNames),
  279. defaultIndex
  280. );
  281. if (selectedIndex === null || selectedIndex === -1) {
  282. return null;
  283. }
  284. return topSemesters[selectedIndex];
  285. }
  286. /**
  287. * 获取开学日期与总周数配置
  288. */
  289. async function fetchSemesterConfig(xn, xq) {
  290. const headers = {
  291. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  292. "x-requested-with": "XMLHttpRequest"
  293. };
  294. try {
  295. const response = await fetch("http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/jshkcb/cxjcs.do", {
  296. headers,
  297. body: `XN=${xn}&XQ=${xq}`,
  298. method: "POST",
  299. credentials: "include"
  300. });
  301. const resData = await response.json();
  302. const row = resData?.datas?.cxjcs?.rows?.[0];
  303. if (row) {
  304. const rawDate = row.XQKSRQ;
  305. const startDate = rawDate ? rawDate.split(' ')[0] : null;
  306. const totalWeeks = parseInt(row.ZZC) || 20;
  307. return {
  308. semesterStartDate: startDate,
  309. semesterTotalWeeks: totalWeeks
  310. };
  311. }
  312. } catch (e) {
  313. console.error("Fetch Config Error:", e);
  314. }
  315. return {
  316. semesterTotalWeeks: 20
  317. };
  318. }
  319. /**
  320. * 获取并保存节次作息时间段(含备用逻辑)
  321. */
  322. async function importPresetTimeSlots() {
  323. window.shiguangBridge.showToast("正在获取作息时间...");
  324. const headers = {
  325. "accept": "application/json, text/javascript, */*; q=0.01",
  326. "x-requested-with": "XMLHttpRequest"
  327. };
  328. let presetTimeSlots = null;
  329. try {
  330. const response = await fetch("http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/jshkcb/jc.do", {
  331. headers,
  332. method: "POST",
  333. credentials: "include"
  334. });
  335. const resData = await response.json();
  336. const rows = resData?.datas?.jc?.rows || [];
  337. if (rows.length > 0) {
  338. presetTimeSlots = rows.map(item => ({
  339. number: parseInt(item.DM),
  340. startTime: item.KSSJ,
  341. endTime: item.JSSJ
  342. }));
  343. }
  344. } catch (error) {
  345. console.warn("拉取作息时间失败,将使用备用作息时间表:", error);
  346. }
  347. if (!presetTimeSlots || presetTimeSlots.length === 0) {
  348. window.shiguangBridge.showToast("未能获取线上作息时间,已启用备用作息表。");
  349. presetTimeSlots = DEFAULT_TIME_SLOTS;
  350. }
  351. try {
  352. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  353. window.shiguangBridge.showToast("预设时间段导入成功!");
  354. return true;
  355. } catch (error) {
  356. window.shiguangBridge.showToast("保存时间段失败: " + error.message);
  357. return false;
  358. }
  359. }
  360. /**
  361. * 获取并解析课程数据
  362. */
  363. async function fetchAndParseCourses(semesterObj) {
  364. const XNXQDM = semesterObj.DM;
  365. const headers = {
  366. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  367. "x-requested-with": "XMLHttpRequest"
  368. };
  369. // 获取理论课程
  370. const courseUrl = "http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/xskcb/cxxszhxqkb.do";
  371. const courseBody = `XNXQDM=${XNXQDM}`;
  372. let rawCourseData;
  373. try {
  374. const response = await fetch(courseUrl, { headers, body: courseBody, method: "POST", credentials: "include" });
  375. rawCourseData = await response.json();
  376. } catch (e) {
  377. window.shiguangBridge.showToast("请求课表 API 失败,请检查网络或登录状态。");
  378. console.error("Fetch Course Error:", e);
  379. return null;
  380. }
  381. const rawCourses = rawCourseData?.datas?.cxxszhxqkb?.rows || [];
  382. // 获取实验课程
  383. let rawExperimentCourses = [];
  384. try {
  385. const expUrl = "http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/syjxkcb/cxsyjxxskb.do";
  386. const expBody = `XNXQDM=${XNXQDM}`;
  387. const expResponse = await fetch(expUrl, { headers, body: expBody, method: "POST", credentials: "include" });
  388. const expData = await expResponse.json();
  389. rawExperimentCourses = expData?.datas?.cxsyjxxskb?.rows || [];
  390. } catch (e) {
  391. console.warn("获取实验课程失败,将仅使用理论课程:", e);
  392. }
  393. // 合并理论课程和实验课程
  394. const allRawCourses = [...rawCourses, ...rawExperimentCourses];
  395. if (allRawCourses.length === 0) {
  396. window.shiguangBridge.showToast("该学期未查询到您的课程数据。");
  397. return null;
  398. }
  399. // 解析所有课程
  400. let parsedCourses = allRawCourses.map(c => parseSingleCourse(c)).filter(c => c !== null);
  401. // 获取调课数据
  402. const changeUrl = "http://jwxt.syist.edu.cn:30334/jwapp/sys/wdkb/modules/xskcb/xsdkkc.do";
  403. const changeBody = `XNXQDM=${XNXQDM}&*order=-SQSJ`;
  404. let rawChangeData;
  405. try {
  406. const response = await fetch(changeUrl, { headers, body: changeBody, method: "POST", credentials: "include" });
  407. rawChangeData = await response.json();
  408. } catch (e) {
  409. window.shiguangBridge.showToast("请求调课 API 失败,将使用原始课表。");
  410. console.error("Fetch Change Error:", e);
  411. }
  412. const rawChanges = rawChangeData?.datas?.xsdkkc?.rows || [];
  413. if (rawChanges.length > 0) {
  414. parsedCourses = applyCourseChanges(parsedCourses, rawChanges);
  415. }
  416. // 清理临时字段
  417. const cleanList = cleanCourses(parsedCourses);
  418. // 执行两阶段合并(节次合并 + 周次合并)
  419. const finalCourses = mergeAndDistinctCourses(cleanList);
  420. const courseConfig = await fetchSemesterConfig(semesterObj.XNDM, semesterObj.XQDM);
  421. // 显示实验课程数量提示
  422. if (rawExperimentCourses.length > 0) {
  423. window.shiguangBridge.showToast(`共获取到 ${rawCourses.length} 门理论课程和 ${rawExperimentCourses.length} 门实验课程。`);
  424. }
  425. return {
  426. courses: finalCourses,
  427. config: courseConfig
  428. };
  429. }
  430. /**
  431. * 保存课程数据
  432. */
  433. async function saveCourses(parsedCourses) {
  434. if (!parsedCourses || parsedCourses.length === 0) {
  435. window.shiguangBridge.showToast("没有有效的课程数据可供保存。");
  436. return false;
  437. }
  438. try {
  439. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses));
  440. window.shiguangBridge.showToast(`成功导入 ${parsedCourses.length} 门课程!`);
  441. return true;
  442. } catch (error) {
  443. window.shiguangBridge.showToast(`保存课程数据失败: ${error.message}`);
  444. return false;
  445. }
  446. }
  447. /**
  448. * 保存配置数据
  449. */
  450. async function saveConfig(configData) {
  451. try {
  452. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(configData));
  453. window.shiguangBridge.showToast("课表配置更新成功!");
  454. return true;
  455. } catch (error) {
  456. window.shiguangBridge.showToast("保存配置失败: " + error.message);
  457. return false;
  458. }
  459. }
  460. /**
  461. * 主流程控制
  462. */
  463. async function runImportFlow() {
  464. window.shiguangBridge.showToast("课程导入流程启动...");
  465. const alertConfirmed = await promptUserToStart();
  466. if (!alertConfirmed) return;
  467. const selectedSemester = await selectSemester();
  468. if (!selectedSemester) {
  469. window.shiguangBridge.showToast("导入已取消。");
  470. return;
  471. }
  472. await importPresetTimeSlots();
  473. const courseData = await fetchAndParseCourses(selectedSemester);
  474. if (!courseData) return;
  475. const configSaveResult = await saveConfig(courseData.config);
  476. if (!configSaveResult) return;
  477. const saveResult = await saveCourses(courseData.courses);
  478. if (!saveResult) return;
  479. window.shiguangBridge.showToast("所有任务已完成!课表导入成功。");
  480. window.shiguangBridge.notifyTaskCompletion();
  481. }
  482. // 启动导入流程
  483. runImportFlow();