scuec.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631
  1. // ==========================================
  2. // 文件: scuec.js
  3. // 中南民族大学教务系统课程表导入脚本
  4. // 开发规范: 结构化编程 + async/await 流程控制树
  5. // ==========================================
  6. // ========== 第一部分:工具函数 ==========
  7. /**
  8. * 检查是否在正确的教务系统页面
  9. */
  10. function isOnSchedulePage() {
  11. const url = window.location.href;
  12. return /jiaowu|jwgl|course|schedule|curriculum/i.test(url) ||
  13. document.querySelector('table.CourseFormTable') !== null;
  14. }
  15. /**
  16. * 解析周次字符串
  17. */
  18. function parseWeeks(weekStr) {
  19. const weeks = [];
  20. if (!weekStr) return weeks;
  21. weekStr = weekStr.trim();
  22. const isSingleWeek = weekStr.includes('(单)');
  23. const match = weekStr.match(/(\d+)\s*[-~]\s*(\d+)|(\d+)\s*周/);
  24. if (match) {
  25. let start, end;
  26. if (match[1] && match[2]) {
  27. start = parseInt(match[1]);
  28. end = parseInt(match[2]);
  29. } else if (match[3]) {
  30. start = end = parseInt(match[3]);
  31. } else {
  32. return weeks;
  33. }
  34. if (isSingleWeek) {
  35. for (let i = start; i <= end; i += 2) {
  36. weeks.push(i);
  37. }
  38. } else {
  39. for (let i = start; i <= end; i++) {
  40. weeks.push(i);
  41. }
  42. }
  43. }
  44. return weeks;
  45. }
  46. /**
  47. * 清理文本:移除HTML标签但保留文本内容
  48. * 特别处理空标签和多余空格
  49. */
  50. function cleanHTML(html) {
  51. if (!html) return '';
  52. // 创建临时元素
  53. const temp = document.createElement('div');
  54. temp.innerHTML = html;
  55. // 获取纯文本
  56. let text = temp.textContent || temp.innerText || '';
  57. // 清理多余空格和特殊字符
  58. text = text
  59. .replace(/&nbsp;/g, ' ') // 替换 nbsp
  60. .replace(/\s+/g, ' ') // 多个空格合并为一个
  61. .trim();
  62. return text;
  63. }
  64. /**
  65. * 智能分割文本为行
  66. * 支持 \n, <br>, <hr> 分隔符
  67. */
  68. function smartSplitLines(html, separator = '<br') {
  69. if (!html) return [];
  70. let parts = [];
  71. // 如果指定了分隔符,先用分隔符分割
  72. if (separator === '<hr') {
  73. parts = html.split(/<hr\s*\/?>/i);
  74. } else if (separator === '<br') {
  75. parts = html.split(/<br\s*\/?>/i);
  76. } else {
  77. parts = [html];
  78. }
  79. // 对每个部分清理并分行
  80. let lines = [];
  81. parts.forEach(part => {
  82. let cleaned = cleanHTML(part);
  83. if (cleaned) {
  84. // 再按空行分割
  85. let subLines = cleaned.split(/\n+/).map(l => l.trim()).filter(l => l !== '');
  86. lines.push(...subLines);
  87. }
  88. });
  89. return lines;
  90. }
  91. /**
  92. * 解析单个课程信息(更加健壮)
  93. */
  94. function parseSingleCourse(courseHTML) {
  95. if (!courseHTML || courseHTML.trim() === '') {
  96. return null;
  97. }
  98. try {
  99. // 用 <br> 分割成行
  100. const lines = smartSplitLines(courseHTML, '<br');
  101. if (lines.length === 0) {
  102. return null;
  103. }
  104. console.log(`[DEBUG] 课程块行数: ${lines.length}`, lines);
  105. // ========== 第一行:课程名 + 周次 + 节次 ==========
  106. const firstLine = lines[0];
  107. // 提取课程名
  108. let courseName = '';
  109. const courseNameMatch = firstLine.match(/^(.+?)(?:\s*\[|\s+\d+-|\s*$)/);
  110. if (courseNameMatch) {
  111. courseName = courseNameMatch[1].trim();
  112. }
  113. if (!courseName) {
  114. console.warn('[WARN] 无法提取课程名:', firstLine);
  115. return null;
  116. }
  117. // 提取周次
  118. const weekMatch = firstLine.match(/(\d+[-~]\d+周(?:\(单\))?|\d+周)/);
  119. let weeks = [];
  120. if (weekMatch) {
  121. weeks = parseWeeks(weekMatch[1]);
  122. }
  123. if (weeks.length === 0) {
  124. console.warn('[WARN] 无法提取周次:', firstLine);
  125. return null;
  126. }
  127. // 提取节次
  128. let startSection = 0;
  129. let endSection = 0;
  130. const sectionRangeMatch = firstLine.match(/[((]第(\d+)[-~](\d+)节[))]/);
  131. if (sectionRangeMatch) {
  132. startSection = parseInt(sectionRangeMatch[1]);
  133. endSection = parseInt(sectionRangeMatch[2]);
  134. } else {
  135. const singleSectionMatch = firstLine.match(/[((]第(\d+)节[))]/);
  136. if (singleSectionMatch) {
  137. startSection = endSection = parseInt(singleSectionMatch[1]);
  138. }
  139. }
  140. // ========== 后续行:教师和地点 ==========
  141. let teacher = '';
  142. let position = '';
  143. // 简单逻辑:第二行是教师,第三行是地点
  144. if (lines.length > 1) {
  145. const secondLine = lines[1];
  146. // 检查是否是教师名(通常是汉字,且不包含"楼"等地点关键词)
  147. if (secondLine && /[\u4e00-\u9fa5]/.test(secondLine) && !/[楼号室厅]/.test(secondLine)) {
  148. teacher = secondLine;
  149. } else if (secondLine && /[楼号室厅]/.test(secondLine)) {
  150. // 第二行看起来是地点
  151. position = secondLine;
  152. } else {
  153. // 其他情况作为教师
  154. teacher = secondLine;
  155. }
  156. }
  157. if (lines.length > 2) {
  158. const thirdLine = lines[2];
  159. // 如果第三行看起来是地点,就作为地点
  160. if (thirdLine && /[楼号室厅]/.test(thirdLine)) {
  161. position = thirdLine;
  162. } else if (thirdLine && !teacher) {
  163. // 如果还没有教师,就作为教师
  164. teacher = thirdLine;
  165. } else if (thirdLine && !position) {
  166. // 否则作为地点
  167. position = thirdLine;
  168. }
  169. }
  170. // 如果还有第四行,作为地点
  171. if (lines.length > 3 && !position) {
  172. position = lines[3];
  173. }
  174. console.log(`[DEBUG] 解析: 名="${courseName}", 师="${teacher}", 地="${position}", 周=${weeks.join(',')}, 节=${startSection}-${endSection}`);
  175. return {
  176. name: courseName,
  177. teacher: teacher || '',
  178. position: position || '未指定',
  179. startSection: startSection,
  180. endSection: endSection,
  181. weeks: weeks
  182. };
  183. } catch (error) {
  184. console.error('[ERROR] 解析课程出错:', error);
  185. return null;
  186. }
  187. }
  188. /**
  189. * 从单个单元格中提取所有课程(支持 <hr> 分隔的多个课程)
  190. */
  191. function extractCoursesFromCell(cellElement, dayIndex) {
  192. if (!cellElement) return [];
  193. try {
  194. const cellHTML = cellElement.innerHTML || '';
  195. const cellText = cellElement.textContent || '';
  196. if (!cellText || cellText.trim() === '' || cellText === '&nbsp;') {
  197. return [];
  198. }
  199. // 按 <hr> 分割
  200. const courseParts = cellHTML.split(/<hr\s*\/?>/i);
  201. const courses = [];
  202. console.log(`[DEBUG] 单元格分解为 ${courseParts.length} 个课程块`);
  203. courseParts.forEach((part, idx) => {
  204. const courseInfo = parseSingleCourse(part);
  205. if (courseInfo) {
  206. courseInfo.day = dayIndex + 1;
  207. courses.push(courseInfo);
  208. console.log(`[DEBUG] 块${idx + 1}: ${courseInfo.name}`);
  209. }
  210. });
  211. return courses;
  212. } catch (error) {
  213. console.error('[ERROR] 提取单元格课程失败:', error);
  214. return [];
  215. }
  216. }
  217. /**
  218. * 从表格中提取所有课程
  219. */
  220. function extractCoursesFromTable() {
  221. const courses = [];
  222. const courseMap = new Map();
  223. try {
  224. const table = document.querySelector('table.CourseFormTable');
  225. if (!table) {
  226. console.error('[ERROR] 找不到课程表');
  227. return null;
  228. }
  229. const rows = Array.from(table.rows);
  230. if (rows.length < 2) {
  231. console.error('[ERROR] 表格行数不足');
  232. return null;
  233. }
  234. console.log(`[INFO] 开始解析课程表(共 ${rows.length} 行)`);
  235. const headerRow = rows[0];
  236. const headers = Array.from(headerRow.cells).map(cell => cell.textContent.trim());
  237. const dayColumns = headers.slice(2);
  238. const pendingRowspans = new Array(dayColumns.length).fill(0);
  239. console.log(`[INFO] 日期列: ${dayColumns.join(', ')}`);
  240. // 遍历数据行
  241. for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) {
  242. const row = rows[rowIndex];
  243. const cells = Array.from(row.cells);
  244. if (cells.length === 0) continue;
  245. // 检查"未安排时间课程"部分
  246. const captionCell = cells.find(cell => cell.querySelector('table.NoFitCourse'));
  247. if (captionCell) {
  248. console.log('[INFO] 检测到未安排课程表');
  249. const unscheduledCourses = extractUnscheduledCourses(captionCell);
  250. if (unscheduledCourses) {
  251. courses.push(...unscheduledCourses);
  252. }
  253. break;
  254. }
  255. // 获取行的节次信息
  256. const sectionCell = cells[1];
  257. let dayStartSection = 0;
  258. if (sectionCell) {
  259. const sectionText = sectionCell.textContent.trim();
  260. const sectionMatch = sectionText.match(/第(\d+)节/);
  261. if (sectionMatch) {
  262. dayStartSection = parseInt(sectionMatch[1]);
  263. }
  264. }
  265. const dayCells = cells.slice(2);
  266. let dayCellPointer = 0;
  267. // 遍历每天的课程,跳过被上方 rowspan 占用的列
  268. for (let dayIndex = 0; dayIndex < dayColumns.length; dayIndex++) {
  269. if (pendingRowspans[dayIndex] > 0) {
  270. pendingRowspans[dayIndex]--;
  271. continue;
  272. }
  273. const courseCell = dayCells[dayCellPointer];
  274. if (!courseCell) continue;
  275. dayCellPointer++;
  276. const cellCourses = extractCoursesFromCell(courseCell, dayIndex);
  277. cellCourses.forEach(courseInfo => {
  278. if (courseInfo.startSection === 0 && courseInfo.endSection === 0) {
  279. courseInfo.startSection = dayStartSection;
  280. courseInfo.endSection = dayStartSection;
  281. }
  282. const courseKey = `${courseInfo.day}-${courseInfo.name}-${courseInfo.teacher}-${courseInfo.position}-${courseInfo.weeks.join(',')}`;
  283. if (courseMap.has(courseKey)) {
  284. const existing = courseMap.get(courseKey);
  285. existing.startSection = Math.min(existing.startSection, courseInfo.startSection);
  286. existing.endSection = Math.max(existing.endSection, courseInfo.endSection);
  287. } else {
  288. courseMap.set(courseKey, courseInfo);
  289. }
  290. });
  291. const rowspan = Math.max(parseInt(courseCell.getAttribute('rowspan') || '1', 10), 1);
  292. const colspan = Math.max(parseInt(courseCell.getAttribute('colspan') || '1', 10), 1);
  293. if (rowspan > 1) {
  294. for (let offset = 0; offset < colspan && dayIndex + offset < dayColumns.length; offset++) {
  295. pendingRowspans[dayIndex + offset] = Math.max(
  296. pendingRowspans[dayIndex + offset],
  297. rowspan - 1
  298. );
  299. }
  300. }
  301. if (colspan > 1) {
  302. dayIndex += colspan - 1;
  303. }
  304. }
  305. }
  306. const courseList = Array.from(courseMap.values());
  307. courseList.sort((a, b) => {
  308. if (a.day !== b.day) return a.day - b.day;
  309. if (a.startSection !== b.startSection) return a.startSection - b.startSection;
  310. return a.endSection - b.endSection;
  311. });
  312. courses.push(...courseList);
  313. console.log(`[INFO] ✓ 成功提取 ${courses.length} 门课程`);
  314. return courses;
  315. } catch (error) {
  316. console.error('[ERROR] 解析课程表失败:', error);
  317. return null;
  318. }
  319. }
  320. /**
  321. * 提取未安排时间的课程
  322. */
  323. function extractUnscheduledCourses(element) {
  324. try {
  325. const table = element.querySelector('table.NoFitCourse');
  326. if (!table) return null;
  327. const courses = [];
  328. const rows = table.querySelectorAll('tbody tr');
  329. console.log(`[INFO] 未安排课程表有 ${rows.length} 行`);
  330. rows.forEach((row) => {
  331. const cells = row.querySelectorAll('td');
  332. if (cells.length >= 3) {
  333. const courseName = cells[0].textContent.trim();
  334. const weekStr = cells[1].textContent.trim();
  335. const teacher = cells[2].textContent.trim();
  336. const weeks = parseWeeks(weekStr);
  337. if (courseName && weeks.length > 0) {
  338. courses.push({
  339. name: courseName,
  340. teacher: teacher,
  341. position: '待定',
  342. day: 0,
  343. startSection: 0,
  344. endSection: 0,
  345. weeks: weeks
  346. });
  347. console.log(`[INFO] 未安排课程: ${courseName}`);
  348. }
  349. }
  350. });
  351. return courses.length > 0 ? courses : null;
  352. } catch (error) {
  353. console.error('[ERROR] 解析未安排课程失败:', error);
  354. return null;
  355. }
  356. }
  357. /**
  358. * 生成时间段配置
  359. */
  360. function generateTimeSlots() {
  361. return [
  362. { "number": 1, "startTime": "08:00", "endTime": "08:45" },
  363. { "number": 2, "startTime": "08:55", "endTime": "09:40" },
  364. { "number": 3, "startTime": "10:00", "endTime": "10:45" },
  365. { "number": 4, "startTime": "10:55", "endTime": "11:40" },
  366. { "number": 5, "startTime": "14:10", "endTime": "14:55" },
  367. { "number": 6, "startTime": "15:05", "endTime": "15:50" },
  368. { "number": 7, "startTime": "16:00", "endTime": "16:45" },
  369. { "number": 8, "startTime": "16:55", "endTime": "17:40" },
  370. { "number": 9, "startTime": "18:40", "endTime": "19:25" },
  371. { "number": 10, "startTime": "19:30", "endTime": "20:15" },
  372. { "number": 11, "startTime": "20:20", "endTime": "21:05" }
  373. ];
  374. }
  375. // ========== 第二部分:业务函数 ==========
  376. /**
  377. * 业务函数: 从页面获取课程数据
  378. */
  379. async function fetchCoursesFromPage() {
  380. console.log('\n[步骤1] 开始从页面提取课程数据...');
  381. try {
  382. const courses = extractCoursesFromTable();
  383. if (!courses || courses.length === 0) {
  384. console.error('[ERROR] 未找到课程数据');
  385. return null;
  386. }
  387. console.log(`[步骤1] ✓ 成功提取 ${courses.length} 门课程\n`);
  388. console.log('课程详情:');
  389. courses.forEach((c, i) => {
  390. console.log(` ${i + 1}. ${c.name} | 师:${c.teacher} | 地:${c.position} | 周:${c.weeks.join(',')} | 第${c.startSection}-${c.endSection}节 | 星期${c.day}`);
  391. });
  392. console.log();
  393. return courses;
  394. } catch (error) {
  395. console.error('[步骤1] ✗ 提取课程失败:', error);
  396. throw error;
  397. }
  398. }
  399. /**
  400. * 业务函数: 显示确认弹窗
  401. */
  402. async function showConfirmDialog(courseCount) {
  403. console.log('[步骤2] 显示确认弹窗...');
  404. try {
  405. const confirmed = await window.AndroidBridgePromise.showAlert(
  406. "导入课程表",
  407. `检测到 ${courseCount} 门课程,是否导入?`,
  408. "确认导入"
  409. );
  410. if (confirmed) {
  411. console.log('[步骤2] ✓ 用户确认导入\n');
  412. return true;
  413. } else {
  414. console.log('[步骤2] ✗ 用户取消导入\n');
  415. return false;
  416. }
  417. } catch (error) {
  418. console.error('[步骤2] ✗ 显示弹窗失败:', error);
  419. throw error;
  420. }
  421. }
  422. /**
  423. * 业务函数: 保存课程
  424. */
  425. async function saveCourses(courses) {
  426. console.log('[步骤3] 开始保存课程数据...');
  427. try {
  428. AndroidBridge.showToast('正在保存课程...');
  429. const result = await window.AndroidBridgePromise.saveImportedCourses(
  430. JSON.stringify(courses)
  431. );
  432. if (result === true) {
  433. console.log(`[步骤3] ✓ 成功保存 ${courses.length} 门课程\n`);
  434. AndroidBridge.showToast(`成功导入 ${courses.length} 门课程!`);
  435. return true;
  436. } else {
  437. console.error('[步骤3] ✗ 课程保存失败');
  438. AndroidBridge.showToast('课程保存失败');
  439. throw new Error('课程保存失败');
  440. }
  441. } catch (error) {
  442. console.error('[步骤3] ✗ 保存课程出错:', error);
  443. throw error;
  444. }
  445. }
  446. /**
  447. * 业务函数: 保存时间段配置
  448. */
  449. async function saveTimeSlots() {
  450. console.log('[步骤4] 开始保存时间段配置...');
  451. try {
  452. AndroidBridge.showToast('正在保存时间段配置...');
  453. const timeSlots = generateTimeSlots();
  454. const result = await window.AndroidBridgePromise.savePresetTimeSlots(
  455. JSON.stringify(timeSlots)
  456. );
  457. if (result === true) {
  458. console.log('[步骤4] ✓ 时间段配置保存成功\n');
  459. AndroidBridge.showToast('时间段配置成功!');
  460. return true;
  461. } else {
  462. console.error('[步骤4] ✗ 时间段配置保存失败');
  463. AndroidBridge.showToast('时间段配置失败');
  464. throw new Error('时间段配置失败');
  465. }
  466. } catch (error) {
  467. console.error('[步骤4] ✗ 保存时间段出错:', error);
  468. throw error;
  469. }
  470. }
  471. // ========== 第三部分:流程控制树 ==========
  472. /**
  473. * 主流程: 导入课程表
  474. */
  475. async function runImportFlow() {
  476. console.log('\n╔════════════════════════════════════════╗');
  477. console.log('║ 开始导入中南民族大学课程表 ║');
  478. console.log('╚════════════════════════════════════════╝\n');
  479. try {
  480. const courses = await fetchCoursesFromPage();
  481. if (!courses) {
  482. AndroidBridge.showToast('未找到课程数据');
  483. console.log('❌ 流程终止: 无课程数据\n');
  484. return false;
  485. }
  486. const userConfirmed = await showConfirmDialog(courses.length);
  487. if (!userConfirmed) {
  488. console.log('❌ 流程终止: 用户取消导入\n');
  489. return false;
  490. }
  491. const coursesSaved = await saveCourses(courses);
  492. if (!coursesSaved) {
  493. console.log('❌ 流程终止: 课程保存失败\n');
  494. return false;
  495. }
  496. const timeSlotsSaved = await saveTimeSlots();
  497. if (!timeSlotsSaved) {
  498. console.log('❌ 流程终止: 时间段配置失败\n');
  499. return false;
  500. }
  501. console.log('[步骤5] 发送完成信号...');
  502. AndroidBridge.notifyTaskCompletion();
  503. AndroidBridge.showToast('课程表导入完成!');
  504. console.log('\n╔════════════════════════════════════════╗');
  505. console.log('║ 导入流程完成 ✓ ║');
  506. console.log('╚════════════════════════════════════════╝\n');
  507. return true;
  508. } catch (error) {
  509. console.error('\n❌ 导入流程出错:', error);
  510. console.log('╚════════════════════════════════════════╝\n');
  511. AndroidBridge.showToast('导入失败: ' + error.message);
  512. return false;
  513. }
  514. }
  515. // ========== 第四部分:程序入口 ==========
  516. if (isOnSchedulePage() || document.querySelector('table.CourseFormTable')) {
  517. console.log('✓ 检测到中南民族大学教务系统课程表页面');
  518. setTimeout(() => {
  519. runImportFlow();
  520. }, 1000);
  521. } else {
  522. console.log('✗ 当前不在课程表页面');
  523. AndroidBridge.showToast('请先在教务系统打开课程表页面!');
  524. }