// ==UserScript==
// @name quizii组卷查重工具——试卷管理
// @namespace http://jz.quizii.com/
// @version 0.4.4
// @description 查重流程:1. 在练习箱页面“保存试卷”; 2. 在试卷编辑页面“重题审核”
// @author JinJunwei
// @match http://jz.quizii.com/*/papers/mine*
// @grant none
// ==/UserScript==
'use strict';
// 悬浮按钮的容器
let referenceElement=function (referenceElement){
let newElement = document.createElement('div');
newElement.innerHTML = '<div class="myInjectContainer" ></div>';
newElement = newElement.firstChild
// 定位
newElement.style.top = referenceElement.offsetTop+ referenceElement.offsetHeight+"px";
// 插入元素
referenceElement.parentElement.insertBefore(newElement, referenceElement.nextElementSibling);
return newElement;
}(document.querySelector('div#feedback_btn'));
// 悬浮按钮,接受试卷
(function() {
let newElement = document.createElement('div');
newElement.innerHTML = '<div class="tampermonker-like-feedback_btn" title="油猴脚本,接受全部试卷">接受试卷</div>';
newElement = newElement.firstChild
referenceElement.appendChild(newElement); // 插入到页面
// 功能脚本
newElement.onclick = function(){
//全部接收
document.querySelectorAll("#sharing_list a:nth-child(1)").forEach(el=>el.click());
};
})();
// 第2个悬浮按钮,保存试卷
(function() {
let newElement = document.createElement('div');
newElement.innerHTML = '<div class="tampermonker-like-feedback_btn" title="油猴脚本,导出当前页的全部或选中试卷到本地浏览器数据库,用于组卷排重">保存试卷</div>';
newElement = newElement.firstChild
referenceElement.appendChild(newElement); // 插入到页面
// 功能脚本
newElement.onclick = async function(){
// 导出当前页的全部或选中试卷到本地浏览器数据库
// 选中的试卷id
let papers = [...document.querySelectorAll("#paper_wrap tr.paper")].filter(el=>el.querySelector("input.paper_selected").checked);
// 如果没有选中,则清空
if(papers.length===0){
if(confirm("是否清空保存到本地浏览器数据库的试卷习题内容")){
indexedDB.deleteDatabase("quizii");
}
return;
}
// paper ids
for(var paper of papers){
const href = paper.querySelector("a.paper_name").href;
console.log("load iframe of:"+ href);
let iframeElement = openPaperInIframe(href, paper);
await waitRemoved(iframeElement);
};
};
function openPaperInIframe(url,el){
const ifrm = document.createElement('iframe');
ifrm.setAttribute('width', el.clientWidth);
//document.body.appendChild(ifrm); // to place at end of document
el.parentNode.insertBefore(ifrm, el.nextElementSibling);
// assign url
ifrm.setAttribute('src',url);
// 提取保存习题,并关闭iframe;
const extractAndClose = ()=>extractAndSavePaper(ifrm.contentDocument,ifrm.contentWindow, ()=>ifrm.parentElement.removeChild(ifrm));
// 自动开始下载
ifrm.onload = ()=> ifrm.contentWindow.MathJax.Hub.Queue(extractAndClose);
return ifrm;
}
function extractAndSavePaper(iframeDoc, iframeWin, callback){
console.log("开始处理页面,。。。");
//const paperTitle = iframeDoc.querySelector("div.paper_header #title").innerText;
const g_paper = iframeWin.g_paper;
const paperStoreName = g_paper.name+"@"+g_paper._id;
// 提取所有题目 // ct -> content; sn -> Serial Number
let exerciseList = [...iframeDoc.querySelectorAll("div.item")]
.map(qItem=>{return{
"id": qItem.dataset.id,
"ct":qItem.querySelector("q>ol>li").innerText.replace(/\s+/g,""),
"pId":g_paper._id,
"sn":qItem.querySelector("li").value,
"pName":g_paper.name,
"html":qItem.firstElementChild.outerHTML,
};});
openAndResetDB(iframeWin.indexedDB, paperStoreName)
.onsuccess = (event)=>batchPutOrAdd(event.target.result, paperStoreName, exerciseList, callback);
}
// openAndResetDB(storeName).onsuccess = (event)=>?
function openAndResetDB(indexedDB, paperStoreName){
//每次open都触发onupgradeneeded
const openRequest = indexedDB.open("quizii", Date.now());
openRequest.onerror = function(event) {
console.error(event);
};
openRequest.onupgradeneeded = function(event) {
// 新建数据库
const db = event.target.result;
const paperId = paperStoreName.substring(paperStoreName.lastIndexOf("@"));
const oldStores = [...db.objectStoreNames].filter(n=>n.endsWith(paperId))
// 先删除
if(oldStores.length>0){
console.log("清空了数据表:"+oldStores.join("; "));
oldStores.forEach(n=>db.deleteObjectStore(n))
}
// 再新建
db.createObjectStore(paperStoreName, { keyPath: 'id' });
console.log("新建了数据表:"+paperStoreName);
};
return openRequest;
}
function batchPutOrAdd(db, paperStoreName, items, callback){
const transaction = db.transaction(paperStoreName, "readwrite");
const itemStore = transaction.objectStore(paperStoreName);
putNext(0);
if(callback){transaction.oncomplete = callback;}
function putNext(i) {
if (i<items.length) {
itemStore.put(items[i]).onsuccess = ()=>putNext(i+1);
} else { // complete
console.log('重置或新建了'+items.length+"条数据到表:"+paperStoreName);
}
}
}
function saveText(title, content){
const link = document.createElement('a');
link.download = (title + '.txt').replace(/[/\\?%*:|"<>\s]/g, '-');
const blob = new Blob([content], { type: 'text/plain' });
link.href = window.URL.createObjectURL(blob);
link.click();
}
async function waitLoad(iframeDoc){
console.log("正在等待载入试卷。。。");
// 等待载入
while(iframeDoc.readyState !== 'complete') {
// console.log(document.querySelector(selector).innerHTML);
await new Promise(r => setTimeout(r, 500));
}
}
async function waitRemoved(iframeElement){
console.log("正在等待载入试卷。。。");
// 等待移除
while(iframeElement.parentElement) {
// console.log(document.querySelector(selector).innerHTML);
await new Promise(r => setTimeout(r, 500));
}
}
async function sleep(seconds){
// sleep 10s
console.log("等待"+seconds+"s");
await new Promise(r => setTimeout(r, seconds*1000));
}
})();
// 第3个悬浮按钮,生成试卷
(function() {
let newElement = document.createElement('div');
newElement.innerHTML = '<div class="tampermonker-like-feedback_btn" title="油猴脚本,批量生成试卷">生成试卷</div>';
newElement = newElement.firstChild
referenceElement.appendChild(newElement); // 插入到页面
// 功能脚本
newElement.onclick = function(){
// 批量生成试卷
let paperNameAndIdsList = prompt(`1. 输入 excel脚本 生成的结果,格式为:试卷名1,id,...id;...;
2. 输入 导出试卷 的内容,格式为:试卷id 试卷名 id ...`, "");
if(paperNameAndIdsList.endsWith(" ;")){
paperNameAndIdsList = paperNameAndIdsList.split(" ;")
.map(s=>s.trim().split(" , ")).filter(a=>a.length>1);
}else{
paperNameAndIdsList = paperNameAndIdsList.split(/\s/g);
// 按试卷名 重新分组,不包含试卷id
const ids = paperNameAndIdsList.map(s=>/^[\da-f]{24}$/.test(s))
.map((b,i)=>b?-2:i-1).filter(i=>i>-2);
ids.push(paperNameAndIdsList.length);
paperNameAndIdsList = ids.filter((_,i)=>i<ids.length-1).map((ind,i)=>paperNameAndIdsList.slice(ind+1,ids[i+1]));
}
batchGeneratePaper(paperNameAndIdsList);
};
// 生成试卷api
function customGeneratePaper(callback,paper_name, ...idList){
let item_list = idList.map(id=>{return {type:1001,subtype:1,difficulty:0,_id:id,"sources":[]}}).map(o=>JSON.stringify(o));
let teaching_basic = {grade:3,textbook_ver:"",grade_cat: 0};
qishi.http.post("/paper/generate",
{item_list:item_list,teaching_basic:JSON.stringify(teaching_basic),paper_name:paper_name},
function(data) {
var id=data.tid;
if(id){
console.log("成功生成试卷:"+qishi.util.make_url('/paper/'+id+'/edit'))
callback();
}else{
console.log("试卷生成出错了:"+paper_name);
}
}
);
}
function batchGeneratePaper(paperNameAndIdsList){
// 递归
if (paperNameAndIdsList.length===0){alert("生成试卷完成。");return;}
customGeneratePaper(
()=>setTimeout(()=>batchGeneratePaper(paperNameAndIdsList),5000),
...paperNameAndIdsList.shift()
);
}
})();
// 第4个悬浮按钮,导出试卷
(function() {
let newElement = document.createElement('div');
newElement.innerHTML = '<div class="tampermonker-like-feedback_btn" title="油猴脚本,导出保存的试卷习题id">导出试卷</div>';
newElement = newElement.firstChild
referenceElement.appendChild(newElement); // 插入到页面
// 功能脚本
newElement.onclick = async function(){
// 统一输出 试卷名和习题id,用于导入excel
console.log("开始统一输出 试卷名和习题id,用于导入excel");
const result=[]
// 文件名 默认为 标签名,否则按时间命名
const tagEl=document.querySelector("#select_tag > option[selected]");
const filename = tagEl?tagEl.innerText:getDateFormat();
readNameAndIdsFromDB((...arr)=>result.push(arr),
()=>download(filename+".txt", result.map(arr=>arr.join("\t")).join("\n"))
);
};
function readNameAndIdsFromDB(callback, callback2){
const openRequest = indexedDB.open("quizii");
openRequest.onerror = function(event) {
console.error(event);
};
openRequest.onsuccess = (event)=>{
const db = event.target.result;
const dbNames = [...db.objectStoreNames];
const transaction = db.transaction(dbNames, "readonly");
dbNames.forEach(paperStoreName=>{
const itemStore = transaction.objectStore(paperStoreName);
itemStore.getAllKeys().onsuccess =(event)=>{
callback(...paperStoreName.split("@").reverse(),...event.target.result)
};
});
transaction.objectStore(dbNames[0])
.getAllKeys()
.onsuccess =callback2;
}
}
function download(filename, text) {
var element = document.createElement('a');
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text));
element.setAttribute('download', filename);
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function getDateFormat(){
const d = new Date;
return [d.getFullYear(), d.getMonth()+1,d.getDate(),
d.getHours(),d.getMinutes(),d.getSeconds()].join('-');
}
})();
// 设置样式
(function (){
var style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = `.myInjectContainer{
position: fixed;
top: 150px;
right: -1px;
width: 43px;
}
.tampermonker-like-feedback_btn{
width: 24px;
padding:9px;
border:1px solid #ccc;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
background-color: #fff;
line-height: 16px;
font-size: 12px;
color: #666;
cursor: pointer;
z-index: 9999;
}
.tampermonker-like-feedback_btn:hover{
border-color: #1bbc9b;
background-position: center -48px;
background-color: #1bbc9b;
color: #fff;
}`;
document.getElementsByTagName('head')[0].appendChild(style);
})();
// 悬浮按钮,导入已审
(function() {
let newElement = document.createElement('div');
newElement.innerHTML = '<div class="tampermonker-like-feedback_btn" title="油猴脚本,导入已审核的习题-试卷列表">导入已审</div>';
newElement = newElement.firstChild
referenceElement.appendChild(newElement); // 插入到页面
// 功能脚本
newElement.onclick = loadLoadTxtFile;
function loadLoadTxtFile() {
const element = document.createElement('input');
element.setAttribute('type', 'file' );
element.setAttribute('multiple', 'multiple');
element.setAttribute('downacceptload', 'text/plain');
element.style.display = 'none';
element.onchange=function(event) {
const input = event.target;
const reader = new FileReader();
const texts =[]
let ind =0;
reader.onload =function(){
// save ind result
texts.push(reader.result);
console.log("读取了文件:"+input.files[ind].name);
// next ind
ind = ind+1
// read or end
if(ind<input.files.length){
reader.readAsText(input.files[ind]);
}else{
// 保存到indexedDB本地数据库
batchSaveToIndexedDB(
getEIdPIdsList(texts.join("\n"))
);
}
}
reader.readAsText(input.files[ind]);
};
element.onclick = function (){
document.body.onfocus = function (){
document.body.onfocus = null;
// Cancel clicked
setTimeout(()=>{
if(!element.files.length && confirm("是否清空保存到本地浏览器数据库的已审核习题id")){
indexedDB.deleteDatabase("quizii_已审核");
console.log("删除了数据表:quizii_已审核");
}
}, 500);
};
};
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
function batchSaveToIndexedDB(items){
console.log("请求打开 quizii_已审核 indexedDB :"+Date());
const objectStoreName="已审核习题";// 数据表名
// const openRequest = indexedDB.open("quizii_已审核", Date.now());//每次open都触发onupgradeneeded
const openRequest = indexedDB.open("quizii_已审核");
openRequest.onerror = function(event) {console.error(event);}; // 打开数据库失败
// 新建数据表
openRequest.onupgradeneeded = function(event) {
console.log("开始更新 quizii_已审核 indexedDB :"+Date());
// 新建数据库
const db = event.target.result;
if (db.objectStoreNames.contains(objectStoreName)) {
// 先删除
db.deleteObjectStore(objectStoreName);
console.log("删除了数据表:"+objectStoreName);
}
// 再新建
db.createObjectStore(objectStoreName, { keyPath: 'id' });
console.log("新建了数据表:"+objectStoreName);
};
openRequest.onsuccess = function(event){
// 批量保存数据
const db = event.target.result;
const transaction = db.transaction(objectStoreName, "readwrite");
const itemStore = transaction.objectStore(objectStoreName);
putNext(0);
function putNext(i) {
if (i<items.length) {
itemStore.put(items[i]).onsuccess = ()=>putNext(i+1);
} else {
console.log('重置或新建了'+items.length+"条数据到表:"+objectStoreName);
var countRequest = itemStore.count();
countRequest.onsuccess = function(event) {
alert('新增或覆盖了 '+items.length+" 道已审核的习题id到浏览器的本地数据库,共 "+event.target.result+ "条记录");
}
}
}
}
}
function getEIdPIdsList(idsText){
const m = new Map(); // "eID: sn@pName@pId;..."
idsText.split("\n").forEach(p=>{
const ids = p.split("\t");
const pId = ids[1]+"@"+ids[0];
for(let i=3;i<ids.length;i++){
const eId = ids[i];
if(m.has(eId)){
m.set(eId,m.get(eId)+";"+i+"@"+pId);
}else{
m.set(eId,i+"@"+pId);
}
}
});
return [...m.entries()].map(arr=>{return{id:arr[0],pIds:arr[1]}});// map -> list
}
})();