1 line
711 KiB
JSON
1 line
711 KiB
JSON
{"repo": "870110437-sudo/fantastic-broccoli", "files": {"app.js": {"a2cc3a2b575859c127ef7c85e50c3abed53621b5": {"t": 1785746440.7761497, "n": 24491, "h": "c2025519788a3c26", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst GET_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY\n};\n\nconst POST_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\nconst res = await fetch(\n `${BASE_URL}/Words?limit=1&skip=${skip}`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers: GET_HEADERS }\n);\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n {headers: POST_HEADERS}\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers: GET_HEADERS }\n);\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\nconst words = [];\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: GET_HEADERS }\n );\n\n const w = await r.json();\n\n // 跳过已删除的单词\nif (w.error || !w.objectId) {\n console.warn(\"单词不存在,用固定单词替代:\", id);\n\n try {\n const fallbackRes = await fetch(\n `${BASE_URL}/Words/c42d7cb0f2`,\n { headers: GET_HEADERS }\n );\n\n const fallbackWord =\n await fallbackRes.json();\n\n if (\n fallbackWord &&\n fallbackWord.objectId\n ) {\n words.push(fallbackWord);\n }\n\n } catch (err) {\n console.error(\n \"固定替代单词加载失败\",\n err\n );\n }\n\n continue;\n}\n\n// 全部失效了\nif (!words.length) {\n console.warn(\"恢复的单词全部失效,重新开始\");\n return startSession();\n}\n\nstate.groupWords = words;\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds.filter(id =>\n words.some(w => w.objectId === id)\n )\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// currentIndex 越界保护\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\ncurrentWord = queue[currentIndex];\n\n// currentWord 为空保护\nif (!currentWord) {\n console.warn(\"currentWord 为空,重新开始\");\n return startSession();\n}\n\nrenderWord();\n\n\n// 队列为空,重新开始\nif (!queue.length) {\n console.warn(\"恢复队列为空,重新开始\");\n return startSession();\n}\n\n// currentIndex 越界修正\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\n// 设置当前单词\ncurrentWord = queue[currentIndex];\n\n// 再保险\nif (!currentWord) {\n console.warn(\"当前单词为空,重新开始\");\n return startSession();\n}\nrenderWord();\n\n\n \n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(\n `${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers: POST_HEADERS});\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n {headers: POST_HEADERS }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "8fdf26097a610392784de3cbc640590decdc8746": {"t": 1785764272.0817401, "n": 24575, "h": "5f52798b4437fb87", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst GET_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY\n};\n\nconst POST_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\nconst res = await fetch(\n `${BASE_URL}/Words?limit=1&skip=${skip}`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers: GET_HEADERS }\n);\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n {headers: POST_HEADERS}\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers: GET_HEADERS }\n);\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\nconst words = [];\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: GET_HEADERS }\n );\n\n const w = await r.json();\n\n // 跳过已删除的单词\nif (w.error || !w.objectId) {\n console.warn(\"单词不存在,用固定单词替代:\", id);\n\n try {\n const fallbackRes = await fetch(\n `${BASE_URL}/Words/c42d7cb0f2`,\n { headers: GET_HEADERS }\n );\n\n const fallbackWord =\n await fallbackRes.json();\n\n if (\n fallbackWord &&\n fallbackWord.objectId\n ) {\n words.push(fallbackWord);\n }\n\n } catch (err) {\n console.error(\n \"固定替代单词加载失败\",\n err\n );\n }\n\n continue;\n}\n\n// 全部失效了\nif (!words.length) {\n console.warn(\"恢复的单词全部失效,重新开始\");\n return startSession();\n}\n\nstate.groupWords = words;\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds.filter(id =>\n words.some(w => w.objectId === id)\n )\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// currentIndex 越界保护\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\ncurrentWord = queue[currentIndex];\n\n// currentWord 为空保护\nif (!currentWord) {\n console.warn(\"currentWord 为空,重新开始\");\n return startSession();\n}\n\nrenderWord();\n\n\n// 选择当前学习队列\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// 队列为空,重新开始\nif (!queue.length) {\n console.warn(\"恢复队列为空,重新开始\");\n return startSession();\n}\n\n// currentIndex 越界修正\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\n// 设置当前单词\ncurrentWord = queue[currentIndex];\n\n// 再保险\nif (!currentWord) {\n console.warn(\"当前单词为空,重新开始\");\n return startSession();\n}\nrenderWord();\n\n\n \n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(\n `${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers: POST_HEADERS});\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n {headers: POST_HEADERS }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "2ab9da617b4669e4f22c18339f8093f0bf9586fb": {"t": 1785764273.8972208, "n": 24744, "h": "af85eec5532df93f", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst GET_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY\n};\n\nconst POST_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\nconst res = await fetch(\n `${BASE_URL}/Words?limit=1&skip=${skip}`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers: GET_HEADERS }\n);\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n {headers: POST_HEADERS}\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // ���记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers: GET_HEADERS }\n);\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\nconst words = [];\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: GET_HEADERS }\n );\n\n const w = await r.json();\n\n // 跳过已删除的单词\nif (w.error || !w.objectId) {\n console.warn(\"单词不存在,用固定单词替代:\", id);\n\n try {\n const fallbackRes = await fetch(\n `${BASE_URL}/Words/c42d7cb0f2`,\n { headers: GET_HEADERS }\n );\n\n const fallbackWord =\n await fallbackRes.json();\n\n if (\n fallbackWord &&\n fallbackWord.objectId\n ) {\n words.push(fallbackWord);\n }\n\n } catch (err) {\n console.error(\n \"固定替代单词加载失败\",\n err\n );\n }\n\n continue;\n}\n\n// 全部失效了\nif (!words.length) {\n console.warn(\"恢复的单词全部失效,重新开始\");\n return startSession();\n}\n\nstate.groupWords = words;\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds.filter(id =>\n words.some(w => w.objectId === id)\n )\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// currentIndex 越界保护\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\ncurrentWord = queue[currentIndex];\n\n// currentWord 为空保护\nif (!currentWord) {\n console.warn(\"currentWord 为空,重新开始\");\n return startSession();\n}\n\nrenderWord();\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\n// 选择当前学习队列\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// 队列为空,重新开始\nif (!queue.length) {\n console.warn(\"恢复队列为空,重新开始\");\n return startSession();\n}\n\n// currentIndex 越界修正\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\n// 设置当前单词\ncurrentWord = queue[currentIndex];\n\n// 再保险\nif (!currentWord) {\n console.warn(\"当前单词为空,重新开始\");\n return startSession();\n}\nrenderWord();\n\n\n \n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(\n `${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers: POST_HEADERS});\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n {headers: POST_HEADERS }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "f77ae743ca202fd57afc81d49cdd417287da968d": {"t": 1785764276.6811166, "n": 24684, "h": "872931333e796618", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst GET_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY\n};\n\nconst POST_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\nconst res = await fetch(\n `${BASE_URL}/Words?limit=1&skip=${skip}`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers: GET_HEADERS }\n);\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n {headers: POST_HEADERS}\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers: GET_HEADERS }\n);\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\nconst words = [];\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: GET_HEADERS }\n );\n\n const w = await r.json();\n\n // 跳过已删除的单词\nif (w.error || !w.objectId) {\n console.warn(\"单词不存在,用固定单词替代:\", id);\n\n try {\n const fallbackRes = await fetch(\n `${BASE_URL}/Words/c42d7cb0f2`,\n { headers: GET_HEADERS }\n );\n\n const fallbackWord =\n await fallbackRes.json();\n\n if (\n fallbackWord &&\n fallbackWord.objectId\n ) {\n words.push(fallbackWord);\n }\n\n } catch (err) {\n console.error(\n \"固定替代单词加载失败\",\n err\n );\n }\n\n continue;\n}\n\n// 全部失效了\nif (!words.length) {\n console.warn(\"恢复的单词全部失效,重新开始\");\n return startSession();\n}\n\nstate.groupWords = words;\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// currentIndex 越界保护\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\ncurrentWord = queue[currentIndex];\n\n// currentWord 为空保护\nif (!currentWord) {\n console.warn(\"currentWord 为空,重新开始\");\n return startSession();\n}\n\nrenderWord();\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\n// 选择当前学习队列\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// 队列为空,重新开始\nif (!queue.length) {\n console.warn(\"恢复队列为空,重新开始\");\n return startSession();\n}\n\n// currentIndex 越界修正\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\n// 设置当前单词\ncurrentWord = queue[currentIndex];\n\n// 再保险\nif (!currentWord) {\n console.warn(\"当前单词为空,重新开始\");\n return startSession();\n}\nrenderWord();\n\n\n \n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(\n `${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers: POST_HEADERS});\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n {headers: POST_HEADERS }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "8c25f29b568ec40651f40f1bd2e6800119924212": {"t": 1785764278.1966367, "n": 24919, "h": "2fd14457df2a3950", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst GET_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY\n};\n\nconst POST_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\nconst res = await fetch(\n `${BASE_URL}/Words?limit=1&skip=${skip}`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers: GET_HEADERS }\n);\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n {headers: POST_HEADERS}\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers: GET_HEADERS }\n);\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\nconst words = [];\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: GET_HEADERS }\n );\n\n const w = await r.json();\n\n // 跳过已删除的单词\n const wordIds = Array.isArray(progress.groupWordIds)\n ? progress.groupWordIds\n : [];\n\nconst words = [];\n\n// 没有单词,直接重新开始\nif (!wordIds.length) {\n console.warn(\"没有可恢复的单词,重新开始\");\n return startSession();\n}\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${c42d7cb0f2}`,\n { headers: GET_HEADERS }\n );\n\n // 请求失败(比如404)\n if (!r.ok) {\n console.warn(\"请求失败,跳过:\", id, r.status);\n continue;\n }\n\n const w = await r.json();\n\n // 数据异常\n if (!w || !w.objectId) {\n console.warn(\"单词不存在,跳过:\", id);\n continue;\n }\n\n words.push(w);\n\n } catch (err) {\n console.warn(\"读取单词失败:\", id, err);\n continue;\n }\n}\n\n// 全部失效了\nif (!words.length) {\n console.warn(\"恢复的单词全部失效,重新开始\");\n return startSession();\n}\n\nstate.groupWords = words;\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// currentIndex 越界保护\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\ncurrentWord = queue[currentIndex];\n\n// currentWord 为空保护\nif (!currentWord) {\n console.warn(\"currentWord 为空,重新开始\");\n return startSession();\n}\n\nrenderWord();\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\n// 选择当前学习队列\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// 队列为空,重新开始\nif (!queue.length) {\n console.warn(\"恢复队列为空,重新开始\");\n return startSession();\n}\n\n// currentIndex 越界修正\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\n// 设置当前单词\ncurrentWord = queue[currentIndex];\n\n// 再保险\nif (!currentWord) {\n console.warn(\"当前单词为空,重新开始\");\n return startSession();\n}\nrenderWord();\n\n\n \n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(\n `${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers: POST_HEADERS});\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n {headers: POST_HEADERS }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "35c2773c222e47a53528b52e474a4a9716200dbe": {"t": 1785764279.9653237, "n": 24911, "h": "4fd13952e25f2289", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst GET_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY\n};\n\nconst POST_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\nconst res = await fetch(\n `${BASE_URL}/Words?limit=1&skip=${skip}`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers: GET_HEADERS }\n);\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n {headers: POST_HEADERS}\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers: GET_HEADERS }\n);\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\nconst words = [];\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: GET_HEADERS }\n );\n\n const w = await r.json();\n\n // 跳过已删除的单词\n const wordIds = Array.isArray(progress.groupWordIds)\n ? progress.groupWordIds\n : [];\n\nconst words = [];\n\n// 没有单词,直接重新开始\nif (!wordIds.length) {\n console.warn(\"没有可恢复的单词,重新开始\");\n return startSession();\n}\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: GET_HEADERS }\n );\n\n // 请求失败(比如404)\n if (!r.ok) {\n console.warn(\"请求失败,跳过:\", id, r.status);\n continue;\n }\n\n const w = await r.json();\n\n // 数据异常\n if (!w || !w.objectId) {\n console.warn(\"单词不存在,跳过:\", id);\n continue;\n }\n\n words.push(w);\n\n } catch (err) {\n console.warn(\"读取单词失败:\", id, err);\n continue;\n }\n}\n\n// 全部失效了\nif (!words.length) {\n console.warn(\"恢复的单词全部失效,重新开始\");\n return startSession();\n}\n\nstate.groupWords = words;\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// currentIndex 越界保护\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\ncurrentWord = queue[currentIndex];\n\n// currentWord 为空保护\nif (!currentWord) {\n console.warn(\"currentWord 为空,重新开始\");\n return startSession();\n}\n\nrenderWord();\n\n// 恢复错词\nconst wrongIds = Array.isArray(progress.wrongWordIds)\n ? progress.wrongWordIds\n : [];\n\nstate.wrongWords = words.filter(w =>\n wrongIds.includes(w.objectId)\n);\n\n// 选择当前学习队列\nconst queue = inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n// 队列为空,重新开始\nif (!queue.length) {\n console.warn(\"恢复队列为空,重新开始\");\n return startSession();\n}\n\n// currentIndex 越界修正\nif (currentIndex >= queue.length) {\n currentIndex = 0;\n}\n\n// 设置当前单词\ncurrentWord = queue[currentIndex];\n\n// 再保险\nif (!currentWord) {\n console.warn(\"当前单词为空,重新开始\");\n return startSession();\n}\nrenderWord();\n\n\n \n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(\n `${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers: POST_HEADERS});\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n {headers: POST_HEADERS }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "fb6a066c96e52e3b4260e080e4a28acc57564e44": {"t": 1785764281.6944268, "n": 23627, "h": "4bde1dee3674e475", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst GET_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY\n};\n\nconst POST_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\nconst res = await fetch(\n `${BASE_URL}/Words?limit=1&skip=${skip}`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers: GET_HEADERS }\n);\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n {headers: POST_HEADERS}\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers: GET_HEADERS }\n);\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\nconst words = [];\n\nfor (const id of wordIds) {\n try {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: GET_HEADERS }\n );\n\n const w = await r.json();\n\n // 跳过已删除的单词\n if (w.error || !w.objectId) {\n console.warn(\"单词不存在,跳过:\", id);\n continue;\n }\n\n words.push(w);\n\n } catch (err) {\n console.warn(\"读取单词失败:\", id, err);\n }\n}\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(\n `${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers: POST_HEADERS});\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n {headers: POST_HEADERS }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "a02d65718e1d0b1ef1d5d52c148ab20502fa85e9": {"t": 1785764283.560515, "n": 23475, "h": "79899c5ccda38712", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst GET_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY\n};\n\nconst POST_HEADERS = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\nconst res = await fetch(\n `${BASE_URL}/Words?limit=1&skip=${skip}`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers: GET_HEADERS }\n);\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n {headers: POST_HEADERS}\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers: POST_HEADERS,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers: GET_HEADERS }\n);\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers: POST_HEADERS }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers: POST_HEADERS,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(\n `${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`,\n { headers: GET_HEADERS }\n);\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers: POST_HEADERS});\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n {headers: POST_HEADERS }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "16cf84794f616ac0283283e144c6d62ac418a7ed": {"t": 1785764286.0965877, "n": 23240, "h": "5ce60d26fceeb542", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n // \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers });\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "969d9cb13965f0285c8e75ac0bded85586e3a71b": {"t": 1785764287.6115208, "n": 23214, "h": "6192df9c53363cd6", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-LC-Id\": APP_ID,\n \"X-LC-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers });\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "b3af7f9e55c39156ed55f86c64025758fd045e13": {"t": 1785764289.0694385, "n": 23238, "h": "46f44b7d62cf0c97", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers });\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "2969a9cdffb805fc42bbd2455cae42e0027ee0d5": {"t": 1785764290.5664594, "n": 23246, "h": "d5ab25b6ed07b5a9", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers });\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n// 打开新增单词弹窗\nfunction openAddWordModal() {\n const modal = document.getElementById(\"addWordModal\");\n const wordInput = document.getElementById(\"newWord\");\n const meaningInput = document.getElementById(\"newMeaning\");\n\n if (!modal) {\n console.error(\"没找到 addWordModal\");\n return;\n }\n\n if (wordInput) wordInput.value = \"\";\n if (meaningInput) meaningInput.value = \"\";\n\n modal.classList.remove(\"hidden\");\n}\n\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeAddWordModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "c309ecdc9f920f6cea8517f307cffd41f727543d": {"t": 1785764291.9767358, "n": 23086, "h": "1a72619074051b83", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n// 关闭新增单词弹窗\nfunction closeModal() {\n const modal = document.getElementById(\"addWordModal\");\n if (modal) {\n modal.classList.add(\"hidden\");\n }\n}\n//保存单词\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers });\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n function openAddWordModal(event) {\n if (event) event.stopPropagation();\n\n document\n .getElementById(\"addWordModal\")\n .classList.remove(\"hidden\");\n\n document\n .getElementById(\"newWordInput\")\n .value = \"\";\n\n document\n .getElementById(\"newMeaningInput\")\n .value = \"\";\n}\n\nfunction closeAddWordModal() {\n document\n .getElementById(\"addWordModal\")\n .classList.add(\"hidden\");\n}\n\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeAddWordModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "baafbc591b0e99e49fb5ffce0050448604643a21": {"t": 1785764293.271262, "n": 22931, "h": "b4453f7ad24b2971", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers });\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\n\n\nasync function nextWord() {\n currentIndex += 1;\n\n const queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\n if (currentIndex >= queue.length) {\n\n // 错词复习结束\n if (inWrongReview) {\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 第一轮结束:切换到中→英\n if (\n currentMode ===\n studyModes.EN_TO_CN\n ) {\n currentMode =\n studyModes.CN_TO_EN;\n\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress();\n\n switchMode(\"cn\");\n return;\n }\n\n // 第二轮结束:进入错词复习\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(\n \"进入错词复习\"\n );\n\n currentWord =\n state.wrongWords[\n currentIndex\n ];\n\n renderWord();\n return;\n }\n\n // 全部完成\n return openPassageTest(\n state.groupWords\n );\n }\n\n // 正常进入下一词\n currentWord =\n queue[currentIndex];\n\n renderWord();\n}\n\n\n\n\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n function openAddWordModal(event) {\n if (event) event.stopPropagation();\n\n document\n .getElementById(\"addWordModal\")\n .classList.remove(\"hidden\");\n\n document\n .getElementById(\"newWordInput\")\n .value = \"\";\n\n document\n .getElementById(\"newMeaningInput\")\n .value = \"\";\n}\n\nfunction closeAddWordModal() {\n document\n .getElementById(\"addWordModal\")\n .classList.add(\"hidden\");\n}\n\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeAddWordModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "6bd14d1d6abf190a808e004f59b9d972732fb070": {"t": 1785764295.0728757, "n": 22558, "h": "029c0025b8c9cc13", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n\nconst findRes = await fetch(\n `${BASE_URL}/userwords?where=${where}`,\n { headers });\nconsole.log(\"userwords status:\", findRes.status);\nconst findData = await findRes.json();\nconsole.log(\"userwords response:\", findData);\n\n \n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) {\n currentMode = studyModes.CN_TO_EN;\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress(); // 加这个\n\n switchMode(\"cn\");\n return;\n}\nif (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(`进入错词复习`);\n currentWord = state.wrongWords[currentIndex];\n renderWord();\n return;\n}\n\n} // ← 缺的就是这个\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n function openAddWordModal(event) {\n if (event) event.stopPropagation();\n\n document\n .getElementById(\"addWordModal\")\n .classList.remove(\"hidden\");\n\n document\n .getElementById(\"newWordInput\")\n .value = \"\";\n\n document\n .getElementById(\"newMeaningInput\")\n .value = \"\";\n}\n\nfunction closeAddWordModal() {\n document\n .getElementById(\"addWordModal\")\n .classList.add(\"hidden\");\n}\n\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeAddWordModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "3ddf7bb1fcef05f8019a020300d2337f83604bfe": {"t": 1785764296.539787, "n": 22456, "h": "0c9bd19d42e1b3ef", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) {\n currentMode = studyModes.CN_TO_EN;\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress(); // 加这个\n\n switchMode(\"cn\");\n return;\n}\nif (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress();\n\n showToast(`进入错词复习`);\n currentWord = state.wrongWords[currentIndex];\n renderWord();\n return;\n}\n\n} // ← 缺的就是这个\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n//添加单词\n function openAddWordModal(event) {\n if (event) event.stopPropagation();\n\n document\n .getElementById(\"addWordModal\")\n .classList.remove(\"hidden\");\n\n document\n .getElementById(\"newWordInput\")\n .value = \"\";\n\n document\n .getElementById(\"newMeaningInput\")\n .value = \"\";\n}\n\nfunction closeAddWordModal() {\n document\n .getElementById(\"addWordModal\")\n .classList.add(\"hidden\");\n}\n\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeAddWordModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "c11a3721c9d17dddb5ae62008ed3c3d8f25f012f": {"t": 1785764297.9760072, "n": 22451, "h": "24166ff73b26185b", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) {\n currentMode = studyModes.CN_TO_EN;\n currentIndex = 0;\n state.phaseDone = 0;\n\n await saveStudyProgress(); // 加这个\n\n switchMode(\"cn\");\n return;\n}\n if (state.wrongWords.length) {\n inWrongReview = true;\n currentIndex = 0;\n\n await saveStudyProgress(); // 加这个\n\n showToast(`进入错词复习`);\n currentWord = state.wrongWords[currentIndex];\n renderWord();\n return;\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n//添加单词\n function openAddWordModal(event) {\n if (event) event.stopPropagation();\n\n document\n .getElementById(\"addWordModal\")\n .classList.remove(\"hidden\");\n\n document\n .getElementById(\"newWordInput\")\n .value = \"\";\n\n document\n .getElementById(\"newMeaningInput\")\n .value = \"\";\n}\n\nfunction closeAddWordModal() {\n document\n .getElementById(\"addWordModal\")\n .classList.add(\"hidden\");\n}\n\n\nasync function saveNewWord() {\n const word = document\n .getElementById(\"newWordInput\")\n .value\n .trim()\n .toLowerCase();\n\n const meaning = document\n .getElementById(\"newMeaningInput\")\n .value\n .trim();\n\n if (!word || !meaning) {\n showToast(\"请填写完整\");\n return;\n }\n\n try {\n // 先查重\n const where =\n encodeURIComponent(\n JSON.stringify({\n word\n })\n );\n\n const checkRes =\n await fetch(\n `${BASE_URL}/Words?where=${where}`,\n { headers }\n );\n\n const checkData =\n await checkRes.json();\n\n if (\n checkData.results?.length\n ) {\n showToast(\n \"单词已存在\"\n );\n return;\n }\n\n // 保存\n await fetch(\n `${BASE_URL}/Words`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n word,\n meaning\n })\n }\n );\n\n showToast(\n \"已加入单词库\"\n );\n\n closeAddWordModal();\n\n } catch (err) {\n console.error(err);\n showToast(\n \"添加失败\"\n );\n }\n}\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "518e5f528e8e2486956f7a016a94937dfc4af19a": {"t": 1785764299.4572585, "n": 21123, "h": "79bea7d8a1560724", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n\n// 恢复错词\nconst wrongIds =\n progress.wrongWordIds || [];\n\nstate.wrongWords =\n words.filter(w =>\n wrongIds.includes(\n w.objectId\n )\n );\n\nconst queue =\n inWrongReview\n ? state.wrongWords\n : state.groupWords;\n\ncurrentWord =\n queue[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal =\n document.getElementById(\n \"passageModal\"\n );\n\n if (passageModal) {\n passageModal.classList.add(\n \"hidden\"\n );\n }\n\n if (\n pendingNextSession &&\n startNext\n ) {\n pendingNextSession = false;\n\n // 修复:不要恢复旧记录\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n\n // 修复:不要累计\n state.todayNew = GROUP_SIZE;\n\n state.phaseDone = 0;\n\n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n\n document.querySelectorAll(\".seg-btn\")\n .forEach(btn =>\n btn.classList.toggle(\n \"active\",\n btn.dataset.mode === \"en\"\n )\n );\n\n currentWord = state.groupWords[0] || null;\n\n if (!currentWord) {\n document.getElementById(\n \"progressText\"\n ).textContent = \"词库不足,请先补充单词\";\n return;\n }\n\n await saveStudyProgress(); // 新增\n renderWord();\n}\n\nloadStudyProgress();\n"}, "c56b5203636a67ec522487094ea65b6f3dc247d7": {"t": 1785764300.9591193, "n": 20835, "h": "0864d8c90db18efe", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n\n todayDone: state.todayDone,\n todayNew: state.todayNew,\n streak: state.streak,\n\n groupWordIds: state.groupWords.map(\n w => w.objectId\n ),\n\n wrongWordIds: state.wrongWords.map(\n w => w.objectId\n )\n};\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n state.todayDone = progress.todayDone || 0;\nstate.todayNew = progress.todayNew || 0;\nstate.streak = progress.streak || 1;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n loadStudyProgress();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nloadStudyProgress();\n"}, "8b009926a43a3159a6ee13e3a03caebaef59660a": {"t": 1785764303.59439, "n": 20623, "h": "ff4d3fc258e7c9f4", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n//缓存占位符\nfunction showLoading(text = \"AI生成中...\") {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n const label = overlay.querySelector(\".loading-text\");\n if (label) label.textContent = text;\n\n overlay.classList.remove(\"hidden\");\n}\n\nfunction hideLoading() {\n const overlay = document.getElementById(\"loadingOverlay\");\n if (!overlay) return;\n\n overlay.classList.add(\"hidden\");\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n groupWordIds: state.groupWords.map(w => w.objectId),\n wrongWordIds: state.wrongWords.map(w => w.objectId)\n };\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n showLoading(\"正在生成词根记忆...\");\nconst generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\nhideLoading();\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n hideLoading();\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n loadStudyProgress();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n hideLoading();\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nloadStudyProgress();\n"}, "4ebc46ce6399deb5ba7b1b087bf4640df81f8430": {"t": 1785764304.9478896, "n": 20139, "h": "f172bf3e00907550", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\nconst GEN_API_KEY = \"sk-Cc6RjpYaHQmyJctCxaJEVjOcroZCvQZTwe7PQRiXDBpGZcsd\";\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n groupWordIds: state.groupWords.map(w => w.objectId),\n wrongWordIds: state.wrongWords.map(w => w.objectId)\n };\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n const generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\nconst genKey = GEN_API_KEY;\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = GEN_API_KEY;\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查 后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n loadStudyProgress();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nloadStudyProgress();\n"}, "40b1d9b4d92c22aca1621f83a6a2af50fe36297e": {"t": 1785764306.5912077, "n": 21081, "h": "04852964345a4cf0", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n groupWordIds: state.groupWords.map(w => w.objectId),\n wrongWordIds: state.wrongWords.map(w => w.objectId)\n };\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n const generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\n const genKey =\n localStorage.getItem(\n \"genApiKey\"\n );\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n loadStudyProgress();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nloadStudyProgress();\n"}, "14d393770ae3abe69021da5adee8315180b40397": {"t": 1785764308.1740396, "n": 22734, "h": "04120028c2b6db96", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n groupWordIds: state.groupWords.map(w => w.objectId),\n wrongWordIds: state.wrongWords.map(w => w.objectId)\n };\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\nasync function renderWord() {\n if (!currentWord) return;\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n \n if (!primary || !meta || !meaning || !example || !root || !memoryMethodInput) return;\n \n expanded = false;\n document.getElementById(\"card\").classList.remove(\"expanded\");\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\n// 渲染单词卡片\nasync function renderWord() {\n if (!currentWord) return;\n\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n\n if (\n !primary ||\n !meta ||\n !meaning ||\n !example ||\n !root ||\n !memoryMethodInput\n ) return;\n\n expanded = false;\n document\n .getElementById(\"card\")\n .classList.remove(\"expanded\");\n\n // ===== 1. 查免费词典 =====\n const dictInfo = await fetchWordInfo(\n currentWord.word\n );\n\n // 保存发音地址\n window.currentAudioUrl =\n dictInfo.audio || \"\";\n\n // ===== 2. 自动生成词根联想(只生成一次)=====\n let memory =\n currentWord.memorymethod ||\n currentWord.root ||\n \"\";\n\n if (!memory) {\n root.textContent =\n \"词根词缀: 生成中...\";\n\n const generated =\n await generateMemoryMethod(\n currentWord.word,\n currentWord.meaning\n );\n\n if (generated) {\n memory = generated;\n\n // 更新当前对象\n currentWord.memorymethod =\n generated;\n\n // 写回数据库\n try {\n await fetch(\n `${BASE_URL}/Words/${currentWord.objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify({\n memorymethod:\n generated\n })\n }\n );\n } catch (err) {\n console.error(\n \"写回词根失败\",\n err\n );\n }\n }\n }\n\n root.textContent = memory\n ? `词根词缀: ${memory}`\n : \"词根词缀: 暂无\";\n\n memoryMethodInput.value =\n memory;\n\n // ===== 3. 模式渲染 =====\n const modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () =>\n currentWord.word || \"-\",\n\n meta: () =>\n `${dictInfo.phonetic} ${dictInfo.pos}`.trim(),\n\n meaning: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n example: () =>\n dictInfo.example\n ? `例句: ${dictInfo.example}`\n : \"例句: 暂无\"\n },\n\n [studyModes.CN_TO_EN]: {\n primary: () =>\n currentWord.meaning ||\n \"暂无释义\",\n\n meta: () =>\n \"先回忆英文,再点击查看\",\n\n meaning: () =>\n `${currentWord.word || \"-\"} ${dictInfo.phonetic}`.trim(),\n\n example: () =>\n dictInfo.pos\n ? `词性: ${dictInfo.pos}`\n : \"词性: 暂无\"\n }\n };\n\n const renderRule =\n modeRenderConfig[currentMode];\n\n if (renderRule) {\n primary.textContent =\n renderRule.primary();\n\n meta.textContent =\n renderRule.meta();\n\n meaning.textContent =\n renderRule.meaning();\n\n example.textContent =\n renderRule.example();\n }\n\n updateProgress();\n}\n\n\n// 免费词典 API\nasync function fetchWordInfo(word) {\n try {\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n const entry = data[0];\n\n return {\n phonetic:\n entry.phonetic ||\n entry.phonetics?.find(\n x => x.text\n )?.text ||\n \"\",\n\n audio:\n entry.phonetics?.find(\n x => x.audio\n )?.audio ||\n \"\",\n\n pos:\n entry.meanings?.[0]\n ?.partOfSpeech || \"\",\n\n example:\n entry.meanings?.[0]\n ?.definitions?.find(\n x => x.example\n )?.example || \"\"\n };\n\n } catch (err) {\n console.error(\n \"词典查询失败\",\n err\n );\n\n return {\n phonetic: \"\",\n audio: \"\",\n pos: \"\",\n example: \"\"\n };\n }\n}\n\n\n// DeepSeek 自动生成词根联想\nasync function generateMemoryMethod(\n word,\n meaning\n) {\n const genKey =\n localStorage.getItem(\n \"genApiKey\"\n );\n\n if (!genKey) {\n console.warn(\n \"未设置生成 API Key\"\n );\n return \"\";\n }\n\n try {\n const res = await fetch(\n \"https://shiyunapi.com/v1/chat/completions\",\n {\n method: \"POST\",\n headers: {\n \"Content-Type\":\n \"application/json\",\n Authorization:\n `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model:\n \"deepseek-chat\",\n messages: [\n {\n role: \"user\",\n content:\n `请为英语单词 ${word}(意思:${meaning})生成简短词根词缀联想记忆法,控制在30字以内,只返回结果,不要解释。`\n }\n ]\n })\n }\n );\n\n const data =\n await res.json();\n\n return (\n data.choices?.[0]\n ?.message?.content?.trim() ||\n \"\"\n );\n\n } catch (err) {\n console.error(\n \"生成词根联想失败\",\n err\n );\n\n return \"\";\n }\n}\n\n\n// 发音\nfunction playPronunciation() {\n if (\n !window.currentAudioUrl\n ) {\n showToast(\n \"没有发音\"\n );\n return;\n }\n\n new Audio(\n window.currentAudioUrl\n ).play();\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n loadStudyProgress();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nstartSession();\n"}, "275eaaa486162798b18fb9f75bac03546119e6a6": {"t": 1785764309.716601, "n": 19726, "h": "3594990326ff0517", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n groupWordIds: state.groupWords.map(w => w.objectId),\n wrongWordIds: state.wrongWords.map(w => w.objectId)\n };\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\nfunction renderWord() {\n if (!currentWord) return;\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n \n if (!primary || !meta || !meaning || !example || !root || !memoryMethodInput) return;\n \n expanded = false;\n document.getElementById(\"card\").classList.remove(\"expanded\");\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\nconst modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () => currentWord.word || \"-\",\n meta: () => `${currentWord.IPA ?? \"\"} ${currentWord.pos ?? \"\"}`.trim(),\n meaning: () => currentWord.meaning || \"暂无释义\",\n example: () => currentWord.example?.trim() ? `例句: ${currentWord.example}` : \"例句: 暂无\"\n },\n // 反向模式\n [studyModes.CN_TO_EN]: {\n primary: () => currentWord.meaning || \"暂无释义\",\n meta: () => \"先回忆英文,再点击查看\",\n meaning: () => `${currentWord.word || \"-\"} ${currentWord.IPA ?? \"\"}`.trim(),\n example: () => currentWord.pos ? `词性: ${currentWord.pos}` : \"词性: 暂无\"\n }\n};\n\n// 公共逻辑(和模式无关,抽离出来,只写一次)\nconst memory = currentWord.memorymethod || currentWord.root || \"\";\nroot.textContent = memory ? `词根词缀: ${memory}` : \"词根词缀: 预留\";\nmemoryMethodInput.value = currentWord.memorymethod || \"\";\n\n// 根据当前模式直接拿配置渲染\nconst renderRule = modeRenderConfig[currentMode];\nif (renderRule) {\n primary.textContent = renderRule.primary();\n meta.textContent = renderRule.meta();\n meaning.textContent = renderRule.meaning();\n example.textContent = renderRule.example();\n}\n\nupdateProgress();\n}\n//发音\nasync function playPronunciation() {\n const word = currentWord.word;\n\n if (!word) {\n showToast(\"没有单词\");\n return;\n }\n\n try {\n // 查询免费词典 API\n const res = await fetch(\n `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`\n );\n\n const data = await res.json();\n\n // 找到第一个有 audio 的发音\n let audioUrl = \"\";\n\n for (const phonetic of data[0]?.phonetics || []) {\n if (phonetic.audio) {\n audioUrl = phonetic.audio;\n break;\n }\n }\n\n if (!audioUrl) {\n showToast(\"没有找到发音\");\n return;\n }\n\n // 播放\n const audio = new Audio(audioUrl);\n audio.play();\n\n } catch (err) {\n console.error(err);\n showToast(\"发音加载失败\");\n }\n}\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n loadStudyProgress();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nstartSession();\n"}, "2f37eb291b75842a41ab2c5abc5a79346c45798f": {"t": 1785764311.2455473, "n": 19269, "h": "bd9bb23d39e35df8", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\n\n\n//新增“保存进度”函数\nasync function saveStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n if (!currentUserId) return;\n\n const payload = {\n userId: currentUserId,\n currentIndex,\n currentMode,\n inWrongReview,\n groupWordIds: state.groupWords.map(w => w.objectId),\n wrongWordIds: state.wrongWords.map(w => w.objectId)\n };\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n // 先查有没有已有记录\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (data.results?.length) {\n // 有记录 → 更新\n const objectId = data.results[0].objectId;\n\n await fetch(\n `${BASE_URL}/studyProgress/${objectId}`,\n {\n method: \"PUT\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n } else {\n // 没记录 → 新建\n await fetch(\n `${BASE_URL}/studyProgress`,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify(payload)\n }\n );\n }\n\n } catch (err) {\n console.error(\"保存学习进度失败\", err);\n }\n}\n\n//新增“恢复进度”函数\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\n//-----------------------------------\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\nasync function loadStudyProgress() {\n const currentUserId = localStorage.getItem(\"userId\");\n\n if (!currentUserId) {\n return startSession();\n }\n\n try {\n const where = encodeURIComponent(\n JSON.stringify({ userId: currentUserId })\n );\n\n const res = await fetch(\n `${BASE_URL}/studyProgress?where=${where}`,\n { headers }\n );\n\n const data = await res.json();\n\n if (!data.results?.length) {\n return startSession();\n }\n\n const progress = data.results[0];\n\n currentIndex = progress.currentIndex || 0;\n currentMode = progress.currentMode || studyModes.EN_TO_CN;\n inWrongReview = progress.inWrongReview || false;\n\n const wordIds = progress.groupWordIds || [];\n const words = [];\n\n for (const id of wordIds) {\n const r = await fetch(\n `${BASE_URL}/Words/${id}`,\n { headers }\n );\n const w = await r.json();\n words.push(w);\n }\n\n state.groupWords = words;\n currentWord = words[currentIndex];\n\n renderWord();\n\n } catch (err) {\n console.error(\"恢复学习进度失败\", err);\n startSession();\n }\n}\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\nfunction renderWord() {\n if (!currentWord) return;\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n \n if (!primary || !meta || !meaning || !example || !root || !memoryMethodInput) return;\n \n expanded = false;\n document.getElementById(\"card\").classList.remove(\"expanded\");\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\nconst modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () => currentWord.word || \"-\",\n meta: () => `${currentWord.IPA ?? \"\"} ${currentWord.pos ?? \"\"}`.trim(),\n meaning: () => currentWord.meaning || \"暂无释义\",\n example: () => currentWord.example?.trim() ? `例句: ${currentWord.example}` : \"例句: 暂无\"\n },\n // 反向模式\n [studyModes.CN_TO_EN]: {\n primary: () => currentWord.meaning || \"暂无释义\",\n meta: () => \"先回忆英文,再点击查看\",\n meaning: () => `${currentWord.word || \"-\"} ${currentWord.IPA ?? \"\"}`.trim(),\n example: () => currentWord.pos ? `词性: ${currentWord.pos}` : \"词性: 暂无\"\n }\n};\n\n// 公共逻辑(和模式无关,抽离出来,只写一次)\nconst memory = currentWord.memorymethod || currentWord.root || \"\";\nroot.textContent = memory ? `词根词缀: ${memory}` : \"词根词缀: 预留\";\nmemoryMethodInput.value = currentWord.memorymethod || \"\";\n\n// 根据当前模式直接拿配置渲染\nconst renderRule = modeRenderConfig[currentMode];\nif (renderRule) {\n primary.textContent = renderRule.primary();\n meta.textContent = renderRule.meta();\n meaning.textContent = renderRule.meaning();\n example.textContent = renderRule.example();\n}\n\nupdateProgress();\n}\n\nfunction playAudio(event, slow = false) { \n if (event) event.stopPropagation(); \n if (!currentWord?.pronunciation) return alert(\"没有发音\"); \n const a = new Audio(currentWord.pronunciation); \n a.playbackRate = slow ? 0.75 : 1;\n a.play();\n}\n\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\nawait saveWord(known ? 1 : 0);\nawait saveStudyProgress();\nawait nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n loadStudyProgress();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nstartSession();\n"}, "b69a32f569ed2369842ee7b672e227c6d5ad497b": {"t": 1785764312.9353216, "n": 15840, "h": "7a2aaf654d6924e0", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\nfunction renderWord() {\n if (!currentWord) return;\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n \n if (!primary || !meta || !meaning || !example || !root || !memoryMethodInput) return;\n \n expanded = false;\n document.getElementById(\"card\").classList.remove(\"expanded\");\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\nconst modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () => currentWord.word || \"-\",\n meta: () => `${currentWord.IPA ?? \"\"} ${currentWord.pos ?? \"\"}`.trim(),\n meaning: () => currentWord.meaning || \"暂无释义\",\n example: () => currentWord.example?.trim() ? `例句: ${currentWord.example}` : \"例句: 暂无\"\n },\n // 反向模式\n [studyModes.CN_TO_EN]: {\n primary: () => currentWord.meaning || \"暂无释义\",\n meta: () => \"先回忆英文,再点击查看\",\n meaning: () => `${currentWord.word || \"-\"} ${currentWord.IPA ?? \"\"}`.trim(),\n example: () => currentWord.pos ? `词性: ${currentWord.pos}` : \"词性: 暂无\"\n }\n};\n\n// 公共逻辑(和模式无关,抽离出来,只写一次)\nconst memory = currentWord.memorymethod || currentWord.root || \"\";\nroot.textContent = memory ? `词根词缀: ${memory}` : \"词根词缀: 预留\";\nmemoryMethodInput.value = currentWord.memorymethod || \"\";\n\n// 根据当前模式直接拿配置渲染\nconst renderRule = modeRenderConfig[currentMode];\nif (renderRule) {\n primary.textContent = renderRule.primary();\n meta.textContent = renderRule.meta();\n meaning.textContent = renderRule.meaning();\n example.textContent = renderRule.example();\n}\n\nupdateProgress();\n}\n\nfunction playAudio(event, slow = false) { \n if (event) event.stopPropagation(); \n if (!currentWord?.pronunciation) return alert(\"没有发音\"); \n const a = new Audio(currentWord.pronunciation); \n a.playbackRate = slow ? 0.75 : 1;\n a.play();\n}\n\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\n await saveWord(known ? 1 : 0);\n await nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n\n // 调试:看看有没有读到 key\n console.log(\"读取到 genKey:\", genKey);\n\n if (!genKey) {\n throw new Error(\"缺少API key\");\n }\n\n const wordPairs = words\n .map(w => `${w.word}: ${w.meaning}`)\n .join(\"\\n\");\n\n const prompt = `\n请用以下10个英语单词写一个120词以内自然短文,要求:\n1. 必须包含所有单词\n2. 每个单词只出现一次\n3. 只返回 JSON 格式:\n{\"passage\":\"短文内容\"}\n\n单词列表:\n${wordPairs}\n`;\n\n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${genKey}`\n },\n body: JSON.stringify({\n model: CHEAP_MODEL,\n temperature: 0.4,\n messages: [\n {\n role: \"system\",\n content: \"You are a concise English writing assistant.\"\n },\n {\n role: \"user\",\n content: prompt\n }\n ]\n })\n });\n\n if (!res.ok) {\n throw new Error(\"短文生成失败\");\n }\n\n const data = await res.json();\n\n let raw =\n data.choices?.[0]?.message?.content?.trim() || \"{}\";\n\n // 修复:去掉 ```json ``` 包裹\n raw = raw\n .replace(/^```json/, \"\")\n .replace(/^```/, \"\")\n .replace(/```$/, \"\")\n .trim();\n\n const parsed = JSON.parse(raw);\n\n if (!parsed.passage) {\n throw new Error(\"模型返回格式错误\");\n }\n\n return parsed.passage;\n}\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nstartSession();\n"}, "1ce3b493f69be45949c556548508b5a9cd5579f4": {"t": 1785764315.3007421, "n": 15594, "h": "9f1d809c16ac2635", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) {\n // 点击按钮、输入框、文本域时,不触发展开/收起\n if (\n event.target.closest(\"button\") ||\n event.target.closest(\"input\") ||\n event.target.closest(\"textarea\")\n ) {\n return;\n }\n\n expanded = !expanded;\n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded);\n}\n\nfunction renderWord() {\n if (!currentWord) return;\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n \n if (!primary || !meta || !meaning || !example || !root || !memoryMethodInput) return;\n \n expanded = false;\n document.getElementById(\"card\").classList.remove(\"expanded\");\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\nconst modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () => currentWord.word || \"-\",\n meta: () => `${currentWord.IPA ?? \"\"} ${currentWord.pos ?? \"\"}`.trim(),\n meaning: () => currentWord.meaning || \"暂无释义\",\n example: () => currentWord.example?.trim() ? `例句: ${currentWord.example}` : \"例句: 暂无\"\n },\n // 反向模式\n [studyModes.CN_TO_EN]: {\n primary: () => currentWord.meaning || \"暂无释义\",\n meta: () => \"先回忆英文,再点击查看\",\n meaning: () => `${currentWord.word || \"-\"} ${currentWord.IPA ?? \"\"}`.trim(),\n example: () => currentWord.pos ? `词性: ${currentWord.pos}` : \"词性: 暂无\"\n }\n};\n\n// 公共逻辑(和模式无关,抽离出来,只写一次)\nconst memory = currentWord.memorymethod || currentWord.root || \"\";\nroot.textContent = memory ? `词根词缀: ${memory}` : \"词根词缀: 预留\";\nmemoryMethodInput.value = currentWord.memorymethod || \"\";\n\n// 根据当前模式直接拿配置渲染\nconst renderRule = modeRenderConfig[currentMode];\nif (renderRule) {\n primary.textContent = renderRule.primary();\n meta.textContent = renderRule.meta();\n meaning.textContent = renderRule.meaning();\n example.textContent = renderRule.example();\n}\n\nupdateProgress();\n}\n\nfunction playAudio(event, slow = false) { \n if (event) event.stopPropagation(); \n if (!currentWord?.pronunciation) return alert(\"没有发音\"); \n const a = new Audio(currentWord.pronunciation); \n a.playbackRate = slow ? 0.75 : 1;\n a.play();\n}\n\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\n await saveWord(known ? 1 : 0);\n await nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n if (!genKey) throw new Error(\"缺少API key\");\n const wordPairs = words.map(w => `${w.word}: ${w.meaning}`).join(\"\\n\");\n const prompt = `请用以下10个英语单词写一个120词以内自然短文,要求:\\n1) 必须包含所有单词且每个只出现一次\\n2) 仅返回 JSON 格式 {\\\"passage\\\":\\\"...\\\"}\\n单词列表:\\n${wordPairs}`;\n \n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${genKey}` },\n body: JSON.stringify({ model: CHEAP_MODEL, temperature: 0.4, messages: [{ role: \"system\", content: \"You are a concise English writing assistant.\" }, { role: \"user\", content: prompt }] })\n });\n \n if (!res.ok) throw new Error(\"短文生成失败\");\n const data = await res.json();\n const raw = data.choices?.[0]?.message?.content?.trim() || \"{}\";\n let parsed;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(\"模型返回格式错误\");\n }\n if (!parsed.passage || typeof parsed.passage !== \"string\") throw new Error(\"模型返回格式错误\");\n return parsed.passage;\n}\n\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nstartSession();\n"}, "7f6ca8651872a1a54ed187ff093d021c468e2ad0": {"t": 1785764316.7708464, "n": 15474, "h": "07a9f8ef9c20899f", "c": "const APP_ID = \"1f1dcb2717bc41a2d196167508f83458\";\nconst APP_KEY = \"2efd06f7769a606544357909a7503793\";\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) { \n if (event.target.closest(\"button\")) return; \n expanded = !expanded; \n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded); \n}\n\nfunction renderWord() {\n if (!currentWord) return;\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n \n if (!primary || !meta || !meaning || !example || !root || !memoryMethodInput) return;\n \n expanded = false;\n document.getElementById(\"card\").classList.remove(\"expanded\");\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\nconst modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () => currentWord.word || \"-\",\n meta: () => `${currentWord.IPA ?? \"\"} ${currentWord.pos ?? \"\"}`.trim(),\n meaning: () => currentWord.meaning || \"暂无释义\",\n example: () => currentWord.example?.trim() ? `例句: ${currentWord.example}` : \"例句: 暂无\"\n },\n // 反向模式\n [studyModes.CN_TO_EN]: {\n primary: () => currentWord.meaning || \"暂无释义\",\n meta: () => \"先回忆英文,再点击查看\",\n meaning: () => `${currentWord.word || \"-\"} ${currentWord.IPA ?? \"\"}`.trim(),\n example: () => currentWord.pos ? `词性: ${currentWord.pos}` : \"词性: 暂无\"\n }\n};\n\n// 公共逻辑(和模式无关,抽离出来,只写一次)\nconst memory = currentWord.memorymethod || currentWord.root || \"\";\nroot.textContent = memory ? `词根词缀: ${memory}` : \"词根词缀: 预留\";\nmemoryMethodInput.value = currentWord.memorymethod || \"\";\n\n// 根据当前模式直接拿配置渲染\nconst renderRule = modeRenderConfig[currentMode];\nif (renderRule) {\n primary.textContent = renderRule.primary();\n meta.textContent = renderRule.meta();\n meaning.textContent = renderRule.meaning();\n example.textContent = renderRule.example();\n}\n\nupdateProgress();\n}\n\nfunction playAudio(event, slow = false) { \n if (event) event.stopPropagation(); \n if (!currentWord?.pronunciation) return alert(\"没有发音\"); \n const a = new Audio(currentWord.pronunciation); \n a.playbackRate = slow ? 0.75 : 1;\n a.play();\n}\n\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\n await saveWord(known ? 1 : 0);\n await nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n if (!genKey) throw new Error(\"缺少API key\");\n const wordPairs = words.map(w => `${w.word}: ${w.meaning}`).join(\"\\n\");\n const prompt = `请用以下10个英语单词写一个120词以内自然短文,要求:\\n1) 必须包含所有单词且每个只出现一次\\n2) 仅返回 JSON 格式 {\\\"passage\\\":\\\"...\\\"}\\n单词列表:\\n${wordPairs}`;\n \n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${genKey}` },\n body: JSON.stringify({ model: CHEAP_MODEL, temperature: 0.4, messages: [{ role: \"system\", content: \"You are a concise English writing assistant.\" }, { role: \"user\", content: prompt }] })\n });\n \n if (!res.ok) throw new Error(\"短文生成失败\");\n const data = await res.json();\n const raw = data.choices?.[0]?.message?.content?.trim() || \"{}\";\n let parsed;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(\"模型返回格式错误\");\n }\n if (!parsed.passage || typeof parsed.passage !== \"string\") throw new Error(\"模型返回格式错误\");\n return parsed.passage;\n}\n\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nstartSession();\n"}, "3672c3f3d07530d863da93a273b3b70544172c6a": {"t": 1785764319.353579, "n": 15546, "h": "9942aa271df68b67", "c": "// 凭证应通过环境变量或后端API获取,不要在客户端硬编码\nconst APP_ID = localStorage.getItem(\"appId\") || \"\"; // 从localStorage获取\nconst APP_KEY = localStorage.getItem(\"appKey\") || \"\"; // 从localStorage获取\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) { \n if (event.target.closest(\"button\")) return; \n expanded = !expanded; \n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded); \n}\n\nfunction renderWord() {\n if (!currentWord) return;\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n \n if (!primary || !meta || !meaning || !example || !root || !memoryMethodInput) return;\n \n expanded = false;\n document.getElementById(\"card\").classList.remove(\"expanded\");\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\nconst modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () => currentWord.word || \"-\",\n meta: () => `${currentWord.IPA ?? \"\"} ${currentWord.pos ?? \"\"}`.trim(),\n meaning: () => currentWord.meaning || \"暂无释义\",\n example: () => currentWord.example?.trim() ? `例句: ${currentWord.example}` : \"例句: 暂无\"\n },\n // 反向模式\n [studyModes.CN_TO_EN]: {\n primary: () => currentWord.meaning || \"暂无释义\",\n meta: () => \"先回忆英文,再点击查看\",\n meaning: () => `${currentWord.word || \"-\"} ${currentWord.IPA ?? \"\"}`.trim(),\n example: () => currentWord.pos ? `词性: ${currentWord.pos}` : \"词性: 暂无\"\n }\n};\n\n// 公共逻辑(和模式无关,抽离出来,只写一次)\nconst memory = currentWord.memorymethod || currentWord.root || \"\";\nroot.textContent = memory ? `词根词缀: ${memory}` : \"词根词缀: 预留\";\nmemoryMethodInput.value = currentWord.memorymethod || \"\";\n\n// 根据当前模式直接拿配置渲染\nconst renderRule = modeRenderConfig[currentMode];\nif (renderRule) {\n primary.textContent = renderRule.primary();\n meta.textContent = renderRule.meta();\n meaning.textContent = renderRule.meaning();\n example.textContent = renderRule.example();\n}\n\nupdateProgress();\n}\n\nfunction playAudio(event, slow = false) { \n if (event) event.stopPropagation(); \n if (!currentWord?.pronunciation) return alert(\"没有发音\"); \n const a = new Audio(currentWord.pronunciation); \n a.playbackRate = slow ? 0.75 : 1;\n a.play();\n}\n\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\n await saveWord(known ? 1 : 0);\n await nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n if (!genKey) throw new Error(\"缺少API key\");\n const wordPairs = words.map(w => `${w.word}: ${w.meaning}`).join(\"\\n\");\n const prompt = `请用以下10个英语单词写一个120词以内自然短文,要求:\\n1) 必须包含所有单词且每个只出现一次\\n2) 仅返回 JSON 格式 {\\\"passage\\\":\\\"...\\\"}\\n单词列表:\\n${wordPairs}`;\n \n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${genKey}` },\n body: JSON.stringify({ model: CHEAP_MODEL, temperature: 0.4, messages: [{ role: \"system\", content: \"You are a concise English writing assistant.\" }, { role: \"user\", content: prompt }] })\n });\n \n if (!res.ok) throw new Error(\"短文生成失败\");\n const data = await res.json();\n const raw = data.choices?.[0]?.message?.content?.trim() || \"{}\";\n let parsed;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(\"模型返回格式错误\");\n }\n if (!parsed.passage || typeof parsed.passage !== \"string\") throw new Error(\"模型返回格式错误\");\n return parsed.passage;\n}\n\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nstartSession();\n"}, "3d1f8d3e5f0a5433da61958c9bfc52763b7f42de": {"t": 1785764321.085976, "n": 15547, "h": "af8041ed5b878514", "c": "// 凭证应通过环境变量或后端API获取,不要在客户端硬编码\nconst APP_ID = localStorage.getItem(\"appId\") || \"\"; // 从localStorage获取\nconst APP_KEY = localStorage.getItem(\"appKey\") || \"\"; // 从localStorage获取\nconst BASE_URL = \"https://api.codenow.cn/1/classes\";\nconst GEN_BASE_URL = \"https://shiyunapi.com/v1\";\nconst CHEAP_MODEL = \"deepseek-chat\";\n\nconst headers = {\n \"X-Bmob-Application-Id\": APP_ID,\n \"X-Bmob-REST-API-Key\": APP_KEY,\n \"Content-Type\": \"application/json\"\n};\n\nconst studyModes = { EN_TO_CN: \"en\", CN_TO_EN: \"cn\" };\nconst GROUP_SIZE = 10;\n\nlet currentWord = null;\nlet expanded = false;\nlet currentMode = studyModes.EN_TO_CN;\nlet currentIndex = 0;\nlet inWrongReview = false;\nlet pendingNextSession = false;\n\nconst state = { groupWords: [], wrongWords: [], todayDone: 0, todayNew: 0, phaseDone: 0, streak: 1 };\n\n// 修复:删除重复的userId赋值,只保留一次\nlet userId = localStorage.getItem(\"userId\");\nif (!userId) {\n userId = (prompt(\"请输入用户名\") || \"\").trim().slice(0, 32) || `guest_${Date.now()}`;\n localStorage.setItem(\"userId\", userId);\n}\n\nconst savedGenKey = localStorage.getItem(\"genApiKey\");\nif (!savedGenKey) {\n showToast('未配置生成 API Key,请先点 \"API设置\"');\n}\n\nfunction showToast(message) {\n const toast = document.getElementById(\"toast\");\n if (!toast) return; // 安全检查\n toast.textContent = message;\n toast.classList.add(\"show\");\n setTimeout(() => toast.classList.remove(\"show\"), 1800);\n}\n\nfunction escapeHtml(text = \"\") {\n return String(text).replaceAll(\"&\", \"&\").replaceAll(\"<\", \"<\").replaceAll(\">\", \">\").replaceAll('\"', \""\").replaceAll(\"'\", \"'\");\n}\n\nasync function getRandomWord() {\n const skip = Math.floor(Math.random() * 120);\n const res = await fetch(`${BASE_URL}/Words?limit=1&skip=${skip}`, { headers });\n const data = await res.json();\n return data.results?.[0] || null;\n}\n\nasync function getWordGroup(size = GROUP_SIZE) {\n const map = new Map();\n let tries = 0;\n while (map.size < size && tries < size * 12) {\n const w = await getRandomWord();\n tries += 1;\n if (w?.objectId && !map.has(w.objectId)) map.set(w.objectId, { ...w, known_en: false, known_cn: false, wrongCount: w.wrongCount || 0 });\n }\n return Array.from(map.values());\n}\n\nasync function saveWord(status) {\n if (!currentWord) return;\n const where = encodeURIComponent(JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId } }));\n const findRes = await fetch(`${BASE_URL}/userwords?where=${where}`, { headers });\n const findData = await findRes.json();\n if (findData.results?.length) {\n const record = findData.results[0];\n await fetch(`${BASE_URL}/userwords/${record.objectId}`, { method: \"PUT\", headers, body: JSON.stringify({ status, reviewCount: (record.reviewCount || 0) + 1 }) });\n } else {\n await fetch(`${BASE_URL}/userwords`, { method: \"POST\", headers, body: JSON.stringify({ userId, wordId: { \"__type\": \"Pointer\", className: \"Words\", objectId: currentWord.objectId }, status, reviewCount: 1 }) });\n }\n}\n\nfunction switchMode(mode) {\n if (currentMode === mode || inWrongReview) return;\n currentMode = mode;\n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === mode));\n document.getElementById(\"segIndicator\").style.transform = mode === \"en\" ? \"translateX(0)\" : \"translateX(100%)\";\n flipCard();\n renderWord();\n}\n\nfunction flipCard() { \n const card = document.getElementById(\"card\"); \n if (!card) return;\n card.classList.remove(\"flip\"); \n void card.offsetWidth; \n card.classList.add(\"flip\"); \n}\n\nfunction toggleExpand(event) { \n if (event.target.closest(\"button\")) return; \n expanded = !expanded; \n document.getElementById(\"card\").classList.toggle(\"expanded\", expanded); \n}\n\nfunction renderWord() {\n if (!currentWord) return;\n const primary = document.getElementById(\"primary\");\n const meta = document.getElementById(\"meta\");\n const meaning = document.getElementById(\"meaning\");\n const example = document.getElementById(\"example\");\n const root = document.getElementById(\"root\");\n const memoryMethodInput = document.getElementById(\"memoryMethodInput\");\n \n if (!primary || !meta || !meaning || !example || !root || !memoryMethodInput) return;\n \n expanded = false;\n document.getElementById(\"card\").classList.remove(\"expanded\");\n \n // 模式配置映射,把两种模式的渲染规则写死在这里\nconst modeRenderConfig = {\n [studyModes.EN_TO_CN]: {\n primary: () => currentWord.word || \"-\",\n meta: () => `${currentWord.IPA ?? \"\"} ${currentWord.pos ?? \"\"}`.trim(),\n meaning: () => currentWord.meaning || \"暂无释义\",\n example: () => currentWord.example?.trim() ? `例句: ${currentWord.example}` : \"例句: 暂无\"\n },\n // 反向模式\n [studyModes.CN_TO_EN]: {\n primary: () => currentWord.meaning || \"暂无释义\",\n meta: () => \"先回忆英文,再点击查看\",\n meaning: () => `${currentWord.word || \"-\"} ${currentWord.IPA ?? \"\"}`.trim(),\n example: () => currentWord.pos ? `词性: ${currentWord.pos}` : \"词性: 暂无\"\n }\n};\n\n// 公共逻辑(和模式无关,抽离出来,只写一次)\nconst memory = currentWord.memorymethod || currentWord.root || \"\";\nroot.textContent = memory ? `词根词缀: ${memory}` : \"词根词缀: 预留\";\nmemoryMethodInput.value = currentWord.memorymethod || \"\";\n\n// 根据当前模式直接拿配置渲染\nconst renderRule = modeRenderConfig[currentMode];\nif (renderRule) {\n primary.textContent = renderRule.primary();\n meta.textContent = renderRule.meta();\n meaning.textContent = renderRule.meaning();\n example.textContent = renderRule.example();\n}\n\nupdateProgress();\n}\n\nfunction playAudio(event, slow = false) { \n if (event) event.stopPropagation(); \n if (!currentWord?.pronunciation) return alert(\"没有发音\"); \n const a = new Audio(currentWord.pronunciation); \n a.playbackRate = slow ? 0.75 : 1;\n a.play();\n}\n\n// 修复:删除重复变量声明,保留一个清晰的赋值流程\nasync function saveMemoryMethod(event) {\n if (event) event.stopPropagation();\n if (!currentWord?.objectId) return;\n const input = document.getElementById(\"memoryMethodInput\");\n if (!input) return;\n const memorymethod = input.value.trim().slice(0, 300);\n try {\n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, {\n method: \"PUT\",\n headers,\n body: JSON.stringify({ memorymethod })\n });\n currentWord.memorymethod = memorymethod;\n document.getElementById(\"root\").textContent =memorymethod ? `词根词缀: ${memorymethod}` : \"词根词缀: 预留\";\n showToast(\"记忆法已保存\");\n } catch (e) {\n showToast(\"保存失败,请稍后重试\");\n }\n}\n\nasync function handleKnow(known, event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n const key = currentMode === studyModes.EN_TO_CN ? \"known_en\" : \"known_cn\";\n currentWord[key] = known;\n if (!known) {\n currentWord.wrongCount = (currentWord.wrongCount || 0) + 1;\n if (!state.wrongWords.find(w => w.objectId === currentWord.objectId)) state.wrongWords.push(currentWord);\n }\n state.todayDone += 1;\n state.phaseDone += 1;\n await saveWord(known ? 1 : 0);\n await nextWord();\n}\n\nasync function nextWord() {\n currentIndex += 1;\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (currentIndex >= queue.length) {\n if (inWrongReview) return openPassageTest(state.groupWords);\n if (currentMode === studyModes.EN_TO_CN) { \n currentMode = studyModes.CN_TO_EN; \n currentIndex = 0; \n state.phaseDone = 0; \n switchMode(\"cn\"); \n return; \n }\n if (state.wrongWords.length) { \n inWrongReview = true; \n currentIndex = 0; \n showToast(`进入错词复习:${state.wrongWords.length} 个`); \n currentWord = state.wrongWords[currentIndex]; \n renderWord();\n return;\n }\n return openPassageTest(state.groupWords);\n }\n currentWord = queue[currentIndex];\n renderWord();\n}\n\n// 修复:仅保留一个generatePassage函数,使用localStorage中的API Key\nasync function generatePassage(words) {\n const genKey = localStorage.getItem(\"genApiKey\");\n if (!genKey) throw new Error(\"缺少API key\");\n const wordPairs = words.map(w => `${w.word}: ${w.meaning}`).join(\"\\n\");\n const prompt = `请用以下10个英语单词写一个120词以内自然短文,要求:\\n1) 必须包含所有单词且每个只出现一次\\n2) 仅返回 JSON 格式 {\\\"passage\\\":\\\"...\\\"}\\n单词列表:\\n${wordPairs}`;\n \n const res = await fetch(`${GEN_BASE_URL}/chat/completions`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", Authorization: `Bearer ${genKey}` },\n body: JSON.stringify({ model: CHEAP_MODEL, temperature: 0.4, messages: [{ role: \"system\", content: \"You are a concise English writing assistant.\" }, { role: \"user\", content: prompt }] })\n });\n \n if (!res.ok) throw new Error(\"短文生成失败\");\n const data = await res.json();\n const raw = data.choices?.[0]?.message?.content?.trim() || \"{}\";\n let parsed;\n try {\n parsed = JSON.parse(raw);\n } catch (e) {\n throw new Error(\"模型返回格式错误\");\n }\n if (!parsed.passage || typeof parsed.passage !== \"string\") throw new Error(\"模型返回格式错误\");\n return parsed.passage;\n}\n\n// 修复:仅保留一个renderPassageWithHighlights函数,使用textContent避免XSS\nfunction renderPassageWithHighlights(passage, words) {\n const dict = new Map(words.map(w => [String(w.word || \"\").toLowerCase(), w.meaning || \"暂无释义\"]));\n const frag = document.createDocumentFragment();\n const parts = String(passage || \"\").split(/(\\b)/);\n \n for (const p of parts) {\n const key = p.toLowerCase().replace(/[^a-z'-]/gi, \"\");\n if (dict.has(key)) {\n const span = document.createElement(\"span\");\n span.className = \"hl-word\";\n span.textContent = p; // 使用textContent而非innerHTML避免XSS\n const meaning = dict.get(key);\n span.title = meaning; // 使用title属性作为提示\n span.style.cursor = \"pointer\";\n span.onclick = () => showToast(`${span.textContent}:${meaning}`);\n frag.appendChild(span);\n } else {\n frag.appendChild(document.createTextNode(p));\n }\n }\n \n const box = document.getElementById(\"passageContent\");\n if (box) {\n box.textContent = \"\";\n box.appendChild(frag);\n }\n}\n\nasync function savePassage(minitext) {\n await fetch(`${BASE_URL}/shortpassage`, { method: \"POST\", headers, body: JSON.stringify({ minitext, userId, score: Date.now() }) });\n}\n\n// 修复:修复错误处理,避免无限递归\nasync function openPassageTest(words) {\n const passageModal = document.getElementById(\"passageModal\");\n const passageContent = document.getElementById(\"passageContent\");\n \n if (!passageModal || !passageContent) return;\n \n passageModal.classList.remove(\"hidden\");\n passageContent.textContent = \"短文生成中...\";\n pendingNextSession = true;\n \n try {\n const passage = await generatePassage(words);\n await savePassage(passage);\n renderPassageWithHighlights(passage, words);\n } catch (e) {\n passageContent.textContent = `短文生成失败:${e.message}。请检查API设置后再试。`;\n // 移除递归调用以避免无限循环\n }\n}\n\nfunction closePassageModal(startNext = false) {\n const passageModal = document.getElementById(\"passageModal\");\n if (passageModal) {\n passageModal.classList.add(\"hidden\");\n }\n if (pendingNextSession && startNext) {\n pendingNextSession = false;\n startSession();\n }\n}\n\nasync function openHistoryModal(event) {\n if (event) event.stopPropagation();\n const list = document.getElementById(\"historyList\");\n if (!list) return;\n \n list.textContent = \"加载中...\";\n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.remove(\"hidden\");\n }\n \n try {\n const where = encodeURIComponent(JSON.stringify({ userId }));\n const res = await fetch(`${BASE_URL}/shortpassage?where=${where}&order=-createdAt&limit=30`, { headers });\n const data = await res.json();\n const rows = data.results || [];\n \n list.textContent = \"\";\n if (!rows.length) { \n list.textContent = \"暂无历史短文\"; \n return; \n }\n \n rows.forEach(r => {\n const item = document.createElement(\"div\");\n item.className = \"history-item\";\n const time = document.createElement(\"div\");\n time.className = \"time\";\n time.textContent = escapeHtml(r.createdAt || \"\"); // 使用textContent避免XSS\n const text = document.createElement(\"div\");\n text.textContent = escapeHtml(r.minitext || \"\"); // 使用textContent避免XSS\n item.appendChild(time);\n item.appendChild(text);\n list.appendChild(item);\n });\n } catch (e) {\n list.textContent = \"加载失败\";\n }\n}\n\nfunction closeHistoryModal() { \n const historyModal = document.getElementById(\"historyModal\");\n if (historyModal) {\n historyModal.classList.add(\"hidden\");\n }\n}\n\nfunction openSettingsModal() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (apiKeyInput) {\n apiKeyInput.value = localStorage.getItem(\"genApiKey\") || \"\";\n }\n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.remove(\"hidden\");\n }\n}\n\nfunction closeSettingsModal() { \n const settingsModal = document.getElementById(\"settingsModal\");\n if (settingsModal) {\n settingsModal.classList.add(\"hidden\");\n }\n}\n\nfunction saveApiKey() {\n const apiKeyInput = document.getElementById(\"apiKeyInput\");\n if (!apiKeyInput) return;\n \n const key = apiKeyInput.value.trim();\n if (!key.startsWith(\"sk-\")) {\n showToast(\"Key 格式不正确\");\n return;\n }\n \n localStorage.setItem(\"genApiKey\", key);\n closeSettingsModal();\n showToast(\"API Key 已保存\");\n}\n\nfunction updateProgress() {\n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n const done = Math.min(state.phaseDone, queue.length);\n const pct = queue.length ? (done / queue.length) * 100 : 0;\n const phase = inWrongReview ? \"错词复习\" : (currentMode === \"en\" ? \"第一阶段 英→中\" : \"第二阶段 中→英\");\n \n const progressText = document.getElementById(\"progressText\");\n const progressBar = document.getElementById(\"progressBar\");\n const sNew = document.getElementById(\"sNew\");\n const sDone = document.getElementById(\"sDone\");\n const sLeft = document.getElementById(\"sLeft\");\n const sStreak = document.getElementById(\"sStreak\");\n \n if (progressText) progressText.textContent = `${phase} · ${Math.min(currentIndex + 1, queue.length)}/${queue.length}`;\n if (progressBar) progressBar.style.width = `${pct}%`;\n if (sNew) sNew.textContent = state.todayNew;\n if (sDone) sDone.textContent = state.todayDone;\n if (sLeft) sLeft.textContent = Math.max(0, (GROUP_SIZE * 2) - state.todayDone);\n if (sStreak) sStreak.textContent = `${state.streak} 天`;\n}\n\nasync function deleteWord(event) {\n if (event) event.stopPropagation();\n if (!currentWord) return;\n if (!confirm(`确定删除单词:${currentWord.word} 吗?`)) return;\n \n await fetch(`${BASE_URL}/Words/${currentWord.objectId}`, { method: \"DELETE\", headers });\n state.groupWords = state.groupWords.filter(w => w.objectId !== currentWord.objectId);\n state.wrongWords = state.wrongWords.filter(w => w.objectId !== currentWord.objectId);\n \n const queue = inWrongReview ? state.wrongWords : state.groupWords;\n if (!queue.length) return startSession();\n if (currentIndex >= queue.length) currentIndex = queue.length - 1;\n \n currentWord = queue[currentIndex];\n renderWord();\n}\n\nasync function startSession() {\n state.groupWords = await getWordGroup(GROUP_SIZE);\n state.wrongWords = [];\n currentMode = studyModes.EN_TO_CN;\n inWrongReview = false;\n currentIndex = 0;\n state.todayNew += state.groupWords.length;\n state.phaseDone = 0;\n \n const segIndicator = document.getElementById(\"segIndicator\");\n if (segIndicator) {\n segIndicator.style.transform = \"translateX(0)\";\n }\n \n document.querySelectorAll(\".seg-btn\").forEach(btn => btn.classList.toggle(\"active\", btn.dataset.mode === \"en\"));\n \n currentWord = state.groupWords[0] || null;\n \n const progressText = document.getElementById(\"progressText\");\n if (!currentWord) { \n if (progressText) {\n progressText.textContent = \"词库不足,请先补充单词\";\n }\n return; \n }\n \n renderWord();\n}\n\nstartSession();\n"}}}} |