|
|
@@ -0,0 +1,265 @@
|
|
|
+// 文件: ECJTU_01.js
|
|
|
+// 功能:从华东交通大学系统获取课程表,解析后导入到拾光课程表
|
|
|
+// 适配:华东交通大学教务系统
|
|
|
+// 维护者:glxgo
|
|
|
+
|
|
|
+const BASE = window.location.origin;
|
|
|
+const SCHEDULE_PATHS = [
|
|
|
+ '/Schedule/Schedule_getUserSchedume.action?item=0207',
|
|
|
+ '/Schedule/Schedule_getUserSchedume.action?item=0205',
|
|
|
+ '/Schedule/Schedule_getUserSchedume.action'
|
|
|
+];
|
|
|
+
|
|
|
+function cleanText(value) {
|
|
|
+ return String(value || '')
|
|
|
+ .replace(/[-]/g, '')
|
|
|
+ .replace(/ /g, ' ')
|
|
|
+ .trim();
|
|
|
+}
|
|
|
+
|
|
|
+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 parseSections(sectionStr) {
|
|
|
+ const sections = String(sectionStr || '')
|
|
|
+ .split(',')
|
|
|
+ .map(s => parseInt(s.trim(), 10))
|
|
|
+ .filter(n => !Number.isNaN(n));
|
|
|
+ if (!sections.length) return null;
|
|
|
+ return {
|
|
|
+ startSection: sections[0],
|
|
|
+ endSection: sections[sections.length - 1]
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function parseTeacherPosition(line) {
|
|
|
+ const raw = cleanText(line);
|
|
|
+ const atIndex = raw.indexOf('@');
|
|
|
+ if (atIndex === -1) {
|
|
|
+ return { teacher: raw, position: '' };
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ teacher: cleanText(raw.slice(0, atIndex)),
|
|
|
+ position: cleanText(raw.slice(atIndex + 1))
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function parseCourseLines(lines, day) {
|
|
|
+ const items = [];
|
|
|
+ for (let i = 2; i < lines.length; i++) {
|
|
|
+ const line = cleanText(lines[i]);
|
|
|
+ const match = line.match(/^([\d,,\-单双()]+)\s+(\d+(?:,\d+)*)$/);
|
|
|
+ if (!match) continue;
|
|
|
+
|
|
|
+ const name = cleanText(lines[i - 2]);
|
|
|
+ const teacherPosition = parseTeacherPosition(lines[i - 1]);
|
|
|
+ const weekText = match[1];
|
|
|
+ const sectionText = match[2];
|
|
|
+ const weeks = parseWeeks(weekText);
|
|
|
+ const sections = parseSections(sectionText);
|
|
|
+
|
|
|
+ if (!name || !weeks.length || !sections) continue;
|
|
|
+
|
|
|
+ items.push({
|
|
|
+ name,
|
|
|
+ teacher: teacherPosition.teacher || '未知教师',
|
|
|
+ position: teacherPosition.position || '未排地点',
|
|
|
+ day,
|
|
|
+ startSection: sections.startSection,
|
|
|
+ endSection: sections.endSection,
|
|
|
+ weeks
|
|
|
+ });
|
|
|
+ }
|
|
|
+ return items;
|
|
|
+}
|
|
|
+
|
|
|
+function mergeCourses(rawItems) {
|
|
|
+ const groupMap = new Map();
|
|
|
+ rawItems.forEach((item) => {
|
|
|
+ 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.sort((a, b) => (
|
|
|
+ a.day - b.day
|
|
|
+ || a.startSection - b.startSection
|
|
|
+ || a.name.localeCompare(b.name, 'zh-CN')
|
|
|
+ ));
|
|
|
+}
|
|
|
+
|
|
|
+function parseScheduleTable(doc) {
|
|
|
+ const table = doc.getElementById('courseSche');
|
|
|
+ if (!table) return [];
|
|
|
+
|
|
|
+ const rows = Array.from(table.rows);
|
|
|
+ if (rows.length < 2) return [];
|
|
|
+
|
|
|
+ const rawItems = [];
|
|
|
+ for (let r = 1; r < rows.length; r++) {
|
|
|
+ const cells = Array.from(rows[r].cells);
|
|
|
+ if (cells.length < 2) continue;
|
|
|
+
|
|
|
+ const dayCells = cells.slice(1, 8);
|
|
|
+ dayCells.forEach((cell, index) => {
|
|
|
+ const rawText = cleanText(cell.innerText);
|
|
|
+ if (!rawText || !rawText.includes('@')) return;
|
|
|
+ const lines = cell.innerText
|
|
|
+ .split(/\n+/)
|
|
|
+ .map(cleanText)
|
|
|
+ .filter(Boolean);
|
|
|
+ rawItems.push(...parseCourseLines(lines, index + 1));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return mergeCourses(rawItems);
|
|
|
+}
|
|
|
+
|
|
|
+function getCurrentTermInfo(doc) {
|
|
|
+ const select = doc.querySelector('#term');
|
|
|
+ if (!select) return null;
|
|
|
+ const selected = select.querySelector('option:checked') || select.options[select.selectedIndex];
|
|
|
+ return {
|
|
|
+ value: String(select.value || '').trim(),
|
|
|
+ text: selected ? cleanText(selected.textContent) : ''
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+function isScheduleDoc(doc) {
|
|
|
+ return !!(doc && (doc.getElementById('courseSche') || doc.querySelector('#term')));
|
|
|
+}
|
|
|
+
|
|
|
+function findScheduleDoc(win) {
|
|
|
+ try {
|
|
|
+ if (isScheduleDoc(win.document)) return win.document;
|
|
|
+ } catch (_) {}
|
|
|
+ for (let i = 0; i < win.frames.length; i++) {
|
|
|
+ try {
|
|
|
+ const found = findScheduleDoc(win.frames[i]);
|
|
|
+ if (found) return found;
|
|
|
+ } catch (_) {}
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+async function fetchScheduleDoc() {
|
|
|
+ for (const path of SCHEDULE_PATHS) {
|
|
|
+ try {
|
|
|
+ const res = await fetch(`${BASE}${path}`, { credentials: 'include' });
|
|
|
+ if (!res.ok) continue;
|
|
|
+ const html = await res.text();
|
|
|
+ const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
|
+ if (isScheduleDoc(doc)) return doc;
|
|
|
+ } catch (_) {}
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+}
|
|
|
+
|
|
|
+async function loadScheduleDoc() {
|
|
|
+ const currentDoc = findScheduleDoc(window);
|
|
|
+ if (currentDoc && currentDoc.getElementById('courseSche')) return currentDoc;
|
|
|
+ const fetched = await fetchScheduleDoc();
|
|
|
+ if (fetched) return fetched;
|
|
|
+ throw new Error('未找到课表页面,请先登录后进入“我的课表/个人课表”页面');
|
|
|
+}
|
|
|
+
|
|
|
+async function runImportFlow() {
|
|
|
+ try {
|
|
|
+ const confirmed = await window.AndroidBridgePromise.showAlert(
|
|
|
+ '华东交通大学教务导入',
|
|
|
+ '请确认你已经登录教务系统;如需导入其他学期,请先在页面上切换到目标学期后再导入。',
|
|
|
+ '确定,开始导入'
|
|
|
+ );
|
|
|
+ if (!confirmed) return;
|
|
|
+
|
|
|
+ const doc = await loadScheduleDoc();
|
|
|
+ const termInfo = getCurrentTermInfo(doc);
|
|
|
+ AndroidBridge.showToast(termInfo?.text ? `正在导入 ${termInfo.text} 课表...` : '正在解析课表数据...');
|
|
|
+
|
|
|
+ const courses = parseScheduleTable(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.saveImportedCourses(JSON.stringify(courses));
|
|
|
+
|
|
|
+ AndroidBridge.showToast(`导入成功:共 ${courses.length} 门课程`);
|
|
|
+ AndroidBridge.notifyTaskCompletion();
|
|
|
+ } catch (error) {
|
|
|
+ console.error(error);
|
|
|
+ AndroidBridge.showToast(`导入失败: ${error.message}`);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+runImportFlow();
|