|
|
@@ -0,0 +1,354 @@
|
|
|
+// 适配目标:武汉商学院-超星综合教学管理系统
|
|
|
+
|
|
|
+(function () {
|
|
|
+ function toast(message) {
|
|
|
+ if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
|
|
|
+ window.AndroidBridge.showToast(message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function sleep(ms) {
|
|
|
+ return new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
+ }
|
|
|
+
|
|
|
+ async function getTargetDocument() {
|
|
|
+ if (location.href.includes("queryKbForXsd")) {
|
|
|
+ return document;
|
|
|
+ }
|
|
|
+
|
|
|
+ const iframe = document.querySelector("iframe[src*='queryKbForXsd']");
|
|
|
+ if (!iframe) return null;
|
|
|
+
|
|
|
+ for (let i = 0; i < 20; i += 1) {
|
|
|
+ try {
|
|
|
+ const doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
|
|
|
+ if (doc && doc.readyState && doc.readyState !== "loading") {
|
|
|
+ return doc;
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ // ignore
|
|
|
+ }
|
|
|
+ await sleep(500);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ async function waitForScheduleData(doc, timeoutMs = 15000) {
|
|
|
+ const start = Date.now();
|
|
|
+ while (Date.now() - start < timeoutMs) {
|
|
|
+ const cells = Array.from(doc.querySelectorAll("td.cell, td[id^='Cell']"));
|
|
|
+ const filled = cells.filter((cell) => {
|
|
|
+ const text = (cell.innerText || cell.textContent || "").trim();
|
|
|
+ return text.length > 0 && /周/.test(text);
|
|
|
+ });
|
|
|
+ if (filled.length > 0) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ await sleep(500);
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ function uniqueSortedNumbers(nums) {
|
|
|
+ const set = new Set(nums.filter((n) => Number.isFinite(n)));
|
|
|
+ return Array.from(set).sort((a, b) => a - b);
|
|
|
+ }
|
|
|
+
|
|
|
+ function parseWeekText(weekText) {
|
|
|
+ if (!weekText) return [];
|
|
|
+ let text = String(weekText).trim();
|
|
|
+ if (!text) return [];
|
|
|
+
|
|
|
+ let oddOnly = false;
|
|
|
+ let evenOnly = false;
|
|
|
+ if (text.includes("单")) oddOnly = true;
|
|
|
+ if (text.includes("双")) evenOnly = true;
|
|
|
+
|
|
|
+ text = text.replace(/周/g, "");
|
|
|
+ text = text.replace(/\s+/g, "");
|
|
|
+ text = text.replace(/\(.*?\)/g, "");
|
|
|
+ text = text.replace(/(.*?)/g, "");
|
|
|
+
|
|
|
+ const weeks = [];
|
|
|
+ const segments = text.split(",").map((s) => s.trim()).filter(Boolean);
|
|
|
+ segments.forEach((seg) => {
|
|
|
+ if (!seg) return;
|
|
|
+ const rangeMatch = seg.match(/^(\d+)-(\d+)$/);
|
|
|
+ if (rangeMatch) {
|
|
|
+ const start = parseInt(rangeMatch[1], 10);
|
|
|
+ const end = parseInt(rangeMatch[2], 10);
|
|
|
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return;
|
|
|
+ for (let w = start; w <= end; w += 1) {
|
|
|
+ weeks.push(w);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const single = parseInt(seg, 10);
|
|
|
+ if (Number.isFinite(single)) weeks.push(single);
|
|
|
+ });
|
|
|
+
|
|
|
+ let filtered = weeks;
|
|
|
+ if (oddOnly && !evenOnly) {
|
|
|
+ filtered = weeks.filter((w) => w % 2 === 1);
|
|
|
+ } else if (evenOnly && !oddOnly) {
|
|
|
+ filtered = weeks.filter((w) => w % 2 === 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ return uniqueSortedNumbers(filtered);
|
|
|
+ }
|
|
|
+
|
|
|
+ function splitCourseBlocks(cellText) {
|
|
|
+ const text = cellText.replace(/\r/g, "").trim();
|
|
|
+ if (!text) return [];
|
|
|
+ return text
|
|
|
+ .split(/\n{2,}/)
|
|
|
+ .map((block) => block.trim())
|
|
|
+ .filter(Boolean);
|
|
|
+ }
|
|
|
+
|
|
|
+ function extractWeeksTextFromLine(line) {
|
|
|
+ if (!line) return { weeksText: "", rest: line || "" };
|
|
|
+ const match = line.match(/(\d+(?:-\d+)?(?:,\d+(?:-\d+)?)*)\s*(?:\((单|双)\))?\s*周/);
|
|
|
+ if (!match) return { weeksText: "", rest: line };
|
|
|
+ const weeksCore = match[1];
|
|
|
+ const oddEven = match[2] ? `(${match[2]})` : "";
|
|
|
+ const weeksText = `${weeksCore}${oddEven}周`;
|
|
|
+ const rest = line.replace(match[0], "").trim();
|
|
|
+ return { weeksText, rest };
|
|
|
+ }
|
|
|
+
|
|
|
+ function parseCourseBlock(block) {
|
|
|
+ const lines = block
|
|
|
+ .split(/\n+/)
|
|
|
+ .map((l) => l.trim())
|
|
|
+ .filter(Boolean);
|
|
|
+ if (!lines.length) return null;
|
|
|
+
|
|
|
+ const name = lines[0] || "";
|
|
|
+ let teacher = "";
|
|
|
+ let weeksText = "";
|
|
|
+ let position = "";
|
|
|
+
|
|
|
+ const weekLineIndex = lines.findIndex((l) => /周/.test(l));
|
|
|
+ if (weekLineIndex >= 0) {
|
|
|
+ const { weeksText: extractedWeeks, rest } = extractWeeksTextFromLine(
|
|
|
+ lines[weekLineIndex]
|
|
|
+ );
|
|
|
+ weeksText = extractedWeeks;
|
|
|
+ if (weekLineIndex === 1) {
|
|
|
+ teacher = rest || lines[1];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!teacher && lines.length > 1) {
|
|
|
+ teacher = lines[1];
|
|
|
+ const { weeksText: extractedWeeks, rest } = extractWeeksTextFromLine(teacher);
|
|
|
+ if (extractedWeeks) {
|
|
|
+ weeksText = weeksText || extractedWeeks;
|
|
|
+ teacher = rest;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!weeksText) {
|
|
|
+ for (const line of lines) {
|
|
|
+ const { weeksText: extractedWeeks } = extractWeeksTextFromLine(line);
|
|
|
+ if (extractedWeeks) {
|
|
|
+ weeksText = extractedWeeks;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (weekLineIndex >= 0 && weekLineIndex + 1 < lines.length) {
|
|
|
+ position = lines[weekLineIndex + 1];
|
|
|
+ }
|
|
|
+ if (!position) {
|
|
|
+ position =
|
|
|
+ lines.find((l) => l !== name && l !== teacher && !/周/.test(l)) || "";
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ name: name || "未知课程",
|
|
|
+ teacher: teacher || "",
|
|
|
+ weeksText,
|
|
|
+ position: position || "",
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ function padTime(value) {
|
|
|
+ const text = String(value || "").trim();
|
|
|
+ const match = text.match(/^(\d{1,2}):(\d{2})$/);
|
|
|
+ if (!match) return text;
|
|
|
+ const h = match[1].padStart(2, "0");
|
|
|
+ return `${h}:${match[2]}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function randomColor() {
|
|
|
+ return Math.floor(Math.random() * 12) + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ function createId() {
|
|
|
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
|
+ return crypto.randomUUID();
|
|
|
+ }
|
|
|
+ return `id-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
|
+ }
|
|
|
+
|
|
|
+ function mergeCourses(courses) {
|
|
|
+ const byKey = new Map();
|
|
|
+ courses.forEach((course) => {
|
|
|
+ const weeksKey = (course.weeks || []).join(",");
|
|
|
+ const key = [course.name, course.teacher, course.position, course.day, weeksKey].join("|");
|
|
|
+ if (!byKey.has(key)) byKey.set(key, []);
|
|
|
+ byKey.get(key).push({ ...course });
|
|
|
+ });
|
|
|
+
|
|
|
+ const merged = [];
|
|
|
+ byKey.forEach((items) => {
|
|
|
+ items.sort((a, b) => a.startSection - b.startSection);
|
|
|
+ let current = null;
|
|
|
+ items.forEach((item) => {
|
|
|
+ if (!current) {
|
|
|
+ current = { ...item };
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (item.startSection === current.endSection + 1) {
|
|
|
+ current.endSection = Math.max(current.endSection, item.endSection);
|
|
|
+ } else {
|
|
|
+ merged.push(current);
|
|
|
+ current = { ...item };
|
|
|
+ }
|
|
|
+ });
|
|
|
+ if (current) merged.push(current);
|
|
|
+ });
|
|
|
+
|
|
|
+ return merged;
|
|
|
+ }
|
|
|
+
|
|
|
+ function parseScheduleFromDocument(doc) {
|
|
|
+ const cells = Array.from(doc.querySelectorAll("td.cell"));
|
|
|
+ const fallbackCells = cells.length ? [] : Array.from(doc.querySelectorAll("td[id^='Cell']"));
|
|
|
+ const targetCells = cells.length ? cells : fallbackCells;
|
|
|
+ const courses = [];
|
|
|
+ const seen = new Set();
|
|
|
+
|
|
|
+ targetCells.forEach((cell) => {
|
|
|
+ const id = cell.getAttribute("id") || "";
|
|
|
+ const match = id.match(/^Cell(\d)(\d{1,2})$/);
|
|
|
+ if (!match) return;
|
|
|
+
|
|
|
+ const day = parseInt(match[1], 10);
|
|
|
+ const startSection = parseInt(match[2], 10);
|
|
|
+ const rowspan = parseInt(cell.getAttribute("rowspan") || "1", 10);
|
|
|
+ const endSection = startSection + Math.max(rowspan, 1) - 1;
|
|
|
+
|
|
|
+ const blocks = splitCourseBlocks(cell.innerText || "");
|
|
|
+ blocks.forEach((blockText) => {
|
|
|
+ const parsed = parseCourseBlock(blockText);
|
|
|
+ if (!parsed) return;
|
|
|
+ const weeks = parseWeekText(parsed.weeksText);
|
|
|
+ if (!weeks.length) return;
|
|
|
+
|
|
|
+ const key = [
|
|
|
+ parsed.name,
|
|
|
+ parsed.teacher,
|
|
|
+ parsed.position,
|
|
|
+ day,
|
|
|
+ startSection,
|
|
|
+ endSection,
|
|
|
+ weeks.join(","),
|
|
|
+ ].join("|");
|
|
|
+ if (seen.has(key)) return;
|
|
|
+ seen.add(key);
|
|
|
+
|
|
|
+ courses.push({
|
|
|
+ id: createId(),
|
|
|
+ name: parsed.name,
|
|
|
+ teacher: parsed.teacher,
|
|
|
+ position: parsed.position,
|
|
|
+ day,
|
|
|
+ startSection,
|
|
|
+ endSection,
|
|
|
+ color: randomColor(),
|
|
|
+ weeks,
|
|
|
+ });
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ return mergeCourses(courses);
|
|
|
+ }
|
|
|
+
|
|
|
+ function parseTimeSlots(doc) {
|
|
|
+ const slots = [];
|
|
|
+ const seenNumbers = new Set();
|
|
|
+ const timeRegex = /(\d{1,2}:\d{2})/g;
|
|
|
+
|
|
|
+ const timeCells = Array.from(
|
|
|
+ doc.querySelectorAll("td[data-jcindex], td[data-jcIndex]")
|
|
|
+ );
|
|
|
+
|
|
|
+ timeCells.forEach((cell) => {
|
|
|
+ const text = (cell.innerText || cell.textContent || "").trim();
|
|
|
+ if (!text) return;
|
|
|
+
|
|
|
+ const indexAttr = cell.getAttribute("data-jcindex") || cell.getAttribute("data-jcIndex");
|
|
|
+ const numberMatch = text.match(/^(\d{1,2})/);
|
|
|
+ const number = parseInt(indexAttr || (numberMatch && numberMatch[1]) || "", 10);
|
|
|
+ if (!Number.isFinite(number)) return;
|
|
|
+
|
|
|
+ const times = text.match(timeRegex) || [];
|
|
|
+ if (times.length < 2) return;
|
|
|
+
|
|
|
+ if (seenNumbers.has(number)) return;
|
|
|
+ seenNumbers.add(number);
|
|
|
+
|
|
|
+ slots.push({
|
|
|
+ number,
|
|
|
+ startTime: padTime(times[0]),
|
|
|
+ endTime: padTime(times[1]),
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ return slots.sort((a, b) => a.number - b.number);
|
|
|
+ }
|
|
|
+
|
|
|
+ async function run() {
|
|
|
+ toast("开始解析课表...");
|
|
|
+ const doc = await getTargetDocument();
|
|
|
+ if (!doc) {
|
|
|
+ toast("未找到课表页面 iframe");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ await waitForScheduleData(doc);
|
|
|
+
|
|
|
+ const courses = parseScheduleFromDocument(doc);
|
|
|
+ const timeSlots = parseTimeSlots(doc);
|
|
|
+
|
|
|
+ if (!courses.length) {
|
|
|
+ toast("未解析到课程,请确认课表已加载完成");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const result = await window.AndroidBridgePromise.saveImportedCourses(
|
|
|
+ JSON.stringify(courses)
|
|
|
+ );
|
|
|
+ if (result === true) {
|
|
|
+ if (timeSlots.length) {
|
|
|
+ await window.AndroidBridgePromise.savePresetTimeSlots(
|
|
|
+ JSON.stringify(timeSlots)
|
|
|
+ );
|
|
|
+ }
|
|
|
+ toast("课表导出成功");
|
|
|
+ window.AndroidBridge.notifyTaskCompletion();
|
|
|
+ } else {
|
|
|
+ toast("课表导出失败,请查看控制台日志");
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ toast("导出失败: " + error.message);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ run();
|
|
|
+})();
|