sxgcxy_01.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. // 山西工程职业学院(sxgcxy.cn)拾光课程表适配脚本
  2. // 基于超星教务系统接口适配
  3. // 非该大学开发者适配,开发者无法及时发现问题
  4. // 出现问题请提issues或者提交pr更改,这更加快速
  5. // 唐槐校区作息时间
  6. const TangHuaiTimeSlots = [
  7. { number: 1, startTime: "08:20", endTime: "09:05" },
  8. { number: 2, startTime: "09:05", endTime: "09:50" },
  9. { number: 3, startTime: "10:00", endTime: "10:45" },
  10. { number: 4, startTime: "10:45", endTime: "11:30" },
  11. { number: 5, startTime: "13:40", endTime: "14:25" },
  12. { number: 6, startTime: "14:25", endTime: "15:10" },
  13. { number: 7, startTime: "15:20", endTime: "16:05" },
  14. { number: 8, startTime: "16:05", endTime: "16:50" },
  15. { number: 9, startTime: "17:30", endTime: "18:15" },
  16. { number: 10, startTime: "18:15", endTime: "19:00" }
  17. ];
  18. // 龙潭校区作息时间
  19. const LongTanTimeSlots = [
  20. { number: 1, startTime: "08:00", endTime: "08:45" },
  21. { number: 2, startTime: "08:45", endTime: "09:30" },
  22. { number: 3, startTime: "10:00", endTime: "10:45" },
  23. { number: 4, startTime: "10:45", endTime: "11:30" },
  24. { number: 5, startTime: "14:30", endTime: "15:15" },
  25. { number: 6, startTime: "15:15", endTime: "16:00" },
  26. { number: 7, startTime: "16:30", endTime: "17:15" },
  27. { number: 8, startTime: "17:15", endTime: "18:00" },
  28. { number: 9, startTime: "19:00", endTime: "19:45" },
  29. { number: 10, startTime: "19:45", endTime: "20:30" }
  30. ];
  31. // 校区作息选项列表
  32. const CampusTimeTables = [
  33. { label: "唐槐校区", slots: TangHuaiTimeSlots },
  34. { label: "龙潭校区", slots: LongTanTimeSlots }
  35. ];
  36. /**
  37. * 从 HTML 字符串中提取纯文本内容。
  38. */
  39. function extractAnchorText(htmlStr) {
  40. if (!htmlStr) return '';
  41. // 移除 HTML 标签,返回剩余的文本内容
  42. const match = htmlStr.match(/>([^<]+)</);
  43. return match ? match[1].trim() : htmlStr.trim();
  44. }
  45. /**
  46. * 清理教师名称,去除括号及其内容。
  47. */
  48. function cleanTeacherName(name) {
  49. if (!name) return '';
  50. // 移除全角或半角的括号及其中的内容
  51. return name.replace(/([^)]*)/g, '').replace(/\([^)]*\)/g, '').trim();
  52. }
  53. /**
  54. * 解析周次字符串,超星系统直接提供逗号分隔的周次数字。
  55. */
  56. function parseWeeks(weekStr) {
  57. if (!weekStr) return [];
  58. return weekStr.split(',')
  59. .map(w => Number(w.trim()))
  60. .filter(w => !isNaN(w) && w > 0)
  61. .sort((a, b) => a - b);
  62. }
  63. /**
  64. * 解析 API 返回的 JSON 数据,并合并连续节次。
  65. */
  66. function parseJsonData(jsonData) {
  67. console.log("JS: parseJsonData 正在解析超星 JSON 数据...");
  68. if (!jsonData || !Array.isArray(jsonData.data)) {
  69. console.warn("JS: JSON 数据结构错误或缺少 data 字段。");
  70. return [];
  71. }
  72. const rawCourseList = jsonData.data;
  73. // 1. 预处理,并标准化周次信息
  74. const processedList = rawCourseList
  75. .map(rawCourse => {
  76. const name = extractAnchorText(rawCourse.kcmc);
  77. const teacher = cleanTeacherName(extractAnchorText(rawCourse.tmc));
  78. const position = extractAnchorText(rawCourse.croommc) || '待定';
  79. const day = Number(rawCourse.xingqi);
  80. const section = Number(rawCourse.djc);
  81. // 将原始周次字符串解析为排序后的数组,然后转换为 JSON 字符串
  82. const weeksArray = parseWeeks(rawCourse.zcstr);
  83. const standardizedWeeks = JSON.stringify(weeksArray);
  84. if (!name || isNaN(day) || isNaN(section) || day < 1 || day > 7 || section < 1 || weeksArray.length === 0) {
  85. return null;
  86. }
  87. return { name, teacher, position, day, section, standardizedWeeks, weeksArray };
  88. })
  89. .filter(c => c !== null)
  90. // 排序顺序:星期 > 周次 > 课程名 > 教师 > 教室 > 节次
  91. .sort((a, b) =>
  92. a.day - b.day ||
  93. a.standardizedWeeks.localeCompare(b.standardizedWeeks) ||
  94. a.name.localeCompare(b.name) ||
  95. a.teacher.localeCompare(b.teacher) ||
  96. a.position.localeCompare(b.position) ||
  97. a.section - b.section
  98. );
  99. const finalCourseList = [];
  100. let i = 0;
  101. // 2. 迭代合并连续节次
  102. while (i < processedList.length) {
  103. let current = processedList[i];
  104. let startSection = current.section;
  105. let endSection = current.section;
  106. let j = i + 1;
  107. while (j < processedList.length) {
  108. let next = processedList[j];
  109. // 检查合并条件:周次、星期、名称、教师、教室 必须相同,且节次连续
  110. if (
  111. next.day === current.day &&
  112. next.name === current.name &&
  113. next.teacher === current.teacher &&
  114. next.position === current.position &&
  115. next.standardizedWeeks === current.standardizedWeeks &&
  116. next.section === endSection + 1
  117. ) {
  118. endSection = next.section;
  119. j++;
  120. } else {
  121. break;
  122. }
  123. }
  124. // 3. 构建合并后的课程对象
  125. finalCourseList.push({
  126. name: current.name,
  127. teacher: current.teacher,
  128. position: current.position,
  129. day: current.day,
  130. startSection: startSection,
  131. endSection: endSection,
  132. weeks: current.weeksArray
  133. });
  134. i = j;
  135. }
  136. console.log(`JS: JSON 数据解析完成,共找到 ${finalCourseList.length} 门课程(合并后)。`);
  137. return finalCourseList;
  138. }
  139. /**
  140. * 提示用户选择校区作息时间。
  141. * @returns {Promise<Array<Object>|null>} 选中的作息时间数组或 null。
  142. */
  143. async function selectCampusTimeTable() {
  144. console.log("JS: 提示用户选择校区作息时间。");
  145. const labels = CampusTimeTables.map(opt => opt.label);
  146. const defaultIndex = -1;
  147. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  148. "请选择校区对应的作息时间",
  149. JSON.stringify(labels),
  150. defaultIndex
  151. );
  152. if (selectedIndex === null || selectedIndex === -1) {
  153. return null;
  154. }
  155. return CampusTimeTables[selectedIndex].slots;
  156. }
  157. /**
  158. * 生成超星教务系统所需的学年学期选项。
  159. */
  160. function getSemesterOptions() {
  161. const currentYear = new Date().getFullYear();
  162. const years = [currentYear - 1, currentYear, currentYear + 1];
  163. const semesterCodes = ["1", "2"];
  164. let labels = [];
  165. let values = [];
  166. let defaultIndex = -1;
  167. for (let i = 0; i < years.length; i++) {
  168. const startYear = years[i];
  169. const endYear = startYear + 1;
  170. const yearStr = `${startYear}-${endYear}`;
  171. for (let j = 0; j < semesterCodes.length; j++) {
  172. const code = semesterCodes[j];
  173. const apiValue = `${yearStr}-${code}`;
  174. labels.push(apiValue);
  175. values.push(apiValue);
  176. }
  177. }
  178. return { labels, values, defaultIndex };
  179. }
  180. /**
  181. * 提示用户选择学年和学期。
  182. */
  183. async function selectAcademicYearAndSemester() {
  184. console.log("JS: 提示用户选择学年学期 (纯参数格式)。");
  185. const { labels, values, defaultIndex } = getSemesterOptions();
  186. const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
  187. "选择学年学期 (参数格式:YYYY-YYYY-X)",
  188. JSON.stringify(labels),
  189. defaultIndex
  190. );
  191. if (selectedIndex === null || selectedIndex === -1) {
  192. return null;
  193. }
  194. return values[selectedIndex];
  195. }
  196. /**
  197. * 检查是否在登录页面。
  198. */
  199. function isLoginPage() {
  200. const url = window.location.href;
  201. return url.includes('login') || url.includes('slogin');
  202. }
  203. async function promptUserToStart() {
  204. return await window.AndroidBridgePromise.showAlert(
  205. "教务系统课表导入 (超星)",
  206. "导入前请确保您已在浏览器中成功登录教务系统",
  207. "好的,开始导入"
  208. );
  209. }
  210. /**
  211. * 请求和解析课程数据
  212. */
  213. async function fetchAndParseCourses(xnxq) {
  214. AndroidBridge.showToast(`正在请求 ${xnxq} 的课表数据...`);
  215. // xhid 需要从页面获取
  216. const xhid = document.getElementById('encodeId')?.value || 'UNKNOWN';
  217. const dynamicUrl = `https://sxevc.jw.chaoxing.com/admin/pkgl/xskb/sdpkkbList?xnxq=${xnxq}&xhid=${xhid}`;
  218. console.log(`JS: 发送请求到 ${dynamicUrl}`);
  219. const requestOptions = {
  220. "headers": {
  221. "Accept": "application/json, text/plain, */*",
  222. "X-Requested-With": "XMLHttpRequest"
  223. },
  224. "method": "GET",
  225. "credentials": "include"
  226. };
  227. try {
  228. const response = await fetch(dynamicUrl, requestOptions);
  229. if (!response.ok) {
  230. throw new Error(`网络请求失败。状态码: ${response.status} (${response.statusText})`);
  231. }
  232. const jsonData = await response.json();
  233. if (jsonData.ret !== 0) {
  234. console.error(`JS: API 返回错误: ${jsonData.msg}`);
  235. AndroidBridge.showToast(`数据请求失败: ${jsonData.msg},请检查是否登录或参数是否正确。`);
  236. return null;
  237. }
  238. const courses = parseJsonData(jsonData);
  239. if (courses.length === 0) {
  240. AndroidBridge.showToast("未找到任何课程数据,请检查所选学年学期是否正确或本学期无课。");
  241. return null;
  242. }
  243. console.log(`JS: 课程数据解析成功,共找到 ${courses.length} 门课程。`);
  244. return { courses: courses };
  245. } catch (error) {
  246. AndroidBridge.showToast(`请求或解析失败: ${error.message}`);
  247. console.error('JS: Fetch/Parse Error:', error);
  248. return null;
  249. }
  250. }
  251. async function saveCourses(parsedCourses) {
  252. AndroidBridge.showToast(`正在保存 ${parsedCourses.length} 门课程...`);
  253. try {
  254. await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(parsedCourses, null, 2));
  255. return true;
  256. } catch (error) {
  257. AndroidBridge.showToast(`课程保存失败: ${error.message}`);
  258. return false;
  259. }
  260. }
  261. async function importPresetTimeSlots(timeSlots) {
  262. console.log(`JS: 导入预设时间段。`);
  263. if (timeSlots.length > 0) {
  264. AndroidBridge.showToast(`正在导入 ${timeSlots.length} 个预设时间段...`);
  265. try {
  266. await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  267. AndroidBridge.showToast("预设时间段导入成功!");
  268. } catch (error) {
  269. AndroidBridge.showToast("导入时间段失败: " + error.message);
  270. }
  271. } else {
  272. AndroidBridge.showToast("警告:时间段为空,未导入时间段信息。");
  273. }
  274. }
  275. async function runImportFlow() {
  276. if (isLoginPage()) {
  277. AndroidBridge.showToast("导入失败:请先登录教务系统!");
  278. return;
  279. }
  280. const alertConfirmed = await promptUserToStart();
  281. if (!alertConfirmed) {
  282. AndroidBridge.showToast("用户取消了导入。");
  283. return;
  284. }
  285. const selectedTimeSlots = await selectCampusTimeTable();
  286. if (selectedTimeSlots === null) {
  287. AndroidBridge.showToast("导入已取消,未选择作息时间。");
  288. return;
  289. }
  290. console.log(`JS: 已选择包含 ${selectedTimeSlots.length} 节课的作息时间。`);
  291. const xnxq = await selectAcademicYearAndSemester();
  292. if (xnxq === null) {
  293. AndroidBridge.showToast("导入已取消,未选择学年学期。");
  294. return;
  295. }
  296. console.log(`JS: 已选择学年学期参数: ${xnxq}`);
  297. const result = await fetchAndParseCourses(xnxq);
  298. if (result === null) {
  299. return;
  300. }
  301. const { courses } = result;
  302. const saveResult = await saveCourses(courses);
  303. if (!saveResult) {
  304. return;
  305. }
  306. await importPresetTimeSlots(selectedTimeSlots);
  307. AndroidBridge.showToast(`课程导入成功,共导入 ${courses.length} 门课程!`);
  308. AndroidBridge.notifyTaskCompletion();
  309. }
  310. runImportFlow();