Refined Elective

选课网体验增强

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