// ==UserScript== // @name 学习通万能题目 提取器 // @namespace http://tampermonkey.net/ // @version 5.0.0 // @description 自动提取学习通作业/考试/章节测验题目并一键导出Word。智能识别试卷标题,支持参考答案表格与解析汇总生成。 // @author kkkxfr // @match *://*.chaoxing.com/exam-ans/exam/test/reVersionPaperMarkContentNew* // @match *://*.chaoxing.com/exam-ans/exam/test/look* // @match *://*.chaoxing.com/mooc-ans/work/selectWorkQuestionYiPiYue* // @match *://*.mooc1.chaoxing.com/mooc-ans/mooc2/work/view?* // @match *://*.chaoxing.com/work/doHomeWorkNew* // @match *://*.mooc-ans/work/doHomeWorkNew* // @match *://*.chaoxing.com/api/selectWorkQuestionYiPiYue* // @grant GM_setClipboard // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // ==/UserScript== (function(){ 'use strict'; const AUTO_EXTRACT_KEY = 'chaoxing_auto_extract_enabled'; /* ---------- 1. 基础辅助函数 ---------- */ function normalizeText(s){ if(!s) return ''; return String(s).replace(/\u00A0/g,' ').replace(/\r?\n/g,' ').replace(/\s+/g,' ').trim(); } // 【核心修复】:将 .rightAnswerContent 从移除列表中剔除! // 之前的版本这里误删了答案容器,导致第一步提取失败 function getCleanText(el){ if(!el) return ''; const c = el.cloneNode(true); // 仅移除无关元素,保留答案类名 c.querySelectorAll('script, style, .mark_letter, .Cy_ulTop').forEach(x => x.remove()); let html = c.innerHTML || ''; html = html.replace(/]*>/gi, '\n').replace(/]*>/gi, '\n'); const tmp = document.createElement('div'); tmp.innerHTML = html; return normalizeText(tmp.textContent || tmp.innerText || ''); } function xmlEscape(s){ if(!s) return ''; return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function stripLeadingLabels(s){ if(!s) return ''; let t = normalizeText(s); while(/^(?:答案解析|正确答案|参考答案|答案|解析)[::\s]*/i.test(t)){ t = t.replace(/^(?:答案解析|正确答案|参考答案|答案|解析)[::\s]*/i, ''); } return t.trim(); } function pullLeadingScore(stem){ if(!stem) return {score:'', rest: ''}; const s = stem.trim(); const m = s.match(/^[,,]?\s*[\((]?\s*([0-9]+(?:\.[0-9]+)?)\s*分\s*[\)\)]?\s*(.*)$/); if(m){ return { score: m[1] + ' 分', rest: (m[2]||'').trim() }; } return { score: '', rest: s }; } /* ---------- 2. 页面标题提取 ---------- */ function getPageTitle(){ const selectors=[ '#workTitle', '.work_title', 'h3.mark_title', '.courseName', '.course-title', '.mark_title', '.title', '.detailsHead', 'h1', 'h2', 'h3', '.fanyaMarking_left.whiteBg' ]; for(const sel of selectors){ try{ const els = document.querySelectorAll(sel); for (const el of els) { const t = normalizeText(el.innerText||el.textContent||''); if(t && t.length > 2 && !/^(?:error|404|登录|未找到|提示|查看已批阅|作业详情)$/i.test(t) && !/[((]\s*(?:单选|多选|判断|填空)/.test(t) && !/^[0-9]+[\.、]\s/.test(t) ) { return t.split(/\r?\n/)[0]; } } } catch(e){} } const docTitle = normalizeText(document.title||''); if(docTitle && docTitle.length > 2 && !/查看已批阅|作业详情/i.test(docTitle)) return docTitle; return '试卷'; } /* ---------- 3. 题目与选项定位 ---------- */ function findOptionContainers(qNode){ const sels = ['.mark_letter','.Cy_ulTop','.Zy_ulTop','.options','.option-list','.optionsList']; const found = []; for(const s of sels){ const el = qNode.querySelector(s); if(el) found.push(el); } const uls = Array.from(qNode.querySelectorAll('ul,ol')); uls.forEach(u=>{ if((u.className && /ul|options|Cy_ulTop|Zy_ulTop|mark_letter/.test(u.className)) || (u.querySelector('li') && !u.textContent.includes('我的答案'))) { if(!found.includes(u)) found.push(u); } }); return found; } /* ---------- 4. 答案提取逻辑 (修复核心 v3.7.2) ---------- */ function extractCorrectAnswerFromNode(qNode){ if(!qNode) return null; // A. 优先尝试标准答案容器 // 新增 .mark_key 和 .mark_answer 以匹配你提供的 HTML 结构 const selectors=[ '.rightAnswerContent', // 最精准,直接就是答案 '.mark_key', // 包含 "我的答案:A 正确答案:C" '.mark_answer', // 更外层 '.right_answer', '.answerRight', '.Py_answer', '.answer-content', '.answer' ]; for (const s of selectors) { const els = qNode.querySelectorAll(s); for (const el of els) { // 策略 A1: 如果元素本身就是 .rightAnswerContent,直接取值 if (el.classList.contains('rightAnswerContent') || el.querySelector('.rightAnswerContent')) { // 如果内部还有 .rightAnswerContent,优先取内部的 const inner = el.querySelector('.rightAnswerContent') || el; const t = getCleanText(inner); if(t && /[A-D对错√×]/.test(t)) return t.replace(/\s+/g,''); } // 策略 A2: 分析容器文本 (例如 .mark_key) const t = getCleanText(el); if (t && !/未提取|未作答|暂无/.test(t)) { // 尝试从文本中解析 "正确答案: C" const match = t.match(/(?:正确答案|参考答案|答案)[::\s]*([A-Da-d对错√×]+)/i); if (match) return match[1].trim(); // 如果文本很短且像答案,直接返回 if (/^[A-D]+$/.test(t.replace(/\s/g,''))) return t.replace(/\s/g,''); } } } // B. “手术刀”策略:兜底提取 let clone = qNode.cloneNode(true); clone.querySelectorAll('ul, ol, .mark_letter, .Cy_ulTop, .options, .option-list').forEach(el => el.remove()); clone.querySelectorAll('.mark_name, .Cy_TItle, .Qtitle, h3').forEach(el => el.remove()); const remainingText = normalizeText(clone.innerText || clone.textContent || ''); // 正则:优先找“正确答案”,找不到再找“我的答案” // 你的 HTML 里正确答案在我的答案后面,所以这个正则应该能匹配到 const exactMatch = remainingText.match(/(?:正确答案|参考答案|答案)[::\s]*([A-Da-d对错√×]+)(?![^A-D\.])/i); if(exactMatch) return exactMatch[1].trim(); // 找不到正确答案,退而求其次找“我的答案” const myMatch = remainingText.match(/(?:我的答案|My Answer)[::\s]*([A-Da-d对错√×]+)/i); if(myMatch) return myMatch[1].trim(); return null; } function normalizeAnswerString(s, qType){ if(!s) return ''; s = normalizeText(s); const blanks = s.match(/(?:\(\s*\d+\s*\)|(\s*\d+\s*))[\s\S]*?(?=(?:\(\s*\d+\s*\)|(\s*\d+\s*)|$))/g); if(blanks && blanks.length > 1) return blanks.map(b => normalizeText(b)).join(' '); const letters = s.match(/[A-D]/gi); if(letters){ const upper = letters.map(x => x.toUpperCase()); const uniq = Array.from(new Set(upper)); // 单选题防误判 if((qType === '单选题' || qType === '单选') && uniq.length > 1) { const strictMatch = s.match(/(?:正确答案|答案)[::\s]*([A-D])/i); if(strictMatch) return strictMatch[1].toUpperCase(); // 如果无法确定,且有3个以上选项,返回空;否则(如AB)保留 if(uniq.length > 2) return ''; return uniq.sort().join(''); } return uniq.sort().join(''); } if(/^(对|正确|true|√)$/i.test(s)) return '对'; if(/^(错|错误|false|×)$/i.test(s)) return '错'; return s; } /* ---------- 5. 题目解析 ---------- */ function parseQuestionNode(node){ try{ const titleEl = node.querySelector('.mark_name, .Cy_TItle, .Qtitle, .question-title, .marking-title') || node; let stemRaw = ''; if(titleEl === node) { let clone = node.cloneNode(true); clone.querySelectorAll('.mark_letter, .Cy_ulTop, .Zy_ulTop, .options, ul, ol, .Py_answer').forEach(el => el.remove()); stemRaw = getCleanText(clone); } else { stemRaw = getCleanText(titleEl); } let stem = normalizeText(stemRaw); stem = stem.replace(/(我的答案|My Answer)[::\s]*.*$/i, ''); stem = stem.replace(/^\d+[\.\、\s]*【.*?】\s*/, ''); stem = stem.replace(/^\d+[\.\、]\s*/, ''); stem = stem.replace(/^[((【\[]\s*(?:单选题|多选题|判断题|填空题|单选|多选|判断|填空)[^))】\]]*[))】\]]\s*/, ''); stem = stem.trim(); const wholeTxt = (node.innerText||node.textContent||'').toLowerCase(); let type = '单选题'; if(/多选|multiple|multi/i.test(wholeTxt)) type = '多选题'; else if(/判断|是非|对错|true|false/i.test(wholeTxt)) type = '判断题'; else if(/填空|空格|空/i.test(wholeTxt)) type = '填空题'; else { const dt = node.getAttribute && (node.getAttribute('data-type') || node.getAttribute('qtype') || ''); if(dt && /multi|checkbox|多选/i.test(dt)) type = '多选题'; } const options = []; const containers = findOptionContainers(node); if(containers.length > 0){ let chosen = containers.find(c => c.querySelector && c.querySelector('li')); if(!chosen) chosen = containers[0]; if(chosen){ const lis = Array.from(chosen.querySelectorAll('li')); const items = lis.length ? lis : Array.from(chosen.children); items.forEach((li, idx)=>{ let label = (li.querySelector('.mark_letter_span')?.innerText || li.querySelector('i.fl')?.innerText || '').replace(/[^A-Za-z0-9]/g,'').trim(); let clone = li.cloneNode(true); if(clone.querySelector('.mark_letter_span')) clone.querySelector('.mark_letter_span').remove(); if(clone.querySelector('i.fl')) clone.querySelector('i.fl').remove(); let text = normalizeText(clone.textContent || clone.innerText || ''); const m = text.match(/^[\(\[]?\s*([A-Da-d])[\.\、\)\]\s]+/); if(!label && m){ label = m[1].toUpperCase(); text = text.replace(/^[\(\[]?\s*([A-Da-d])[\.\、\)\]\s]+/,'').trim(); } if(!label) label = String.fromCharCode(65 + (idx % 26)); if(text.length > 0 && text !== '查看解析') { options.push({ key: label.toUpperCase(), text: text }); } }); } } let answerRaw = extractCorrectAnswerFromNode(node) || ''; let answer = normalizeAnswerString(answerRaw, type); answer = stripLeadingLabels(answer); const analysisEl = node.querySelector('.analysisDiv, .analysis, .py_analyse, .Py_addpy .pingyu, .explain, .analysisTxt'); let analysis = analysisEl ? getCleanText(analysisEl) : ''; analysis = stripLeadingLabels(analysis); const sectionTitle = findPrevSectionTitle(node) || ''; return { type, stem, options, answer, analysis, sectionTitle }; } catch(e){ console.error('parseQuestionNode error', e); return null; } } /* ---------- 6. 章节标题查找 ---------- */ function findPrevSectionTitle(node){ const headerSels=['.type_tit','.Cy_TItle1','h2','.markTitle','.typeTitle','.mark_section','.section-title']; let cur=node; for(let i=0;i<15;i++){ if(!cur) break; let ps=cur.previousElementSibling; for(let k=0;k<30 && ps;k++){ for(const sel of headerSels) { if(ps.matches && ps.matches(sel)) { const t = normalizeText(ps.innerText||ps.textContent||''); if(t && !/^\d+[.、]/.test(t) && !/(\d+.*分)/.test(t) && !/\(\d+.*分\)/.test(t)) return t; } } ps=ps.previousElementSibling; } cur=cur.parentElement; } const headers = Array.from(document.querySelectorAll('h1,h2,h3,.type_tit,.Cy_TItle1,.markTitle')); let candidate = null; for(const h of headers){ if(h.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING) break; const t = normalizeText(h.innerText||h.textContent||''); if(t && !/^\d+[.、]/.test(t)) candidate = t; } if(candidate) return candidate; return ''; } /* ---------- 7. 构建试卷对象 ---------- */ function buildStructuredPaperFromDOM(){ const root = document.querySelector('.fanyaMarking') || document.querySelector('.mark_table') || document.body; const selectors = ['.questionLi','.TiMu','.question-item','.mark_question','.exam-question','.paper-question','.Ques','.questionBox']; const nodeSet = new Set(); selectors.forEach(sel=> document.querySelectorAll(sel).forEach(n=> { if(n && root.contains(n)) nodeSet.add(n); })); if(nodeSet.size === 0){ Array.from(root.querySelectorAll('*')).forEach(n=>{ try{ if(!(n instanceof HTMLElement)) return; const len = (n.innerText||'').length; const hasStem = !!n.querySelector('.mark_name, .Cy_TItle'); if(hasStem || (len>12 && n.querySelector('ul') && n.innerText.includes('A'))) nodeSet.add(n); }catch(e){} }); } const arr = Array.from(nodeSet).sort((a,b)=> (a.compareDocumentPosition(b) & 2) ? 1 : -1); const parsedQuestions = []; const seenFp = new Set(); for(const node of arr){ const q = parseQuestionNode(node); if(!q) continue; const fp = (q.stem+'||'+ (q.options||[]).map(o=>o.text).join('||')).slice(0,2000); if(seenFp.has(fp)) continue; seenFp.add(fp); parsedQuestions.push(q); } const sectionsMap = new Map(); const pageTitle = getPageTitle(); for(const q of parsedQuestions){ const key = q.sectionTitle && q.sectionTitle.trim() ? q.sectionTitle.trim() : '未分组'; if(!sectionsMap.has(key)) sectionsMap.set(key, []); sectionsMap.get(key).push(q); } if(sectionsMap.size === 1 && sectionsMap.has('未分组')){ sectionsMap.set(pageTitle, sectionsMap.get('未分组')); sectionsMap.delete('未分组'); } const sections = []; for(const [title, qlist] of sectionsMap.entries()){ sections.push({ title, qlist }); } let counter = 1; const paper = { title: pageTitle, sections: [] }; for(const s of sections){ const sec = { title: s.title, questions: [] }; for(const q of s.qlist){ q.no = counter++; sec.questions.push(q); } paper.sections.push(sec); } return paper; } /* ---------- 8. DOCX 生成核心 ---------- */ function buildContentTypes(){ return ''; } function buildRels(){ return ''; } function buildStyles(){ return ''; } function buildDocumentXmlFromPaper(paper){ const questionsFlat = []; for(const s of paper.sections) for(const q of s.questions) questionsFlat.push(q); const totalCount = questionsFlat.length; let xml=''; xml += `${xmlEscape(paper.title)}`; xml += `${xmlEscape('(共 ' + totalCount + ' 题)')}`; xml += ``; for(const sec of paper.sections){ if(normalizeText(sec.title) !== normalizeText(paper.title) && !/未分组|题目|^$|^\d+[.、]/.test(sec.title)){ xml += `${xmlEscape(sec.title)}`; } for(const q of sec.questions){ const scoreInfo = pullLeadingScore(q.stem); let stemBody = scoreInfo.score ? scoreInfo.rest : q.stem; stemBody = (stemBody||'').trim(); const heading = scoreInfo.score ? `${q.no}. (${q.type} , ${scoreInfo.score}) ${stemBody}` : `${q.no}. (${q.type}) ${stemBody || q.stem}`; xml += `${xmlEscape(heading)}`; if(q.options && q.options.length){ for(let i=0;i0) ? op.key : String.fromCharCode(65 + (i % 26)); const text = op.text || ''; xml += `${xmlEscape(label + '. ' + text)}`; } } xml += ``; } } xml += `参考答案`; const cols=8; const rows=Math.ceil(Math.max(questionsFlat.length,1)/cols); xml += ``; let idx=0; for(let r=0;r`; for(let c=0;c${xmlEscape(txt)}`; idx++; } xml += ``; } xml += ``; const analyses = questionsFlat.filter(q=> q.analysis && q.analysis.length > 1); if(analyses.length > 0){ xml += `答案解析汇总`; for(const q of analyses){ xml += `${xmlEscape('第' + q.no + '题:')}${xmlEscape(stripLeadingLabels(q.analysis))}`; xml += ``; } } xml += ``; xml += ``; return xml; } /* ---------- 9. ZIP 打包与导出 ---------- */ function strToUint8(s){ return new TextEncoder().encode(s); } function uint32ToLE(n){ return [n & 0xff, (n>>>8)&0xff, (n>>>16)&0xff, (n>>>24)&0xff]; } function uint16ToLE(n){ return [n & 0xff, (n>>>8)&0xff]; } function crc32(buf){ const table = crc32.table || (crc32.table=(function(){ const t=new Uint32Array(256); for(let n=0;n<256;n++){ let c=n; for(let k=0;k<8;k++){ c=(c&1)?(0xEDB88320^(c>>>1)):(c>>>1); } t[n]=c>>>0; } return t; })()); let crc=0^(-1); for(let i=0;i>>8) ^ table[(crc ^ buf[i]) & 0xff]; return (crc ^ (-1))>>>0; } function buildZip(files){ const localEntries=[]; let offset=0; for(const f of files){ const nameBuf=strToUint8(f.name); const data=f.data; const crc=crc32(data); const compSize=data.length; const uncompSize=data.length; const localHeader=[...uint32ToLE(0x04034b50), ...uint16ToLE(20), ...uint16ToLE(0), ...uint16ToLE(0), ...uint16ToLE(0), ...uint16ToLE(0), ...uint32ToLE(crc), ...uint32ToLE(compSize), ...uint32ToLE(uncompSize), ...uint16ToLE(nameBuf.length), ...uint16ToLE(0)]; const headerBuf=new Uint8Array(localHeader.length + nameBuf.length + data.length); headerBuf.set(new Uint8Array(localHeader),0); headerBuf.set(nameBuf, localHeader.length); headerBuf.set(data, localHeader.length + nameBuf.length); localEntries.push({ name: f.name, headerBuf, crc, compSize, uncompSize, offset }); offset += headerBuf.length; } const cdParts=[]; let cdSize=0; for(const e of localEntries){ const nameBuf=strToUint8(e.name); const cdr=[...uint32ToLE(0x02014b50), ...uint16ToLE(0x0314), ...uint16ToLE(20), ...uint16ToLE(0), ...uint16ToLE(0), ...uint16ToLE(0), ...uint16ToLE(0), ...uint32ToLE(e.crc), ...uint32ToLE(e.compSize), ...uint32ToLE(e.uncompSize), ...uint16ToLE(nameBuf.length), ...uint16ToLE(0), ...uint16ToLE(0), ...uint16ToLE(0), ...uint16ToLE(0), ...uint32ToLE(0), ...uint32ToLE(e.offset)]; const cdbuf=new Uint8Array(cdr.length + nameBuf.length); cdbuf.set(new Uint8Array(cdr),0); cdbuf.set(nameBuf, cdr.length); cdParts.push(cdbuf); cdSize += cdbuf.length; } const total = offset + cdSize + 22; const out=new Uint8Array(total); let pos=0; for(const e of localEntries){ out.set(e.headerBuf, pos); pos += e.headerBuf.length; } for(const c of cdParts){ out.set(c, pos); pos += c.length; } const eocd=[...uint32ToLE(0x06054b50), ...uint16ToLE(0), ...uint16ToLE(0), ...uint16ToLE(localEntries.length), ...uint16ToLE(localEntries.length), ...uint32ToLE(cdSize), ...uint32ToLE(offset), ...uint16ToLE(0)]; out.set(new Uint8Array(eocd), pos); return out; } /* ---------- 10. 交互与UI ---------- */ function exportPaperToDocx(paper){ let n=1; for(const s of paper.sections) for(const q of s.questions) if(!q.no) q.no = n++; const docXml = buildDocumentXmlFromPaper(paper); const files=[ { name:'[Content_Types].xml', data: strToUint8(buildContentTypes()) }, { name:'_rels/.rels', data: strToUint8(buildRels()) }, { name:'word/document.xml', data: strToUint8(docXml) }, { name:'word/styles.xml', data: strToUint8(buildStyles()) } ]; const zipBuf = buildZip(files); const blob = new Blob([zipBuf], { type:'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }); const a=document.createElement('a'); const safe = (paper.title || getPageTitle()).replace(/[\\\/:*?"<>|]+/g,'_'); a.href = URL.createObjectURL(blob); a.download = safe + '.docx'; document.body.appendChild(a); a.click(); setTimeout(()=>{ document.body.removeChild(a); URL.revokeObjectURL(a.href); }, 3000); } function structuredPaperToText(paper){ let out = `# ${paper.title}\n\n`; for(const sec of paper.sections){ if(sec.title && !/未分组|题目|^$|^\d+[.、]/.test(sec.title) && sec.title !== paper.title){ out += `## ${sec.title}(共 ${sec.questions.length} 题)\n\n`; } for(const q of sec.questions){ const scoreInfo = pullLeadingScore(q.stem); if(scoreInfo.score){ const rest = scoreInfo.rest || ''; out += `${q.no}. (${q.type} , ${scoreInfo.score}) ${rest}\n`; } else { out += `${q.no}. (${q.type}) ${q.stem}\n`; } if(q.options && q.options.length) for(let i=0;i0) ? op.key : String.fromCharCode(65 + (i % 26)); out += ` ${label}. ${op.text}\n`; } if(q.answer) out += `正确答案:${stripLeadingLabels(q.answer)}\n`; if(q.analysis) out += `答案解析:${stripLeadingLabels(q.analysis)}\n`; out += `\n`; } } return out; } function extractAndShow(isManual=false){ const paper = buildStructuredPaperFromDOM(); const total = paper.sections.reduce((s,sec)=>s + (sec.questions||[]).length, 0); if(total === 0){ if(isManual) showNotification('未找到题目 — 请确认页面已加载或切换到答题/查看页', true); return; } const text = structuredPaperToText(paper); GM_setClipboard(text); showUI(text, paper); showNotification(`已提取 ${total} 道题(复制到剪贴板)`); } let notificationElement = null; function showNotification(msg, isError=false){ if(!notificationElement){ notificationElement = document.createElement('div'); notificationElement.id='cx-notification-gm'; document.body.appendChild(notificationElement); } notificationElement.innerHTML = `${isError?'⚠️':'✅'} ${msg}`; notificationElement.className = isError ? 'error' : 'success'; notificationElement.style.display = 'flex'; notificationElement.style.animation = 'none'; notificationElement.offsetHeight; notificationElement.style.animation = 'cx-slide-in 0.3s forwards'; setTimeout(()=>{ notificationElement.style.animation = 'cx-slide-out 0.3s forwards'; setTimeout(()=>{ notificationElement.style.display='none'; },300); }, 2500); } /* ---------- New UI Construction ---------- */ let uiContainer = null; let uiOverlay = null; function showUI(text, paper){ if(!uiContainer){ uiOverlay = document.createElement('div'); uiOverlay.id = 'cx-ui-overlay'; document.body.appendChild(uiOverlay); uiOverlay.onclick = closeUI; uiContainer = document.createElement('div'); uiContainer.id = 'cx-extractor-ui'; uiContainer.innerHTML = `
📝 提取结果预览
×
`; document.body.appendChild(uiContainer); document.getElementById('cx-close-icon').onclick = closeUI; document.getElementById('cx-close').onclick = closeUI; document.getElementById('cx-copy').onclick = () => { const ta = document.getElementById('cx-extractor-output'); ta.select(); GM_setClipboard(ta.value); showNotification('已复制到剪贴板'); }; document.getElementById('cx-export').onclick = () => exportPaperToDocx(paper); } const ta = document.getElementById('cx-extractor-output'); if(ta) ta.value = text; uiOverlay.style.display = 'block'; requestAnimationFrame(() => { uiOverlay.style.opacity = '1'; uiContainer.classList.add('active'); }); } function closeUI(){ if(!uiContainer) return; uiOverlay.style.opacity = '0'; uiContainer.classList.remove('active'); setTimeout(() => { uiOverlay.style.display = 'none'; }, 300); } function createFloatingButton(){ if(document.getElementById('cx-float-btn')) return; const btn = document.createElement('button'); btn.id = 'cx-float-btn'; btn.innerHTML = '📝'; btn.title = '提取题目'; btn.onclick = () => extractAndShow(true); document.body.appendChild(btn); } function toggleAuto(){ const v = !GM_getValue(AUTO_EXTRACT_KEY,false); GM_setValue(AUTO_EXTRACT_KEY,v); alert(`自动提取已${v?'开启':'关闭'},刷新页面生效。`); updateMenu(); } function updateMenu(){ const isAuto = GM_getValue(AUTO_EXTRACT_KEY,false); try{ GM_registerMenuCommand(`${isAuto?'✅':'❌'} 自动提取开关`, toggleAuto); GM_registerMenuCommand('🖐 手动提取题目', ()=> extractAndShow(true)); }catch(e){ console.warn('GM_registerMenuCommand 可能不可用', e); } } GM_addStyle(` #cx-ui-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); z-index: 99997; backdrop-filter: blur(2px); display: none; opacity: 0; transition: opacity 0.3s; } #cx-extractor-ui { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.95); width: 600px; max-width: 90vw; max-height: 85vh; background: #fff; border-radius: 16px; box-shadow: 0 20px 60px rgba(0,0,0,0.2); z-index: 99998; display: flex; flex-direction: column; opacity: 0; pointer-events: none; transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1); font-family: -apple-system, sans-serif; } #cx-extractor-ui.active { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); } .cx-header { padding: 16px 24px; border-bottom: 1px solid #f0f0f0; display: flex; justify-content: space-between; align-items: center; background: #fff; border-radius: 16px 16px 0 0; } .cx-title { font-size: 18px; font-weight: 600; color: #333; display: flex; align-items: center; gap: 8px; } .cx-close-icon { font-size: 24px; cursor: pointer; color: #999; transition: color 0.2s; } .cx-close-icon:hover { color: #333; } .cx-body { flex: 1; padding: 0; overflow: hidden; display: flex; flex-direction: column; background: #fafafa; } #cx-extractor-output { width: 100%; height: 400px; padding: 20px; border: none; font-family: 'Consolas', monospace; font-size: 13px; color: #444; background: #fafafa; line-height: 1.6; resize: none; outline: none; box-sizing: border-box; } .cx-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; gap: 12px; background: #fff; border-radius: 0 0 16px 16px; } .cx-btn { flex: 1; padding: 12px 0; border: none; border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: transform 0.1s, filter 0.2s; display: flex; align-items: center; justify-content: center; gap: 6px; } .cx-btn:active { transform: scale(0.98); } .cx-btn-primary { background: #1890ff; color: #fff; } .cx-btn-success { background: #52c41a; color: #fff; } .cx-btn-secondary { background: #f5f5f5; color: #666; border: 1px solid #e8e8e8; } #cx-float-btn { position: fixed; top: 180px; right: 20px; z-index: 99999; width: 56px; height: 56px; border-radius: 50%; background: linear-gradient(135deg, #1890ff, #096dd9); color: #fff; border: none; font-size: 24px; box-shadow: 0 6px 20px rgba(24,144,255,0.4); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.3s; } #cx-float-btn:hover { transform: scale(1.1) rotate(10deg); } #cx-notification-gm { position: fixed; top: 30px; left: 50%; transform: translateX(-50%); padding: 12px 24px; border-radius: 30px; display: flex; align-items: center; gap: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); z-index: 100000; font-family: sans-serif; font-size: 14px; font-weight: 600; pointer-events: none; background: #fff; color: #333; border: 1px solid #eee; } #cx-notification-gm.success span { color: #52c41a; } #cx-notification-gm.error span { color: #ff4d4f; } @keyframes cx-slide-in { from { transform: translate(-50%, -20px); opacity: 0; } to { transform: translate(-50%, 0); opacity: 1; } } @keyframes cx-slide-out { from { transform: translate(-50%, 0); opacity: 1; } to { transform: translate(-50%, -20px); opacity: 0; } } `); updateMenu(); createFloatingButton(); window.addEventListener('load', ()=> { if(GM_getValue(AUTO_EXTRACT_KEY,false)) setTimeout(()=> extractAndShow(false), 1500); }); })();