ynufe.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855
  1. // ========== 工具函数 ==========
  2. /**
  3. * 解析周数字符串
  4. * @param {string} Str 如:1-6,7-13周(单)
  5. * @returns {Array} 返回数组 [1,3,5,7,9,11,13]
  6. */
  7. function getWeeks(Str) {
  8. function range(con, tag) {
  9. let retWeek = [];
  10. con.slice(0, -1).split(',').forEach(w => {
  11. let tt = w.split('-');
  12. let start = parseInt(tt[0]);
  13. let end = parseInt(tt[tt.length - 1]);
  14. if (tag === 1 || tag === 2) {
  15. retWeek.push(...Array(end + 1 - start).fill(start).map((x, y) => x + y).filter(f => {
  16. return f % tag === 0;
  17. }));
  18. } else {
  19. retWeek.push(...Array(end + 1 - start).fill(start).map((x, y) => x + y).filter(v => {
  20. return v % 2 !== 0;
  21. }));
  22. }
  23. });
  24. return retWeek;
  25. }
  26. Str = Str.replace(/[(){}|第\[\]]/g, "").replace(/到/g, "-");
  27. let reWeek = [];
  28. let week1 = [];
  29. while (Str.search(/周|\s/) !== -1) {
  30. let index = Str.search(/周|\s/);
  31. if (Str[index + 1] === '单' || Str[index + 1] === '双') {
  32. week1.push(Str.slice(0, index + 2).replace(/周|\s/g, ""));
  33. index += 2;
  34. } else {
  35. week1.push(Str.slice(0, index + 1).replace(/周|\s/g, ""));
  36. index += 1;
  37. }
  38. Str = Str.slice(index);
  39. index = Str.search(/\d/);
  40. if (index !== -1) Str = Str.slice(index);
  41. else Str = "";
  42. }
  43. if (Str.length !== 0) week1.push(Str);
  44. week1.forEach(v => {
  45. if (v.slice(-1) === "双") {
  46. reWeek.push(...range(v, 2));
  47. } else if (v.slice(-1) === "单") {
  48. reWeek.push(...range(v, 3));
  49. } else {
  50. reWeek.push(...range(v + "全", 1));
  51. }
  52. });
  53. return reWeek;
  54. }
  55. /**
  56. * 解析节次字符串
  57. * @param {string} Str 如: 1-4节 或 1-2-3-4节
  58. * @returns {Array} [1,2,3,4]
  59. */
  60. function getSection(Str) {
  61. let reJc = [];
  62. let strArr = Str.replace("节", "").trim().split("-");
  63. if (strArr.length <= 2) {
  64. for (let i = Number(strArr[0]); i <= Number(strArr[strArr.length - 1]); i++) {
  65. reJc.push(Number(i));
  66. }
  67. } else {
  68. strArr.forEach(v => {
  69. reJc.push(Number(v));
  70. });
  71. }
  72. return reJc;
  73. }
  74. /**
  75. * 检查是否在登录页面
  76. * @returns {boolean}
  77. */
  78. function isLoginPage() {
  79. const url = window.location.href;
  80. // 检查URL是否包含登录页面特征
  81. return url.includes('login') || url.includes('Login') ||
  82. document.querySelector('input[type="password"]') !== null;
  83. }
  84. /**
  85. * 解析课程HTML数据
  86. * @param {string} html 课程表HTML
  87. * @returns {Array} 课程数组
  88. */
  89. function parseScheduleHtml(html) {
  90. let result = [];
  91. let uniqueCourses = []; // 移到外部作用域
  92. try {
  93. // 创建临时div来解析HTML
  94. const tempDiv = document.createElement('div');
  95. tempDiv.innerHTML = html;
  96. console.log('开始解析课程表HTML...');
  97. // 查找课程表格
  98. const table = tempDiv.querySelector('#kbtable') || tempDiv.querySelector('table');
  99. if (!table) {
  100. throw new Error('未找到课程表格');
  101. }
  102. // 遍历所有行(每行是一个大节时间段)
  103. const rows = table.querySelectorAll('tr');
  104. console.log(`找到 ${rows.length} 行`);
  105. // 用于记录已处理的div,避免重复(跨大节课程会在多行出现)
  106. const processedDivs = new Set();
  107. rows.forEach((tr, rowIdx) => {
  108. const tds = tr.querySelectorAll('td');
  109. // 遍历这一行的所有td列
  110. // 注意: querySelectorAll('td')只选择td元素,不包括th
  111. // 所以td[0]就是星期一, td[1]是星期二, ..., td[6]是星期日
  112. tds.forEach((td, colIdx) => {
  113. // 查找这个单元格里的课程内容div
  114. const hiddenDiv = td.querySelector('div.kbcontent');
  115. // 如果没有隐藏div或内容为空,跳过
  116. if (!hiddenDiv) {
  117. return;
  118. }
  119. // 检查是否已经处理过这个div(根据name属性去重)
  120. const divName = hiddenDiv.getAttribute('name') || hiddenDiv.getAttribute('id');
  121. if (divName && processedDivs.has(divName)) {
  122. return; // 已处理过,跳过
  123. }
  124. const divText = hiddenDiv.textContent.trim();
  125. if (!divText || divText.length <= 6) {
  126. return;
  127. }
  128. // 从div的name属性提取星期信息
  129. // name格式: "hash-星期-序号" 例如 "EBC6F96389D143DC9C53084617F9C7D2-2-1"
  130. // 其中第二部分的数字: 1=星期一, 2=星期二, ..., 7=星期日
  131. let day = colIdx + 1; // 默认使用列索引
  132. if (divName) {
  133. const nameParts = divName.split('-');
  134. if (nameParts.length >= 3) {
  135. const dayFromName = parseInt(nameParts[1]);
  136. if (!isNaN(dayFromName) && dayFromName >= 1 && dayFromName <= 7) {
  137. day = dayFromName;
  138. }
  139. }
  140. }
  141. // 标记为已处理
  142. if (divName) {
  143. processedDivs.add(divName);
  144. }
  145. console.log(`\n[行${rowIdx} 列${colIdx} 星期${day}]`);
  146. console.log(`内容预览: ${divText.substring(0, 50)}...`);
  147. // 可能包含多个课程,用 ----- 分隔
  148. const courseSections = hiddenDiv.innerHTML.split(/-----+/);
  149. console.log(`分割成 ${courseSections.length} 个课程段`);
  150. // 用于课程段去重(避免完全相同的课程段被重复添加)
  151. const processedSections = new Set();
  152. // 遍历每个课程段
  153. courseSections.forEach((section, sectionIdx) => {
  154. const sectionText = section.replace(/<[^>]*>/g, '').trim();
  155. if (!sectionText || sectionText.length < 3) {
  156. return;
  157. }
  158. // 检查是否已经处理过完全相同的课程段(内容去重)
  159. if (processedSections.has(sectionText)) {
  160. console.log(` 跳过重复课程段 ${sectionIdx + 1}`);
  161. return;
  162. }
  163. processedSections.add(sectionText);
  164. console.log(` 课程段 ${sectionIdx + 1}:`);
  165. console.log(` 原始HTML:`, section.substring(0, 200));
  166. let course = {
  167. day: day, // 星期几(1=周一, 2=周二, ..., 7=周日)
  168. weeks: [],
  169. sections: [],
  170. name: '',
  171. teacher: '',
  172. position: ''
  173. };
  174. // 解析HTML,按br分割成行
  175. const lines = section.split(/<br\s*\/?>/i);
  176. console.log(` 分割成 ${lines.length} 行`);
  177. let firstTextLine = true; // 标记是否是第一个有效文本行
  178. lines.forEach((line, lineIdx) => {
  179. // 跳过空行
  180. const plainText = line.replace(/<[^>]*>/g, '').trim();
  181. if (!plainText || plainText === '&nbsp;') {
  182. return;
  183. }
  184. console.log(` 行${lineIdx}: ${line.substring(0, 100)}`);
  185. console.log(` 纯文本: ${plainText}`);
  186. // 第一个有效文本行就是课程名(没有title属性)
  187. if (firstTextLine && !course.name) {
  188. // 移除span标签(包含调课标记如&nbspO)但保留其他内容
  189. let courseName = line.replace(/<span[^>]*>.*?<\/span>/gi, '').trim();
  190. // 提取纯文本
  191. courseName = courseName.replace(/<[^>]*>/g, '').trim();
  192. // 清理HTML实体
  193. courseName = courseName.replace(/&nbsp;/g, ' ').trim();
  194. course.name = courseName;
  195. console.log(` ✓ 第一行作为课程名: ${course.name}`);
  196. firstTextLine = false;
  197. return;
  198. }
  199. firstTextLine = false;
  200. // 检查这一行的title属性(使用双引号)
  201. if (line.includes('title="老师"')) {
  202. course.teacher = plainText;
  203. console.log(` ✓ 匹配老师: ${course.teacher}`);
  204. }
  205. else if (line.includes('title="教室"')) {
  206. // 对于教室,需要先移除隐藏的font标签,再提取文本
  207. const cleanLine = line.replace(/<font[^>]*style="display:none;"[^>]*>.*?<\/font>/gi, '');
  208. const cleanText = cleanLine.replace(/<[^>]*>/g, '').trim();
  209. // 再移除可能残留的前导数字
  210. const finalPosition = cleanText.replace(/^[\d-]+/, '').trim();
  211. course.position = finalPosition;
  212. console.log(` ✓ 匹配教室: ${course.position}`);
  213. }
  214. else if (line.includes('title="周次(节次)"')) {
  215. console.log(` ✓ 匹配周次节次: ${plainText}`);
  216. // 解析周次: "1-18(周)[06-07节]"
  217. const weekMatch = plainText.match(/^(.+?)\(周\)/);
  218. if (weekMatch) {
  219. const weekStr = weekMatch[1];
  220. course.weeks = getWeeks(weekStr + '周');
  221. console.log(` -> 周: ${course.weeks}`);
  222. }
  223. // 解析节次: "[06-07节]"
  224. const sectionMatch = plainText.match(/\[(.+?)节?\]/);
  225. if (sectionMatch) {
  226. const sectionStr = sectionMatch[1];
  227. course.sections = getSection(sectionStr + '节');
  228. console.log(` -> 节: ${course.sections}`);
  229. }
  230. }
  231. // 如果没有找到教室,尝试从包含隐藏font的行提取
  232. // 这行可能格式如: <font style="display:none;">01-02</font><font style="display:none;">20</font>北院卓媒220
  233. else if (!course.position && line.includes('style="display:none;"')) {
  234. // 移除所有隐藏的font标签
  235. const visibleText = line.replace(/<font[^>]*style="display:none;"[^>]*>.*?<\/font>/gi, '')
  236. .replace(/<[^>]*>/g, '')
  237. .trim();
  238. if (visibleText && visibleText.length > 0) {
  239. // 移除所有前导的数字和连字符(如 "01-0220" 或 "06-0722")
  240. // 匹配模式:开头的数字-数字组合
  241. const cleanPosition = visibleText.replace(/^[\d-]+/, '').trim();
  242. if (cleanPosition.length > 0) {
  243. course.position = cleanPosition;
  244. console.log(` ✓ 提取教室(清理后): ${course.position}`);
  245. }
  246. }
  247. }
  248. });
  249. // 验证并添加课程
  250. if (course.name && course.weeks.length > 0 && course.sections.length > 0) {
  251. course.teacher = course.teacher || "未知教师";
  252. course.position = course.position || "未知地点";
  253. console.log(` ✓ 完整课程:`, {
  254. name: course.name,
  255. teacher: course.teacher,
  256. position: course.position,
  257. day: course.day,
  258. weeks: `${course.weeks.length}周`,
  259. sections: course.sections
  260. });
  261. result.push(course);
  262. } else {
  263. console.warn(` ✗ 信息不完整:`, {
  264. name: course.name || '无',
  265. teacher: course.teacher || '无',
  266. weeks: course.weeks.length,
  267. sections: course.sections.length
  268. });
  269. }
  270. });
  271. });
  272. });
  273. console.log(`\n解析完成,共得到 ${result.length} 条课程记录(去重前)`);
  274. // 合并完全相同的课程(去重),并合并仅周次不同的同节次课程
  275. const courseMap = new Map();
  276. result.forEach(course => {
  277. // 生成课程唯一标识: 名称+老师+地点+星期+节次(不包含周次)
  278. const mergeKey = `${course.name}|${course.teacher}|${course.position}|${course.day}|${course.sections.join(',')}`;
  279. if (!courseMap.has(mergeKey)) {
  280. // 深拷贝一份以避免引用问题
  281. courseMap.set(mergeKey, { ...course, weeks: [...course.weeks] });
  282. } else {
  283. console.log(` 合并同时间不同周次的课程: ${course.name} (${course.teacher})`);
  284. const existingCourse = courseMap.get(mergeKey);
  285. // 合并并去重然后重新排序周次
  286. existingCourse.weeks = [...new Set([...existingCourse.weeks, ...course.weeks])].sort((a, b) => a - b);
  287. }
  288. });
  289. uniqueCourses = Array.from(courseMap.values());
  290. console.log(`合并/去重后剩余 ${uniqueCourses.length} 条课程记录`);
  291. } catch (err) {
  292. console.error('解析课程表出错:', err);
  293. throw new Error('解析课程表失败: ' + err.message);
  294. }
  295. return uniqueCourses;
  296. }
  297. /**
  298. * 转换课程数据格式以符合拾光课程表规范
  299. * @param {Array} rawCourses 原始课程数据
  300. * @returns {Array} 转换后的课程数据
  301. */
  302. function convertCoursesToStandardFormat(rawCourses) {
  303. const validCourses = [];
  304. rawCourses.forEach((course, index) => {
  305. try {
  306. // 处理节次:将原始格式转换为startSection和endSection
  307. let startSection = 1;
  308. let endSection = 1;
  309. if (course.sections && course.sections.length > 0) {
  310. const sections = course.sections.sort((a, b) => a - b);
  311. startSection = sections[0];
  312. endSection = sections[sections.length - 1];
  313. }
  314. // 验证必需字段
  315. if (!startSection || !endSection || startSection < 1 || endSection < 1) {
  316. console.error(`课程 ${index + 1} 缺少有效的节次信息:`, course);
  317. throw new Error(`课程节次信息无效: startSection=${startSection}, endSection=${endSection}`);
  318. }
  319. if (!course.day || course.day < 1 || course.day > 7) {
  320. console.error(`课程 ${index + 1} 星期数据无效:`, course);
  321. throw new Error(`课程星期数据无效: day=${course.day}`);
  322. }
  323. if (!course.weeks || course.weeks.length === 0) {
  324. console.error(`课程 ${index + 1} 缺少周次信息:`, course);
  325. throw new Error(`课程周次信息缺失`);
  326. }
  327. const convertedCourse = {
  328. name: course.name || "未知课程",
  329. teacher: course.teacher || "未知教师",
  330. position: course.position || "未知地点",
  331. day: course.day,
  332. startSection: startSection,
  333. endSection: endSection,
  334. weeks: course.weeks
  335. };
  336. validCourses.push(convertedCourse);
  337. } catch (err) {
  338. console.error(`转换课程 ${index + 1} 时出错:`, err.message);
  339. // 如果任何课程转换失败,抛出错误
  340. throw new Error(`课程数据验证失败: ${err.message}`);
  341. }
  342. });
  343. return validCourses;
  344. }
  345. /**
  346. * 生成时间段配置
  347. * @param {number} campusIdx 校区索引(0为龙泉校区,1为安宁校区)
  348. * @returns {Array} 时间段数组
  349. */
  350. function generateTimeSlots(campusIdx = 0) {
  351. // 云南财经大学默认时间配置(龙泉校区)
  352. const timeSlots = [
  353. { "number": 1, "startTime": "08:00", "endTime": "08:40" },
  354. { "number": 2, "startTime": "08:50", "endTime": "09:30" },
  355. { "number": 3, "startTime": "10:00", "endTime": "10:40" },
  356. { "number": 4, "startTime": "10:50", "endTime": "11:30" },
  357. { "number": 5, "startTime": "11:40", "endTime": "12:20" },
  358. { "number": 6, "startTime": "14:30", "endTime": "15:10" },
  359. { "number": 7, "startTime": "15:20", "endTime": "16:00" },
  360. { "number": 8, "startTime": "16:30", "endTime": "17:10" },
  361. { "number": 9, "startTime": "17:20", "endTime": "18:00" },
  362. { "number": 10, "startTime": "18:10", "endTime": "18:30" },
  363. { "number": 11, "startTime": "19:00", "endTime": "19:40" },
  364. { "number": 12, "startTime": "19:50", "endTime": "20:30" },
  365. { "number": 13, "startTime": "20:50", "endTime": "21:30" },
  366. { "number": 14, "startTime": "21:40", "endTime": "22:20" }
  367. ];
  368. // 安宁校区时间配置
  369. const timeSlots_AN = [
  370. { "number": 1, "startTime": "08:20", "endTime": "09:00" },
  371. { "number": 2, "startTime": "09:10", "endTime": "09:50" },
  372. { "number": 3, "startTime": "10:10", "endTime": "10:50" },
  373. { "number": 4, "startTime": "11:00", "endTime": "11:40" },
  374. { "number": 5, "startTime": "11:50", "endTime": "12:30" },
  375. { "number": 6, "startTime": "14:00", "endTime": "14:40" },
  376. { "number": 7, "startTime": "14:50", "endTime": "15:30" },
  377. { "number": 8, "startTime": "15:40", "endTime": "16:20" },
  378. { "number": 9, "startTime": "16:40", "endTime": "17:20" },
  379. { "number": 10, "startTime": "17:30", "endTime": "18:10" },
  380. { "number": 11, "startTime": "19:00", "endTime": "19:40" },
  381. { "number": 12, "startTime": "19:50", "endTime": "20:30" },
  382. { "number": 13, "startTime": "20:40", "endTime": "21:20" }
  383. ];
  384. return campusIdx === 1 ? timeSlots_AN : timeSlots;
  385. }
  386. // ========== 网络请求功能 ==========
  387. /**
  388. * POST 教学周历页面(获取当前学期的开学日期与最大周次)
  389. * @param {string} semesterId 形如 "2025-2026-2"
  390. */
  391. async function requestSemesterDetailPage(semesterId) {
  392. const response = await fetch(`/jsxsd/jxzl/jxzl_query`, {
  393. method: "POST",
  394. headers: { "Content-Type": "application/x-www-form-urlencoded" },
  395. body: `xnxq01id=${semesterId}`,
  396. credentials: "include"
  397. });
  398. return await response.text();
  399. }
  400. /**
  401. * 获取当前学期的开学日期 + 最大周次
  402. * @param {string} semesterId 形如 "2025-2026-2"
  403. * @returns {Promise<{startDate: string|null, totalWeeks: number|null}>}
  404. */
  405. async function fetchSemesterInfo(semesterId) {
  406. const html = await requestSemesterDetailPage(semesterId);
  407. const doc = new DOMParser().parseFromString(html, "text/html");
  408. const timetable = doc.getElementById("kbtable");
  409. if (!timetable) return { startDate: null, totalWeeks: null };
  410. const rows = Array.from(timetable.querySelectorAll("tr")).filter(r => r.querySelector("td"));
  411. if (rows.length === 0) return { startDate: null, totalWeeks: null };
  412. const firstDateCell = rows[0].querySelector("td[title]");
  413. const title = firstDateCell?.getAttribute("title") || "";
  414. const match = title.match(/^(\d{4})年(\d{2})月(\d{2})$/);
  415. const startDate = match ? `${match[1]}-${match[2]}-${match[3]}` : null;
  416. // 最大周次:所有行第一个 td 中能解析为整数的最大值
  417. let totalWeeks = null;
  418. for (const row of rows) {
  419. const firstTd = row.querySelector("td");
  420. if (!firstTd) continue;
  421. const n = parseInt(firstTd.textContent.trim(), 10);
  422. if (!isNaN(n) && n > 0) {
  423. if (totalWeeks === null || n > totalWeeks) totalWeeks = n;
  424. }
  425. }
  426. return { startDate, totalWeeks };
  427. }
  428. /**
  429. * 获取学期列表
  430. * @returns {Promise<Object>} 学期列表数据
  431. */
  432. async function getSemesterList() {
  433. window.shiguangBridge.showToast('正在获取学期列表...');
  434. const response = await fetch('/jsxsd/xskb/xskb_list.do', { method: 'GET', credentials: 'include' });
  435. const htmlText = await response.text();
  436. const parser = new DOMParser();
  437. let doc = parser.parseFromString(htmlText, 'text/html');
  438. const selectElem = doc.getElementById('xnxq01id');
  439. let semesters = [];
  440. let semesterValues = [];
  441. let defaultIndex = 0;
  442. let currentSemesterValue = null;
  443. if (selectElem) {
  444. const options = selectElem.querySelectorAll('option');
  445. options.forEach((opt, index) => {
  446. const isCurrentSemester = opt.hasAttribute('selected') || opt.selected;
  447. semesters.push(opt.innerText.trim());
  448. semesterValues.push(opt.value);
  449. if (isCurrentSemester) {
  450. defaultIndex = index;
  451. currentSemesterValue = opt.value;
  452. }
  453. });
  454. }
  455. // 仅保留最新学期往前五年内的学期选项
  456. const semesterYearPattern = /^(\d{4})-(\d{4})-(\d+)$/;
  457. const semesterStartYears = [];
  458. const semesterRecords = semesterValues.map((value, index) => {
  459. const label = semesters[index] || '';
  460. const valueMatch = String(value).match(semesterYearPattern);
  461. const labelMatch = String(label).match(semesterYearPattern);
  462. const startYear = valueMatch ? Number(valueMatch[1]) : (labelMatch ? Number(labelMatch[1]) : null);
  463. if (startYear !== null) {
  464. semesterStartYears.push(startYear);
  465. }
  466. return { index, value, label, startYear };
  467. });
  468. if (semesterStartYears.length > 0) {
  469. const latestStartYear = Math.max(...semesterStartYears);
  470. const minStartYear = latestStartYear - 4;
  471. const filteredSemesters = [];
  472. const filteredSemesterValues = [];
  473. let filteredDefaultIndex = -1;
  474. semesterRecords.forEach((record) => {
  475. if (record.startYear !== null && record.startYear >= minStartYear) {
  476. const isCurrentSemester = record.value === currentSemesterValue;
  477. filteredSemesters.push(isCurrentSemester ? `${record.label}(当前学期)` : record.label);
  478. filteredSemesterValues.push(record.value);
  479. if (isCurrentSemester || record.index === defaultIndex) {
  480. filteredDefaultIndex = filteredSemesters.length - 1;
  481. }
  482. }
  483. });
  484. semesters = filteredSemesters;
  485. semesterValues = filteredSemesterValues;
  486. if (filteredDefaultIndex !== -1) {
  487. defaultIndex = filteredDefaultIndex;
  488. } else if (defaultIndex >= semesterValues.length) {
  489. defaultIndex = Math.max(semesterValues.length - 1, 0);
  490. }
  491. }
  492. return { semesters, semesterValues, defaultIndex, htmlText };
  493. }
  494. /**
  495. * 根据学期值获取课程表HTML
  496. * @param {string} semesterValue 学期参数值
  497. * @returns {Promise<string>} 课程表HTML
  498. */
  499. async function fetchScheduleForSemester(semesterValue, semesterPageHtml = '', timeModeValue = null) {
  500. let formData = new URLSearchParams();
  501. formData.append('jx0404id', '');
  502. formData.append('cj0701id', '');
  503. formData.append('zc', '');
  504. formData.append('demo', '');
  505. formData.append('xnxq01id', semesterValue);
  506. formData.append('kbjcmsid', timeModeValue === null
  507. ? getTimeModeValue(semesterPageHtml, option => option.textContent.trim() === '全部')
  508. : timeModeValue);
  509. const postResponse = await fetch('/jsxsd/xskb/xskb_list.do', {
  510. method: 'POST',
  511. headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  512. body: formData.toString(),
  513. credentials: 'include'
  514. });
  515. return await postResponse.text();
  516. }
  517. /**
  518. * 根据文本匹配时间模式选项 value
  519. * @param {string} html 页面HTML
  520. * @param {Function} matcher 选项匹配函数
  521. * @returns {string} 时间模式 value
  522. */
  523. function getTimeModeValue(html, matcher) {
  524. const sourceDoc = html
  525. ? new DOMParser().parseFromString(html, 'text/html')
  526. : document;
  527. const modeSelect = sourceDoc.getElementById('kbjcmsid');
  528. if (!modeSelect) return '';
  529. const option = Array.from(modeSelect.options).find(matcher);
  530. return option ? option.value : '';
  531. }
  532. /**
  533. * 判断课表页面是否存在课程
  534. * @param {string} html 课表HTML
  535. * @returns {boolean} 是否存在课程
  536. */
  537. function hasScheduleCourses(html) {
  538. const doc = new DOMParser().parseFromString(html, 'text/html');
  539. return Array.from(doc.querySelectorAll('#kbtable div.kbcontent'))
  540. .some(course => course.textContent.trim().length > 6);
  541. }
  542. // ========== 主要功能函数 ==========
  543. /**
  544. * 获取和解析课程数据
  545. * @param {string} html 课程表HTML
  546. */
  547. async function fetchAndParseCourses(html) {
  548. try {
  549. console.log('开始解析获取到的课程表HTML...');
  550. if (!html) {
  551. console.warn('未传入有效的课程表HTML');
  552. return null;
  553. }
  554. console.log('成功获取课程表HTML,开始解析...');
  555. // 解析课程数据
  556. const rawCourses = parseScheduleHtml(html);
  557. if (!rawCourses || rawCourses.length === 0) {
  558. console.warn('未解析到课程数据');
  559. return null;
  560. }
  561. console.log(`原始解析到 ${rawCourses.length} 条课程记录`);
  562. // 转换为标准格式
  563. const courses = convertCoursesToStandardFormat(rawCourses);
  564. console.log(`转换为标准格式后有 ${courses.length} 门课程`);
  565. return courses;
  566. } catch (error) {
  567. console.error('获取或解析课程数据失败:', error);
  568. return null;
  569. }
  570. }
  571. /**
  572. * 保存课程数据到拾光课程表
  573. * @param {Array} courses 课程数组
  574. */
  575. async function saveCourses(courses) {
  576. try {
  577. console.log(`正在保存 ${courses.length} 门课程...`);
  578. await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses));
  579. console.log('课程数据保存成功');
  580. return true;
  581. } catch (error) {
  582. console.error('保存课程失败:', error);
  583. return false;
  584. }
  585. }
  586. /**
  587. * 导入预设时间段到拾光课程表
  588. * @param {number} campusIdx 校区索引
  589. */
  590. async function importPresetTimeSlots(campusIdx = 0) {
  591. try {
  592. console.log('正在导入时间段配置...');
  593. const presetTimeSlots = generateTimeSlots(campusIdx);
  594. await window.shiguangBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  595. console.log('时间段配置导入成功');
  596. return true;
  597. } catch (error) {
  598. console.error('导入时间段失败:', error);
  599. return false;
  600. }
  601. }
  602. // 导入剩余配置
  603. async function saveAppConfig(semesterStartDate, totalWeeks) {
  604. const config = {
  605. "semesterTotalWeeks": totalWeeks || 20,
  606. "firstDayOfWeek": 1,
  607. "defaultClassDuration": 40
  608. };
  609. if (semesterStartDate) {
  610. config.semesterStartDate = semesterStartDate;
  611. }
  612. return await window.shiguangBridgePromise.saveCourseConfig(JSON.stringify(config));
  613. }
  614. // ========== 主执行流程 ==========
  615. /**
  616. * 主导入函数:云南财经大学课程表导入
  617. */
  618. async function importYnufeCourseSchedule() {
  619. // 检查是否在登录页面
  620. if (isLoginPage()) {
  621. console.log('检测到在登录页面,终止导入');
  622. window.shiguangBridge.showToast('请先登录教务系统!');
  623. return; // 直接返回,不抛出错误,不调用notifyTaskCompletion
  624. }
  625. try {
  626. console.log('云南财经大学课程导入开始...');
  627. // 获取学期列表
  628. let semesters = [], semesterValues = [], defaultIndex = 0, htmlText = '';
  629. try {
  630. const listData = await getSemesterList();
  631. semesters = listData.semesters;
  632. semesterValues = listData.semesterValues;
  633. defaultIndex = listData.defaultIndex;
  634. htmlText = listData.htmlText;
  635. } catch (e) {
  636. console.warn('获取学期列表网络请求失败:', e);
  637. }
  638. let targetHtml = htmlText;
  639. let selectedCampusIdx = 0; // 默认龙泉校区
  640. let semesterStartDate = null;
  641. let totalWeeks = null;
  642. if (semesters && semesters.length > 0) {
  643. // 选择学期,允许用户取消导入
  644. const selectedIdx = await window.shiguangBridgePromise.showSingleSelection(
  645. "选择学期",
  646. JSON.stringify(semesters),
  647. defaultIndex
  648. );
  649. if (selectedIdx === null || selectedIdx === -1) {
  650. window.shiguangBridge.showToast("用户取消了导入");
  651. return false;
  652. }
  653. const semesterId = semesterValues[selectedIdx];
  654. // 获取当前学期的开学日期与最大周次
  655. const semesterInfo = await fetchSemesterInfo(semesterId);
  656. semesterStartDate = semesterInfo.startDate;
  657. totalWeeks = semesterInfo.totalWeeks;
  658. // 获取对应学期的课表HTML
  659. window.shiguangBridge.showToast(`正在获取 [${semesters[selectedIdx]}] 的课表...`);
  660. targetHtml = await fetchScheduleForSemester(semesterId, htmlText);
  661. // 时间模式列表始终包含所有校区,必须使用当前所选学期的页面实际请求安宁校区模式
  662. const anningModeValue = getTimeModeValue(targetHtml, option => {
  663. return option.textContent.trim().includes('安宁校区');
  664. });
  665. if (anningModeValue) {
  666. const anningScheduleHtml = await fetchScheduleForSemester(
  667. semesterId,
  668. targetHtml,
  669. anningModeValue
  670. );
  671. if (hasScheduleCourses(anningScheduleHtml)) {
  672. selectedCampusIdx = 1;
  673. }
  674. }
  675. console.log(`根据时间模式自动选择${selectedCampusIdx === 1 ? '安宁校区' : '龙泉校区'}时间表`);
  676. }
  677. // 获取和解析课程数据
  678. let courses = await fetchAndParseCourses(targetHtml);
  679. // 如果没有获取到任何课程
  680. if (!courses || courses.length === 0) {
  681. console.log('未获取到课程数据');
  682. // 检查是否真的是空课表
  683. if (targetHtml && targetHtml.includes('kbtable')) {
  684. // 找到了课表元素但没有课程,是真的空课表
  685. console.log('检测到空课表');
  686. window.shiguangBridge.showToast('当前学期课表为空!');
  687. return false;
  688. } else {
  689. // 找不到课表元素,解析失败
  690. window.shiguangBridge.showToast('获取课表失败,请检查网络和页面状态');
  691. throw new Error('未找到课表数据');
  692. }
  693. } else {
  694. console.log(`成功解析 ${courses.length} 门课程`);
  695. }
  696. // 保存课程数据
  697. const saveResult = await saveCourses(courses);
  698. if (!saveResult) {
  699. return;
  700. }
  701. // 提前提示导入结果,避免被导入完成通知立即覆盖
  702. if (courses.length > 0) {
  703. const campusName = selectedCampusIdx === 1 ? "安宁校区" : "龙泉校区";
  704. window.shiguangBridge.showToast(`成功导入 ${courses.length} 门课程!(${campusName})`);
  705. }
  706. // 保存开学日期与最大周数
  707. const configResult = await saveAppConfig(semesterStartDate, totalWeeks);
  708. if (!configResult) {
  709. return;
  710. }
  711. // 导入时间段配置
  712. const timeSlotResult = await importPresetTimeSlots(selectedCampusIdx);
  713. if (!timeSlotResult) {
  714. window.shiguangBridge.showToast('导入时间段配置失败');
  715. throw new Error('导入时间段失败');
  716. }
  717. console.log('课程导入完成');
  718. return true;
  719. } catch (error) {
  720. console.error('导入过程出错:', error);
  721. window.shiguangBridge.showToast('导入失败: ' + error.message);
  722. return false;
  723. }
  724. }
  725. /**
  726. * 启动导入流程并处理完成信号
  727. */
  728. async function runImportFlow() {
  729. const success = await importYnufeCourseSchedule();
  730. // 只有成功导入时才发送完成信号
  731. if (success) {
  732. window.shiguangBridge.notifyTaskCompletion();
  733. }
  734. return success;
  735. }
  736. // 启动导入流程
  737. runImportFlow();