| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- // 文件: XYAFU_01.js
- // 功能:从信阳农林学院青果教务系统获取课程表,解析后导入到拾光课程表
- // 适配:信阳农林学院青果教务系统
- // 维护者:glxgo
- const BASE = `${window.location.origin}`;
- const CONTROL_PAGE = '/student/xkjg.wdkb.jsp?menucode=S20301';
- const TIMETABLE_PAGE = '/student/wsxk.xskcb10319.jsp?params=';
- const TIME_SLOTS_SPRING_SUMMER = [
- { number: 1, startTime: '08:00', endTime: '08:45' },
- { number: 2, startTime: '08:50', endTime: '09:35' },
- { number: 3, startTime: '10:00', endTime: '10:45' },
- { number: 4, startTime: '10:50', endTime: '11:35' },
- { number: 5, startTime: '14:30', endTime: '15:15' },
- { number: 6, startTime: '15:20', endTime: '16:05' },
- { number: 7, startTime: '16:30', endTime: '17:15' },
- { number: 8, startTime: '17:20', endTime: '18:05' },
- { number: 9, startTime: '19:00', endTime: '19:45' },
- { number: 10, startTime: '19:50', endTime: '20:35' }
- ];
- const TIME_SLOTS_AUTUMN_WINTER = [
- { number: 1, startTime: '08:00', endTime: '08:45' },
- { number: 2, startTime: '08:50', endTime: '09:35' },
- { number: 3, startTime: '10:00', endTime: '10:45' },
- { number: 4, startTime: '10:50', endTime: '11:35' },
- { number: 5, startTime: '14:00', endTime: '14:45' },
- { number: 6, startTime: '14:50', endTime: '15:35' },
- { number: 7, startTime: '16:00', endTime: '16:45' },
- { number: 8, startTime: '16:50', endTime: '17:35' },
- { number: 9, startTime: '19:00', endTime: '19:45' },
- { number: 10, startTime: '19:50', endTime: '20:35' }
- ];
- function cleanText(value) {
- return String(value || '')
- .replace(/[-]/g, '')
- .replace(/ /g, ' ')
- .trim();
- }
- function encodeParams(xn, xq, xh) {
- return btoa(`xn=${xn}&xq=${xq}&xh=${xh}`);
- }
- function parseWeeks(weekStr) {
- const weeks = [];
- String(weekStr || '')
- .replace(/\s+/g, '')
- .split(/[,,]/)
- .forEach((part) => {
- if (!part) return;
- const isSingle = part.includes('单');
- const isDouble = part.includes('双');
- const rangeMatch = part.match(/(\d+)-(\d+)/);
- if (rangeMatch) {
- const start = parseInt(rangeMatch[1], 10);
- const end = parseInt(rangeMatch[2], 10);
- for (let i = start; i <= end; i++) {
- if (isSingle && i % 2 === 0) continue;
- if (isDouble && i % 2 !== 0) continue;
- weeks.push(i);
- }
- } else {
- const num = parseInt(part.replace(/[^\d]/g, ''), 10);
- if (!Number.isNaN(num)) weeks.push(num);
- }
- });
- return [...new Set(weeks)].sort((a, b) => a - b);
- }
- function decodeParams(encoded) {
- try {
- return atob(encoded);
- } catch (_) {
- return '';
- }
- }
- function parseXhFromEncodedParams(encoded) {
- const decoded = decodeParams(encoded);
- if (!decoded) return '';
- const search = new URLSearchParams(decoded);
- return String(search.get('xh') || '').trim();
- }
- function extractParamsFromHtml(html) {
- const match = String(html || '').match(/wsxk\.xskcb10319\.jsp\?params=([^"'&\s>]+)/);
- return match ? decodeURIComponent(match[1]) : '';
- }
- function findControlFrame(win) {
- try {
- if (win.document.querySelector('#xnxq')) return win;
- } catch (_) {}
- for (let i = 0; i < win.frames.length; i++) {
- try {
- const found = findControlFrame(win.frames[i]);
- if (found) return found;
- } catch (_) {}
- }
- return null;
- }
- function findTimetableFrame(win) {
- try {
- if (win.document.getElementById('mytable')) return win;
- } catch (_) {}
- for (let i = 0; i < win.frames.length; i++) {
- try {
- const found = findTimetableFrame(win.frames[i]);
- if (found) return found;
- } catch (_) {}
- }
- return null;
- }
- async function fetchControlDoc() {
- const res = await fetch(`${BASE}${CONTROL_PAGE}`, { credentials: 'include' });
- if (!res.ok) throw new Error(`课表控制页请求失败: ${res.status}`);
- const html = await res.text();
- return new DOMParser().parseFromString(html, 'text/html');
- }
- async function fetchTimetableDoc(xn, xq, xh) {
- const params = encodeParams(xn, xq, xh);
- const res = await fetch(`${BASE}${TIMETABLE_PAGE}${encodeURIComponent(params)}`, {
- method: 'GET',
- credentials: 'include'
- });
- if (!res.ok) throw new Error(`课表页面请求失败: ${res.status}`);
- const buffer = await res.arrayBuffer();
- let html = '';
- try {
- html = new TextDecoder('gbk').decode(buffer);
- } catch (_) {
- html = new TextDecoder('utf-8').decode(buffer);
- }
- return new DOMParser().parseFromString(html, 'text/html');
- }
- function parseSelectOptions(selectEl) {
- if (!selectEl) return { options: [], defaultIndex: 0 };
- const options = [];
- let defaultIndex = 0;
- Array.from(selectEl.querySelectorAll('option')).forEach((opt) => {
- const value = String(opt.value || '').trim();
- if (!value) return;
- const text = cleanText(opt.textContent) || value;
- if (opt.selected) defaultIndex = options.length;
- options.push({ value, text });
- });
- return { options, defaultIndex };
- }
- function isSpringSummerTerm(selectedText) {
- return /第二学期|春|夏/.test(selectedText || '');
- }
- async function resolveTermSelection() {
- let controlDoc = null;
- let controlFrame = findControlFrame(window);
- if (controlFrame) {
- controlDoc = controlFrame.document;
- } else {
- controlDoc = await fetchControlDoc();
- }
- const select = controlDoc.querySelector('#xnxq');
- if (!select) {
- throw new Error('未找到学期选择器,请先登录并打开“学生个人课表”页面');
- }
- const { options, defaultIndex } = parseSelectOptions(select);
- if (!options.length) {
- throw new Error('未读取到学期列表,请先进入“学生个人课表”页面');
- }
- const selectedIndex = await window.AndroidBridgePromise.showSingleSelection(
- '选择学期',
- JSON.stringify(options.map(item => item.text)),
- defaultIndex
- );
- if (selectedIndex === null || selectedIndex === -1) {
- throw new Error('已取消学期选择');
- }
- const selected = options[selectedIndex];
- const [xn, xq] = String(selected.value).split('-');
- if (!xn || typeof xq === 'undefined') {
- throw new Error(`学期值解析失败: ${selected.value}`);
- }
- let encodedParams = extractParamsFromHtml(controlDoc.documentElement.outerHTML);
- if (!encodedParams) {
- const timetableFrame = findTimetableFrame(window);
- if (timetableFrame) {
- const url = new URL(timetableFrame.location.href);
- encodedParams = url.searchParams.get('params') || '';
- }
- }
- const xh = parseXhFromEncodedParams(encodedParams);
- if (!xh) {
- throw new Error('未读取到学号参数,请先点击进入“学生个人课表”后再导入');
- }
- return {
- xn,
- xq,
- xh,
- selectedValue: selected.value,
- selectedText: selected.text,
- isSpringSummer: isSpringSummerTerm(selected.text)
- };
- }
- function findTable(doc) {
- return doc.getElementById('mytable')
- || Array.from(doc.querySelectorAll('table')).find(table => {
- const text = cleanText(table.innerText);
- return text.includes('星期一') && text.includes('[');
- })
- || null;
- }
- function parseCellByDivBlocks(cell, day) {
- const blocks = Array.from(cell.querySelectorAll('div[style*="padding-bottom:5px"], div[style*="padding-bottom: 5px"]'));
- if (!blocks.length) return [];
- const items = [];
- blocks.forEach((block) => {
- const lines = block.innerText
- .split(/\n+/)
- .map(cleanText)
- .filter(Boolean);
- const joined = lines.join('\n');
- const match = joined.match(/([\d,-单双]+)\[(\d+)-(\d+)\]/);
- if (!match) return;
- const before = joined.slice(0, match.index).split(/\n+/).map(cleanText).filter(Boolean);
- const after = joined.slice(match.index + match[0].length).split(/\n+/).map(cleanText).filter(Boolean);
- const name = before[0] || '';
- const teacher = before[1] || '';
- if (!name) return;
- const weeks = parseWeeks(match[1]);
- const startSection = parseInt(match[2], 10);
- const endSection = parseInt(match[3], 10);
- if (!weeks.length) return;
- items.push({
- name,
- teacher,
- position: after.join(' '),
- day,
- startSection,
- endSection,
- weeks
- });
- });
- return items;
- }
- function parseCellByTextFallback(cell, day) {
- const lines = cell.innerText
- .split(/\n+/)
- .map(cleanText)
- .filter(Boolean);
- if (!lines.length) return [];
- const items = [];
- const textLines = lines.map(line => {
- const m = line.match(/^(.+?)\s+([\d,\-单双]+)\[(\d+)-(\d+)\]\s+(.+)$/);
- if (m) {
- return { raw: line, match: { nameTeacher: m[1], weeks: parseWeeks(m[2]), startSection: parseInt(m[3], 10), endSection: parseInt(m[4], 10), position: m[5] } };
- }
- return { raw: line, match: null };
- });
- const timeBlocks = textLines.filter(tl => tl.match);
- timeBlocks.forEach((tl) => {
- const m = tl.match;
- const name = cleanText(m.nameTeacher).replace(/\s+/g, '');
- items.push({
- name,
- teacher: '',
- position: m.position,
- day,
- startSection: m.startSection,
- endSection: m.endSection,
- weeks: m.weeks
- });
- });
- return items;
- }
- function parseAndMergeQingguoTable(doc) {
- const table = findTable(doc);
- if (!table) {
- throw new Error('未找到课表表格,请先进入“学生个人课表”页面');
- }
- const rawItems = [];
- Array.from(table.rows).forEach((row) => {
- const cells = Array.from(row.cells);
- if (cells.length < 7) return;
- cells.forEach((cell, colIndex) => {
- const distanceToLast = cells.length - 1 - colIndex;
- if (distanceToLast > 6) return;
- const day = 7 - distanceToLast;
- const rawText = cleanText(cell.innerText);
- if (!rawText.includes('[')) return;
- const divParsed = parseCellByDivBlocks(cell, day);
- if (divParsed.length) {
- rawItems.push(...divParsed);
- return;
- }
- const textParsed = parseCellByTextFallback(cell, day);
- if (textParsed.length) {
- rawItems.push(...textParsed);
- return;
- }
- });
- });
- const groupMap = new Map();
- rawItems.forEach((item) => {
- if (!item || !item.name || !item.weeks.length) return;
- const key = `${item.name}|${item.teacher}|${item.position}|${item.day}`;
- if (!groupMap.has(key)) groupMap.set(key, {});
- const weekMap = groupMap.get(key);
- item.weeks.forEach((week) => {
- if (!weekMap[week]) weekMap[week] = new Set();
- for (let section = item.startSection; section <= item.endSection; section++) {
- weekMap[week].add(section);
- }
- });
- });
- const finalCourses = [];
- groupMap.forEach((weekMap, key) => {
- const [name, teacher, position, day] = key.split('|');
- const patternMap = new Map();
- Object.keys(weekMap).forEach((weekStr) => {
- const week = parseInt(weekStr, 10);
- const sections = Array.from(weekMap[week]).sort((a, b) => a - b);
- if (!sections.length) return;
- let start = sections[0];
- for (let i = 0; i < sections.length; i++) {
- if (i === sections.length - 1 || sections[i + 1] !== sections[i] + 1) {
- const pKey = `${start}-${sections[i]}`;
- if (!patternMap.has(pKey)) patternMap.set(pKey, []);
- patternMap.get(pKey).push(week);
- if (i < sections.length - 1) start = sections[i + 1];
- }
- }
- });
- patternMap.forEach((weeks, patternKey) => {
- const [startSection, endSection] = patternKey.split('-').map(Number);
- finalCourses.push({
- name,
- teacher,
- position,
- day: parseInt(day, 10),
- startSection,
- endSection,
- weeks: weeks.sort((a, b) => a - b)
- });
- });
- });
- return finalCourses;
- }
- async function loadTimetableDoc(term) {
- const currentTableFrame = findTimetableFrame(window);
- if (currentTableFrame && currentTableFrame.document.getElementById('mytable')) {
- const currentUrl = new URL(currentTableFrame.location.href);
- const currentParams = currentUrl.searchParams.get('params') || '';
- const decoded = decodeParams(currentParams);
- if (decoded.includes(`xn=${term.xn}`) && decoded.includes(`xq=${term.xq}`)) {
- return currentTableFrame.document;
- }
- }
- return await fetchTimetableDoc(term.xn, term.xq, term.xh);
- }
- async function runImportFlow() {
- try {
- const confirmed = await window.AndroidBridgePromise.showAlert(
- '信阳农林学院教务导入',
- '请确认你已登录教务系统,并且最好已经打开“学生个人课表”页面。',
- '确定,开始导入'
- );
- if (!confirmed) return;
- const term = await resolveTermSelection();
- AndroidBridge.showToast('正在提取青果课表数据...');
- const timeSlots = term.isSpringSummer ? TIME_SLOTS_SPRING_SUMMER : TIME_SLOTS_AUTUMN_WINTER;
- const doc = await loadTimetableDoc(term);
- const courses = parseAndMergeQingguoTable(doc);
- if (!courses.length) {
- throw new Error('未找到有效课程,请确认当前学期课表已正常显示');
- }
- const allWeeks = courses.flatMap(course => course.weeks);
- const semesterTotalWeeks = allWeeks.length ? Math.max(...allWeeks) : 20;
- await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify({
- semesterTotalWeeks,
- semesterStartDate: null,
- firstDayOfWeek: 1
- }));
- await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(timeSlots));
- await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
- AndroidBridge.showToast(`导入成功:共 ${courses.length} 门课程`);
- AndroidBridge.notifyTaskCompletion();
- } catch (error) {
- console.error(error);
- AndroidBridge.showToast(`导入失败: ${error.message}`);
- }
- }
- runImportFlow();
|