exam

exam faster

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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