// ==UserScript==
// @name 宜宾智慧校园助手
// @namespace 智慧校园,解决宜宾学院智慧校园的题目,能够自动获取宜宾学院的智慧校园的作业的答案===来自计算机科学与技术学院--修改自若离智慧校园
// @version 4.0
// @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
// @run-at document-end
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @grant GM_getResourceText
// ==/UserScript==
// 脚本初始化
var setting = {'logs':['初始化脚本完成,','当前脚本版本:V4.0'],'datas': []};
// 日志
function log(logText){
setting.logs.unshift(logText);
if(Math.random() > 0.92){
setting.logs.unshift('请勿用于非法用途哦!!');
}
}
// 从后台获取答案
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;
}
var question = 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(){
var $div =$('<div class="rlBox" :style="{opacity: isShow}">' +
' <a-card title="宜宾学院智慧校园助手" style="width: 100%;height: 100%;">' +
' <template slot="extra">' +
' <a-button :type="buttonColor" shape="circle" :icon="buttonIcon" @click="toClose" size="small"/>' +
' </template>' +
' <div style="margin-bottom: 15px;" v-show="!close">' +
' <a-button-group style="width: 100%;">' +
' <a-button type="danger" style="width: 33.33%;" @click="passVideo()">秒过视频</a-button>' +
' <a-button type="primary" style="width: 33.33%;" @click="exportExcel()">导出题库</a-button>' +
' <a-button type="success" style="width: 33.33%;" @click="clearLogs()">清除日志</a-button>' +
' </a-button-group>' +
' </div>' +
' <a-tabs default-active-key="1" @change="callback" v-show="!close">' +
' <a-tab-pane key="1" tab="运行日志">' +
' <div class="rl-panel log">' +
' <p v-for="item in logs" class="log_content">' +
' {{item}}' +
' </p>' +
' </div>' +
' </a-tab-pane>' +
' <a-tab-pane key="2" :tab="answerTabTitle" :disabled="!hasAnswer">' +
' <div class="rl-panel">' +
' <a-table id="rlTable"' +
' :scroll="{y: 275}" :pagination="false" bordered size="small" :columns="columns" :data-source="answerList">' +
' </a-table>' +
' </div>' +
' </a-tab-pane>' +
' </a-tabs>' +
' </a-card>' +
'</div>');
// 更新样式
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;
}
.rlBox.minimized {
width: 200px !important;
height: auto !important;
}
.ant-card-head {
cursor: move;
background: #f0f2f5;
border-radius: 8px 8px 0 0;
padding: 0 12px;
min-height: 40px;
}
.ant-card-head-title {
padding: 8px 0;
font-size: 14px;
}
.ant-card-extra {
padding: 8px 0;
}
.ant-btn-circle {
min-width: 24px;
width: 24px;
height: 24px;
}
.ant-card-body {
padding: 12px;
}
@media screen and (max-width: 768px) {
.rlBox {
width: 300px;
right: 5px;
top: 5px;
}
.ant-table {
font-size: 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: false,
key: '1',
columns:[
{
title: '序号.选项',
dataIndex: 'idAndOptions',
key: 'idAndOptions',
width: '120px',
fixed: 'left',
align: 'center'
},
{
title: '题目',
dataIndex: 'question',
key: 'question',
width: '300px',
ellipsis: true
},
{
title: '答案',
dataIndex: 'answer',
key: 'answer',
width: '200px',
customRender: (text) => {
return text ? text.split('\n').join('<br/>') : '';
}
}
],
answerList: [], // 初始化为空数组
isDragging: false,
currentX: 0,
currentY: 0,
initialX: 0,
initialY: 0,
xOffset: 0,
yOffset: 0,
hasAnswer: false, // 添加答案获取状态标志
},
mounted() {
// 保存 Vue 实例的引用,以便在其他函数中使用
window.vue = this;
this.initDragEvents();
// 设置初始位置
const box = document.querySelector('.rlBox');
box.style.right = '10px';
box.style.top = '10px';
},
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 ? '答案列表' : '答案列表 (等待获取...)';
}
},
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');
if (this.close) {
box.classList.add('minimized');
} else {
box.classList.remove('minimized');
}
},
passVideo(){
let video = document.getElementsByTagName("video");
if(video.length == 0){
log("您当前页面不存在视频,请先打开学习视频页面");
}else{
document.getElementsByTagName("video")[0].currentTime=document.getElementsByTagName("video")[0].duration;
log("视频已秒刷完成!");
}
},
exportExcel(){
// 准备数据
const data = this.answerList.map(item => ({
'序号': item.id,
'选项': item.options,
'题目': item.question,
'答案': Array.isArray(item.answer) ? item.answer.join('; ') : item.answer
}));
// 创建工作簿
const wb = XLSX.utils.book_new();
// 创建工作表
const ws = XLSX.utils.json_to_sheet(data);
// 设置列宽
const colWidths = {
'序号': 4,
'选项': 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 = ['日志已清除'];
},
// 优化拖动处理
initDragEvents() {
const box = document.querySelector('.rlBox');
const dragZone = document.querySelector('.ant-card-head');
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
const setTranslate = (xPos, yPos) => {
box.style.left = `${xPos}px`;
box.style.top = `${yPos}px`;
};
dragZone.addEventListener('mousedown', (e) => {
if (e.target.closest('.ant-btn')) return; // 避免按钮区域触发拖动
const rect = box.getBoundingClientRect();
isDragging = true;
initialX = e.clientX - rect.left;
initialY = e.clientY - rect.top;
box.style.transition = 'none';
box.style.cursor = 'grabbing';
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// 边界检查
const maxX = window.innerWidth - box.offsetWidth;
const maxY = window.innerHeight - box.offsetHeight;
currentX = Math.min(Math.max(0, currentX), maxX);
currentY = Math.min(Math.max(0, currentY), maxY);
setTranslate(currentX, currentY);
});
document.addEventListener('mouseup', () => {
if (!isDragging) return;
isDragging = false;
box.style.transition = 'all 0.2s';
box.style.cursor = 'default';
});
}
}
});
}
// 初始化获取答案,延迟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);
}
});
})