174 lines
7.5 KiB
JavaScript
174 lines
7.5 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
// 源代码:构建后连同 question-bank-core.js 一起装入单个油猴脚本。
|
|
if (window.YianQuestionBankExporter) return;
|
|
const core = window.QuestionBankCore;
|
|
if (!core) throw new Error('题库提取核心未加载');
|
|
const config = { autoExport: true, format: 'csv', settleMs: 1800 };
|
|
const completed = new Set();
|
|
let state = null;
|
|
let panel = null;
|
|
let statusNode = null;
|
|
let debounceTimer = null;
|
|
let previousPractice = false;
|
|
let previousRoot = null;
|
|
let previousSignature = '';
|
|
|
|
function routeInfo() {
|
|
const url = new URL(location.href);
|
|
let params = url.searchParams;
|
|
let path = url.pathname;
|
|
if (url.hash.startsWith('#/')) {
|
|
const hashUrl = new URL(url.hash.slice(1), url.origin);
|
|
params = hashUrl.searchParams;
|
|
path = hashUrl.pathname;
|
|
}
|
|
const root = document.querySelector('app-examtest, .pc_examtest');
|
|
const local = url.protocol === 'file:';
|
|
const practice = !!root && (local
|
|
? /[?&]flag=exercise(?:&|$)/.test(core.savedSourceURL(document))
|
|
: params.get('flag') === 'exercise' && /\/exam\/[^/]+\/questions\/?$/.test(path));
|
|
return { practice, root, title: params.get('title') || '', key: path + '|' + (params.get('title') || ''), local };
|
|
}
|
|
|
|
function fingerprint(bank) {
|
|
return JSON.stringify([bank.title, bank.expectedTotal, bank.questions.map(q =>
|
|
[q.number, q.type, q.question, q.options, q.answer, q.explanation, q.images])]);
|
|
}
|
|
|
|
function setStatus(message) {
|
|
if (statusNode && statusNode.textContent !== message) statusNode.textContent = message;
|
|
}
|
|
|
|
function createPanel() {
|
|
if (panel) return;
|
|
panel = document.createElement('div');
|
|
panel.id = 'yian-question-bank-exporter';
|
|
panel.style.cssText = 'position:fixed;right:16px;bottom:16px;z-index:2147483600;max-width:min(330px,calc(100vw - 32px));padding:14px;border:1px solid #d5dde5;border-radius:12px;background:#fff;color:#233447;box-shadow:0 4px 20px #0002;font:13px/1.6 system-ui,sans-serif;text-align:left';
|
|
const heading = document.createElement('strong');
|
|
heading.textContent = '练习题库导出';
|
|
statusNode = document.createElement('div');
|
|
statusNode.setAttribute('role', 'status');
|
|
statusNode.style.cssText = 'margin:6px 0;white-space:pre-line';
|
|
const actions = document.createElement('div');
|
|
actions.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap';
|
|
for (const format of ['csv', 'json', 'txt']) {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.textContent = '导出 ' + format.toUpperCase();
|
|
button.style.cssText = 'cursor:pointer;border:1px solid #d5dde5;border-radius:6px;background:#f5f8fa;padding:5px 9px;color:#233447;font:inherit';
|
|
button.addEventListener('click', () => {
|
|
try { exportCurrent(format); }
|
|
catch (error) { setStatus('导出失败:' + error.message); }
|
|
});
|
|
actions.append(button);
|
|
}
|
|
const label = document.createElement('label');
|
|
label.style.cssText = 'display:flex;gap:6px;align-items:center;margin-top:8px;cursor:pointer';
|
|
const checkbox = document.createElement('input');
|
|
checkbox.type = 'checkbox';
|
|
checkbox.checked = config.autoExport;
|
|
checkbox.addEventListener('change', () => {
|
|
config.autoExport = checkbox.checked;
|
|
tick();
|
|
});
|
|
label.append(checkbox, document.createTextNode('进入练习后自动导出 CSV'));
|
|
panel.append(heading, statusNode, actions, label);
|
|
document.body.append(panel);
|
|
}
|
|
|
|
function exportCurrent(format = config.format) {
|
|
if (!routeInfo().practice) throw new Error('请先进入练习页面');
|
|
const bank = core.extract(document, location.href);
|
|
if (!bank.total) throw new Error('页面还没有题目,请稍后重试');
|
|
// 手动导出可保存不完整内容,文件及提示都会标记缺失。
|
|
core.download(bank, format);
|
|
setStatus((bank.complete ? '已发起下载:' : '已发起不完整题库下载:') + bank.title + '\n'
|
|
+ bank.total + ' 题,' + bank.answered + ' 个答案'
|
|
+ (bank.complete ? '。' : ',请检查缺失内容。')
|
|
+ '\n若未出现文件,请点击上方导出按钮。');
|
|
return bank;
|
|
}
|
|
|
|
function tick() {
|
|
const route = routeInfo();
|
|
if (!route.practice) {
|
|
if (panel) panel.remove();
|
|
panel = statusNode = null;
|
|
state = null;
|
|
previousPractice = false;
|
|
previousRoot = route.root;
|
|
return;
|
|
}
|
|
createPanel();
|
|
const bank = core.extract(document, location.href);
|
|
const signature = fingerprint(bank);
|
|
const now = Date.now();
|
|
if (!state || state.key !== route.key) {
|
|
// SPA 改 URL 后旧题目可能仍留在 DOM,等待题目或组件更新。
|
|
const transitioning = !!previousRoot && !!(previousPractice || previousSignature)
|
|
&& previousRoot === route.root && previousSignature === signature;
|
|
state = { key: route.key, signature, since: now, initialRoot: route.root,
|
|
initialSignature: signature, waitingForReplacement: transitioning };
|
|
}
|
|
if (state.waitingForReplacement && (route.root !== state.initialRoot || signature !== state.initialSignature)) {
|
|
state.waitingForReplacement = false;
|
|
state.since = now;
|
|
}
|
|
previousPractice = true;
|
|
previousRoot = route.root;
|
|
previousSignature = signature;
|
|
if (state.signature !== signature) {
|
|
state.signature = signature;
|
|
state.since = now;
|
|
}
|
|
if (state.waitingForReplacement || (route.title && route.title.trim() !== bank.title.trim())) {
|
|
setStatus('等待本次练习的题目加载…');
|
|
return;
|
|
}
|
|
if (!bank.total || now - state.since < config.settleMs) {
|
|
setStatus('正在读取题目与隐藏答案…');
|
|
return;
|
|
}
|
|
if (bank.expectedTotal !== null && bank.expectedTotal !== bank.total) {
|
|
setStatus('已读取 ' + bank.total + ' / ' + bank.expectedTotal + ' 题,等待全部加载。\n若页面分页,请逐页导出;手动导出会标记不完整。');
|
|
return;
|
|
}
|
|
if (!bank.complete) {
|
|
setStatus('已读取 ' + bank.total + ' 题,缺少 ' + bank.missingAnswers.length + ' 个答案。\n自动导出等待答案加载;可手动保存现有内容。');
|
|
return;
|
|
}
|
|
const exportKey = state.key + '|' + signature;
|
|
if (completed.has(exportKey)) return;
|
|
if (!config.autoExport) {
|
|
setStatus(bank.title + '\n' + bank.total + ' 题,答案齐全,可点击导出。');
|
|
return;
|
|
}
|
|
try {
|
|
exportCurrent(config.format);
|
|
completed.add(exportKey);
|
|
} catch (error) {
|
|
config.autoExport = false;
|
|
const checkbox = panel.querySelector('input');
|
|
if (checkbox) checkbox.checked = false;
|
|
setStatus('自动下载失败:' + error.message + '\n请点击导出按钮重试。');
|
|
console.error('[谊安学题库导出]', error);
|
|
}
|
|
}
|
|
|
|
const observer = new MutationObserver(records => {
|
|
if (records.every(record => panel && (record.target === panel || panel.contains(record.target)))) return;
|
|
clearTimeout(debounceTimer);
|
|
debounceTimer = setTimeout(tick, 300);
|
|
});
|
|
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
|
|
const timer = setInterval(tick, 750); // 同时覆盖 Angular 路由和返回/前进。
|
|
window.YianQuestionBankExporter = Object.freeze({
|
|
exportCurrent,
|
|
collectCurrent: () => core.extract(document, location.href),
|
|
stop: () => { observer.disconnect(); clearInterval(timer); clearTimeout(debounceTimer); if (panel) panel.remove(); }
|
|
});
|
|
tick();
|
|
})();
|