exam

exam faster

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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)
})()