Browse Source

Merge pull request #228 from XingHeYuZhuan/pending

Pending
星河欲转 3 tuần trước cách đây
mục cha
commit
ebeef4b225

+ 10 - 1
index/root_index.yaml

@@ -33,6 +33,11 @@ schools:
     initial: "C"
     resource_folder: "CQU"
 
+  - id: "CQCST"
+    name: "重庆城市科技学院"
+    initial: "C"
+    resource_folder: "CQCST"
+
   - id: "CUST"
     name: "长春理工大学"
     initial: "C"
@@ -347,6 +352,10 @@ schools:
     name: "江苏大学"
     initial: "J"
     resource_folder: "UJS"
+  - id: "CQUET"
+    name: "重庆电子科技职业大学"
+    initial: "C"
+    resource_folder: "CQUET"
 
   - id: "YIBINU"
     name: "宜宾学院"
@@ -371,4 +380,4 @@ schools:
   - id: "SDNU"
     name: "山东师范大学"
     initial: "S"
-    resource_folder: "SDNU"
+    resource_folder: "SDNU"

+ 9 - 0
resources/CQCST/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/CQRK/adapters.yaml
+adapters:
+  - adapter_id: "CQCST_01"
+    adapter_name: "重庆城市科技学院强智适配"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "cqcst_01.js"
+    import_url: "http://jw.cqcst.edu.cn/cqdxcskjxy_jsxsd/"
+    maintainer: "Mutx163"
+    description: "登录后需要点击进入教务系统才可以导入,重庆城市科技学院强智适配,"

+ 137 - 0
resources/CQCST/cqcst_01.js

@@ -0,0 +1,137 @@
+async function runImportFlow() {
+    // 兼容电脑端测试
+    if (typeof window.AndroidBridgePromise === 'undefined') {
+        window.AndroidBridgePromise = {
+            showAlert: async () => true,
+            saveImportedCourses: async (json) => {
+                console.log("===============================");
+                console.log("🎉 【解析成功】以下是整理好的课表数据:");
+                console.table(JSON.parse(json)); 
+                console.log("===============================");
+                alert("抓取成功!请在 F12 控制台查看具体的课程数据格式。");
+                return true;
+            }
+        };
+        window.AndroidBridge = {
+            showToast: (msg) => console.log("[系统提示] " + msg),
+            notifyTaskCompletion: () => console.log("[流程结束] 任务已完成并通知APP")
+        };
+    }
+
+    AndroidBridge.showToast("开始提取课表数据...");
+
+    const table = document.getElementById('kbtable') || document.querySelector('.table_border') || document.querySelector('table');
+    if (!table || !table.innerText.includes('星期')) {
+        AndroidBridge.showToast("没找到课表!请确保您当前在“学期理论课表”页面。");
+        return;
+    }
+
+    const alertConfirmed = await window.AndroidBridgePromise.showAlert(
+        "强智教务解析",
+        "已检测到课表页面,是否提取数据并导入?",
+        "确认导入"
+    );
+    if (!alertConfirmed) return;
+
+    try {
+        let courses = [];
+        let courseSet = new Set(); 
+        let rows = table.querySelectorAll('tr');
+
+        // 遍历课表每一行(跳过第一行的表头)
+        for (let i = 1; i < rows.length; i++) {
+            // 【关键修复1】同时获取 th 和 td,防止错位
+            let cells = rows[i].querySelectorAll('td, th'); 
+            
+            for (let j = 0; j < cells.length; j++) {
+                let cell = cells[j];
+                
+                // 【关键修复2】逆向计算星期几:倒数第7列永远是周一,倒数第1列永远是周日
+                // 这能完美解决强智系统左侧节次列导致的数据错位问题
+                let day = 7 - (cells.length - 1 - j);
+                if (day < 1 || day > 7) continue; // 如果算出来不是1-7,说明是左侧的节次列,跳过
+
+                let blocks = cell.innerText.split(/-{5,}/).map(t => t.trim()).filter(t => t);
+
+                for (let block of blocks) {
+                    if (!block || block === ' ' || block === '') continue;
+                    
+                    let lines = block.split(/\n/).map(l => l.trim()).filter(l => l);
+                    if(lines.length < 4) {
+                        lines = block.split(/\s+/).map(l => l.trim()).filter(l => l);
+                    }
+                    if (lines.length < 3) continue;
+
+                    let name = lines[0].replace(/\[.*?\]/g, '').trim();
+                    let teacher = lines[1] || "未知";
+
+                    let timeRegex = /([\d\-,]+)(?:\((单|双|.*?)\))?.*?\[([\d\-]+)节\]/;
+                    let timeLineIdx = lines.findIndex(l => timeRegex.test(l));
+                    if (timeLineIdx === -1) continue;
+
+                    let match = lines[timeLineIdx].match(timeRegex);
+                    let weeksStr = match[1]; 
+                    let oddEven = match[2];  
+                    let sectionsStr = match[3]; 
+
+                    let position = (timeLineIdx + 1 < lines.length) ? lines[timeLineIdx + 1] : "未知地点";
+
+                    let weeks = [];
+                    let weekParts = weeksStr.split(',');
+                    for (let wp of weekParts) {
+                        if (wp.includes('-')) {
+                            let parts = wp.split('-');
+                            let start = parseInt(parts[0]);
+                            let end = parseInt(parts[1]);
+                            for (let w = start; w <= end; w++) {
+                                if (oddEven === '单' && w % 2 === 0) continue;
+                                if (oddEven === '双' && w % 2 !== 0) continue;
+                                weeks.push(w);
+                            }
+                        } else {
+                            weeks.push(parseInt(wp));
+                        }
+                    }
+
+                    let secParts = sectionsStr.split('-');
+                    let startSection = parseInt(secParts[0]);
+                    let endSection = parseInt(secParts[secParts.length - 1]);
+
+                    let uid = `${name}-${day}-${startSection}-${endSection}-${weeks.join(',')}`;
+                    if (!courseSet.has(uid)) {
+                        courseSet.add(uid);
+                        courses.push({
+                            name: name,
+                            teacher: teacher,
+                            position: position,
+                            day: day,
+                            startSection: startSection,
+                            endSection: endSection,
+                            weeks: weeks
+                        });
+                    }
+                }
+            }
+        }
+
+        if (courses.length === 0) {
+            AndroidBridge.showToast("没有抓取到数据,可能当前表格为空。");
+            return;
+        }
+
+        AndroidBridge.showToast(`提取成功,共发现 ${courses.length} 门课程,正在保存...`);
+        
+        const saveResult = await window.AndroidBridgePromise.saveImportedCourses(JSON.stringify(courses));
+        
+        if (saveResult) {
+            AndroidBridge.showToast("导入大功告成!");
+            AndroidBridge.notifyTaskCompletion(); 
+        }
+
+    } catch (error) {
+        console.error("解析过程中发生错误:", error);
+        AndroidBridge.showToast("解析出错啦: " + error.message);
+    }
+}
+
+runImportFlow();

+ 9 - 0
resources/CQUET/adapters.yaml

@@ -0,0 +1,9 @@
+# resources/CQUET/adapters.yaml
+adapters:
+  - adapter_id: "CQUET_01"
+    adapter_name: "强智"
+    category: "BACHELOR_AND_ASSOCIATE"
+    asset_js_path: "cquet_01.js"
+    import_url: "https://cquet.edu.cn/"
+    maintainer: "Mutx666"
+    description: ""

+ 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);
+  }
+})();