// ==UserScript==
// @name 【自制】问卷星输入答案自动填写
// @namespace http://tampermonkey.net/
// @version 1.0.2
// @description 使用可配置的选择器来适配不同网站,支持复杂的输入格式。
// @match https://lms.ouchn.cn/exam/*
// @match https://ks.wjx.top/vm/mBcE5Ax.aspx
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(function() {
'use strict';
// 全局变量和常量
let questions = [];
let isQuestionDetected = false;
const GLOBAL = {
fillAnswerDelay: 300,
debounceDelay: 300,
};
const DEFAULT_SELECTORS = {
subjectContainer: '.exam-subjects > ol > li.subject',
questionText: '.subject-description',
options: '.subject-options input[type="radio"], .subject-options input[type="checkbox"]',
answerElement: '.answer-options',
};
let SELECTORS = {...DEFAULT_SELECTORS};
// 工具函数
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// UI 相关函数
function createMainInterface() {
const container = document.createElement('div');
container.id = 'auto-fill-container';
container.className = 'fixed top-5 right-5 bg-white p-6 rounded-lg shadow-xl w-96 max-w-[90%] transition-all duration-300 ease-in-out';
container.innerHTML = `
<h3 class="text-2xl font-bold mb-4 text-gray-800">自动填写答案</h3>
<div id="status-message" class="mb-4 text-sm font-medium text-gray-600"></div>
<div id="main-content">
<textarea id="bulk-input" class="w-full h-32 p-3 mb-4 border border-gray-300 rounded-md resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" placeholder="输入答案,如: A,B,C 或 1-3(ABC,DEF,GHI)"></textarea>
<div id="questions-preview" class="max-h-64 overflow-y-auto mb-4 bg-gray-50 rounded-md p-3"></div>
<div class="grid grid-cols-2 gap-3">
<button id="fillButton" class="col-span-2 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded transition duration-300">填写答案</button>
<button id="clearButton" class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-2 px-4 rounded transition duration-300">清空输入</button>
<button id="pasteButton" class="bg-green-500 hover:bg-green-600 text-white font-medium py-2 px-4 rounded transition duration-300">粘贴识别</button>
<button id="configButton" class="bg-purple-500 hover:bg-purple-600 text-white font-medium py-2 px-4 rounded transition duration-300">配置选择器</button>
<button id="detectButton" class="bg-yellow-500 hover:bg-yellow-600 text-white font-medium py-2 px-4 rounded transition duration-300">智能识别</button>
</div>
</div>
`;
document.body.appendChild(container);
// 添加事件监听器
document.getElementById('fillButton').addEventListener('click', fillAnswers);
document.getElementById('clearButton').addEventListener('click', clearInputs);
document.getElementById('pasteButton').addEventListener('click', pasteAndRecognize);
document.getElementById('configButton').addEventListener('click', showSelectorWizard);
document.getElementById('detectButton').addEventListener('click', smartDetectAnswers);
document.getElementById('bulk-input').addEventListener('input', debounce(updateQuestionsPreview, GLOBAL.debounceDelay));
}
function updateQuestionsPreview() {
const bulkInput = document.getElementById('bulk-input');
const questionsPreview = document.getElementById('questions-preview');
const answers = parseAnswers(bulkInput.value);
// 添加自定义样式
if (!document.getElementById('custom-question-preview-style')) {
const style = document.createElement('style');
style.id = 'custom-question-preview-style';
style.textContent = `
.question-row {
transition: all 0.3s ease;
}
.question-row:hover {
transform: translateY(-2px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
`;
document.head.appendChild(style);
}
questionsPreview.innerHTML = questions
.filter(q => q.type !== '未知类型')
.map((q, i) => {
const answer = answers[i] || '-';
const isValid = validateAnswer(answer, q);
const isFilled = answer !== '-';
const backgroundClass = isFilled ? (isValid ? 'bg-green-100' : 'bg-red-100') : 'bg-gray-100';
const answerColorClass = isFilled ? (isValid ? 'text-green-600' : 'text-red-600') : 'text-gray-600';
return `
<div class="question-row flex items-center mb-2 p-2 rounded ${backgroundClass}">
<div class="flex-none w-8 text-right mr-2">
<span class="font-bold text-gray-700">${i + 1}.</span>
</div>
<div class="flex-grow flex items-center overflow-hidden">
<span class="text-xs px-2 py-1 rounded mr-2 ${getTypeColor(q.type)}">${q.type}</span>
<span class="text-sm text-gray-600 truncate flex-grow" title="${q.text}">
${q.text.length > 10 ? q.text.substring(0, 10) + '...' : q.text}
</span>
<span class="font-semibold ml-2 ${answerColorClass}">${answer}</span>
</div>
</div>
`;
}).join('');
}
function getTypeColor(type) {
switch(type) {
case '单选题': return 'bg-blue-500 text-white';
case '多选题': return 'bg-purple-500 text-white';
case '判断题': return 'bg-yellow-500 text-white';
default: return 'bg-gray-500 text-white';
}
}
function showMessage(message, type = 'info', duration = 0) {
const statusElement = document.getElementById('status-message');
statusElement.textContent = message;
statusElement.className = `mb-4 text-sm font-medium p-3 rounded ${
type === 'error' ? 'bg-red-100 text-red-700' :
type === 'success' ? 'bg-green-100 text-green-700' :
'bg-blue-100 text-blue-700'
}`;
if (duration > 0) {
setTimeout(() => {
statusElement.textContent = '';
statusElement.className = 'mb-4 text-sm font-medium text-gray-600';
}, duration);
}
}
// 核心功能函数
function determineQuestionType(subject, options) {
if (options.length === 2 && options[0].type === 'radio' && options[1].type === 'radio') {
return '判断题';
} else if (options.length > 0 && options[0].type === 'radio') {
return '单选题';
} else if (options.length > 0 && options[0].type === 'checkbox') {
return '多选题';
}
return '未知类型';
}
function detectQuestions() {
const subjectElements = document.querySelectorAll(SELECTORS.subjectContainer);
questions = Array.from(subjectElements)
.map((subject, index) => {
const questionText = subject.querySelector(SELECTORS.questionText)?.textContent.trim();
const options = subject.querySelectorAll(SELECTORS.options);
if (!questionText || options.length === 0) {
return null;
}
let questionType = determineQuestionType(subject, options);
return {
type: questionType,
optionCount: options.length,
text: questionText,
index: index + 1
};
})
.filter(q => q !== null);
isQuestionDetected = questions.filter(q => q.type !== '未知类型').length > 0;
updateQuestionsPreview();
if (isQuestionDetected) {
showMessage(`检测到 ${questions.filter(q => q.type !== '未知类型').length} 道题目`, 'success');
} else {
showMessage('未检测到题目,请配置选择器或重新检测', 'error');
}
}
function parseAnswers(input) {
if (!input.trim()) {
return [];
}
input = input.replace(/\s/g, '').toUpperCase();
const patterns = [
{
regex: /(\d+)-(\d+)([A-Z]+)/,
process: (match, answers) => {
const [_, start, end, choices] = match;
for (let i = parseInt(start); i <= parseInt(end); i++) {
answers[i - 1] = choices[i - parseInt(start)] || '';
}
}
},
{
regex: /(\d+)([A-Z]+)/,
process: (match, answers) => {
const [_, number, choices] = match;
answers[parseInt(number) - 1] = choices;
}
},
{
regex: /([A-Z]+)/,
process: (match, answers) => {
answers.push(match[1]);
}
}
];
let answers = [];
const segments = input.split(',');
segments.forEach(segment => {
let matched = false;
for (const pattern of patterns) {
const match = segment.match(pattern.regex);
if (match) {
pattern.process(match, answers);
matched = true;
break;
}
}
if (!matched) {
showMessage(`无法解析的输入段: ${segment}`, 'error');
}
});
return answers;
}
function validateAnswer(answer, question) {
if (!answer || answer === '') return true;
const options = answer.split('');
if (question.type === '单选题' || question.type === '判断题') {
return options.length === 1 && options[0].charCodeAt(0) - 64 <= question.optionCount;
} else if (question.type === '多选题') {
return options.every(option => option.charCodeAt(0) - 64 <= question.optionCount);
}
return false;
}
async function fillAnswers() {
const bulkInput = document.getElementById('bulk-input');
const answers = parseAnswers(bulkInput.value);
let filledCount = 0;
for (let i = 0; i < questions.length; i++) {
if (i >= answers.length) break;
const question = questions[i];
const answer = answers[i];
if (answer && validateAnswer(answer, question)) {
const subject = document.querySelector(`${SELECTORS.subjectContainer}:nth-child(${question.index})`);
const options = subject.querySelectorAll(SELECTORS.options);
for (let optionIndex = 0; optionIndex < options.length; optionIndex++) {
const option = options[optionIndex];
const optionLetter = String.fromCharCode(65 + optionIndex);
const shouldBeChecked = answer.includes(optionLetter);
if (shouldBeChecked !== option.checked) {
option.click();
await sleep(GLOBAL.fillAnswerDelay);
filledCount++;
}
}
}
}
showMessage(`已成功填写 ${filledCount} 道题目的答案`, 'success');
}
function clearInputs() {
document.getElementById('bulk-input').value = '';
updateQuestionsPreview();
showMessage('输入已清空', 'info');
}
async function pasteAndRecognize() {
try {
const text = await navigator.clipboard.readText();
const bulkInput = document.getElementById('bulk-input');
bulkInput.value = text;
updateQuestionsPreview();
showMessage('已从剪贴板粘贴并识别答案', 'success');
} catch (err) {
showMessage('无法访问剪贴板,请手动粘贴', 'error');
}
}
function smartDetectAnswers() {
const subjectContainers = document.querySelectorAll(SELECTORS.subjectContainer);
console.log(`找到 ${subjectContainers.length} 个题目容器`);
const detectedAnswers = questions.filter(q => q.type !== '未知类型').map((question, index) => {
const subject = document.querySelector(`${SELECTORS.subjectContainer}:nth-child(${question.index})`);
if (!subject) return '';
const answerElement = subject.querySelector(SELECTORS.answerElement);
if (!answerElement) return '';
const answerText = answerElement.textContent.trim();
if (!answerText) return '';
return processAnswer(answerText, question.type);
});
const bulkInput = document.getElementById('bulk-input');
bulkInput.value = detectedAnswers.join(',');
updateQuestionsPreview();
showMessage('已智能识别当前答案', 'success');
}
function processAnswer(answerText, questionType) {
answerText = answerText.toUpperCase();
switch (questionType) {
case '单选题':
return answerText.match(/[A-Z]/)?.[0] || '';
case '多选题':
return answerText.match(/[A-Z]/g)?.join('') || '';
case '判断题':
if (answerText.includes('对') || answerText.includes('A') || answerText === 'T') {
return 'A';
} else if (answerText.includes('错') || answerText.includes('B') || answerText === 'F') {
return 'B';
}
return '';
default:
return answerText;
}
}
// 新的选择器配置工具
function showSelectorWizard() {
const wizard = document.createElement('div');
wizard.id = 'selector-wizard';
wizard.className = 'fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-6 rounded-lg shadow-xl z-50 w-96 max-w-[90%]';
wizard.innerHTML = `
<div class="wizard-header flex justify-between items-center mb-4">
<h3 class="text-2xl font-bold text-gray-800">DOM 选择器配置</h3>
<button id="close-wizard" class="text-gray-500 hover:text-gray-700">×</button>
</div>
<div class="wizard-body">
${createSelectorInput('subjectContainer', '题目容器选择器')}
${createSelectorInput('questionText', '问题文本选择器')}
${createSelectorInput('options', '选项选择器')}
${createSelectorInput('answerElement', '答案元素选择器')}
<div class="selector-input mt-4 flex items-center">
<input type="text" id="selector-input" class="w-2/3 px-3 py-2 border border-gray-300 rounded-md placeholder="输入CSS选择器" readonly>
<button id="test-selector" class="w-1/3 ml-2 bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded transition duration-300">测试</button>
</div>
<div id="selector-results" class="mt-2 text-sm text-gray-600"></div>
<div class="wizard-controls mt-4 flex justify-between gap-2">
<button id="pick-element" class="flex-grow bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded transition duration-300">选择元素</button>
<button id="reset-selectors" class="flex-grow bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded transition duration-300">重置</button>
<button id="save-selector" class="flex-grow bg-purple-500 hover:bg-purple-600 text-white font-bold py-2 px-4 rounded transition duration-300">保存</button>
</div>
</div>
`;
document.body.appendChild(wizard);
let highlightedElements = [];
let pickerActive = false;
let currentSelectorType = '';
function highlightElements(elements) {
highlightedElements.forEach(el => el.style.outline = '');
highlightedElements = Array.from(elements);
highlightedElements.forEach(el => {
el.style.outline = '2px solid red';
});
}
function clearHighlights() {
highlightedElements.forEach(el => el.style.outline = '');
highlightedElements = [];
}
function testSelector() {
const selector = document.getElementById('selector-input').value;
const elements = document.querySelectorAll(selector);
const results = document.getElementById('selector-results');
results.textContent = `找到 ${elements.length} 个元素`;
highlightElements(elements);
}
function pickElement(e) {
if (!pickerActive) return;
e.preventDefault();
e.stopPropagation();
const path = e.composedPath();
const selector = uniqueSelector(path[0]);
document.getElementById('selector-input').value = selector;
if (currentSelectorType) {
document.getElementById(currentSelectorType).value = selector;
}
testSelector();
pickerActive = false;
}
function uniqueSelector(el) {
if (el.id) return '#' + el.id;
if (el.className) return '.' + el.className.split(' ').join('.');
let selector = el.tagName.toLowerCase();
let siblings = Array.from(el.parentNode.children);
if (siblings.length > 1) {
let index = siblings.indexOf(el) + 1;
selector += `:nth-child(${index})`;
}
return el.parentNode ? uniqueSelector(el.parentNode) + ' > ' + selector : selector;
}
document.getElementById('test-selector').addEventListener('click', testSelector);
document.getElementById('pick-element').addEventListener('click', () => {
pickerActive = true;
showMessage('请点击页面元素以选择', 'info');
});
document.addEventListener('click', pickElement, true);
document.getElementById('save-selector').addEventListener('click', () => {
SELECTORS = {
subjectContainer: document.getElementById('subjectContainer').value,
questionText: document.getElementById('questionText').value,
options: document.getElementById('options').value,
answerElement: document.getElementById('answerElement').value
};
GM_setValue('customSelectors', JSON.stringify(SELECTORS));
clearHighlights();
wizard.remove();
showMessage('选择器配置已保存,正在重新检测题目', 'success');
detectQuestions();
});
document.getElementById('close-wizard').addEventListener('click', () => {
clearHighlights();
wizard.remove();
});
document.getElementById('reset-selectors').addEventListener('click', () => {
SELECTORS = {...DEFAULT_SELECTORS};
['subjectContainer', 'questionText', 'options', 'answerElement'].forEach(id => {
document.getElementById(id).value = DEFAULT_SELECTORS[id];
});
document.getElementById('selector-input').value = '';
clearHighlights();
showMessage('选择器已重置为默认值', 'info');
});
// 为每个选择器输入框添加事件监听器
['subjectContainer', 'questionText', 'options', 'answerElement'].forEach(id => {
document.getElementById(id).addEventListener('focus', () => {
currentSelectorType = id;
document.getElementById('selector-input').value = document.getElementById(id).value;
});
});
}
function createSelectorInput(id, label) {
return `
<div class="mb-4">
<label for="${id}" class="block text-sm font-medium text-gray-700 mb-1">${label}</label>
<input id="${id}" type="text" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-transparent" value="${SELECTORS[id]}">
</div>
`;
}
// 初始化函数
function init() {
// 加载 Tailwind CSS
const tailwindCSS = `https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css`;
const link = document.createElement('link');
link.href = tailwindCSS;
link.rel = 'stylesheet';
document.head.appendChild(link);
// 加载自定义选择器
const savedSelectors = GM_getValue('customSelectors');
if (savedSelectors) {
SELECTORS = JSON.parse(savedSelectors);
}
// 创建主界面
createMainInterface();
// 延迟执行检测题目,确保页面完全加载
setTimeout(detectQuestions, 2000);
}
// 当 DOM 加载完成时初始化脚本
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();