HUAT.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595
  1. // 文件: school.js - 修复节次错误
  2. // ==================== 验证函数 ====================
  3. function validateDate(dateStr) {
  4. if (!dateStr || dateStr.trim().length === 0) {
  5. return "日期不能为空!";
  6. }
  7. const datePattern = /^\d{4}-\d{2}-\d{2}$/;
  8. if (!datePattern.test(dateStr)) {
  9. return "日期格式必须是 YYYY-MM-DD!";
  10. }
  11. const parts = dateStr.split('-');
  12. const year = parseInt(parts[0]);
  13. const month = parseInt(parts[1]);
  14. const day = parseInt(parts[2]);
  15. const date = new Date(year, month - 1, day);
  16. if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
  17. return "请输入有效的日期!";
  18. }
  19. return false;
  20. }
  21. function validateName(name) {
  22. if (name === null || name.trim().length === 0) {
  23. return "输入不能为空!";
  24. }
  25. if (name.length < 2) {
  26. return "至少需要2个字符!";
  27. }
  28. return false;
  29. }
  30. // ==================== 课表数据提取函数 ====================
  31. /**
  32. * 从页面提取课表数据 - 修复节次错误
  33. */
  34. function extractCourseData() {
  35. console.log("开始提取湖北汽车工业学院课表数据...");
  36. const courses = [];
  37. // 获取所有课表行 - 使用更精确的选择器
  38. const rows = document.querySelectorAll('.el-table__body-wrapper tbody tr');
  39. console.log(`找到 ${rows.length} 行课表数据`);
  40. // 获取时间标签,用于验证行对应的节次
  41. const timeLabels = document.querySelectorAll('.el-table__header-wrapper th .cell, .el-table__header-wrapper th span');
  42. console.log("时间标签:", Array.from(timeLabels).map(el => el.textContent));
  43. rows.forEach((row, rowIndex) => {
  44. // 获取该行的所有单元格
  45. const cells = row.querySelectorAll('td');
  46. // 从第1个单元格开始是周一至周日(跳过第0个时间单元格)
  47. for (let dayIndex = 1; dayIndex < cells.length; dayIndex++) {
  48. const cell = cells[dayIndex];
  49. // 星期映射修正:第1列=周一(0), 第2列=周二(1), ... 第7列=周日(6)
  50. const day = dayIndex - 1;
  51. // 查找单元格内的所有课程块 - 使用更通用的选择器
  52. const courseBlocks = cell.querySelectorAll('[class*="theory"], .theory, [class*="course"], div[style*="background"]');
  53. if (courseBlocks.length > 0) {
  54. courseBlocks.forEach(block => {
  55. try {
  56. const course = parseCourseBlock(block, day, rowIndex);
  57. if (course) {
  58. courses.push(course);
  59. }
  60. } catch (e) {
  61. console.error("解析课程块失败:", e);
  62. }
  63. });
  64. }
  65. }
  66. });
  67. // 如果没找到课程,尝试另一种选择器
  68. if (courses.length === 0) {
  69. console.log("尝试使用备用选择器...");
  70. const allCourseElements = document.querySelectorAll('[class*="theory"], .theory, [class*="course"]');
  71. console.log(`找到 ${allCourseElements.length} 个可能的课程元素`);
  72. allCourseElements.forEach(element => {
  73. // 尝试找到元素所在的单元格和行
  74. const td = element.closest('td');
  75. if (td) {
  76. const tr = td.closest('tr');
  77. if (tr) {
  78. const rowIndex = Array.from(tr.parentNode.children).indexOf(tr);
  79. const dayIndex = Array.from(td.parentNode.children).indexOf(td);
  80. if (dayIndex >= 1 && dayIndex <= 7) {
  81. // 星期映射修正:第1列=周一(0), 第2列=周二(1), ... 第7列=周日(6)
  82. const day = dayIndex - 1;
  83. const course = parseCourseBlock(element, day, rowIndex);
  84. if (course) {
  85. courses.push(course);
  86. }
  87. }
  88. }
  89. }
  90. });
  91. }
  92. // 去重
  93. const uniqueCourses = removeDuplicates(courses);
  94. // 按星期和节次排序
  95. uniqueCourses.sort((a, b) => {
  96. if (a.day !== b.day) return a.day - b.day;
  97. return a.startSection - b.startSection;
  98. });
  99. console.log(`共提取到 ${uniqueCourses.length} 门课程`);
  100. uniqueCourses.forEach(c => {
  101. console.log(`星期${c.day+1}: ${c.name} - ${c.teacher} - ${c.position} - 第${c.startSection}-${c.endSection}节`);
  102. });
  103. return uniqueCourses;
  104. }
  105. /**
  106. * 解析课程块 - 修复节次映射
  107. */
  108. function parseCourseBlock(block, day, rowIndex) {
  109. // 获取所有子元素
  110. const children = block.children;
  111. let name = '', teacher = '', position = '', weeks = [];
  112. // 方法1:通过子元素解析
  113. if (children.length >= 3) {
  114. // 课程名称通常在h3标签中
  115. const nameElement = block.querySelector('h3');
  116. if (nameElement) {
  117. name = nameElement.innerText.trim();
  118. // 只移除括号内是纯数字且不是课程名称标识的情况
  119. // 但保留(24)这种课程代码
  120. // 如果括号内是纯数字且长度大于2,可能是周次信息,否则保留
  121. name = name.replace(/[((]\d+[))]/g, function(match) {
  122. // 提取括号内的数字
  123. const num = match.replace(/[(())]/g, '');
  124. // 如果数字大于30,可能是周次信息,移除;否则保留(如24是课程代码)
  125. if (parseInt(num) > 30) {
  126. return '';
  127. }
  128. return match;
  129. }).trim();
  130. }
  131. // 教师信息通常在第一个p标签中
  132. const teacherElement = block.querySelector('p:first-child span:first-child, p:nth-child(1) span');
  133. if (teacherElement) {
  134. teacher = teacherElement.innerText.trim();
  135. // 移除课时信息(如"2H")
  136. teacher = teacher.replace(/\d+H?$/, '').trim();
  137. }
  138. // 周次和教室信息通常在第二个p标签中
  139. const weekPositionElement = block.querySelector('p:nth-child(2) span, p:last-child span');
  140. if (weekPositionElement) {
  141. const text = weekPositionElement.innerText.trim();
  142. const result = parseWeekAndPosition(text);
  143. weeks = result.weeks;
  144. position = result.position;
  145. }
  146. }
  147. // 方法2:如果子元素解析失败,通过文本行解析
  148. if (!name || !teacher || !position) {
  149. const text = block.innerText;
  150. const lines = text.split('\n').filter(line => line.trim());
  151. if (lines.length >= 3) {
  152. // 第1行:课程名称
  153. if (!name) {
  154. name = lines[0].trim();
  155. // 只移除括号内是纯数字且不是课程名称标识的情况
  156. name = name.replace(/[((]\d+[))]/g, function(match) {
  157. const num = match.replace(/[(())]/g, '');
  158. if (parseInt(num) > 30) {
  159. return '';
  160. }
  161. return match;
  162. }).trim();
  163. }
  164. // 第2行:教师信息
  165. if (!teacher && lines[1]) {
  166. teacher = lines[1].trim();
  167. teacher = teacher.replace(/\d+H?$/, '').trim();
  168. }
  169. // 第3行:周次和教室
  170. if (!position && lines[2]) {
  171. const result = parseWeekAndPosition(lines[2].trim());
  172. weeks = result.weeks;
  173. position = result.position;
  174. }
  175. }
  176. }
  177. // 如果还是没有找到教室,尝试从整个块中提取数字教室
  178. if (!position || position === '未知教室') {
  179. position = extractClassroom(block.innerText);
  180. }
  181. // 节次映射彻底修正:按行索引重新定义startSection和endSection
  182. let startSection, endSection;
  183. switch(rowIndex) {
  184. case 0: // 第1行:第1节
  185. startSection = 1;
  186. endSection = 1;
  187. break;
  188. case 1: // 第2行:第2节
  189. startSection = 2;
  190. endSection = 2;
  191. break;
  192. case 2: // 第3行:第3节
  193. startSection = 3;
  194. endSection = 3;
  195. break;
  196. case 3: // 第4行:第4节
  197. startSection = 4;
  198. endSection = 4;
  199. break;
  200. case 4: // 第5行:第5节
  201. startSection = 5;
  202. endSection = 5;
  203. break;
  204. case 5: // 第6行:第6节
  205. startSection = 6;
  206. endSection = 6;
  207. break;
  208. case 6: // 第7行:第7节
  209. startSection = 7;
  210. endSection = 7;
  211. break;
  212. case 7: // 第8行:第8节
  213. startSection = 8;
  214. endSection = 8;
  215. break;
  216. case 8: // 第9行:第9节
  217. startSection = 9;
  218. endSection = 9;
  219. break;
  220. case 9: // 第10行:第10节
  221. startSection = 10;
  222. endSection = 10;
  223. break;
  224. case 10: // 第11行:第11节
  225. startSection = 11;
  226. endSection = 11;
  227. break;
  228. default: // 默认第1节
  229. startSection = 1;
  230. endSection = 1;
  231. }
  232. // 检查是否有rowspan(连堂课程)- 修正连堂节次计算逻辑
  233. const td = block.closest('td');
  234. if (td) {
  235. const rowspan = td.getAttribute('rowspan');
  236. if (rowspan) {
  237. const span = parseInt(rowspan);
  238. if (span > 1) {
  239. // 连堂时,结束节次 = 开始节次 + 跨行数 - 1
  240. endSection = startSection + span - 1;
  241. // 限制节次最大值不超过11
  242. if (endSection > 11) endSection = 11;
  243. }
  244. }
  245. }
  246. // 只有提取到有效的课程名称才返回
  247. if (!name || name.includes('节') || name.length < 2 || name.includes('理论课')) {
  248. return null;
  249. }
  250. // 特殊处理体育课(可能没有教室)
  251. if (name.includes('体育') && position === '未知教室') {
  252. position = '操场';
  253. }
  254. const course = {
  255. name: name,
  256. teacher: teacher || '未知教师',
  257. position: position || '未知教室',
  258. day: day,
  259. startSection: startSection,
  260. endSection: endSection,
  261. weeks: weeks.length > 0 ? weeks : [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20],
  262. isCustomTime: false
  263. };
  264. return course;
  265. }
  266. /**
  267. * 解析周次和教室
  268. */
  269. function parseWeekAndPosition(text) {
  270. let weeks = [];
  271. let position = '未知教室';
  272. if (!text) return { weeks, position };
  273. console.log("解析周次和教室:", text);
  274. // 匹配周次模式:如 "3-16周"、"1-12周"、"6-8周"
  275. const weekPattern = /(\d+-\d+周|\d+,\d+周|\d+周)/;
  276. const weekMatch = text.match(weekPattern);
  277. if (weekMatch) {
  278. const weekStr = weekMatch[1];
  279. weeks = parseWeeks(weekStr);
  280. // 剩余部分可能是教室
  281. let remaining = text.replace(weekStr, '').trim();
  282. // 如果剩余部分包含数字,很可能是教室
  283. if (remaining && /\d+/.test(remaining)) {
  284. position = remaining;
  285. } else {
  286. // 尝试从原文本中提取教室(通常是4位数字)
  287. const roomMatch = text.match(/\b\d{3,4}\b/);
  288. if (roomMatch) {
  289. position = roomMatch[0];
  290. }
  291. }
  292. } else {
  293. // 如果没有周次信息,直接尝试提取教室
  294. const roomMatch = text.match(/\b\d{3,4}\b/);
  295. if (roomMatch) {
  296. position = roomMatch[0];
  297. weeks = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];
  298. }
  299. }
  300. // 清理教室字符串
  301. if (position && position !== '未知教室') {
  302. // 只保留数字
  303. position = position.replace(/[^\d]/g, '');
  304. }
  305. return { weeks, position };
  306. }
  307. /**
  308. * 从文本中提取教室
  309. */
  310. function extractClassroom(text) {
  311. // 匹配常见的教室格式:1205、6604、1231、2303、6403、6212、2103、1233、6104、1203
  312. const roomPatterns = [
  313. /\b\d{4}\b/, // 4位数字
  314. /\b\d{3}\b/, // 3位数字
  315. /[0-9]{3,4}/ // 任意3-4位数字
  316. ];
  317. for (let pattern of roomPatterns) {
  318. const match = text.match(pattern);
  319. if (match) {
  320. return match[0];
  321. }
  322. }
  323. return '未知教室';
  324. }
  325. /**
  326. * 解析周次字符串
  327. */
  328. function parseWeeks(weekStr) {
  329. const weeks = [];
  330. if (!weekStr) return [];
  331. // 移除"周"字
  332. weekStr = weekStr.replace(/周/g, '').trim();
  333. try {
  334. if (weekStr.includes(',')) {
  335. weekStr.split(',').forEach(part => {
  336. if (part.includes('-')) {
  337. const [start, end] = part.split('-').map(Number);
  338. for (let i = start; i <= end; i++) weeks.push(i);
  339. } else {
  340. const w = parseInt(part);
  341. if (!isNaN(w)) weeks.push(w);
  342. }
  343. });
  344. } else if (weekStr.includes('-')) {
  345. const [start, end] = weekStr.split('-').map(Number);
  346. for (let i = start; i <= end; i++) weeks.push(i);
  347. } else {
  348. const w = parseInt(weekStr);
  349. if (!isNaN(w)) weeks.push(w);
  350. }
  351. } catch (e) {
  352. console.error("解析周次失败:", weekStr, e);
  353. }
  354. return weeks.length > 0 ? weeks.sort((a,b) => a-b) : [];
  355. }
  356. /**
  357. * 去重
  358. */
  359. function removeDuplicates(courses) {
  360. const seen = new Map();
  361. return courses.filter(course => {
  362. // 创建唯一键:课程名+教师+星期+开始节次
  363. const key = `${course.name}-${course.teacher}-${course.day}-${course.startSection}`;
  364. if (seen.has(key)) {
  365. return false;
  366. }
  367. seen.set(key, true);
  368. return true;
  369. });
  370. }
  371. /**
  372. * 提取学期信息
  373. */
  374. function extractSemesterInfo() {
  375. return {
  376. semesterStartDate: "2026-03-01",
  377. semesterTotalWeeks: 20
  378. };
  379. }
  380. // ==================== 弹窗和导入函数 ====================
  381. async function demoAlert() {
  382. try {
  383. const confirmed = await window.AndroidBridgePromise.showAlert(
  384. "📚 湖北汽车工业学院课表导入",
  385. "将提取当前页面的课表数据并导入到App\n\n" +
  386. "📌 请确认已在课表页面\n" +
  387. "📌 将提取所有可见课程",
  388. "开始导入",
  389. "取消"
  390. );
  391. return confirmed;
  392. } catch (error) {
  393. console.error("显示弹窗错误:", error);
  394. return false;
  395. }
  396. }
  397. async function demoPrompt() {
  398. try {
  399. const semesterInfo = extractSemesterInfo();
  400. const semesterStart = await window.AndroidBridgePromise.showPrompt(
  401. "📅 设置开学日期",
  402. "请输入本学期开学日期",
  403. semesterInfo.semesterStartDate,
  404. "validateDate"
  405. );
  406. return semesterStart || semesterInfo.semesterStartDate;
  407. } catch (error) {
  408. console.error("日期输入错误:", error);
  409. return "2026-03-01";
  410. }
  411. }
  412. /**
  413. * 导入预设时间段 - 新增函数
  414. * 用于导入测试用的11个时间段(每个时间段1分钟)
  415. */
  416. async function importPresetTimeSlots() {
  417. console.log("正在准备预设时间段数据...");
  418. const presetTimeSlots = [
  419. { "number": 1, "startTime": "08:10", "endTime": "08:55" },
  420. { "number": 2, "startTime": "09:00", "endTime": "09:45" },
  421. { "number": 3, "startTime": "10:05", "endTime": "10:50" },
  422. { "number": 4, "startTime": "10:55", "endTime": "11:40" },
  423. { "number": 5, "startTime": "14:30", "endTime": "15:15" },
  424. { "number": 6, "startTime": "15:20", "endTime": "16:05" },
  425. { "number": 7, "startTime": "16:25", "endTime": "17:10" },
  426. { "number": 8, "startTime": "17:15", "endTime": "18:00" },
  427. { "number": 9, "startTime": "18:45", "endTime": "19:30" },
  428. { "number": 10, "startTime": "19:35", "endTime": "20:20" },
  429. { "number": 11, "startTime": "20:25", "endTime": "21:10" }
  430. ];
  431. try {
  432. console.log("正在尝试导入预设时间段...");
  433. const result = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots));
  434. if (result === true) {
  435. console.log("预设时间段导入成功!");
  436. window.AndroidBridge.showToast("测试时间段导入成功!");
  437. return true;
  438. } else {
  439. console.log("预设时间段导入未成功,结果:" + result);
  440. window.AndroidBridge.showToast("测试时间段导入失败,请查看日志。");
  441. return false;
  442. }
  443. } catch (error) {
  444. console.error("导入时间段时发生错误:", error);
  445. window.AndroidBridge.showToast("导入时间段失败: " + error.message);
  446. return false;
  447. }
  448. }
  449. async function importSchedule() {
  450. try {
  451. AndroidBridge.showToast("正在提取课表数据...");
  452. const courses = extractCourseData();
  453. if (courses.length === 0) {
  454. await window.AndroidBridgePromise.showAlert(
  455. "⚠️ 提取失败",
  456. "未找到课表数据,请确认已在课表页面",
  457. "知道了"
  458. );
  459. return false;
  460. }
  461. // 预览
  462. const preview = await window.AndroidBridgePromise.showAlert(
  463. "📊 数据预览",
  464. `共找到 ${courses.length} 门课程\n\n` +
  465. `示例:\n${courses.slice(0, 5).map(c =>
  466. `• 周${c.day+1} ${c.name} - 第${c.startSection}-${c.endSection}节`
  467. ).join('\n')}`,
  468. "确认导入",
  469. "取消"
  470. );
  471. if (!preview) {
  472. return false;
  473. }
  474. // 导入课程
  475. AndroidBridge.showToast("正在导入课程...");
  476. const result = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
  477. if (result === true) {
  478. const semesterDate = await demoPrompt();
  479. const configResult = await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
  480. semesterStartDate: semesterDate,
  481. semesterTotalWeeks: 20,
  482. defaultClassDuration: 45,
  483. defaultBreakDuration: 10,
  484. firstDayOfWeek: 1
  485. }));
  486. if (configResult === true) {
  487. AndroidBridge.showToast(`✅ 导入成功!共${courses.length}门课程`);
  488. return true;
  489. }
  490. }
  491. AndroidBridge.showToast("❌ 导入失败");
  492. return false;
  493. } catch (error) {
  494. console.error("导入错误:", error);
  495. AndroidBridge.showToast("导入出错: " + error.message);
  496. return false;
  497. }
  498. }
  499. async function runAllDemosSequentially() {
  500. AndroidBridge.showToast("🚀 课表导入助手启动...");
  501. // 检查页面
  502. if (!window.location.href.includes('studentHome/expectCourseTable')) {
  503. const goToPage = await window.AndroidBridgePromise.showAlert(
  504. "页面提示",
  505. "当前不在课表页面,是否跳转?",
  506. "跳转",
  507. "取消"
  508. );
  509. if (goToPage) {
  510. window.location.href = 'http://neweas.huat.edu.cn/#/studentHome/expectCourseTable';
  511. }
  512. return;
  513. }
  514. const start = await demoAlert();
  515. if (!start) {
  516. AndroidBridge.showToast("已取消");
  517. return;
  518. }
  519. // 可以选择是否导入时间段
  520. const importTimeSlots = await window.AndroidBridgePromise.showAlert(
  521. "⏰ 导入时间段",
  522. "是否要导入预设的时间段数据?\n",
  523. "导入",
  524. "跳过"
  525. );
  526. if (importTimeSlots) {
  527. await importPresetTimeSlots();
  528. }
  529. await importSchedule();
  530. AndroidBridge.notifyTaskCompletion();
  531. }
  532. // 导出函数
  533. window.validateDate = validateDate;
  534. window.validateName = validateName;
  535. window.extractCourseData = extractCourseData;
  536. window.importPresetTimeSlots = importPresetTimeSlots;
  537. // 启动
  538. runAllDemosSequentially();