feat: support desktop exams with strict answer text verification
This commit is contained in:
+43
-13
@@ -14,6 +14,12 @@
|
||||
const emptyMetrics = () => ({ selected: 0, pending: 0, missing: 0, conflict: 0, locked: 0, failed: 0, unsupported: 0, total: 0 });
|
||||
let metrics = emptyMetrics();
|
||||
|
||||
function visible(node) {
|
||||
if (!node || node.closest('[hidden],.hidden')) return false;
|
||||
const style = getComputedStyle(node);
|
||||
return node.getClientRects().length > 0 && style.display !== 'none' && style.visibility !== 'hidden';
|
||||
}
|
||||
|
||||
function routeInfo() {
|
||||
const original = location.protocol === 'file:' ? core.savedSourceURL(document) : '';
|
||||
const url = new URL(original || location.href);
|
||||
@@ -23,11 +29,24 @@
|
||||
const mode = (params.get('flag') || '').toLowerCase();
|
||||
const marker = root && Array.from(root.querySelectorAll('.pc_examtest_commit'))
|
||||
.some(node => node.textContent.trim() === '重新练习' && !node.classList.contains('hidden'));
|
||||
const desktop = root && root.querySelector('.pc_examtest_left') && root.querySelector('.pc_examtest_card')
|
||||
&& (root.matches('.pc_examtest') || root.querySelector('.pc_examtest'));
|
||||
const commit = root && root.querySelector('.pc_examtest_commit');
|
||||
const countdown = root && root.querySelector('.pc_examtest_countdown .time');
|
||||
const expired = countdown && /^\s*0{1,2}:00:00\s*$/.test(countdown.textContent);
|
||||
const submitting = Array.from(document.querySelectorAll('[beingsubmitted],.beingsubmitting,.exam_submited_box')).some(visible);
|
||||
const submitted = !!(root && root.querySelector('#examtestDisabled,.pc_examtest_commitdefult')) || submitting;
|
||||
// app-dialog-box 的 modal 模板会挂到全局;不能只检查组件后代。
|
||||
const confirming = Array.from(document.querySelectorAll('.dialog_box,app-dialog-box > *,.examine_box')).some(visible);
|
||||
const practice = !!root && (mode ? mode === 'exercise' : !!marker);
|
||||
const exam = !!desktop && mode === 'test' && !!commit && visible(commit)
|
||||
&& commit.textContent.trim() === '交卷' && !submitted && !expired && !confirming;
|
||||
const identity = new URL(location.href);
|
||||
identity.searchParams.delete('t');
|
||||
return { root, practice: !!root && (mode ? mode === 'exercise' : !!marker),
|
||||
return { root, practice, exam, answerSupported: practice || exam, expired: !!expired,
|
||||
submitted: !!submitted, confirming,
|
||||
local: location.protocol === 'file:', sourceUrl: url.href, key: identity.href,
|
||||
expectedTitle: mode === 'exercise' ? params.get('title') || '' : '', mode };
|
||||
expectedTitle: mode === 'exercise' || mode === 'test' ? params.get('title') || '' : '', mode };
|
||||
}
|
||||
|
||||
function structureSignature(bank) {
|
||||
@@ -42,7 +61,7 @@
|
||||
const signature = route.root ? structureSignature(bank) : '';
|
||||
return { ready: !!page && page.ready && !loading && page.key === route.key
|
||||
&& page.root === route.root && page.signature === signature,
|
||||
practice: route.practice, local: route.local, role: config.role, course: bank.title,
|
||||
practice: route.practice, exam: route.exam, mode: route.mode, local: route.local, role: config.role, course: bank.title,
|
||||
sourceUrl: route.sourceUrl, routeKey: route.key, root: route.root,
|
||||
pageSignature: signature, revision: snapshot.revision, snapshot };
|
||||
}
|
||||
@@ -84,8 +103,8 @@
|
||||
}
|
||||
function setAutoAnswer(enabled) {
|
||||
const context = getContext();
|
||||
if (enabled && (!context.ready || !context.practice || context.local || !config.role || !snapshot.counts[config.role])) {
|
||||
panel.setNotice('请先导入所选岗位题库,并进入已加载完成的兼容练习页面。', 'warning'); refreshPanel(); return false;
|
||||
if (enabled && (!context.ready || !(context.practice || context.exam) || context.local || !config.role || !snapshot.counts[config.role])) {
|
||||
panel.setNotice('请先导入所选岗位题库,并进入已加载完成的兼容练习或桌面考试页面。', 'warning'); refreshPanel(); return false;
|
||||
}
|
||||
config.autoAnswer = !!enabled; runKey = '';
|
||||
if (!enabled) runner.stop('自动选择已关闭,已生效的选择保留。');
|
||||
@@ -140,7 +159,7 @@
|
||||
if (runner.running) runner.stop('当前页面未识别到练习题目');
|
||||
if (page) previous = page;
|
||||
page = null;
|
||||
refreshPanel({ canAnswer: false, canExport: false, pageStatus: '当前网页可搜索题库和复制文字;未识别到支持的练习题目。' }); return;
|
||||
refreshPanel({ canAnswer: false, canExport: false, pageMode: '', pageStatus: '当前网页可搜索题库和复制文字;未识别到支持的题目。' }); return;
|
||||
}
|
||||
const bank = core.extract(document, route.sourceUrl), signature = structureSignature(bank), now = Date.now();
|
||||
if (!page || page.key !== route.key || page.root !== route.root) {
|
||||
@@ -148,27 +167,38 @@
|
||||
const last = page || previous;
|
||||
page = { key: route.key, root: route.root, signature, since: now, ready: false,
|
||||
waiting: !!last && last.key !== route.key && last.root === route.root && last.signature === signature };
|
||||
if (route.mode === 'test') config.autoAnswer = false;
|
||||
runKey = ''; metrics = emptyMetrics(); refreshPanel({ expanded: true });
|
||||
}
|
||||
if (page.signature !== signature) {
|
||||
if (runner.running) runner.stop('题目内容已更新');
|
||||
if (route.mode === 'test') config.autoAnswer = false;
|
||||
page.signature = signature; page.since = now; page.waiting = false; runKey = ''; metrics = emptyMetrics();
|
||||
}
|
||||
const countComplete = bank.expectedTotal === null || bank.expectedTotal === bank.total;
|
||||
const countComplete = route.mode === 'test' ? bank.expectedTotal > 0 && bank.expectedTotal === bank.total
|
||||
: bank.expectedTotal === null || bank.expectedTotal === bank.total;
|
||||
const titleReady = !route.expectedTitle || route.expectedTitle.trim() === bank.title.trim();
|
||||
page.ready = !!bank.total && countComplete && titleReady && !page.waiting && now - page.since >= settleMs;
|
||||
let status;
|
||||
if (!route.practice) status = '已识别题目,但未确认练习模式;自动选择和导出暂停。';
|
||||
if (route.mode === 'test' && !route.exam) status = route.expired ? '考试计时已结束,自动选择已停止。'
|
||||
: route.submitted ? '考试已提交或提交中,自动选择已停止。'
|
||||
: route.confirming ? '考试对话框已打开,自动选择已停止。' : '考试页面尚未确认可作答,等待完整桌面试卷加载。';
|
||||
else if (!route.answerSupported) status = '已识别题目,但未确认支持的练习或桌面考试模式。';
|
||||
else if (page.waiting || !titleReady) status = '等待本次练习的题目加载…';
|
||||
else if (!bank.total || now - page.since < settleMs) status = '正在读取题目与隐藏答案…';
|
||||
else if (!countComplete) status = '已读取 ' + bank.total + ' / ' + bank.expectedTotal + ' 题,等待全部加载。';
|
||||
else if (!countComplete) status = bank.expectedTotal === null ? '等待考试答题卡总题数,确认完整试卷后才能自动选择。'
|
||||
: '已读取 ' + bank.total + ' / ' + bank.expectedTotal + ' 题,等待全部加载。';
|
||||
else if (route.exam) status = bank.title + ':' + bank.total + ' 题。按所选岗位题库严格核对题干、选项和正确答案全文。'
|
||||
+ (route.local ? ' 保存网页只用于核对,自动选择需要在线考试页面。' : '完成后请检查结果并自行交卷。');
|
||||
else status = bank.title + ':' + bank.total + ' 题,' + (bank.missingAnswers.length
|
||||
? '缺少 ' + bank.missingAnswers.length + ' 个网页答案;可用导入题库选择。' : '网页答案齐全。')
|
||||
+ (route.local ? ' 保存网页支持搜索与导出;自动选择需要在线练习页面。' : '');
|
||||
refreshPanel({ pageStatus: status, canExport: route.practice && !!bank.total,
|
||||
canAnswer: route.practice && !route.local && page.ready && !loading && !!config.role && !!snapshot.counts[config.role] });
|
||||
if (!route.practice || !page.ready || loading) { if (runner.running) runner.stop('等待兼容练习页面准备完成'); return; }
|
||||
if (config.exportEnabled && bank.complete) {
|
||||
if (route.mode === 'test' && !route.exam) { config.autoAnswer = false; runKey = ''; }
|
||||
refreshPanel({ pageStatus: status, pageMode: route.mode === 'test' ? 'test' : route.practice ? 'exercise' : '',
|
||||
canExport: route.practice && !!bank.total,
|
||||
canAnswer: route.answerSupported && !route.local && page.ready && !loading && !!config.role && !!snapshot.counts[config.role] });
|
||||
if (!route.answerSupported || !page.ready || loading) { if (runner.running) runner.stop('等待可作答页面准备完成'); return; }
|
||||
if (route.practice && config.exportEnabled && bank.complete) {
|
||||
const key = route.key + '|' + signature;
|
||||
if (!exported.has(key)) {
|
||||
try { exportCurrent('csv'); exported.add(key); }
|
||||
|
||||
Reference in New Issue
Block a user