93 lines
9.4 KiB
HTML
93 lines
9.4 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'unsafe-inline'; img-src 'none'; connect-src 'none'; frame-src 'none'; object-src 'none'; base-uri 'none'">
|
||
<title>离线导出题库</title>
|
||
<style>
|
||
:root{font-family:system-ui,"Microsoft YaHei",sans-serif;color:#182a3a;background:#f4f7fa;line-height:1.65}*{box-sizing:border-box}body{margin:0;padding:40px 20px}.wrap{max-width:980px;margin:auto}.eyebrow{color:#326b95;font-size:13px;font-weight:700;letter-spacing:.12em}h1{font-size:32px;margin:8px 0 10px}p{margin:8px 0;color:#526577}.card{background:#fff;border:1px solid #dce5ed;border-radius:15px;margin:25px 0;padding:26px}.drop{display:block;border:2px dashed #a5becf;border-radius:12px;background:#f8fbfd;padding:30px;text-align:center;cursor:pointer}.drop.drag{border-color:#267eae;background:#eaf5fc}.drop strong{display:block;font-size:19px}.drop small{display:block;color:#687c8a;margin-top:4px}input[type=file]{max-width:100%;margin:18px 0 4px}button{font:inherit;font-weight:600;border:1px solid #c3d2dd;background:#fff;color:#22455f;padding:9px 17px;border-radius:8px;cursor:pointer}button.primary{background:#236d98;color:#fff;border-color:#236d98}button:disabled{opacity:.4;cursor:default}button:hover:not(:disabled){filter:brightness(.96)}.buttons{display:flex;gap:10px;flex-wrap:wrap;margin-top:20px}.status{padding:13px 16px;border-radius:9px;background:#edf3f7;white-space:pre-wrap;overflow-wrap:anywhere}.status.success{background:#e9f5ef;color:#17613e}.status.warning{background:#fff4dc;color:#755414}.status.error{background:#fff0ee;color:#a13228}.stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin:20px 0}.stat{padding:15px;background:#f5f8fa;border-radius:10px}.stat b{display:block;font-size:27px;line-height:1.3}.stat span{color:#637785;font-size:13px}h2{font-size:21px;margin:0 0 12px}h3{font-size:16px;margin:0 0 8px}.preview{padding:18px 0;border-top:1px solid #e1e8ee}.preview p{color:#2f4354;margin:4px 0;white-space:pre-wrap;overflow-wrap:anywhere}.preview .answer{color:#17613e;font-weight:600}.meta{font-size:13px;overflow-wrap:anywhere}.hidden{display:none}.footer{font-size:13px;color:#687b89}@media(max-width:600px){body{padding:22px 12px}h1{font-size:26px}.card{padding:18px}.stats{grid-template-columns:repeat(2,1fr)}.drop{padding:22px 12px}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<main class="wrap">
|
||
<div class="eyebrow">LOCAL QUESTION BANK EXPORTER</div>
|
||
<h1>从已保存的练习网页导出题库</h1>
|
||
<p>先解压下载的压缩包,再选择其中的 HTML 文件。隐藏的参考答案也能读取,无需逐题点击“查看答案”。</p>
|
||
<section class="card">
|
||
<label id="drop" class="drop" for="file"><strong>选择或拖入练习网页</strong><small>支持 .html / .htm;所有处理都在本机完成</small><input id="file" type="file" accept=".html,.htm,text/html"></label>
|
||
<p class="meta">保留网页旁的资源文件夹,方便后续查看图片。此工具会导出图片链接,不会加载或上传图片。</p>
|
||
<div id="status" class="status" role="status" aria-live="polite">等待选择文件。</div>
|
||
<div class="buttons"><button id="csv" class="primary" disabled>导出 CSV(Excel)</button><button id="json" disabled>导出 JSON</button><button id="txt" disabled>导出 TXT</button></div>
|
||
</section>
|
||
<section id="result" class="card hidden">
|
||
<h2 id="title"></h2>
|
||
<div class="stats"><div class="stat"><b id="total">0</b><span>已提取题目</span></div><div class="stat"><b id="answered">0</b><span>包含参考答案</span></div><div class="stat"><b id="expected">—</b><span>网页答题卡题数</span></div><div class="stat"><b id="resolved">0</b><span>包含文字解析</span></div></div>
|
||
<p id="types"></p><p id="source" class="meta"></p>
|
||
<h2 style="margin-top:24px">题目预览</h2><p class="meta">展示前 3 题,导出文件包含全部已提取题目。</p><div id="preview"></div>
|
||
</section>
|
||
<p class="footer">CSV 使用 UTF-8 编码并带中文 BOM;JSON 额外保留题目和解析 HTML、原始图片路径。若 HTML 本身没有参考答案,本工具会标注缺失。</p>
|
||
</main>
|
||
<script src="question-bank-core.js"></script>
|
||
<script>
|
||
(function () {
|
||
'use strict';
|
||
const byId = function (id) { return document.getElementById(id); };
|
||
const buttons = ['csv', 'json', 'txt'].map(byId);
|
||
let bank = null;
|
||
let loadToken = 0;
|
||
function status(message, level) { byId('status').textContent = message; byId('status').className = 'status ' + (level || ''); }
|
||
function paragraph(parent, text, className) { const p = document.createElement('p'); p.textContent = text; if (className) p.className = className; parent.appendChild(p); }
|
||
function render() {
|
||
byId('result').classList.remove('hidden');
|
||
byId('title').textContent = bank.title;
|
||
byId('total').textContent = bank.total;
|
||
byId('answered').textContent = bank.answered;
|
||
byId('expected').textContent = bank.expectedTotal === null ? '未知' : bank.expectedTotal;
|
||
byId('resolved').textContent = bank.questions.filter(function (q) { return q.explanation; }).length;
|
||
byId('types').textContent = Object.entries(QuestionBankCore.typeCounts(bank)).map(function (entry) { return entry[0] + ' ' + entry[1] + ' 题'; }).join(' · ');
|
||
byId('source').textContent = '原始网址:' + (bank.sourceUrl || '网页未保存原始网址;图片相对路径将保留原值');
|
||
const preview = byId('preview'); preview.replaceChildren();
|
||
bank.questions.slice(0, 3).forEach(function (q) {
|
||
const article = document.createElement('article'); article.className = 'preview';
|
||
const heading = document.createElement('h3'); heading.textContent = q.number + '. [' + q.type + '] ' + q.question; article.appendChild(heading);
|
||
q.options.forEach(function (o) { paragraph(article, o.label + '. ' + o.text + (o.images.length ? ' [图片链接已保留]' : '')); });
|
||
paragraph(article, '参考答案:' + (q.answer || '缺失') + (q.answerText ? '(' + q.answerText + ')' : ''), 'answer');
|
||
paragraph(article, '解析:' + (q.explanation || '网页未提供'));
|
||
preview.appendChild(article);
|
||
});
|
||
}
|
||
async function load(file) {
|
||
const current = ++loadToken;
|
||
bank = null; buttons.forEach(function (button) { button.disabled = true; }); byId('result').classList.add('hidden');
|
||
if (!file) { status('等待选择文件。'); return; }
|
||
if (!/\.html?$/i.test(file.name)) { status('请选择已解压的 .html 或 .htm 网页文件。', 'error'); return; }
|
||
status('正在读取 ' + file.name + ' …');
|
||
try {
|
||
const html = await file.text();
|
||
if (current !== loadToken) return;
|
||
if (!window.QuestionBankCore) throw new Error('找不到 question-bank-core.js,请将它与本工具放在同一文件夹。');
|
||
bank = QuestionBankCore.extract(QuestionBankCore.parseHTML(html));
|
||
if (!bank.total) { status('没有找到练习题目。请保存已进入“练习”的完整网页后重新导入。', 'error'); bank = null; return; }
|
||
render(); buttons.forEach(function (button) { button.disabled = false; });
|
||
const issues = [];
|
||
if (bank.expectedTotal === null) issues.push('网页没有答题卡,只能核对当前 HTML 中的题目。');
|
||
else if (bank.expectedTotal !== bank.total) issues.push('答题卡为 ' + bank.expectedTotal + ' 题,当前仅提取 ' + bank.total + ' 题。');
|
||
if (bank.missingAnswers.length) issues.push('缺失答案的题号:' + bank.missingAnswers.join('、') + '。');
|
||
if (!bank.questions.some(function (q) { return q.explanation; })) issues.push('网页中的文字解析为空,导出时会保留为空。');
|
||
status('已读取 ' + bank.total + ' 题,其中 ' + bank.answered + ' 题包含参考答案。' + (issues.length ? '\n' + issues.join('\n') : '\n题目与参考答案完整。'), bank.complete ? 'success' : 'warning');
|
||
} catch (error) { if (current === loadToken) { bank = null; status('读取失败:' + error.message, 'error'); } }
|
||
}
|
||
byId('file').addEventListener('change', function (event) { load(event.target.files[0]); });
|
||
const drop = byId('drop');
|
||
['dragenter', 'dragover'].forEach(function (name) { drop.addEventListener(name, function (event) { event.preventDefault(); drop.classList.add('drag'); }); });
|
||
['dragleave', 'drop'].forEach(function (name) { drop.addEventListener(name, function (event) { event.preventDefault(); drop.classList.remove('drag'); }); });
|
||
drop.addEventListener('drop', function (event) { load(event.dataTransfer.files[0]); });
|
||
buttons.forEach(function (button) { button.addEventListener('click', function () { if (!bank) return; try { const filename = QuestionBankCore.download(bank, button.id); status('已发起下载:' + filename + '\n如未看到文件,请检查浏览器下载列表。', 'success'); } catch (error) { status('导出失败:' + error.message, 'error'); } }); });
|
||
window.addEventListener('dragover', function (event) { event.preventDefault(); });
|
||
window.addEventListener('drop', function (event) { event.preventDefault(); });
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|