aufe_01.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  1. // 安徽财经大学 (aufe.edu.cn) 拾光课程表适配脚本
  2. // 非该大学开发者适配,开发者无法及时发现问题
  3. // 出现问题请联系开发者或者提交PR更改,这更加快速
  4. // 配置
  5. // 可手动指定域名,留空则自动从当前页面获取
  6. const BASE_URL = ""; // 例如 "http://jwcxk2-aufe-edu-cn.vpn2.aufe.edu.cn:8118"
  7. // 工具函数
  8. // 自动获取当前域名
  9. function getBaseUrl() {
  10. if (BASE_URL) return BASE_URL;
  11. const url = new URL(window.location.href);
  12. return url.origin;
  13. }
  14. // 解析 classWeek 字符串 (支持不定长度)
  15. function parseWeekString(weekStr) {
  16. let weeks = [];
  17. if (!weekStr) return weeks;
  18. for (let i = 0; i < weekStr.length; i++) {
  19. if (weekStr[i] === '1') weeks.push(i + 1);
  20. }
  21. return weeks;
  22. }
  23. // 格式化时间 (0800 -> 08:00)
  24. function formatTime(timeStr) {
  25. if (timeStr && timeStr.length === 4) {
  26. return timeStr.substring(0, 2) + ":" + timeStr.substring(2);
  27. }
  28. return timeStr;
  29. }
  30. // 格式化日期 (20260831 -> 2026-08-31)
  31. function formatDate(dateStr) {
  32. if (dateStr && dateStr.length === 8) {
  33. return dateStr.substring(0, 4) + "-" + dateStr.substring(4, 6) + "-" + dateStr.substring(6, 8);
  34. }
  35. return dateStr;
  36. }
  37. /**
  38. * 节次与周次合并去重函数(供开发者参考)
  39. * @param {Array<Object>} courses 原始解析课程数组
  40. * @returns {Array<Object>} 合并去重后的课程数组
  41. */
  42. function mergeAndDistinctCourses(courses) {
  43. if (!Array.isArray(courses) || courses.length <= 1) return courses;
  44. // 1. 深拷贝并规范周次数据,过滤无效项
  45. const list = courses.map(c => ({
  46. ...c,
  47. name: c.name || '',
  48. teacher: c.teacher || '',
  49. position: c.position || '',
  50. weeks: Array.isArray(c.weeks) ? [...c.weeks].sort((a, b) => a - b) : []
  51. }));
  52. // 阶段 1:合并连续节次与完全重复记录(前提:名称、教师、地点、星期、周次一致)
  53. list.sort((a, b) => {
  54. return a.name.localeCompare(b.name) ||
  55. a.teacher.localeCompare(b.teacher) ||
  56. a.position.localeCompare(b.position) ||
  57. (a.day || 0) - (b.day || 0) ||
  58. a.weeks.join(',').localeCompare(b.weeks.join(',')) ||
  59. (a.startSection || 0) - (b.startSection || 0);
  60. });
  61. const step1Merged = [];
  62. let current = list[0];
  63. for (let i = 1; i < list.length; i++) {
  64. const next = list[i];
  65. const isSameCourseAndWeeks =
  66. current.name === next.name &&
  67. current.teacher === next.teacher &&
  68. current.position === next.position &&
  69. current.day === next.day &&
  70. current.weeks.join(',') === next.weeks.join(',');
  71. const isContinuous = current.endSection + 1 === next.startSection;
  72. const isDuplicate = current.startSection === next.startSection && current.endSection === next.endSection;
  73. if (isSameCourseAndWeeks && isContinuous) {
  74. // 节次连续:延长结束节次 (如 1-2 节 + 3-4 节 -> 1-4 节)
  75. current.endSection = next.endSection;
  76. } else if (isSameCourseAndWeeks && isDuplicate) {
  77. // 完全重复:跳过
  78. continue;
  79. } else {
  80. step1Merged.push(current);
  81. current = next;
  82. }
  83. }
  84. step1Merged.push(current);
  85. // 阶段 2:合并同节次的周次(前提:名称、教师、地点、星期、开始/结束节次一致)
  86. step1Merged.sort((a, b) => {
  87. return a.name.localeCompare(b.name) ||
  88. a.teacher.localeCompare(b.teacher) ||
  89. a.position.localeCompare(b.position) ||
  90. (a.day || 0) - (b.day || 0) ||
  91. (a.startSection || 0) - (b.startSection || 0) ||
  92. (a.endSection || 0) - (b.endSection || 0);
  93. });
  94. const step2Merged = [];
  95. let cur = step1Merged[0];
  96. for (let i = 1; i < step1Merged.length; i++) {
  97. const nxt = step1Merged[i];
  98. const isSameCourseAndSection =
  99. cur.name === nxt.name &&
  100. cur.teacher === nxt.teacher &&
  101. cur.position === nxt.position &&
  102. cur.day === nxt.day &&
  103. cur.startSection === nxt.startSection &&
  104. cur.endSection === nxt.endSection;
  105. if (isSameCourseAndSection) {
  106. // 周次合并去重 (如 1-8 周 + 9-16 周 -> 1-16 周)
  107. cur.weeks = Array.from(new Set([...cur.weeks, ...nxt.weeks])).sort((a, b) => a - b);
  108. } else {
  109. step2Merged.push(cur);
  110. cur = nxt;
  111. }
  112. }
  113. step2Merged.push(cur);
  114. return step2Merged;
  115. }
  116. // 从HTML中提取学期列表和动态接口路径
  117. function parseIndexHtml(html, baseUrl) {
  118. const parser = new DOMParser();
  119. const doc = parser.parseFromString(html, "text/html");
  120. const select = doc.getElementById("planCode");
  121. if (!select) return null;
  122. const options = select.querySelectorAll("option");
  123. const semesterList = [];
  124. let defaultIndex = 0;
  125. options.forEach((opt, index) => {
  126. const value = opt.getAttribute("value");
  127. const text = opt.textContent.trim();
  128. if (value && value !== "no") {
  129. semesterList.push({
  130. value: value,
  131. label: text,
  132. isCurrent: text.includes("当前")
  133. });
  134. if (text.includes("当前")) defaultIndex = semesterList.length - 1;
  135. }
  136. });
  137. // 从JS中提取动态接口路径
  138. const scripts = doc.querySelectorAll("script");
  139. let ajaxUrl = null;
  140. for (const script of scripts) {
  141. const content = script.textContent;
  142. if (content && content.includes("ajaxStudentSchedule")) {
  143. const match = content.match(/url\s*:\s*"([^"]+ajaxStudentSchedule[^"]+)"/);
  144. if (match) {
  145. ajaxUrl = match[1];
  146. break;
  147. }
  148. const match2 = content.match(/\/student\/courseSelect\/thisSemesterCurriculum\/[A-Za-z0-9]+\/ajaxStudentSchedule\/past\/callback/);
  149. if (match2) {
  150. ajaxUrl = match2[0];
  151. break;
  152. }
  153. }
  154. }
  155. if (!ajaxUrl) {
  156. const forms = doc.querySelectorAll("form");
  157. for (const form of forms) {
  158. const action = form.getAttribute("action");
  159. if (action && action.includes("ajaxStudentSchedule")) {
  160. ajaxUrl = action;
  161. break;
  162. }
  163. }
  164. }
  165. return { semesterList, defaultIndex, ajaxUrl };
  166. }
  167. // 从校历页面解析开学日期
  168. function parseStartDate(html) {
  169. // 匹配 var rq = "20260831";
  170. const match = html.match(/var\s+rq\s*=\s*"(\d{8})"/);
  171. if (match) {
  172. return formatDate(match[1]);
  173. }
  174. return null;
  175. }
  176. // 用户交互函数
  177. async function promptUserToStart() {
  178. return await window.shiguangBridgePromise.showAlert(
  179. "教务系统课表导入",
  180. "导入前请确保您已在浏览器中成功登录教务系统",
  181. "好的,开始导入"
  182. );
  183. }
  184. async function selectSemesterFromList(semesterList, defaultIndex) {
  185. const labels = semesterList.map(s => s.label);
  186. const index = await window.shiguangBridgePromise.showSingleSelection(
  187. "选择学期",
  188. JSON.stringify(labels),
  189. defaultIndex
  190. );
  191. if (index === null) return null;
  192. return semesterList[index];
  193. }
  194. async function getStartDate(defaultDate) {
  195. const result = await window.shiguangBridgePromise.showPrompt(
  196. "设置开学日期",
  197. "请输入本学期开学日期(格式:YYYY-MM-DD):",
  198. defaultDate || "",
  199. "validateDate"
  200. );
  201. return result;
  202. }
  203. // 验证函数
  204. function validateDate(input) {
  205. if (!input || input.trim().length === 0) return "开学日期不能为空!";
  206. const regex = /^\d{4}-\d{2}-\d{2}$/;
  207. if (!regex.test(input)) return "请输入正确格式(例如:2026-08-31)";
  208. const parts = input.split("-");
  209. const year = parseInt(parts[0]);
  210. const month = parseInt(parts[1]);
  211. const day = parseInt(parts[2]);
  212. if (month < 1 || month > 12) return "月份必须在1-12之间";
  213. if (day < 1 || day > 31) return "日期必须在1-31之间";
  214. return false;
  215. }
  216. // 数据获取与解析
  217. async function fetchIndexPage(baseUrl) {
  218. try {
  219. const response = await fetch(`${baseUrl}/student/courseSelect/calendarSemesterCurriculum/index`, {
  220. method: "GET",
  221. credentials: "include"
  222. });
  223. const html = await response.text();
  224. const result = parseIndexHtml(html, baseUrl);
  225. if (!result || !result.semesterList || result.semesterList.length === 0) {
  226. throw new Error("未找到学期列表");
  227. }
  228. if (!result.ajaxUrl) {
  229. throw new Error("未找到课表接口路径");
  230. }
  231. return result;
  232. } catch (e) {
  233. window.shiguangBridge.showToast("获取首页失败: " + e.message);
  234. return null;
  235. }
  236. }
  237. async function fetchStartDate(baseUrl) {
  238. try {
  239. const response = await fetch(`${baseUrl}/indexCalendar`, {
  240. method: "GET",
  241. credentials: "include"
  242. });
  243. const html = await response.text();
  244. return parseStartDate(html);
  245. } catch (e) {
  246. console.error("获取开学日期失败:", e);
  247. return null;
  248. }
  249. }
  250. async function fetchTimeSlots(baseUrl) {
  251. try {
  252. const response = await fetch(`${baseUrl}/ajax/getSectionAndTime`, {
  253. headers: {
  254. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  255. "x-requested-with": "XMLHttpRequest"
  256. },
  257. body: "planNumber=&ff=f",
  258. method: "POST",
  259. credentials: "include"
  260. });
  261. const data = await response.json();
  262. if (!data || !data.sectionTime || !Array.isArray(data.sectionTime)) {
  263. return null;
  264. }
  265. const timeSlots = data.sectionTime.map(item => ({
  266. number: parseInt(item.sessionName) || item.djjc,
  267. startTime: formatTime(item.startTime),
  268. endTime: formatTime(item.endTime)
  269. }));
  270. // 提取总周数
  271. let totalWeeks = null;
  272. if (data.section && data.section.zs) {
  273. totalWeeks = parseInt(data.section.zs);
  274. }
  275. return { timeSlots, totalWeeks };
  276. } catch (e) {
  277. console.error("获取时间段失败:", e);
  278. return null;
  279. }
  280. }
  281. async function fetchCourses(baseUrl, ajaxUrl, planCode) {
  282. try {
  283. window.shiguangBridge.showToast("正在获取教务数据...");
  284. const fullUrl = `${baseUrl}${ajaxUrl}`;
  285. const response = await fetch(fullUrl, {
  286. headers: {
  287. "content-type": "application/x-www-form-urlencoded; charset=UTF-8",
  288. "x-requested-with": "XMLHttpRequest"
  289. },
  290. body: `&planCode=${planCode}`,
  291. method: "POST",
  292. credentials: "include"
  293. });
  294. const data = await response.json();
  295. if (!data) throw new Error("服务器未返回任何数据");
  296. if (!data.dateList || !Array.isArray(data.dateList)) {
  297. console.error("教务返回数据异常:", data);
  298. throw new Error("未能获取到课程列表,请检查是否已登录或该学期是否有课");
  299. }
  300. let courses = [];
  301. data.dateList.forEach(plan => {
  302. if (plan && plan.selectCourseList && Array.isArray(plan.selectCourseList)) {
  303. plan.selectCourseList.forEach(c => {
  304. const teacher = (c.attendClassTeacher || "").replace(/\* /g, "").trim();
  305. if (c.timeAndPlaceList && Array.isArray(c.timeAndPlaceList)) {
  306. c.timeAndPlaceList.forEach(tp => {
  307. let position = "";
  308. if (tp.campusName) position += tp.campusName;
  309. if (tp.teachingBuildingName) position += tp.teachingBuildingName;
  310. if (tp.classroomName) position += tp.classroomName;
  311. courses.push({
  312. name: c.courseName || c.coureName || "未知课程",
  313. teacher: teacher,
  314. position: position || "未知地点",
  315. day: tp.classDay,
  316. startSection: tp.classSessions,
  317. endSection: tp.classSessions + tp.continuingSession - 1,
  318. weeks: parseWeekString(tp.classWeek),
  319. isCustomTime: false
  320. });
  321. });
  322. }
  323. });
  324. }
  325. });
  326. if (courses.length === 0) {
  327. throw new Error("该学期暂无排课数据");
  328. }
  329. return courses;
  330. } catch (e) {
  331. window.shiguangBridge.showToast("获取课程失败: " + e.message);
  332. return null;
  333. }
  334. }
  335. // 数据保存
  336. async function saveToApp(courses, timeSlots, startDate, totalWeeks) {
  337. try {
  338. // 合并去重
  339. const mergedCourses = mergeAndDistinctCourses(courses);
  340. const courseSuccess = await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(mergedCourses));
  341. if (!courseSuccess) {
  342. throw new Error("课程保存失败");
  343. }
  344. if (timeSlots && timeSlots.length > 0) {
  345. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
  346. }
  347. const config = {};
  348. if (totalWeeks) {
  349. config.semesterTotalWeeks = totalWeeks;
  350. } else {
  351. config.semesterTotalWeeks = 20;
  352. }
  353. if (startDate) {
  354. config.semesterStartDate = startDate;
  355. }
  356. await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  357. return true;
  358. } catch (e) {
  359. window.shiguangBridge.showToast("保存失败: " + e.message);
  360. return false;
  361. }
  362. }
  363. // 流程控制
  364. async function runImportFlow() {
  365. const baseUrl = getBaseUrl();
  366. console.log("使用域名:", baseUrl);
  367. const alertResult = await promptUserToStart();
  368. if (!alertResult) return;
  369. // 获取学期列表
  370. window.shiguangBridge.showToast("正在获取学期列表...");
  371. const indexResult = await fetchIndexPage(baseUrl);
  372. if (!indexResult) return;
  373. // 选择学期
  374. const selected = await selectSemesterFromList(indexResult.semesterList, indexResult.defaultIndex);
  375. if (selected === null) {
  376. window.shiguangBridge.showToast("已取消");
  377. return;
  378. }
  379. // 获取开学日期(从校历页面自动获取,用户可修改)
  380. window.shiguangBridge.showToast("正在获取校历信息...");
  381. let startDate = await fetchStartDate(baseUrl);
  382. if (startDate) {
  383. startDate = await getStartDate(startDate);
  384. if (startDate === null) {
  385. window.shiguangBridge.showToast("已取消");
  386. return;
  387. }
  388. } else {
  389. startDate = await getStartDate("");
  390. if (startDate === null) {
  391. window.shiguangBridge.showToast("已取消");
  392. return;
  393. }
  394. }
  395. // 获取课程和时间段
  396. window.shiguangBridge.showToast("正在获取数据...");
  397. const [courses, timeSlotResult] = await Promise.all([
  398. fetchCourses(baseUrl, indexResult.ajaxUrl, selected.value),
  399. fetchTimeSlots(baseUrl)
  400. ]);
  401. if (!courses || courses.length === 0) {
  402. window.shiguangBridge.showToast("未获取到课程数据");
  403. return;
  404. }
  405. const timeSlots = timeSlotResult ? timeSlotResult.timeSlots : null;
  406. const totalWeeks = timeSlotResult ? timeSlotResult.totalWeeks : null;
  407. // 保存数据
  408. const saveResult = await saveToApp(courses, timeSlots, startDate, totalWeeks);
  409. if (!saveResult) return;
  410. window.shiguangBridge.showToast(`成功导入 ${courses.length} 个课程时段`);
  411. window.shiguangBridge.notifyTaskCompletion();
  412. }
  413. // 启动
  414. runImportFlow();