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