职教云题库导出脚本
// ==UserScript==
// @name 职教云题库导出脚本
// @namespace https://schhz.cn
// @version 0.1.9
// @description 获取职教云题库的题目
// @license MIT
// @author schlibra
// @match https://spoc-exam.icve.com.cn/student/exam/examrecord_recordDetail.action*
// @match https://course.icve.com.cn/*
// @match https://user.icve.com.cn/*
// @require https://unpkg.com/sweetalert@2.1.2/dist/sweetalert.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_info
// @grant GM_setClipboard
// @grant GM_listValues
// @grant GM_deleteValue
// ==/UserScript==
// 脚本版本
const version = GM_info.script.version;
// 定义无用关键字,从题目和答案中替换
const answerUnuseWord = ["(1 分)", "(2 分)", "(3 分)", "(4 分)", "(5 分)", "(1分)", "(2分)", "(3分)", "(4分)", "(5分)", "\n", " ", " ", decodeURI("%C2%A0")];
// 存储标记前缀
const storagePrefix = "sch_ocs_export_";
// 创建按钮
let button = document.createElement("div");
// 定义数据存储函数
function setData(key = "", value = "") {
GM_setValue(storagePrefix + key, value);
}
function getData(key = "", defaults = "") {
return GM_getValue(storagePrefix + key, defaults);
}
+(function () {
'use strict';
checkVersion();
createButton();
})();
// 检测脚本版本更新
function checkVersion(manual=false){
$.get("https://search.schhz.cn/version/",res=>{
if(res.code){
if(res.version!=version&&(res.version!=getData("ignore_update_version")||manual)){
swal({
title: "检测到版本更新",
text: `检测到新版本:${res.version},当前脚本版本:${version}\n版本更新日志:${res.log}`,
icon: "info",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
ignore: {
text: "忽略此版本更新",
value: "ignore"
},
update: {
text: "立即更新",
value: "update"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "ignore":
setData("ignore_update_version", res.version);
break;
case "update":
let link = res.url[GM_info.scriptHandler];
window.open(link,"_blank");
break;
}
})
}else{
if(manual){
swal({
title: "已经是最新版",
text: "您当前安装的脚本已经是最新版本了",
closeOnClickOutside: false,
closeOnEsc: false,
icon: "success",
buttons: {
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
if(value=="back"){
settingsAboutWindow();
}
})
}
}
}else{
swal({
title: "版本检查失败",
text: "无法从更新服务器获取到版本信息,请稍后尝试",
icon: "error",
buttons: ["确定", "关闭"]
})
}
})
}
// 创建脚本功能按钮,显示在左上角
function createButton() {
// 设置按钮样式
button.style.width = "50px";
button.style.height = "50px";
button.style.borderRadius = "50%";
button.style.backgroundColor = "white";
button.style.boxShadow = "0 0 24px -12px #3f3f3f";
button.style.position = "fixed";
button.style.zIndex = "9999";
button.style.left = `${getData("posX", 100)}px`;
button.style.top = `${getData("posY",100)}px`;
button.style.textAlign = "center";
button.style.lineHeight = "50px";
button.style.color = "#3624ff";
button.style.fontSize = "20px";
button.style.transition = "all .5s";
button.innerText = getData("text", "S");
// START: 设置鼠标悬浮效果
button.onmouseover = e => {
button.style.backgroundColor = "#eee"
button.style.color = "deepskyblue"
}
button.onmouseout = e => {
button.style.backgroundColor = "white"
button.style.color = "#3624ff"
}
// END
// START: 设置鼠标按住效果
button.onmousedown = e => {
button.style.backgroundColor = "#ccc"
button.style.color = "blue"
}
button.onmouseup = e => {
button.style.backgroundColor = "white"
button.style.color = "#3624ff"
}
// END
// 绑定点击事件,展示主窗口
button.onclick = showMain
// 追加按钮到页面
document.body.appendChild(button);
}
// 显示主窗口
function showMain() {
swal({
title: "职教云题库导出工具",
text: "请在下方选择一个操作",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
exp: {
text: "导出题目",
value: "export"
},
settings: {
text: "脚本设置",
value: "settings"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value => {
switch (value) {
case "export":
exportWindow();
break;
case "settings":
settingsWindow();
break;
}
}
)
}
// 导出题目确认窗口
function exportWindow() {
if(location.href.startsWith("https://spoc-exam.icve.com.cn/student/exam/examrecord_recordDetail.action")){
let length = $(".q_content").length;
let rightLength = $("span[name='rightAnswer']").length;
if (length == rightLength) {
swal({
title: "导出确认",
text: `这套题一共有 ${length}道题,并且有正确答案,即将导出全部题目`,
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
confirm: {
text: "导出",
value: "confirm"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value => {
if (value == "confirm") {
exportWindowHasAnswer(length)
}else if(value== "back"){
showMain()
}
})
} else {
swal({
title: "导出确认",
text: `这套题一共有 ${length}道题,但是没有正确答案,即将导出您选择正确的题目`,
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
confirm: {
text: "导出",
value: "confirm"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value => {
if (value == "confirm") {
exportWindowNoAnswer(length)
}else if(value == "back"){
showMain()
}
})
}
}else{
swal({
title: "无法导出",
text: "导出题目功能只能在作业详情中使用,该页面不支持导出题目",
closeOnClickOutside: false,
closeOnEsc: false,
icon: "error",
buttons: {
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
if(value=="back"){
showMain();
}
})
}
}
/**
* 删除字符串中的无用字符
* 例如(X 分)
*/
function removeUnuseWord(text) {
answerUnuseWord.forEach(word => {
text = text.replaceAll(word, "")
}
);
for (let i = 65; i <= 90; ++i) {
text = text.replaceAll(`${String.fromCharCode(i)}.`, "")
}
return text;
}
// 导出答案(有答案)确认窗口
function exportWindowHasAnswer(length) {
let list = [];
let work = $("#paperTitle").text();
for (let i = 0; i < length; ++i) {
let title = removeUnuseWord($(".divQuestionTitle")[i].innerText.replace(`${i + 1}、`, ""))
let answer = []
$("span[name='rightAnswer']")[i].innerText.split("").forEach(item => {
answer.push($(".questionOptions")[i].children[item.charCodeAt(0) - 65].innerText)
})
answer = removeUnuseWord(answer.join("#"));
list.push({
title,
answer,
work
});
}
console.log(list)
exportBefotrResultWindow(list);
}
// 导出答案(无答案)确认窗口
function exportWindowNoAnswer(length) {
let list = [];
let work = $("#paperTitle").text();
for (let i = 0; i < length; ++i) {
let ans_obj = $(".exam_answers_tit")[i].children;
console.log(ans_obj[1].classList[1]);
if (ans_obj[1].classList[1] == "icon_examright") {
console.log(ans_obj)
let title = removeUnuseWord($(".divQuestionTitle")[i].innerText.replace(`${i + 1}、`, ""))
let answer = []
ans_obj[0].innerText.split("").forEach(item => {
answer.push($(".questionOptions")[i].children[item.charCodeAt(0) - 65].innerText)
})
answer = removeUnuseWord(answer.join("#"))
list.push({
title,
answer,
work
});
}
}
console.log(list);
exportBefotrResultWindow(list);
}
/**
* 导出前处理,用于填写课程名称
* 因为是后期想到的功能,所以中途加入,就命名了before
*/
function exportBefotrResultWindow(list=[]){
let content = document.createElement("div");
content.style.textAlign="left";
content.innerHTML = `
<label for="${storagePrefix}course_input">课程名称: </label><input id="${storagePrefix}course_input" placeholder="课程名称" style="border: 1px solid #ccc;margin-left: 5px;padding: 3px;"/><br />
`;
swal({
title: "输入课程名",
text: "请输入课程名称",
closeOnClickOutside: false,
closeOnEsc: false,
content,
buttons: {
confirm: {
text: "确定",
value: "confirm"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "confirm":
let course = $(`#${storagePrefix}course_input`).val();
for(let i=0;i<list.length;++i){
list[i]["course"] = course;
}
exportResultWindow(list);
break;
case "back":
exportWindow();
break;
}
})
}
// 导出答案结果窗口
function exportResultWindow(list=[]){
let count = list.length;
let content = document.createElement("textarea");
content.setAttribute("rows", 10);
content.setAttribute("cols", 50);
let text = JSON.stringify(list);
content.value = text;
swal({
title: "导出结果",
text: `题目导出完成,共导出${count}题,结果如下`,
content,
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
upload: {
text: "上传",
value: "upload"
},
copy: {
text: "复制",
value: "copy",
closeModal: false
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "copy":
// content.select();
// document.execCommand("copy");
GM_setClipboard(text, "text");
swal.stopLoading();
break;
case "back":
showMain();
break;
case "upload":
uploadFunction(list);
break;
}
})
}
// 上传答案到服务器
function uploadFunction(list=[]){
let server = getData("server", "");
let token = getData("token", "");
if(server === ""){
swal({
title: "上传失败",
text: "还没有正确配置服务器地址,暂时不能上传题目",
icon: "error",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
settings: {
text: "设置服务器",
value: "settings"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "settings":
settingsUploadWindow();
break;
case "back":
showMain();
break;
}
})
}else{
let data = JSON.stringify(list);
$.post(`${server}/api/?action=import&type=multi&token=${token}`,{data},res=>{
if(res.code){
swal({
title: "上传成功",
text: res.msg,
icon: "success",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: ["确定", "关闭"]
})
}else{
swal({
title:"上传失败",
text: res.msg ?? "服务器没有正常返回",
icon: "error",
closeOnEsc: false,
closeOnClickOutside: false,
buttons: ["确定", "取消"]
})
}
})
}
}
// 脚本设置窗口
function settingsWindow() {
swal({
title: "设置页面",
text: "在这里修改你的设置",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
about: {
text: "关于脚本",
value: "about"
},
upload: {
text: "题库上传",
value: "upload"
},
logo: {
text: "按钮设置",
value: "logo"
},
backup: {
text: "备份恢复",
value: "backup"
},
back: {
text: "返回",
value: "back"
},
cancel: {
visible: true,
text: "关闭",
value: "cancel"
}
}
}).then(value => {
switch (value) {
case "about":
settingsAboutWindow();
break;
case "upload":
settingsUploadWindow();
break;
case "logo":
settingsLogoWindow();
break;
case "back":
showMain();
break;
case "backup":
settingBackupWindow();
break;
}
})
}
// 脚本备份与恢复设置页面
function settingBackupWindow(){
swal({
title: "备份与恢复",
text: "备份或恢复该脚本的配置信息",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
backup: {
text: "备份",
value: "backup"
},
restore: {
text: "恢复",
value: "restore"
},
reset: {
text: "重置",
value: "reset"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "backup":
backupData();
break;
case "restore":
restoreData();
break;
case "reset":
resetData();
break;
case "back":
settingsWindow();
break;
}
})
}
// 备份数据执行
function backupData(){
let keys = GM_listValues();
let data = {};
keys.forEach(key=>{
let value = GM_getValue(key);
data[key] = value;
})
data = JSON.stringify(data);
let content = document.createElement("div");
content.innerHTML = `
<textarea cols="30" rows="10">${data}</textarea>
`;
swal({
title: "导出备份",
text: "下面是该脚本的配置备份数据,请选择对应的操作",
icon: "info",
closeOnClickOutside: false,
closeOnEsc: false,
content,
buttons: {
copy: {
text: "复制",
value: "copy",
closeModal: false
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "copy":
GM_setClipboard(data, "text");
swal.stopLoading();
break;
case "back":
settingBackupWindow();
break;
}
})
}
// 恢复数据输入框
function restoreData(){
let content = document.createElement("div");
content.innerHTML = `
<textarea id="backupImportText" cols="30" rows="10"></textarea>
`;
swal({
title: "导入备份",
text: "导入数据以还原配置数据",
content,
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
confirm: {
text: "确定",
value: "confirm"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel"
}
}
}).then(value=>{
switch(value){
case "confirm":
restoreImportData();
break;
case "back":
settingBackupWindow();
break;
}
})
}
// 导入数据后的提示窗口
function restoreImportData(){
let text = $("#backupImportText").val();
try{
let data = Object.entries(JSON.parse(text));
let count = 0;
data.forEach(item=>{
GM_setValue(item[0], item[1]);
count+=1;
})
swal({
title: "导入完成",
text: `共导入${count}条数据`,
icon: "success",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
if(value=="back"){
settingBackupWindow();
}
})
}catch(e){
swal({
title: "导入失败",
text: "数据格式有误",
icon: "error",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
if(value=="back"){
restoreData();
}
})
}
}
// 重置脚本数据
function resetData(){
swal({
title: "重置数据",
text: "是否要重置脚本全部数据?该操作不可逆",
icon: "info",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
confirm: {
text: "确定",
value: "confirm"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "confirm":
doResetData();
break;
case "back":
settingBackupWindow();
break;
}
})
}
// 执行重置数据
function doResetData(){
let count = 0;
GM_listValues().forEach(key=>{
GM_deleteValue(key)
count+=1
});
swal({
title: "重置成功",
text: `已重置脚本所有数据,共删除了${count}条数据`,
icon: "success",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
confirm: {
text: "确定并刷新",
value: "confirm"
}
}
}).then(value=>{
if(value=="confirm"){
location.reload();
}
})
}
// 设置页面关于脚本窗口
function settingsAboutWindow() {
swal({
title: "关于脚本",
text: `职教云题库导出脚本,用于一键将职教云题目导出,同时支持自建题库,并一键上传至自建题库中\n脚本版本:${version}\n脚本引擎:${GM_info.scriptHandler}`,
icon: "info",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
check: {
text: "检查更新",
value: "check"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "back":
settingsWindow();
break;
case "check":
checkVersion(true);
break;
}
})
}
// 设置上传服务器窗口
function settingsUploadWindow() {
let server = getData("server", "");
let token = getData("token","");
let content = document.createElement("div");
content.style.textAlign="left"
content.innerHTML = `
<label for="${storagePrefix}server_input">服务器地址: </label><input id="${storagePrefix}server_input" value="${server}" placeholder="https://example.com" style="border: 1px solid #ccc;margin-left: 5px;padding: 3px;"/><br />
<p>token用于在调用上传接口时鉴权使用,如果自己实现了接口但是没有使用到token,可以不填写这个值</p>
<label for="${storagePrefix}server_input">认证token: </label><input id="${storagePrefix}token_input" value="${token}" placeholder="token" style="border: 1px solid #ccc;margin-left: 5px;padding: 3px;"/>
`;
swal({
title: "设置上传服务器",
text: "设置题库上传服务器,将导出的题库一键上传到题库服务器中,在下面填写服务器地址,开头需要加协议名,链接结尾不需要加斜杠",
content,
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
confirm: {
text:"确定",
value: "confirm"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
switch(value){
case "confirm":
setData("token", $(`#${storagePrefix}token_input`).val());
settingServer($(`#${storagePrefix}server_input`).val());
break;
case "back":
settingsWindow();
break;
}
})
}
// 设置服务器窗口
function settingServer(server=""){
if(server.startsWith("https://") && (! server.endsWith("/"))){
$.get(`${server}/status/`, data=>{
if(data.code){
setData("server", server);
swal({
title: "设置成功",
text: `服务器设置成功,服务器返回消息:${data.msg}`,
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
visible: true,
value: "cancel"
}
}
}).then(value=>{
if(value=="cancel"){
settingsWindow()
}
})
}else{
swal({
title: "错误提示",
text: "服务器没有正常返回结果,您是否要继续设置该地址?",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
confirm: {
text: "确定",
value: "confirm"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
visible: true,
value: "cancel"
}
}
}).then(value=>{
if(value=="confirm"){
setData("server", server)
}else if(value=="back"){
settingsUploadWindow()
}
})
}
})
}else{
swal({
title: "错误提示",
text: "您设置的服务器地址不符合要求,请重新设置",
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
back: {
text:"返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value=>{
if(value=="back"){
settingsUploadWindow();
}
})
}
}
// 按钮设置
function settingsLogoWindow() {
let text = getData("text", "S");
let posX = getData("posX", 100);
let posY = getData("posY", 100)
let dom = document.createElement("div");
dom.innerHTML = `
<label for="${storagePrefix}text_input"> 按钮文本: </label><input id="${storagePrefix}text_input" value="${text}" style="border: 1px solid #ccc;margin-left: 5px;padding: 3px;"/><br />
<label for="${storagePrefix}posX_input">按钮X坐标: </label><input id="${storagePrefix}posX_input" value="${posX}" style="border: 1px solid #ccc;margin-left: 5px;padding: 3px;"/><br />
<label for="${storagePrefix}posY_input">按钮Y坐标: </label><input id="${storagePrefix}posY_input" value="${posY}" style="border: 1px solid #ccc;margin-left: 5px;padding: 3px;"/>
`;
swal({
title: "按钮设置",
text: "修改按钮文字和位置",
content: dom,
closeOnClickOutside: false,
closeOnEsc: false,
buttons: {
confirm: {
text: "确定",
value: "confirm"
},
back: {
text: "返回",
value: "back"
},
cancel: {
text: "关闭",
value: "cancel",
visible: true
}
}
}).then(value => {
if (value == "confirm") {
setData("text", $(`#${storagePrefix}text_input`).val());
setData("posX", $(`#${storagePrefix}posX_input`).val());
setData("posY", $(`#${storagePrefix}posY_input`).val());
button.innerText = getData("text");
button.style.left = getData("posX")+"px";
button.style.top = getData("posY")+"px";
}else if(value == "back"){
settingsWindow()
}
})
}