Refined Elective

选课网体验增强

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Refined Elective
// @namespace    https://greasyfork.org/users/1429968
// @version      1.4.1
// @description  选课网体验增强
// @author       ha0xin
// @match        https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/*
// @exclude      https://elective.pku.edu.cn/elective2008/edu/pku/stu/elective/controller/courseQuery/goNested.do*
// @license      MIT License
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function () {
  "use strict";

  // --- 配置项 ---
  const CONFIG_HIGHLIGHT_ENABLED = "conflictHighlightEnabled";
  const CONFIG_PROGRESS_BAR_ENABLED = "progressBarEnabled";
  const CONFIG_TABLE_SORTER_ENABLED = "tableSorterEnabled";
  const CONFIG_PROGRESS_OVERFLOW_ENABLED = "progressOverflowEnabled";

  // =========================================================================
  // 功能一:课程冲突高亮
  // =========================================================================
  const conflictHighlighter = {
    // --- Properties to store column indexes ---
    courseNameColumnIndex: null,
    courseTimeColumnIndex: null,
    parentScope: null,

    // --- Helper functions ---
    parseTimeSegment(text) {
      const weekTypeMatch = text.match(/(每周|单周|双周)/);
      const weekType = weekTypeMatch ? weekTypeMatch[1] : "每周";
      const dayMatch = text.match(/周([一二三四五六日])/);
      const dayMap = { 一: 1, 二: 2, 三: 3, 四: 4, 五: 5, 六: 6, 日: 7 };
      const day = dayMap[dayMatch?.[1]] || null;
      const sectionMatch = text.match(/(\d+)~(\d+)节/);
      const sections = [];
      if (sectionMatch) {
        const start = parseInt(sectionMatch[1], 10);
        const end = parseInt(sectionMatch[2], 10);
        for (let i = start; i <= end; i++) sections.push(i);
      }
      return { weekType, day, sections };
    },
    parseCourseTime(cell) {
      const timeSegments = [];
      if (!cell) return timeSegments;
      const html = cell.innerHTML.replace(/<br\s*\/?>/g, "|");
      const texts = html.split("|").filter((t) => t.trim());
      texts.forEach((text) => {
        const cleanText = text.replace(/周数信息.*?节/g, "");
        const segment = this.parseTimeSegment(cleanText);
        if (segment.day && segment.sections.length > 0) {
          timeSegments.push(segment);
        }
      });
      return timeSegments;
    },
    isConflict(seg1, seg2) {
      const weekConflict = seg1.weekType === "每周" || seg2.weekType === "每周" || seg1.weekType === seg2.weekType;
      return weekConflict && seg1.day === seg2.day && seg1.sections.some((s) => seg2.sections.includes(s));
    },
    checkCoursesConflict(courses, selectedCourses) {
      const conflicts = new Map();
      courses.forEach((course) => {
        selectedCourses.forEach((selectedCourse) => {
          course.timeSegments.forEach((seg1) => {
            selectedCourse.timeSegments.forEach((seg2) => {
              if (this.isConflict(seg1, seg2)) {
                if (!conflicts.has(course.element)) {
                  conflicts.set(course.element, []);
                }
                if (!conflicts.get(course.element).includes(selectedCourse.name)) {
                  conflicts.get(course.element).push(selectedCourse.name);
                }
              }
            });
          });
        });
      });
      return conflicts;
    },
    extractCoursesFromPage(parent, timeColumnIndex, nameColumnIndex) {
      const rows = parent.querySelectorAll("table.datagrid tr:is(.datagrid-odd, .datagrid-even)");
      const allCourses = [];
      rows.forEach((row) => {
        const cells = row.querySelectorAll("td");
        if (cells.length > timeColumnIndex && cells.length > nameColumnIndex) {
          const timeCell = cells[timeColumnIndex];
          const timeSegments = this.parseCourseTime(timeCell);
          if (timeSegments.length > 0) {
            allCourses.push({
              element: row,
              name: cells[nameColumnIndex].textContent.trim(),
              timeSegments: timeSegments,
            });
          }
        }
      });
      return allCourses;
    },
    extractSelectedCourses() {
      const rows = document.querySelectorAll("table.datagrid tr:is(.datagrid-odd, .datagrid-even)");
      const selectedCourses = [];
      rows.forEach((row) => {
        const cells = row.querySelectorAll("td");
        if (cells.length < 10) return;
        const timeCell = cells[7];
        const statusCell = cells[9];
        const timeSegments = this.parseCourseTime(timeCell);
        if (
          timeSegments.length > 0 &&
          statusCell &&
          (statusCell.textContent.trim() === "已选上" || statusCell.textContent.trim() === "待抽签")
        ) {
          selectedCourses.push({
            element: row,
            name: cells[0].textContent.trim(),
            timeSegments: timeSegments,
          });
        }
      });
      return selectedCourses;
    },
    highlightConflicts(allCourses, conflictElements, courseNameColumnIndex) {
      allCourses.forEach((course) => {
        const courseElement = course.element.querySelector(`td:nth-child(${courseNameColumnIndex + 1})`);
        if (!courseElement) return;
        if (conflictElements.has(course.element)) {
          courseElement.style.backgroundColor = "#ffcccc";
          const conflictingCourses = conflictElements.get(course.element).join(", ");
          courseElement.title = `与以下已选课程冲突: ${conflictingCourses}`;
        } else {
          courseElement.style.backgroundColor = "#ccffcc";
          courseElement.title = "无时间冲突";
        }
      });
    },

    // --- Function to re-apply highlighting using saved settings ---
    reHighlight() {
      console.log("课程冲突高亮: 重新高亮...");
      if (this.courseNameColumnIndex === null || this.courseTimeColumnIndex === null) {
        console.log("课程冲突高亮: 未找到初始列索引,跳过重新高亮。");
        return;
      }
      const selectedCourses = JSON.parse(GM_getValue("selectedCourses", "[]"));
      if (selectedCourses.length === 0) return;

      const allCourses = this.extractCoursesFromPage(
        this.parentScope,
        this.courseTimeColumnIndex,
        this.courseNameColumnIndex
      );
      const conflictElements = this.checkCoursesConflict(allCourses, selectedCourses);
      this.highlightConflicts(allCourses, conflictElements, this.courseNameColumnIndex);
    },

    // --- Original run function, now saves its findings ---
    run() {
      const href = window.location.href;
      const isResultPage = href.includes("showResults.do");
      const isQueryPage =
        href.includes("courseQuery/") ||
        href.includes("getCurriculmByForm.do") ||
        href.includes("engGridFilter.do") ||
        href.includes("addToPlan.do");
      const isPlanPage = href.includes("electivePlan/");
      const isWorkPage = href.includes("electiveWork/");
      const isSupplyCancelPage = href.includes("supplement/");
      if (isResultPage) {
        console.log("课程冲突高亮: 正在已选课程页面提取数据...");
        const selectedCourses = this.extractSelectedCourses();
        GM_setValue("selectedCourses", JSON.stringify(selectedCourses));
        console.log("课程冲突高亮: 已选课程数据已存储", selectedCourses);
        alert("已成功更新已选课程列表!现在可以去其他页面查看冲突情况。");
        return;
      }
      const selectedCourses = JSON.parse(GM_getValue("selectedCourses", "[]"));
      if (selectedCourses.length === 0) {
        console.log("课程冲突高亮: 未找到已选课程数据,请先访问“已选课程”页面以同步数据。");
        return;
      }

      let nameIndex,
        timeIndex,
        parent = document;
      let pageType = "";
      if (isQueryPage) {
        pageType = "添加课程页面";
        const tableHeader = document.querySelector("table.datagrid tr[class*='datagrid-']");
        const isEngQueryPage = tableHeader && tableHeader.querySelector("th > form#engfilterForm") !== null;
        nameIndex = 1;
        timeIndex = isEngQueryPage ? 10 : 9;
      } else if (isPlanPage) {
        pageType = "选课计划页面";
        nameIndex = 1;
        timeIndex = 8;
      } else if (isWorkPage || isSupplyCancelPage) {
        pageType = isWorkPage ? "预选页面" : "补退选页面";
        const scopeSelector = isWorkPage ? "#scopeOneSpan" : "body";
        const container = document.querySelector(scopeSelector);
        if (!container) return;
        const allTrs = container.querySelectorAll("table > tbody > tr");
        const targetTr = Array.from(allTrs).find((tr) => tr.textContent.includes("选课计划中本学期可选列表"));
        if (targetTr && targetTr.nextElementSibling) {
          parent = targetTr.nextElementSibling;
          nameIndex = 0;
          timeIndex = 8;
        } else {
          return;
        }
      } else {
        return;
      }

      // Save the calculated indexes for later use
      this.courseNameColumnIndex = nameIndex;
      this.courseTimeColumnIndex = timeIndex;
      this.parentScope = parent;

      console.log(`课程冲突高亮: 正在分析 ${pageType}`);
      const allCourses = this.extractCoursesFromPage(parent, timeIndex, nameIndex);
      const conflictElements = this.checkCoursesConflict(allCourses, selectedCourses);
      this.highlightConflicts(allCourses, conflictElements, nameIndex);
    },
  };

  // =========================================================================
  // 功能二:课程空余及满员高亮显示(进度条版)
  // =========================================================================
  const progressBar = {
    COLORS: {
      LOW: "hsl(120, 70%, 80%)",
      MEDIUM: "hsl(55, 85%, 75%)",
      HIGH: "hsl(30, 90%, 80%)",
      FULL: "hsl(0, 100%, 90%)",
      OVERFLOW: "hsl(15, 100%, 85%)",
      EMPTY_BG: "hsl(0, 0%, 95%)",
      ZERO_LIMIT: "hsl(0, 0%, 88%)",
    },
    COLUMN_WIDTH: "120px",
    run() {
      console.log("课程容量进度条: 正在渲染...");
      const overflowEnabled = GM_getValue(CONFIG_PROGRESS_OVERFLOW_ENABLED, false);
      const tables = document.querySelectorAll("table.datagrid");
      tables.forEach((table) => {
        const headers = table.querySelectorAll("tr.datagrid-header th");
        if (headers.length === 0) return;
        const limitColumnIndex = Array.from(headers).findIndex((header) => header.textContent.trim().includes("限数/已选"));
        if (limitColumnIndex === -1) return;
        const limitHeader = headers[limitColumnIndex];
        if (limitHeader) {
          limitHeader.style.width = this.COLUMN_WIDTH;
        }
        const dataRows = table.querySelectorAll("tbody tr:is(.datagrid-odd, .datagrid-even, .datagrid-all)");
        console.log(`课程容量进度条: 找到 ${dataRows.length} 条课程数据行进行处理...`);
        dataRows.forEach((row) => {
          const cell = row.cells[limitColumnIndex];
          if (!cell) return;
          cell.style.textAlign = "center";
          cell.style.fontWeight = "500";
          cell.style.position = "relative";

          // 为溢出功能重置相关样式
          if (overflowEnabled) {
            cell.style.overflow = "visible";
            cell.style.zIndex = "1";
            cell.style.position = "relative"; // 确保定位上下文正确
          } else {
            cell.style.overflow = "hidden";
            cell.style.zIndex = "auto";
          }

          const text = cell.textContent.trim();
          if (!text.includes("/")) return;
          const [limitStr, selectedStr] = text.split("/");
          const limit = parseInt(limitStr.trim(), 10);
          const selected = parseInt(selectedStr.trim(), 10);
          if (isNaN(limit) || isNaN(selected)) return;
          if (limit === 0) {
            cell.style.backgroundColor = this.COLORS.ZERO_LIMIT;
            cell.title = "无名额限制";
            return;
          }

          const actualPercentage = (selected / limit) * 100;
          const displayPercentage = overflowEnabled ? actualPercentage : Math.min(actualPercentage, 100);
          cell.title = `已选: ${selected}, 限额: ${limit}, 占用率: ${actualPercentage.toFixed(1)}%`;

          let barColor;
          if (selected >= limit) {
            barColor = actualPercentage > 100 ? this.COLORS.OVERFLOW : this.COLORS.FULL;
          } else if (actualPercentage < 70) {
            barColor = this.COLORS.LOW;
          } else if (actualPercentage < 90) {
            barColor = this.COLORS.MEDIUM;
          } else {
            barColor = this.COLORS.HIGH;
          }

          if (overflowEnabled && actualPercentage > 100) {
            // 创建溢出的进度条效果
            const backgroundDiv = document.createElement("div");
            backgroundDiv.style.position = "absolute";
            backgroundDiv.style.top = "0";
            backgroundDiv.style.left = "0";
            backgroundDiv.style.width = `${displayPercentage}%`;
            backgroundDiv.style.height = "100%";
            backgroundDiv.style.backgroundColor = barColor;
            backgroundDiv.style.opacity = "0.6"; // 添加半透明效果
            backgroundDiv.style.zIndex = "-1"; // 确保在文字后面
            backgroundDiv.style.pointerEvents = "none";

            // 清除之前的背景div(如果存在)
            const existingDiv = cell.querySelector(".overflow-progress-bar");
            if (existingDiv) {
              existingDiv.remove();
            }

            backgroundDiv.className = "overflow-progress-bar";
            cell.appendChild(backgroundDiv);
            cell.style.background = this.COLORS.EMPTY_BG;
            cell.style.position = "relative"; // 确保文字内容显示在前面
            cell.style.zIndex = "1"; // 确保单元格内容在前面
          } else {
            // 移除可能存在的溢出div
            const existingDiv = cell.querySelector(".overflow-progress-bar");
            if (existingDiv) {
              existingDiv.remove();
            }

            // 使用原来的渐变背景方式
            cell.style.background = `linear-gradient(to right, ${barColor} ${displayPercentage}%, ${this.COLORS.EMPTY_BG} ${displayPercentage}%)`;
          }
        });
      });
    },
  };

  // =========================================================================
  // 功能三:课程排序
  // =========================================================================
  const tableSorter = {
    // 存储当前排序状态
    currentSortColumn: null,
    currentSortDirection: null,
    currentTable: null,

    run() {
      console.log("表格排序: 正在初始化...");
      document.querySelectorAll("table.datagrid").forEach((table) => {
        const tbody = table.querySelector("tbody");
        if (!tbody) return;
        const headerRow = tbody.querySelector("tr.datagrid-header");
        if (!headerRow) return;

        headerRow.querySelectorAll("th").forEach((header, index) => {
          if (header.querySelector("form, input, button")) return;

          header.style.cursor = "pointer";
          header.title = "点击排序";
          header.addEventListener("click", () => this.sort(table, index));
        });
      });
    },

    // 重新应用当前的排序规则
    reApplySort() {
      if (this.currentSortColumn !== null && this.currentTable) {
        console.log(`表格排序: 重新应用排序 - 列${this.currentSortColumn}, 方向${this.currentSortDirection}`);
        this.performSort(this.currentTable, this.currentSortColumn, this.currentSortDirection, false);
      }
    },
    sort(table, colIndex) {
      const tbody = table.querySelector("tbody");
      if (!tbody) return;

      const headerItem = tbody.querySelector(`tr.datagrid-header th:nth-child(${colIndex + 1})`);
      const currentDir = headerItem.dataset.sortDir || "desc";
      const newDir = currentDir === "desc" ? "asc" : "desc";
      
      // 保存排序状态
      this.currentSortColumn = colIndex;
      this.currentSortDirection = newDir;
      this.currentTable = table;
      
      this.performSort(table, colIndex, newDir, true);
    },

    performSort(table, colIndex, sortDir, updateHeader) {
      const tbody = table.querySelector("tbody");
      if (!tbody) return;

      const dataRows = Array.from(tbody.querySelectorAll("tr:is(.datagrid-odd, .datagrid-even, .datagrid-all)"));
      const allPaginationRows = Array.from(tbody.querySelectorAll('tr:has(form[name="pageForm"])'));

      if (dataRows.length === 0) return;

      let masterPaginationRow = null;
      if (allPaginationRows.length > 0) {
        masterPaginationRow = allPaginationRows[0];
      }

      if (updateHeader) {
        const headerItem = tbody.querySelector(`tr.datagrid-header th:nth-child(${colIndex + 1})`);
        headerItem.dataset.sortDir = sortDir;
        tbody
          .querySelectorAll("tr.datagrid-header th")
          .forEach((th) => (th.innerHTML = th.innerHTML.replace(/ [▲▼]$/, "")));
        headerItem.innerHTML += sortDir === "asc" ? " ▲" : " ▼";
      }

      const headerItem = tbody.querySelector(`tr.datagrid-header th:nth-child(${colIndex + 1})`);
      const headerText = headerItem.textContent.replace(/ [▲▼]$/, "").trim();
      const isCapacityColumn = headerText.startsWith("限数/已选");

      dataRows.sort((a, b) => {
        const cellA = a.cells[colIndex];
        const cellB = b.cells[colIndex];
        let valA, valB;
        if (isCapacityColumn) {
          const getPercentage = (cell) => {
            if (!cell) return 0;
            const text = cell.textContent.trim();
            if (!text.includes("/")) return -1;
            const [limitStr, selectedStr] = text.split("/");
            const limit = parseInt(limitStr.trim(), 10);
            const selected = parseInt(selectedStr.trim(), 10);
            if (isNaN(limit) || isNaN(selected) || limit === 0) {
              return 0;
            }
            return (selected / limit) * 100;
          };
          valA = getPercentage(cellA);
          valB = getPercentage(cellB);
        } else {
          valA = cellA.textContent.trim();
          valB = cellB.textContent.trim();
          const isNumeric = !isNaN(parseFloat(valA)) && isFinite(valA) && !isNaN(parseFloat(valB)) && isFinite(valB);
          if (isNumeric) {
            valA = parseFloat(valA);
            valB = parseFloat(valB);
          }
        }
        if (valA < valB) return sortDir === "asc" ? -1 : 1;
        if (valA > valB) return sortDir === "asc" ? 1 : -1;
        return 0;
      });

      dataRows.forEach((row) => row.remove());
      allPaginationRows.forEach((row) => row.remove());

      if (masterPaginationRow) {
        tbody.appendChild(masterPaginationRow);
      }

      dataRows.forEach((row, i) => {
        tbody.appendChild(row);
        row.className = i % 2 === 0 ? "datagrid-odd" : "datagrid-even";
      });

      if (masterPaginationRow) {
        tbody.appendChild(masterPaginationRow.cloneNode(true));
      }
    },
  };

  // =========================================================================
  // 功能四:加载所有分页数据
  // =========================================================================
  const allPagesLoader = {
    async fetchAllPages(button) {
      const table = button.closest("table.datagrid");
      const tbody = table.querySelector("tbody");
      const paginationSelect = document.querySelector('select[name="netui_row"]');

      if (!table || !tbody || !paginationSelect) {
        alert("无法找到分页元素!");
        return;
      }

      button.textContent = "正在加载中...";
      button.disabled = true;

      // 1. 找到包含下拉菜单的表单
      const pageForm = paginationSelect.closest('form[name="pageForm"]');
      if (!pageForm) {
        alert("关键的翻页表单 'pageForm' 未找到!");
        button.textContent = "初始化失败";
        button.disabled = false;
        return;
      }

      // 获取当前默认的分页选项
      const paginationOptionElement = paginationSelect.querySelector("option[selected]");
      const currentPage = parseInt(paginationOptionElement.innerText);
      console.log(`当前页: ${currentPage}`);

      // 2. 从表单的 action 属性构建基础 URL
      const baseActionUrl = new URL(pageForm.action, window.location.href).href;

      // 3. 获取表单中所有参数(包括所有隐藏的查询条件)
      const formParams = new URLSearchParams();

      // 4. 获取所有需要抓取的页面选项(跳过当前页)
      const pageOptions = Array.from(paginationSelect.options).filter((opt) => parseInt(opt.innerText) !== currentPage);

      // 5. 遍历页面选项,生成每一个页面的准确 URL
      const urlsToFetch = pageOptions.map((opt) => {
        // 在表单参数副本上,设置正确的页码
        formParams.set("netui_row", opt.value);
        // 组合成最终的 URL
        return `${baseActionUrl}?${formParams.toString()}`;
      });

      console.log("所有页 URL:");
      console.log(urlsToFetch);

      if (urlsToFetch.length === 0) {
        button.textContent = "只有一页";
        return;
      }

      console.log(`准备获取 ${urlsToFetch.length} 个页面的数据...`);

      try {
        const responses = await Promise.all(urlsToFetch.map((url) => fetch(url)));
        const htmlStrings = await Promise.all(responses.map((res) => res.text()));

        const parser = new DOMParser();
        const newRows = [];
        htmlStrings.forEach((html) => {
          const doc = parser.parseFromString(html, "text/html");
          const rows = doc.querySelectorAll("table.datagrid tbody tr:is(.datagrid-odd, .datagrid-even)");
          newRows.push(...rows);
        });

        console.log(`成功获取了 ${newRows.length} 条新的课程数据。`);

        // 移除页面底部的所有翻页行
        const allPaginationRows = tbody.querySelectorAll('tr:has(form[name="pageForm"])');
        allPaginationRows.forEach((row) => row.remove());

        // 将新抓取的数据行添加到表格中
        const fragment = document.createDocumentFragment();
        newRows.forEach((row) => fragment.appendChild(row));
        tbody.appendChild(fragment);

        button.textContent = "全部加载完毕!";
        button.style.backgroundColor = "#90ee90";

        // 重新计算并设置所有数据行的奇偶样式
        const allDataRows = tbody.querySelectorAll("tr:is(.datagrid-odd, .datagrid-even)");
        allDataRows.forEach((row, i) => {
          row.className = i % 2 === 0 ? "datagrid-odd" : "datagrid-even";
        });

        // 重新应用排序(如果之前有排序的话)
        if (GM_getValue(CONFIG_TABLE_SORTER_ENABLED, true)) {
          tableSorter.reApplySort();
        }

        // 重新应用冲突高亮
        if (GM_getValue(CONFIG_HIGHLIGHT_ENABLED, true)) {
          conflictHighlighter.reHighlight();
        }

        // 重新计算进度条
        if (GM_getValue(CONFIG_PROGRESS_BAR_ENABLED, true)) {
          progressBar.run();
        }
      } catch (error) {
        console.error("加载所有页面时出错:", error);
        button.textContent = "加载失败,请查看控制台";
        button.style.backgroundColor = "#ffcccb";
        button.disabled = false;
      }
    },

    run() {
      console.log("加载所有页: 正在初始化...");
      const paginationCell = document.querySelector('tr:has(form[name="pageForm"]) > td[align="right"]');
      // 总页数
      const totalPages = Array.from(paginationCell.querySelectorAll("option")).length;
      console.log(`加载所有页: 检测到总页数为 ${totalPages}`);
      if (totalPages <= 1) return;
      if (paginationCell && !document.getElementById("load-all-pages-btn")) {
        const loadButton = document.createElement("button");
        loadButton.id = "load-all-pages-btn";
        loadButton.textContent = "✨ 加载所有页";
        loadButton.style.marginLeft = "15px";
        loadButton.style.padding = "2px 8px";
        loadButton.style.cursor = "pointer";
        loadButton.style.border = "1px solid #ccc";
        loadButton.style.borderRadius = "4px";

        loadButton.addEventListener("click", (e) => this.fetchAllPages(e.target));

        paginationCell.appendChild(loadButton);
      }
    },
  };

  // =========================================================================
  // 脚本菜单注册与主执行逻辑
  // =========================================================================
  function setupMenu() {
    let highlightEnabled = GM_getValue(CONFIG_HIGHLIGHT_ENABLED, true);
    let progressBarEnabled = GM_getValue(CONFIG_PROGRESS_BAR_ENABLED, true);
    let tableSorterEnabled = GM_getValue(CONFIG_TABLE_SORTER_ENABLED, true);
    let progressOverflowEnabled = GM_getValue(CONFIG_PROGRESS_OVERFLOW_ENABLED, false);

    GM_registerMenuCommand(`${highlightEnabled ? "✅ 禁用" : "❌ 启用"} 课程冲突高亮`, () => {
      const newState = !highlightEnabled;
      GM_setValue(CONFIG_HIGHLIGHT_ENABLED, newState);
      alert(`课程冲突高亮功能已${newState ? "启用" : "禁用"}。\n请刷新页面以应用更改。`);
      location.reload();
    });
    GM_registerMenuCommand(`${progressBarEnabled ? "✅ 禁用" : "❌ 启用"} 容量进度条`, () => {
      const newState = !progressBarEnabled;
      GM_setValue(CONFIG_PROGRESS_BAR_ENABLED, newState);
      alert(`课程容量进度条功能已${newState ? "启用" : "禁用"}。\n请刷新页面以应用更改。`);
      location.reload();
    });
    GM_registerMenuCommand(`${progressOverflowEnabled ? "✅ 禁用" : "❌ 启用"} 进度条溢出显示`, () => {
      const newState = !progressOverflowEnabled;
      GM_setValue(CONFIG_PROGRESS_OVERFLOW_ENABLED, newState);
      alert(
        `进度条溢出显示功能已${
          newState ? "启用" : "禁用"
        }。\n启用后,选课人数超过限额的课程进度条会向右溢出显示真实长度。\n请刷新页面以应用更改。`
      );
      location.reload();
    });
    GM_registerMenuCommand(`${tableSorterEnabled ? "✅ 禁用" : "❌ 启用"} 表格排序`, () => {
      const newState = !tableSorterEnabled;
      GM_setValue(CONFIG_TABLE_SORTER_ENABLED, newState);
      alert(`表格排序功能已${newState ? "启用" : "禁用"}。\n请刷新页面以应用更改。`);
      location.reload();
    });
  }

  function main() {
    // 依次执行各项功能
    if (GM_getValue(CONFIG_PROGRESS_BAR_ENABLED, true)) {
      progressBar.run();
    }
    if (GM_getValue(CONFIG_TABLE_SORTER_ENABLED, true)) {
      tableSorter.run();
    }
    if (GM_getValue(CONFIG_HIGHLIGHT_ENABLED, true)) {
      conflictHighlighter.run();
    }
    allPagesLoader.run();
  }

  // --- 脚本启动 ---
  setupMenu();
  if (document.readyState === "complete" || document.readyState === "interactive") {
    main();
  } else {
    window.addEventListener("load", main);
  }
})();