Forráskód Böngészése

Update adapter script cquet_01.js

Mutx666 1 hónapja
szülő
commit
583447d62d
1 módosított fájl, 629 hozzáadás és 0 törlés
  1. 629 0
      resources/CQUET/cquet_01.js

+ 629 - 0
resources/CQUET/cquet_01.js

@@ -0,0 +1,629 @@
+(async function () {
+  function toast(msg) {
+    try {
+      if (window.AndroidBridge && typeof window.AndroidBridge.showToast === "function") {
+        window.AndroidBridge.showToast(String(msg));
+      }
+    } catch (_) {}
+  }
+
+  async function fail(message, error) {
+    var detail = String(message || "导入失败");
+    if (error) {
+      detail += "\n" + (error.stack || error.message || String(error));
+    }
+    try {
+      console.error(detail, error || "");
+    } catch (_) {}
+    try {
+      if (window.AndroidBridgePromise && typeof window.AndroidBridgePromise.showAlert === "function") {
+        await window.AndroidBridgePromise.showAlert("提示", detail, "确定");
+      }
+    } catch (_) {}
+    throw new Error(detail);
+  }
+
+  function sleep(ms) {
+    return new Promise(function (resolve) { setTimeout(resolve, ms); });
+  }
+
+  async function waitFor(cond, timeout, interval) {
+    var start = Date.now();
+    timeout = timeout || 15000;
+    interval = interval || 300;
+    while (Date.now() - start < timeout) {
+      try {
+        var value = await cond();
+        if (value) return value;
+      } catch (_) {}
+      await sleep(interval);
+    }
+    return null;
+  }
+
+  function normalizeText(text) {
+    return String(text || "")
+      .replace(/\u00a0/g, " ")
+      .replace(/\r/g, "\n")
+      .replace(/\t/g, " ")
+      .replace(/[ ]+\n/g, "\n")
+      .replace(/\n[ ]+/g, "\n")
+      .replace(/[ ]{2,}/g, " ")
+      .replace(/\n{3,}/g, "\n\n")
+      .trim();
+  }
+
+  function getAccessibleDocuments() {
+    var docs = [];
+    function pushDoc(doc) {
+      if (doc && docs.indexOf(doc) === -1) docs.push(doc);
+    }
+    function scoreDoc(doc) {
+      try {
+        var score = 0;
+        var href = String((doc.location && doc.location.href) || "");
+        var title = normalizeText(doc.title || "");
+        var text = normalizeText((doc.body && doc.body.innerText) || "");
+        if (/\/xskb\/xskb_list\.do/i.test(href)) score += 100;
+        if (/学期理论课表/.test(title)) score += 50;
+        if (/学期理论课表/.test(text)) score += 30;
+        if (doc.querySelector("#xnxq01id")) score += 20;
+        if (doc.querySelector("#zc")) score += 10;
+        if (getCourseTable(doc)) score += 10;
+        return score;
+      } catch (_) {
+        return 0;
+      }
+    }
+    pushDoc(document);
+    Array.from(document.querySelectorAll("iframe")).forEach(function (iframe) {
+      try {
+        var doc = iframe.contentDocument || (iframe.contentWindow && iframe.contentWindow.document);
+        if (doc) pushDoc(doc);
+      } catch (_) {}
+    });
+    docs.sort(function (a, b) { return scoreDoc(b) - scoreDoc(a); });
+    return docs;
+  }
+
+  function getCourseTable(doc) {
+    return doc.querySelector("#kbtable") ||
+      doc.querySelector("#tab1") ||
+      doc.querySelector("table.kb_table") ||
+      doc.querySelector("table.kbtable") ||
+      doc.querySelector("table");
+  }
+
+  function isScheduleDoc(doc) {
+    try {
+      var text = normalizeText((doc.body && doc.body.innerText) || "");
+      if (/登录|用户名|密码/.test(text) && !/课表/.test(text)) return false;
+      if (/学期理论课表|我的课表/.test(text) && getCourseTable(doc)) return true;
+      if (doc.querySelector("#xnxq01id") && getCourseTable(doc)) return true;
+      return false;
+    } catch (_) {
+      return false;
+    }
+  }
+
+  function parseDayFromHeader(text) {
+    var m = normalizeText(text).match(/星期([一二三四五六日天])/);
+    if (!m) return 0;
+    return { "一": 1, "二": 2, "三": 3, "四": 4, "五": 5, "六": 6, "日": 7, "天": 7 }[m[1]] || 0;
+  }
+
+  function parseWeekText(weekText) {
+    var text = normalizeText(weekText);
+    if (!text) return [];
+    text = text.replace(/(/g, "(").replace(/)/g, ")");
+    text = text.replace(/\s+/g, "");
+    text = text.replace(/周次[::]?/g, "");
+    var odd = /单/.test(text);
+    var even = /双/.test(text);
+    text = text.replace(/\((?:单|双)\)/g, "");
+    text = text.replace(/[单双]/g, "");
+    text = text.replace(/\(周\)/g, "");
+    text = text.replace(/周/g, "");
+    text = text.replace(/[;;]/g, ",");
+    var result = [];
+    var seen = {};
+    text.split(/[,,]/).map(function (x) { return x.trim(); }).filter(Boolean).forEach(function (part) {
+      var range = part.match(/^(\d+)-(\d+)$/);
+      if (range) {
+        var start = parseInt(range[1], 10);
+        var end = parseInt(range[2], 10);
+        if (start > end) {
+          var t = start;
+          start = end;
+          end = t;
+        }
+        for (var i = start; i <= end; i++) {
+          if (odd && i % 2 === 0) continue;
+          if (even && i % 2 !== 0) continue;
+          if (!seen[i]) {
+            seen[i] = true;
+            result.push(i);
+          }
+        }
+        return;
+      }
+      var single = part.match(/^(\d+)$/);
+      if (single) {
+        var w = parseInt(single[1], 10);
+        if (odd && w % 2 === 0) return;
+        if (even && w % 2 !== 0) return;
+        if (!seen[w]) {
+          seen[w] = true;
+          result.push(w);
+        }
+      }
+    });
+    result.sort(function (a, b) { return a - b; });
+    return result;
+  }
+
+  function parseSectionText(text) {
+    var raw = normalizeText(text).replace(/(/g, "(").replace(/)/g, ")");
+    if (!raw) return null;
+    var m = raw.match(/\[(\d+(?:-\d+)*)节\]/) || raw.match(/\[(\d+(?:[-,,]\d+)*)小节\]/);
+    if (!m) return null;
+    var nums = (m[1].match(/\d+/g) || []).map(function (x) { return parseInt(x, 10); }).filter(function (n) { return !isNaN(n); });
+    if (!nums.length) return null;
+    return {
+      startSection: Math.min.apply(null, nums),
+      endSection: Math.max.apply(null, nums)
+    };
+  }
+
+  function extractWeekAndSectionLine(lines) {
+    for (var i = 0; i < lines.length; i++) {
+      if (/\(周\)/.test(lines[i]) && /\[\d+(?:-\d+)*节\]/.test(lines[i])) {
+        return { index: i, text: lines[i] };
+      }
+    }
+    return null;
+  }
+
+  function isMeaninglessLine(line) {
+    var text = normalizeText(line);
+    if (!text) return true;
+    if (/^(学期理论课表|理论课表|实践课表|课表查询|筛选|放大|时间模式[::]?.*|周次[::]?.*)$/.test(text)) return true;
+    return false;
+  }
+
+  function splitCoursesInCell(doc, cell) {
+    function getCellLines() {
+      var text = normalizeText(cell.innerText || cell.textContent || "");
+      if (!text) return [];
+      return text.split("\n").map(function (x) { return normalizeText(x); }).filter(Boolean).filter(function (line) {
+        return !isMeaninglessLine(line) && !/^[-]{3,}$/.test(line);
+      });
+    }
+
+    function splitByParagraphs() {
+      var ps = Array.from(cell.querySelectorAll("p"));
+      if (!ps.length) return [];
+      return ps.map(function (p) {
+        return normalizeText(p.innerText || p.textContent || "");
+      }).filter(Boolean).filter(function (t) {
+        return /\(周\)/.test(t) && /\[(?:\d+(?:[-,,]\d+)*)节\]/.test(t);
+      });
+    }
+
+    function splitBySequentialLines(lines) {
+      var blocks = [];
+      var i = 0;
+      while (i < lines.length) {
+        var line = lines[i];
+        if (!line) {
+          i++;
+          continue;
+        }
+        if (!/\(周\)/.test(line) || !/\[(?:\d+(?:[-,,]\d+)*)节\]/.test(line)) {
+          i++;
+          continue;
+        }
+
+        var parts = [];
+        if (i - 2 >= 0) {
+          parts.push(lines[i - 2]);
+          parts.push(lines[i - 1]);
+        } else if (i - 1 >= 0) {
+          parts.push(lines[i - 1]);
+        }
+        parts.push(line);
+        if (i + 1 < lines.length) parts.push(lines[i + 1]);
+
+        var cleaned = [];
+        parts.forEach(function (p) {
+          p = normalizeText(p);
+          if (!p) return;
+          if (/^[-]{3,}$/.test(p)) return;
+          cleaned.push(p);
+        });
+
+        if (cleaned.length) {
+          var last = cleaned[cleaned.length - 1];
+          if (/\(周\)/.test(last) && i + 1 < lines.length) cleaned.push(lines[i + 1]);
+          blocks.push(cleaned.join("\n"));
+        }
+        i += 2;
+      }
+      return blocks;
+    }
+
+    function splitCompactText(text) {
+      var lines = text.split("\n").map(function (x) { return normalizeText(x); }).filter(Boolean);
+      if (!lines.length) return [];
+      return splitBySequentialLines(lines);
+    }
+
+    var byP = splitByParagraphs();
+    if (byP.length) return byP;
+
+    var text2 = normalizeText(cell.innerText || cell.textContent || "");
+    if (!text2) return [];
+
+    var lineBlocks = splitBySequentialLines(getCellLines());
+    if (lineBlocks.length) return lineBlocks;
+
+    var normalized = text2
+      .replace(/([\u4e00-\u9fa5A-Za-z0-9()()《》·,,、\-\s]+?)\s+([0-9]+(?:-[0-9]+)?(?:[,,][0-9]+(?:-[0-9]+)?)*(?:\((?:单|双)\))?\(周\)\[[0-9\-,,]+节\])/g, function (_, a, b) {
+        return normalizeText(a) + "\n" + normalizeText(b);
+      })
+      .replace(/(\[[0-9\-,,]+节\])\s*([^\n\[]+)/g, function (_, a, b) {
+        return a + "\n" + normalizeText(b);
+      })
+      .replace(/\s{2,}/g, "\n");
+
+    var compactBlocks = splitCompactText(normalized);
+    if (compactBlocks.length) return compactBlocks;
+
+    return [];
+  }
+
+  function parseCourseBlock(text, day) {
+    var raw = normalizeText(text);
+    if (!raw) return null;
+    raw = raw.replace(/(/g, "(").replace(/)/g, ")");
+    if (!/\(周\)/.test(raw) || !/\[(?:\d+(?:[-,,]\d+)*)节\]/.test(raw)) return null;
+
+    var lines = raw.split("\n").map(function (x) { return normalizeText(x); }).filter(Boolean);
+    if (!lines.length) return null;
+
+    var wsIndex = -1;
+    for (var li = 0; li < lines.length; li++) {
+      if (/\(周\)/.test(lines[li]) && /\[(?:\d+(?:[-,,]\d+)*)节\]/.test(lines[li])) {
+        wsIndex = li;
+        break;
+      }
+    }
+
+    if (wsIndex <= 0) {
+      for (var i = 0; i < lines.length; i++) {
+        var line = lines[i];
+        if (!/\(周\)/.test(line)) continue;
+        var weekPartMatch = line.match(/([0-9,,\-]+(?:\((?:单|双)\))?\(周\))/);
+        var sectionPartMatch = line.match(/(\[(?:\d+(?:[-,,]\d+)*)节\])/);
+        if (weekPartMatch && sectionPartMatch) {
+          var prefix = normalizeText(line.slice(0, line.indexOf(weekPartMatch[1])));
+          var suffix = normalizeText(line.slice(line.indexOf(sectionPartMatch[1]) + sectionPartMatch[1].length));
+          var rebuilt = [];
+          if (prefix) rebuilt.push(prefix);
+          rebuilt.push(weekPartMatch[1] + sectionPartMatch[1]);
+          if (suffix) rebuilt.push(suffix);
+          lines.splice.apply(lines, [i, 1].concat(rebuilt));
+          wsIndex = prefix ? i + 1 : i;
+          break;
+        }
+      }
+    }
+
+    if (wsIndex <= 0) return null;
+
+    var wsLine = lines[wsIndex];
+    var weekMatch = wsLine.match(/([0-9,,\-]+(?:\((?:单|双)\))?\(周\))/);
+    if (!weekMatch) return null;
+    var weeks = parseWeekText(weekMatch[1]);
+    if (!weeks.length) return null;
+
+    var section = parseSectionText(wsLine);
+    if (!section) return null;
+
+    var name = lines[0] || "";
+    if (!name) return null;
+    if (/^[\d,\-,()\[\]单双周节小节]+$/.test(name)) return null;
+
+    var courseNature = "";
+    var natureMatch = name.match(/\[(必修|选修)\]/);
+    if (natureMatch) {
+      courseNature = natureMatch[1] === "必修" ? "required" : "elective";
+      name = name.replace(/\[(必修|选修)\]/g, "").trim();
+    }
+    name = name.replace(/\[(\d+)\]/g, "").trim();
+    if (!name) return null;
+
+    var beforeWeek = lines.slice(1, wsIndex);
+    var teacher = "";
+    var noteParts = [];
+
+    if (beforeWeek.length) {
+      if (beforeWeek.length === 1) {
+        teacher = beforeWeek[0];
+      } else {
+        teacher = beforeWeek[beforeWeek.length - 1] || "";
+        noteParts = beforeWeek.slice(0, -1);
+      }
+    }
+
+    if (/^\[[0-9]+(?:-[0-9]+)?\]班$/.test(teacher) || /^\d+$/.test(teacher) || /\(周\)|\[(?:\d+(?:[-,,]\d+)*)节\]/.test(teacher)) {
+      noteParts.push(teacher);
+      teacher = "";
+    }
+
+    var position = "";
+    for (var j = wsIndex + 1; j < lines.length; j++) {
+      var nextLine = lines[j];
+      if (!nextLine) continue;
+      position = nextLine;
+      break;
+    }
+
+    noteParts = noteParts.filter(Boolean).map(function (x) { return x.trim(); }).filter(Boolean);
+
+    var course = {
+      name: name,
+      teacher: teacher || "",
+      position: position || "",
+      day: Number(day),
+      startSection: section.startSection,
+      endSection: section.endSection,
+      weeks: weeks
+    };
+
+    course.location = course.position;
+    course.dayOfWeek = course.day;
+    course.startWeek = weeks[0];
+    course.endWeek = weeks[weeks.length - 1];
+    if (courseNature) course.courseNature = courseNature;
+    if (weeks.length && weeks.every(function (w) { return w % 2 === 1; })) course.isOddWeek = true;
+    if (weeks.length && weeks.every(function (w) { return w % 2 === 0; })) course.isEvenWeek = true;
+    if (noteParts.length) course.note = noteParts.join(" ");
+
+    return course;
+  }
+
+  function dedupeCourses(courses) {
+    var map = {};
+    var result = [];
+    courses.forEach(function (c) {
+      var key = [
+        c.name, c.teacher, c.position, c.day, c.startSection, c.endSection, (c.weeks || []).join(",")
+      ].join("||");
+      if (!map[key]) {
+        map[key] = true;
+        result.push(c);
+      }
+    });
+    return result;
+  }
+
+  function parseTimeSlots(doc) {
+    var table = getCourseTable(doc);
+    if (!table) return [];
+    var rows = Array.from(table.querySelectorAll("tr"));
+    var result = [];
+    var seen = {};
+    function toMin(v) {
+      var p = String(v).split(":");
+      return parseInt(p[0], 10) * 60 + parseInt(p[1], 10);
+    }
+    function toHHMM(mins) {
+      mins = Math.round(mins);
+      var h = Math.floor(mins / 60);
+      var m = mins % 60;
+      return String(h).padStart(2, "0") + ":" + String(m).padStart(2, "0");
+    }
+    rows.forEach(function (row, idx) {
+      if (idx === 0) return;
+      var firstCell = row.cells && row.cells[0];
+      if (!firstCell) return;
+      var txt = normalizeText(firstCell.innerText || firstCell.textContent || "");
+      var secMatch = txt.match(/(\d+(?:,\d+)*)节/);
+      var timeMatch = txt.match(/(\d{1,2}:\d{2})\s*-\s*(\d{1,2}:\d{2})/);
+      if (!secMatch || !timeMatch) return;
+      var nums = secMatch[1].split(",").map(function (x) { return parseInt(x, 10); }).filter(function (n) { return !isNaN(n); });
+      if (!nums.length) return;
+      var start = timeMatch[1].padStart(5, "0");
+      var end = timeMatch[2].padStart(5, "0");
+      var startMin = toMin(start);
+      var endMin = toMin(end);
+      var step = (endMin - startMin) / nums.length;
+      nums.forEach(function (num, index) {
+        if (seen[num]) return;
+        var item = {
+          number: num,
+          section: num,
+          startTime: toHHMM(startMin + step * index),
+          endTime: toHHMM(index === nums.length - 1 ? endMin : (startMin + step * (index + 1)))
+        };
+        seen[num] = true;
+        result.push(item);
+      });
+    });
+    result.sort(function (a, b) { return a.number - b.number; });
+    return result;
+  }
+
+  function parseCourseConfig(doc) {
+    var config = {
+      firstDayOfWeek: 1,
+      semesterStartDate: null
+    };
+
+    var termSelect = doc.querySelector("#xnxq01id");
+    if (termSelect) {
+      var selectedOption = termSelect.options && termSelect.selectedIndex >= 0 ? termSelect.options[termSelect.selectedIndex] : null;
+      var termValue = normalizeText((selectedOption && (selectedOption.value || selectedOption.text)) || termSelect.value || "");
+      if (termValue) {
+        config.term = termValue;
+        var m = termValue.match(/^(\d{4})-(\d{4})-(\d)$/);
+        if (m) {
+          config.schoolYear = m[1] + "-" + m[2];
+          config.termName = "第" + m[3] + "学期";
+        }
+      }
+    }
+
+    var weekSelect = doc.querySelector("#zc");
+    if (weekSelect) {
+      var maxWeek = 0;
+      Array.from(weekSelect.options || []).forEach(function (opt) {
+        var n = parseInt(opt.value, 10);
+        if (!isNaN(n) && n > maxWeek) maxWeek = n;
+      });
+      if (maxWeek > 0) {
+        config.totalWeeks = maxWeek;
+        config.semesterTotalWeeks = maxWeek;
+      }
+    }
+
+    config.defaultClassDuration = 45;
+    config.defaultBreakDuration = 10;
+
+    return config;
+  }
+
+  function parseFromTable(doc) {
+    var table = getCourseTable(doc);
+    if (!table) throw new Error("未找到课表表格");
+
+    var rows = Array.from(table.querySelectorAll("tr"));
+    if (rows.length < 2) throw new Error("课表表格行数不足");
+
+    var headerRow = rows[0];
+    var headerCells = Array.from((headerRow && headerRow.cells) || []);
+    if (headerCells.length < 8 && rows[0].querySelectorAll("th,td").length >= 8) {
+      headerCells = Array.from(rows[0].querySelectorAll("th,td"));
+    }
+    if (headerCells.length < 8) throw new Error("课表表头异常");
+
+    var dayMap = {};
+    for (var i = 1; i < headerCells.length; i++) {
+      var day = parseDayFromHeader(headerCells[i].innerText || headerCells[i].textContent || "");
+      if (day) dayMap[i] = day;
+    }
+    if (Object.keys(dayMap).length < 7) throw new Error("星期列识别不完整");
+
+    var courses = [];
+    for (var r = 1; r < rows.length; r++) {
+      var cells = Array.from(rows[r].cells || []);
+      if (cells.length < 8) continue;
+      for (var c = 1; c <= 7 && c < cells.length; c++) {
+        var dayNum = dayMap[c];
+        if (!dayNum) continue;
+        var cell = cells[c];
+        var cellText = normalizeText(cell.innerText || cell.textContent || "");
+        if (!cellText || isMeaninglessLine(cellText)) continue;
+        var blocks = splitCoursesInCell(doc, cell);
+        blocks.forEach(function (block) {
+          var parsed = parseCourseBlock(block, dayNum);
+          if (parsed) courses.push(parsed);
+        });
+      }
+    }
+
+    return {
+      courses: dedupeCourses(courses),
+      timeSlots: parseTimeSlots(doc),
+      config: parseCourseConfig(doc)
+    };
+  }
+
+  function toFinalCourse(course) {
+    var weeks = (course.weeks || []).map(function (x) { return Number(x); }).filter(function (x) { return !isNaN(x); }).sort(function (a, b) { return a - b; });
+    var day = Number(course.day);
+    var finalCourse = {
+      name: String(course.name || "").trim(),
+      teacher: String(course.teacher || "").trim(),
+      position: String(course.position || "").trim(),
+      day: day,
+      startSection: Number(course.startSection),
+      endSection: Number(course.endSection),
+      weeks: weeks
+    };
+    finalCourse.location = finalCourse.position;
+    finalCourse.dayOfWeek = day;
+    finalCourse.startWeek = weeks.length ? weeks[0] : 0;
+    finalCourse.endWeek = weeks.length ? weeks[weeks.length - 1] : 0;
+    finalCourse.customWeeks = weeks.slice();
+    if (course.courseNature) finalCourse.courseNature = course.courseNature;
+    if (course.note) finalCourse.note = String(course.note).trim();
+    if (course.isOddWeek) finalCourse.isOddWeek = true;
+    if (course.isEvenWeek) finalCourse.isEvenWeek = true;
+    return finalCourse;
+  }
+
+  async function main() {
+    if (!window.AndroidBridgePromise || !window.AndroidBridge || typeof window.AndroidBridge.notifyTaskCompletion !== "function") {
+      throw new Error("桥接接口不可用");
+    }
+
+    var targetDoc = await waitFor(function () {
+      var docs = getAccessibleDocuments();
+      for (var i = 0; i < docs.length; i++) {
+        var doc = docs[i];
+        try {
+          var href = String((doc.location && doc.location.href) || "");
+          if (/\/xskb\/xskb_list\.do/i.test(href) && getCourseTable(doc)) return doc;
+        } catch (_) {}
+      }
+      for (var j = 0; j < docs.length; j++) {
+        if (isScheduleDoc(docs[j])) return docs[j];
+      }
+      return null;
+    }, 20000, 300);
+
+    if (!targetDoc) {
+      throw new Error("当前页面不是可解析的课表页,或课表 iframe 未加载完成");
+    }
+
+    var parsed = parseFromTable(targetDoc);
+
+    if (!parsed || !parsed.courses || !parsed.courses.length) {
+      throw new Error("未解析到任何课程,请确认当前学期课表已加载且不是空课表");
+    }
+
+    var finalCourses = parsed.courses.map(toFinalCourse).filter(function (c) {
+      return c.name &&
+        c.day >= 1 && c.day <= 7 &&
+        c.startSection > 0 &&
+        c.endSection >= c.startSection &&
+        Array.isArray(c.weeks) &&
+        c.weeks.length > 0;
+    });
+
+    if (!finalCourses.length) {
+      throw new Error("课程字段转换后为空,无法保存");
+    }
+
+    await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(finalCourses));
+
+    if (parsed.timeSlots && parsed.timeSlots.length) {
+      await window.AndroidBridgePromise.savePresetTimeSlots(JSON.stringify(parsed.timeSlots));
+    }
+
+    if (parsed.config) {
+      await window.AndroidBridgePromise.saveCourseConfig(JSON.stringify(parsed.config));
+    }
+
+    toast("课表导入成功,共" + finalCourses.length + "门课程");
+    window.AndroidBridge.notifyTaskCompletion();
+  }
+
+  try {
+    await main();
+  } catch (error) {
+    await fail("解析学期理论课表失败", error);
+  }
+})();