// 文件: FAFU.js (v3) // 福建农林大学教务系统课程表导入脚本 // 适配对象: 经典正方教务系统 (jwgl.fafu.edu.cn, default2.aspx) // 说明: 解析"时间网格"式课表 —— 行为节次、列为星期, // 课程单元格为分行文本(课程名 / 周X第Y节{第X-Y周} / 教师 / 地点)。 // v3: 支持课表位于同源 iframe 内;用 innerHTML 按
切行;失败时输出诊断。 // ---------------- 通用工具 ---------------- function cleanText(s) { return (s || '') .replace(/\u00a0/g, ' ') .replace(/\s+/g, ' ') .trim(); } // 展开周次数组 "1-16" 及 单/双周 过滤 function expandWeeks(start, end, parity) { const weeks = []; for (let i = start; i <= end; i++) { if (parity === 1 && i % 2 !== 1) continue; if (parity === 2 && i % 2 !== 0) continue; weeks.push(i); } return weeks; } // 解析课程信息行:"周一第1,2节{第1-5周}" / "{第1-17周|2节/周}" / "{第16-16周|双周}" function parseInfoLine(info) { let day = 0; const dayM = /星期?([一二三四五六日])/.exec(info || ''); if (dayM) day = '一二三四五六日'.indexOf(dayM[1]) + 1; let sections = []; const secM = /第(\d+(?:,\d+)*)节/.exec(info || ''); if (secM) sections = secM[1].split(',').map(Number); let weekStart = 0, weekEnd = 0; const wM = /第(\d+)(?:-(\d+))?周/.exec(info || ''); if (wM) { weekStart = parseInt(wM[1]); weekEnd = wM[2] ? parseInt(wM[2]) : weekStart; } let parity = 0; if (/单周/.test(info || '')) parity = 1; else if (/双周/.test(info || '')) parity = 2; return { day, sections, weekStart, weekEnd, parity }; } // ---------------- 若课表在 iframe 中,需递归收集同源文档 ---------------- function getAllDocs() { const docs = [document]; const seen = new Set([document]); (function walk(doc) { try { const frames = doc.querySelectorAll('iframe, frame'); frames.forEach(f => { const inner = f.contentDocument; if (inner && !seen.has(inner)) { seen.add(inner); docs.push(inner); walk(inner); } }); } catch (e) { /* 跨域 iframe 无法访问,忽略 */ } })(document); return docs; } // 在所有文档中定位课表网格 function locateGridTable() { for (const doc of getAllDocs()) { const tables = doc.querySelectorAll('table'); for (const t of tables) { const txt = cleanText(t.textContent || ''); if (txt.includes('上午') && txt.includes('第1节') && /星期[一二三四五六日]/.test(txt)) { return t; } } } return null; } function countTables() { return getAllDocs().reduce((n, d) => n + d.querySelectorAll('table').length, 0); } // 星期列的水平范围 {星期号: {left, right}} function buildDayColumns(table) { const headerTr = Array.from(table.querySelectorAll('tr')) .find(tr => /星期[一二三四五六日]/.test(tr.textContent || '')); if (!headerTr) return {}; const cols = {}; Array.from(headerTr.querySelectorAll('td, th')).forEach(td => { const m = /星期([一二三四五六日])/.exec(td.textContent || ''); if (!m) return; const dayNum = '一二三四五六日'.indexOf(m[1]) + 1; const r = td.getBoundingClientRect(); if (r.width > 0) cols[dayNum] = { left: r.left, right: r.right }; }); return cols; } // 因单元格有 rowspan/colspan,用几何位置判断其属于哪个星期列 function dayFromRect(td, dayCols) { const r = td.getBoundingClientRect(); const cx = (r.left + r.right) / 2; for (const dayNum in dayCols) { const c = dayCols[dayNum]; if (cx >= c.left - 2 && cx <= c.right + 2) return parseInt(dayNum); } return 0; } // 用单元格 HTML 按
切行(不依赖 innerText,隐藏表格也能解析) function cellLines(td) { const html = td.innerHTML || ''; return html.split(//i) .map(s => cleanText(s.replace(/<[^>]*>/g, ''))); } // 把行按空行切成课程块,每块 4 行:课程名/节次周次/教师/地点 function splitBlocks(lines) { const blocks = []; let cur = []; for (const ln of lines) { if (ln === '') { if (cur.length) { blocks.push(cur); cur = []; } } else { cur.push(ln); } } if (cur.length) blocks.push(cur); return blocks; } // 解析网格课表 function parseGridTable(table) { const dayCols = buildDayColumns(table); const courses = []; const rows = Array.from(table.querySelectorAll('tr')); rows.forEach(tr => { // 该行首个"第X节"标签(用于无节次信息的课程,如体育) let rowSectionStart = 0; Array.from(tr.querySelectorAll('td')).forEach(td => { if (rowSectionStart) return; const m = /^第(\d+)节$/.exec(cleanText(td.innerText || td.textContent || '')); if (m) rowSectionStart = parseInt(m[1]); }); Array.from(tr.querySelectorAll('td')).forEach(td => { const lines = cellLines(td).map(l => l.trim()); if (!lines.some(l => l)) return; // 全空则跳过 const blocks = splitBlocks(lines); blocks.forEach(block => { // 课程块需至少 4 行且末行为地点(可过滤"上午/早晨/第X节"等标签) if (block.length < 4 || !block[3]) return; const name = block[0] || ''; const info = block[1] || ''; const teacher = block[2] || ''; const location = block[3] || ''; const parsed = parseInfoLine(info); const day = parsed.day || dayFromRect(td, dayCols); let sections = parsed.sections; if (!sections.length && rowSectionStart) { const span = td.rowSpan || 1; sections = Array.from({ length: span }, (_, i) => rowSectionStart + i); } const weeks = expandWeeks(parsed.weekStart, parsed.weekEnd, parsed.parity); if (!day || !sections.length || !weeks.length || !name) return; courses.push({ name: name, day: day, weeks: weeks, teacher: teacher, position: location, startSection: sections[0], endSection: sections[sections.length - 1] }); }); }); }); return courses; } // ---------------- 主流程 ---------------- async function scrapeAndParseCourses() { window.shiguangBridge.showToast("正在解析课表数据..."); const table = locateGridTable(); if (!table) { const ts = countTables(); await window.shiguangBridgePromise.showAlert( "导入失败", "未定位到课表网格。\n当前共检测到 " + ts + " 个表格。\n请确认:\n1.已登录教务系统\n2.已在【信息查询-学生个人课表】点击【查询】并显示课程\n3.若课表在 iframe/新窗口,脚本已自动尝试进入同源 iframe" + (ts === 0 ? "\n\n提示:检测到 0 个表格,可能脚本运行于错误页面/跨域 iframe,请返回课表页面后重试。" : ""), "确定" ); return null; } const result = parseGridTable(table); if (result.length === 0) { await window.shiguangBridgePromise.showAlert( "导入失败", "已定位到课表,但未解析出任何课程。\n请点击【查询】让课表显示课程数据后重试。", "确定" ); return null; } return { courses: result }; } async function saveCourses(courses) { window.shiguangBridge.showToast(`正在保存 ${courses.length} 门课程...`); try { await window.shiguangBridgePromise.saveImportedCourses(JSON.stringify(courses)); return true; } catch (error) { window.shiguangBridge.showToast("课程保存失败: " + error.message); return false; } } async function runImportFlow() { const confirmed = await window.shiguangBridgePromise.showAlert( "福建农林大学课表导入", "导入前请确保已在【学生个人课表】页面点击了【查询】并显示了课程。\n确认后脚本将自动解析当前课表。", "好的,开始导入" ); if (!confirmed) { window.shiguangBridge.showToast("用户取消了导入。"); return; } const result = await scrapeAndParseCourses(); if (!result) return; const ok = await saveCourses(result.courses); if (!ok) return; window.shiguangBridge.showToast(`课程导入成功,共 ${result.courses.length} 门!`); window.shiguangBridge.notifyTaskCompletion(); } runImportFlow();