// ==UserScript== // @name 右键加入墨墨背单词·阅读理解版(多域名·美化版) // @namespace maimemo.contextmenu // @version 2.0.0 // @description 任意网页右键单词一键加入墨墨待背列表;新增「阅读理解生成」:拉取今日词汇,自选/随机选词,对接 DeepSeek 生成考研风格阅读理解+译文+题目。原功能保持不变。 // @author you // @match *://*/* // @connect open.maimemo.com // @connect api.deepseek.com // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @run-at document-idle // @license MIT // ==/UserScript== (function () { 'use strict'; // ============================================================ // 一、配置(本地存储) // ============================================================ const CONFIG_KEY = 'mm_catchword_config'; const HISTORY_KEY = 'mm_catchword_history'; // 阅读理解历史(带上限) const DEFAULT_CONFIG = { token: '', // 墨墨开放 API 凭证(App: 我的->更多设置->实验功能->开放API) apiBase: 'https://open.maimemo.com/open', domains: '', // 生效域名白名单:每行一个,或用逗号/分号/空格分隔;留空=所有网站 enabled: true, // 总开关 advance: false, // 添加后是否一并提前复习 maxWords: 20, // 单次批量最大单词数(受频控 10秒20次 限制) showFab: true, // 是否显示右下角悬浮按钮 debug: false, // 调试日志开关 // ---- 阅读理解 / AI 生成 ---- deepseekApiKey: '', // DeepSeek API Key(仅存脚本本地) deepseekApiBase: 'https://api.deepseek.com', // DeepSeek 基址 deepseekModel: 'deepseek-chat', // 模型名,可改 deepseek-reasoner / deepseek-flash 等 storeHistory: true, // 是否保存生成历史 historyCap: 50, // 历史最大条数(约每条3-5KB,50条≈250KB,本地无压力) readingCount: 20, // 默认生成词数 readingDifficulty: '中' // 易 / 中 / 难 }; function loadConfig() { try { const s = GM_getValue(CONFIG_KEY, {}); return Object.assign({}, DEFAULT_CONFIG, s || {}); } catch (e) { return Object.assign({}, DEFAULT_CONFIG); } } function saveConfig(c) { GM_setValue(CONFIG_KEY, c); } let cfg = loadConfig(); // ============================================================ // 二、多域名白名单 // ============================================================ function parseDomains(str) { return (str || '').split(/[\s,;,;]+/) .map(s => s.trim().toLowerCase().replace(/^(\*\.)?/, '').replace(/:\d+$/, '')) .filter(Boolean); } function hostAllowed() { const list = parseDomains(cfg.domains); if (!list.length) return true; const host = location.hostname.toLowerCase(); return list.some(d => host === d || host.endsWith('.' + d)); } // ============================================================ // 三、请求封装(GM_xmlhttpRequest 解决跨域 + 调试日志) // token 传入:墨墨用 cfg.token,DeepSeek 用 cfg.deepseekApiKey // ============================================================ function request(method, url, body, token) { return new Promise((resolve) => { const tok = token || cfg.token; const headers = { 'Accept': 'application/json' }; if (tok) headers['Authorization'] = 'Bearer ' + tok.trim(); if (body) headers['Content-Type'] = 'application/json'; GM_xmlhttpRequest({ method: method, url: url, headers: headers, data: body ? JSON.stringify(body) : undefined, timeout: 30000, onload: (res) => { if (cfg.debug) { console.log('[墨墨] 请求', method, url); if (body) console.log('[墨墨] 请求体', JSON.stringify(body)); console.log('[墨墨] 状态码', res.status); console.log('[墨墨] 返回', res.responseText); } let json = null; try { json = JSON.parse(res.responseText); } catch (e) { /* not json */ } resolve({ status: res.status, json: json, raw: res.responseText }); }, onerror: (err) => { if (cfg.debug) console.error('[墨墨] 网络请求失败', err); resolve({ status: 0, json: null, raw: '' }); }, ontimeout: () => { if (cfg.debug) console.error('[墨墨] 请求超时', method, url); resolve({ status: 0, json: null, raw: '' }); } }); }); } // 从响应中提取词汇(兼容 data.voc / data / voc 三种结构) function pickVoc(json) { if (!json) return null; const d = json.data !== undefined ? json.data : json; if (d && d.voc !== undefined) return d.voc; return d || null; } // 1) 按拼写查词(官方 demo 用法:GET /vocabulary?spelling=) async function queryVocab(words) { const base = (cfg.apiBase || DEFAULT_CONFIG.apiBase).replace(/\/+$/, ''); const found = {}; const missing = []; for (const w of words) { const res = await request('GET', base + '/api/v1/memo/vocabulary?spelling=' + encodeURIComponent(w), null, cfg.token); if (res.status === 401 || res.status === 403) { return { err: 'TOKEN', found: {}, missing: words }; } if (res.status !== 200 || !res.json) { missing.push(w); continue; } const voc = pickVoc(res.json); if (voc && voc.id && voc.spelling) { found[voc.spelling] = voc; } else { missing.push(w); } } if (cfg.debug) console.log('[墨墨] 查词命中:', Object.keys(found), '未命中:', missing); return { err: null, found: found, missing: missing }; } // 2) 把单词 id 加入学习规划(待背);服务端自动去重,added_count 表示实际新增数 async function addWords(ids) { const base = (cfg.apiBase || DEFAULT_CONFIG.apiBase).replace(/\/+$/, ''); const body = { words: ids.map(id => ({ id: id })), advance: !!cfg.advance }; const res = await request('POST', base + '/api/v1/memo/study/add_words', body, cfg.token); if (res.status === 401 || res.status === 403) return { err: 'TOKEN', added: 0 }; if (res.status !== 200 || !res.json) return { err: 'ADD', added: 0 }; const d = res.json.data !== undefined ? res.json.data : res.json; return { err: null, added: (d && d.added_count) || 0 }; } // 3) 拉取今日学习单词列表(GET /study/get_today_items) // 返回结构有差异,这里做防御式解析:兼容 data.items / data.list / data / 元素内含 voc / spelling / voc_spelling function pickSpelling(it) { if (!it || typeof it !== 'object') return ''; const c = (v) => (typeof v === 'string' ? v : (v && v.spelling)); for (const key of ['voc_spelling', 'spelling', 'word', 'voc', 'vocabulary']) { const v = c(it[key]); if (v) return String(v).toLowerCase(); } return ''; } async function getTodayItems() { const base = (cfg.apiBase || DEFAULT_CONFIG.apiBase).replace(/\/+$/, ''); // const res = await request('GET', base + '/api/v1/memo/study/get_today_items', null, cfg.token); // ✅ POST,body 空对象 const res = await request('POST', base + '/api/v1/memo/study/get_today_items', {}); if (res.status === 401 || res.status === 403) return { err: 'TOKEN', words: [] }; if (res.status !== 200 || !res.json) return { err: 'FETCH', words: [] }; const d = res.json.data !== undefined ? res.json.data : res.json; let items = []; if (Array.isArray(d)) items = d; else if (d && Array.isArray(d.items)) items = d.items; else if (d && Array.isArray(d.list)) items = d.list; else if (d && Array.isArray(d.today_items)) items = d.today_items; const seen = new Set(), words = []; for (const it of items) { const sp = pickSpelling(it); if (sp && !seen.has(sp)) { seen.add(sp); words.push(sp); } } return { err: null, words: words }; } // ============================================================ // 四、浮动 toast(同原版) // ============================================================ let toastWrap = null; function toast(msg, type, ms) { type = type || 'info'; if (!toastWrap) { toastWrap = document.createElement('div'); toastWrap.style.cssText = 'position:fixed;top:18px;left:50%;transform:translateX(-50%);' + 'z-index:2147483647;display:flex;flex-direction:column;align-items:center;' + 'gap:8px;pointer-events:none;'; document.body.appendChild(toastWrap); } const style = ({ success: { bg: '#ecfdf5', color: '#065f46', border: '#34d399', icon: '✓' }, error: { bg: '#fef2f2', color: '#991b1b', border: '#f87171', icon: '✕' }, info: { bg: '#eff6ff', color: '#1e40af', border: '#93c5fd', icon: 'ℹ' }, warn: { bg: '#fffbeb', color: '#92400e', border: '#fcd34d', icon: '!' } })[type] || { bg: '#eff6ff', color: '#1e40af', border: '#93c5fd', icon: 'ℹ' }; const el = document.createElement('div'); el.style.cssText = 'display:flex;align-items:flex-start;gap:8px;max-width:520px;padding:10px 14px;' + 'border-radius:10px;background:' + style.bg + ';color:' + style.color + ';' + 'border:1px solid ' + style.border + ';box-shadow:0 8px 24px rgba(0,0,0,.12);' + 'font:13px/1.55 sans-serif;white-space:pre-wrap;word-break:break-word;' + 'opacity:0;transform:translateY(-14px);transition:all .28s ease;'; const icon = document.createElement('span'); icon.textContent = style.icon; icon.style.cssText = 'font-weight:700;flex-shrink:0;'; const span = document.createElement('span'); span.textContent = msg; el.appendChild(icon); el.appendChild(span); toastWrap.appendChild(el); requestAnimationFrame(() => { el.style.opacity = '1'; el.style.transform = 'translateY(0)'; }); const t = ms || 2800; setTimeout(() => { el.style.opacity = '0'; el.style.transform = 'translateY(-14px)'; setTimeout(() => el.remove(), 300); }, t); } // ============================================================ // 五、右键菜单(加入墨墨 / 阅读理解 / 配置) // ============================================================ let ctxEl = null; function removeCtx() { if (ctxEl) { ctxEl.remove(); ctxEl = null; } } function wordAtPoint(x, y) { try { let range = null; if (document.caretRangeFromPoint) range = document.caretRangeFromPoint(x, y); else if (document.caretPositionFromPoint) { const pos = document.caretPositionFromPoint(x, y); if (pos && pos.offsetNode) { range = document.createRange(); range.setStart(pos.offsetNode, pos.offset); } } if (!range || !range.startContainer) return ''; const node = range.startContainer; if (node.nodeType !== 3) return ''; const offset = range.startOffset; const text = node.textContent || ''; const left = text.slice(0, offset), right = text.slice(offset); const mL = left.match(/([A-Za-z]+(?:['’-][A-Za-z]+)*)$/); const mR = right.match(/^([A-Za-z]+(?:['’-][A-Za-z]+)*)/); return ((mL ? mL[1] : '') + (mR ? mR[1] : '')).toLowerCase().replace(/['’]/g, "'"); } catch (e) { return ''; } } function menuItem(text, onHover, onOut) { const it = document.createElement('div'); it.textContent = text; it.style.cssText = 'padding:10px 12px;cursor:pointer;display:flex;align-items:center;gap:6px;'; it.onmouseover = () => { it.style.background = onHover; }; it.onmouseout = () => { it.style.background = onOut; }; it.onmousedown = (e) => e.preventDefault(); return it; } function showCtx(x, y, word) { removeCtx(); ctxEl = document.createElement('div'); ctxEl.style.cssText = 'position:fixed;z-index:2147483646;min-width:200px;background:#fff;color:#1f2937;' + 'border:1px solid #e5e7eb;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.16);' + 'font:13px/1.4 sans-serif;overflow:hidden;user-select:none;'; const title = document.createElement('div'); title.textContent = '墨墨背单词'; title.style.cssText = 'padding:8px 12px;font-size:11px;color:#6b7280;background:#f9fafb;' + 'border-bottom:1px solid #f3f4f6;'; ctxEl.appendChild(title); const addItem = menuItem(word ? '📖 加入墨墨:' + word : '📖 加入墨墨(选中)', '#eff6ff', '#fff'); addItem.onclick = (e) => { e.stopPropagation(); removeCtx(); if (word) handleAdd([word]); else { const sel = window.getSelection() ? window.getSelection().toString() : ''; const ws = extractWords(sel).slice(0, cfg.maxWords); if (ws.length) handleAdd(ws); else toast('未识别到英文单词', 'warn'); } }; const readItem = menuItem('📝 阅读理解(今日词汇)', '#eff6ff', '#fff'); readItem.onclick = (e) => { e.stopPropagation(); removeCtx(); openReadingPanel(); }; const cfgItem = menuItem('⚙️ 配置', '#eff6ff', '#fff'); cfgItem.style.cssText = cfgItem.style.cssText + 'border-top:1px solid #f3f4f6;'; cfgItem.onclick = (e) => { e.stopPropagation(); removeCtx(); openPanel(); }; ctxEl.appendChild(addItem); ctxEl.appendChild(readItem); ctxEl.appendChild(cfgItem); const vw = document.documentElement.clientWidth, vh = document.documentElement.clientHeight; let px = x, py = y; if (px + 200 > vw) px = vw - 210; if (py + 150 > vh) py = vh - 160; if (px < 4) px = 4; if (py < 4) py = 4; ctxEl.style.left = px + 'px'; ctxEl.style.top = py + 'px'; document.body.appendChild(ctxEl); } function extractWords(text) { const raw = (text.match(/[A-Za-z]+(?:['’-][A-Za-z]+)*/g) || []); const seen = new Set(), out = []; for (const w of raw) { const k = w.toLowerCase().replace(/['’]/g, "'"); if (!seen.has(k)) { seen.add(k); out.push(k); } } return out; } async function handleAdd(words) { if (!cfg.token) { toast('未配置墨墨 Token,正在打开配置面板…', 'warn'); openPanel(); return; } if (!words.length) return; toast('正在查询 ' + words.slice(0, 5).join('、') + (words.length > 5 ? ' …' : ''), 'info'); const q = await queryVocab(words); if (q.err === 'TOKEN') { toast('❌ Token 无效或已过期,请到配置面板更新', 'error'); openPanel(); return; } const ids = Object.keys(q.found).map(sp => q.found[sp].id); const missing = q.missing; if (!ids.length) { toast('墨墨词库未收录:\n' + missing.join('、'), 'warn'); return; } const r = await addWords(ids); if (r.err === 'TOKEN') { toast('❌ Token 无效或已过期,请到配置面板更新', 'error'); openPanel(); return; } if (r.err === 'ADD') { toast('⚠️ 添加接口异常,请检查 API 地址或稍后重试', 'error'); return; } const added = r.added, existed = ids.length - added; const lines = []; lines.push('✅ 已新增 ' + added + ' 个待背单词'); if (existed > 0) lines.push('⏭️ ' + existed + ' 个已存在,自动跳过'); if (missing.length > 0) lines.push('❓ 词库未收录:' + missing.slice(0, 5).join('、') + (missing.length > 5 ? ' …' : '')); toast(lines.join('\n'), added > 0 ? 'success' : 'info', 3600); } // ============================================================ // 六、DeepSeek 对话(非流式 JSON,解析最稳) // base: https://api.deepseek.com POST /chat/completions // ============================================================ async function chatCompletion(messages, temperature) { if (!cfg.deepseekApiKey) return { err: 'NO_KEY', content: '' }; const base = (cfg.deepseekApiBase || 'https://api.deepseek.com').replace(/\/+$/, ''); const body = { model: cfg.deepseekModel || 'deepseek-chat', messages: messages, temperature: (temperature === undefined ? 0.7 : temperature), stream: false }; const res = await request('POST', base + '/chat/completions', body, cfg.deepseekApiKey); if (res.status === 401 || res.status === 403) return { err: 'KEY', content: '' }; if (res.status !== 200 || !res.json) { const msg = (res.json && res.json.error && res.json.error.message) || ('HTTP ' + res.status); return { err: msg, content: '' }; } const content = res.json.choices && res.json.choices[0] && res.json.choices[0].message ? (res.json.choices[0].message.content || '') : ''; return { err: null, content: content }; } // 难度 → 提示词 function difficultyDesc(d) { return ({ '易': '文章用词相对简单、句子结构清晰,主题贴近日常生活或科普常识,尽量让今天学的词自然出现。', '中': '标准考研英语(二)难度:词汇有一定学术性、含少量长难句,题材接近社科/科技/经济类。', '难': '接近考研英语(二)偏难篇章:长难句较多、逻辑复杂,用词更书面化、有一定抽象度。' })[d] || '标准考研英语(二)难度。'; } function passageLen(n) { if (n <= 20) return '约250-300词'; if (n <= 50) return '约400-500词'; return '约650-800词'; } // 构造生成提示词 function buildReadingPrompt(words, difficulty, count) { const wordStr = words.slice(0, count).join(', '); return [ '你是资深考研英语命题人。请根据下面给定的英文词汇,创作一篇考研英语(二)阅读理解风格的英文文章,并出4道阅读理解题。', '', '【词汇清单】', wordStr, '', '【要求】', '1. 文章长度:' + passageLen(count) + ',主题自选,语言地道自然。', '2. 尽量把清单里的高频词汇自然融入文章(不必逐一硬用,优先覆盖有实际表达价值的词,可作词性/单复数/时态变形)。', '3. 难度:' + difficultyDesc(difficulty), '4. 出4道题,题型尽量多样(主旨大意、事实细节、推理判断、词义猜测、作者态度等),每道题给出A/B/C/D四个选项,并附正确答案与中文简要解析。', '5. 同时给出整篇文章的中文翻译(translation字段),供学习者参考(学习者可自行对照翻译)。', '', '【输出】只输出一个合法JSON对象,不要任何额外文字、不要markdown代码块。结构如下:', '{', ' "title": "中文标题",', ' "passage": "英文正文(用\\n分段)",', ' "translation": "中文全文翻译",', ' "questions": [', ' {', ' "question": "题干",', ' "options": {"A":"...","B":"...","C":"...","D":"..."},', ' "answer": "A",', ' "explanation": "中文解析"', ' }', ' ]', '}' ].join('\n'); } // 解析 AI 返回的 JSON(容忍 ``` 包裹) function parseReadingJson(text) { if (!text) return null; let t = text.trim(); const fence = t.match(/```(?:json)?([\s\S]*?)```/); if (fence) t = fence[1].trim(); // 若开头不是 { ,尝试截取第一个 { 到最后一个 } if (t[0] !== '{') { const s = t.indexOf('{'), e = t.lastIndexOf('}'); if (s >= 0 && e > s) t = t.slice(s, e + 1); } try { const obj = JSON.parse(t); if (obj && (obj.passage || obj.questions)) return obj; return null; } catch (e) { if (cfg.debug) console.error('[墨墨] 解析AI返回失败', e, text); return null; } } // ============================================================ // 七、历史记录存储(带条数上限,本地开销可忽略) // ============================================================ function loadHistory() { try { const h = GM_getValue(HISTORY_KEY, []); return Array.isArray(h) ? h : []; } catch (e) { return []; } } function saveHistory(rec) { if (!cfg.storeHistory) return; let h = loadHistory(); h.unshift(rec); const cap = Math.max(1, parseInt(cfg.historyCap, 10) || 50); if (h.length > cap) h = h.slice(0, cap); GM_setValue(HISTORY_KEY, h); } // ============================================================ // 八、阅读理解面板 // ============================================================ let readPanel = null, readMask = null; let todayWords = []; // 今日词汇(含释义占位) let readingMode = 'random'; // random / self let curResult = null; // 当前显示的结果 let readCountInput = null, diffBtns = {}; function closeReadingPanel() { if (readPanel) { readPanel.remove(); readPanel = null; } if (readMask) { readMask.remove(); readMask = null; } } function randPick(arr, n) { const a = arr.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a.slice(0, n); } function openReadingPanel() { closeReadingPanel(); readMask = document.createElement('div'); readMask.style.cssText = 'position:fixed;inset:0;z-index:2147483646;background:rgba(17,24,39,.45);' + 'backdrop-filter:blur(2px);display:flex;align-items:center;justify-content:center;'; readMask.onmousedown = (e) => { if (e.target === readMask) closeReadingPanel(); }; readPanel = document.createElement('div'); readPanel.style.cssText = 'width:780px;max-width:94vw;max-height:92vh;overflow:auto;background:#fff;' + 'border-radius:16px;box-shadow:0 24px 60px rgba(0,0,0,.28);font:14px/1.6 sans-serif;color:#1f2937;'; // 头部 const head = document.createElement('div'); head.style.cssText = 'padding:16px 18px;border-bottom:1px solid #f3f4f6;display:flex;justify-content:space-between;' + 'align-items:center;background:linear-gradient(135deg,#10b981,#059669);border-radius:16px 16px 0 0;color:#fff;'; const hTitle = document.createElement('div'); hTitle.textContent = '📝 阅读理解生成(今日词汇 × AI)'; hTitle.style.cssText = 'font-weight:600;font-size:15px;'; const hClose = document.createElement('span'); hClose.textContent = '✕'; hClose.style.cssText = 'cursor:pointer;font-size:16px;opacity:.85;padding:2px 6px;'; hClose.onclick = closeReadingPanel; head.appendChild(hTitle); head.appendChild(hClose); const body = document.createElement('div'); body.style.cssText = 'padding:16px 18px;'; // ---------- 词汇选择区 ---------- const selCard = document.createElement('div'); selCard.style.cssText = 'border:1px solid #e5e7eb;border-radius:12px;padding:14px;margin-bottom:14px;background:#fafafa;'; const selTitle = document.createElement('div'); selTitle.textContent = '① 选词(今日待学词汇)'; selTitle.style.cssText = 'font-size:12px;font-weight:600;color:#374151;margin-bottom:8px;'; const loadRow = document.createElement('div'); loadRow.style.cssText = 'display:flex;align-items:center;gap:10px;margin-bottom:8px;'; const loadBtn = document.createElement('button'); loadBtn.textContent = '加载今日词汇'; loadBtn.style.cssText = 'padding:7px 14px;border:0;border-radius:8px;background:linear-gradient(135deg,#10b981,#059669);color:#fff;font-size:13px;cursor:pointer;'; const loadStatus = document.createElement('span'); loadStatus.textContent = '未加载'; loadStatus.style.cssText = 'font-size:12px;color:#6b7280;'; loadRow.appendChild(loadBtn); loadRow.appendChild(loadStatus); // 选词模式 const modeRow = document.createElement('div'); modeRow.style.cssText = 'display:flex;align-items:center;gap:14px;margin:6px 0 8px;flex-wrap:wrap;'; function modeLabel(text, key) { const lab = document.createElement('label'); lab.style.cssText = 'display:flex;align-items:center;gap:5px;font-size:13px;color:#374151;cursor:pointer;'; const rb = document.createElement('input'); rb.type = 'radio'; rb.name = 'mm_mode'; rb.value = key; rb.style.cssText = 'accent-color:#10b981;cursor:pointer;'; lab.appendChild(rb); lab.appendChild(document.createTextNode(text)); modeRow.appendChild(lab); return rb; } const rbRandom = modeLabel('随机抽取', 'random'); const rbSelf = modeLabel('自选今日词汇', 'self'); rbRandom.checked = true; // 数量 const countLabel = document.createElement('span'); countLabel.textContent = '词数:'; countLabel.style.cssText = 'font-size:13px;color:#374151;margin-left:6px;'; readCountInput = document.createElement('input'); readCountInput.type = 'number'; readCountInput.min = '1'; readCountInput.max = '200'; readCountInput.value = cfg.readingCount || 20; readCountInput.style.cssText = 'width:70px;padding:5px 8px;border:1px solid #d1d5db;border-radius:7px;font-size:13px;'; const quick = document.createElement('span'); quick.style.cssText = 'display:inline-flex;gap:5px;margin-left:8px;'; [20, 50, 100].forEach(n => { const b = document.createElement('button'); b.textContent = n; b.style.cssText = 'padding:3px 9px;border:1px solid #d1d5db;border-radius:6px;background:#fff;font-size:12px;cursor:pointer;'; b.onclick = () => { readCountInput.value = n; }; quick.appendChild(b); }); countLabel.style.cssText = 'font-size:13px;color:#374151;margin-left:6px;'; modeRow.appendChild(countLabel); modeRow.appendChild(readCountInput); modeRow.appendChild(quick); // 难度 const diffWrap = document.createElement('div'); diffWrap.style.cssText = 'display:flex;align-items:center;gap:6px;margin:8px 0;flex-wrap:wrap;'; const diffLab = document.createElement('span'); diffLab.textContent = '难度:'; diffLab.style.cssText = 'font-size:13px;color:#374151;'; diffWrap.appendChild(diffLab); ['易', '中', '难'].forEach(d => { const b = document.createElement('button'); b.textContent = d; b.style.cssText = 'padding:4px 12px;border:1px solid #d1d5db;border-radius:7px;background:#fff;font-size:13px;cursor:pointer;'; const sel = d === (cfg.readingDifficulty || '中'); if (sel) { b.style.background = '#10b981'; b.style.color = '#fff'; b.style.borderColor = '#10b981'; } b.onclick = () => { Object.values(diffBtns).forEach(x => { x.style.background = '#fff'; x.style.color = '#374151'; x.style.borderColor = '#d1d5db'; }); b.style.background = '#10b981'; b.style.color = '#fff'; b.style.borderColor = '#10b981'; cfg.readingDifficulty = d; }; diffBtns[d] = b; diffWrap.appendChild(b); }); // 自选词汇列表(加载后显示) const wordListWrap = document.createElement('div'); wordListWrap.style.cssText = 'display:none;max-height:180px;overflow:auto;border:1px solid #e5e7eb;border-radius:8px;background:#fff;padding:8px;margin-top:6px;'; // 生成按钮 const genRow = document.createElement('div'); genRow.style.cssText = 'display:flex;gap:10px;margin-top:12px;'; const genBtn = document.createElement('button'); genBtn.textContent = '🚀 生成阅读理解'; genBtn.style.cssText = 'flex:1;padding:10px 0;border:0;border-radius:9px;background:linear-gradient(135deg,#10b981,#059669);color:#fff;font-size:14px;font-weight:600;cursor:pointer;'; const clearBtn = document.createElement('button'); clearBtn.textContent = '清空结果'; clearBtn.style.cssText = 'padding:10px 18px;border:1px solid #d1d5db;border-radius:9px;background:#fff;color:#374151;font-size:13px;cursor:pointer;'; genRow.appendChild(genBtn); genRow.appendChild(clearBtn); selCard.appendChild(selTitle); selCard.appendChild(loadRow); selCard.appendChild(modeRow); selCard.appendChild(diffWrap); selCard.appendChild(wordListWrap); selCard.appendChild(genRow); // ---------- 结果区 ---------- const resultCard = document.createElement('div'); resultCard.style.cssText = 'border:1px solid #e5e7eb;border-radius:12px;padding:14px;margin-bottom:14px;background:#fff;'; const resHead = document.createElement('div'); resHead.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;flex-wrap:wrap;gap:8px;'; const resTitle = document.createElement('div'); resTitle.textContent = '② 生成结果'; resTitle.style.cssText = 'font-size:12px;font-weight:600;color:#374151;'; // 译文开关 const transToggle = document.createElement('label'); transToggle.style.cssText = 'display:flex;align-items:center;gap:6px;font-size:12px;color:#374151;cursor:pointer;'; const transCb = document.createElement('input'); transCb.type = 'checkbox'; transCb.style.cssText = 'accent-color:#10b981;cursor:pointer;'; const transText = document.createTextNode('显示中文译文'); transToggle.appendChild(transCb); transToggle.appendChild(transText); transCb.addEventListener('change', () => { const tr = readPanel.querySelector('.mm-translation'); if (tr) tr.style.display = transCb.checked ? 'block' : 'none'; }); resHead.appendChild(resTitle); resHead.appendChild(transToggle); const resContent = document.createElement('div'); resContent.style.cssText = 'font-size:14px;line-height:1.75;color:#1f2937;'; resContent.innerHTML = '