| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- /**
- * 将周次字符串(如 "1-4,6,8-10")解析为有序的周次数字数组。
- * 支持连续区间(如 "1-4")和单个周次(如 "6")的混合格式。
- * @param {string} weekStr - 原始周次字符串
- * @returns {number[]} 去重并升序排列的周次数组
- */
- function parseWeeks(weekStr) {
- const weeks = [];
- const parts = weekStr.split(',');
- for (const part of parts) {
- if (part.includes('-')) {
- const [start, end] = part.split('-');
- for (let w = parseInt(start); w <= parseInt(end); w++) {
- if (!weeks.includes(w)) weeks.push(w);
- }
- } else {
- const w = parseInt(part);
- if (!isNaN(w) && !weeks.includes(w)) weeks.push(w);
- }
- }
- return weeks.sort((a, b) => a - b);
- }
- /**
- * 从教务系统返回的 HTML 文档中解析所有课程信息。
- * 遍历课表格 #timetable 中每个单元格,提取课程名、教师、教室及周次节次。
- * @param {Document} doc - 已解析的 HTML 文档对象
- * @returns {object[]} 课程对象数组,每项包含 day/name/teacher/position/weeks/startSection/endSection
- */
- function extractCoursesFromDoc(doc) {
- const courses = [];
- const table = doc.getElementById('timetable');
- if (!table) throw new Error("未找到课表格元素(#timetable),请确认教务系统页面已正常加载。");
- const rows = table.getElementsByTagName('tr');
- // 跳过首行(表头)和末行(通常为空白行)
- for (let rowIdx = 1; rowIdx < rows.length - 1; rowIdx++) {
- const cells = rows[rowIdx].getElementsByTagName('td');
- for (let colIdx = 0; colIdx < cells.length; colIdx++) {
- const dayOfWeek = colIdx + 1; // 列索引对应星期几(1=周一)
- const cell = cells[colIdx];
- const contentDivs = cell.querySelectorAll('div.kbcontent');
- if (contentDivs.length === 0) continue;
- contentDivs.forEach(div => {
- const rawHtml = div.innerHTML;
- // 跳过空单元格
- if (!rawHtml.trim() || rawHtml === ' ') return;
- // 同一格内多门课以 10 个以上连字符分隔
- const blocks = rawHtml.split(/-{10,}\s*<br\s*\/?>/i);
- blocks.forEach(block => {
- if (!block.trim()) return;
- const tmp = document.createElement('div');
- tmp.innerHTML = block;
- const course = {
- day: dayOfWeek,
- isCustomTime: false
- };
- // 课程名称取第一个文本节点,fallback 到 innerText 首行
- const firstNode = tmp.childNodes[0];
- if (firstNode && firstNode.nodeType === Node.TEXT_NODE) {
- course.name = firstNode.nodeValue.trim();
- } else {
- course.name = tmp.innerText.split('\n')[0].trim();
- }
- // 教师姓名
- const teacherEl = tmp.querySelector('font[title="教师"]');
- course.teacher = teacherEl ? teacherEl.innerText.trim() : "未知";
- // 上课教室
- const roomEl = tmp.querySelector('font[title="教室"]');
- course.position = roomEl ? roomEl.innerText.trim() : "待定";
- // 周次与节次:格式为 "X-Y(周)[A-B节]" 或仅 "X-Y(周)"
- const timeEl = tmp.querySelector('font[title="周次(节次)"]');
- if (!timeEl) return;
- const timeText = timeEl.innerText.trim();
- const fullMatch = timeText.match(/(.+?)\(周\)\[(\d+)-(\d+)节\]/);
- if (fullMatch) {
- course.weeks = parseWeeks(fullMatch[1]);
- course.startSection = parseInt(fullMatch[2]);
- course.endSection = parseInt(fullMatch[3]);
- } else {
- const weekOnlyMatch = timeText.match(/(.+?)\(周\)/);
- if (weekOnlyMatch) {
- // 节次信息缺失时,根据行索引推算默认节次
- course.weeks = parseWeeks(weekOnlyMatch[1]);
- course.startSection = rowIdx * 2 - 1;
- course.endSection = rowIdx * 2;
- } else {
- return; // 无法识别的时间格式,跳过
- }
- }
- if (course.name && course.weeks && course.weeks.length > 0) {
- courses.push(course);
- }
- });
- });
- }
- }
- return courses;
- }
- /**
- * 根据当前日期返回对应学期的作息时间表。
- * 通过配置参数动态推算各节课起止时间,支持早、午、晚三段以及
- * 普通课间 / 中课间 / 大课间三级课间时长。
- * 5月~9月使用夏季作息,其余月份使用春秋冬季作息。
- * @returns {object[]} 节次时间数组,每项包含 number/startTime/endTime
- */
- function getPresetTimeSlots() {
- const month = new Date().getMonth() + 1;
- const isSummer = month >= 5 && month <= 9;
- // ── 作息参数配置 ──────────────────────────────────────────────
- const params = isSummer ? {
- // 夏季作息参数
- morningStartTime: "08:10", // 早上第 1 节开始时间
- afternoonStartTime: "14:45", // 中午第 1 节开始时间(第 5 节)
- eveningStartTime: "19:30", // 晚上第 1 节开始时间(第 9 节)
- classDuration: 45, // 单节课时长(分钟)
- normalBreakDuration: 10, // 普通课间时长(分钟)
- mediumBreakDuration: 15, // 中课间时长(分钟)
- longBreakDuration: 20, // 大课间时长(分钟)
- longBreakAfterSlot: 2, // 大课间插入位置(第 N 节课后)
- mediumBreakAfterSlot: 6, // 中课间插入位置(第 N 节课后)
- } : {
- // 春秋冬季作息参数
- morningStartTime: "08:20",
- afternoonStartTime: "14:30",
- eveningStartTime: "19:10",
- classDuration: 45,
- normalBreakDuration: 10,
- mediumBreakDuration: 15,
- longBreakDuration: 20,
- longBreakAfterSlot: 2,
- mediumBreakAfterSlot: 6,
- };
- // 早上 4 节 / 下午 4 节 / 晚上 3 节
- const SESSION_SLOTS = [4, 4, 3];
- const SESSION_STARTS = [
- params.morningStartTime,
- params.afternoonStartTime,
- params.eveningStartTime,
- ];
- const toMinutes = (t) => { const [h, m] = t.split(':').map(Number); return h * 60 + m; };
- const toTimeStr = (min) => `${String(Math.floor(min / 60)).padStart(2, '0')}:${String(min % 60).padStart(2, '0')}`;
- const { classDuration, normalBreakDuration, mediumBreakDuration, longBreakDuration,
- longBreakAfterSlot, mediumBreakAfterSlot } = params;
- const result = [];
- let slotNumber = 1;
- for (let s = 0; s < SESSION_SLOTS.length; s++) {
- let cursor = toMinutes(SESSION_STARTS[s]);
- const count = SESSION_SLOTS[s];
- for (let i = 0; i < count; i++) {
- result.push({
- number: slotNumber,
- startTime: toTimeStr(cursor),
- endTime: toTimeStr(cursor + classDuration),
- });
- cursor += classDuration;
- // 非本段末节时,按位置选择课间时长
- if (i < count - 1) {
- if (slotNumber === longBreakAfterSlot) cursor += longBreakDuration;
- else if (slotNumber === mediumBreakAfterSlot) cursor += mediumBreakDuration;
- else cursor += normalBreakDuration;
- }
- slotNumber++;
- }
- }
- return result;
- }
- /**
- * 从教学周历页面推断学期开学日期(YYYY-MM-DD)。
- * 解析策略:
- * 1) 优先读取第 1 周对应的周一日期;
- * 2) 若未命中,则在周历表中收集全部日期并取最小值兜底。
- * @returns {Promise<{ semesterStartDate: string|null, totalWeeks: number|null }>} 开学日期和总周数;无法解析时返回 null
- */
- async function getStartDateandTotalWeeks() {
- const response = await fetch('/jsxsd/jxzl/jxzl_query', { method: 'GET' });
- if (!response.ok) {
- throw new Error(`周历请求失败:${response.status}`);
- }
- const htmlText = await response.text();
- const doc = new DOMParser().parseFromString(htmlText, 'text/html');
- const table = doc.querySelector('#kbtable');
- if (!table) {
- console.log('未找到周历表格 #kbtable');
- return { semesterStartDate: null, totalWeeks: null };
- }
- const parseCNDate = (text) => {
- const m = String(text || '').match(/(\d{4})年(\d{1,2})月(\d{1,2})/);
- if (!m) return null;
- const [, y, mo, d] = m;
- return `${y}-${mo.padStart(2, '0')}-${d.padStart(2, '0')}`;
- };
- let week1Monday = null;
- let minDate = null;
- let totalWeeks = null;
- const rows = table.querySelectorAll('tr');
- if(!rows || rows.length === 0) {
- console.log('周历表格 #kbtable 中未找到任何行');
- return { semesterStartDate: null, totalWeeks: null };
- }
- rows.forEach((row) => {
- const firstCellText = row.cells?.[0]?.textContent?.trim() || '';
- const week = /^\d+$/.test(firstCellText) ? Number(firstCellText) : NaN;
- if (!Number.isNaN(week)) {
- totalWeeks = Math.max(totalWeeks, week);
- if (week === 1 && !week1Monday) {
- const candidate = parseCNDate(row.cells?.[1]?.getAttribute('title'));
- if (candidate) week1Monday = candidate;
- }
- }
- row.querySelectorAll('td[title]').forEach((td) => {
- const candidate = parseCNDate(td.getAttribute('title'));
- if (!candidate) return;
- if (!minDate || candidate < minDate) {
- minDate = candidate;
- }
- });
- });
- const semesterStartDate = week1Monday || minDate;
- console.log('开学日期:', semesterStartDate || '未找到');
- console.log('总周数:', totalWeeks);
- return { semesterStartDate, totalWeeks };
- }
- /**
- * 返回全局课表基础配置(单节课时长与课间休息时长)。
- * @returns {Promise<{ semesterStartDate: string|null, semesterTotalWeeks: number, defaultClassDuration: number, defaultBreakDuration: number }>}
- */
- async function getCourseConfig() {
- const { semesterStartDate, totalWeeks } = await getStartDateandTotalWeeks();
- return {
- semesterStartDate: semesterStartDate,
- semesterTotalWeeks: totalWeeks,
- defaultClassDuration: 45,
- defaultBreakDuration: 10
- };
- }
- /**
- * 课表导入主流程。
- * 依次完成:发起请求 → 解析 HTML → 提取课程 → 保存配置/作息/课程 → 通知完成。
- * 在浏览器调试环境中仅打印结果,不调用 AndroidBridge。
- */
- async function runImportFlow() {
- const isApp = typeof window.AndroidBridgePromise !== 'undefined';
- const hasToast = typeof window.AndroidBridge !== 'undefined';
- try {
- if (hasToast) {
- AndroidBridge.showToast("正在拉取课表,请稍候...");
- } else {
- console.log("[HUSE] 开始请求课表页面...");
- }
- const response = await fetch('/jsxsd/xskb/xskb_list.do', { method: 'GET' });
- const htmlText = await response.text();
- const doc = new DOMParser().parseFromString(htmlText, 'text/html');
- // 读取学期列表(当前仅用于记录,实际取最新学期)
- const selectEl = doc.getElementById('xnxq01id');
- const semesters = [];
- const semesterValues = [];
- let defaultIndex = 0;
- if (selectEl) {
- selectEl.querySelectorAll('option').forEach((opt, idx) => {
- semesters.push(opt.innerText.trim());
- semesterValues.push(opt.value);
- if (opt.hasAttribute('selected')) defaultIndex = idx;
- });
- }
- // 始终选取列表首的最新学期
- defaultIndex = 0;
- console.log(`[HUSE] 共找到 ${semesters.length} 个学期,当前使用:${semesters[defaultIndex] || '未知'}`);
- const courses = extractCoursesFromDoc(doc);
- if (courses.length === 0) {
- const msg = "未解析到任何课程,当前学期可能暂无排课。";
- console.warn("[HUSE] " + msg);
- if (isApp) {
- await window.AndroidBridgePromise.showAlert("提示", msg, "好的");
- } else {
- alert(msg);
- }
- return;
- }
- console.log(`[HUSE] 成功解析 ${courses.length} 门课程。`);
- const config = await getCourseConfig();
- const timeSlots = getPresetTimeSlots();
- // 浏览器调试环境:输出结果后退出,不执行 APP 存储逻辑
- if (!isApp) {
- console.log("[HUSE] 课表基础配置:", config);
- console.log("[HUSE] 作息时间表:", timeSlots);
- console.log("[HUSE] 课程列表:", courses);
- alert(`解析完成!共获取 ${courses.length} 门课程及作息时间,详情见控制台(F12)。`);
- return;
- }
- // APP 环境:保存课表配置与作息时间
- const configSaved = await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(config));
- const slotsSaved = await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
- if (!configSaved || !slotsSaved) {
- // 时间配置保存失败不强制中断,继续尝试导入课程
- console.warn("[HUSE] 课表时间配置保存失败,将继续尝试导入课程。");
- AndroidBridge.showToast("时间配置保存失败,继续导入课程...");
- }
- // APP 环境:保存课程数据
- const courseSaved = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
- if (!courseSaved) {
- console.error("[HUSE] 课程数据保存失败。");
- AndroidBridge.showToast("课程保存失败,请重试!");
- return;
- }
- console.log(`[HUSE] 导入完成,共写入 ${courses.length} 门课程。`);
- AndroidBridge.showToast(`成功导入 ${courses.length} 门课程及作息时间!`);
- AndroidBridge.notifyTaskCompletion();
- } catch (err) {
- console.error("[HUSE] 导入流程发生异常:", err);
- if (hasToast) {
- AndroidBridge.showToast("导入失败:" + err.message);
- } else {
- alert("导入失败:" + err.message);
- }
- }
- }
- runImportFlow();
|