// ==UserScript==
// @name 超星自动采集(优化版)
// @namespace http://tampermonkey.net/
// @version 0.4
// @description 超星自动采集 - 支持单个考试页面和考试列表页面(性能优化版)
// @author You
// @match https://mooc2-ans.chaoxing.com/mooc2-ans/work/dowork*
// @match https://mooc2-ans.chaoxing.com/mooc2-ans/exam/lookpaper*
// @icon https://www.google.com/s2/favicons?sz=64&domain=chaoxing.com
// @grant none
// @require https://unpkg.com/[email protected]/dist/layui.js
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 全局配置
const CONFIG = {
API_ENDPOINT: 'http://localhost:5000/api/questions/add', // 主要API端点
API_ENDPOINTS: ['http://localhost:5000/api/questions/add', 'http://127.0.0.1:5000/api/questions/add'], // 备用端点列表
MAX_RETRIES: 2,
RETRY_DELAY: 1000, // 重试间隔(毫秒)
AUTO_COLLECT_DELAY: 500,
NOTIFICATION_DURATION: 3000,
AUTO_CLOSE_DELAY: 100,
BATCH_SIZE: 20, // 批量处理题目时的批次大小
DEBUG: true // 调试模式
};
// 日志工具
const Logger = {
log: function (message, data) {
if (CONFIG.DEBUG) {
console.log(`[超星采集] ${message}`, data || '');
}
},
warn: function (message, data) {
if (CONFIG.DEBUG) {
console.warn(`[超星采集] ${message}`, data || '');
}
},
error: function (message, error) {
console.error(`[超星采集] ${message}`, error || '');
}
};
// 检测当前页面类型
const PageDetector = {
isExamPage: location.href.includes('/exam/lookpaper'),
isExamListPage: function () {
return this.isExamPage && !!document.querySelector('.dataBody');
},
isExamDetailPage: function () {
return this.isExamPage && !!document.querySelector('.stem_con');
},
detectPageType: function () {
if (this.isExamListPage()) {
return 'examList';
} else if (this.isExamDetailPage()) {
return 'examDetail';
}
return 'unknown';
}
};
// 创建全局状态对象
const state = {
examLinks: [],
currentExamIndex: 0,
isCollecting: false,
totalCollected: 0,
startTime: null,
// 新增:本地存储支持
saveState: function () {
const stateToSave = {
examLinks: this.examLinks,
currentExamIndex: this.currentExamIndex,
totalCollected: this.totalCollected
};
localStorage.setItem('examCollectorState', JSON.stringify(stateToSave));
Logger.log('状态已保存到本地存储');
},
loadState: function () {
try {
const savedState = localStorage.getItem('examCollectorState');
if (savedState) {
const parsedState = JSON.parse(savedState);
this.examLinks = parsedState.examLinks || [];
this.currentExamIndex = parsedState.currentExamIndex || 0;
this.totalCollected = parsedState.totalCollected || 0;
Logger.log('从本地存储加载状态成功');
return true;
}
} catch (e) {
Logger.error('从本地存储加载状态失败', e);
}
return false;
},
clearState: function () {
localStorage.removeItem('examCollectorState');
Logger.log('本地存储状态已清除');
}
};
// 根据页面类型执行不同的初始化
const pageType = PageDetector.detectPageType();
Logger.log(`检测到页面类型: ${pageType}`);
if (pageType === 'examList') {
initExamListPage();
} else if (pageType === 'examDetail') {
initExamDetailPage();
} else {
Logger.warn('未知页面类型,脚本停止执行');
return;
}
// 考试列表页面初始化函数
function initExamListPage() {
// 防止重复添加按钮
if (document.getElementById('batch-collect-btn')) return;
// 尝试从本地存储加载状态
const hasRestoredState = state.loadState();
// 收集所有考试链接
if (!hasRestoredState || state.examLinks.length === 0) {
collectExamLinks();
} else {
Logger.log('已从本地存储恢复考试链接列表', state.examLinks.length);
}
// 创建UI
createExamListUI();
// 检查是否满足自动开始批量采集的条件
const autoStartParam = new URLSearchParams(window.location.search).get('autostart');
const hasAutoStartFlag = autoStartParam === 'true' || autoStartParam === '1';
const hasEnoughExams = state.examLinks.length > 0;
const shouldAutoStart = (hasAutoStartFlag || localStorage.getItem('autoCollectEnabled') !== 'false') && hasEnoughExams;
// 如果满足自动开始条件,延迟一秒后自动开始批量采集
if (shouldAutoStart) {
Logger.log('检测到自动采集已启用,将自动开始批量采集...');
setTimeout(() => {
startBatchCollection(true); // 传入true表示自动模式
}, 1000);
}
}
// 创建考试列表页面UI
function createExamListUI() {
// 创建批量采集按钮和自动采集开关
const buttonContainer = document.createElement('div');
Object.assign(buttonContainer.style, {
position: 'fixed',
right: '30px',
bottom: '30px',
zIndex: '9999',
display: 'flex',
flexDirection: 'column',
gap: '10px'
});
document.body.appendChild(buttonContainer);
// 自动采集开关
const autoCollectContainer = document.createElement('div');
Object.assign(autoCollectContainer.style, {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
gap: '8px',
marginBottom: '5px',
backgroundColor: 'rgba(255,255,255,0.9)',
padding: '5px 10px',
borderRadius: '5px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
});
const autoCollectLabel = document.createElement('label');
autoCollectLabel.textContent = '自动开始采集';
Object.assign(autoCollectLabel.style, {
fontSize: '14px',
color: '#333',
userSelect: 'none',
cursor: 'pointer'
});
const autoCollectCheckbox = document.createElement('input');
autoCollectCheckbox.type = 'checkbox';
autoCollectCheckbox.id = 'auto-collect-checkbox';
// 默认选中
autoCollectCheckbox.checked = localStorage.getItem('autoCollectEnabled') !== 'false';
autoCollectCheckbox.style.cursor = 'pointer';
// 保存选择状态
autoCollectCheckbox.addEventListener('change', function () {
localStorage.setItem('autoCollectEnabled', this.checked);
// 如果勾选了自动采集,并且有考试链接,则自动开始采集
if (this.checked && state.examLinks.length > 0) {
setTimeout(() => {
startBatchCollection(true);
}, 500);
}
});
autoCollectLabel.setAttribute('for', 'auto-collect-checkbox');
autoCollectContainer.appendChild(autoCollectLabel);
autoCollectContainer.appendChild(autoCollectCheckbox);
buttonContainer.appendChild(autoCollectContainer);
// 批量采集按钮
const batchBtn = document.createElement('button');
batchBtn.id = 'batch-collect-btn';
batchBtn.textContent = `批量采集题目 (${state.examLinks.length} 个考试)`;
Object.assign(batchBtn.style, {
padding: '10px 20px',
background: '#409EFF',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontWeight: 'bold',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
width: '100%'
});
buttonContainer.appendChild(batchBtn);
// 创建状态提示
const statusTip = document.createElement('div');
statusTip.id = 'batch-status-tip';
statusTip.style.display = 'none';
Object.assign(statusTip.style, {
position: 'fixed',
top: '10px',
left: '50%',
transform: 'translateX(-50%)',
padding: '10px 20px',
background: 'rgba(0, 0, 0, 0.7)',
color: '#fff',
borderRadius: '5px',
zIndex: '9999',
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
fontWeight: 'bold',
textAlign: 'center'
});
document.body.appendChild(statusTip);
// 添加批量采集点击事件
batchBtn.onclick = startBatchCollection;
// 添加进度条
const progressContainer = document.createElement('div');
progressContainer.id = 'collection-progress-container';
Object.assign(progressContainer.style, {
position: 'fixed',
bottom: '100px',
right: '30px',
width: '200px',
backgroundColor: 'rgba(255,255,255,0.9)',
padding: '10px',
borderRadius: '5px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
display: 'none',
zIndex: '9998'
});
const progressLabel = document.createElement('div');
progressLabel.id = 'progress-label';
progressLabel.textContent = '采集进度';
Object.assign(progressLabel.style, {
fontSize: '14px',
marginBottom: '5px',
fontWeight: 'bold'
});
const progressBar = document.createElement('div');
progressBar.id = 'progress-bar-container';
Object.assign(progressBar.style, {
width: '100%',
height: '8px',
backgroundColor: '#e0e0e0',
borderRadius: '4px',
overflow: 'hidden'
});
const progressFill = document.createElement('div');
progressFill.id = 'progress-bar-fill';
Object.assign(progressFill.style, {
height: '100%',
width: '0%',
backgroundColor: '#409EFF',
borderRadius: '4px',
transition: 'width 0.3s'
});
progressBar.appendChild(progressFill);
progressContainer.appendChild(progressLabel);
progressContainer.appendChild(progressBar);
document.body.appendChild(progressContainer);
}
// 收集考试链接函数
function collectExamLinks() {
state.examLinks = [];
// 从页面中获取所有考试链接
const links = document.querySelectorAll('.random_opera_td_20 a.look_td');
if (links.length === 0) {
Logger.warn('没有找到考试链接');
return;
}
// 将相对路径转换为完整URL
links.forEach((link, index) => {
const href = link.getAttribute('href');
const fullUrl = new URL(href, window.location.origin).href;
const examName = link.closest('.randomBody_td').querySelector('.random_name_td').textContent.trim();
state.examLinks.push({
url: fullUrl,
name: examName,
index: index + 1
});
});
Logger.log(`共找到 ${state.examLinks.length} 个考试链接`);
// 保存状态到本地存储
state.saveState();
}
// 开始批量采集函数
function startBatchCollection(isAuto = false) {
if (state.isCollecting) {
alert('正在采集中,请等待当前采集完成');
return;
}
if (state.examLinks.length === 0) {
alert('没有可采集的考试链接');
return;
}
// 更新状态
state.isCollecting = true;
state.startTime = new Date();
// 显示进度条
const progressContainer = document.getElementById('collection-progress-container');
if (progressContainer) {
progressContainer.style.display = 'block';
}
// 显示状态提示
const statusTip = document.getElementById('batch-status-tip');
if (statusTip) {
statusTip.textContent = `正在准备批量采集 ${state.examLinks.length} 个考试...`;
statusTip.style.display = 'block';
}
// 更新进度
updateProgressBar(0, state.examLinks.length);
// 开始处理第一个考试
Logger.log('开始批量采集');
setTimeout(() => {
processNextExam();
}, 500);
}
// 更新进度条
function updateProgressBar(current, total) {
const progressFill = document.getElementById('progress-bar-fill');
const progressLabel = document.getElementById('progress-label');
if (progressFill && progressLabel) {
const percent = Math.round((current / total) * 100);
progressFill.style.width = `${percent}%`;
progressLabel.textContent = `采集进度: ${current}/${total} (${percent}%)`;
}
}
// 处理下一个考试 - 使用自动定时检查方式
function processNextExam() {
// 检查是否已完成所有考试
if (state.currentExamIndex >= state.examLinks.length) {
finishBatchCollection();
return;
}
// 获取当前要处理的考试
const currentExam = state.examLinks[state.currentExamIndex];
// 更新状态提示
const statusTip = document.getElementById('batch-status-tip');
if (statusTip) {
statusTip.textContent = `正在采集: ${currentExam.name} (${state.currentExamIndex + 1}/${state.examLinks.length})`;
}
// 更新进度条
updateProgressBar(state.currentExamIndex, state.examLinks.length);
// 记录当前处理的索引到会话存储,用于恢复
sessionStorage.setItem('currentProcessingIndex', state.currentExamIndex);
sessionStorage.setItem('batchCollection', 'true');
sessionStorage.setItem('batchCollectionIndex', state.currentExamIndex);
// 记录当前时间戳,用于检测超时
const startTime = new Date().getTime();
localStorage.setItem('lastExamStartTime', startTime);
localStorage.setItem('lastExamIndex', state.currentExamIndex);
Logger.log(`处理考试 ${state.currentExamIndex + 1}/${state.examLinks.length}: ${currentExam.name}`);
// 打开考试链接
const examWindow = window.open(currentExam.url, '_blank');
// 增加考试索引,为下一个做准备
state.currentExamIndex++;
// 保存状态到本地存储
state.saveState();
// 如果无法打开新窗口(可能被浏览器阻止)
if (!examWindow) {
Logger.error('无法打开新窗口,可能被浏览器阻止');
alert('无法打开新窗口,请检查浏览器是否阻止了弹出窗口');
state.isCollecting = false;
return;
}
// 设置定时器检查是否需要继续下一个
// 如果当前窗口已关闭,或者超过一定时间,自动继续下一个
setTimeout(checkAndContinue, 5000);
}
// 检查是否需要继续下一个考试
function checkAndContinue() {
if (!state.isCollecting) return;
const lastExamIndex = parseInt(localStorage.getItem('lastExamIndex') || '0');
const lastStartTime = parseInt(localStorage.getItem('lastExamStartTime') || '0');
const currentTime = new Date().getTime();
const timeElapsed = currentTime - lastStartTime;
// 检查是否有完成标记
const examCompleted = localStorage.getItem('examCompleted') === 'true';
const lastCompletedIndex = parseInt(localStorage.getItem('lastCompletedIndex') || '-1');
const lastCompletedTime = parseInt(localStorage.getItem('lastCompletedTime') || '0');
// 如果有完成标记,并且完成的是当前考试
if (examCompleted && lastCompletedIndex === lastExamIndex) {
// 如果完成时间足够新(在最近的一分钟内)
if (currentTime - lastCompletedTime < 60000) {
Logger.log(`检测到考试已完成,索引: ${lastCompletedIndex}`);
// 清除完成标记
localStorage.removeItem('examCompleted');
// 继续下一个考试
processNextExam();
return;
}
}
// 如果当前索引不等于上次记录的索引,说明已经开始了下一个考试
if (state.currentExamIndex > lastExamIndex + 1) {
Logger.log('已经开始了新的考试,不需要自动继续');
return;
}
// 检查是否超时,默认30秒
if (timeElapsed > 30000) {
Logger.log(`当前考试处理超时 ${timeElapsed / 1000} 秒,自动继续下一个`);
processNextExam();
return;
}
// 如果没有超时,继续定时检查
setTimeout(checkAndContinue, 5000);
}
// 完成批量采集
function finishBatchCollection() {
const endTime = new Date();
const timeUsed = (endTime - state.startTime) / 1000;
// 更新状态
state.isCollecting = false;
// 清除会话存储
sessionStorage.removeItem('batchCollection');
sessionStorage.removeItem('batchCollectionIndex');
sessionStorage.removeItem('currentProcessingIndex');
// 清除本地存储状态
state.clearState();
// 隐藏进度条
const progressContainer = document.getElementById('collection-progress-container');
if (progressContainer) {
progressContainer.style.display = 'none';
}
// 更新状态提示
const statusTip = document.getElementById('batch-status-tip');
if (statusTip) {
statusTip.textContent = `批量采集完成! 共采集 ${state.totalCollected} 道题目,耗时 ${timeUsed.toFixed(1)} 秒`;
statusTip.style.background = 'rgba(40, 167, 69, 0.9)';
// 5秒后隐藏状态提示
setTimeout(() => {
statusTip.style.display = 'none';
statusTip.style.background = 'rgba(0, 0, 0, 0.7)';
}, 5000);
}
Logger.log(`批量采集完成! 共采集 ${state.totalCollected} 道题目,耗时 ${timeUsed.toFixed(1)} 秒`);
// 重置状态
state.currentExamIndex = 0;
state.totalCollected = 0;
}
// 考试详情页面初始化函数
function initExamDetailPage() {
// 移除左侧菜单,优化页面布局
$("#lookLeft").remove();
// 添加选中状态
$(".check_dx").addClass("checked_dx")
// 显示答案
$(".answerDiv").show();
// 防止重复初始化
if (document.getElementById('my-question-btn')) return;
// 检查是否来自自动采集
const isFromBatchCollection = sessionStorage.getItem('batchCollection') === 'true';
// 创建UI元素
createExamDetailUI();
// 自动开始采集和导入功能
Logger.log('自动采集已启动');
// 使用较短的延迟确保页面完全加载
setTimeout(() => {
// 直接调用采集函数,避免额外的DOM操作
const questions = parseQuestionsFromPage();
if (!questions.length) {
showNotification('未采集到题目', 'error');
// 如果来自批量采集,则关闭页面并继续下一个
if (isFromBatchCollection) {
safeCloseWindow(0);
}
return;
}
Logger.log(`采集完成,共 ${questions.length} 道题目,开始导入`);
// 更新状态提示
const statusTip = document.querySelector('div[style*="position: fixed"][style*="top: 10px"]');
if (statusTip) {
statusTip.textContent = `正在导入 ${questions.length} 道题目...`;
}
// 如果来自批量采集,设置自动导入标记
if (isFromBatchCollection) {
const autoImportFlag = document.querySelector('.auto-import-flag');
if (autoImportFlag) {
autoImportFlag.style.display = 'block';
}
}
// 直接发送请求,跳过预览弹窗步骤
importQuestions(questions);
}, 300); // 等待300ms确保页面完全加载
}
// 创建考试详情页面UI
function createExamDetailUI() {
// 创建按钮
const btn = document.createElement('button');
btn.id = 'my-question-btn';
btn.textContent = '开始采集';
Object.assign(btn.style, {
position: 'fixed',
right: '30px',
bottom: '30px',
zIndex: 9999,
padding: '10px 20px',
background: '#409EFF',
color: '#fff',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
fontWeight: 'bold',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)'
});
document.body.appendChild(btn);
// 创建自动导入标记
const autoImportFlag = document.createElement('div');
autoImportFlag.className = 'auto-import-flag';
autoImportFlag.style.display = 'none';
document.body.appendChild(autoImportFlag);
// 创建状态提示
const statusTip = document.createElement('div');
Object.assign(statusTip.style, {
position: 'fixed',
top: '10px',
left: '50%',
transform: 'translateX(-50%)',
padding: '10px 20px',
background: 'rgba(0, 0, 0, 0.7)',
color: '#fff',
borderRadius: '5px',
zIndex: '9999',
boxShadow: '0 2px 10px rgba(0, 0, 0, 0.2)',
fontWeight: 'bold'
});
statusTip.textContent = '自动采集中...';
document.body.appendChild(statusTip);
// 添加按钮点击事件
btn.onclick = function () {
const questions = parseQuestionsFromPage();
if (!questions.length) {
showNotification('未采集到题目', 'error');
return;
}
// 格式化预览HTML
const html = renderQuestionsPreview(questions);
showQuestionsPreview(html, questions);
};
// 添加消息监听器,接收子窗口的采集完成消息
window.addEventListener('message', function (event) {
if (event.data && event.data.type === 'examCollected') {
const collectedCount = event.data.collectedCount || 0;
Logger.log(`收到采集完成消息,采集数量: ${collectedCount}`);
// 更新总采集数量
state.totalCollected += collectedCount;
// 更新状态提示
const statusTip = document.getElementById('batch-status-tip');
if (statusTip) {
statusTip.textContent = `正在采集: 已完成 ${state.currentExamIndex}/${state.examLinks.length}, 共采集 ${state.totalCollected} 道题目`;
}
// 处理下一个考试
setTimeout(() => {
processNextExam();
}, 50); // 极大减少等待时间,加快批量采集速度
}
});
}
// 显示题目预览
function showQuestionsPreview(html, questions) {
layui.use('layer', function () {
let layer = layui.layer;
layer.open({
type: 1,
title: '<div style="display:flex;align-items:center;gap:10px;font-size:18px;font-weight:500;line-height:1.2;min-height:32px;">' +
'<img src="https://img1.imgtp.com/2023/07/16/2Qv7Qw1b.png" style="width:22px;height:22px;vertical-align:middle;">' +
'<span>采集题目预览</span>' +
'</div>',
closeBtn: 1,
area: ['750px', '520px'],
content: `<div id="plum-bg" style="position:relative;min-height:420px;">${html}</div>` +
`<style>
#plum-bg::before {
content: '';
position: absolute;
left: 0; top: 0; right: 0; bottom: 0;
background: url('https://img1.imgtp.com/2023/07/16/2Qv7Qw1b.png') repeat center center;
opacity: 0.07;
pointer-events: none;
z-index: 0;
}
#plum-bg table {
border-radius: 14px;
box-shadow: 0 4px 24px 0 rgba(120,80,160,0.13);
overflow: hidden;
background: rgba(255,255,255,0.97);
font-size: 15px;
}
#plum-bg th {
background: linear-gradient(90deg,#f5f7fa 60%,#f0e6f6 100%);
color: #6d3b7b;
font-weight: 600;
border-bottom: 2px solid #e0d7f3;
}
#plum-bg tr:hover {background: #f0f6ff;}
#plum-bg th, #plum-bg td {
transition: background 0.2s;
padding: 8px 10px;
}
#plum-bg td {
border:1px solid #eee;
border-left: none;
border-right: none;
}
.layui-layer-content { border-radius: 16px!important; }
.layui-layer { box-shadow: 0 8px 32px 0 rgba(120,80,160,0.18)!important; border-radius: 18px!important; }
.layui-layer-title { font-size:18px!important; font-weight:500; background:linear-gradient(90deg,#fff 60%,#f0e6f6 100%)!important; color:#6d3b7b!important; border-radius: 18px 18px 0 0!important; min-height: 32px; display: flex; align-items: center; gap: 10px;}
.layui-layer-setwin { right: 16px!important; top: 16px!important; }
.layui-layer-setwin .layui-layer-close1 { font-size:22px!important; color:#b08bc7!important; }
.layui-layer-btn {
padding-bottom: 18px !important;
display: flex;
justify-content: center;
align-items: center;
gap: 0;
}
.layui-layer-btn .layui-layer-btn0 {
background: linear-gradient(90deg,#8f6ed5 0%,#4e8cff 100%);
color: #fff !important;
border: none !important;
border-radius: 12px !important;
font-size: 17px;
font-weight: 700;
box-shadow: 0 4px 16px 0 rgba(120,80,160,0.18);
padding: 0 38px;
height: 44px;
min-width: 120px;
margin: 0 12px 0 0;
transition: background 0.2s, box-shadow 0.2s, transform 0.1s;
outline: none;
}
.layui-layer-btn .layui-layer-btn0:hover, .layui-layer-btn .layui-layer-btn0:focus {
background: linear-gradient(90deg,#4e8cff 0%,#8f6ed5 100%);
box-shadow: 0 8px 32px 0 rgba(120,80,160,0.22);
transform: translateY(-2px) scale(1.04);
}
.layui-layer-btn .layui-layer-btn1 {
background: #f5f7fa !important;
color: #8f6ed5 !important;
border: 1.5px solid #e0d7f3 !important;
border-radius: 12px !important;
font-size: 16px;
font-weight: 500;
min-width: 100px;
height: 44px;
margin-left: 0;
margin-right: 12px;
transition: background 0.2s, color 0.2s, border 0.2s, transform 0.1s;
outline: none;
}
.layui-layer-btn .layui-layer-btn1:hover, .layui-layer-btn .layui-layer-btn1:focus {
background: #e6e6f7 !important;
color: #6d3b7b !important;
border: 1.5px solid #b08bc7 !important;
transform: translateY(-1px) scale(1.03);
}
@media (max-width: 600px) {
.layui-layer-btn .layui-layer-btn0, .layui-layer-btn .layui-layer-btn1 {
min-width: 80px;
font-size: 15px;
padding: 0 12px;
height: 38px;
}
}
</style>`,
btn: ['确定导入', '取消'],
btnAlign: 'c',
yes: function (index) {
importQuestions(questions);
}
});
});
}
// 导入题目到服务器
async function importQuestions(questions) {
try {
const isAutoImport = document.querySelector('.auto-import-flag') !== null;
// 检查是否来自批量采集
const isFromBatchCollection = sessionStorage.getItem('batchCollection') === 'true';
Logger.log(`开始录入题目到数据库,共 ${questions.length} 道题目${isAutoImport ? ' (自动导入模式)' : ''}`);
if (!questions || questions.length === 0) {
Logger.warn('未找到可采集的题目');
// 只在批量采集模式下才调用closeAndContinue
if (isFromBatchCollection && typeof closeAndContinue === 'function') {
closeAndContinue(0);
} else {
showNotification('未找到可采集的题目', 'error');
}
return;
}
// 记录开始时间
const startTime = new Date();
// 发送数据到服务器
let body = { questions };
Logger.log('发送数据:', JSON.stringify(body).substring(0, 200) + '...');
// 添加重试机制
let retryCount = 0;
let success = false;
while (retryCount < CONFIG.MAX_RETRIES && !success) {
try {
// 确保使用正确的API端点
const apiEndpoint = CONFIG.API_ENDPOINT || 'http://localhost:5000/api/questions/add';
Logger.log(`正在连接到API端点: ${apiEndpoint}`);
const response = await fetch(apiEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (response.ok) {
const result = await response.json();
Logger.log('导入成功:', result);
// 显示更醒目的导入成功提示
const successMsg = `导入成功: ${result.message || `已导入 ${questions.length} 道题目`}`;
// 使用layui的通知
if (typeof layui !== 'undefined' && layui.layer) {
layui.layer.msg(successMsg, {
icon: 1,
offset: 't',
time: 3000,
shade: 0.3,
anim: 6,
skin: 'layui-layer-molv'
});
} else {
// 创建一个更醒目的成功提示框
const successNotice = document.createElement('div');
Object.assign(successNotice.style, {
position: 'fixed',
top: '20%',
left: '50%',
transform: 'translate(-50%, -50%)',
padding: '15px 25px',
background: 'rgba(82, 196, 26, 0.9)',
color: '#fff',
borderRadius: '8px',
boxShadow: '0 4px 20px rgba(0, 0, 0, 0.2)',
zIndex: 10000,
fontWeight: 'bold',
fontSize: '16px',
textAlign: 'center',
animation: 'fadeInDown 0.5s forwards',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
});
// 添加动画样式
if (!document.getElementById('successNoticeStyles')) {
const style = document.createElement('style');
style.id = 'successNoticeStyles';
style.textContent = `
@keyframes fadeInDown {
from { opacity: 0; transform: translate(-50%, -70%); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
@keyframes fadeOutUp {
from { opacity: 1; transform: translate(-50%, -50%); }
to { opacity: 0; transform: translate(-50%, -70%); }
}
`;
document.head.appendChild(style);
}
// 添加成功图标
const iconSpan = document.createElement('span');
iconSpan.innerHTML = '✔'; // 勾选图标
iconSpan.style.marginRight = '10px';
iconSpan.style.fontSize = '20px';
successNotice.appendChild(iconSpan);
// 添加文本
const textSpan = document.createElement('span');
textSpan.textContent = successMsg;
successNotice.appendChild(textSpan);
// 添加到文档
document.body.appendChild(successNotice);
// 定时移除
setTimeout(() => {
successNotice.style.animation = 'fadeOutUp 0.5s forwards';
setTimeout(() => {
if (document.body.contains(successNotice)) {
document.body.removeChild(successNotice);
}
}, 500);
}, 3000);
}
// 同时更新状态提示
updateStatusTip({
text: successMsg,
background: 'rgba(40, 167, 69, 0.9)',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.2)'
});
success = true;
// 如果是批量采集模式,则关闭当前页面并继续下一个
if (isFromBatchCollection) {
Logger.log('批量采集模式,关闭当前页面并继续');
// 安全地关闭窗口并继续
safeCloseWindow(questions.length);
}
} else {
const errorText = await response.text();
throw new Error(`服务器响应错误: ${response.status} ${errorText}`);
}
} catch (error) {
retryCount++;
Logger.error(`导入失败 (重试 ${retryCount}/${CONFIG.MAX_RETRIES}):`, error);
if (retryCount >= CONFIG.MAX_RETRIES) {
showNotification(`导入失败: ${error.message}`, 'error');
// 如果是批量采集模式,则关闭当前页面并继续下一个
if (isFromBatchCollection) {
Logger.log('导入失败,但仍然关闭页面并继续');
safeCloseWindow(0);
}
} else {
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, CONFIG.RETRY_DELAY));
}
}
}
} catch (e) {
Logger.error('导入过程中发生异常:', e);
showNotification(`导入过程中发生异常: ${e.message}`, 'error');
// 如果是批量采集模式,即使出错也继续下一个
const isFromBatchCollection = sessionStorage.getItem('batchCollection') === 'true';
if (isFromBatchCollection) {
Logger.log('导入过程异常,但仍然关闭页面并继续');
safeCloseWindow(0);
}
Logger.error('录入题目失败,错误信息:', e);
showNotification('网络错误,录入失败!', 'error');
updateStatusTip({
text: `导入失败: 网络错误`,
background: 'rgba(220, 53, 69, 0.9)'
});
}
}
// 显示通知提示
function showNotification(message, type = 'info') {
// 使用layui提供的通知功能
if (typeof layui !== 'undefined' && layui.layer) {
// 根据类型设置图标
let icon = 0; // 默认信息图标
switch (type) {
case 'success': icon = 1; break;
case 'error': icon = 2; break;
case 'warning': icon = 0; break;
case 'info': icon = 6; break;
}
// 显示通知
layui.layer.msg(message, {
icon: icon,
offset: 't',
anim: 6,
time: CONFIG.NOTIFICATION_DURATION
});
} else {
// 如果layui不可用,创建一个简单的通知元素
const notificationId = 'custom-notification-' + Date.now();
const notification = document.createElement('div');
notification.id = notificationId;
// 根据类型设置样式
let backgroundColor = '#1890ff';
let textColor = '#fff';
switch (type) {
case 'success':
backgroundColor = '#52c41a';
break;
case 'error':
backgroundColor = '#f5222d';
break;
case 'warning':
backgroundColor = '#faad14';
break;
}
// 设置样式
Object.assign(notification.style, {
position: 'fixed',
top: '20px',
left: '50%',
transform: 'translateX(-50%)',
padding: '10px 20px',
background: backgroundColor,
color: textColor,
borderRadius: '4px',
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
zIndex: 10000,
fontWeight: 'bold',
animation: 'fadeIn 0.3s forwards'
});
// 添加动画样式
if (!document.getElementById('notificationStyles')) {
const style = document.createElement('style');
style.id = 'notificationStyles';
style.textContent = `
@keyframes fadeIn {
from { opacity: 0; transform: translate(-50%, -20px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
@keyframes fadeOut {
from { opacity: 1; transform: translate(-50%, 0); }
to { opacity: 0; transform: translate(-50%, -20px); }
}
`;
document.head.appendChild(style);
}
// 设置内容
notification.textContent = message;
// 添加到文档
document.body.appendChild(notification);
// 定时移除
setTimeout(() => {
notification.style.animation = 'fadeOut 0.3s forwards';
setTimeout(() => {
if (document.getElementById(notificationId)) {
document.body.removeChild(notification);
}
}, 300);
}, CONFIG.NOTIFICATION_DURATION);
}
// 同时更新状态提示
updateStatusTip({
text: message,
background: type === 'error' ? 'rgba(220, 53, 69, 0.9)' :
type === 'success' ? 'rgba(40, 167, 69, 0.9)' :
'rgba(0, 123, 255, 0.9)'
});
// 输出到控制台
if (type === 'error') {
Logger.error(message);
} else {
Logger.log(message);
}
}
// 更新状态提示
function updateStatusTip(options) {
const statusTip = document.querySelector('div[style*="position: fixed"][style*="top: 10px"]');
if (statusTip) {
if (options.text) statusTip.textContent = options.text;
if (options.background) statusTip.style.background = options.background;
if (options.boxShadow) statusTip.style.boxShadow = options.boxShadow;
// 5秒后消失
setTimeout(() => {
statusTip.style.animation = 'fadeOut 0.5s forwards';
// 添加消失动画
if (!document.getElementById('fadeOutStyle')) {
const fadeStyle = document.createElement('style');
fadeStyle.id = 'fadeOutStyle';
fadeStyle.textContent = `
@keyframes fadeOut {
from { opacity: 1; transform: translateX(-50%) translateY(0); }
to { opacity: 0; transform: translateX(-50%) translateY(-20px); }
}
`;
document.head.appendChild(fadeStyle);
}
setTimeout(() => statusTip.style.display = 'none', 500);
}, 5000);
}
}
// 显示成功通知
function showSuccessNotification(options) {
layui.use('layer', function () {
let layer = layui.layer;
layer.open({
type: 1,
title: false,
closeBtn: false,
shade: 0.3,
area: ['400px', 'auto'],
skin: 'success-notification',
time: CONFIG.NOTIFICATION_DURATION,
anim: 2,
shadeClose: true,
content: `<div style="padding: 20px; text-align: center; background: linear-gradient(135deg, #28a745, #20c997); border-radius: 10px; box-shadow: 0 10px 30px rgba(40, 167, 69, 0.3);">
<div style="font-size: 60px; margin-bottom: 10px; color: white;"><i class="layui-icon layui-icon-ok-circle"></i></div>
<div style="font-size: 22px; font-weight: bold; color: white; margin-bottom: 5px;">${options.title}</div>
<div style="color: rgba(255,255,255,0.9); font-size: 16px;">${options.message}</div>
<div style="margin-top: 15px; color: rgba(255,255,255,0.8); font-size: 14px;">${options.details}</div>
</div>`
});
});
}
// 显示通知
function showNotification(message, type = 'info') {
if (layui && layui.layer) {
const icons = {
info: 0,
success: 1,
error: 2,
warning: 0
};
layui.layer.msg(message, { icon: icons[type] || 0 });
} else {
alert(message);
}
}
// 安全关闭窗口并继续下一个考试的辅助函数
function safeCloseWindow(collectedCount = 0) {
try {
// 直接使用现有的closeAndContinue函数
if (typeof closeAndContinue === 'function') {
Logger.log(`调用closeAndContinue函数,采集数量: ${collectedCount}`);
closeAndContinue(collectedCount);
return;
}
// 如果closeAndContinue函数不存在,尝试直接与父窗口交互
if (window.opener && !window.opener.closed) {
try {
// 尝试通知父窗口更新采集状态
if (window.opener.state) {
window.opener.state.totalCollected += collectedCount;
}
// 调用父窗口的processNextExam函数
if (typeof window.opener.processNextExam === 'function') {
Logger.log(`直接调用父窗口的processNextExam函数,采集数量: ${collectedCount}`);
window.opener.processNextExam();
}
} catch (e) {
Logger.error('通知父窗口失败:', e);
}
}
// 清除会话存储
sessionStorage.removeItem('batchCollection');
sessionStorage.removeItem('batchCollectionIndex');
sessionStorage.removeItem('currentProcessingIndex');
// 关闭当前窗口
Logger.log('关闭当前窗口,继续下一个考试');
try { window.close(); } catch (e) { /* 忽略错误 */ }
// 如果窗口没有关闭(浏览器可能阻止了脚本关闭窗口)
setTimeout(() => {
if (!window.closed) {
alert('请手动关闭此窗口以继续采集下一个考试');
}
}, 500);
} catch (err) {
Logger.error('关闭页面失败:', err);
try { window.close(); } catch (e) { /* 忽略错误 */ }
}
}
// 关闭当前页面并继续下一个考试 - 使用本地存储方式
function closeAndContinue(collectedCount) {
// 输出详细日志,帮助调试
Logger.log('==== 开始处理关闭并继续下一个考试 ====');
Logger.log(`当前采集数量: ${collectedCount}`);
// 检查会话存储中的批量采集状态
const batchCollection = sessionStorage.getItem('batchCollection');
const batchIndex = parseInt(sessionStorage.getItem('batchCollectionIndex') || '0');
Logger.log(`批量采集状态: ${batchCollection}, 索引: ${batchIndex}`);
// 获取当前总采集数量
const oldTotal = parseInt(localStorage.getItem('totalCollected') || '0');
const newTotal = oldTotal + collectedCount;
// 更新本地存储中的采集数量
localStorage.setItem('totalCollected', newTotal);
Logger.log(`更新采集计数: ${oldTotal} + ${collectedCount} = ${newTotal}`);
// 标记当前考试已完成
localStorage.setItem('examCompleted', 'true');
localStorage.setItem('lastCompletedIndex', batchIndex);
localStorage.setItem('lastCompletedTime', new Date().getTime());
// 清除会话存储
sessionStorage.removeItem('batchCollection');
sessionStorage.removeItem('batchCollectionIndex');
sessionStorage.removeItem('currentProcessingIndex');
// 关闭窗口
closeWindow();
// 关闭窗口函数
function closeWindow() {
// 关闭当前窗口
Logger.log('关闭当前窗口,继续下一个考试');
try {
window.close();
} catch (e) {
Logger.error('关闭窗口失败:', e);
}
// 如果窗口没有关闭(浏览器可能阻止了脚本关闭窗口)
setTimeout(() => {
if (!window.closed) {
alert('请手动关闭此窗口以继续采集下一个考试');
}
}, 500);
}
}
// 从页面解析问题
function parseQuestionsFromPage() {
const questions = [];
const stemElements = $('.stem_con').get();
// 使用批量处理提高性能
for (let i = 0; i < stemElements.length; i++) {
const stemElement = $(stemElements[i]);
// 题型识别 - 使用缓存的选择器结果
const typeTextElement = stemElement.find('span.colorShallow');
const typeText = typeTextElement.length ? typeTextElement.text() : '';
// 根据关键词识别题目类型
let type = 'single'; // 默认为单选题
// 获取题目完整内容用于进一步分析
const fullQuestionText = stemElement.text().trim();
// 先检查类型文本
if (/多选|多项|多个|多种|选出多个|选出正确的选项有/.test(typeText)) {
type = 'multiple';
}
// 如果类型文本中没有多选关键词,检查完整题目内容
else if (/\(多选题|(多选题|多选题,|多选题,|选出多个|选出正确的选项有|正确的有|有\(\)$|有()$/.test(fullQuestionText)) {
type = 'multiple';
Logger.log('基于题目内容识别为多选题:', fullQuestionText.substring(0, 50) + '...');
}
// 检查选项数量,如果超过4个选项且没有其他类型标识,可能是多选题
else if (stemElement.next('.stem_answer').find('.num_option').length > 4) {
type = 'multiple';
Logger.log('基于选项数量识别为多选题');
}
// 其他题型检测
else if (/判断|对错|是非/.test(typeText)) {
type = 'judgement';
} else if (/填空|补充|填写/.test(typeText)) {
type = 'completion';
} else if (/简答|问答|说明|说说|讨论|分析/.test(typeText)) {
type = 'short';
}
// 获取完整题目,包括序号、题型信息和题干
let questionPrefix = '';
// 获取题目序号(如果有)
const questionNumberNode = stemElement.contents().filter(function () {
return this.nodeType === 3; // 文本节点
}).first();
const questionNumber = questionNumberNode.length ? questionNumberNode.text().trim() : '';
if (questionNumber) {
questionPrefix = questionNumber + ' ';
}
// 获取题型信息
if (typeText) {
questionPrefix += typeText + ' ';
}
// 题干(合并所有非空p,防止空p导致题干丢失)
// 使用数组存储并过滤空元素,提高性能
const paragraphs = stemElement.find('p').map(function () {
return $(this).html().trim();
}).get().filter(Boolean);
// 将所有段落合并为题干内容
let questionContent = paragraphs.join(' ');
// 如果题干开头包含序号和题型前缀,尝试去除
// 匹配序号和题型前缀,如"61. (判断题,1分)"
// 保存原始内容用于日志记录
const originalContent = questionContent;
// 先尝试去除常见的序号+题型前缀格式
questionContent = questionContent.replace(/^\s*\d+\.?\s*[\(\uff08][^\)\uff09]+[\)\uff09]\s*/i, '');
// 再尝试去除可能的其他格式前缀
questionContent = questionContent.replace(/^\s*\d+\.?\s*/i, ''); // 去除只有序号的前缀
questionContent = questionContent.replace(/^\s*[\(\uff08][^\)\uff09]+[\)\uff09]\s*/i, ''); // 去除只有括号的前缀
// 去除可能的空格
questionContent = questionContent.trim();
// 如果内容发生了变化,记录日志
if (originalContent !== questionContent) {
Logger.log(`题目前缀去除: 原始="${originalContent.substring(0, 50)}..." → 处理后="${questionContent.substring(0, 50)}..."`);
}
// 组合前缀和处理后的题干
const question = questionPrefix + questionContent;
Logger.log(`提取题干: ${questionContent.substring(0, 50)}...`);
// 选项处理
const $answerBlock = stemElement.next('.stem_answer');
const options = [];
const optionMap = {};
// 只有选择题才处理选项
if ((type === 'single' || type === 'multiple') && $answerBlock.length) {
// 使用缓存的jQuery对象减少DOM查询
const optionElements = $answerBlock.find('.num_option').get();
for (let j = 0; j < optionElements.length; j++) {
const optionElement = $(optionElements[j]);
const letter = optionElement.text().replace(/[..、]/, '').trim();
// 使用更精确的选择器获取选项内容
let content = optionElement.next('.answer_p').html() || optionElement.next().html();
content = content ? content.trim() : (optionElement.next('.answer_p').text().trim() || optionElement.next().text().trim());
options.push(letter + '.' + content);
optionMap[letter] = content;
}
}
// 答案处理:根据题型不同,获取答案区域
let $answerDiv;
if (type === 'single' || type === 'multiple') {
// 选择题:取 .stem_answer 的下一个兄弟 .answerDiv
$answerDiv = $answerBlock.length ? $answerBlock.next('.answerDiv') : $();
} else {
// 非选择题:直接从题干区域获取下一个兄弟 .answerDiv
$answerDiv = stemElement.next('.stem_answer').next('.answerDiv');
if (!$answerDiv.length) {
$answerDiv = stemElement.next('.answerDiv');
}
}
// 根据题型提取答案
let answer = '';
if (type === 'single' || type === 'multiple') {
// 选择题答案处理
const ansText = $answerDiv.find('.answer_tit p').text().trim().replace(/[^A-Z]/ig, '');
const ansArr = ansText.split('');
const ansContentArr = ansArr.map(l => optionMap[l] || l);
answer = ansContentArr.join(';');
} else if (type === 'completion') {
// 填空题答案处理
const blanks = [];
// 尝试多种选择器获取填空题答案
const blankElements = $answerDiv.find('.tiankong_con .ans-wid-cRight p').get();
if (blankElements.length) {
for (let k = 0; k < blankElements.length; k++) {
const blankElement = $(blankElements[k]);
let v = blankElement.html();
v = v ? v.trim() : blankElement.text().trim();
if (v) blanks.push(v);
}
} else {
// 备用选择器
const paragraphElements = $answerDiv.find('p').get();
for (let k = 0; k < paragraphElements.length; k++) {
const paragraphElement = $(paragraphElements[k]);
let v = paragraphElement.html();
v = v ? v.trim() : paragraphElement.text().trim();
if (v) blanks.push(v);
}
}
answer = blanks.join(';');
} else if (type === 'judgement') {
// 判断题答案处理
answer = $answerDiv.find('.answer_tit p').text().trim();
if (!answer) {
answer = $answerDiv.find('p').text().trim();
}
// 标准化判断题答案
if (/^(对|正确|true|√)$/i.test(answer)) answer = '正确';
if (/^(错|错误|false|×)$/i.test(answer)) answer = '错误';
} else if (type === 'short') {
// 简答题答案处理
const ansArr = [];
const shortAnswerElements = $answerDiv.find('.ans-wid-cRight p').get();
if (shortAnswerElements.length) {
for (let k = 0; k < shortAnswerElements.length; k++) {
const shortAnswerElement = $(shortAnswerElements[k]);
let v = shortAnswerElement.html();
v = v ? v.trim() : shortAnswerElement.text().trim();
if (v) ansArr.push(v);
}
}
if (!ansArr.length) {
// 尝试获取HTML内容
let v = $answerDiv.find('.ans-wid-cRight').html();
v = v ? v.trim() : $answerDiv.find('.ans-wid-cRight').text().trim();
if (v) ansArr.push(v);
}
answer = ansArr.filter(Boolean).join('\n');
} else {
// 其他类型题目的答案处理
const otherAnswerElements = $answerDiv.find('p').get();
const otherAnswers = [];
for (let k = 0; k < otherAnswerElements.length; k++) {
const otherAnswerElement = $(otherAnswerElements[k]);
let v = otherAnswerElement.html();
v = v ? v.trim() : otherAnswerElement.text().trim();
if (v) otherAnswers.push(v);
}
answer = otherAnswers.filter(Boolean).join(';');
}
// 根据题型构建题目对象
if (type === 'single' || type === 'multiple') {
questions.push({
question,
type,
options: options.join(';'),
answer
});
} else {
questions.push({
question,
type,
answer
});
}
}
return questions;
}
// 格式化题目预览
function renderQuestionsPreview(questions) {
// 使用DocumentFragment提高性能
const fragment = document.createDocumentFragment();
const tempContainer = document.createElement('div');
// 创建表格内容
let html = '<div style="max-height:350px;overflow:auto;"><table style="width:100%;border-collapse:collapse;">';
html += '<tr style="background:#f5f7fa;"><th>题干</th><th>类型</th><th>选项</th><th>答案</th></tr>';
// 高效处理所有题目
questions.forEach(q => {
// 根据题目类型显示不同的标签颜色
let typeClass = '';
switch (q.type) {
case 'single': typeClass = 'background-color:#e6f7ff;color:#1890ff;'; break;
case 'multiple': typeClass = 'background-color:#f6ffed;color:#52c41a;'; break;
case 'judgement': typeClass = 'background-color:#fff7e6;color:#fa8c16;'; break;
case 'completion': typeClass = 'background-color:#f9f0ff;color:#722ed1;'; break;
case 'short': typeClass = 'background-color:#fcf5e9;color:#fa541c;'; break;
}
// 类型显示中文
let typeText = '';
switch (q.type) {
case 'single': typeText = '单选题'; break;
case 'multiple': typeText = '多选题'; break;
case 'judgement': typeText = '判断题'; break;
case 'completion': typeText = '填空题'; break;
case 'short': typeText = '简答题'; break;
default: typeText = q.type;
}
// 限制题干显示长度,避免表格过宽
const maxQuestionLength = 100;
let questionText = q.question;
if (questionText.length > maxQuestionLength) {
questionText = questionText.substring(0, maxQuestionLength) + '...';
}
html += `<tr>
<td style="border:1px solid #eee;padding:6px 8px;">${questionText}</td>
<td style="border:1px solid #eee;padding:6px 8px;${typeClass}">${typeText}</td>
<td style="border:1px solid #eee;padding:6px 8px;">${q.options || ''}</td>
<td style="border:1px solid #eee;padding:6px 8px;">${q.answer}</td>
</tr>`;
});
html += '</table></div>';
html += `<div style="color:#888;font-size:13px;margin-top:10px;">共采集到 <b>${questions.length}</b> 道题目</div>`;
// 高效处理所有题目
questions.forEach(q => {
// 根据题目类型显示不同的标签颜色
// 获取题型信息
if (typeText) {
questionPrefix += typeText + ' ';
}
// 题干(合并所有非空p,防止空p导致题干丢失)
// 使用数组存储并过滤空元素,提高性能
const paragraphs = stemElement.find('p').map(function () {
return $(this).html().trim();
}).get().filter(Boolean);
// 将所有段落合并为题干内容
let questionContent = paragraphs.join(' ');
// 如果题干开头包含序号和题型前缀,尝试去除
// 匹配序号和题型前缀,如"61. (判断题,1分)"
// 保存原始内容用于日志记录
const originalContent = questionContent;
// 先尝试去除常见的序号+题型前缀格式
questionContent = questionContent.replace(/^\s*\d+\.?\s*[\(\uff08][^\)\uff09]+[\)\uff09]\s*/i, '');
// 再尝试去除可能的其他格式前缀
questionContent = questionContent.replace(/^\s*\d+\.?\s*/i, ''); // 去除只有序号的前缀
questionContent = questionContent.replace(/^\s*[\(\uff08][^\)\uff09]+[\)\uff09]\s*/i, ''); // 去除只有括号的前缀
// 去除可能的空格
questionContent = questionContent.trim();
// 如果内容发生了变化,记录日志
if (originalContent !== questionContent) {
Logger.log(`题目前缀去除: 原始="${originalContent.substring(0, 50)}..." → 处理后="${questionContent.substring(0, 50)}..."`);
}
// 组合前缀和处理后的题干
const question = questionPrefix + questionContent;
Logger.log(`提取题干: ${questionContent.substring(0, 50)}...`);
// 选项处理
const $answerBlock = stemElement.next('.stem_answer');
const options = [];
const optionMap = {};
// 只有选择题才处理选项
if ((type === 'single' || type === 'multiple') && $answerBlock.length) {
// 使用缓存的jQuery对象减少DOM查询
const optionElements = $answerBlock.find('.num_option').get();
for (let j = 0; j < optionElements.length; j++) {
const optionElement = $(optionElements[j]);
const letter = optionElement.text().replace(/[..、]/, '').trim();
// 使用更精确的选择器获取选项内容
let content = optionElement.next('.answer_p').html() || optionElement.next().html();
content = content ? content.trim() : (optionElement.next('.answer_p').text().trim() || optionElement.next().text().trim());
options.push(letter + '.' + content);
optionMap[letter] = content;
}
}
// 答案处理:根据题型不同,获取答案区域
let $answerDiv;
if (type === 'single' || type === 'multiple') {
// 选择题:取 .stem_answer 的下一个兄弟 .answerDiv
$answerDiv = $answerBlock.length ? $answerBlock.next('.answerDiv') : $();
} else {
// 非选择题:直接从题干区域获取下一个兄弟 .answerDiv
$answerDiv = stemElement.next('.stem_answer').next('.answerDiv');
if (!$answerDiv.length) {
$answerDiv = stemElement.next('.answerDiv');
}
}
// 根据题型提取答案
let answer = '';
if (type === 'single' || type === 'multiple') {
// 选择题答案处理
const ansText = $answerDiv.find('.answer_tit p').text().trim().replace(/[^A-Z]/ig, '');
const ansArr = ansText.split('');
const ansContentArr = ansArr.map(l => optionMap[l] || l);
answer = ansContentArr.join(';');
} else if (type === 'completion') {
// 填空题答案处理
const blanks = [];
// 尝试多种选择器获取填空题答案
const blankElements = $answerDiv.find('.tiankong_con .ans-wid-cRight p').get();
if (blankElements.length) {
for (let k = 0; k < blankElements.length; k++) {
const blankElement = $(blankElements[k]);
let v = blankElement.html();
v = v ? v.trim() : blankElement.text().trim();
if (v) blanks.push(v);
}
} else {
// 备用选择器
const paragraphElements = $answerDiv.find('p').get();
for (let k = 0; k < paragraphElements.length; k++) {
const paragraphElement = $(paragraphElements[k]);
let v = paragraphElement.html();
v = v ? v.trim() : paragraphElement.text().trim();
if (v) blanks.push(v);
}
}
answer = blanks.join(';');
} else if (type === 'judgement') {
// 判断题答案处理
answer = $answerDiv.find('.answer_tit p').text().trim();
if (!answer) {
answer = $answerDiv.find('p').text().trim();
}
// 标准化判断题答案
if (/^(对|正确|true|√)$/i.test(answer)) answer = '正确';
if (/^(错|错误|false|×)$/i.test(answer)) answer = '错误';
} else if (type === 'short') {
// 简答题答案处理
const ansArr = [];
const shortAnswerElements = $answerDiv.find('.ans-wid-cRight p').get();
if (shortAnswerElements.length) {
for (let k = 0; k < shortAnswerElements.length; k++) {
const shortAnswerElement = $(shortAnswerElements[k]);
let v = shortAnswerElement.html();
v = v ? v.trim() : shortAnswerElement.text().trim();
if (v) ansArr.push(v);
}
}
if (!ansArr.length) {
// 尝试获取HTML内容
let v = $answerDiv.find('.ans-wid-cRight').html();
v = v ? v.trim() : $answerDiv.find('.ans-wid-cRight').text().trim();
if (v) ansArr.push(v);
}
answer = ansArr.filter(Boolean).join('\n');
} else {
// 其他类型题目的答案处理
const otherAnswerElements = $answerDiv.find('p').get();
const otherAnswers = [];
for (let k = 0; k < otherAnswerElements.length; k++) {
const otherAnswerElement = $(otherAnswerElements[k]);
let v = otherAnswerElement.html();
v = v ? v.trim() : otherAnswerElement.text().trim();
if (v) otherAnswers.push(v);
}
answer = otherAnswers.filter(Boolean).join(';');
}
// 根据题型构建题目对象
if (type === 'single' || type === 'multiple') {
questions.push({
question,
type,
options: options.join(';'),
answer
});
} else {
questions.push({
question,
type,
answer
});
}
return questions;
})
}
})();