// ==UserScript==
// @name 智能考试助手(拾取选择器版·已修复)
// @namespace http://tampermonkey.net/
// @version 1.4
// @description 支持悬停题目、点击题目、划词选中、拾取选择器(无需控制台)、自定义选择器即时生效
// @author You
// @icon https://tse2-mm.cn.bing.net/th/id/OIP-C.s8WZvq7biii2N7NkGdXGTwAAAA?rs=1
// @match *://*/*
// @grant GM_addStyle
// @grant GM_setClipboard
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// ============= 题库数据(请替换为你的完整题库) =============
const QUESTION_DATABASE = [
// 示例数据(实际使用时请替换为你的所有题目)
{
"question": "1、()是制定《安全生产法》的根本出发点和落脚点。",
"options": [],
"answer": "重视和保护人的生命权",
"type": "choice"
},
{
"question": "2、安装在进风流中的局部通风机距回风口不得小于()。",
"options": [],
"answer": "10m",
"type": "choice"
}
// ... 请在此处粘贴你的完整题库数据
];
// ============= 脚本配置 =============
const DEBUG = true;
let lastClickedQuestion = null;
let lastHoveredQuestion = null;
let customQuestionSelector = localStorage.getItem('customQuestionSelector') || '';
let pickMode = false; // 拾取模式标志
let originalCursor = ''; // 保存原始光标
let highlightElement = null; // 高亮元素
function log(...args) {
if (DEBUG) console.log('[智能考试助手]', ...args);
}
log('脚本已加载,题库大小:', QUESTION_DATABASE.length);
if (customQuestionSelector) log('使用自定义选择器:', customQuestionSelector);
// ============= 工具函数 =============
// 从文本中提取纯净的题干(去掉选项部分)
function extractPureQuestion(text) {
if (!text) return '';
// 1. 去掉开头的题型标签(如【单选题】、【多选题】等)
let cleaned = text.replace(/^【[^】]+】\s*/, '');
// 2. 查找第一个选项字母的位置(如 A. A、等)
const optionPattern = /\s+[A-D][\.、]/;
const match = cleaned.match(optionPattern);
if (match) {
const index = match.index;
if (index > 0) {
cleaned = cleaned.substring(0, index).trim();
}
}
// 3. 如果还有换行,取第一行(作为题干)
const lines = cleaned.split('\n');
if (lines.length > 1 && lines[0].trim().length > 5) {
return lines[0].trim();
}
return cleaned;
}
// 从元素中智能提取题干(优先查找常见子元素,否则分析文本)
function getQuestionTextFromElement(elem) {
if (!elem) return '';
// 优先查找可能的题干子元素(类名包含 title, stem, question 等)
const titleSelectors = [
'.exam-topic-item-title-name',
'.question-title',
'.topic-title',
'.question-stem',
'.exam-question-title',
'[class*="title"]',
'[class*="stem"]',
'[class*="question"]'
];
for (const sel of titleSelectors) {
const sub = elem.querySelector(sel);
if (sub && sub.textContent.trim()) {
let text = sub.textContent.trim();
text = extractPureQuestion(text);
if (text) return text;
}
}
// 如果没有找到子元素,获取整个元素文本并提取
const fullText = elem.textContent.trim();
if (fullText) {
// 先尝试直接提取纯净题干
let cleaned = extractPureQuestion(fullText);
if (cleaned) return cleaned;
// 如果文本较长(包含很多内容),尝试取第一段
if (fullText.length > 200) {
const lines = fullText.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (/^\d+[\.、]/.test(line) && line.length > 5 && line.length < 200) {
cleaned = extractPureQuestion(line);
if (cleaned) return cleaned;
}
}
if (lines[0] && lines[0].trim().length > 5) {
cleaned = extractPureQuestion(lines[0]);
if (cleaned) return cleaned;
}
}
return cleaned || fullText;
}
return '';
}
// 生成元素的唯一CSS选择器(优先id,其次class组合,最后标签+索引)
function generateSelector(element) {
if (!element) return '';
// 优先使用 id
if (element.id) {
return `#${element.id}`;
}
// 尝试获取元素的主要 class(优先取第一个)
const getMainClass = (el) => {
if (!el.className || typeof el.className !== 'string') return '';
const classes = el.className.trim().split(/\s+/);
// 过滤掉常见的表示位置、大小、颜色等无关类(可自定义)
const blacklist = ['active', 'selected', 'hover', 'focus', 'clearfix', 'left', 'right', 'content', 'info'];
const valid = classes.filter(c => !blacklist.includes(c));
return valid.length ? valid[0] : classes[0];
};
// 向上查找可能包含题目特征的父容器(最大深度5)
let path = [];
let current = element;
let depth = 0;
while (current && current !== document.body && depth < 5) {
const tag = current.tagName.toLowerCase();
let selector = tag;
const mainClass = getMainClass(current);
if (mainClass) {
selector += `.${mainClass}`;
} else if (current.id) {
selector = `#${current.id}`;
}
// 如果该选择器在当前上下文唯一,则停止继续向上(避免过度组合)
if (depth === 0) {
// 当前元素本身
if (selector !== tag) {
// 有 class 或 id,检查唯一性
if (document.querySelectorAll(selector).length === 1) {
return selector;
}
}
}
// 组合进路径
path.unshift(selector);
current = current.parentElement;
depth++;
}
// 如果路径为空,返回原始标签名(最坏情况)
if (path.length === 0) return element.tagName.toLowerCase();
// 组合最终选择器,去掉可能出现的 :nth-of-type(我们避免使用)
let finalSelector = path.join(' > ');
// 移除可能存在的 :nth-of-type(我们不用)
finalSelector = finalSelector.replace(/:nth-of-type\(\d+\)/g, '');
// 检查是否唯一,如果不唯一,提示用户手动修改
if (document.querySelectorAll(finalSelector).length > 1) {
// 不唯一,尝试简化(取最后一段)
const lastPart = path[path.length - 1];
if (document.querySelectorAll(lastPart).length === 1) {
return lastPart;
}
// 否则返回带提示
return finalSelector + ' (可能不唯一,建议手动修改)';
}
return finalSelector;
}
// 退出拾取模式
function exitPickMode() {
if (!pickMode) return;
pickMode = false;
document.body.style.cursor = originalCursor;
if (highlightElement) {
highlightElement.style.outline = '';
highlightElement = null;
}
// 移除临时事件监听
document.removeEventListener('mouseover', pickMouseOver);
document.removeEventListener('click', pickClick);
document.removeEventListener('keydown', pickKeydown);
log('退出拾取模式');
}
// 拾取模式鼠标悬停高亮
function pickMouseOver(e) {
const target = e.target;
if (highlightElement === target) return;
if (highlightElement) {
highlightElement.style.outline = '';
}
highlightElement = target;
target.style.outline = '2px solid #ff6b6b';
}
// 拾取模式点击
function pickClick(e) {
e.preventDefault();
e.stopPropagation();
if (e.target === pickBtn || pickBtn.contains(e.target)) return;
const target = e.target;
let selector = generateSelector(target);
// 获取文本预览
let preview = target.textContent.trim().substring(0, 50);
if (preview) preview = '文本预览: ' + preview;
// 如果选择器可能不唯一,提示用户手动修改
let message = `已捕获元素选择器:\n${selector}\n${preview}\n是否应用为题目选择器?`;
if (selector.includes('(可能不唯一)')) {
message = `注意:该选择器可能不唯一!\n${selector}\n${preview}\n建议手动修改,是否继续应用?\n\n可以点击“取消”然后手动输入。`;
}
const userConfirmed = confirm(message);
if (userConfirmed) {
customQuestionSelector = selector.replace(/\s*\(可能不唯一.*\)/, ''); // 去掉提示文字
localStorage.setItem('customQuestionSelector', customQuestionSelector);
alert(`选择器已保存并立即生效!\n${customQuestionSelector}`);
log('自定义选择器更新为:', customQuestionSelector);
lastHoveredQuestion = null;
lastClickedQuestion = null;
} else {
// 允许用户手动输入
const manualSelector = prompt('请输入CSS选择器(例如 .exam-topic-item):', selector.replace(/\s*\(可能不唯一.*\)/, ''));
if (manualSelector && manualSelector.trim()) {
customQuestionSelector = manualSelector.trim();
localStorage.setItem('customQuestionSelector', customQuestionSelector);
alert(`选择器已保存!\n${customQuestionSelector}`);
lastHoveredQuestion = null;
lastClickedQuestion = null;
}
}
exitPickMode();
}
// 拾取模式下按ESC退出
function pickKeydown(e) {
if (e.key === 'Escape') {
exitPickMode();
}
}
// 进入拾取模式
function enterPickMode() {
if (pickMode) return;
pickMode = true;
originalCursor = document.body.style.cursor;
document.body.style.cursor = 'crosshair';
document.addEventListener('mouseover', pickMouseOver);
document.addEventListener('click', pickClick);
document.addEventListener('keydown', pickKeydown);
log('进入拾取模式,点击任意元素生成选择器');
}
// ============= 题库搜索引擎 =============
function normalizeText(text) {
let normalized = text.replace(/(/g, '(').replace(/)/g, ')');
normalized = normalized.replace(/\s+/g, '')
.replace(/[,,.。??!!::;;(())\[\]【】]/g, '')
.toLowerCase();
return normalized;
}
function calculateSimilarity(str1, str2) {
const set1 = new Set(str1);
const set2 = new Set(str2);
let intersection = 0;
for (const char of set1) {
if (set2.has(char)) intersection++;
}
const union = set1.size + set2.size - intersection;
return intersection / union;
}
function findQuestionInDatabase(query) {
if (!query) return null;
const cleanQuery = normalizeText(query);
log('搜索查询:', cleanQuery);
for (const item of QUESTION_DATABASE) {
const cleanQuestion = normalizeText(item.question);
if (cleanQuestion === cleanQuery) return item;
}
for (const item of QUESTION_DATABASE) {
const cleanQuestion = normalizeText(item.question);
if (cleanQuestion.includes(cleanQuery) || cleanQuery.includes(cleanQuestion)) return item;
}
let bestMatch = null;
let highestSimilarity = 0.7;
for (const item of QUESTION_DATABASE) {
const cleanQuestion = normalizeText(item.question);
const similarity = calculateSimilarity(cleanQuery, cleanQuestion);
if (similarity > highestSimilarity) {
highestSimilarity = similarity;
bestMatch = item;
}
}
if (bestMatch) log('相似匹配,相似度:', highestSimilarity);
return bestMatch;
}
// ============= 样式设置 =============
GM_addStyle(`
#search-panel {
position: fixed;
background: #ffffff;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
padding: 10px;
z-index: 999999;
display: none;
font-size: 13px;
color: #333;
width: 300px;
max-height: 400px;
overflow-y: auto;
transition: opacity 0.3s ease;
cursor: default;
opacity: 0.95;
top: 100px;
left: 100px;
}
#search-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
cursor: move;
user-select: none;
}
#header-controls {
display: flex;
align-items: center;
gap: 10px;
}
#pin-button {
cursor: pointer;
color: #999;
font-size: 16px;
padding: 0 5px;
transition: all 0.3s ease;
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
}
#pin-button:hover {
background-color: #f0f0f0;
color: #666;
}
#pin-button.pinned {
color: #4CAF50;
background-color: #e8f5e9;
}
#opacity-control {
display: flex;
align-items: center;
margin-bottom: 8px;
padding: 5px;
border-bottom: 1px solid #eee;
font-size: 12px;
}
#opacity-slider {
flex: 1;
margin: 0 10px;
cursor: pointer;
}
#search-title {
font-weight: bold;
font-size: 14px;
}
#search-close {
cursor: pointer;
color: #999;
font-size: 16px;
}
#search-close:hover {
color: #555;
}
#selected-text {
font-weight: bold;
margin-bottom: 8px;
white-space: normal;
word-break: break-word;
background: #f9f9f9;
padding: 5px;
border-radius: 3px;
}
.action-buttons {
display: flex;
margin-bottom: 8px;
flex-wrap: wrap;
}
.action-button {
cursor: pointer;
padding: 3px 8px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 3px;
margin-right: 8px;
margin-bottom: 5px;
font-size: 12px;
}
.action-button:hover {
background: #e9e9e9;
}
#search-result {
border-top: 1px dashed #eee;
padding-top: 8px;
max-height: 250px;
overflow-y: auto;
font-size: 13px;
line-height: 1.5;
}
.result-item {
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed #eee;
}
.loading {
text-align: center;
color: #888;
padding: 10px 0;
}
.no-result {
color: #888;
text-align: center;
padding: 15px 0;
}
.options-list {
margin-top: 5px;
padding-left: 20px;
}
.option {
margin-bottom: 3px;
}
.answer {
font-weight: bold;
color: #2c7a4d;
margin-top: 5px;
}
.hint {
color: #888;
font-size: 12px;
margin: 5px 0;
font-style: italic;
}
.result-actions {
margin-top: 10px;
font-size: 12px;
color: #666;
}
.result-action {
cursor: pointer;
color: #1a75ff;
margin-right: 10px;
}
.result-action:hover {
text-decoration: underline;
}
#cached-answers {
font-size: 12px;
color: #888;
margin-top: 5px;
}
#db-stats {
font-size: 11px;
color: #888;
margin-top: 5px;
}
#activate-button, #settings-button, #pick-button {
position: fixed;
bottom: 20px;
background: #4CAF50;
color: white;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
z-index: 999998;
font-size: 14px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: 0.2s;
}
#activate-button {
right: 20px;
}
#settings-button {
right: 100px;
background: #2196F3;
}
#pick-button {
right: 180px;
background: #ff9800;
}
#activate-button:hover, #settings-button:hover, #pick-button:hover {
opacity: 0.9;
}
`);
// ============= 界面元素创建 =============
const activateButton = document.createElement('div');
activateButton.id = 'activate-button';
activateButton.textContent = '激活搜题助手';
document.body.appendChild(activateButton);
const settingsBtn = document.createElement('div');
settingsBtn.id = 'settings-button';
settingsBtn.textContent = '⚙️ 手动设置';
document.body.appendChild(settingsBtn);
const pickBtn = document.createElement('div');
pickBtn.id = 'pick-button';
pickBtn.textContent = '🔍 拾取选择器';
document.body.appendChild(pickBtn);
const searchPanel = document.createElement('div');
searchPanel.id = 'search-panel';
searchPanel.innerHTML = `
透明度:
95%
搜索答案
复制题目
自动填答案
题库: ${QUESTION_DATABASE.length}题
`;
document.body.appendChild(searchPanel);
// ============= 面板状态 =============
let isPinned = false;
let panelVisible = false;
let lastPanelPosition = { left: 100, top: 100 };
const answerCache = {};
function closePanel() {
searchPanel.style.display = 'none';
activateButton.style.display = 'block';
panelVisible = false;
isPinned = false;
pinButton.classList.remove('pinned');
}
function showPanelWithContent(text) {
if (!panelVisible) {
searchPanel.style.left = `${lastPanelPosition.left}px`;
searchPanel.style.top = `${lastPanelPosition.top}px`;
searchPanel.style.display = 'block';
panelVisible = true;
activateButton.style.display = 'none';
}
document.getElementById('selected-text').textContent = text;
if (text && text !== "请选中题目文本") {
setTimeout(searchAnswer, 300);
}
}
// ============= 提取题目(优先使用悬停/点击记录) =============
function extractCurrentQuestion() {
try {
if (lastHoveredQuestion) {
log('使用悬停记录:', lastHoveredQuestion);
return lastHoveredQuestion;
}
if (lastClickedQuestion) {
log('使用点击记录:', lastClickedQuestion);
return lastClickedQuestion;
}
if (customQuestionSelector) {
const elem = document.querySelector(customQuestionSelector);
if (elem) {
const text = getQuestionTextFromElement(elem);
if (text) {
log('使用自定义选择器获取题目:', text);
return text;
}
}
}
const titleElement = document.querySelector('.exam-topic-item-title-name');
if (titleElement && titleElement.textContent) {
const text = titleElement.textContent.trim();
if (text) return text;
}
return null;
} catch (e) {
log('提取题目失败', e);
return null;
}
}
function searchAnswer() {
const query = document.getElementById('selected-text').textContent.trim();
if (!query) return;
document.getElementById('search-result').innerHTML = '正在搜索答案...
';
if (answerCache[query]) {
displayResults(answerCache[query]);
return;
}
setTimeout(() => {
const result = findQuestionInDatabase(query);
if (result) {
answerCache[query] = result;
displayResults(result);
} else {
document.getElementById('search-result').innerHTML = '未找到答案,请尝试修改关键词
';
}
updateCacheStats();
}, 300);
}
function displayResults(result) {
const container = document.getElementById('search-result');
if (!result) return;
let html = '';
html += `
题目: ${escapeHtml(result.question)}
`;
if (result.options && result.options.length) {
html += '
';
result.options.forEach(opt => {
const isAnswer = opt.includes(result.answer) || opt.startsWith(result.answer + '.');
html += `
${escapeHtml(opt)}
`;
});
html += '
';
}
html += `
答案: ${escapeHtml(result.answer)}
`;
if (result.explanation) {
html += `
解析: ${escapeHtml(result.explanation)}
`;
}
html += `
复制答案
复制全部
`;
container.innerHTML = html;
container.querySelector('.copy-answer')?.addEventListener('click', () => {
GM_setClipboard(result.answer);
alert('答案已复制');
});
container.querySelector('.copy-all')?.addEventListener('click', () => {
let text = `题目: ${result.question}\n`;
if (result.options?.length) text += result.options.join('\n') + '\n';
text += `答案: ${result.answer}`;
if (result.explanation) text += `\n解析: ${result.explanation}`;
GM_setClipboard(text);
alert('已复制题目和答案');
});
}
function escapeHtml(str) {
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&';
if (m === '<') return '<';
if (m === '>') return '>';
return m;
});
}
// 自动填答案
function autoFillAnswer(answer) {
if (!answer) return false;
log('自动填写答案:', answer);
let cleanAnswer = answer.trim().replace(/^【正确答案】/, '').replace(/^答案[::]/, '').trim();
const trueValues = ['正确', '对', '是', '√', 'A', 'A.'];
const falseValues = ['错误', '错', '否', '×', 'B', 'B.'];
const radios = document.querySelectorAll('input[type="radio"]');
const checkboxes = document.querySelectorAll('input[type="checkbox"]');
if (radios.length > 0) {
if (trueValues.some(v => cleanAnswer.includes(v)) && falseValues.some(v => cleanAnswer.includes(v))) {
for (let radio of radios) {
const label = getLabelForInput(radio);
if (label && label.textContent.includes(cleanAnswer)) {
radio.click();
return true;
}
}
} else {
for (let radio of radios) {
const label = getLabelForInput(radio);
if (!label) continue;
const labelText = label.textContent.trim();
if (labelText.startsWith(cleanAnswer + '.') || labelText === cleanAnswer ||
(cleanAnswer.length === 1 && 'ABCD'.includes(cleanAnswer.toUpperCase()) &&
labelText.toUpperCase().startsWith(cleanAnswer.toUpperCase()))) {
radio.click();
return true;
}
if (labelText.includes(cleanAnswer)) {
radio.click();
return true;
}
}
}
}
if (checkboxes.length > 0) {
const parts = cleanAnswer.split(/[,,]/).map(p => p.trim());
for (let part of parts) {
for (let checkbox of checkboxes) {
const label = getLabelForInput(checkbox);
if (!label) continue;
const labelText = label.textContent.trim();
if (labelText.startsWith(part + '.') || labelText === part ||
(part.length === 1 && 'ABCD'.includes(part.toUpperCase()) &&
labelText.toUpperCase().startsWith(part.toUpperCase()))) {
checkbox.click();
} else if (labelText.includes(part)) {
checkbox.click();
}
}
}
return true;
}
const textInputs = document.querySelectorAll('input[type="text"], textarea');
if (textInputs.length) {
textInputs[0].value = cleanAnswer;
textInputs[0].dispatchEvent(new Event('input', { bubbles: true }));
return true;
}
return false;
}
function getLabelForInput(input) {
if (input.labels && input.labels.length) return input.labels[0];
const id = input.id;
if (id) {
const label = document.querySelector(`label[for="${id}"]`);
if (label) return label;
}
let parent = input.parentElement;
while (parent) {
if (parent.tagName === 'LABEL') return parent;
parent = parent.parentElement;
}
return null;
}
function getCurrentAnswer() {
const answerDiv = document.querySelector('#search-result .answer');
if (answerDiv) {
let text = answerDiv.textContent;
const match = text.match(/答案[::]\s*(.+)/);
return match ? match[1].trim() : text.trim();
}
return null;
}
function updateCacheStats() {
const count = Object.keys(answerCache).length;
const elem = document.getElementById('cached-answers');
if (count > 0) elem.textContent = `已缓存 ${count} 个答案`;
else elem.textContent = '';
}
// ============= 事件监听 =============
const pinButton = document.getElementById('pin-button');
pinButton.addEventListener('click', () => {
isPinned = !isPinned;
pinButton.classList.toggle('pinned', isPinned);
pinButton.title = isPinned ? '取消置顶' : '置顶';
});
document.getElementById('search-close').addEventListener('click', closePanel);
document.addEventListener('mousedown', (e) => {
if (!isPinned && panelVisible &&
!searchPanel.contains(e.target) && !activateButton.contains(e.target) &&
!settingsBtn.contains(e.target) && !pickBtn.contains(e.target)) {
closePanel();
}
});
// 快捷键
document.addEventListener('keydown', (e) => {
if (!isPinned && e.key === 'Escape' && panelVisible) {
closePanel();
e.preventDefault();
}
if (e.key === 'q' && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
const activeEl = document.activeElement;
const isInputField = activeEl &&
(activeEl.tagName === 'INPUT' || activeEl.tagName === 'TEXTAREA' ||
activeEl.isContentEditable ||
activeEl.getAttribute('contenteditable') === 'true');
if (!isInputField) {
const question = extractCurrentQuestion();
if (question) {
showPanelWithContent(question);
} else {
showPanelWithContent("请选中题目文本");
}
e.preventDefault();
}
}
if (e.altKey && e.key === 'a' && panelVisible) {
document.getElementById('search-btn').click();
e.preventDefault();
}
});
activateButton.addEventListener('click', () => {
const question = extractCurrentQuestion();
if (question) {
showPanelWithContent(question);
} else {
showPanelWithContent("请选中题目文本");
}
});
settingsBtn.addEventListener('click', () => {
const newSelector = prompt(
'请输入题目文本的CSS选择器(例如 .question-title, .exam-topic-item-title-name):\n' +
'留空则使用自动检测。\n\n当前选择器:' + (customQuestionSelector || '未设置'),
customQuestionSelector
);
if (newSelector !== null) {
customQuestionSelector = newSelector.trim();
localStorage.setItem('customQuestionSelector', customQuestionSelector);
alert('选择器已保存并立即生效!无需刷新页面。');
lastHoveredQuestion = null;
lastClickedQuestion = null;
}
});
// 拾取按钮(修复:不会捕获按钮自身)
pickBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (pickMode) {
exitPickMode();
} else {
enterPickMode();
}
});
document.getElementById('copy-btn').addEventListener('click', () => {
const text = document.getElementById('selected-text').textContent;
if (text) {
GM_setClipboard(text);
const btn = document.getElementById('copy-btn');
const orig = btn.textContent;
btn.textContent = '已复制!';
setTimeout(() => btn.textContent = orig, 1500);
}
});
document.getElementById('search-btn').addEventListener('click', searchAnswer);
document.getElementById('auto-answer').addEventListener('click', () => {
const answer = getCurrentAnswer();
if (answer) {
autoFillAnswer(answer);
} else {
alert('请先搜索答案');
}
});
// ============= 划词选中自动搜索 =============
let selectionTimer = null;
document.addEventListener('mouseup', (e) => {
if (searchPanel.contains(e.target) || activateButton.contains(e.target) ||
settingsBtn.contains(e.target) || pickBtn.contains(e.target)) return;
if (selectionTimer) clearTimeout(selectionTimer);
selectionTimer = setTimeout(() => {
const selection = window.getSelection();
const selectedText = selection.toString().trim();
if (selectedText && selectedText.length >= 3) {
let rect = null;
if (selection.rangeCount > 0) {
rect = selection.getRangeAt(0).getBoundingClientRect();
}
const pos = rect ? {
left: rect.left + window.scrollX,
top: rect.bottom + window.scrollY + 5
} : {
left: window.innerWidth / 2 - 150,
top: window.innerHeight / 3
};
if (!panelVisible) {
searchPanel.style.left = `${pos.left}px`;
searchPanel.style.top = `${pos.top}px`;
}
showPanelWithContent(selectedText);
}
}, 50);
});
// ============= 鼠标悬停捕获题目(精确提取题干) =============
document.addEventListener('mouseenter', (e) => {
if (pickMode) return; // 拾取模式下不干扰
const targetElem = e.target.nodeType === Node.ELEMENT_NODE ? e.target : e.target.parentElement;
if (!targetElem) return;
if (customQuestionSelector) {
let parent = targetElem;
let depth = 0;
while (parent && parent !== document.body && depth < 10) {
if (parent.matches && parent.matches(customQuestionSelector)) {
const question = getQuestionTextFromElement(parent);
if (question) {
lastHoveredQuestion = question;
log('悬停捕获题目(自定义选择器):', lastHoveredQuestion);
return;
}
}
parent = parent.parentElement;
depth++;
}
}
const topicItem = targetElem.closest('.exam-topic-item');
if (topicItem) {
const titleElem = topicItem.querySelector('.exam-topic-item-title-name');
if (titleElem && titleElem.textContent) {
lastHoveredQuestion = titleElem.textContent.trim();
log('悬停捕获题目(类名):', lastHoveredQuestion);
return;
}
}
let target = targetElem;
let depth = 0;
while (target && target !== document.body && depth < 10) {
if (target.nodeType === Node.ELEMENT_NODE) {
const text = target.textContent ? target.textContent.trim() : '';
if (/^\d+[\.、]/.test(text) && text.length > 10 && text.length < 500 && !target.querySelector('input[type="radio"], input[type="checkbox"]')) {
const pure = extractPureQuestion(text);
if (pure) {
lastHoveredQuestion = pure;
log('悬停捕获题目(后备):', lastHoveredQuestion);
return;
}
}
}
target = target.parentElement;
depth++;
}
}, true);
// 点击捕获题目(同样精确提取)
document.addEventListener('click', (e) => {
if (pickMode) return;
const targetElem = e.target.nodeType === Node.ELEMENT_NODE ? e.target : e.target.parentElement;
if (!targetElem) return;
if (customQuestionSelector) {
let parent = targetElem;
let depth = 0;
while (parent && parent !== document.body && depth < 10) {
if (parent.matches && parent.matches(customQuestionSelector)) {
const question = getQuestionTextFromElement(parent);
if (question) {
lastClickedQuestion = question;
log('点击捕获题目(自定义选择器):', lastClickedQuestion);
return;
}
}
parent = parent.parentElement;
depth++;
}
}
const topicItem = targetElem.closest('.exam-topic-item');
if (topicItem) {
const titleElem = topicItem.querySelector('.exam-topic-item-title-name');
if (titleElem && titleElem.textContent) {
lastClickedQuestion = titleElem.textContent.trim();
log('点击捕获题目(类名):', lastClickedQuestion);
return;
}
}
let target = targetElem;
let depth = 0;
while (target && target !== document.body && depth < 10) {
if (target.nodeType === Node.ELEMENT_NODE) {
const text = target.textContent ? target.textContent.trim() : '';
if (/^\d+[\.、]/.test(text) && text.length > 10 && text.length < 500 && !target.querySelector('input')) {
const pure = extractPureQuestion(text);
if (pure) {
lastClickedQuestion = pure;
log('点击捕获题目(后备):', lastClickedQuestion);
return;
}
}
}
target = target.parentElement;
depth++;
}
});
// ============= 面板拖拽 =============
let dragActive = false;
let dragStartX = 0, dragStartY = 0;
let panelStartLeft = 0, panelStartTop = 0;
function onDragStart(e) {
if (e.target.closest('#search-result, .action-buttons, #opacity-control')) return;
if (e.target === searchPanel || e.target.closest('#search-header')) {
dragActive = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
const style = window.getComputedStyle(searchPanel);
panelStartLeft = parseFloat(style.left);
panelStartTop = parseFloat(style.top);
e.preventDefault();
}
}
function onDragMove(e) {
if (!dragActive) return;
e.preventDefault();
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
let newLeft = panelStartLeft + dx;
let newTop = panelStartTop + dy;
newLeft = Math.max(0, Math.min(window.innerWidth - searchPanel.offsetWidth, newLeft));
newTop = Math.max(0, Math.min(window.innerHeight - searchPanel.offsetHeight, newTop));
searchPanel.style.left = `${newLeft}px`;
searchPanel.style.top = `${newTop}px`;
}
function onDragEnd() {
if (dragActive) {
lastPanelPosition.left = parseFloat(searchPanel.style.left);
lastPanelPosition.top = parseFloat(searchPanel.style.top);
dragActive = false;
}
}
searchPanel.addEventListener('mousedown', onDragStart);
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragEnd);
// 透明度控制
const slider = document.getElementById('opacity-slider');
const opacityVal = document.getElementById('opacity-value');
slider.addEventListener('input', (e) => {
const val = e.target.value;
searchPanel.style.opacity = val / 100;
opacityVal.textContent = `${val}%`;
});
log('初始化完成,题库数量:', QUESTION_DATABASE.length);
})();