显示课程余量

在「已选课程」中显示课程余量

目前為 2025-05-15 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

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