feat: collect judgment standards through opt-in normal practice

This commit is contained in:
suanyuit-cyber
2026-10-30 04:32:48 +08:00
parent 6ba4905a7d
commit fcc8390faa
9 changed files with 1153 additions and 24 deletions
+326 -7
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name 华谊题库助手 · CSV导出与划词检索
// @namespace local.huayi.question-bank
// @version 1.2.6
// @version 1.2.7
// @description 全网页快捷键唤出、本地题库文字检索与自动选择答案,按库导入导出;每页默认完全隐藏。
// @match *://*/*
// @run-at document-start
@@ -2097,6 +2097,271 @@ const QBAuto = (() => {
if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
// ---- src/collector.js ----
/* Opt-in completion of one Huayi judgment practice, through ordinary DOM clicks. */
const QBCollector = (() => {
'use strict';
const MAIN = '.yxtulcdsdk-pc-marking-main';
const FORM = `${MAIN} .yxtulcdsdk-form[practice]`;
const EVIDENCE = new Set(['submitted-list', 'answer-response', 'bound-dom', 'explicit-standard']);
const trim = value => String(value == null ? '' : value).replace(/\u00a0/g, ' ').trim();
const flat = value => trim(value).replace(/\s+/gu, '');
const optionsKey = question => JSON.stringify((question.options || []).map(option => [trim(option.id), flat(option.text)]).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))));
const identity = question => JSON.stringify([question.bankId, question.questionId, question.questionIndex, flat(question.stem), optionsKey(question)]);
const evidence = record => EVIDENCE.has(record.answerEvidence) || (record.answerSource || record.source)?.kind === 'network' &&
/\/upm\/[^/]+\/users\/[^/]+\/(?:wrong\/)?answer(?:[?#]|$)/u.test((record.answerSource || record.source)?.url || '');
function create({ window: win, document: doc, core, getRecords = () => [], getQuestion = () => null,
getBank = () => null, isCollecting = () => false, onChange = () => {} }) {
let state = { active: false, phase: 'idle', message: '练习补齐未启动。', reason: '', answered: 0, skipped: 0,
current: 0, total: 0, bankId: '', bankName: '', taskId: '' };
let destroyed = false, timer = null, generation = 0, binding = null, pending = null, navigation = null;
let indexSource = null, indexLength = -1, recordIndex = null, idIndex = null;
const visited = new Map();
const now = () => win.Date?.now ? win.Date.now() : Date.now();
const getState = () => ({ ...state });
function update(patch) {
const old = state;
state = { ...state, ...patch };
if (old.phase !== state.phase || old.active !== state.active) {
try { onChange(getState()); } catch (_) { /* UI code cannot grant further actions. */ }
}
}
function stop(reason = '已停止练习补齐。') {
generation++;
if (timer !== null) win.clearTimeout(timer);
timer = null; pending = null; navigation = null;
update({ active: false, phase: 'stopped', reason: trim(reason), message: trim(reason) || '已停止练习补齐。' });
return getState();
}
function visible(node) {
if (!node || node.isConnected === false || node.closest?.('#huayi-qb-root')) return false;
for (let current = node; current && current.nodeType === 1; current = current.parentElement) {
if (current.hidden || current.getAttribute?.('aria-hidden') === 'true') return false;
const style = win.getComputedStyle?.(current);
if (style?.display === 'none' || style?.visibility === 'hidden' || style?.visibility === 'collapse') return false;
}
return true;
}
function context() {
const url = new URL(String(win.location?.href || ''));
if (!['www.shhuayi-elearning.cn', 'shhuayi-elearning.cn'].includes(url.hostname) ||
url.pathname !== '/o2o/' || !/^#\/playinfo(?:\?|$)/u.test(url.hash)) throw new Error('仅支持华谊培训大纲中的判断练习页面。');
const params = new URLSearchParams(url.hash.includes('?') ? url.hash.slice(url.hash.indexOf('?') + 1) : '');
if (params.get('wq') === '1' || url.searchParams.get('wq') === '1' || /wrong/iu.test(url.hash)) throw new Error('错题重做不属于本次完整题库补齐范围。');
if (!isCollecting()) throw new Error('采集已关闭或题库存储不可用,练习补齐已停止。');
const selected = doc.querySelector('.task-content-active');
if (selected && (!/练习/u.test(selected.textContent || '') || /考试/u.test(selected.textContent || ''))) throw new Error('当前培训任务不是明确的练习,不能启动。');
const forms = [...doc.querySelectorAll(FORM)].filter(visible);
if (forms.length !== 1) throw new Error('当前页面不是唯一的正常练习题,正式考试或不完整页面不能补齐。');
const form = forms[0], main = form.closest(MAIN);
if (!main || !visible(main)) throw new Error('练习题目区域尚未完整显示。');
const heading = trim(main.querySelector('.standard-size-14 span')?.textContent);
if (heading !== '判断题') throw new Error('本功能只支持判断题练习。');
const bank = getBank();
const taskId = trim(bank?.taskId || params.get('taskId'));
const total = bank?.questionCount;
if (!bank || bank.loading || !trim(bank.bankId) || !taskId || ![500, 232].includes(total)) throw new Error('当前题库身份或页面总题数未确认,请等待题库加载。');
const progress = [...doc.querySelectorAll('.yxtulcdsdk-answer-card__top')].filter(visible);
const counts = progress.map(node => trim(node.textContent).match(/(\d+)\s*\/\s*(\d+)(?!\d)/u)).filter(Boolean);
if (counts.length !== 1 || Number(counts[0][2]) !== total || Number(counts[0][1]) > total) throw new Error('页面答题卡总数与当前题库不一致,已停止。');
const route = `${url.pathname}${url.search}${url.hash}`;
if (binding && (bank.bankId !== binding.bankId || taskId !== binding.taskId || route !== binding.route || total !== binding.total)) {
throw new Error('题库、任务或地址已变化,练习补齐已停止。');
}
return { bank, taskId, total, route, form, main };
}
function readView(ctx) {
const question = getQuestion();
const number = trim(ctx.form.querySelector('.yxtulcdsdk-form__sub')?.textContent).match(/^(\d+)\s*[..、]?$/u);
if (!number || !question || question.type !== 'judge' || question.bankId !== ctx.bank.bankId || !trim(question.questionId) ||
!Number.isSafeInteger(question.questionIndex) || question.questionIndex !== Number(number[1]) - 1 ||
question.questionIndex < 0 || question.questionIndex >= ctx.total || question.options?.length !== 2 || !trim(question.stem)) return null;
const labels = [...ctx.form.querySelectorAll('.yxtulcdsdk-ques-radio')].filter(visible);
if (labels.length !== 2) return null;
const controls = labels.map(label => ({ label, input: label.querySelector('input[type="radio"]'),
text: trim(label.querySelector('.yxtf-radio__label')?.textContent || label.textContent) }));
if (controls.some(control => !control.input || !control.input.isConnected || !control.input.value) ||
controls.some((control, position) => control.text !== trim(question.options[position].text) || trim(control.input.value) !== trim(question.options[position].id)) ||
new Set(controls.map(control => control.text)).size !== 2 || !controls.some(control => control.text === '正确') || !controls.some(control => control.text === '错误')) return null;
const feedback = [...ctx.main.querySelectorAll('.yxtulcdsdk-examing__result-line')].filter(visible).filter(row =>
['正确答案', '标准答案'].includes(trim(row.querySelector('.yxtulcdsdk-examing__result-line-left')?.textContent)));
if (feedback.length > 1) throw new Error('当前题出现多个标准答案展示,已停止核验。');
let standard = '';
if (feedback.length) {
const row = feedback[0];
const rowId = ['data-question-id', 'data-ques-id', 'question-id', 'ques-id'].map(name => trim(row.getAttribute(name))).find(Boolean);
if (rowId && rowId !== question.questionId) throw new Error('页面标准答案仍属于另一道题,已停止。');
standard = trim(row.querySelector('.yxtulcdsdk-examing__result-line-right')?.textContent);
if (!['正确', '错误'].includes(standard)) throw new Error('网站标准答案尚不明确,已停止。');
}
return { ...ctx, question, controls, standard, feedbackPresent: feedback.length === 1,
checked: controls.some(control => control.input.checked), mark: identity(question) };
}
function recordsAt(index) {
const records = getRecords();
if (!Array.isArray(records)) throw new Error('本地题库不可读取,已停止。');
if (indexSource !== records || indexLength !== records.length) {
indexSource = records; indexLength = records.length; recordIndex = new Map(); idIndex = new Map();
for (const record of records) {
if (!record || record.bankId !== binding.bankId || !Number.isSafeInteger(record.questionIndex)) continue;
if (!recordIndex.has(record.questionIndex)) recordIndex.set(record.questionIndex, []);
recordIndex.get(record.questionIndex).push(record);
for (const id of new Set([record.questionId, ...(record.questionIdAliases || [])].filter(Boolean))) {
if (!idIndex.has(id)) idIndex.set(id, []);
idIndex.get(id).push(record);
}
}
}
return recordIndex.get(index) || [];
}
function matchingRecord(question) {
const candidates = recordsAt(question.questionIndex).filter(record => record.type === 'judge' && flat(record.stem) === flat(question.stem) && optionsKey(record) === optionsKey(question) &&
[record.questionId, ...(record.questionIdAliases || [])].includes(question.questionId));
if (candidates.length > 1) throw new Error('同一题目位置存在多个匹配记录,已停止避免重复提交。');
const record = candidates[0];
if (!record) {
throw new Error('当前题不能与本地题库的位置、完整内容及题目ID唯一对应,已停止。');
}
if ((idIndex.get(question.questionId) || []).length !== 1) throw new Error('当前题目ID或别名对应多个原题位置,已停止。');
if (record.answerStatus === 'conflict' || record.conflicts?.length) throw new Error('本地当前题标准答案存在冲突,已停止。');
if (record.answerStatus === 'missing') return { kind: 'missing', record };
if (record.answerStatus !== 'confirmed' || !evidence(record)) throw new Error('当前题的已保存答案缺少可核验来源,已停止。');
const mapped = core.mapAnswers(record, question);
if (mapped.status !== 'matched' || mapped.answerTexts.length !== 1 || !['正确', '错误'].includes(mapped.answerTexts[0])) throw new Error('标准答案不能唯一对应当前题目,已停止。');
return { kind: 'confirmed', record, standard: mapped.answerTexts[0] };
}
function clickable(element) {
return visible(element) && typeof element.click === 'function' && !element.disabled && element.getAttribute?.('aria-disabled') !== 'true' &&
!element.closest?.('[aria-disabled="true"]');
}
function assertBeforeClick(view) {
if (!state.active || destroyed) return false;
const fresh = readView(context());
if (!fresh || fresh.mark !== view.mark || fresh.form !== view.form) throw new Error('点击前题目身份发生变化,已停止。');
return true;
}
function schedule() {
if (!state.active || destroyed || timer !== null) return;
const token = generation;
timer = win.setTimeout(() => { timer = null; if (state.active && !destroyed && token === generation) tick(); }, 300);
}
function beginNavigation(view, target) {
let button;
if (target === 0 && view.question.questionIndex !== 0) {
const matches = [...doc.querySelectorAll('.yxtulcdsdk-pc-card-list li')].filter(node => visible(node) && trim(node.textContent) === '1');
if (matches.length !== 1) throw new Error('答题卡第1题入口不唯一,不能安全从头核验。');
button = matches[0];
} else {
const matches = [...view.main.querySelectorAll('.yxtulcdsdk-examing__btns button')].filter(node => visible(node) && trim(node.textContent) === '下一题');
if (matches.length !== 1) throw new Error('没有唯一的下一题按钮;不会点击结束、提交或交卷。');
button = matches[0];
}
if (!clickable(button)) throw new Error('换题按钮暂不可用,已停止保留当前结果。');
navigation = { from: view.question.questionIndex, target, deadline: now() + 15000 };
update({ phase: 'navigating', message: `正在核对第 ${target + 1} 题。` });
if (!assertBeforeClick(view)) return;
button.click();
}
function finish() {
if (!state.active) return;
if (visited.size !== binding.total) throw new Error('本次尚未核对完整题库,不能标记完成。');
for (let index = 0; index < binding.total; index++) {
const question = visited.get(index);
if (!question || matchingRecord(question).kind !== 'confirmed') throw new Error(`第 ${index + 1} 题标准答案尚未完整核验,已停止。`);
}
generation++;
if (timer !== null) win.clearTimeout(timer);
timer = null; pending = null; navigation = null;
update({ active: false, phase: 'completed', reason: '', message: `已核验 ${binding.total} 道判断题。本次提交 ${state.answered} 题,跳过 ${state.skipped} 道已有标准答案的题目。` });
}
function advance(view, skipped) {
visited.set(view.question.questionIndex, { ...view.question, options: view.question.options.map(option => ({ ...option })) });
if (skipped) state = { ...state, skipped: state.skipped + 1 };
pending = null;
if (view.question.questionIndex === binding.total - 1) return finish();
beginNavigation(view, view.question.questionIndex + 1);
}
function decide(view) {
update({ phase: 'checking', current: view.question.questionIndex + 1, message: `正在核验第 ${view.question.questionIndex + 1} / ${binding.total} 题。` });
if (!state.active) return;
const local = matchingRecord(view.question);
if (local.kind === 'confirmed') {
if (view.feedbackPresent && view.standard !== local.standard) throw new Error('网页反馈与本地标准答案不一致,已停止保留冲突。');
return advance(view, true);
}
pending = { mark: view.mark, questionIndex: view.question.questionIndex, deadline: now() + 15000, clicked: false };
if (view.feedbackPresent || view.checked) {
update({ phase: 'waiting-answer', message: '当前题已有选择或反馈,正在等待同题标准答案入库,不会重复提交。' });
return;
}
const target = view.controls.find(control => control.text === '正确');
if (!target || !clickable(target.label) || target.input.disabled) throw new Error('当前题“正确”选项不可正常操作,已停止。');
update({ phase: 'selecting', message: `正在通过正常练习提交第 ${view.question.questionIndex + 1} 题,以取得网站标准反馈。` });
if (!assertBeforeClick(view)) return;
target.label.click();
if (!state.active) return;
pending.clicked = true;
update({ phase: 'waiting-answer', answered: state.answered + 1, message: '已提交一次,等待网站标准反馈及同题可信记录;不会重复点击。' });
}
function tick() {
try {
const ctx = context(), view = readView(ctx);
if (state.phase === 'navigating') {
if (now() >= navigation.deadline) throw new Error('换题等待超过15秒,已停止。');
if (!view || view.question.questionIndex === navigation.from) return;
if (view.question.questionIndex !== navigation.target) throw new Error('页面跳到了其他题目,已停止。');
navigation = null;
decide(view);
} else if (state.phase === 'waiting-answer') {
if (!view || view.mark !== pending.mark) throw new Error('等待反馈时题目或选项已变化,已停止。');
if (now() >= pending.deadline) throw new Error('标准反馈或可信入库等待超过15秒,已停止;请人工核对当前题。');
const local = matchingRecord(view.question);
if (!view.feedbackPresent || local.kind !== 'confirmed') return;
if (local.standard !== view.standard) throw new Error('网站标准反馈与本地入库答案不一致,已停止。');
advance(view, !pending.clicked);
} else {
if (!view) throw new Error('当前题目或选项尚不完整,已停止。');
if (view.question.questionIndex !== 0 && visited.size === 0) beginNavigation(view, 0);
else decide(view);
}
} catch (error) { stop(error.message || '练习补齐遇到异常,已停止。'); }
finally { schedule(); }
}
function start() {
if (destroyed || state.active) return getState();
try {
if (!core?.mapAnswers) throw new Error('题库核验模块不可用。');
binding = null;
const ctx = context(), view = readView(ctx);
if (!view) throw new Error('当前判断练习题目未完整加载,不能启动。');
binding = { bankId: ctx.bank.bankId, bankName: ctx.bank.bankName, taskId: ctx.taskId, total: ctx.total, route: ctx.route };
visited.clear(); pending = null; navigation = null; indexSource = null; indexLength = -1; recordIndex = null; idIndex = null;
generation++;
update({ active: true, phase: 'checking', reason: '', answered: 0, skipped: 0, current: view.question.questionIndex + 1,
total: ctx.total, bankId: binding.bankId, bankName: binding.bankName, taskId: binding.taskId,
message: '练习补齐已手动启动,将从第1题核验本题库;仅使用正常练习控件。' });
schedule();
} catch (error) { stop(error.message || '不能启动练习补齐。'); }
return getState();
}
const routeChanged = () => { if (state.active) stop('页面地址已变化,练习补齐已停止。'); };
win.addEventListener?.('hashchange', routeChanged);
win.addEventListener?.('popstate', routeChanged);
win.addEventListener?.('pagehide', routeChanged);
function destroy() {
stop('练习补齐已关闭。'); destroyed = true;
win.removeEventListener?.('hashchange', routeChanged);
win.removeEventListener?.('popstate', routeChanged);
win.removeEventListener?.('pagehide', routeChanged);
}
return { start, stop, getState, destroy };
}
return { create };
})();
if (typeof module !== 'undefined' && module.exports) module.exports = QBCollector;
// ---- src/app.js ----
// UI and persistence stay inside the userscript sandbox. Page data is rendered as text.
(() => {
@@ -2273,10 +2538,46 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
let status = loadError || cleanupNotice || '可输入文字或划选题干检索。';
let storageError = false;
let mounted = false;
let collector = null, collectorInitError = '';
const createAdapter = () => QBAdapter.create({ window: pageWindow, document, onQuestions: receive, onStatus: adapterStatus, onBankChange: bankChanged, observeNetwork: networkEnabled });
let adapter = createAdapter();
const autoAnswer = QBAuto.create({ window: pageWindow, document, core: QBCore, getRecords: () => records, getQuestion: currentQuestion, getBank: currentBank,
onStatus: message => { tell(message); }, onChange: () => { renderAuto(); } });
try {
if (typeof QBCollector !== 'undefined') collector = QBCollector.create({ window: pageWindow, document, core: QBCore,
getRecords: () => records, getQuestion: currentQuestion, getBank: currentBank,
isCollecting: () => networkEnabled && config.collecting && !loadError && !storageError && !importing,
onChange: () => { if (mounted && config.mode === 'open' && config.tab === 'auto') renderAuto(); } });
} catch (error) { collectorInitError = `练习补齐模块加载失败:${error.message}`; }
function stopCollector(message) {
if (!collector) return;
try { if (collector.getState().active) collector.stop(message); }
catch (error) { tell(`停止练习补齐失败:${error.message}`, true); }
}
function collectorBlockReason() {
if (!collector) return collectorInitError || '练习补齐模块未加载,请更新完整脚本。';
if (!networkEnabled || !['www.shhuayi-elearning.cn', 'shhuayi-elearning.cn'].includes(window.location.hostname)) return '仅支持华谊网站当前判断题练习。';
if (!config.collecting) return '请先继续题库采集,再启动练习补齐。';
if (loadError || storageError || importing) return '题库读取、保存或导入尚未就绪,练习补齐已停用。';
const bank = currentBank();
if (!bank?.bankId || bank.loading || currentQuestion()?.type !== 'judge' ||
!document.querySelector('.yxtulcdsdk-pc-marking-main .yxtulcdsdk-form[practice]')) return '请先打开当前判断题的正常练习页面。';
return '';
}
function startCollector() {
const blocked = collectorBlockReason();
if (blocked) { tell(blocked, true); renderAuto(); return; }
try {
autoAnswer.setEnabled(false);
collector.start();
renderAuto();
} catch (error) {
stopCollector(`练习补齐失败:${error.message}`);
tell(`练习补齐失败:${error.message}`, true);
renderAuto();
}
}
function activateKnownPage() {
if (!networkEnabled && document.querySelector('.yxtulcdsdk-pc-marking-main')) {
@@ -2292,6 +2593,7 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
function bankChanged(bank) {
if (!bank || (!bank.bankName && !bank.bankId)) {
stopCollector('当前练习题库已离开,练习补齐已停止。');
if (activeBank) {
activeBank = null; scope = ''; searchScope = '';
renderVisible();
@@ -2303,6 +2605,7 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
const identityChanged = previous && (!sameName || (previous.taskId && bank.taskId && previous.taskId !== bank.taskId) || (previous.bankId && bank.bankId && previous.bankId !== bank.bankId));
const changed = JSON.stringify(previous) !== JSON.stringify(bank);
activeBank = { ...bank };
if (identityChanged || bank.loading) stopCollector('题库正在切换,练习补齐已停止。');
if (identityChanged) { scope = '__current__'; searchScope = '__current__'; }
if (changed) renderVisible();
}
@@ -2340,6 +2643,7 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
storageError = false;
} catch (error) {
storageError = true;
stopCollector('题库保存失败,练习补齐已停止,请先备份数据。');
tell(`保存失败:${error.message}。当前内存数据仍可导出,请及时备份。`, true);
}
}
@@ -2408,7 +2712,7 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
recordsDirty = true;
schedulePersist();
renderVisible();
} catch (error) { tell(`这批题目未保存:${error.message}`, true); }
} catch (error) { stopCollector('题目未能保存,练习补齐已停止。'); tell(`这批题目未保存:${error.message}`, true); }
}
function tell(message, error = false) {
status = message;
@@ -2575,12 +2879,18 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
shadow.getElementById('qb-current-bank').textContent = name;
}
function renderAuto() {
if (!shadow || config.mode !== 'open' || config.tab !== 'auto') return;
if (!mounted || !shadow || config.mode !== 'open' || config.tab !== 'auto') return;
const state = autoAnswer.getState();
shadow.getElementById('qb-auto-enabled').checked = state.enabled;
shadow.getElementById('qb-auto-status').textContent = state.message || '自动答题尚未开启。可先点击“答当前题”。';
shadow.getElementById('qb-auto-count').textContent = `本页已选择 ${state.answeredCount || 0} 题 · 已跳过 ${state.skippedCount || 0} 题`;
shadow.querySelector('[data-action="auto-answer-current"]').disabled = Boolean(state.busy);
const collection = collector?.getState() || { active: false, answered: 0, skipped: 0, current: 0, total: 0 };
const blocked = collectorBlockReason();
shadow.getElementById('qb-collector-status').textContent = collection.message || blocked || '练习补齐默认关闭;仅在点击启动后运行。';
shadow.getElementById('qb-collector-count').textContent = `本次已作答 ${collection.answered || 0} 题 · 已跳过 ${collection.skipped || 0} 题 · 当前 ${collection.current || 0}/${collection.total || 0}`;
shadow.querySelector('[data-action="start-collector"]').disabled = Boolean(collection.active || blocked);
shadow.querySelector('[data-action="stop-collector"]').disabled = !collection.active;
}
function matchesCurrentQuestion(record, current) {
if (!current || record.type !== current.type) return false;
@@ -2799,6 +3109,7 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
</div>
<div class="view" id="qb-view-auto" data-view="auto" role="tabpanel" hidden>
<div class="section"><h3>自动选择正确答案</h3><p>根据本地已确认答案核对题干、题型和完整选项,按当前顺序选择。没有答案、答案冲突或无法唯一匹配的题目会跳过。</p><button type="button" class="btn primary" data-action="auto-answer-current">答当前题</button><label class="check"><input id="qb-auto-enabled" type="checkbox"><span>连续自动选择<small>开启后随题目变化自动选择,仅本页有效;刷新或打开新页面后关闭。</small></span></label><p id="qb-auto-status" role="status"></p><p id="qb-auto-count"></p><p>选择后按页面流程检查、提交或换题;部分网站会自动保存作答。</p></div>
<div class="section"><h3>补齐判断题标准答案</h3><p>会逐题进行正常练习作答并改变练习记录;以网站标准反馈保存。只限当前判断练习,刷新默认关闭。隐藏面板后仍会继续;需要停止时请点击“停止补齐”。</p><div class="row"><button type="button" class="btn primary" data-action="start-collector">补齐当前判断题库</button><button type="button" class="btn" data-action="stop-collector" disabled>停止补齐</button></div><p id="qb-collector-status" role="status">练习补齐默认关闭。</p><p id="qb-collector-count"></p></div>
<div class="section"><h3>适配范围</h3><p>支持华谊练习页面,以及使用标准单选框/复选框和明确题干容器的网页。网页未识别时可继续使用文字搜索。</p></div>
</div>
<div class="view" id="qb-view-settings" data-view="settings" role="tabpanel" hidden>
@@ -2830,7 +3141,9 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
case 'collapse': setMode(config.mode === 'collapsed' ? 'open' : 'collapsed'); break;
case 'search': runSearch(shadow.getElementById('qb-search').value); break;
case 'clear-search': clearTimeout(searchTimer); runSearch(''); shadow.getElementById('qb-search').focus(); break;
case 'auto-answer-current': Promise.resolve(autoAnswer.answerCurrent()).catch(error => tell(`自动选择失败:${error.message}`, true)); break;
case 'auto-answer-current': stopCollector('已切换为答当前题,练习补齐已停止。'); Promise.resolve(autoAnswer.answerCurrent()).catch(error => tell(`自动选择失败:${error.message}`, true)); break;
case 'start-collector': startCollector(); break;
case 'stop-collector': stopCollector('已手动停止练习补齐。'); renderAuto(); break;
case 'capture-current': {
const captured = adapter.captureCurrent();
renderCurrent();
@@ -2839,6 +3152,7 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
}
case 'toggle-capture':
config.collecting = !config.collecting;
if (!config.collecting) stopCollector('题库采集已暂停,练习补齐已停止。');
config.collecting ? adapter.start() : adapter.stop();
adapter.setCopyEnabled(Boolean(config.copyEnabled));
persistConfig(); renderStats();
@@ -2864,13 +3178,18 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
shadow.getElementById('qb-scope').addEventListener('change', event => { scope = event.target.value; renderStats(); });
shadow.getElementById('qb-search-scope').addEventListener('change', event => { searchScope = event.target.value; renderResults(); });
shadow.getElementById('qb-search-field').addEventListener('change', event => { searchField = event.target.value; renderResults(); });
shadow.getElementById('qb-auto-enabled').addEventListener('change', event => { autoAnswer.setEnabled(event.target.checked); renderAuto(); });
shadow.getElementById('qb-auto-enabled').addEventListener('change', event => {
const enabled = event.target.checked;
if (enabled) stopCollector('已启用连续自动选择,练习补齐已停止。');
autoAnswer.setEnabled(enabled); renderAuto();
});
shadow.getElementById('qb-copy-enabled').addEventListener('change', event => { config.copyEnabled = event.target.checked; adapter.setCopyEnabled(config.copyEnabled); persistConfig(); });
shadow.getElementById('qb-import-file').addEventListener('change', async event => {
const input = event.currentTarget;
if (importing) return;
const files = Array.from(input.files || []);
importing = true;
stopCollector('开始导入题库,练习补齐已停止。');
const button = shadow.querySelector('[data-action="import"]');
button.disabled = true;
try { await importFiles(files); }
@@ -2902,9 +3221,9 @@ if (typeof module !== 'undefined' && module.exports) module.exports = QBAuto;
event.preventDefault(); setMode(config.mode === 'hidden' ? 'open' : 'hidden');
}
}, true);
window.addEventListener('pagehide', () => { autoAnswer.setEnabled(false); persistRecords(); });
window.addEventListener('pagehide', () => { stopCollector('页面已离开,练习补齐已停止。'); autoAnswer.setEnabled(false); persistRecords(); });
window.addEventListener('pageshow', event => {
if (event.persisted) { autoAnswer.setEnabled(false); config.mode = 'hidden'; renderMode(); }
if (event.persisted) { stopCollector('页面已恢复,练习补齐保持关闭。'); autoAnswer.setEnabled(false); config.mode = 'hidden'; renderMode(); }
});
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('显示题库助手(Alt+Shift+Q)', () => setMode('open'));