// ========================================== // 文件: scuec.js // 中南民族大学教务系统课程表导入脚本 // 开发规范: 结构化编程 + async/await 流程控制树 // ========================================== // ========== 第一部分:工具函数 ========== /** * 检查是否在正确的教务系统页面 */ function isOnSchedulePage() { const url = window.location.href; return /jiaowu|jwgl|course|schedule|curriculum/i.test(url) || document.querySelector('table.CourseFormTable') !== null; } /** * 解析周次字符串 */ function parseWeeks(weekStr) { const weeks = []; if (!weekStr) return weeks; weekStr = weekStr.trim(); const isSingleWeek = weekStr.includes('(单)'); const match = weekStr.match(/(\d+)\s*[-~]\s*(\d+)|(\d+)\s*周/); if (match) { let start, end; if (match[1] && match[2]) { start = parseInt(match[1]); end = parseInt(match[2]); } else if (match[3]) { start = end = parseInt(match[3]); } else { return weeks; } if (isSingleWeek) { for (let i = start; i <= end; i += 2) { weeks.push(i); } } else { for (let i = start; i <= end; i++) { weeks.push(i); } } } return weeks; } /** * 清理文本:移除HTML标签但保留文本内容 * 特别处理空标签和多余空格 */ function cleanHTML(html) { if (!html) return ''; // 创建临时元素 const temp = document.createElement('div'); temp.innerHTML = html; // 获取纯文本 let text = temp.textContent || temp.innerText || ''; // 清理多余空格和特殊字符 text = text .replace(/ /g, ' ') // 替换 nbsp .replace(/\s+/g, ' ') // 多个空格合并为一个 .trim(); return text; } /** * 智能分割文本为行 * 支持 \n,
,
分隔符 */ function smartSplitLines(html, separator = '/i); } else if (separator === '/i); } else { parts = [html]; } // 对每个部分清理并分行 let lines = []; parts.forEach(part => { let cleaned = cleanHTML(part); if (cleaned) { // 再按空行分割 let subLines = cleaned.split(/\n+/).map(l => l.trim()).filter(l => l !== ''); lines.push(...subLines); } }); return lines; } /** * 解析单个课程信息(更加健壮) */ function parseSingleCourse(courseHTML) { if (!courseHTML || courseHTML.trim() === '') { return null; } try { // 用
分割成行 const lines = smartSplitLines(courseHTML, ' 1) { const secondLine = lines[1]; // 检查是否是教师名(通常是汉字,且不包含"楼"等地点关键词) if (secondLine && /[\u4e00-\u9fa5]/.test(secondLine) && !/[楼号室厅]/.test(secondLine)) { teacher = secondLine; } else if (secondLine && /[楼号室厅]/.test(secondLine)) { // 第二行看起来是地点 position = secondLine; } else { // 其他情况作为教师 teacher = secondLine; } } if (lines.length > 2) { const thirdLine = lines[2]; // 如果第三行看起来是地点,就作为地点 if (thirdLine && /[楼号室厅]/.test(thirdLine)) { position = thirdLine; } else if (thirdLine && !teacher) { // 如果还没有教师,就作为教师 teacher = thirdLine; } else if (thirdLine && !position) { // 否则作为地点 position = thirdLine; } } // 如果还有第四行,作为地点 if (lines.length > 3 && !position) { position = lines[3]; } console.log(`[DEBUG] 解析: 名="${courseName}", 师="${teacher}", 地="${position}", 周=${weeks.join(',')}, 节=${startSection}-${endSection}`); return { name: courseName, teacher: teacher || '', position: position || '未指定', startSection: startSection, endSection: endSection, weeks: weeks }; } catch (error) { console.error('[ERROR] 解析课程出错:', error); return null; } } /** * 从单个单元格中提取所有课程(支持
分隔的多个课程) */ function extractCoursesFromCell(cellElement, dayIndex) { if (!cellElement) return []; try { const cellHTML = cellElement.innerHTML || ''; const cellText = cellElement.textContent || ''; if (!cellText || cellText.trim() === '' || cellText === ' ') { return []; } // 按
分割 const courseParts = cellHTML.split(//i); const courses = []; console.log(`[DEBUG] 单元格分解为 ${courseParts.length} 个课程块`); courseParts.forEach((part, idx) => { const courseInfo = parseSingleCourse(part); if (courseInfo) { courseInfo.day = dayIndex + 1; courses.push(courseInfo); console.log(`[DEBUG] 块${idx + 1}: ${courseInfo.name}`); } }); return courses; } catch (error) { console.error('[ERROR] 提取单元格课程失败:', error); return []; } } /** * 从表格中提取所有课程 */ function extractCoursesFromTable() { const courses = []; const courseMap = new Map(); try { const table = document.querySelector('table.CourseFormTable'); if (!table) { console.error('[ERROR] 找不到课程表'); return null; } const rows = table.querySelectorAll('tr'); if (rows.length < 2) { console.error('[ERROR] 表格行数不足'); return null; } console.log(`[INFO] 开始解析课程表(共 ${rows.length} 行)`); const headerRow = rows[0]; const headers = Array.from(headerRow.querySelectorAll('th')).map(th => th.textContent.trim()); const dayColumns = headers.slice(2); console.log(`[INFO] 日期列: ${dayColumns.join(', ')}`); // 遍历数据行 for (let rowIndex = 1; rowIndex < rows.length; rowIndex++) { const row = rows[rowIndex]; const cells = Array.from(row.querySelectorAll('td')); if (cells.length === 0) continue; // 检查"未安排时间课程"部分 const captionCell = row.querySelector('[colspan="9"]'); if (captionCell) { console.log('[INFO] 检测到未安排课程表'); const unscheduledCourses = extractUnscheduledCourses(captionCell); if (unscheduledCourses) { courses.push(...unscheduledCourses); } break; } // 获取行的节次信息 const sectionCell = cells[1]; let dayStartSection = 0; if (sectionCell) { const sectionText = sectionCell.textContent.trim(); const sectionMatch = sectionText.match(/第(\d+)节/); if (sectionMatch) { dayStartSection = parseInt(sectionMatch[1]); } } // 遍历每天的课程 for (let dayIndex = 0; dayIndex < dayColumns.length; dayIndex++) { const cellIndex = dayIndex + 2; if (cellIndex >= cells.length) continue; const courseCell = cells[cellIndex]; if (!courseCell) continue; const cellCourses = extractCoursesFromCell(courseCell, dayIndex); cellCourses.forEach(courseInfo => { if (courseInfo.startSection === 0 && courseInfo.endSection === 0) { courseInfo.startSection = dayStartSection; courseInfo.endSection = dayStartSection; } const courseKey = `${courseInfo.day}-${courseInfo.name}-${courseInfo.teacher}-${courseInfo.position}-${courseInfo.weeks.join(',')}`; if (courseMap.has(courseKey)) { const existing = courseMap.get(courseKey); existing.startSection = Math.min(existing.startSection, courseInfo.startSection); existing.endSection = Math.max(existing.endSection, courseInfo.endSection); } else { courseMap.set(courseKey, courseInfo); } }); } } const courseList = Array.from(courseMap.values()); courseList.sort((a, b) => { if (a.day !== b.day) return a.day - b.day; if (a.startSection !== b.startSection) return a.startSection - b.startSection; return a.endSection - b.endSection; }); courses.push(...courseList); console.log(`[INFO] ✓ 成功提取 ${courses.length} 门课程`); return courses; } catch (error) { console.error('[ERROR] 解析课程表失败:', error); return null; } } /** * 提取未安排时间的课程 */ function extractUnscheduledCourses(element) { try { const table = element.querySelector('table.NoFitCourse'); if (!table) return null; const courses = []; const rows = table.querySelectorAll('tbody tr'); console.log(`[INFO] 未安排课程表有 ${rows.length} 行`); rows.forEach((row) => { const cells = row.querySelectorAll('td'); if (cells.length >= 3) { const courseName = cells[0].textContent.trim(); const weekStr = cells[1].textContent.trim(); const teacher = cells[2].textContent.trim(); const weeks = parseWeeks(weekStr); if (courseName && weeks.length > 0) { courses.push({ name: courseName, teacher: teacher, position: '待定', day: 0, startSection: 0, endSection: 0, weeks: weeks }); console.log(`[INFO] 未安排课程: ${courseName}`); } } }); return courses.length > 0 ? courses : null; } catch (error) { console.error('[ERROR] 解析未安排课程失败:', error); return null; } } /** * 生成时间段配置 */ function generateTimeSlots() { return [ { "number": 1, "startTime": "08:00", "endTime": "08:45" }, { "number": 2, "startTime": "08:55", "endTime": "09:40" }, { "number": 3, "startTime": "10:00", "endTime": "10:45" }, { "number": 4, "startTime": "10:55", "endTime": "11:40" }, { "number": 5, "startTime": "14:10", "endTime": "14:55" }, { "number": 6, "startTime": "15:05", "endTime": "15:50" }, { "number": 7, "startTime": "16:00", "endTime": "16:45" }, { "number": 8, "startTime": "16:55", "endTime": "17:40" }, { "number": 9, "startTime": "18:40", "endTime": "19:25" }, { "number": 10, "startTime": "19:30", "endTime": "20:15" }, { "number": 11, "startTime": "20:20", "endTime": "21:05" } ]; } // ========== 第二部分:业务函数 ========== /** * 业务函数: 从页面获取课程数据 */ async function fetchCoursesFromPage() { console.log('\n[步骤1] 开始从页面提取课程数据...'); try { const courses = extractCoursesFromTable(); if (!courses || courses.length === 0) { console.error('[ERROR] 未找到课程数据'); return null; } console.log(`[步骤1] ✓ 成功提取 ${courses.length} 门课程\n`); console.log('课程详情:'); courses.forEach((c, i) => { console.log(` ${i + 1}. ${c.name} | 师:${c.teacher} | 地:${c.position} | 周:${c.weeks.join(',')} | 第${c.startSection}-${c.endSection}节 | 星期${c.day}`); }); console.log(); return courses; } catch (error) { console.error('[步骤1] ✗ 提取课程失败:', error); throw error; } } /** * 业务函数: 显示确认弹窗 */ async function showConfirmDialog(courseCount) { console.log('[步骤2] 显示确认弹窗...'); try { const confirmed = await window.AndroidBridgePromise.showAlert( "导入课程表", `检测到 ${courseCount} 门课程,是否导入?`, "确认导入" ); if (confirmed) { console.log('[步骤2] ✓ 用户确认导入\n'); return true; } else { console.log('[步骤2] ✗ 用户取消导入\n'); return false; } } catch (error) { console.error('[步骤2] ✗ 显示弹窗失败:', error); throw error; } } /** * 业务函数: 保存课程 */ async function saveCourses(courses) { console.log('[步骤3] 开始保存课程数据...'); try { AndroidBridge.showToast('正在保存课程...'); const result = await window.AndroidBridgePromise.saveImportedCourses( JSON.stringify(courses) ); if (result === true) { console.log(`[步骤3] ✓ 成功保存 ${courses.length} 门课程\n`); AndroidBridge.showToast(`成功导入 ${courses.length} 门课程!`); return true; } else { console.error('[步骤3] ✗ 课程保存失败'); AndroidBridge.showToast('课程保存失败'); throw new Error('课程保存失败'); } } catch (error) { console.error('[步骤3] ✗ 保存课程出错:', error); throw error; } } /** * 业务函数: 保存时间段配置 */ async function saveTimeSlots() { console.log('[步骤4] 开始保存时间段配置...'); try { AndroidBridge.showToast('正在保存时间段配置...'); const timeSlots = generateTimeSlots(); const result = await window.AndroidBridgePromise.savePresetTimeSlots( JSON.stringify(timeSlots) ); if (result === true) { console.log('[步骤4] ✓ 时间段配置保存成功\n'); AndroidBridge.showToast('时间段配置成功!'); return true; } else { console.error('[步骤4] ✗ 时间段配置保存失败'); AndroidBridge.showToast('时间段配置失败'); throw new Error('时间段配置失败'); } } catch (error) { console.error('[步骤4] ✗ 保存时间段出错:', error); throw error; } } // ========== 第三部分:流程控制树 ========== /** * 主流程: 导入课程表 */ async function runImportFlow() { console.log('\n╔════════════════════════════════════════╗'); console.log('║ 开始导入中南民族大学课程表 ║'); console.log('╚════════════════════════════════════════╝\n'); try { const courses = await fetchCoursesFromPage(); if (!courses) { AndroidBridge.showToast('未找到课程数据'); console.log('❌ 流程终止: 无课程数据\n'); return false; } const userConfirmed = await showConfirmDialog(courses.length); if (!userConfirmed) { console.log('❌ 流程终止: 用户取消导入\n'); return false; } const coursesSaved = await saveCourses(courses); if (!coursesSaved) { console.log('❌ 流程终止: 课程保存失败\n'); return false; } const timeSlotsSaved = await saveTimeSlots(); if (!timeSlotsSaved) { console.log('❌ 流程终止: 时间段配置失败\n'); return false; } console.log('[步骤5] 发送完成信号...'); AndroidBridge.notifyTaskCompletion(); AndroidBridge.showToast('课程表导入完成!'); console.log('\n╔════════════════════════════════════════╗'); console.log('║ 导入流程完成 ✓ ║'); console.log('╚════════════════════════════════════════╝\n'); return true; } catch (error) { console.error('\n❌ 导入流程出错:', error); console.log('╚════════════════════════════════════════╝\n'); AndroidBridge.showToast('导入失败: ' + error.message); return false; } } // ========== 第四部分:程序入口 ========== if (isOnSchedulePage() || document.querySelector('table.CourseFormTable')) { console.log('✓ 检测到中南民族大学教务系统课程表页面'); setTimeout(() => { runImportFlow(); }, 1000); } else { console.log('✗ 当前不在课程表页面'); AndroidBridge.showToast('请先在教务系统打开课程表页面!'); }