// ==UserScript==
// @name 【自制】问卷星输入答案自动填写
// @namespace http://tampermonkey.net/
// @version 1.1.0
// @description 使用可配置的选择器来适配不同网站,支持复杂的输入格式。根据网站自动选择合适的选择器。
// @match https://lms.ouchn.cn/exam/*
// @match https://ks.wjx.top/vm/mBcE5Ax.aspx
// @match https://www.wjx.cn/vm/eU7tjdY.aspx
// @match https://www.szxuexiao.com/zuoti/html/33.html
// @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 = {
"lms.ouchn.cn": {
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 = JSON.parse(
GM_getValue("domainSelectors", JSON.stringify(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));
}
function getCurrentDomain() {
return window.location.hostname;
}
function getSelectorsForCurrentDomain() {
const currentDomain = getCurrentDomain();
return SELECTORS[currentDomain] || null;
}
// 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
.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-200 text-blue-800";
case "多选题":
return "bg-green-200 text-green-800";
case "判断题":
return "bg-yellow-200 text-yellow-800";
default:
return "bg-gray-200 text-gray-800";
}
}
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) {
// 检查选项的类型
const optionTypes = Array.from(options).map((option) => {
const input =
option.tagName.toLowerCase() === "input"
? option
: option.querySelector("input");
return input ? input.type : null;
});
// 根据选项类型确定题目类型
if (optionTypes.every((type) => type === "radio")) {
return optionTypes.length === 2 ? "判断题" : "单选题";
} else if (optionTypes.every((type) => type === "checkbox")) {
return "多选题";
}
// 如果无法通过input类型确定,尝试通过其他特征判断
const optionTexts = Array.from(options).map((option) =>
option.textContent.trim().toLowerCase()
);
if (optionTexts.includes("正确") && optionTexts.includes("错误")) {
return "判断题";
}
// 如果仍然无法确定,返回未知类型
return "未知类型";
}
function detectQuestions() {
const currentSelectors = getSelectorsForCurrentDomain();
if (!currentSelectors) {
showSelectorWizard();
return;
}
const subjectElements = document.querySelectorAll(
currentSelectors.subjectContainer
);
if (subjectElements.length === 0) {
const questionTexts = Array.from(
document.querySelectorAll(currentSelectors.questionText)
);
const optionGroups = groupOptions(
document.querySelectorAll(currentSelectors.options)
);
questions = questionTexts.map((text, index) => {
const questionText = questionTexts[index].textContent.trim();
const options = optionGroups[index];
if (!text || !options) {
return null;
}
const questionType = determineQuestionType(text, options);
return {
type: questionType,
optionCount: options.length,
text: questionText,
index: index + 1,
};
});
console.log(questions);
} else {
questions = Array.from(subjectElements)
.map((subject, index) => {
const questionText = subject
.querySelector(currentSelectors.questionText)
?.textContent.trim();
const options = subject.querySelectorAll(currentSelectors.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.length > 0;
updateQuestionsPreview();
if (isQuestionDetected) {
showMessage(`检测到 ${questions.length} 道题目`, "success");
} else {
showMessage("未检测到题目,请配置选择器或重新检测", "error");
}
}
function groupOptions(options) {
const groups = {};
options.forEach((option) => {
if (!groups[option.name]) {
groups[option.name] = [];
}
groups[option.name].push(option);
});
return Object.values(groups);
}
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 true;
}
async function fillAnswers() {
const currentSelectors = getSelectorsForCurrentDomain();
if (!currentSelectors) {
showMessage("未找到当前网站的选择器配置,请先配置选择器", "error");
return;
}
const bulkInput = document.getElementById("bulk-input");
const answers = parseAnswers(bulkInput.value);
let filledCount = 0;
const subjectContainers = document.querySelectorAll(currentSelectors.subjectContainer);
const useSubjectLogic = subjectContainers.length > 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)) {
if (useSubjectLogic) {
const subject = subjectContainers[question.index - 1];
if (subject) {
filledCount += await fillAnswerForSubject(subject, answer, currentSelectors);
}
} else {
filledCount += await fillAnswerForOptionGroup(i, answer, currentSelectors);
}
}
}
showMessage(`已填写 ${filledCount} 个答案`, "success");
}
async function fillAnswerForSubject(subject, answer, currentSelectors) {
let filledCount = 0;
const options = subject.querySelectorAll(currentSelectors.options);
for (let optionIndex = 0; optionIndex < options.length; optionIndex++) {
const option = options[optionIndex];
const optionLetter = String.fromCharCode(65 + optionIndex);
const shouldBeChecked = answer.includes(optionLetter);
const input = option.tagName.toLowerCase() === "input" ? option : option.querySelector("input");
if (input && shouldBeChecked !== input.checked) {
const label = option.closest("span") || option;
label.click();
await sleep(GLOBAL.fillAnswerDelay);
filledCount++;
}
}
return filledCount;
}
async function fillAnswerForOptionGroup(questionIndex, answer, currentSelectors) {
let filledCount = 0;
const options = groupOptions(document.querySelectorAll(currentSelectors.options))[questionIndex];
if (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++;
}
}
}
return filledCount;
}
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 currentSelectors = getSelectorsForCurrentDomain();
if (!currentSelectors) {
showMessage("未找到当前网站的选择器配置,请先配置选择器", "error");
return;
}
const subjectContainers = document.querySelectorAll(
currentSelectors.subjectContainer
);
const detectedAnswers = questions.map((question, index) => {
const subject = document.querySelector(
`${currentSelectors.subjectContainer}:nth-child(${question.index})`
);
if (!subject) return "";
const answerElement = subject.querySelector(
currentSelectors.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 currentDomain = getCurrentDomain();
const currentSelectors = SELECTORS[currentDomain] || {};
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">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700 mb-1">当前网站</label>
<input type="text" id="current-domain" class="w-full px-3 py-2 border border-gray-300 rounded-md" value="${currentDomain}" readonly>
</div>
${createSelectorInput(
"subjectContainer",
"题目容器选择器",
currentSelectors.subjectContainer
)}
${createSelectorInput(
"questionText",
"问题文本选择器",
currentSelectors.questionText
)}
${createSelectorInput(
"options",
"选项选择器",
currentSelectors.options
)}
${createSelectorInput(
"answerElement",
"答案元素选择器",
currentSelectors.answerElement
)}
<div class="wizard-controls mt-4 flex justify-between gap-2">
<button id="test-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);
document
.getElementById("test-selectors")
.addEventListener("click", testSelectors);
document
.getElementById("save-selector")
.addEventListener("click", saveSelectors);
document
.getElementById("close-wizard")
.addEventListener("click", () => wizard.remove());
}
function testSelectors() {
const testResults = {};
["subjectContainer", "questionText", "options", "answerElement"].forEach(
(selectorType) => {
const selector = document.getElementById(selectorType).value;
const elements = document.querySelectorAll(selector);
testResults[selectorType] = elements.length;
}
);
let message = "测试结果:\n";
for (const [type, count] of Object.entries(testResults)) {
message += `${type}: 找到 ${count} 个元素\n`;
}
alert(message);
}
function saveSelectors() {
const currentDomain = getCurrentDomain();
SELECTORS[currentDomain] = {
subjectContainer: document.getElementById("subjectContainer").value,
questionText: document.getElementById("questionText").value,
options: document.getElementById("options").value,
answerElement: document.getElementById("answerElement").value,
};
GM_setValue("domainSelectors", JSON.stringify(SELECTORS));
document.getElementById("selector-wizard").remove();
showMessage("选择器配置已保存,正在重新检测题目", "success");
detectQuestions();
}
function createSelectorInput(id, label, value = "") {
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="${value}">
</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);
// 创建主界面
createMainInterface();
// 延迟执行检测题目,确保页面完全加载
setTimeout(detectQuestions, 2000);
}
// 当 DOM 加载完成时初始化脚本
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();