// ==UserScript==
// @name 东南大学抢课助手改版(终极版)-2025.9更新
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description 听说你抢不到课
// @author realhuhu,一只路过的毒蘑菇
// @license MIT
// @match https://newxk.urp.seu.edu.cn/xsxk/elective/grablessons?*
// @run-at document-loaded
// @icon https://huhu-1304907527.cos.ap-nanjing.myqcloud.com/share/qkzs
// ==/UserScript==
(function () {
//版本
let version = [1, 0, 0]
//请求
let request = axios.create();
//提示
let tip = grablessonsVue.$message
//设置
let settings = {
auto: false,
batchSize: 1,
intervalMs: 300
}
//所选课程
let enrollDict = {};
//挂载的顶层组件
let app = document.getElementById("xsxkapp");
//定时器ID
let grabTimer = null;
//组件生成
(self => {
//生成组件
self.mount = () => {
self.createTag()
self.createPanel()
self.createMask()
}
//生成节点
self.createNode = ({tagName, text, HTML, obj, ev, children}) => {
let node = document.createElement(tagName)
if (obj) {
for (let key of Object.keys(obj)) {
node.setAttribute(key, obj[key])
}
}
if (text) {
node.innerText = text
}
if (HTML) {
node.innerHTML = HTML
}
if (ev) {
for (let key of Object.keys(ev)) {
node.addEventListener(key, ev[key])
}
}
if (children) {
children.map(x => node.appendChild(x))
}
return node
}
//生成打开和关闭面板的按钮
self.createTag = () => {
let node = self.createNode({
tagName: "div",
obj: {
"class": "slideMenu",
"style": `
position: fixed;
top: 250px;
left:30px;width:
40px;z-index: 1314;
`
},
children: [self.createNode(
{
tagName: "div",
obj: {
"class": "centre-btn item el-icon-date",
"style": `background-color: #2b2b2b`
},
ev: {
"mousedown": e => {
methods.drag(e, node)
}
}
})]
})
app.appendChild(node)
}
//生成面板
self.createPanel = () => {
app.appendChild(self.createNode({
tagName: "div",
obj: {
"id": "panel",
"style": `
position: fixed;
right: 0;
top: 0;
z-index: 520;
width: 350px;
height: 100%;
background-color: rgba(61,72,105,0.8);
display: none;
`
},
children: [
self.createNode({tagName: "hr"}),
self.createNode({
tagName: "h1",
text: "东大抢课脚本",
obj: {
"style": "color: #c7e6e6; text-align: center",
}
}),
self.createNode({tagName: "hr"}),
self.createNode({
tagName: "input",
obj: {
"id": "input-box",
"class": "el-input__inner",
"style": `
width: 96%;
margin-left: 2%;
height: 30px;
`,
"placeholder": "输入课程代码(不区分大小写),按回车确定"
},
ev: {
"keydown": methods.enter
}
}),
// 一键捡漏按钮
self.createNode({
tagName: "button",
obj: {
"id": "start-grab-button",
"class": "el-button el-button--primary el-button--small is-round",
"style": `
margin: 20px;
position: absolute;
right: 30%;
bottom: 25%;
`
},
text: "开始捡漏",
ev: {
"click": () => {
if (!methods.isRunning) {
methods.enroll();
tip({
type: "success",
message: "自动抢课已启动",
duration: 1000
});
} else {
tip({
type: "info",
message: "抢课已在进行中",
duration: 1000
});
}
}
}
}),
// 停止捡漏按钮
self.createNode({
tagName: "button",
obj: {
"id": "stop-grab-button",
"class": "el-button el-button--danger el-button--small is-round",
"style": `
margin: 20px;
position: absolute;
right: 30%;
bottom: 20%;
`
},
text: "停止捡漏",
ev: {
"click": () => {
if (grabTimer) {
clearInterval(grabTimer);
grabTimer = null;
}
if (methods.isRunning) {
methods.isRunning = false;
tip({
type: "info",
message: "自动抢课已停止",
duration: 1000
});
}
}
}
}),
self.createNode({
tagName: "div",
obj: {
"id": "list-wrap",
"style": `
overflow: auto;
margin: 10px;
border: 1px solid white;
height: 50%;
`
}
}),
self.createNode({
tagName: "button",
obj: {
"id": "enroll-button",
"class": "el-button el-button--primary el-button--small is-round",
"style": `
margin: 20px;
position: absolute;
right: 2%;
bottom: 25%;
`
},
text: "一键抢课",
ev: {
"click": () => {
methods.enroll();
}
}
}),
self.createNode({
tagName: "button",
obj: {
"id": "advanced-settings-button",
"class": "el-button el-button--default el-button--small is-round",
"style": `
margin: 20px;
position: absolute;
right: 2%;
bottom: 20%;
`
},
text: "高级设置",
ev: {
"click": () => {
document.getElementById("mask").style.display = "block";
self.createPopUp("高级设置", self.createAdvancedPop());
}
}
}),
self.createNode({
tagName: "button",
obj: {
"id": "export-button",
"class": "el-button el-button--default el-button--small is-round",
"style": `
margin: 20px;
position: absolute;
right: 2%;
bottom: 15%;
`
},
text: "导出课程",
ev: {
"click": methods.exportCourses
}
}),
self.createNode({
tagName: "input",
obj: {
"type": "file",
"id": "import-input",
"style": "display: none"
},
ev: {
"change": methods.importCourses
}
}),
self.createNode({
tagName: "button",
obj: {
"id": "import-button",
"class": "el-button el-button--default el-button--small is-round",
"style": `
margin: 20px;
position: absolute;
right: 2%;
bottom: 10%;
`
},
text: "导入课程",
ev: {
"click": () => document.getElementById("import-input").click()
}
}),
self.createNode({
tagName: "div",
obj: {
"style": `
margin: 20px;
position: absolute;
right: 2%;
bottom: 5%;
color: white;
float: right;
`
},
text: "ver" + version.join(".")
}),
self.createNode({
tagName: "div",
obj: {
"id": "update-tip",
"style": `
margin: 20px;
position: absolute;
right: 2%;
bottom: 0%;
color: red;
float: right;
cursor: pointer;
display: none;
`
},
text: "有新版本,点击更新。更新后请重新进入选课页面",
ev: {
"click": () => {
window.open("https://greasyfork.org/scripts/427237");
}
}
})
]
}));
self.reloadList();
}
//生成遮罩
self.createMask = () => {
let node = self.createNode({
tagName: "div",
obj: {
"id": "mask",
"style": `
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
z-index: 2002;
background-color: rgba(66, 66, 66, 0.6);
display: none
`
},
ev: {
"click": () => {
node.style.display = "none"
app.removeChild(document.getElementsByClassName("temp")[0])
}
}
})
app.appendChild(node)
}
//生成抢课表格
self.reloadList = () => {
let list_wrap = document.querySelector("#panel #list-wrap")
list_wrap.innerHTML = ""
if (JSON.stringify(enrollDict) === '{}') {
list_wrap.innerHTML = "<h3 style='text-align: center;color:lightblue;margin-top: 50%'>还未选择课程</h3>"
} else {
list_wrap.appendChild(self.createNode({
tagName: "table",
obj: {
width: "100%",
border: "1",
style: `
background-color: rgba(0,0,0,0);
color: lightblue
`
},
children: [self.createNode({
tagName: "tr",
obj: {
style: `
height: 30px;
background-color: #255e95
`
},
HTML: `
<th style="text-align:center;width: 55%">课程</th>
<th style="text-align:center;width: 15%">教师</th>
<th style="text-align:center;width: 30%">操作</th>
`
}),
...Object.keys(enrollDict).filter(key => enrollDict[key].courseBatch === grablessonsVue.lcParam.currentBatch.code).map(key => {
return self.createNode({
tagName: "tr",
obj: {
style: `height: 30px`
},
children: [
self.createNode({
tagName: "td",
obj: {
style: `text-align: center`
},
text: enrollDict[key].courseName
}),
self.createNode({
tagName: "td",
obj: {
style: `text-align: center`
},
text: enrollDict[key].teacherName
}),
self.createNode({
tagName: "td",
obj: {
style: `text-align: center`
},
children: [
self.createNode({
tagName: "button",
text: "删除",
obj: {
"style": `
color: red;
background: transparent;
border: 1px solid red;
border-radius: 6px;
text-align: center;
cursor: pointer;
text-decoration: none;
margin-right: 2px
`
},
ev: {
"click": () => {
delete enrollDict[key]
methods.saveCourse()
tip({
type: "success",
message: "已删除",
duration: 1000
})
self.reloadList()
}
}
}),
self.createNode({
tagName: "button",
text: "更多",
obj: {
"style": `
color: orange;
background: transparent;
border: 1px solid orange;
border-radius: 6px;
text-align: center;
cursor: pointer;
text-decoration: none;
margin-left: 2px
`
},
ev: {
"click": () => {
document.getElementById("mask").style.display = "block"
self.createPopUp("更多操作", self.createCourseDetailPop(enrollDict[key]))
}
}
})
]
})
]
})
})]
}))
}
}
//生成弹出窗
self.createPopUp = (title, node, width, height) => app.appendChild(self.createNode({
tagName: "div",
obj: {
"class": "temp",
"style": `
position: fixed;
left: ${width ? 50 - 0.5 * width : 30}%;
top: ${height ? 50 - 0.5 * height : 30}%;
width: ${width || 40}%;
height: ${height || 40}%;
z-index: 2021;
background-color: white;
border-radius: 30px
`
},
children: [
self.createNode({
tagName: "h1",
obj: {
"style": `
margin: 20px 0;
width: 100%;
text-align: center;
`
},
text: title
}),
node,
self.createNode({
tagName: "button",
obj: {
"class": "el-button el-button--default el-button--large is-round",
"style": `
margin: 20px;
position: absolute;
right:10%;
bottom:0
`
},
text: "确定",
ev: {
"click": () => {
document.getElementById("mask").style.display = "none"
app.removeChild(document.getElementsByClassName("temp")[0])
}
}
})
]
}))
//生成课程详情页
self.createCourseDetailPop = course => self.createNode({
tagName: "div",
obj: {
"style": `margin:30px`
},
})
//生成高级操作
self.createAdvancedPop = () => self.createNode({
tagName: "div",
obj: {
"style": `
margin:50px
`
},
children: [
self.createNode({
tagName: "div",
children: [
self.createNode({
tagName: "input",
obj: {
"id": "auto",
"type": "checkbox",
"value": "settings.auto",
"checked": !!settings.auto
},
ev: {
"change": (e) => {
settings.auto = e.target.checked;
methods.saveCourse();
}
}
}),
self.createNode({
tagName: "label",
obj: {
"for": "auto"
},
text: "自动抢课(开发中)"
})
]
}),
self.createNode({
tagName: "div",
obj: { "style": "margin-top: 16px;" },
children: [
self.createNode({
tagName: "label",
obj: { "for": "batchSize", "style": "margin-right: 8px;" },
text: "批次大小:"
}),
self.createNode({
tagName: "input",
obj: {
"id": "batchSize",
"type": "number",
"min": "1",
"max": "10",
"value": settings.batchSize || 3,
"style": "width: 80px;"
},
ev: {
"change": (e) => {
let v = parseInt(e.target.value);
if (isNaN(v) || v < 1) v = 1;
if (v > 10) v = 10;
settings.batchSize = v;
e.target.value = v;
methods.saveCourse();
}
}
})
]
}),
self.createNode({
tagName: "div",
obj: { "style": "margin-top: 12px;" },
children: [
self.createNode({
tagName: "label",
obj: { "for": "intervalMs", "style": "margin-right: 8px;" },
text: "批次间隔(毫秒):"
}),
self.createNode({
tagName: "input",
obj: {
"id": "intervalMs",
"type": "number",
"min": "100",
"step": "100",
"value": settings.intervalMs || 1000,
"style": "width: 120px;"
},
ev: {
"change": (e) => {
let v = parseInt(e.target.value);
if (isNaN(v) || v < 100) v = 100;
settings.intervalMs = v;
e.target.value = v;
methods.saveCourse();
}
}
})
]
})
]
})
})(window.Components = window.Components || {})
let methods = {
isRunning: false,
//初始化数据
init() {
methods.checkVersion();
let raw = JSON.parse(localStorage.getItem("huhu"));
if (raw) {
settings = raw.settings;
if (settings.jwt === sessionStorage.token) {
enrollDict = raw.enrollDict;
} else if (JSON.stringify(raw.enrollDict) !== "{}") {
tip({
type: "warning",
message: "登录信息发生变动,已清空抢课列表",
duration: 1000
});
enrollDict = {};
settings.jwt = sessionStorage.token;
methods.saveCourse();
}
} else {
settings.jwt = sessionStorage.token;
}
window.Components.reloadList();
},
checkVersion() {
request.get("https://api.seutools.com/enroll/", {
transformRequest: [(data, headers) => {
delete headers.Authorization;
delete headers.batchId;
return data;
}]
}).then(res => {
if (res.data.version.split(".").map(x => parseInt(x)) > version) {
document.getElementById("update-tip").style.display = "block";
}
});
},
//保存数据
saveCourse() {
localStorage.setItem("huhu", JSON.stringify({ enrollDict, settings }));
},
//处理按钮拖动与点击
drag(e, node) {
let is_move = false;
let x = e.pageX - node.offsetLeft;
let y = e.pageY - node.offsetTop;
document.onmousemove = function (e) {
node.style.left = e.pageX - x + 'px';
node.style.top = e.pageY - y + 'px';
is_move = true;
};
document.onmouseup = function () {
document.onmousemove = document.onmouseup = null;
if (!is_move) {
let panel = document.getElementById("panel");
panel.style.display === "block" ? panel.style.display = "none" : panel.style.display = "block";
}
is_move = false;
};
},
//处理输入框事件
enter(e) {
let evt = window.event || e;
if (evt.keyCode === 13) {
let currentType = grablessonsVue.teachingClassType;
let currentCourseList = grablessonsVue.courseList;
let node = document.getElementById("input-box");
let code = node.value.toUpperCase();
if (!code) return;
if (enrollDict[code]) {
tip({
type: "warning",
message: "已经添加过了",
duration: 1000
});
return;
}
let courseCode = code.substring(0, 8);
let teacherCode = code.substring(8);
let courseFlag = false, teacherFlag = false;
for (let course of currentCourseList) {
if (course.KCH === courseCode) {
courseFlag = true;
if (grablessonsVue.teachingClassType !== 'XGKC') {
for (let teacher of course.tcList) {
if (teacher.KXH === teacherCode) {
enrollDict[code] = {
courseBatch: grablessonsVue.lcParam.currentBatch.code,
courseCode: teacher.JXBID,
courseType: currentType,
courseName: course.KCM,
teacherName: teacher.SKJS,
secretVal: teacher.secretVal,
};
teacherFlag = true;
}
}
} else {
if (course.KXH === teacherCode) {
enrollDict[code] = {
courseBatch: grablessonsVue.lcParam.currentBatch.code,
courseCode: course.JXBID,
courseType: currentType,
courseName: course.KCM,
teacherName: course.SKJS,
secretVal: course.secretVal,
};
teacherFlag = true;
}
}
}
}
if (!courseFlag) {
tip({
type: "warning",
message: "没有查找到课程,请检查课程代码",
duration: 1000
});
} else if (!teacherFlag) {
tip({
type: "warning",
message: "没有查找到该教师,请检查教师号",
duration: 1000
});
} else {
tip({
type: "success",
message: "添加成功",
duration: 1000
});
node.value = "";
window.Components.reloadList();
methods.saveCourse();
}
}
},
//一键抢课(批次并发 + 循环重试 + 可停止)
enroll() {
if (methods.isRunning) {
tip({
type: "warning",
message: "抢课已在进行中,请稍候",
duration: 1000
});
return;
}
methods.isRunning = true;
const getKeys = () => Object.keys(enrollDict).filter(key => enrollDict[key].courseBatch === grablessonsVue.lcParam.currentBatch.code);
let key_list = getKeys();
if (!key_list.length) {
tip({
type: "warning",
message: "还没有输入课程",
duration: 1000
});
methods.isRunning = false;
return;
}
const batchSize = Math.max(1, parseInt(settings.batchSize || 3));
const interval = Math.max(100, parseInt(settings.intervalMs || 1000));
let currentIndex = 0;
const sendBatch = () => {
if (!methods.isRunning) {
if (grabTimer) {
clearInterval(grabTimer);
grabTimer = null;
}
return;
}
// 列表为空则结束
key_list = getKeys();
if (key_list.length === 0) {
if (grabTimer) {
clearInterval(grabTimer);
grabTimer = null;
}
methods.isRunning = false;
tip({
type: "success",
message: "已全部抢到或列表为空,自动捡漏结束",
duration: 1500
});
return;
}
// 一轮结束则从头开始下一轮
if (currentIndex >= key_list.length) {
currentIndex = 0;
return;
}
const batch = key_list.slice(currentIndex, currentIndex + batchSize);
batch.forEach(key => {
const courseName = enrollDict[key] ? enrollDict[key].courseName : "";
request({
url: "/elective/clazz/add",
method: "POST",
headers: {
'batchId': enrollDict[key].courseBatch,
'content-type': 'application/x-www-form-urlencoded'
},
data: Qs.stringify({
clazzType: enrollDict[key].courseType,
clazzId: enrollDict[key].courseCode,
secretVal: enrollDict[key].secretVal
})
})
.then(res => {
let type = res.data.code === 100 ? "success" : "warning";
tip({
type,
message: `${courseName}: ${res.data.msg}`,
duration: 1000
});
// 成功或检测为已选中的场景,移除课程
const msgText = String(res.data.msg || "");
const successLike = /(选课成功|加入成功|提交成功|已选|已在(?:志愿|候补|选课)|已加入|已存在|已添加|不能重复|重复|已抢|成功)/;
if (res.data.code === 100 || successLike.test(msgText)) {
// 删除当前键以及同课程(前8位)的所有可冲候选
const baseCode = (key && key.length >= 8) ? key.substring(0, 8) : null;
if (baseCode) {
Object.keys(enrollDict).forEach(k => {
if (k.substring(0, 8) === baseCode) {
delete enrollDict[k];
}
});
} else {
delete enrollDict[key];
}
methods.saveCourse();
window.Components.reloadList();
// 重置索引,避免下一批次跳过或重复判断
currentIndex = 0;
// 立即刷新本地 key_list,确保下一轮不再包含已删项
key_list = getKeys();
}
})
.catch(() => {
tip({
type: "error",
message: `${courseName}: 请求失败`,
duration: 2000
});
});
});
currentIndex += batchSize;
};
sendBatch();
grabTimer = setInterval(() => {
sendBatch();
}, interval);
},
// 生成弹出窗口的方法
createPopUp(title, contentNode) {
let popUp = document.createElement("div");
popUp.setAttribute("class", "popup");
popUp.innerHTML = `
<div class="popup-header">
<span class="popup-title">${title}</span>
<span class="popup-close">×</span>
</div>
<div class="popup-content"></div>
`;
popUp.querySelector(".popup-close").addEventListener("click", () => {
popUp.style.display = "none";
});
popUp.querySelector(".popup-content").appendChild(contentNode);
document.body.appendChild(popUp);
},
exportCourses() {
const courseCodes = Object.keys(enrollDict).join('\n'); // 将课程编号以换行符分隔
const blob = new Blob([courseCodes], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'courses.txt'; // 导出的文件名
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url); // 释放blob对象
},
importCourses(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
const fileContent = e.target.result;
const courseCodes = fileContent.split('\n').map(code => code.trim()).filter(code => code); // 处理每一行课程编号
courseCodes.forEach(code => {
const inputBox = document.getElementById('input-box');
inputBox.value = code;
const event = new Event('keydown', { bubbles: true });
event.keyCode = 13; // 模拟回车键
inputBox.dispatchEvent(event);
});
tip({
type: "success",
message: "课程导入成功",
duration: 1000
});
} catch (error) {
tip({
type: "error",
message: "导入失败,请检查文件格式",
duration: 1000
});
}
};
reader.readAsText(file);
}
};
window.Components.mount();
methods.init()
})();