您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
在「已选课程」中显示课程余量
// ==UserScript== // @name 显示课程余量 // @namespace https://xk.fudan.edu.cn/ // @version 1.0 // @description 在「已选课程」中显示课程余量 // @author Oneton // @include *://xk.fudan.edu.cn/course-selection/* // @icon https://www.fudan.edu.cn/_upload/tpl/00/0e/14/template14/images/favicon.ico // @grant none // @license GNU // ==/UserScript== (function() { 'use strict'; var courseList = []; var courseRemain = []; function getCookieValue(name) { const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)')); return match ? match[2] : null; } function getCourseList(uid, termId) { const token = getCookieValue('cs-course-select-student-token'); if (!token) { console.error('未找到授权令牌'); return Promise.reject('未找到授权令牌'); } const url = `https://xk.fudan.edu.cn/api/v1/student/course-select/selected-lessons/${termId}/${uid}`; return fetch(url, { method: 'GET', headers: { 'Authorization': token, 'Content-Type': 'application/json' } }) .then(response => { if (!response.ok) { throw new Error(`请求失败: ${response.status} ${response.statusText}`); } return response.json(); }) .then(data => { console.log('获取课程列表成功:', data); return data; }) .catch(error => { throw error; }); } function getCourseRemain(courseList) { // 提取课程ID const lessonIds = courseList.map(course => course.id).join(','); if (!lessonIds) { console.error('没有课程ID'); return Promise.reject('没有课程ID'); } // 构建URL和参数 const url = `https://xk.fudan.edu.cn/api/v1/student/course-select/std-count?lessonIds=${lessonIds}`; const token = getCookieValue('cs-course-select-student-token'); if (!token) { console.error('未找到授权令牌'); return Promise.reject('未找到授权令牌'); } // 发送请求 return fetch(url, { method: 'GET', headers: { 'Authorization': token, 'Content-Type': 'application/json' } }) .then(response => { if (!response.ok) { throw new Error(`请求失败: ${response.status} ${response.statusText}`); } return response.json(); }) .then(data => { console.log('获取课程余量成功:', data); return data; }) .catch(error => { console.error('获取课程余量失败:', error); throw error; }); } function displayCourseRemain(courseList, remainData) { if (!courseList || !remainData) { console.error('课程列表或余量数据为空'); return; } console.log('准备显示课程余量'); // 等待表格元素加载完成 function waitForSelectedLessonTable() { return new Promise(resolve => { function checkTable() { // 选择已选课程表格 const tableHeader = document.querySelector('#pane-selectedLesson .el-table__header-wrapper'); const tableBody = document.querySelector('#pane-selectedLesson .el-table__body-wrapper'); if (tableHeader && tableBody && tableBody.querySelectorAll('tbody tr').length > 0) { resolve({ header: tableHeader, body: tableBody, rows: tableBody.querySelectorAll('tbody tr') }); } else { setTimeout(checkTable, 100); } } checkTable(); }); } waitForSelectedLessonTable().then(({header, body, rows}) => { // 1. 修改表头 - 将"是否包含A+成绩"列改为"已选/人数上限" const headerRow = header.querySelector('thead tr'); const headers = headerRow.querySelectorAll('th'); // 查找"是否包含A+成绩"列的索引 let targetColumnIndex = -1; headers.forEach((th, index) => { const cellText = th.textContent.trim(); if (cellText.includes('是否含A+成绩') || cellText.includes('已选/人数上限') ) { targetColumnIndex = index; // 修改表头文本 th.querySelector('.cell').textContent = '已选/人数上限'; } }); if (targetColumnIndex === -1) { console.error('未找到"是否含A+成绩"列'); return; } console.log("A+成绩列:", targetColumnIndex); // 2. 修改每行的对应单元格 rows.forEach(row => { // 获取课程ID const courseCodeDiv = row.querySelector('.lesson-code'); if (!courseCodeDiv) return; // 从课程信息中提取ID const courseCode = courseCodeDiv.innerHTML.trim(); // 获取该行的所有单元格 const cells = row.querySelectorAll('td'); if (cells.length <= targetColumnIndex) return; // 获取对应的单元格 const targetCell = cells[targetColumnIndex]; const cellDiv = targetCell.querySelector('.cell'); if (!cellDiv) return; // 查找对应的余量信息 const remain = remainData[courseList.find(course => course.code === courseCode).id]; console.log("Remain", courseCode, remain); if (!remain) return; const r = parseInt(remain.split('-')[0]) const limit = courseList.find(course => course.code === courseCode).limitCount const percentage = limit > 0 ? (r / limit * 100) : 0; cellDiv.innerHTML = ` <div data-v-15fd4cd3="">${remain}/${limit}</div> <div data-v-15fd4cd3="" role="progressbar" aria-valuenow="${percentage}" aria-valuemin="0" aria-valuemax="100" class="el-progress el-progress--line el-progress--without-text"> <div class="el-progress-bar"> <div class="el-progress-bar__outer" style="height: 6px; background-color: rgb(235, 238, 245);"> <div class="el-progress-bar__inner" style="width: ${percentage.toFixed(4)}%; background-color: ${r > limit ? "rgb(255, 107, 107)" : "rgb(6, 86, 139)"};"> </div> </div> </div> </div> `; }); }); } function main(uid, termId) { console.log(`处理已选课程`); getCourseList(uid, termId) .then(data => { courseList = data.data; getCourseRemain(courseList) .then(data => { courseRemain = data.data; displayCourseRemain(courseList, courseRemain); }) .catch(error => { console.error('处理课程数据时出错:', error); }); }) .catch(error => { console.error('处理课程数据时出错:', error); }); } function checkPath() { const hashPattern = /^#\/course-select\/(\d+)\/turn\/(\d+)\/select$/; const match = hashPattern.exec(window.location.hash); if (match) { return { isMatch: true, uid: match[1], termId: match[2] }; } return { isMatch: false }; } function waitSelectedLessonLoaded(uid, termId) { console.log(`Script loaded! UID: ${uid}, Term ID: ${termId}`); // 添加标志位,避免重复处理 let isProcessing = false; // 等待页面元素加载完成 function waitForElement(selector) { return new Promise(resolve => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver(() => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } // 监听已选课程标签的点击事件 waitForElement('#tab-selectedLesson').then(tabElement => { // 定义一个防抖函数 function debounce(func, wait) { let timeout; return function() { const context = this; const args = arguments; clearTimeout(timeout); timeout = setTimeout(() => { func.apply(context, args); }, wait); }; } // 使用防抖处理函数 const debouncedHandler = debounce(() => { if (tabElement.getAttribute('aria-selected') === 'true' && !isProcessing) { isProcessing = true; main(uid, termId); // 设置延时重置状态,允许下次处理 setTimeout(() => { isProcessing = false; }, 500); } }, 100); // 如果一开始就是激活状态,也执行一次(防抖) debouncedHandler(); // 监听点击事件 tabElement.addEventListener('click', debouncedHandler); // 监听标签页容器,因为有可能通过其他方式切换标签 const tabsContainer = tabElement.parentElement; if (tabsContainer) { tabsContainer.addEventListener('click', debouncedHandler); } }); } window.addEventListener('load', function() { const pathInfo = checkPath(); if (pathInfo.isMatch) { waitSelectedLessonLoaded(pathInfo.uid, pathInfo.termId); } }); })();