// ==UserScript== // @name 本地考证自动答题 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 适配本地file协议的自动答题 // @author AI // @match file:///* // @match http://*/* // @match https://*/* // @run-at document-idle // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_log // @connect * // @license MIT // ==/UserScript== (function () { 'use strict'; const BANK_KEY = 'examBank'; // 兼容不同脚本管理器,屏蔽 GM_log 缺失时抛错 const log = (typeof GM_log === 'function') ? GM_log : console.log.bind(console); /* ============================================================ * 1. 基础工具 * ============================================================ */ // 归一化文本:小写、去空白、去常见标点,用于题干/选项匹配 function normalizeText(s) { if (s == null) return ''; return String(s) .replace(/\s+/g, '') .replace(/[,。;:、?!,.;:?!()()\[\]【】<>《》"'“”‘’「」『』·\-—_/\\|+*#@&%$~~^=]/g, '') .toLowerCase(); } function inputType(input) { const t = (input.type || '').toLowerCase(); if (t === 'radio') return 'radio'; if (t === 'checkbox') return 'checkbox'; return 'text'; // text / number / 无 type / textarea } function detectLetter(text) { const m = String(text || '').trim().match(/^\s*([A-Ha-h])\s*[.、.))::]/); return m ? m[1].toUpperCase() : null; } // 清理选项文本:去掉开头的 A. / A、等标识与前后空白 function cleanOptionText(text) { return String(text || '').replace(/^\s*[A-Ha-h]\s*[.、.))::]\s*/, '').trim(); } // 提取题干:去掉序号前缀,并把可能混在同一元素里的选项行截断 function extractStem(el) { let t = el.textContent || ''; t = t.replace(/^\s*\d{1,4}\s*[.、.))::]\s*/, ''); const lines = t.split(/\r?\n/); const cutIdx = lines.findIndex(line => /^\s*[A-Ha-h]\s*[.、.))::]/.test(line)); if (cutIdx > 0) t = lines.slice(0, cutIdx).join(' ').trim(); return t.trim(); } // 计算两个字符串的 Dice 相似度(字符二元组),作为无 Fuse 时的模糊匹配兜底 function diceSimilarity(a, b) { a = normalizeText(a); b = normalizeText(b); if (!a || !b) return 0; if (a === b) return 1; if (a.length < 2 || b.length < 2) return 0; const bg = (s) => { const set = new Set(); for (let i = 0; i < s.length - 1; i++) set.add(s.substr(i, 2)); return set; }; const A = bg(a), B = bg(b); let hit = 0; A.forEach(x => { if (B.has(x)) hit++; }); return (2 * hit) / (A.size + B.size); } /* ============================================================ * 2. 题库加载与归一化 * ============================================================ */ function normalizeBank(raw) { if (!raw) return []; if (Array.isArray(raw)) { return raw.map(extractQA).filter(Boolean); } if (typeof raw === 'object') { // 常见包裹层:{ questions: [...] } / { data: [...] } / { list: [...] } const inner = raw.questions || raw.data || raw.list || raw.bank || raw.题库; if (Array.isArray(inner)) return normalizeBank(inner); // { "题干文本": "答案" } 映射 或 { id: {question, answer} } const out = []; for (const k in raw) { if (!Object.prototype.hasOwnProperty.call(raw, k)) continue; const v = raw[k]; if (v && typeof v === 'object' && !Array.isArray(v)) { const e = extractQA(v); if (e) out.push(e); } else { out.push({ q: k, answer: v }); } } return out.filter(x => x.q); } return []; } function extractQA(item) { if (item == null || typeof item === 'string') return null; const q = item.question || item.title || item.stem || item.题目 || item.题干 || item.q || ''; let a = item.answer !== undefined ? item.answer : (item.answers !== undefined ? item.answers : (item.答案 !== undefined ? item.答案 : item.a)); if (!q || a === undefined || a === null || a === '') return null; return { q: String(q).trim(), answer: a }; } let bankCache = null; // 归一化后的题库 [{ q, answer }] function getBank() { if (bankCache) return bankCache; const raw = GM_getValue(BANK_KEY, null); bankCache = normalizeBank(raw); return bankCache; } function storeBank(raw) { const list = normalizeBank(raw); if (!list.length) { setStatus('题库格式无法识别'); toast('题库格式无法识别'); throw new Error('empty bank'); } GM_setValue(BANK_KEY, raw); bankCache = list; setStatus('已加载 ' + list.length + ' 题'); toast('题库已保存:' + list.length + ' 题'); return list; } // 下载(http/https 走 GM_xmlhttpRequest;file:// 走原生 XHR/fetch) function downloadBank(url) { url = String(url || '').trim(); if (!url) { toast('请先输入题库链接'); return; } setStatus('正在下载题库...'); if (/^file:/i.test(url)) { fetchJsonViaNative(url) .then(storeBank) .catch(err => { setStatus('下载失败'); toast('下载失败:' + err.message); log(err); }); } else { GM_xmlhttpRequest({ method: 'GET', url: url, timeout: 30000, responseType: 'json', onload: function (res) { if (res.status >= 200 && res.status < 300) { try { let raw = res.response; if (raw == null && res.responseText) raw = JSON.parse(res.responseText); storeBank(raw); } catch (e) { setStatus('解析失败'); toast('题库 JSON 解析失败'); } } else { setStatus('下载失败'); toast('下载失败:HTTP ' + res.status); } }, onerror: function () { setStatus('下载失败'); toast('下载失败,请检查链接或网络'); }, ontimeout: function () { setStatus('下载失败'); toast('下载超时'); } }); } } function fetchJsonViaNative(url) { return new Promise(function (resolve, reject) { try { const xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.responseType = 'json'; xhr.onload = function () { const ok = (xhr.status >= 200 && xhr.status < 300) || (/^file:/i.test(url) && (xhr.status === 0 || xhr.status === 200)); if (ok) { let raw = xhr.response; if (raw == null && xhr.responseText) { try { raw = JSON.parse(xhr.responseText); } catch (e) { reject(new Error('JSON 解析失败')); return; } } resolve(raw); } else { reject(new Error('状态码 ' + xhr.status)); } }; xhr.onerror = function () { reject(new Error('本地读取失败,浏览器可能拦截了 file:// 跨域请求')); }; xhr.send(); } catch (e) { reject(e); } }); } // 本地上传:FileReader 读取 .json function uploadBank(file) { if (!file) return; setStatus('正在读取文件...'); const reader = new FileReader(); reader.onload = function (e) { try { const raw = JSON.parse(e.target.result); storeBank(raw); } catch (err) { setStatus('解析失败'); toast('文件不是有效的 JSON'); } }; reader.onerror = function () { setStatus('读取失败'); toast('文件读取失败'); }; reader.readAsText(file, 'utf-8'); } /* ============================================================ * 3. 题目解析(DOM 优先 + 正则回退) * ============================================================ */ const INPUT_SEL = 'input[type=radio], input[type=checkbox], input[type=text], input[type=number], input:not([type]), textarea'; const INPUT_BLACKLIST = ['hidden', 'submit', 'button', 'reset', 'password', 'file', 'image', 'range', 'date', 'time', 'color']; function collectInputs() { return Array.from(document.querySelectorAll(INPUT_SEL)).filter(function (el) { const t = (el.type || '').toLowerCase(); return INPUT_BLACKLIST.indexOf(t) === -1; }); } // 找到以「数字序号」开头的题干节点(保持叶子级,避免父容器误判) function findQuestionElements() { const cands = Array.from(document.querySelectorAll('p, div, span, li, td, h1, h2, h3, h4, h5, h6, dt, dd, label, dt, dd')) .filter(function (el) { return /^\s*\d{1,4}\s*[.、.))::]/.test(el.textContent.trim()); }); return cands.filter(function (el) { return !cands.some(function (c) { return c !== el && el.contains(c); }); }); } function makeOption(input) { const text = getOptionText(input); return { input: input, type: inputType(input), text: cleanOptionText(text), letter: detectLetter(text) }; } function getOptionText(input) { let lbl = input.closest('label'); if (lbl) return lbl.textContent; if (input.id) { try { const l = document.querySelector('label[for="' + CSS.escape(input.id) + '"]'); if (l) return l.textContent; } catch (e) { /* ignore */ } } // 跟随其后的文本节点 let buf = [], s = input.nextSibling, guard = 0; while (s && guard++ < 4) { if (s.nodeType === Node.TEXT_NODE) buf.push(s.nodeValue); s = s.nextSibling; } const t = buf.join(' ').trim(); if (t) return t; if (input.parentElement) return input.parentElement.textContent || ''; return input.value || input.name || ''; } // 按文档顺序为每个 input 匹配最近的前置题干 function groupInputsToQuestions(questionEls, inputs) { const orderMap = new Map(); const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT); let i = 0, n; while ((n = walker.nextNode())) orderMap.set(n, i++); const qs = questionEls.map(function (el) { return { node: el, idx: orderMap.get(el) == null ? -1 : orderMap.get(el), text: extractStem(el), options: [] }; }).filter(function (q) { return q.text.length > 0; }) .sort(function (a, b) { return a.idx - b.idx; }); inputs.forEach(function (inp) { const ii = orderMap.get(inp) == null ? -1 : orderMap.get(inp); let best = null; for (const q of qs) { if (q.idx !== -1 && q.idx < ii) best = q; else break; } if (best) best.options.push(makeOption(inp)); }); return qs.filter(function (q) { return q.options.length > 0; }); } // 正则回退:按行匹配「数字. 题干」与「A. 选项」 function parseFromRegex() { const text = document.body.innerText || document.body.textContent || ''; const lines = text.split(/\r?\n/); const questions = []; let cur = null; lines.forEach(function (line) { const t = line.trim(); const qm = t.match(/^\d{1,4}\s*[.、.))::]\s*(.+)$/); if (qm && qm[1].trim().length >= 2) { if (cur) questions.push(cur); cur = { text: qm[1].trim(), options: [] }; return; } const om = t.match(/^([A-Ha-h])\s*[.、.))::]\s*(.+)$/); if (om && cur) { cur.options.push({ letter: om[1].toUpperCase(), text: cleanOptionText(om[2]), type: null, input: null }); } }); if (cur) questions.push(cur); return questions; } // 正则模式下,把页面上的 input 按顺序回填到选项里 function bindInputsSequentially(questions, inputs) { let k = 0; questions.forEach(function (q) { q.options.forEach(function (opt) { if (k < inputs.length) { const inp = inputs[k++]; opt.input = inp; opt.type = inputType(inp); if (opt.type === 'text') opt.letter = null; } }); }); return questions.filter(function (q) { return q.options.some(o => o.input); }); } // 主解析入口:优先 DOM,返回空则用正则 function parseQuestionsFromDOM() { const inputs = collectInputs(); if (!inputs.length) return []; let questions = []; const questionEls = findQuestionElements(); if (questionEls.length > 0) { questions = groupInputsToQuestions(questionEls, inputs); } if (questions.length === 0) { questions = bindInputsSequentially(parseFromRegex(), inputs); } log('[脚本猫] 解析到题目 ' + questions.length + ' 道,选项控件 ' + inputs.length + ' 个'); return questions; } /* ============================================================ * 4. 答案匹配 * ============================================================ */ function buildBankIndex(bank) { return bank.map(function (e) { return { n: normalizeText(e.q), q: e.q, answer: e.answer }; }); } function parseAnswer(answer) { if (Array.isArray(answer)) { const letters = answer.map(x => String(x).trim()).filter(Boolean); const allLetters = letters.every(x => /^[A-Ha-h]{1,2}$/.test(x)); if (allLetters) { return { letters: letters.map(x => x.toUpperCase().split('')).flat() }; } return { values: letters }; } let s = String(answer).trim(); if (!s) return { none: true }; // 判断题 const TF = /^(正确|对|√|错|错误|×|true|false|t|f|是|否)$/i; if (TF.test(s)) return { text: s }; // 纯字母答案:A / AB / A,B / A、C 等 const condensed = s.replace(/[,,、\s]/g, ''); if (/^[A-Ha-h]{1,8}$/.test(condensed)) { return { letters: condensed.toUpperCase().split('') }; } return { text: s }; } function matchQuestion(qText, index) { const nq = normalizeText(qText); if (!nq) return null; for (const e of index) if (e.n === nq) return e; // 包含匹配(长文本片段更可靠) let best = null, bestScore = 0; for (const e of index) { if (!e.n) continue; if (nq.length >= 4 && e.n.length >= 4) { if (e.n.indexOf(nq) !== -1 || nq.indexOf(e.n) !== -1) { const score = Math.min(e.n.length, nq.length) / Math.max(e.n.length, nq.length); if (score > bestScore) { bestScore = score; best = e; } } } } if (best && bestScore >= 0.6) return best; // 模糊匹配:优先 Fuse,无 Fuse 用 Dice 兜底 if (typeof Fuse !== 'undefined' && index.length) { try { const fuse = new Fuse(index, { keys: ['n'], threshold: 0.38, includeScore: true }); const res = fuse.search(nq); if (res && res.length && res[0].score <= 0.38) return res[0].item; } catch (e) { /* fallthrough */ } } let fb = null, fbScore = 0; for (const e of index) { const s = diceSimilarity(nq, e.n); if (s > fbScore) { fbScore = s; fb = e; } } return fbScore >= 0.55 ? fb : null; } /* ============================================================ * 5. 答题执行 * ============================================================ */ function markAuto(input) { input.setAttribute('data-scriptcat-auto', '1'); } function isAutoMarked(input) { return input.getAttribute('data-scriptcat-auto') === '1'; } // 防误触:该题是否已有「用户手动」选择(不含本脚本自己勾选的) function isManuallyAnswered(question) { return question.options.some(function (o) { return o.input && o.input.checked && !isAutoMarked(o.input); }); } function setChecked(input) { input.checked = true; try { input.dispatchEvent(new Event('click', { bubbles: true })); input.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) { /* ignore */ } markAuto(input); } function applyAnswer(question, answer) { if (isManuallyAnswered(question)) return false; // 已被手动选中,跳过 const parsed = parseAnswer(answer); if (parsed.none) return false; const opts = question.options; let applied = false; if (parsed.letters && parsed.letters.length) { parsed.letters.forEach(function (L) { const idx = L.charCodeAt(0) - 65; let opt = opts.find(o => o.letter === L && o.type !== 'text') || opts[idx]; if (opt && opt.input) { if (opt.type === 'text') { if (!opt.input.value) { opt.input.value = opt.text || L; } } else { setChecked(opt.input); } applied = true; } }); return applied; } if (parsed.values) { // 答案是选项文字数组 parsed.values.forEach(function (v) { const nv = normalizeText(v); const opt = opts.find(o => o.type !== 'text' && normalizeText(o.text) === nv) || opts.find(o => o.type !== 'text' && (normalizeText(o.text).indexOf(nv) !== -1 || nv.indexOf(normalizeText(o.text)) !== -1)); if (opt && opt.input) { setChecked(opt.input); applied = true; } }); return applied; } if (parsed.text) { const t = parsed.text.trim(); const nt = normalizeText(t); const isTrue = /^(正确|对|√|true|t|是)$/i.test(t); const isFalse = /^(错误|错|×|false|f|否)$/i.test(t); if (isTrue || isFalse) { const target = opts.find(function (o) { if (o.type === 'text') return false; const ot = normalizeText(o.text); return isTrue ? /^(正确|对|√|true|t|是)$/.test(o.text.trim()) || /^(对|√|t)$/.test(ot) : /^(错误|错|×|false|f|否)$/.test(o.text.trim()) || /^(错|×|f)$/.test(ot); }) || opts[isTrue ? 0 : 1]; if (target && target.input) { setChecked(target.input); applied = true; } return applied; } // 选项文字匹配 let opt = opts.find(o => o.type !== 'text' && normalizeText(o.text) === nt) || opts.find(o => o.type !== 'text' && (normalizeText(o.text).indexOf(nt) !== -1 || nt.indexOf(normalizeText(o.text)) !== -1)); if (opt && opt.input) { setChecked(opt.input); applied = true; return applied; } // 填空题:写入文本输入框 const ti = opts.find(o => o.type === 'text'); if (ti && ti.input) { ti.input.value = t; markAuto(ti.input); applied = true; } return applied; } return false; } function sleep(ms) { return new Promise(function (resolve) { setTimeout(resolve, ms); }); } let answerRunning = false; async function runAutoAnswer() { if (answerRunning) { toast('正在答题中,请稍候'); return; } const bank = getBank(); if (!bank || !bank.length) { toast('题库为空,请先下载或上传'); setStatus('题库未加载'); return; } const questions = parseQuestionsFromDOM(); if (!questions.length) { toast('未识别到题目'); updateProgress(0, 0); return; } answerRunning = true; try { const index = buildBankIndex(bank); const total = questions.length; let hit = 0; for (let i = 0; i < total; i++) { const q = questions[i]; // 已手动作答(含已自动作答)的题目不再覆盖 const item = matchQuestion(q.text, index); if (item && applyAnswer(q, item.answer)) hit++; const answered = questions.filter(function (qq) { return qq.options.some(o => o.input && (o.input.checked || (o.type === 'text' && o.input.value))); }).length; updateProgress(answered, total); setStatus('匹配 ' + hit + ' 题,已答 ' + answered + ' / ' + total); if (i < total - 1) { // 每题之间随机 1~10 秒间隔 await sleep((1 + Math.floor(Math.random() * 10)) * 1000); } } const finalAnswered = questions.filter(function (qq) { return qq.options.some(o => o.input && (o.input.checked || (o.type === 'text' && o.input.value))); }).length; updateProgress(finalAnswered, total); setStatus('匹配 ' + hit + ' 题,已答 ' + finalAnswered + ' / ' + total); toast('答题完成:命中 ' + hit + ' 题'); } finally { answerRunning = false; } } // 一键答完:无延迟,所有题目一次性完成 function runAutoAnswerInstant() { if (answerRunning) { toast('正在答题中,请稍候'); return; } const bank = getBank(); if (!bank || !bank.length) { toast('题库为空,请先下载或上传'); setStatus('题库未加载'); return; } const questions = parseQuestionsFromDOM(); if (!questions.length) { toast('未识别到题目'); updateProgress(0, 0); return; } const index = buildBankIndex(bank); const total = questions.length; let hit = 0; for (let i = 0; i < total; i++) { const q = questions[i]; const item = matchQuestion(q.text, index); if (item && applyAnswer(q, item.answer)) hit++; } const finalAnswered = questions.filter(function (qq) { return qq.options.some(o => o.input && (o.input.checked || (o.type === 'text' && o.input.value))); }).length; updateProgress(finalAnswered, total); setStatus('匹配 ' + hit + ' 题,已答 ' + finalAnswered + ' / ' + total); toast('一键答完:命中 ' + hit + ' 题'); } /* ============================================================ * 6. UI(右下角可拖拽悬浮窗) * ============================================================ */ function toast(msg) { let t = document.getElementById('sc-toast'); if (!t) { t = document.createElement('div'); t.id = 'sc-toast'; document.body.appendChild(t); } t.textContent = msg; t.style.display = 'block'; clearTimeout(t._tm); t._tm = setTimeout(function () { t.style.display = 'none'; }, 2600); } GM_addStyle([ '#sc-panel{position:fixed;right:20px;bottom:20px;width:260px;z-index:2147483647;', 'font-family:-apple-system,"Microsoft YaHei",Arial,sans-serif;font-size:13px;color:#e8e8e8;', 'background:#1f1f1f;border:1px solid #3a3a3a;border-radius:10px;box-shadow:0 8px 24px rgba(0,0,0,.45);user-select:none;}', '#sc-panel .sc-head{padding:10px 12px;cursor:move;border-bottom:1px solid #333;font-weight:600;', 'background:linear-gradient(135deg,#2b2b2b,#232323);border-radius:10px 10px 0 0;display:flex;justify-content:space-between;align-items:center;}', '#sc-panel .sc-logo{color:#4dabf7;}', '#sc-hide{cursor:pointer;color:#999;padding:0 4px;}', '#sc-hide:hover{color:#fff;}', '#sc-body{padding:12px;display:block;}', '#sc-panel.collapsed #sc-body{display:none;}', '.sc-row{margin-bottom:10px;}', '.sc-label{display:block;margin-bottom:4px;color:#aaa;font-size:12px;}', '#sc-url{box-sizing:border-box;width:100%;padding:6px 8px;background:#111;border:1px solid #444;', 'border-radius:6px;color:#eee;font-size:12px;outline:none;}', '#sc-url:focus{border-color:#4dabf7;}', '.sc-btns{display:flex;gap:8px;}', '.sc-btn{flex:1;padding:6px 0;text-align:center;background:#2d6cdf;color:#fff;border:none;', 'border-radius:6px;cursor:pointer;font-size:12px;}', '.sc-btn:hover{background:#3b7cf0;}', '.sc-btn.sec{background:#444;}.sc-btn.sec:hover{background:#555;}', '#sc-file{display:none;}', '#sc-status{font-size:12px;color:#9ecbff;margin:2px 0 10px;word-break:break-all;}', '.sc-switch-row{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;}', '.sc-switch{position:relative;width:44px;height:22px;background:#555;border-radius:22px;cursor:pointer;transition:.2s;}', '.sc-switch.on{background:#2d6cdf;}', '.sc-switch:before{content:"";position:absolute;top:2px;left:2px;width:18px;height:18px;', 'background:#fff;border-radius:50%;transition:.2s;}', '.sc-switch.on:before{transform:translateX(22px);}', '.sc-progress{height:8px;background:#333;border-radius:8px;overflow:hidden;margin-bottom:4px;}', '#sc-bar{height:100%;width:0;background:linear-gradient(90deg,#2d6cdf,#4dabf7);transition:width .3s;}', '#sc-progress-text{font-size:11px;color:#aaa;}', '#sc-toast{display:none;position:fixed;left:50%;bottom:60px;transform:translateX(-50%);z-index:2147483647;', 'padding:10px 16px;background:rgba(0,0,0,.82);color:#fff;border-radius:8px;font-size:13px;', 'box-shadow:0 4px 16px rgba(0,0,0,.4);}' ].join('\n')); let panel, urlInput, statusEl, barEl, progressTextEl, switchEl; function buildUI() { panel = document.createElement('div'); panel.id = 'sc-panel'; panel.innerHTML = '
—
' + '
' + '
' + ' ' + ' ' + '
' + '
' + ' ' + ' ' + ' ' + '
' + '
题库未加载
' + '
' + ' 自动答题' + '
' + '
' + '
' + ' ' + ' ' + '
' + '
' + '
已答 0 / 0
' + '
'; document.body.appendChild(panel); urlInput = panel.querySelector('#sc-url'); statusEl = panel.querySelector('#sc-status'); barEl = panel.querySelector('#sc-bar'); progressTextEl = panel.querySelector('#sc-progress-text'); switchEl = panel.querySelector('#sc-switch'); panel.querySelector('#sc-download').addEventListener('click', function () { downloadBank(urlInput.value); }); panel.querySelector('#sc-upload').addEventListener('click', function () { panel.querySelector('#sc-file').click(); }); panel.querySelector('#sc-file').addEventListener('change', function (e) { uploadBank(e.target.files[0]); e.target.value = ''; }); panel.querySelector('#sc-run').addEventListener('click', runAutoAnswer); panel.querySelector('#sc-run-all').addEventListener('click', runAutoAnswerInstant); switchEl.addEventListener('click', function () { switchEl.classList.toggle('on'); if (switchEl.classList.contains('on')) runAutoAnswer(); }); panel.querySelector('#sc-hide').addEventListener('click', function () { panel.classList.toggle('collapsed'); this.textContent = panel.classList.contains('collapsed') ? '□' : '—'; }); makeDraggable(panel, panel.querySelector('.sc-head')); } function makeDraggable(el, handle) { let sx, sy, ox, oy, dragging = false; handle.addEventListener('mousedown', function (e) { dragging = true; sx = e.clientX; sy = e.clientY; const r = el.getBoundingClientRect(); ox = r.left; oy = r.top; e.preventDefault(); }); window.addEventListener('mousemove', function (e) { if (!dragging) return; el.style.left = (ox + e.clientX - sx) + 'px'; el.style.top = (oy + e.clientY - sy) + 'px'; el.style.right = 'auto'; el.style.bottom = 'auto'; }); window.addEventListener('mouseup', function () { dragging = false; }); } function setStatus(msg) { if (statusEl) statusEl.textContent = msg; } function updateProgress(answered, total) { if (barEl) barEl.style.width = total ? ((answered / total) * 100) + '%' : '0%'; if (progressTextEl) progressTextEl.textContent = '已答 ' + answered + ' / ' + total; } /* ============================================================ * 7. 启动 * ============================================================ */ function init() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', buildUI); } else { buildUI(); } // 状态初始化 const existing = getBank(); if (existing.length) { // buildUI 里已显示默认文案,稍后修正 setTimeout(function () { setStatus('已加载 ' + existing.length + ' 题'); }, 50); } } init(); })();