// ==UserScript== // @name 学习通题目 万能提取器 // @namespace http://tampermonkey.net/ // @version 3.0.2 // @description UI全面优化,保留所有核心功能:新增导出docx功能。 // @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 *://*.mooc1-api.chaoxing.com/mooc-ans/mooc2/work* // @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'; /* ---------- 核心逻辑 Helpers (保持不变) ---------- */ function normalizeText(s){ if(!s) return ''; return String(s).replace(/\u00A0/g,' ').replace(/\r?\n/g,' ').replace(/\s+/g,' ').trim(); } function getCleanText(el){ if(!el) return ''; const c=el.cloneNode(true); c.querySelectorAll('script,style').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*([0-9]+(?:\.[0-9]+)?)\s*分[\)\)]?\s*(.*)$/); if(m){ return { score: m[1] + ' 分', rest: (m[2]||'').trim() }; } return { score: '', rest: s }; } /* ---------- 页面标题提取 ---------- */ function getPageTitle(){ const selectors=['#workTitle','.work_title','.courseName','.course-title','.mark_title','.title','h1','h2','.fanyaMarking_left.whiteBg','.detailsHead']; for(const sel of selectors){ try{ const el=document.querySelector(sel); if(el){ const t=normalizeText(el.innerText||el.textContent||''); if(t && t.length>2 && !/error|404|登录|未找到/i.test(t)) return t.split(/\r?\n/)[0]; } }catch(e){} } const t=normalizeText(document.title||''); if(t && t.length>2) return t; return '试卷'; } /* ---------- 题目解析逻辑 ---------- */ 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')) { if(!found.includes(u)) found.push(u); } }); return found; } function extractCorrectAnswerFromNode(qNode){ if(!qNode) return null; const selectors=['.rightAnswerContent','.right_answer','.answerRight','.Py_answer','.answer-content','.answer']; const collected = []; selectors.forEach(s=>{ qNode.querySelectorAll(s).forEach(el=>{ const t=getCleanText(el); if(t && !/未提取|未作答|暂无/i.test(t)) collected.push(t.replace(/\s*[\r\n]+\s*/g,' ')); }); }); if(collected.length>0) return collected.join(' '); const inline = Array.from(qNode.querySelectorAll('p,div,span,li,td')); for(const el of inline){ const t=(el.textContent||'').trim(); if(!t) continue; if(/(?:正确答案|参考答案|答案)[::]/.test(t) && !/我的答案/i.test(t)){ const m=t.match(/(?:正确答案|参考答案|答案)[::]?\s*([\s\S]+)/); if(m && m[1]) return m[1].replace(/\s*[\r\n]+\s*/g,' ').trim(); } } const optContainers = findOptionContainers(qNode); for(const cont of optContainers){ const liList = Array.from(cont.querySelectorAll('li')); for(const li of liList){ const inner = li.innerText || ''; if(/√|正确|selected|checked|icon-ok|dui|check_answer_right/.test(inner)){ const label = (li.querySelector('.mark_letter_span')?.innerText || li.querySelector('i.fl')?.innerText || '').replace(/[^A-Za-z0-9\u4e00-\u9fa5]/g,'').trim(); if(label) return label; return normalizeText(li.textContent || li.innerText || ''); } } } const hidden = qNode.querySelector('.element-invisible-hidden, .Py_answer span.element-invisible-hidden'); if(hidden){ const t = normalizeText(hidden.innerText||hidden.textContent||''); if(t) return t; } 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(' '); if(qType === '判断题'){ if(/^(对|正确|true)$/i.test(s)) return '对'; if(/^(错|错误|false)$/i.test(s)) return '错'; if(/对|正确|true/i.test(s)) return '对'; if(/错|错误|false/i.test(s)) return '错'; return s; } if(qType === '多选题'){ const letters = s.match(/[A-D]/gi); if(letters){ return Array.from(new Set(letters.map(x=>x.toUpperCase()))).sort().join(''); } return s; } let m = s.match(/\b([A-D])\b/i); if(m && m[1]) return m[1].toUpperCase(); const patterns = [ /^\s*([A-Da-d])[\.\)\、\s]/, /[\s\(\[]([A-Da-d])[\.\)\、\s]/, /正确答案[::]?\s*([A-Da-d])/i, /答案[::]?\s*([A-Da-d])/i ]; for(const p of patterns){ m = s.match(p); if(m && m[1]) return m[1].toUpperCase(); } const letters = s.match(/[A-D]/gi); if(letters && letters.length>0){ return letters[0].toUpperCase(); } return s; } function parseQuestionNode(node){ try{ const titleEl = node.querySelector('.mark_name, .Cy_TItle, .Qtitle, .question-title, .marking-title') || node; let stemRaw = getCleanText(titleEl) || getCleanText(node); let stem = normalizeText(stemRaw).replace(/^\d+[.\、]\s*/,'').trim(); stem = stem.replace(/^[\(\(]?(?:单选题|多选题|判断题|填空题)[\)\)]?\s*/,''); stem = stem.replace(/[\s]*[\(\(](?:单选题|多选题|判断题|填空题)[\)\)][\s]*$/,''); 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')); if(lis.length === 0){ const children = Array.from(chosen.children).filter(ch => normalizeText(ch.innerText||'').length>0); children.forEach((ch, idx) => { let label = (ch.querySelector('.mark_letter_span')?.innerText || ch.querySelector('i.fl')?.innerText || '').replace(/[^A-Za-z0-9]/g,'').trim(); let clone = ch.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)); options.push({ key: label.toUpperCase(), text: text.replace(/^[A-D][\.\、\s]*/i,'').trim() }); }); } else { lis.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)); options.push({ key: label.toUpperCase(), text: text.replace(/^[A-D][\.\、\s]*/i,'').trim() }); }); } } } else { const immediateLis = Array.from(node.querySelectorAll(':scope > ul > li, :scope > ol > li')); immediateLis.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)); options.push({ key: label.toUpperCase(), text: text.replace(/^[A-D][\.\、\s]*/i,'').trim() }); }); } let answerRaw = extractCorrectAnswerFromNode(node) || ''; answerRaw = stripLeadingLabels(answerRaw); let answer = normalizeAnswerString(answerRaw || '', type); 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; } } 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 ''; } 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){ const candidates = Array.from(root.querySelectorAll('*')); candidates.forEach(n=>{ try{ if(!(n instanceof HTMLElement)) return; const textLen = (n.innerText||n.textContent||'').trim().length; const hasStem = !!n.querySelector('.mark_name, .Cy_TItle, .question-title, .Qtitle'); const hasOptions = !!n.querySelector('.mark_letter, .Cy_ulTop, .Zy_ulTop, .options, ul, ol'); if(hasStem || (textLen>12 && hasOptions)) nodeSet.add(n); }catch(e){} }); } const arr = Array.from(nodeSet); arr.sort((a,b)=> { if(a===b) return 0; const pos = a.compareDocumentPosition(b); if(pos & Node.DOCUMENT_POSITION_PRECEDING) return 1; if(pos & Node.DOCUMENT_POSITION_FOLLOWING) return -1; return 0; }); 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).slice(0,8).join('||')).slice(0,2000); if(seenFp.has(fp)) continue; seenFp.add(fp); parsedQuestions.push(q); } const sectionsMap = new Map(); 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); } const pageTitle = getPageTitle(); if(sectionsMap.size === 1 && sectionsMap.has('未分组')){ const allQs = parsedQuestions.slice(); sectionsMap.clear(); sectionsMap.set(pageTitle, allQs); } else { if(sectionsMap.size > 1){ const headersTop = Array.from(document.querySelectorAll('h1,h2,.detailsHead,.fanyaMarking_left')).filter(h => { try { return h.getBoundingClientRect && h.getBoundingClientRect().top < 240; } catch(e) { return false; } }); if(headersTop.length === 1){ const merged = []; for(const arrQ of sectionsMap.values()) merged.push(...arrQ); sectionsMap.clear(); sectionsMap.set(normalizeText(headersTop[0].innerText||headersTop[0].textContent||pageTitle), merged); } } } const sections = []; for(const [title, qlist] of sectionsMap.entries()){ let orderIndex = Infinity; const headers = Array.from(document.querySelectorAll('h1,h2,h3,.type_tit,.Cy_TItle1,.markTitle')); headers.forEach((h, idx) => { if(normalizeText(h.innerText||h.textContent||'') === normalizeText(title) && orderIndex===Infinity) orderIndex = idx; }); sections.push({ title, qlist, orderIndex }); } sections.sort((a,b) => { if(a.orderIndex === Infinity && b.orderIndex===Infinity) return 0; if(a.orderIndex === Infinity) return 1; if(b.orderIndex === Infinity) return -1; return a.orderIndex - b.orderIndex; }); 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); } if(paper.sections.length === 0){ const fallbackNodes = Array.from(document.querySelectorAll('.TiMu,.questionLi')); if(fallbackNodes.length>0){ const sec = { title: pageTitle || '题目', questions: [] }; for(const node of fallbackNodes){ const q = parseQuestionNode(node); if(q){ q.no = counter++; sec.questions.push(q); } } if(sec.questions.length) paper.sections.push(sec); } } return paper; } /* ---------- Word 生成 (无需变动逻辑) ---------- */ 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.trim()); if(analyses.length){ xml += `答案解析`; for(const q of analyses){ const scoreInfo = pullLeadingScore(q.stem); const headText = scoreInfo.score ? `${q.no}. (${scoreInfo.score}) ${scoreInfo.rest}` : `${q.no}. ${q.stem}`; xml += `${xmlEscape(headText)}`; xml += `${xmlEscape(stripLeadingLabels(q.analysis))}`; xml += ``; } } xml += ``; xml += ``; return xml; } /* ---------- 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; } 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); } /* ---------- UI & Main Flow (Optimized UI) ---------- */ 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){ // Overlay uiOverlay = document.createElement('div'); uiOverlay.id = 'cx-ui-overlay'; document.body.appendChild(uiOverlay); uiOverlay.onclick = closeUI; // Main Modal uiContainer = document.createElement('div'); uiContainer.id = 'cx-extractor-ui'; uiContainer.innerHTML = `
📝 提取结果预览
×
`; document.body.appendChild(uiContainer); // Event Bindings 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; // Show Animation uiOverlay.style.display = 'block'; // small delay to trigger transition 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); } } /* ---------- Modern CSS ---------- */ GM_addStyle(` /* Overlay */ #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; } /* Main Modal Center */ #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, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } #cx-extractor-ui.active { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); } /* Header */ .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; line-height: 1; color: #999; transition: color 0.2s; } .cx-close-icon:hover { color: #333; } /* Body */ .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', 'Monaco', monospace; font-size: 13px; color: #444; background: #fafafa; line-height: 1.6; resize: none; outline: none; box-sizing: border-box; } /* Footer */ .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; box-shadow: 0 4px 12px rgba(24,144,255,0.25); } .cx-btn-success { background: #52c41a; color: #fff; box-shadow: 0 4px 12px rgba(82,196,26,0.25); } .cx-btn-secondary { background: #f5f5f5; color: #666; border: 1px solid #e8e8e8; } .cx-btn:hover { filter: brightness(1.05); } /* Floating Button */ #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; transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); display: flex; align-items: center; justify-content: center; } #cx-float-btn:hover { transform: scale(1.1) rotate(10deg); } /* Notification */ #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: -apple-system, 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); }); })();