// ========== 工具函数 ========== /** * 解析周数字符串 * @param {string} Str 如:1-6,7-13周(单) * @returns {Array} 返回数组 [1,3,5,7,9,11,13] */ function getWeeks(Str) { function range(con, tag) { let retWeek = []; con.slice(0, -1).split(',').forEach(w => { let tt = w.split('-'); let start = parseInt(tt[0]); let end = parseInt(tt[tt.length - 1]); if (tag === 1 || tag === 2) { retWeek.push(...Array(end + 1 - start).fill(start).map((x, y) => x + y).filter(f => { return f % tag === 0; })); } else { retWeek.push(...Array(end + 1 - start).fill(start).map((x, y) => x + y).filter(v => { return v % 2 !== 0; })); } }); return retWeek; } Str = Str.replace(/[(){}|第\[\]]/g, "").replace(/到/g, "-"); let reWeek = []; let week1 = []; while (Str.search(/周|\s/) !== -1) { let index = Str.search(/周|\s/); if (Str[index + 1] === '单' || Str[index + 1] === '双') { week1.push(Str.slice(0, index + 2).replace(/周|\s/g, "")); index += 2; } else { week1.push(Str.slice(0, index + 1).replace(/周|\s/g, "")); index += 1; } Str = Str.slice(index); index = Str.search(/\d/); if (index !== -1) Str = Str.slice(index); else Str = ""; } if (Str.length !== 0) week1.push(Str); week1.forEach(v => { if (v.slice(-1) === "双") { reWeek.push(...range(v, 2)); } else if (v.slice(-1) === "单") { reWeek.push(...range(v, 3)); } else { reWeek.push(...range(v + "全", 1)); } }); return reWeek; } /** * 解析节次字符串 * @param {string} Str 如: 1-4节 或 1-2-3-4节 * @returns {Array} [1,2,3,4] */ function getSection(Str) { let reJc = []; let strArr = Str.replace("节", "").trim().split("-"); if (strArr.length <= 2) { for (let i = Number(strArr[0]); i <= Number(strArr[strArr.length - 1]); i++) { reJc.push(Number(i)); } } else { strArr.forEach(v => { reJc.push(Number(v)); }); } return reJc; } /** * 检查是否在登录页面 * @returns {boolean} */ function isLoginPage() { const url = window.location.href; // 检查URL是否包含登录页面特征 return url.includes('login') || url.includes('Login') || document.querySelector('input[type="password"]') !== null; } /** * 获取课程表HTML * @returns {string} 课程表HTML内容 */ function getScheduleHtml() { try { let html = ''; let found = false; // 首先尝试从iframe中获取 let iframes = document.getElementsByTagName('iframe'); for (const iframe of iframes) { if (iframe.src && iframe.src.search('/jsxsd/xskb/xskb_list.do') !== -1) { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; if (iframeDoc) { const kbtable = iframeDoc.getElementById('kbtable'); if (kbtable) { html = kbtable.outerHTML; found = true; break; } const contentBox = iframeDoc.getElementsByClassName('content_box')[0]; if (contentBox) { html = contentBox.outerHTML; found = true; break; } } } } // 如果iframe中没找到,尝试直接从主文档获取 if (!found) { const kbtable = document.getElementById('kbtable'); if (kbtable) { html = kbtable.outerHTML; found = true; } } if (!found || !html) { throw new Error('未找到课表元素'); } return html; } catch (error) { console.error('获取课程表HTML失败:', error); throw error; } } /** * 解析课程HTML数据 * @param {string} html 课程表HTML * @returns {Array} 课程数组 */ function parseScheduleHtml(html) { let result = []; let uniqueCourses = []; // 移到外部作用域 try { // 创建临时div来解析HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; console.log('开始解析课程表HTML...'); // 查找课程表格 const table = tempDiv.querySelector('#kbtable') || tempDiv.querySelector('table'); if (!table) { throw new Error('未找到课程表格'); } // 遍历所有行(每行是一个大节时间段) const rows = table.querySelectorAll('tr'); console.log(`找到 ${rows.length} 行`); // 用于记录已处理的div,避免重复(跨大节课程会在多行出现) const processedDivs = new Set(); rows.forEach((tr, rowIdx) => { const tds = tr.querySelectorAll('td'); // 遍历这一行的所有td列 // 注意: querySelectorAll('td')只选择td元素,不包括th // 所以td[0]就是星期一, td[1]是星期二, ..., td[6]是星期日 tds.forEach((td, colIdx) => { // 查找这个单元格里的课程内容div const hiddenDiv = td.querySelector('div.kbcontent'); // 如果没有隐藏div或内容为空,跳过 if (!hiddenDiv) { return; } // 检查是否已经处理过这个div(根据name属性去重) const divName = hiddenDiv.getAttribute('name') || hiddenDiv.getAttribute('id'); if (divName && processedDivs.has(divName)) { return; // 已处理过,跳过 } const divText = hiddenDiv.textContent.trim(); if (!divText || divText.length <= 6) { return; } // 从div的name属性提取星期信息 // name格式: "hash-星期-序号" 例如 "EBC6F96389D143DC9C53084617F9C7D2-2-1" // 其中第二部分的数字: 1=星期一, 2=星期二, ..., 7=星期日 let day = colIdx + 1; // 默认使用列索引 if (divName) { const nameParts = divName.split('-'); if (nameParts.length >= 3) { const dayFromName = parseInt(nameParts[1]); if (!isNaN(dayFromName) && dayFromName >= 1 && dayFromName <= 7) { day = dayFromName; } } } // 标记为已处理 if (divName) { processedDivs.add(divName); } console.log(`\n[行${rowIdx} 列${colIdx} 星期${day}]`); console.log(`内容预览: ${divText.substring(0, 50)}...`); // 可能包含多个课程,用 ----- 分隔 const courseSections = hiddenDiv.innerHTML.split(/-----+/); console.log(`分割成 ${courseSections.length} 个课程段`); // 用于课程段去重(避免完全相同的课程段被重复添加) const processedSections = new Set(); // 遍历每个课程段 courseSections.forEach((section, sectionIdx) => { const sectionText = section.replace(/<[^>]*>/g, '').trim(); if (!sectionText || sectionText.length < 3) { return; } // 检查是否已经处理过完全相同的课程段(内容去重) if (processedSections.has(sectionText)) { console.log(` 跳过重复课程段 ${sectionIdx + 1}`); return; } processedSections.add(sectionText); console.log(` 课程段 ${sectionIdx + 1}:`); console.log(` 原始HTML:`, section.substring(0, 200)); let course = { day: day, // 星期几(1=周一, 2=周二, ..., 7=周日) weeks: [], sections: [], name: '', teacher: '', position: '' }; // 解析HTML,按br分割成行 const lines = section.split(//i); console.log(` 分割成 ${lines.length} 行`); let firstTextLine = true; // 标记是否是第一个有效文本行 lines.forEach((line, lineIdx) => { // 跳过空行 const plainText = line.replace(/<[^>]*>/g, '').trim(); if (!plainText || plainText === ' ') { return; } console.log(` 行${lineIdx}: ${line.substring(0, 100)}`); console.log(` 纯文本: ${plainText}`); // 第一个有效文本行就是课程名(没有title属性) if (firstTextLine && !course.name) { // 移除span标签(包含调课标记如 O)但保留其他内容 let courseName = line.replace(/]*>.*?<\/span>/gi, '').trim(); // 提取纯文本 courseName = courseName.replace(/<[^>]*>/g, '').trim(); // 清理HTML实体 courseName = courseName.replace(/ /g, ' ').trim(); course.name = courseName; console.log(` ✓ 第一行作为课程名: ${course.name}`); firstTextLine = false; return; } firstTextLine = false; // 检查这一行的title属性(使用双引号) if (line.includes('title="老师"')) { course.teacher = plainText; console.log(` ✓ 匹配老师: ${course.teacher}`); } else if (line.includes('title="教室"')) { // 对于教室,需要先移除隐藏的font标签,再提取文本 const cleanLine = line.replace(/]*style="display:none;"[^>]*>.*?<\/font>/gi, ''); const cleanText = cleanLine.replace(/<[^>]*>/g, '').trim(); // 再移除可能残留的前导数字 const finalPosition = cleanText.replace(/^[\d-]+/, '').trim(); course.position = finalPosition; console.log(` ✓ 匹配教室: ${course.position}`); } else if (line.includes('title="周次(节次)"')) { console.log(` ✓ 匹配周次节次: ${plainText}`); // 解析周次: "1-18(周)[06-07节]" const weekMatch = plainText.match(/^(.+?)\(周\)/); if (weekMatch) { const weekStr = weekMatch[1]; course.weeks = getWeeks(weekStr + '周'); console.log(` -> 周: ${course.weeks}`); } // 解析节次: "[06-07节]" const sectionMatch = plainText.match(/\[(.+?)节?\]/); if (sectionMatch) { const sectionStr = sectionMatch[1]; course.sections = getSection(sectionStr + '节'); console.log(` -> 节: ${course.sections}`); } } // 如果没有找到教室,尝试从包含隐藏font的行提取 // 这行可能格式如: 01-0220北院卓媒220 else if (!course.position && line.includes('style="display:none;"')) { // 移除所有隐藏的font标签 const visibleText = line.replace(/]*style="display:none;"[^>]*>.*?<\/font>/gi, '') .replace(/<[^>]*>/g, '') .trim(); if (visibleText && visibleText.length > 0) { // 移除所有前导的数字和连字符(如 "01-0220" 或 "06-0722") // 匹配模式:开头的数字-数字组合 const cleanPosition = visibleText.replace(/^[\d-]+/, '').trim(); if (cleanPosition.length > 0) { course.position = cleanPosition; console.log(` ✓ 提取教室(清理后): ${course.position}`); } } } }); // 验证并添加课程 if (course.name && course.weeks.length > 0 && course.sections.length > 0) { course.teacher = course.teacher || "未知教师"; course.position = course.position || "未知地点"; console.log(` ✓ 完整课程:`, { name: course.name, teacher: course.teacher, position: course.position, day: course.day, weeks: `${course.weeks.length}周`, sections: course.sections }); result.push(course); } else { console.warn(` ✗ 信息不完整:`, { name: course.name || '无', teacher: course.teacher || '无', weeks: course.weeks.length, sections: course.sections.length }); } }); }); }); console.log(`\n解析完成,共得到 ${result.length} 条课程记录(去重前)`); // 合并完全相同的课程(去重) const courseKeys = new Set(); result.forEach(course => { // 生成课程唯一标识: 名称+老师+地点+星期+节次+周次 const key = `${course.name}|${course.teacher}|${course.position}|${course.day}|${course.sections.join(',')}|${course.weeks.join(',')}`; if (!courseKeys.has(key)) { courseKeys.add(key); uniqueCourses.push(course); } else { console.log(` 跳过重复课程: ${course.name} (${course.teacher})`); } }); console.log(`去重后剩余 ${uniqueCourses.length} 条课程记录`); } catch (err) { console.error('解析课程表出错:', err); throw new Error('解析课程表失败: ' + err.message); } return uniqueCourses; } /** * 转换课程数据格式以符合时光课表规范 * @param {Array} rawCourses 原始课程数据 * @returns {Array} 转换后的课程数据 */ function convertCoursesToStandardFormat(rawCourses) { const validCourses = []; rawCourses.forEach((course, index) => { try { // 处理节次:将原始格式转换为startSection和endSection let startSection = 1; let endSection = 1; if (course.sections && course.sections.length > 0) { const sections = course.sections.sort((a, b) => a - b); startSection = sections[0]; endSection = sections[sections.length - 1]; } // 验证必需字段 if (!startSection || !endSection || startSection < 1 || endSection < 1) { console.error(`课程 ${index + 1} 缺少有效的节次信息:`, course); throw new Error(`课程节次信息无效: startSection=${startSection}, endSection=${endSection}`); } if (!course.day || course.day < 1 || course.day > 7) { console.error(`课程 ${index + 1} 星期数据无效:`, course); throw new Error(`课程星期数据无效: day=${course.day}`); } if (!course.weeks || course.weeks.length === 0) { console.error(`课程 ${index + 1} 缺少周次信息:`, course); throw new Error(`课程周次信息缺失`); } const convertedCourse = { name: course.name || "未知课程", teacher: course.teacher || "未知教师", position: course.position || "未知地点", day: course.day, startSection: startSection, endSection: endSection, weeks: course.weeks }; validCourses.push(convertedCourse); } catch (err) { console.error(`转换课程 ${index + 1} 时出错:`, err.message); // 如果任何课程转换失败,抛出错误 throw new Error(`课程数据验证失败: ${err.message}`); } }); return validCourses; } /** * 生成时间段配置 * @returns {Array} 时间段数组 */ function generateTimeSlots() { // 云南财经大学默认时间配置 return [ { "number": 1, "startTime": "08:00", "endTime": "08:40" }, { "number": 2, "startTime": "08:50", "endTime": "09:30" }, { "number": 3, "startTime": "10:00", "endTime": "10:40" }, { "number": 4, "startTime": "10:50", "endTime": "11:30" }, { "number": 5, "startTime": "11:40", "endTime": "12:20" }, { "number": 6, "startTime": "14:30", "endTime": "15:10" }, { "number": 7, "startTime": "15:20", "endTime": "16:00" }, { "number": 8, "startTime": "16:30", "endTime": "17:10" }, { "number": 9, "startTime": "17:20", "endTime": "18:00" }, { "number": 10, "startTime": "18:10", "endTime": "18:30" }, { "number": 11, "startTime": "19:00", "endTime": "19:40" }, { "number": 12, "startTime": "19:50", "endTime": "20:30" }, { "number": 13, "startTime": "20:50", "endTime": "21:30" }, { "number": 14, "startTime": "21:40", "endTime": "22:20" } ]; } // ========== 主要功能函数 ========== /** * 获取和解析课程数据 * @returns {Array|null} 课程数组或null */ async function fetchAndParseCourses() { try { console.log('正在获取课程表数据...'); // 获取课程表HTML const html = getScheduleHtml(); if (!html) { console.warn('未获取到课程表HTML'); return null; } console.log('成功获取课程表HTML,开始解析...'); // 解析课程数据 const rawCourses = parseScheduleHtml(html); if (!rawCourses || rawCourses.length === 0) { console.warn('未解析到课程数据'); return null; } console.log(`原始解析到 ${rawCourses.length} 条课程记录`); // 转换为标准格式 const courses = convertCoursesToStandardFormat(rawCourses); console.log(`转换为标准格式后有 ${courses.length} 门课程`); return courses; } catch (error) { console.error('获取或解析课程数据失败:', error); return null; } } /** * 保存课程数据到时光课表 * @param {Array} courses 课程数组 * @returns {boolean} 保存是否成功 */ async function saveCourses(courses) { try { console.log(`正在保存 ${courses.length} 门课程...`); await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses)); console.log('课程数据保存成功'); return true; } catch (error) { console.error('保存课程失败:', error); return false; } } /** * 导入预设时间段到时光课表 * @returns {boolean} 导入是否成功 */ async function importPresetTimeSlots() { try { console.log('正在导入时间段配置...'); const presetTimeSlots = generateTimeSlots(); await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(presetTimeSlots)); console.log('时间段配置导入成功'); return true; } catch (error) { console.error('导入时间段失败:', error); return false; } } // ========== 主执行流程 ========== /** * 主导入函数:云南财经大学课程表导入 */ async function importYnufeCourseSchedule() { // 检查是否在登录页面 if (isLoginPage()) { console.log('检测到在登录页面,终止导入'); AndroidBridge.showToast('请先登录教务系统!'); return; // 直接返回,不抛出错误,不调用notifyTaskCompletion } try { console.log('云南财经大学课程导入开始...'); // 获取和解析课程数据 let courses = await fetchAndParseCourses(); // 如果没有获取到任何课程 if (!courses || courses.length === 0) { console.log('未获取到课程数据'); // 检查是否真的是空课表(已登录且能找到课表元素但没有课程) const html = getScheduleHtml(); if (html && html.includes('kbtable')) { // 找到了课表元素但没有课程,是真的空课表 console.log('检测到空课表'); AndroidBridge.showToast('当前课表为空'); courses = []; // 返回空数组 } else { // 找不到课表元素,解析失败 AndroidBridge.showToast('获取课表失败,请检查网络和页面状态'); throw new Error('未找到课表数据'); } } else { console.log(`成功解析 ${courses.length} 门课程`); } // 保存课程数据 const saveResult = await saveCourses(courses); if (!saveResult) { AndroidBridge.showToast('保存课程失败'); throw new Error('保存课程数据失败'); } // 导入时间段配置 const timeSlotResult = await importPresetTimeSlots(); if (!timeSlotResult) { AndroidBridge.showToast('导入时间段配置失败'); throw new Error('导入时间段失败'); } // 成功 if (courses.length > 0) { AndroidBridge.showToast(`成功导入 ${courses.length} 门课程!`); } console.log('课程导入完成'); return true; } catch (error) { console.error('导入过程出错:', error); AndroidBridge.showToast('导入失败: ' + error.message); return false; } } /** * 启动导入流程并处理完成信号 */ async function runImportFlow() { const success = await importYnufeCourseSchedule(); // 只有成功导入时才发送完成信号 if (success) { AndroidBridge.notifyTaskCompletion(); } return success; } // 启动导入流程 runImportFlow();