454 lines
25 KiB
JavaScript
454 lines
25 KiB
JavaScript
/* YianAnswerRunner: 只在已明确识别的练习或考试页面串行点击真实选项。 */
|
||
(function (root) {
|
||
'use strict';
|
||
|
||
const CHECKED = 'examtest_content_list_des_checked';
|
||
const WAIT_STEP = 100;
|
||
const WAIT_LIMIT = 1200;
|
||
const CLICK_GAP = 150;
|
||
const COUNT_KEYS = ['selected', 'pending', 'missing', 'conflict', 'locked', 'failed', 'unsupported'];
|
||
const CONTEXT_KEYS = ['role', 'course', 'sourceUrl', 'routeKey', 'pageSignature', 'revision', 'exam', 'mode'];
|
||
|
||
function normalize(value) {
|
||
// 仅中和排版空白和不可见分隔符;不折叠大小写、数字、单位或标点。
|
||
return String(value == null ? '' : value).replace(/[\u200b-\u200d\u2060\uFEFF]/g, '').replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function questionSignature(question) {
|
||
// 答案、checked class 和展开解析不属于题目身份,判分后它们会正常变化。
|
||
return JSON.stringify([question.type, question.question, (question.questionImages || []), question.options.map(function (option) {
|
||
return [option.label, option.text, option.images || []];
|
||
})]);
|
||
}
|
||
|
||
function bankSignature(bank) {
|
||
return JSON.stringify([bank.title, bank.expectedTotal, bank.questions.map(questionSignature)]);
|
||
}
|
||
|
||
function isLocked(box, selectors, context) {
|
||
if (box.querySelector('.pc_examtest_content_form_disabled')) return true;
|
||
if (box.closest('[disabled],[aria-disabled="true"]')) return true;
|
||
if (Array.from(box.querySelectorAll(selectors.option)).some(function (node) { return Boolean(node.closest('[disabled],[aria-disabled="true"]')); })) return true;
|
||
// 考试正常作答时没有查看答案控件,单选/判断也允许改选。
|
||
// 已提交、到时或出现提交对话框由 getContext 将 exam 置 false,每一步都会复核。
|
||
if (context && context.exam === true) return false;
|
||
const component = box.closest('[app-exam-question],app-exam-question');
|
||
const flags = [box, component].filter(Boolean);
|
||
if (flags.some(function (element) {
|
||
return element._showanswer === true || element.getAttribute('_showanswer') === 'true' || element.getAttribute('data-showanswer') === 'true';
|
||
})) return true;
|
||
if (!box.querySelector('.pc_examtest_content_question_showanswer')) return true;
|
||
// 原页面已判分时 option 的直接父容器由 child1 变为 child2;隐藏参考答案不表示锁定。
|
||
return Array.from(box.querySelectorAll(selectors.option)).some(function (node) {
|
||
const parent = node.parentElement;
|
||
return parent && parent.matches('.pc_examtest_content_list_child2,.pc_examtest_content_list_parent_child2,.parent_child2');
|
||
});
|
||
}
|
||
|
||
function optionState(box, selectors, context) {
|
||
const nodes = Array.from(box.querySelectorAll(selectors.option));
|
||
return {
|
||
nodes: nodes,
|
||
selected: nodes.map(function (node) { return node.classList.contains(CHECKED); }),
|
||
locked: isLocked(box, selectors, context)
|
||
};
|
||
}
|
||
|
||
function sameSelection(actual, wanted) {
|
||
return actual.length === wanted.length && actual.every(function (selected, index) { return selected === wanted[index]; });
|
||
}
|
||
|
||
function create(options) {
|
||
options = options || {};
|
||
if (typeof options.getContext !== 'function' || typeof options.match !== 'function') throw new TypeError('自动选择需要 getContext 和 match。');
|
||
const doc = root.document;
|
||
const core = root.QuestionBankCore;
|
||
if (!doc || !core || !core.selectors || typeof core.extract !== 'function') throw new Error('自动选择需要 QuestionBankCore 和当前网页。');
|
||
const selectors = core.selectors;
|
||
let active = null;
|
||
let scope = null;
|
||
let records = new Map();
|
||
let total = 0;
|
||
let destroyed = false;
|
||
let generation = 0;
|
||
|
||
function notifyState(state, reason) {
|
||
if (typeof options.onState === 'function') options.onState(state, reason || '');
|
||
}
|
||
|
||
function progress() {
|
||
const counts = { selected: 0, pending: 0, missing: 0, conflict: 0, locked: 0, failed: 0, unsupported: 0, total: total };
|
||
const details = [];
|
||
records.forEach(function (record) {
|
||
if (COUNT_KEYS.includes(record.status)) counts[record.status]++;
|
||
if (record.pending) counts.pending++;
|
||
details.push({
|
||
number: record.number, question: record.question, course: record.course,
|
||
pageCourse: record.pageCourse, sourceCourses: record.sourceCourses.slice(),
|
||
status: record.status, reason: record.reason || '',
|
||
correctTexts: record.correctTexts.slice(), correctLabels: record.correctLabels.slice(),
|
||
currentLabels: record.currentLabels.slice(), currentTexts: record.currentTexts.slice(),
|
||
currentAnswerText: record.currentAnswerText
|
||
});
|
||
});
|
||
counts.details = details;
|
||
return counts;
|
||
}
|
||
|
||
function notifyProgress() {
|
||
const value = progress();
|
||
if (typeof options.onProgress === 'function') options.onProgress(value);
|
||
return value;
|
||
}
|
||
|
||
function setRecord(item, status, reason, pending) {
|
||
const nodes = Array.from(item.box.querySelectorAll(selectors.option));
|
||
const selected = item.question.options.filter(function (_, index) { return nodes[index] && nodes[index].classList.contains(CHECKED); });
|
||
const sourceCourses = (item.sourceCourses || []).slice();
|
||
records.set(item.key, {
|
||
index: item.index, number: item.question.number, question: item.question.question,
|
||
course: sourceCourses.length ? sourceCourses.join('、') : item.course,
|
||
pageCourse: item.course, sourceCourses: sourceCourses,
|
||
status: status, reason: reason || '', pending: Boolean(pending),
|
||
correctTexts: (item.correctTexts || []).slice(), correctLabels: (item.correctLabels || []).slice(),
|
||
currentLabels: selected.map(function (option) { return option.label; }),
|
||
currentTexts: selected.map(function (option) { return option.text; }),
|
||
currentAnswerText: selected.map(function (option) { return option.label + '. ' + option.text; }).join(';')
|
||
});
|
||
notifyProgress();
|
||
}
|
||
|
||
function captureContext(context) {
|
||
const captured = { root: context.root, pageURL: String(root.location && root.location.href || '') };
|
||
CONTEXT_KEYS.forEach(function (key) { captured[key] = context[key]; });
|
||
return captured;
|
||
}
|
||
|
||
function sameContext(left, right) {
|
||
return left && right && left.root === right.root && left.pageURL === right.pageURL && CONTEXT_KEYS.every(function (key) { return left[key] === right[key]; });
|
||
}
|
||
|
||
function initialReason(context) {
|
||
if (!context || context.ready !== true) return '等待答题页面就绪。';
|
||
if (context.local === true || root.location && root.location.protocol === 'file:') return '保存的静态网页可搜索和导出,不能记录自动选择。';
|
||
if (context.practice !== true && context.exam !== true) return '当前页面未识别为可作答的练习或考试,自动选择已停止。';
|
||
if (!context.role) return '请先选择主操、班长或副操。';
|
||
if (!context.root || !context.root.isConnected || context.root.ownerDocument !== doc) return '答题内容已切换,等待新页面。';
|
||
return '';
|
||
}
|
||
|
||
function cancel(run) {
|
||
if (!run || run.cancelled) return;
|
||
run.cancelled = true;
|
||
run.waiters.forEach(function (finish) { finish(false); });
|
||
run.waiters.clear();
|
||
}
|
||
|
||
function endActive(state, reason) {
|
||
const old = active;
|
||
generation++;
|
||
active = null;
|
||
cancel(old);
|
||
if (!destroyed) notifyState(state, reason);
|
||
return progress();
|
||
}
|
||
|
||
function pause(reason) { return endActive('paused', reason || '已暂停自动选择。'); }
|
||
|
||
function stop(reason) { return endActive('stopped', reason || '自动选择已关闭。'); }
|
||
|
||
function wait(run, milliseconds) {
|
||
if (run.cancelled || active !== run) return Promise.resolve(false);
|
||
return new Promise(function (resolve) {
|
||
let timer;
|
||
let settled = false;
|
||
const finish = function (ok) {
|
||
if (settled) return;
|
||
settled = true;
|
||
root.clearTimeout(timer);
|
||
run.waiters.delete(finish);
|
||
resolve(ok && !run.cancelled && active === run);
|
||
};
|
||
run.waiters.add(finish);
|
||
timer = root.setTimeout(function () { finish(true); }, milliseconds);
|
||
});
|
||
}
|
||
|
||
function contextReason(run) {
|
||
if (destroyed || run.cancelled || active !== run || run.id !== generation) return '自动选择已取消。';
|
||
const context = options.getContext();
|
||
const reason = initialReason(context);
|
||
if (reason) return reason;
|
||
if (!sameContext(run.context, captureContext(context))) return '页面、岗位或题库已切换,已停止本轮自动选择。';
|
||
return '';
|
||
}
|
||
|
||
function extractItems(context) {
|
||
const boxes = Array.from(doc.querySelectorAll(selectors.question));
|
||
const bank = core.extract(doc);
|
||
if (bank.questions.length !== boxes.length) throw new Error('题目与选项尚未同步完成。');
|
||
const items = boxes.map(function (box, index) {
|
||
const question = bank.questions[index];
|
||
const signature = questionSignature(question);
|
||
return { box: box, index: index, question: question, course: context.course || '', signature: signature, key: index + '\u0000' + signature };
|
||
}).filter(function (item) { return context.root === item.box || context.root.contains(item.box); });
|
||
return { items: items, signature: bankSignature(bank) };
|
||
}
|
||
|
||
function readCurrent(run, item) {
|
||
const reason = contextReason(run);
|
||
if (reason) return { reason: reason, cancelled: true };
|
||
if (!item.box.isConnected || !run.context.root.contains(item.box) && run.context.root !== item.box) return { reason: '题目节点已更新,已停止本轮自动选择。', cancelled: true };
|
||
const boxes = Array.from(doc.querySelectorAll(selectors.question));
|
||
if (boxes[item.index] !== item.box) return { reason: '题目顺序已变化,已停止本轮自动选择。', cancelled: true };
|
||
const bank = core.extract(doc);
|
||
// 父面板的 MutationObserver 尚未运行时,也直接阻断标题或其他题目的变化。
|
||
if (bankSignature(bank) !== run.bankSignature) return { reason: '题目或课程已更新,已停止本轮自动选择。', cancelled: true };
|
||
const current = bank.questions[item.index];
|
||
if (!current || questionSignature(current) !== item.signature) return { reason: '题干或选项已变化,已停止本轮自动选择。', cancelled: true };
|
||
const state = optionState(item.box, selectors, run.context);
|
||
if (state.nodes.length !== current.options.length) return { reason: '选项尚未完整加载。' };
|
||
return { question: current, state: state };
|
||
}
|
||
|
||
function clickable(node, item) {
|
||
if (!node || !node.isConnected || !item.box.contains(node) || node.ownerDocument !== doc) return false;
|
||
if (node.closest('[hidden],[inert]') || node.getAttribute('aria-disabled') === 'true' || node.hasAttribute('disabled')) return false;
|
||
const style = root.getComputedStyle(node);
|
||
return node.getClientRects().length > 0 && style.display !== 'none' && style.visibility !== 'hidden' && style.visibility !== 'collapse' && style.pointerEvents !== 'none';
|
||
}
|
||
|
||
function abortForChange(run, reason) {
|
||
if (active === run && !run.cancelled) stop(reason);
|
||
return false;
|
||
}
|
||
|
||
async function clickOption(run, item, optionIndex, wantedChecked) {
|
||
const before = readCurrent(run, item);
|
||
if (before.cancelled) return { cancelled: true, reason: before.reason };
|
||
if (before.reason) return { reason: before.reason };
|
||
if (before.state.locked) return { locked: true, reason: '该题已判分或锁定,不能修改。' };
|
||
if (before.state.selected[optionIndex] === wantedChecked) return { ok: true };
|
||
const node = before.state.nodes[optionIndex];
|
||
if (!clickable(node, item)) return { reason: '选项不可点击,已跳过。' };
|
||
// 最后一次即时检查,避免异步匹配期间已经换路由或岗位。
|
||
const reason = contextReason(run);
|
||
if (reason) return { cancelled: true, reason: reason };
|
||
node.click();
|
||
for (let elapsed = 0; elapsed <= WAIT_LIMIT; elapsed += WAIT_STEP) {
|
||
const current = readCurrent(run, item);
|
||
if (current.cancelled) return { cancelled: true, reason: current.reason };
|
||
if (current.reason) return { reason: current.reason };
|
||
if (current.state.selected[optionIndex] === wantedChecked) {
|
||
if (!await wait(run, CLICK_GAP)) return { cancelled: true, reason: '自动选择已取消。' };
|
||
const settled = readCurrent(run, item);
|
||
if (settled.cancelled) return { cancelled: true, reason: settled.reason };
|
||
if (settled.reason) return { reason: settled.reason };
|
||
if (settled.state.selected[optionIndex] !== wantedChecked) return { reason: '网页未保留选项状态,已停止修改该题。' };
|
||
return { ok: true };
|
||
}
|
||
if (current.state.locked) return { locked: true, reason: '选项点击后题目锁定,但未确认目标选项,已停止修改该题。' };
|
||
if (elapsed < WAIT_LIMIT && !await wait(run, WAIT_STEP)) return { cancelled: true, reason: '自动选择已取消。' };
|
||
}
|
||
return { reason: '网页在限定时间内未更新选项状态,已跳过。' };
|
||
}
|
||
|
||
function resolveTargets(question, matched) {
|
||
const labels = Array.isArray(matched.correctLabels) ? matched.correctLabels.map(normalize) : String(matched.correctLabels || '').split(/[\s,,、;;]+/).filter(Boolean);
|
||
if (!labels.length || new Set(labels).size !== labels.length) return { reason: '正确选项为空或重复,已跳过。' };
|
||
const actualLabels = question.options.map(function (option) { return normalize(option.label); });
|
||
if (new Set(actualLabels).size !== actualLabels.length || labels.some(function (label) { return !actualLabels.includes(label); })) return { reason: '答案不能唯一对应网页选项,已跳过。' };
|
||
if (question.type !== '多选' && labels.length !== 1) return { reason: '单选或判断题匹配到多个答案,已跳过。' };
|
||
if (!Array.isArray(matched.correctTexts) || matched.correctTexts.length !== labels.length) return { reason: '题库缺少完整正确答案文字,不能仅凭答案字母自动选择。' };
|
||
const expectedTexts = matched.correctTexts.map(normalize);
|
||
if (expectedTexts.some(function (text) { return !text; }) || new Set(expectedTexts).size !== expectedTexts.length) return { reason: '正确答案文字为空或重复,无法唯一核验。' };
|
||
const selected = question.options.filter(function (option) { return labels.includes(normalize(option.label)); });
|
||
const actualTexts = selected.map(function (option) { return normalize(option.text); });
|
||
if (JSON.stringify(expectedTexts.slice().sort()) !== JSON.stringify(actualTexts.slice().sort())) return { reason: '题库正确答案全文与网页选项不一致,已跳过。' };
|
||
return {
|
||
wanted: actualLabels.map(function (label) { return labels.includes(label); }),
|
||
correctLabels: selected.map(function (option) { return option.label; }),
|
||
// 原始题库文本按当页选项顺序存入证据,方便与 currentLabels 逐项核对。
|
||
correctTexts: actualTexts.map(function (text) { return matched.correctTexts[expectedTexts.indexOf(text)]; })
|
||
};
|
||
}
|
||
|
||
async function processQuestion(run, item) {
|
||
run.currentBox = item.box;
|
||
let current = readCurrent(run, item);
|
||
if (current.cancelled) return abortForChange(run, current.reason);
|
||
if (current.reason) { setRecord(item, 'failed', current.reason); return true; }
|
||
const previous = records.get(item.key);
|
||
if (previous && previous.status !== 'selected' && !previous.manualInvalid) return true;
|
||
if (!item.box.closest('[app-exam-question],app-exam-question')) { setRecord(item, 'unsupported', '题目缺少已适配的答题组件,已跳过。'); return true; }
|
||
if (!['单选', '多选', '判断'].includes(current.question.type)) { setRecord(item, 'unsupported', '暂不支持该题型自动选择。'); return true; }
|
||
if (current.state.locked && !previous) { setRecord(item, 'locked', '该题已判分或锁定,保留原作答。'); return true; }
|
||
const matched = await options.match(current.question, options.getContext());
|
||
current = readCurrent(run, item);
|
||
if (current.cancelled) return abortForChange(run, current.reason);
|
||
if (current.reason) { setRecord(item, 'failed', current.reason); return true; }
|
||
item.correctTexts = matched && Array.isArray(matched.correctTexts) ? matched.correctTexts.slice() : [];
|
||
item.correctLabels = matched && Array.isArray(matched.correctLabels) ? matched.correctLabels.slice() : [];
|
||
item.sourceCourses = Array.from(new Set((matched && Array.isArray(matched.records) ? matched.records : []).map(function (record) { return normalize(record.course); }).filter(Boolean)));
|
||
if (current.state.locked && !previous) { setRecord(item, 'locked', '匹配期间该题已锁定,保留原作答。'); return true; }
|
||
if (!matched || matched.status !== 'matched') {
|
||
const status = matched && ['missing', 'conflict', 'unsupported'].includes(matched.status) ? matched.status : 'unsupported';
|
||
setRecord(item, status, matched && matched.reason || '未获得可自动选择的唯一答案。');
|
||
return true;
|
||
}
|
||
const target = resolveTargets(current.question, matched);
|
||
if (target.reason) { setRecord(item, 'unsupported', target.reason); return true; }
|
||
item.correctTexts = target.correctTexts;
|
||
item.correctLabels = target.correctLabels;
|
||
if (current.state.locked) {
|
||
if (sameSelection(current.state.selected, target.wanted)) setRecord(item, 'selected', '已重新核验原作答与题库答案一致。');
|
||
else setRecord(item, 'locked', '该题已锁定且当前作答不同于题库答案,保留原作答。');
|
||
return true;
|
||
}
|
||
let changed = false;
|
||
if (current.question.type === '多选') {
|
||
// 每次按当前集合核对;先去掉错误选项,随后补选目标选项,不重复 toggle。
|
||
const planned = [];
|
||
target.wanted.forEach(function (wanted, index) { if (!wanted && current.state.selected[index]) planned.push({ index: index, checked: false }); });
|
||
target.wanted.forEach(function (wanted, index) { if (wanted && !current.state.selected[index]) planned.push({ index: index, checked: true }); });
|
||
for (const step of planned) {
|
||
const result = await clickOption(run, item, step.index, step.checked);
|
||
if (result.cancelled) return abortForChange(run, result.reason);
|
||
if (!result.ok) { setRecord(item, result.locked && !changed ? 'locked' : 'failed', result.reason); return true; }
|
||
changed = true;
|
||
}
|
||
} else {
|
||
const wantedIndex = target.wanted.indexOf(true);
|
||
if (!sameSelection(current.state.selected, target.wanted)) {
|
||
if (current.state.selected[wantedIndex]) { setRecord(item, 'failed', '单选题有多个选中状态,已跳过。'); return true; }
|
||
const result = await clickOption(run, item, wantedIndex, true);
|
||
if (result.cancelled) return abortForChange(run, result.reason);
|
||
if (!result.ok) { setRecord(item, result.locked ? 'locked' : 'failed', result.reason); return true; }
|
||
changed = true;
|
||
}
|
||
}
|
||
current = readCurrent(run, item);
|
||
if (current.cancelled) return abortForChange(run, current.reason);
|
||
if (current.reason) { setRecord(item, 'failed', current.reason); return true; }
|
||
if (!sameSelection(current.state.selected, target.wanted)) { setRecord(item, 'failed', '网页当前选中集合与答案不一致,已停止修改该题。'); return true; }
|
||
const pending = run.context.exam !== true && current.question.type === '多选' && !current.state.locked;
|
||
setRecord(item, 'selected', pending ? '已选择,待网页确认。' : changed ? '已选择匹配答案。' : '当前已选中匹配答案。', pending);
|
||
return true;
|
||
}
|
||
|
||
async function execute(run, items) {
|
||
try {
|
||
for (const item of items) {
|
||
if (!await processQuestion(run, item)) return progress();
|
||
if (run.cancelled || active !== run) return progress();
|
||
}
|
||
if (active === run && !run.cancelled) {
|
||
active = null;
|
||
notifyState('complete', '本页自动选择已完成。');
|
||
}
|
||
} catch (error) {
|
||
if (active === run && !run.cancelled) {
|
||
if (run.currentBox) {
|
||
const item = items.find(function (candidate) { return candidate.box === run.currentBox; });
|
||
if (item) setRecord(item, 'failed', error && error.message || '读取或点击选项失败。');
|
||
}
|
||
pause(error && error.message || '自动选择遇到错误,已暂停。');
|
||
}
|
||
} finally {
|
||
cancel(run);
|
||
}
|
||
return progress();
|
||
}
|
||
|
||
function start() {
|
||
if (destroyed) return Promise.resolve(progress());
|
||
if (active && !active.cancelled) return active.promise;
|
||
const context = options.getContext();
|
||
const reason = initialReason(context);
|
||
if (reason) { notifyState('idle', reason); return Promise.resolve(progress()); }
|
||
const captured = captureContext(context);
|
||
if (!sameContext(scope, captured)) { scope = captured; records = new Map(); total = 0; }
|
||
let extracted;
|
||
try { extracted = extractItems(context); }
|
||
catch (error) { notifyState('idle', error.message || '等待题目加载。'); return Promise.resolve(progress()); }
|
||
const items = extracted.items;
|
||
total = items.length;
|
||
const currentKeys = new Set(items.map(function (item) { return item.key; }));
|
||
records.forEach(function (_, key) { if (!currentKeys.has(key)) records.delete(key); });
|
||
notifyProgress();
|
||
if (!items.length) { notifyState('idle', '当前没有可处理的答题题目。'); return Promise.resolve(progress()); }
|
||
const run = { id: ++generation, context: captured, bankSignature: extracted.signature, cancelled: false, waiters: new Set(), currentBox: null, promise: null };
|
||
active = run;
|
||
// 先写入 promise,再通知或执行,使同步 UI 回调中的 start 也不会并发运行。
|
||
run.promise = Promise.resolve().then(function () {
|
||
if (active !== run || run.cancelled) return progress();
|
||
notifyState('running', '正在选择当前岗位题库中的匹配答案。');
|
||
return execute(run, items);
|
||
});
|
||
return run.promise;
|
||
}
|
||
|
||
function manualClick(event) {
|
||
if (!event.isTrusted || destroyed) return;
|
||
const target = event.target && event.target.nodeType === 1 ? event.target : event.target && event.target.parentElement;
|
||
if (!target) return;
|
||
const run = active && !active.cancelled ? active : null;
|
||
let currentScope = run && run.context;
|
||
if (!currentScope && scope && records.size) {
|
||
const context = options.getContext();
|
||
if (!initialReason(context) && sameContext(scope, captureContext(context))) currentScope = scope;
|
||
}
|
||
if (!currentScope) return;
|
||
const submit = target.closest('.pc_examtest_commit,button[type="submit"],input[type="submit"]');
|
||
if (submit && currentScope.root.contains(submit)) {
|
||
pause('检测到手动交卷,已暂停自动选择。');
|
||
return;
|
||
}
|
||
const box = target.closest(selectors.question);
|
||
if (!box || currentScope.root !== box && !currentScope.root.contains(box)) return;
|
||
let invalidated = false;
|
||
if (target.closest(selectors.option)) {
|
||
const index = Array.from(doc.querySelectorAll(selectors.question)).indexOf(box);
|
||
records.forEach(function (record) {
|
||
if (record.index !== index || record.status !== 'selected') return;
|
||
// 捕获阶段网页 onclick 还未执行,不能把此前的 checked 集合当成改选后的证据。
|
||
record.status = 'unsupported';
|
||
record.reason = '手动作答后需重新核对';
|
||
record.pending = false;
|
||
record.manualInvalid = true;
|
||
record.correctTexts = [];
|
||
record.correctLabels = [];
|
||
record.currentLabels = [];
|
||
record.currentTexts = [];
|
||
record.currentAnswerText = '';
|
||
invalidated = true;
|
||
});
|
||
}
|
||
if (invalidated) notifyProgress();
|
||
if (run || invalidated) pause('检测到手动作答,已暂停自动选择;可重新开启继续。');
|
||
}
|
||
|
||
doc.addEventListener('click', manualClick, true);
|
||
|
||
function reset() {
|
||
stop('自动选择状态已重置。');
|
||
records = new Map();
|
||
scope = null;
|
||
total = 0;
|
||
return notifyProgress();
|
||
}
|
||
|
||
function destroy() {
|
||
if (destroyed) return;
|
||
stop('自动选择已卸载。');
|
||
destroyed = true;
|
||
doc.removeEventListener('click', manualClick, true);
|
||
records.clear();
|
||
}
|
||
|
||
return { start: start, stop: stop, reset: reset, destroy: destroy, get running() { return Boolean(active && !active.cancelled); } };
|
||
}
|
||
|
||
root.YianAnswerRunner = { version: '1.1.0', create: create };
|
||
})(typeof window !== 'undefined' ? window : globalThis);
|