// ==UserScript==
// @name 宜宾智慧校园助手
// @namespace 智慧校园,解决宜宾学院智慧校园的题目,能够自动获取宜宾学院的智慧校园的作业的答案===来自计算机科学与技术学院--修改自若离智慧校园
// @version 5.5
// @description 智慧校园,解决宜宾学院智慧校园的题目,能够自动获取宜宾学院的智慧校园的作业的答案,能够跳过秒看教学视频
// @author 计算机科学与技术学院---软工
// @match https://mooc.yibinu.edu.cn/*
// @icon https://pic.imgdb.cn/item/673c85b1d29ded1a8ce8b97c.png
// @resource cs1 https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/ant-design-vue/1.7.8/antd.css
// @resource cs2 https://pan.ruoli.cc/s/8b0cc4
// @require https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/vue/2.6.14/vue.min.js
// @require https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/ant-design-vue/1.7.8/antd.min.js
// @require https://cdn.sheetjs.com/xlsx-0.19.3/package/dist/xlsx.full.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getResourceText
// @downloadURL none
// ==/UserScript==
// 脚本初始化
var setting = {
'logs': ['初始化脚本完成,', '当前脚本版本:V5.3'],
'datas': [],
'secretKey': '爹', // 暗号
'validDuration': 0 * 24 * 60 * 60 * 1000,
'encryptionKey': 'YiBinXueYuan2024', // 加密密钥
};
// 添加加密和解密函数
function encryptTime(time) {
return CryptoJS.AES.encrypt(time.toString(), setting.encryptionKey).toString();
}
function decryptTime(encrypted) {
try {
const bytes = CryptoJS.AES.decrypt(encrypted, setting.encryptionKey);
return bytes.toString(CryptoJS.enc.Utf8);
} catch (e) {
return null;
}
}
// 获取验证时间
function getValidTime() {
try {
const encryptedTime = localStorage.getItem('scriptValidUntil');
if (!encryptedTime) return null;
const decryptedTime = decryptTime(encryptedTime);
if (!decryptedTime) return null;
const validTime = parseInt(decryptedTime);
return isNaN(validTime) ? null : validTime;
} catch (e) {
console.error('获取验证时间出错:', e);
return null;
}
}
// 验证函数
window.verifySecret = function() {
const input = document.getElementById('secretInput').value;
if(!input) {
const inputElem = document.getElementById('secretInput');
inputElem.style.borderColor = '#ff4d4f';
inputElem.style.animation = 'shake 0.5s';
setTimeout(() => {
inputElem.style.borderColor = '#e8e8e8';
inputElem.style.animation = '';
}, 1000);
return;
}
if(input !== setting.secretKey) {
const modalDiv = document.querySelector('#secretModal > div');
modalDiv.style.animation = 'shake 0.5s';
document.getElementById('secretInput').style.borderColor = '#ff4d4f';
setTimeout(() => {
modalDiv.style.animation = '';
document.getElementById('secretInput').style.borderColor = '#e8e8e8';
}, 1000);
return;
}
// 加密保存验证时间
const validUntil = Date.now() + setting.validDuration;
const encryptedTime = encryptTime(validUntil);
localStorage.setItem('scriptValidUntil', encryptedTime);
// 添加关闭动画
const modal = document.getElementById('secretModal');
modal.style.animation = 'modalFadeOut 0.3s ease';
setTimeout(() => {
modal.remove();
continueInit();
}, 300);
};
// 日志
function log(logText){
setting.logs.unshift(logText);
// Ensure Vue instance logs are updated
if (window.vue) {
window.vue.$nextTick(() => {
window.vue.logs = [...setting.logs];
});
}
}
// 添加一个清理HTML标签的函数
function cleanHtmlTags(text) {
if (!text) return '';
// 先将HTML转换为纯文本
let temp = document.createElement('div');
temp.innerHTML = text;
let cleanText = temp.textContent || temp.innerText;
// 清理多余的空白字符
cleanText = cleanText.replace(/\s+/g, ' ').trim();
return cleanText;
}
// 从后台获取答案
function getAnswer(url, data){
log('获取答案中');
let id = url.match(/\/examSubmit\/(\d+)\/getExamPaper/)[1];
GM_xmlhttpRequest({
method: "post",
url: url,
data: data,
dataType: 'json',
headers: {
'Origin': location.origin,
'User-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.212 Safari/537.36',
'Content-type': 'application/x-www-form-urlencoded;charset=utf-8',
'Referer': `https://mooc.yibinu.edu.cn/examTest/stuExamList/${id}.mooc`
},
onload: function(res){
if(res.status == 200){
try {
let response = JSON.parse(res.responseText);
if (response && response.paper && response.paper.paperStruct) {
log("获取答案成功正在格式化答案!");
formatAnswer(response.paper.paperStruct);
} else {
log("答案数据格式异常,请检查接口返回");
}
} catch (error) {
log("解析答案数据失败:" + error.message);
}
} else {
log("获取答案失败,状态码:" + res.status);
}
},
onerror: function(error) {
log("请求答案失败:" + error.message);
}
});
}
//格式化答案
function formatAnswer(str) {
try {
setting.datas = []; // 清空之前的数据
if (!Array.isArray(str)) {
log("答案数据式错误");
return;
}
str.forEach((listItem, index) => {
if (!listItem.quiz) {
return;
}
// 使用cleanHtmlTags清理题目内容
var question = cleanHtmlTags(listItem.quiz.quizContent) || "未知题目";
var options = {};
var optionContents = {}; // 存储选项内容
var answer = [];
const questionNum = (index + 1).toString();
// 处理选择题
if (listItem.quiz.quizOptionses && listItem.quiz.quizOptionses.length > 0) {
listItem.quiz.quizOptionses.forEach((optionItem, idx) => {
if (optionItem && optionItem.optionId !== undefined) {
const optionLabel = String.fromCharCode(65 + idx);
options[optionItem.optionId] = optionLabel;
optionContents[optionItem.optionId] = optionItem.optionContent || '';
}
});
// 处理答案
if (listItem.quiz.quizResponses) {
listItem.quiz.quizResponses.forEach(answerItem => {
if (answerItem && options[answerItem.optionId]) {
const label = options[answerItem.optionId];
const content = optionContents[answerItem.optionId];
answer.push(`${label}.${content}`);
}
});
}
// 合并序号和选项标签
const answerLabels = listItem.quiz.quizResponses
.map(item => options[item.optionId])
.join('');
const idAndOptions = `${questionNum}.${answerLabels}`;
setting.datas.push({
'key': index.toString(),
'idAndOptions': idAndOptions,
'question': question,
'answer': answer.join('\n') // 每个选项答案换行显示
});
} else {
// 处理填空题
if (listItem.quiz.quizResponses) {
const fillAnswers = [];
listItem.quiz.quizResponses.forEach(answerItem => {
if (answerItem && answerItem.responseContent) {
fillAnswers.push(answerItem.responseContent);
}
});
setting.datas.push({
'key': index.toString(),
'idAndOptions': `${questionNum}.(填空)`,
'question': question,
'answer': fillAnswers.join('\n') // 多个填空答案换行显示
});
}
}
});
// 更新 Vue 实例中的数据
if (window.vue) {
Vue.nextTick(() => {
window.vue.answerList = [...setting.datas];
window.vue.hasAnswer = true; // 设置答案获取状态为 true
});
}
log(`成功处理 ${setting.datas.length} 道题目`);
log('答案获取完成,可以切换到答案列表查看');
} catch (error) {
log("格式化答案时出错:" + error.message);
if (window.vue) {
window.vue.hasAnswer = false;
}
}
}
//初始化界面
function initView(){
// 检查验证是否有效
const validTime = getValidTime();
if (validTime && validTime > Date.now()) {
// 验证仍然有效,直接继续初始化
continueInit();
return;
}
// 创建验证界面的HTML
const createModal = () => {
const modalHtml = `
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 添加事件监听器
const input = document.getElementById('secretInput');
const button = document.querySelector('#secretModal button');
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
window.verifySecret();
}
});
button.addEventListener('click', () => {
window.verifySecret();
});
// 自动聚焦输入框
setTimeout(() => {
input.focus();
}, 100);
};
// 添加抖动动画
const shakeStyle = document.createElement("style");
shakeStyle.textContent = `
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
@keyframes modalFadeOut {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
`;
document.head.appendChild(shakeStyle);
createModal();
}
// 将原来的初始化代码移动到新的函数中
function continueInit() {
var $div =$('' +
'
' +
' ' +
' {{validTimeRemaining}}' +
' ' +
' ' +
' ' +
'
' +
' 秒过视频' +
' 导出题库' +
' 清除日志' +
' ' +
'
' +
' ' +
' ' +
' ' +
'
' +
' {{item}}' +
'
' +
'
' +
' ' +
' ' +
' ' +
' ' +
' ' +
' ' +
'
');
// 更新样式
const customStyle = `
.rlBox {
position: fixed;
top: 10px;
right: 10px;
width: 400px;
z-index: 9999;
background: white;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
padding: 10px;
transition: all 0.3s ease;
transform: none !important;
overflow: hidden;
}
/* 优化最小化状态 */
.rlBox.minimized {
width: 40px !important;
height: 40px !important;
padding: 0 !important;
overflow: hidden;
opacity: 0.8;
cursor: pointer;
border-radius: 50%;
background: #1890ff;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* 按钮样式优化 */
.ant-btn-danger {
background: #ff4d4f !important;
border-color: #ff4d4f !important;
color: white !important;
}
.ant-btn-danger:hover {
background: #ff7875 !important;
border-color: #ff7875 !important;
}
.ant-btn-primary {
background: #1890ff !important;
border-color: #1890ff !important;
}
.ant-btn-primary:hover {
background: #40a9ff !important;
border-color: #40a9ff !important;
}
.ant-btn-success {
background: #52c41a !important;
border-color: #52c41a !important;
color: white !important;
}
.ant-btn-success:hover {
background: #73d13d !important;
border-color: #73d13d !important;
}
/* 最小化状态下的卡片样式 */
.rlBox.minimized .ant-card {
background: transparent;
border: none;
box-shadow: none;
}
/* 最小化状态下的标题隐藏 */
.rlBox.minimized .ant-card-head-title {
display: none;
}
/* 最小化状态下的开按钮样式 */
.rlBox.minimized .ant-btn-circle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: transparent !important;
border: 2px solid white !important;
color: white !important;
box-shadow: none;
}
.rlBox.minimized .ant-btn-circle:hover {
background: rgba(255, 255, 255, 0.2) !important;
}
/* 最小化状态悬停效果 */
.rlBox.minimized:hover {
opacity: 1;
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
transform: translateY(-2px) !important;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.rlBox {
width: 300px;
}
.rlBox.minimized {
width: 36px !important;
height: 36px !important;
}
}
/* 表格容器样式 */
.rl-panel {
height: auto;
max-height: calc(100vh - 250px);
overflow-y: auto;
overflow-x: hidden;
}
/* 表格样式优化 */
.ant-table-wrapper {
overflow: visible;
}
.ant-table {
min-width: 100%;
background: transparent;
}
/* 美化滚动条样式 */
.rl-panel::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.rl-panel::-webkit-scrollbar-thumb {
background: #d9d9d9;
border-radius: 3px;
}
.rl-panel::-webkit-scrollbar-track {
background: #f0f0f0;
border-radius: 3px;
}
/* 表格单元格样式 */
.ant-table-tbody > tr > td {
white-space: normal;
word-break: break-word;
padding: 8px 16px;
line-height: 1.5;
max-width: 0;
}
/* 表格头部样式 */
.ant-table-thead > tr > th {
background: #f5f5f5;
padding: 12px 16px;
white-space: nowrap;
position: sticky;
top: 0;
z-index: 2;
}
/* 确保表格布局合理 */
#rlTable {
table-layout: fixed;
width: 100%;
}
/* 移动端适配 */
@media screen and (max-width: 768px) {
.rl-panel {
max-height: calc(100vh - 200px);
}
.ant-table-tbody > tr > td {
padding: 6px 12px;
}
.ant-table-thead > tr > th {
padding: 8px 12px;
}
}
`;
$("body").append($div);
GM_addStyle(GM_getResourceText("cs1"));
GM_addStyle(GM_getResourceText("cs2"));
GM_addStyle(customStyle);
var vue = new Vue({
el: '.rlBox',
data:{
logs: setting.logs,
close: true,
key: '1',
columns:[
{
title: '序号.选项',
dataIndex: 'idAndOptions',
key: 'idAndOptions',
width: '80px',
fixed: 'left',
align: 'center'
},
{
title: '题目',
dataIndex: 'question',
key: 'question',
width: '45%',
ellipsis: true
},
{
title: '答案',
dataIndex: 'answer',
key: 'answer',
width: '45%',
customRender: (text) => {
return text ? text.split('\n').join('
') : '';
}
}
],
answerList: [], // 初化为空数组
isDragging: false,
currentX: 0,
currentY: 0,
initialX: 0,
initialY: 0,
xOffset: 0,
yOffset: 0,
hasAnswer: false, // 添加答案获取状态标志
validUntil: localStorage.getItem('scriptValidUntil') || null,
},
mounted() {
window.vue = this;
this.initDragEvents();
// 修改初始化位置设置
const box = document.querySelector('.rlBox');
box.style.right = '0px';
box.style.left = 'auto';
box.setAttribute('data-expand-side', 'right'); // 设置默认展开方向
// 添加窗口大小改变监听
window.addEventListener('resize', this.checkPosition);
// 每分钟更新一次验证时间显示
setInterval(() => {
this.validUntil = localStorage.getItem('scriptValidUntil');
}, 60000);
},
computed:{
isShow(){
return this.close ? 0.8 : 1.0;
},
buttonIcon(){
return this.close ? 'plus' : 'minus';
},
buttonColor(){
return this.close ? 'primary' : 'default';
},
answerTabTitle() {
return this.hasAnswer ? '答案列表' : '答案列表 (等待获取...)';
},
validTimeRemaining() {
const validTime = getValidTime();
if (!validTime) return '未验证';
const remaining = validTime - Date.now();
if (remaining <= 0) return '验证已过期';
const days = Math.floor(remaining / (24 * 60 * 60 * 1000));
const hours = Math.floor((remaining % (24 * 60 * 60 * 1000)) / (60 * 60 * 1000));
return `验证剩余: ${days}天${hours}小时`;
}
},
methods: {
callback(key) {
if (key === '2' && !this.hasAnswer) {
this.$message.warning('请等待答案获取完成后再查看答案列表');
this.key = '1'; // 保持在日志页面
return;
}
this.key = key;
},
toClose() {
this.close = !this.close;
const box = document.querySelector('.rlBox');
const rect = box.getBoundingClientRect();
const windowWidth = window.innerWidth;
if (this.close) {
// 最小化时,判断靠近哪边
box.style.transition = 'all 0.3s ease';
const centerX = rect.left + rect.width / 2;
if (centerX > windowWidth / 2) {
// 靠右
box.style.right = '10px';
box.style.left = 'auto';
box.setAttribute('data-side', 'right');
} else {
// 靠左
box.style.left = '10px';
box.style.right = 'auto';
box.setAttribute('data-side', 'left');
}
box.classList.add('minimized');
} else {
// 展开时
box.style.transition = 'all 0.3s ease';
box.classList.remove('minimized');
const side = box.getAttribute('data-side') || 'right';
if (side === 'right') {
box.style.right = '10px';
box.style.left = 'auto';
} else {
box.style.left = '10px';
box.style.right = 'auto';
}
}
},
passVideo() {
let video = document.getElementsByTagName("video");
if(video.length == 0){
log("当前页面不存在视频,请确保视频已加载");
return;
}
let currentVideo = video[0];
// 移除之前的事件监听器(如果存在)
currentVideo.removeEventListener('ended', this.handleNextVideo);
// 添加新的视频结束监听器
currentVideo.addEventListener('ended', () => {
log("视频播放完成,准备切换到下一个视频...");
this.handleNextVideo();
});
try {
// 设置视频结束前的最后一秒
if (currentVideo.duration && !isNaN(currentVideo.duration)) {
currentVideo.currentTime = currentVideo.duration - 0.1;
currentVideo.playbackRate = 1;
log("正在完成当前视频...");
} else {
log("无法获取视频时长,请等待视频加载完成");
// 等待视频加载完成
currentVideo.addEventListener('loadedmetadata', () => {
currentVideo.currentTime = currentVideo.duration - 0.1;
currentVideo.playbackRate = 1;
log("视频加载完成,正在处理...");
});
}
} catch (error) {
log("处理视频时出错:" + error.message);
}
},
exportExcel(){
// 检查是否有答案数据
if (!this.answerList || this.answerList.length === 0) {
this.$message.error('没有可导出的答案数据!请等待答案获取完成。');
log('导出失败:没有答案数据');
return;
}
// 准备数据
const data = this.answerList.map(item => ({
'序号.选项': item.idAndOptions,
'题目': item.question,
'答案': item.answer
}));
// 创建工作簿
const wb = XLSX.utils.book_new();
// 创建工作表
const ws = XLSX.utils.json_to_sheet(data);
// 设置列宽
const colWidths = {
'序号.选项': 10,
'题目': 50,
'答案': 30
};
ws['!cols'] = Object.keys(colWidths).map(key => ({
wch: colWidths[key]
}));
// 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, "题库");
// 生成并下载文件
XLSX.writeFile(wb, "题库.xlsx");
log('题库已导出为 Excel 文件');
},
clearLogs() {
// 清空所有现有日志
this.logs = [];
setting.logs = [];
// 添加清除提示
const clearMessage = [
'日志已清除',
'------------------------',
];
// 直接设置新的日志数组,而不是使用 log 函数
this.logs = clearMessage;
setting.logs = [...clearMessage];
// 阻止其他日志添加
setTimeout(() => {
// 确保清除状态保持
if (this.logs.length > clearMessage.length) {
this.logs = [...clearMessage];
setting.logs = [...clearMessage];
}
}, 200);
},
// 优化拖动处理
initDragEvents() {
const box = document.querySelector('.rlBox');
const dragZone = document.querySelector('.ant-card-head');
let startX, startY, initialMouseX, initialMouseY;
dragZone.addEventListener('mousedown', (e) => {
if (this.close) return; // 小化时禁止拖动
e.preventDefault();
this.isDragging = true;
const rect = box.getBoundingClientRect();
startX = rect.left;
startY = rect.top;
initialMouseX = e.clientX;
initialMouseY = e.clientY;
box.style.transition = 'none';
document.body.style.userSelect = 'none';
});
document.addEventListener('mousemove', (e) => {
if (!this.isDragging) return;
const dx = e.clientX - initialMouseX;
const dy = e.clientY - initialMouseY;
let newX = startX + dx;
let newY = startY + dy;
box.style.left = `${newX}px`;
box.style.top = `${newY}px`;
box.style.right = 'auto';
// 实时检查位置
this.checkPosition();
});
document.addEventListener('mouseup', () => {
if (this.isDragging) {
this.isDragging = false;
box.style.transition = 'all 0.2s';
document.body.style.userSelect = '';
}
});
// 添加触摸支持
dragZone.addEventListener('touchstart', (e) => {
const touch = e.touches[0];
const rect = box.getBoundingClientRect();
startX = rect.left;
startY = rect.top;
initialMouseX = touch.clientX;
initialMouseY = touch.clientY;
this.isDragging = true;
});
document.addEventListener('touchmove', (e) => {
if (!this.isDragging) return;
e.preventDefault();
const touch = e.touches[0];
const dx = touch.clientX - initialMouseX;
const dy = touch.clientY - initialMouseY;
let newX = startX + dx;
let newY = startY + dy;
const maxX = window.innerWidth - box.offsetWidth;
const maxY = window.innerHeight - box.offsetHeight;
newX = Math.min(Math.max(0, newX), maxX);
newY = Math.min(Math.max(0, newY), maxY);
box.style.left = `${newX}px`;
box.style.top = `${newY}px`;
});
document.addEventListener('touchend', () => {
this.isDragging = false;
});
},
handleNextVideo() {
// 获取所有视频列表项
const videoItems = document.querySelectorAll('li[title*="mp4"]');
if (!videoItems || videoItems.length === 0) {
log("未找到视频列表");
return;
}
// 找到当前激活的视频
const currentVideoItem = Array.from(videoItems).find(item =>
item.classList.contains('tab-active') ||
item.querySelector('.tab-inner.current')
);
if (!currentVideoItem) {
log("无法确定当前视频位置");
return;
}
// 判断是否是最后一个视频
const isLastVideo = currentVideoItem === videoItems[videoItems.length - 1];
if (isLastVideo) {
log("当前已是最后一个视频");
return;
}
// 查找"开始下一条目"按钮
const nextButton = document.querySelector('.view-tip[itemid="1"]');
if (!nextButton) {
log("未找到下一个视频按钮");
return;
}
// 点击"开始下一条目"按钮
log("正在切换到下一个视频...");
nextButton.click();
// 等待新视频加载
let attempts = 0;
const maxAttempts = 10;
const checkInterval = setInterval(() => {
const newVideo = document.getElementsByTagName("video")[0];
if (newVideo && newVideo.readyState >= 2) { // 确保视频已经加载足够的数据
clearInterval(checkInterval);
log("新视频加载成功,准备播放");
setTimeout(() => {
this.passVideo();
}, 1500); // 增加延迟以确保视频完全加载
} else {
attempts++;
if (attempts >= maxAttempts) {
clearInterval(checkInterval);
log("新视频加载超时,请手动切换");
}
}
}, 1000);
},
checkPosition() {
const box = document.querySelector('.rlBox');
const rect = box.getBoundingClientRect();
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
// 检查并修正水平位置
if (rect.right > windowWidth) {
box.style.right = '0px';
box.style.left = 'auto';
}
if (rect.left < 0) {
box.style.left = '0px';
box.style.right = 'auto';
}
// 检查并修正垂直位置
if (rect.bottom > windowHeight) {
box.style.top = `${windowHeight - rect.height - 10}px`;
}
if (rect.top < 0) {
box.style.top = '10px';
}
}
}
});
}
// 初始化获取答案,延迟5秒防止流程崩溃
function initGetAnswer(settings){
var url = location.origin + settings.url;
var data = settings.data.replace(/(testPaperId=).*?(&)/,'$1' + '1250' + '$2');
console.log("=====")
console.log(url,'url')
console.log(data)
getAnswer(url,data);
}
// 脚本入口
initView();
//监听跳过视频按钮
$('#rl_passVideo').click(function(){passVideo();});
//监听url访问,当访问了加载题目的url时,将获取答案
$(document).ready(function(){
$(document).ajaxComplete(function (evt, request, settings) {
if(settings.url.search('getExamPaper') != -1){
setting.logs.unshift("您已打开作业界面,5秒后将为您获取答案")
setTimeout(initGetAnswer,5000, settings);
}
});
})