exam faster
// ==UserScript==
// @name exam
// @description exam faster
// @namespace http://tampermonkey.net/
// @version 0.5.0
// @author You
// @match https://l.jd.com/student/exam/analyze.du*
// @match https://l.jd.com/student/exam/no-answer-analyze.du*
// @match https://l.jd.com/student/exam/exam.du*
// @icon https://www.google.com/s2/favicons?sz=64&domain=jd.com
// @grant none
// @license MIT
// ==/UserScript==
;(function () {
"use strict"
const STORAGE_KEY = "exam_answers_map"
function getStoredAnswers() {
const stored = localStorage.getItem(STORAGE_KEY)
return stored ? JSON.parse(stored) : {}
}
function saveAnswers(answers) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(answers))
}
function mergeAnswers(oldAnswers, newAnswers) {
return { ...oldAnswers, ...newAnswers }
}
function createModal(existingAnswers, newAnswers) {
const overlay = document.createElement("div")
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
`
const modal = document.createElement("div")
modal.style.cssText = `
background: white;
border-radius: 8px;
padding: 20px;
max-width: 800px;
width: 90%;
max-height: 80vh;
overflow: auto;
`
const title = document.createElement("h3")
title.innerText = "答案管理"
title.style.cssText = "margin: 0 0 15px 0; text-align: center;"
const content = document.createElement("div")
content.style.cssText = "display: flex; gap: 20px; margin-bottom: 20px;"
const leftPanel = document.createElement("div")
leftPanel.style.cssText = "flex: 1; border: 1px solid #ddd; padding: 10px; border-radius: 4px; max-height: 400px; overflow: auto;"
const leftTitle = document.createElement("h4")
leftTitle.innerText = "已有答案"
leftTitle.style.cssText = "margin: 0 0 10px 0; color: #333;"
const leftContent = document.createElement("pre")
leftContent.style.cssText = "margin: 0; font-size: 12px; white-space: pre-wrap; word-break: break-all;"
leftContent.innerText = JSON.stringify(existingAnswers, null, 2)
leftPanel.appendChild(leftTitle)
leftPanel.appendChild(leftContent)
const rightPanel = document.createElement("div")
rightPanel.style.cssText = "flex: 1; border: 1px solid #ddd; padding: 10px; border-radius: 4px; max-height: 400px; overflow: auto;"
const rightTitle = document.createElement("h4")
rightTitle.innerText = "新获取答案"
rightTitle.style.cssText = "margin: 0 0 10px 0; color: #333;"
const rightContent = document.createElement("pre")
rightContent.style.cssText = "margin: 0; font-size: 12px; white-space: pre-wrap; word-break: break-all;"
rightContent.innerText = JSON.stringify(newAnswers, null, 2)
rightPanel.appendChild(rightTitle)
rightPanel.appendChild(rightContent)
content.appendChild(leftPanel)
content.appendChild(rightPanel)
const btnGroup = document.createElement("div")
btnGroup.style.cssText = "display: flex; gap: 10px; justify-content: center;"
const overwriteBtn = document.createElement("button")
overwriteBtn.innerText = "覆盖更新"
overwriteBtn.style.cssText = "padding: 8px 20px; cursor: pointer; background: #ff6b6b; color: white; border: none; border-radius: 4px;"
overwriteBtn.onclick = () => {
saveAnswers(newAnswers)
document.body.removeChild(overlay)
alert("已覆盖更新到 localStorage")
}
const mergeBtn = document.createElement("button")
mergeBtn.innerText = "合并更新"
mergeBtn.style.cssText = "padding: 8px 20px; cursor: pointer; background: #4ecdc4; color: white; border: none; border-radius: 4px;"
mergeBtn.onclick = () => {
const merged = mergeAnswers(existingAnswers, newAnswers)
saveAnswers(merged)
document.body.removeChild(overlay)
alert("已合并更新到 localStorage")
}
const cancelBtn = document.createElement("button")
cancelBtn.innerText = "取消"
cancelBtn.style.cssText = "padding: 8px 20px; cursor: pointer; background: #ddd; color: #333; border: none; border-radius: 4px;"
cancelBtn.onclick = () => {
document.body.removeChild(overlay)
}
btnGroup.appendChild(overwriteBtn)
btnGroup.appendChild(mergeBtn)
btnGroup.appendChild(cancelBtn)
modal.appendChild(title)
modal.appendChild(content)
modal.appendChild(btnGroup)
overlay.appendChild(modal)
document.body.appendChild(overlay)
}
function exportToFile() {
const answers = getStoredAnswers()
const dataStr = JSON.stringify(answers, null, 2)
const blob = new Blob([dataStr], { type: "application/json" })
const url = URL.createObjectURL(blob)
const a = document.createElement("a")
a.href = url
a.download = `exam_answers_${new Date().toISOString().slice(0, 10)}.json`
a.click()
URL.revokeObjectURL(url)
}
function importFromFile() {
const input = document.createElement("input")
input.type = "file"
input.accept = ".json"
input.onchange = (e) => {
const file = e.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (event) => {
try {
const importedAnswers = JSON.parse(event.target.result)
const currentAnswers = getStoredAnswers()
const merged = mergeAnswers(currentAnswers, importedAnswers)
saveAnswers(merged)
alert("导入成功")
} catch (err) {
alert("文件格式错误,请确保是有效的 JSON")
}
}
reader.readAsText(file)
}
input.click()
}
function createImportExportModal() {
const overlay = document.createElement("div")
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
`
const modal = document.createElement("div")
modal.style.cssText = `
background: white;
border-radius: 8px;
padding: 20px;
max-width: 800px;
width: 90%;
max-height: 80vh;
overflow: auto;
`
const title = document.createElement("h3")
title.innerText = "答案管理"
title.style.cssText = "margin: 0 0 15px 0; text-align: center;"
const textarea = document.createElement("textarea")
textarea.style.cssText = `
width: 100%;
height: 400px;
font-family: monospace;
font-size: 12px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
resize: vertical;
box-sizing: border-box;
`
const currentAnswers = getStoredAnswers()
textarea.value = JSON.stringify(currentAnswers, null, 2)
const btnGroup = document.createElement("div")
btnGroup.style.cssText = "display: flex; gap: 10px; justify-content: center; margin-top: 15px;"
const exportBtn = document.createElement("button")
exportBtn.innerText = "导出到文件"
exportBtn.style.cssText = "padding: 8px 20px; cursor: pointer; background: #4ecdc4; color: white; border: none; border-radius: 4px;"
exportBtn.onclick = () => {
exportToFile()
}
const importBtn = document.createElement("button")
importBtn.innerText = "从文件导入"
importBtn.style.cssText = "padding: 8px 20px; cursor: pointer; background: #ff6b6b; color: white; border: none; border-radius: 4px;"
importBtn.onclick = () => {
importFromFile()
textarea.value = JSON.stringify(getStoredAnswers(), null, 2)
}
const saveBtn = document.createElement("button")
saveBtn.innerText = "保存修改"
saveBtn.style.cssText = "padding: 8px 20px; cursor: pointer; background: #51cf66; color: white; border: none; border-radius: 4px;"
saveBtn.onclick = () => {
try {
const answers = JSON.parse(textarea.value)
saveAnswers(answers)
alert("保存成功")
} catch (e) {
alert("JSON 格式错误")
}
}
const closeBtn = document.createElement("button")
closeBtn.innerText = "关闭"
closeBtn.style.cssText = "padding: 8px 20px; cursor: pointer; background: #ddd; color: #333; border: none; border-radius: 4px;"
closeBtn.onclick = () => {
document.body.removeChild(overlay)
}
btnGroup.appendChild(exportBtn)
btnGroup.appendChild(importBtn)
btnGroup.appendChild(saveBtn)
btnGroup.appendChild(closeBtn)
modal.appendChild(title)
modal.appendChild(textarea)
modal.appendChild(btnGroup)
overlay.appendChild(modal)
document.body.appendChild(overlay)
}
const paper = document.querySelector('.jdu-exam')
const btnCtn = document.createElement("div")
btnCtn.className = "jdu-btn-con";
btnCtn.style.position = "fixed";
paper.appendChild(btnCtn)
const btn = document.createElement("a")
btn.href = "#"
btn.innerText = "Go!"
const importExportBtn = document.createElement("a")
importExportBtn.href = "#"
importExportBtn.innerText = "答案管理"
importExportBtn.style.marginLeft = "10px"
importExportBtn.addEventListener("click", (e) => {
e.preventDefault()
createImportExportModal()
})
btnCtn.appendChild(btn)
btnCtn.appendChild(importExportBtn)
setTimeout(() => {
if (location.pathname.endsWith("analyze.du")) {
btn.addEventListener("click", () => {
const questions = document.querySelectorAll(".question")
const newAnswers = {}
Array.from(questions).map((question) => {
const questionId = question.id
const options = Array.from(
question.querySelectorAll(".answer-options li")
)
const answersStr = question
.querySelector(".jdu-answer-analyze > div:first-child")
.innerText.substr(5)
newAnswers[questionId] = answersStr.split("").reduce((prev, answerKey) => {
const answerStr = options.find(
(option) => option.innerText[1] == answerKey
)
const answer = answerStr.innerText.substr(1)
prev.push(answer.trim().replace(/^ ?[ABCDEFGH]\. ?/, ""))
return prev
}, [])
})
const existingAnswers = getStoredAnswers()
createModal(existingAnswers, newAnswers)
})
}
if (location.pathname.endsWith("exam.du")) {
btn.addEventListener("click", () => {
const questions = document.querySelectorAll(".question")
const answersMap = getStoredAnswers()
if (Object.keys(answersMap).length === 0) {
alert("localStorage 中没有答案数据")
return
}
questions.forEach((question) => {
const answers = answersMap[question.id] || []
const options = question.querySelectorAll(".answer-options li")
options.forEach((option) => {
const purifiedOption = option.innerText
.trim()
.replace(/^ ?[ABCDEFGH]\. ?/, "")
if (
answers.some((answer) => {
return answer === purifiedOption
})
) {
option.click()
}
})
})
})
}
}, 1000)
})()