feat: add manual confirmation toggle for incomplete exams

This commit is contained in:
薇星幻海
2026-10-18 01:43:23 +08:00
parent eb9c610b9f
commit 2e044655bb
14 changed files with 758 additions and 201 deletions
+29 -4
View File
@@ -22,7 +22,7 @@
bankTotal: 0, roleCounts: {}, storageMode: 'userscript', pageStatus: '等待识别题目页面', pageMode: '',
canAnswer: false, canExport: false, metrics: {}, exportEnabled: false, expanded: false, loading: false, position: null,
activeTab: 'overview', uniqueTotal: 0, conflictTotal: 0,
learning: { status: 'idle', mode: 'all', execution: 'sequential', rate: 1, answerInterval: 4, minSubmitMinutes: 6, tasks: [], total: 0, completed: 0, skipped: 0, waiting: 0, failed: 0, etaSeconds: null, video: {}, exam: {}, logs: [], message: '' }
learning: { status: 'idle', mode: 'all', execution: 'sequential', rate: 1, answerInterval: 4, minSubmitMinutes: 6, manualConfirmIncomplete: true, tasks: [], total: 0, completed: 0, skipped: 0, waiting: 0, failed: 0, etaSeconds: null, video: {}, exam: {}, logs: [], message: '' }
};
let destroyed = false;
let selectionText = '';
@@ -440,6 +440,16 @@
});
return input;
}
function learningSwitch(container, key, label, fallback) {
const control = makeSwitch(label, function () {
state.learning[key] = control.input.checked;
invoke('onLearningSetting', key, control.input.checked);
refreshLearning();
}, container);
control.input.setAttribute('data-testid', 'setting-' + key);
learningControls[key] = { input: control.input, fallback: fallback, switch: true };
return control.input;
}
function taskButton(container, label, action, className) {
const button = el(doc, 'button', className || '', label); button.type = 'button';
button.setAttribute('data-testid', 'task-' + action);
@@ -483,13 +493,17 @@
const examModeNote = el(doc, 'p', 'mode-note'); examIntro.appendChild(examModeNote);
learningSetting(examIntro, 'answerInterval', '答题间隔(秒)', null, 4, [0, 120, 0.5]);
learningSetting(examIntro, 'minSubmitMinutes', '最早交卷(分)', null, 6, [0.5, 180, 0.5]);
learningSwitch(examIntro, 'manualConfirmIncomplete', '题目不全时手动确认', true);
const examInfo = el(doc, 'dl', 'info-grid');
const examMessage = el(doc, 'p', 'explanation');
const examThreshold = el(doc, 'p', 'threshold');
const incompleteHint = el(doc, 'p', 'hint');
examThreshold.setAttribute('data-testid', 'exam-threshold');
incompleteHint.setAttribute('data-testid', 'incomplete-confirmation-hint');
const examActions = el(doc, 'div', 'task-actions');
const examPause = taskButton(examActions, '暂停考试任务', 'examPause');
const examResume = taskButton(examActions, '继续考试任务', 'examResume');
examIntro.append(examInfo, el(doc, 'p', 'threshold', '预计得分必须严格大于 80 分;正好 80 分等待人工。'), examMessage, examActions,
el(doc, 'p', 'hint', '仅已精确匹配、点击并核验完整答案文字的题目计分。进入原试卷满设定时长、核验完成且满足条件后才自动交卷;预计分数以本地题库为依据。练习与课中题不使用考试交卷门槛。'));
examIntro.append(examInfo, examThreshold, examMessage, examActions, incompleteHint);
views.exam.insertBefore(examIntro, switches);
const bankCounts = el(doc, 'p', 'bank-counts'); bankSection.appendChild(bankCounts);
@@ -692,8 +706,17 @@
}
Object.keys(learningControls).forEach(function (key) {
const setting = learningControls[key];
if (root.activeElement !== setting.input) setting.input.value = plain(learning[key] == null ? setting.fallback : learning[key]);
if (setting.switch) setting.input.checked = learning[key] == null ? setting.fallback : learning[key] !== false;
else if (root.activeElement !== setting.input) setting.input.value = plain(learning[key] == null ? setting.fallback : learning[key]);
});
const manualConfirmIncomplete = learning.manualConfirmIncomplete !== false;
setText(examThreshold, manualConfirmIncomplete
? '必须整卷每题核验且预计得分严格大于 80 分;题目不全时由用户在原网页手动确认交卷。'
: '允许题库部分缺题时自动交卷:已核验预计得分严格大于 80 分,所有可匹配题均已核验,且已满最早交卷时间。');
setText(incompleteHint, (manualConfirmIncomplete
? '本开关已开启:存在未匹配、冲突或未核验题时,即使超过 80 分、已满交卷时长也不自动交卷;网页提示未答完时同样等待人工。'
: '本开关已关闭:仅已精确匹配、点击并核验完整答案文字的题目计分,其余题目不计分;满足得分、计时与当前试卷核验条件后可自动交卷。')
+ '切换会暂停当前计划,请在任务总览点击继续重新核验。无论开关状态,试卷未完整加载均禁止自动交卷;预计得分正好 80 分等待人工。练习与课中题不使用考试交卷门槛。');
const videoEnabled = learning.mode !== 'exam';
setText(videoModeNote, videoEnabled ? '' : '本模式未启用视频;可查看设置与记录。'); videoModeNote.hidden = videoEnabled;
const examEnabled = learning.mode === 'exam' || learning.mode === 'all' || !learning.mode;
@@ -709,6 +732,8 @@
videoPause.disabled = !videoHasTask || ['paused', 'completed', 'done', 'idle'].includes(video.status || 'idle');
videoResume.disabled = !videoHasTask || !['paused', 'waiting', 'waitingHuman', 'failed', 'error'].includes(video.status);
infoRows(examInfo, [['考试主题', examTitle], ['题库预检', plain(firstValue(exam, ['preflightMessage', 'themeStatus', 'preflight'], '尚未预检'))],
['全文核验', exam.total == null ? '待确认' : plain(exam.verifiedCount || 0) + ' / ' + plain(exam.total) + ' 题'],
['交卷确认', exam.submitted || exam.status === 'submitted' ? '网站已确认交卷' : exam.requiresManualSubmission ? '题目未全部核验或网站提示未答完,请在原网页手动确认' : manualConfirmIncomplete ? '整卷每题核验后才按条件自动交卷' : '已核验得分超过 80 分并满最早交卷时间后才按条件自动交卷'],
['预计得分', firstValue(exam, ['predictedScore', 'score'], null) == null ? '待核算' : plain(firstValue(exam, ['predictedScore', 'score'], 0)) + ' 分(题库估计)'],
['网页剩余', duration(firstValue(exam, ['remainingSeconds', 'remainingTime'], null))], ['进入试卷', duration(firstValue(exam, ['elapsedSeconds', 'elapsedTime'], null))],
['最早交卷', duration(firstValue(exam, ['submitWaitSeconds', 'waitSeconds'], null))], ['交卷状态', statusName(exam.status || 'idle')]]);