Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/870110437-sudo_fantastic-broccoli.json
T

1 line
711 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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(\"&\", \"&amp;\").replaceAll(\"<\", \"&lt;\").replaceAll(\">\", \"&gt;\").replaceAll('\"', \"&quot;\").replaceAll(\"'\", \"&#39;\");\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"}}}}