Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/bxmmxszr_webtemp.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
83 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": "bxmmxszr/webtemp", "files": {"src/translation-practice-4nianji.html": {"19a73deacb8df912fe79eafbbe59d57e0a57c976": {"t": 1786067752.2873461, "n": 73819, "h": "dcb06ac995ecb2d5", "c": "<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>翻译特训 - 四年级英语 - 立信英语</title>\n <link href=\"https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css\" rel=\"stylesheet\">\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css\">\n <style>\n /* 保持与现有页面一致的样式 */\n .passage-container {\n height: 600px; /* 调整高度 */\n overflow-y: auto;\n border: 1px solid #dee2e6;\n border-radius: 0.375rem;\n padding: 1rem;\n background-color: #f8f9fa;\n cursor: default; /* 改为默认光标 */\n /* 移除或注释掉 user-select: none; */\n /* user-select: none; */\n }\n .sentence-highlight {\n background-color: #fff3cd; /* 当前句子高亮 */\n border-radius: 0.25rem;\n padding: 0.25rem;\n }\n /* 新增:可点击的单词样式 */\n .word-token {\n cursor: pointer;\n padding: 0 1px; /* 给单词一点空间 */\n border-radius: 2px;\n transition: background-color 0.2s ease;\n }\n .word-token:hover {\n background-color: #d6d8db; /* 悬停时轻微高亮 */\n }\n /* 新增:被选中的单词样式 */\n .word-token.selected {\n background-color: #ffeb3b; /* 黄色高亮 */\n font-weight: bold;\n }\n /* 新增:选中单词列表区域样式 */\n #selected-words-section {\n margin-top: 2rem;\n }\n #selected-words-table th, #selected-words-table td {\n vertical-align: middle;\n }\n .word-list-container {\n max-height: 300px; /* 设置最大高度 */\n overflow-y: auto; /* 超出时显示滚动条 */\n }\n /* 加载指示器样式 */\n .loading-spinner {\n display: inline-block;\n width: 1rem;\n height: 1rem;\n vertical-align: text-bottom;\n border: 0.2em solid currentColor;\n border-right-color: transparent;\n border-radius: 50%;\n animation: spinner-border 0.75s linear infinite;\n }\n @keyframes spinner-border {\n to { transform: rotate(360deg); }\n }\n </style>\n</head>\n<body>\n <!-- 导航栏 -->\n <nav class=\"navbar navbar-expand-lg navbar-light bg-white fixed-top shadow-sm\">\n <div class=\"container\">\n <a class=\"navbar-brand fw-bold\" href=\"index.html\">立信英语</a>\n <button class=\"navbar-toggler\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarNav\">\n <span class=\"navbar-toggler-icon\"></span>\n </button>\n <div class=\"collapse navbar-collapse\" id=\"navbarNav\">\n <ul class=\"navbar-nav ms-auto\">\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"index.html\">首页</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"index.html#about\">关于</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"index.html#services\">服务</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"si-nian-ji.html\">文章</a></li>\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"si-nian-ji.html\">四年级</a></li>\n <!-- 登录状态显示 - 添加的部分 -->\n <li class=\"nav-item\">\n <span id=\"user-info\" class=\"navbar-text me-2 d-none\"></span>\n <a href=\"profile.html\" class=\"btn btn-outline-primary btn-sm me-1 d-none\" id=\"profile-btn\">个人中心</a>\n <a href=\"learning-records.html\" class=\"btn btn-outline-info btn-sm me-1 d-none\" id=\"records-btn\">学习记录</a>\n <a href=\"admin-vocabulary.html\" class=\"btn btn-outline-warning btn-sm me-1 d-none\" id=\"admin-btn\">管理员</a>\n <button id=\"login-btn\" class=\"btn btn-outline-primary btn-sm me-1\">登录</button>\n <button id=\"logout-btn\" class=\"btn btn-outline-secondary btn-sm d-none\">登出</button>\n </li>\n <!-- 登录状态显示结束 -->\n <li class=\"nav-item\"><a class=\"nav-link\" href=\"index.html#contact\">联系</a></li>\n </ul>\n </div>\n </div>\n </nav>\n\n <!-- 页面内容 -->\n <div class=\"container mt-5 pt-5\">\n <div class=\"row\">\n <!-- 侧边栏 -->\n <div class=\"col-lg-3 mb-4\">\n <div class=\"card\">\n <div class=\"card-header\">\n <h5>四年级英语</h5>\n </div>\n <div class=\"list-group list-group-flush\">\n <a href=\"si-nian-ji.html\" class=\"list-group-item list-group-item-action\">返回主页面</a>\n <a href=\"#\" class=\"list-group-item list-group-item-action active\">翻译特训</a>\n <a href=\"core-vocabulary-4nianji.html\" class=\"list-group-item list-group-item-action\">核心词汇积累</a>\n <a href=\"vocabulary-quiz-4nianji.html\" class=\"list-group-item list-group-item-action\">词汇测试</a>\n <a href=\"si-nian-ji.html\" class=\"list-group-item list-group-item-action\">← 返回文章</a>\n </div>\n </div>\n </div>\n\n <!-- 主要内容区域 -->\n <div class=\"col-lg-9\">\n <nav aria-label=\"breadcrumb\">\n <ol class=\"breadcrumb\">\n <li class=\"breadcrumb-item\"><a href=\"index.html\">首页</a></li>\n <li class=\"breadcrumb-item\"><a href=\"si-nian-ji.html\">文章</a></li>\n <li class=\"breadcrumb-item\"><a href=\"si-nian-ji.html\">四年级</a></li>\n <li class=\"breadcrumb-item active\">翻译特训</li>\n </ol>\n </nav>\n <div class=\"d-flex justify-content-between align-items-center mb-4\">\n <h2>四年级翻译特训</h2>\n <div>\n <!-- 刷新文章按钮 -->\n <button id=\"refresh-passage-btn\" class=\"btn btn-outline-secondary\" onclick=\"translationTrainer.loadPassage()\">\n <i class=\"bi bi-arrow-clockwise\"></i> 刷新文章\n </button>\n <!-- 加载指示器 -->\n <span id=\"loading-indicator\" class=\"ms-2 d-none\">\n <span class=\"loading-spinner\" role=\"status\"></span>\n <span class=\"ms-1\">加载中...</span>\n </span>\n </div>\n </div>\n\n <!-- 学习进度条 -->\n <div class=\"progress-container mb-4\">\n <div class=\"d-flex justify-content-between align-items-center mb-2\">\n <span>训练进度</span>\n <span id=\"progress-text\">0/0</span>\n </div>\n <div class=\"progress\">\n <div id=\"progress-bar\" class=\"progress-bar\" role=\"progressbar\" style=\"width: 0%\"></div>\n </div>\n </div>\n\n <div class=\"row\">\n <!-- 左侧 - 英文短文 -->\n <div class=\"col-md-6 mb-4\">\n <div class=\"card h-100\">\n <div class=\"card-header bg-primary text-white d-flex justify-content-between align-items-center\">\n <h5 class=\"card-title mb-0\"><i class=\"bi bi-book\"></i> 英文短文</h5>\n <small id=\"passage-title\" class=\"text-white-50\"></small>\n </div>\n <div class=\"card-body\">\n <!-- 移除 click 事件监听器 -->\n <div id=\"passage-container\" class=\"passage-container\">\n <!-- 短文内容将通过 JavaScript 动态加载 -->\n <p class=\"text-muted\">正在加载短文...</p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- 右侧 - 翻译训练区 -->\n <div class=\"col-md-6\">\n <!-- 上部分 - 待翻译句子 -->\n <div class=\"card mb-3\">\n <div class=\"card-header bg-info text-white\">\n <h5 class=\"card-title mb-0\"><i class=\"bi bi-translate\"></i> 待翻译句子</h5>\n </div>\n <div class=\"card-body\">\n <p id=\"current-sentence\" class=\"mb-0 text-muted\">请先加载短文...</p>\n </div>\n </div>\n\n <!-- 中间部分 - 用户输入和提交 -->\n <div class=\"card mb-3\">\n <div class=\"card-header\">\n <h5 class=\"card-title mb-0\"><i class=\"bi bi-pencil-square\"></i> 我的翻译</h5>\n </div>\n <div class=\"card-body\">\n <div class=\"mb-3\">\n <label for=\"user-translation\" class=\"form-label\">请输入您的翻译:</label>\n <textarea class=\"form-control\" id=\"user-translation\" rows=\"3\" placeholder=\"在此输入您的翻译...\"></textarea>\n </div>\n <button id=\"submit-translation-btn\" class=\"btn btn-success w-100\" onclick=\"translationTrainer.submitTranslation()\" disabled>\n <span id=\"submit-btn-text\">提交翻译</span>\n <span id=\"submit-btn-loading\" class=\"spinner-border spinner-border-sm d-none\" role=\"status\" aria-hidden=\"true\"></span>\n </button>\n </div>\n </div>\n\n <!-- 下部分 - AI翻译结果 -->\n <div class=\"card\">\n <div class=\"card-header bg-secondary text-white\"> <!-- 确保有 bg-secondary 和 text-white 类 -->\n <h5 class=\"card-title mb-0\"><i class=\"bi bi-robot\"></i> AI参考翻译</h5>\n </div>\n <div class=\"card-body translation-section\"> <!-- 确保有 translation-section 类 -->\n <!-- 确保 ID 是 ai-translation -->\n <p id=\"ai-translation\" class=\"mb-0 translation-placeholder\">\n 提交您的翻译后,AI参考翻译将显示在这里。\n </p>\n </div>\n </div>\t\t\t\t\t\t\n </div>\n </div>\n\n <!-- 选中的单词列表区域 -->\n <div id=\"selected-words-section\" class=\"mt-5\">\n <div class=\"card\">\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <h5 class=\"mb-0\"><i class=\"bi bi-list-check\"></i> 选中的单词</h5>\n <button id=\"clear-selected-btn\" class=\"btn btn-outline-danger btn-sm\" onclick=\"translationTrainer.clearSelectedWords()\">\n <i class=\"bi bi-trash\"></i> 清空列表\n </button>\n </div>\n <div class=\"card-body\">\n <div id=\"message-area\" class=\"alert d-none\" role=\"alert\"></div>\n <div class=\"word-list-container\"> <!-- 添加容器用于滚动 -->\n <table id=\"selected-words-table\" class=\"table table-striped table-hover\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\">#</th>\n <th scope=\"col\">单词</th>\n <th scope=\"col\">所在句子</th>\n <th scope=\"col\">操作</th>\n </tr>\n </thead>\n <tbody id=\"selected-words-tbody\">\n <tr id=\"no-words-row\">\n <td colspan=\"4\" class=\"text-center text-muted\">暂无选中的单词。请点击短文中的单词来添加。</td>\n </tr>\n </tbody>\n </table>\n </div>\n <button id=\"save-to-vocab-btn\" class=\"btn btn-primary mt-3\" onclick=\"translationTrainer.saveAllToVocabulary()\" disabled>\n <i class=\"bi bi-cloud-arrow-up\"></i> 全部添加到生词表\n </button>\n </div>\n </div>\n </div>\n\n </div>\n </div>\n </div>\n\n <!-- 页脚 -->\n <footer class=\"bg-dark text-white py-4 mt-5\">\n <div class=\"container\">\n <div class=\"row\">\n <div class=\"col-md-6\">\n <p class=\"mb-0\">&copy; 2024 立信英语. 保留所有权利.</p>\n </div>\n <div class=\"col-md-6 text-md-end\">\n <p class=\"mb-0\">用心打造高效英语学习平台</p>\n </div>\n </div>\n </div>\n </footer>\n\n <script src=\"https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js\"></script>\n <script src=\"api.js\"></script>\n <script>\n // --- DeepSeek API 配置 ---\n // 注意:将 API Key 硬编码在前端代码中存在安全风险!\n // 生产环境中,强烈建议通过后端代理请求 DeepSeek API,\n // 将 API Key 保存在后端环境变量中。\n const DEEPSEEK_API_KEY = 'sk-c9ed248ef3a84ca1909f6f73d8424d'; // <-- 请替换为实际 API Key\n const DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions';\n\t\n // --- 翻译特训系统 - 四年级版本 (支持后端加载短文 + 改进的单词选择功能) ---\n class TranslationTrainer {\n constructor() {\n this.apiBaseUrl = window.ApiBaseUrl || 'http://localhost:3000/api'; // 从 api.js 获取\n this.passageSentences = []; // 存储分割后的句子\n this.currentIndex = -1; // 当前句子索引\n this.currentPassageId = null; // 当前加载的短文ID\n this.grade = 'grade4'; // 默认加载五年级内容 (根据你的CSV)\n this.selectedWords = []; // 存储选中的单词 { text, sentence, passageId, addedAt }\n this.localStorageKey = 'selectedWords_grade4'; // 用于localStorage的键\n this.init();\n }\n\n init() {\n this.checkAuth(); // 检查登录状态\n this.loadPassage(); // 初始化时加载短文\n this.loadSelectedWordsFromStorage(); // 页面加载时从 localStorage 恢复选中的单词\n this.renderSelectedWords(); // 渲染恢复的单词列表\n }\n\n // --- 原有功能方法 (保持不变或微调) ---\n async checkAuth() {\n const token = localStorage.getItem('token');\n const userInfo = document.getElementById('user-info');\n const profileBtn = document.getElementById('profile-btn');\n const recordsBtn = document.getElementById('records-btn');\n const adminBtn = document.getElementById('admin-btn');\n const loginBtn = document.getElementById('login-btn');\n const logoutBtn = document.getElementById('logout-btn');\n\n if (!token) {\n loginBtn.classList.remove('d-none');\n // window.location.href = 'login.html'; // 未登录直接跳转 (注释掉以便查看页面)\n return;\n }\n\n try {\n const response = await fetch(`${this.apiBaseUrl}/user`, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json'\n }\n });\n const result = await response.json();\n if (result.success) {\n const user = result.user;\n userInfo.textContent = `欢迎, ${user.name || user.email}`;\n userInfo.classList.remove('d-none');\n profileBtn.classList.remove('d-none');\n recordsBtn.classList.remove('d-none');\n // 检查是否为管理员 (应从后端获取)\n const adminEmails = ['admin@example.com', 'admin@liuxinenglish.com'];\n if (adminEmails.includes(user.email)) {\n adminBtn.classList.remove('d-none');\n }\n loginBtn.classList.add('d-none');\n logoutBtn.classList.remove('d-none');\n } else {\n throw new Error(result.error);\n }\n } catch (error) {\n console.error('检查登录状态失败:', error);\n localStorage.removeItem('token');\n // window.location.href = 'login.html'; // 跳转到登录页 (注释掉以便查看页面)\n }\n }\n\n async loadPassage() {\n const refreshBtn = document.getElementById('refresh-passage-btn');\n const loadingIndicator = document.getElementById('loading-indicator');\n const passageContainer = document.getElementById('passage-container');\n const passageTitle = document.getElementById('passage-title');\n\n refreshBtn.disabled = true;\n loadingIndicator.classList.remove('d-none');\n passageContainer.innerHTML = '<p class=\"text-muted\">正在加载短文...</p>';\n passageTitle.textContent = '';\n\n try {\n const token = localStorage.getItem('token');\n if (!token) {\n throw new Error('未登录');\n }\n\n // 确保 grade 参数与后端支持的和CSV中的匹配\n const response = await fetch(`${this.apiBaseUrl}/passages/random?grade=${this.grade}`, {\n method: 'GET',\n headers: {\n 'Authorization': `Bearer ${token}`, // 确保包含认证头\n 'Content-Type': 'application/json'\n }\n });\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `加载失败: ${response.status}`);\n }\n\n const result = await response.json();\n console.log(\"后端返回的短文数据:\", result); // 调试信息\n if (result.success && result.passage) {\n this.currentPassageId = result.passage._id;\n passageTitle.textContent = result.passage.title || '短文';\n\n // --- 关键修改 1: 使用 content 字段 ---\n const passageContent = result.passage.content;\n if (passageContent) {\n // --- 关键修改 2: 调用 processPassage 处理 content ---\n this.processPassage(passageContent);\n } else {\n throw new Error('短文内容为空');\n }\n\n // 注意:currentIndex, nextSentence, displayCurrentSentence 的调用\n // 已经在 processPassage 内部处理了\n } else {\n throw new Error(result.error || '获取短文失败');\n }\n } catch (error) {\n console.error('加载短文失败:', error);\n passageContainer.innerHTML = `<p class=\"text-danger\">加载短文失败: ${error.message}</p>`;\n } finally {\n refreshBtn.disabled = false;\n loadingIndicator.classList.add('d-none');\n }\n }\n\n // --- 关键修改:processPassage 方法 ---\n // 处理原始的 passage.content 字符串,分割成句子并渲染\n processPassage(passageText) {\n // 简单的句子分割 (实际应用中可能需要更复杂的NLP处理)\n // 这里按句号、感叹号、问号分割,并过滤掉空字符串和只包含空白符的句子\n this.passageSentences = passageText.split(/[.!?]+/)\n .map(s => s.trim())\n .filter(s => s.length > 0);\n\n if (this.passageSentences.length === 0) {\n document.getElementById('passage-container').innerHTML = '<div class=\"alert alert-warning\">短文内容为空或无法解析。</div>';\n return;\n }\n\n this.renderPassage(); // 渲染分割后的句子\n this.currentIndex = -1; // 重置索引\n this.nextSentence(); // 加载第一个句子\n this.updateProgress(); // 更新进度条\n }\n\n // --- 关键修改:renderPassage 方法 ---\n // 渲染 this.passageSentences 数组中的句子,并为每个单词添加点击事件\n renderPassage() {\n const container = document.getElementById('passage-container');\n container.innerHTML = ''; // 清空容器\n\n if (!this.passageSentences || this.passageSentences.length === 0) {\n container.innerHTML = '<p class=\"text-warning\">短文内容为空。</p>';\n return;\n }\n\n this.passageSentences.forEach((sentence, index) => {\n const p = document.createElement('p');\n p.id = `sentence-${index}`;\n // 确保句子以标点结尾(如果后端没有处理)\n const trimmedSentence = sentence.trim();\n if (trimmedSentence) { // 检查句子是否为空\n // --- 关键修改:将句子中的单词包裹在 <span> 中 ---\n const sentenceHtml = trimmedSentence\n .split(/\\s+/) // 按空格分割单词\n .map(word => {\n // 简单清理单词,移除可能附带的标点(这里简化处理)\n const cleanWord = word.replace(/[^\\w'-]/g, '');\n if (cleanWord) {\n // 为每个单词创建一个可点击的 span\n return `<span class=\"word-token\" data-word=\"${cleanWord}\" data-sentence=\"${trimmedSentence}\">${word}</span>`;\n } else {\n // 如果清理后为空(例如纯标点),则直接显示\n return word;\n }\n })\n .join(' '); // 用空格重新连接\n\n p.innerHTML = sentenceHtml +\n (trimmedSentence.endsWith('.') ||\n trimmedSentence.endsWith('!') ||\n trimmedSentence.endsWith('?') ? '' : '.');\n container.appendChild(p);\n }\n });\n\n // --- 关键修改:为所有 .word-token 元素添加点击事件监听器 ---\n container.querySelectorAll('.word-token').forEach(token => {\n token.addEventListener('click', (event) => {\n event.stopPropagation(); // 阻止事件冒泡\n const word = event.currentTarget.getAttribute('data-word');\n const sentence = event.currentTarget.getAttribute('data-sentence');\n\n if (word && sentence) {\n this.handleWordClick(word, sentence);\n }\n });\n });\n }\n\n // --- 新增:处理单词点击事件 ---\n handleWordClick(word, sentence) {\n console.log(\"点击的单词:\", word);\n // 创建单词对象\n const wordObj = {\n text: word,\n sentence: sentence,\n passageId: this.currentPassageId || 'unknown_passage',\n addedAt: new Date().toISOString()\n };\n\n // 检查是否已选中 (避免重复添加)\n const isAlreadySelected = this.selectedWords.some(w => w.text === wordObj.text && w.sentence === wordObj.sentence);\n if (!isAlreadySelected) {\n this.selectedWords.push(wordObj);\n this.saveSelectedWordsToStorage(); // 保存到 localStorage\n this.renderSelectedWords(); // 更新页面显示\n\n // --- 关键修改:在短文中高亮单词 ---\n // 找到所有匹配的 token 并添加 selected 类\n document.querySelectorAll(`.word-token[data-word=\"${word}\"][data-sentence=\"${sentence}\"]`).forEach(token => {\n token.classList.add('selected');\n });\n\n // 显示提示信息\n this.showMessage(`已添加 \"${word}\" 到选词列表。`, 'success');\n } else {\n this.showMessage(`\"${word}\" 已在选词列表中。`, 'info');\n }\n }\n\n //// // 移除之前句子的高亮\n // document.querySelectorAll('.sentence-highlight').forEach(el => {\n // el.classList.remove('sentence-highlight');\n // });() {\n // const sentenceElement = document.getElementById('current-sentence');\n // const userInput = document.getElementById('user-translation');\n // const submitBtn = document.getElementById('submit-translation-btn');\n // const aiTranslationElement = document.getElementById('ai-translation');\n\t\t\t//\n // // 移除之前句子的高亮\n // document.querySelectorAll('.sentence-highlight').forEach(el => {\n // el.classList.remove('sentence-highlight');\n // });\n\t\t\t//\n // if (this.currentIndex >= 0 && this.currentIndex < this.passageSentences.length) {\n // const currentSentence = this.passageSentences[this.currentIndex];\n // sentenceElement.textContent = currentSentence;\n // // 高亮当前句子\n // const sentencePara = document.getElementById(`sentence-${this.currentIndex}`);\n // if (sentencePara) {\n // sentencePara.classList.add('sentence-highlight');\n // // 可选:滚动到高亮句子\n // sentencePara.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n // }\n // userInput.value = '';\n // userInput.disabled = false;\n // submitBtn.disabled = false;\n // aiTranslationElement.textContent = '提交您的翻译后,AI参考翻译将显示在这里。';\n // aiTranslationElement.classList.add('translation-placeholder');\n // aiTranslationElement.classList.remove('text-dark', 'text-danger');\n // } else {\n // sentenceElement.textContent = '训练完成!';\n // }\n // this.updateProgress(); // 更新进度条\n //}\n\n // 显示当前句子 (修改后 - 确保重置 AI 翻译区域)\n displayCurrentSentence() {\n const sentenceElement = document.getElementById('current-sentence');\n const userInput = document.getElementById('user-translation');\n const submitBtn = document.getElementById('submit-translation-btn');\n const aiTranslationElement = document.getElementById('ai-translation'); // 获取 AI 翻译元素\n\t\t\t\t\n\t\t\t\t// 移除之前句子的高亮\n document.querySelectorAll('.sentence-highlight').forEach(el => {\n el.classList.remove('sentence-highlight');\n });\n\n if (this.currentIndex >= 0 && this.currentIndex < this.passageSentences.length) {\n const currentSentence = this.passageSentences[this.currentIndex];\n sentenceElement.textContent = currentSentence;\n\n // 高亮当前句子\n const sentencePara = document.getElementById(`sentence-${this.currentIndex}`);\n if (sentencePara) {\n sentencePara.classList.add('sentence-highlight');\n sentencePara.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n }\n\n // --- 重置用户输入和 AI 翻译区域 ---\n userInput.value = '';\n userInput.disabled = false;\n submitBtn.disabled = false;\n\n // 重置 AI 翻译区域为初始占位符状态\n if (aiTranslationElement) {\n aiTranslationElement.textContent = '提交您的翻译后,AI参考翻译将显示在这里。';\n // 确保添加 placeholder 类,移除可能的结果/错误类\n aiTranslationElement.classList.add('translation-placeholder');\n aiTranslationElement.classList.remove('text-dark', 'text-danger');\n }\n \n } else {\n sentenceElement.textContent = '没有更多句子了。';\n // 如果没有更多句子,也可以禁用输入和按钮\n userInput.disabled = true;\n submitBtn.disabled = true;\n // 可选:也重置 AI 翻译区域\n if (aiTranslationElement) {\n aiTranslationElement.textContent = '已完成所有句子。';\n aiTranslationElement.classList.add('translation-placeholder');\n aiTranslationElement.classList.remove('text-dark', 'text-danger');\n }\n }\n this.updateProgress(); // 更新进度条\n }\n\t\t\t\n nextSentence() {\n this.currentIndex++;\n if (this.currentIndex >= this.passageSentences.length) {\n const sentenceElement = document.getElementById('current-sentence');\n sentenceElement.textContent = '🎉 恭喜!您已完成当前短文的所有句子翻译训练。';\n document.getElementById('submit-translation-btn').disabled = true;\n document.getElementById('user-translation').disabled = true;\n console.log(\"所有句子翻译完成\");\n return;\n }\n this.displayCurrentSentence();\n }\n\n //async submitTranslation() {\n // const userInput = document.getElementById('user-translation').value.trim();\n // if (!userInput) {\n // alert('请输入您的翻译。');\n // return;\n // }\n\t\t\t//\n // // 提前加载下一个句子\n // this.nextSentence();\n\t\t\t//\n // const submitBtn = document.getElementById('submit-translation-btn');\n // const submitBtnText = document.getElementById('submit-btn-text');\n // const submitBtnLoading = document.getElementById('submit-btn-loading');\n\t\t\t//\n // // 显示加载状态\n // submitBtn.disabled = true;\n // submitBtnText.textContent = '处理中...';\n // submitBtnLoading.classList.remove('d-none');\n\t\t\t//\n // try {\n // // 调用AI翻译API\n // const aiTranslation = await this.getAITranslation(this.passageSentences[this.currentIndex - 1]); // 使用提交前的句子\n // // 显示AI翻译结果\n // const aiTranslationElement = document.getElementById('ai-translation');\n // aiTranslationElement.textContent = aiTranslation;\n // aiTranslationElement.classList.remove('translation-placeholder');\n // aiTranslationElement.classList.add('text-dark');\n // } catch (error) {\n // console.error('翻译请求失败:', error);\n // const aiTranslationElement = document.getElementById('ai-translation');\n // aiTranslationElement.textContent = `翻译失败: ${error.message}`;\n // aiTranslationElement.classList.remove('translation-placeholder');\n // aiTranslationElement.classList.add('text-danger');\n // } finally {\n // // 恢复按钮状态\n // setTimeout(() => {\n // submitBtn.disabled = false;\n // submitBtnText.textContent = '提交翻译';\n // submitBtnLoading.classList.add('d-none');\n // // 清空用户输入框 (在加载新句子后)\n // document.getElementById('user-translation').value = '';\n // }, 500); // 稍微延迟恢复,让用户感知到处理过程\n // }\n //}\n\n // 提交用户翻译 \n //async submitTranslation() {\n // const userInput = document.getElementById('user-translation').value.trim();\n // const submitBtn = document.getElementById('submit-translation-btn');\n // const submitBtnText = document.getElementById('submit-btn-text');\n // const submitBtnLoading = document.getElementById('submit-btn-loading');\n // const aiTranslationElement = document.getElementById('ai-translation');\n\t\t\t//\n // if (!userInput) {\n // alert('请输入您的翻译。');\n // return;\n // }\n\t\t\t//\n // // --- 修改点 1: 立即加载下一个句子 ---\n // // 在处理 AI 翻译之前,先尝试加载下一个句子\n // // 这样可以减少用户等待下一个句子的时间\n // const currentIndexBeforeSubmit = this.currentIndex; // 保存当前索引\n // this.nextSentence(); // 立即加载下一个句子\n // this.updateProgress(); // 更新进度条\n // \n // // 显示加载状态 (用于 AI 翻译)\n // submitBtn.disabled = true;\n // submitBtnText.textContent = '处理中...';\n // submitBtnLoading.classList.remove('d-none');\n\t\t\t//\n // try {\n // // --- 修改点 2: 调用新的 getAITranslation 方法 ---\n // // 在实际应用中,这里会调用您的后端API,后端再调用 DeepSeek 或其他翻译服务\n // // 确保传入的是提交前的句子\n // const aiTranslation = await this.getAITranslation(this.passageSentences[currentIndexBeforeSubmit]);\n // \n // // 显示AI翻译结果\n // aiTranslationElement.textContent = aiTranslation;\n // aiTranslationElement.classList.remove('translation-placeholder');\n // aiTranslationElement.classList.add('text-dark');\n\t\t\t//\n // } catch (error) {\n // console.error('[翻译特训] 获取AI翻译失败:', error);\n // // 即使 AI 翻译失败,也应显示错误,但下一个句子已经加载了\n // aiTranslationElement.textContent = `翻译失败: ${error.message}`;\n // aiTranslationElement.classList.remove('translation-placeholder');\n // aiTranslationElement.classList.add('text-danger'); // 使用红色显示错误\n // } finally {\n // // 恢复按钮状态\n // // 稍微延迟恢复,让用户感知到处理过程\n // setTimeout(() => {\n // submitBtn.disabled = false;\n // submitBtnText.textContent = '提交翻译';\n // submitBtnLoading.classList.add('d-none');\n // // 清空用户输入框 (在加载新句子后)\n // document.getElementById('user-translation').value = '';\n // }, 500); // 与上面的延迟保持一致或稍长\n // }\n //}\n\n //// 提交用户翻译 (添加保存翻译记录逻辑)\n //async submitTranslation() {\n // const userInput = document.getElementById('user-translation').value.trim();\n // const submitBtn = document.getElementById('submit-translation-btn');\n // const submitBtnText = document.getElementById('submit-btn-text');\n // const submitBtnLoading = document.getElementById('submit-btn-loading');\n // const aiTranslationElement = document.getElementById('ai-translation');\n\t\t\t//\n // if (!userInput) {\n // alert('请输入您的翻译。');\n // return;\n // }\n\t\t\t//\n // // --- 修改点 1: 立即加载下一个句子 ---\n // const currentIndexBeforeSubmit = this.currentIndex;\n // this.nextSentence();\n // this.updateProgress();\n // // --- 修改点 1 结束 ---\n\t\t\t//\n // // 显示加载状态 (用于 AI 翻译和保存记录)\n // submitBtn.disabled = true;\n // submitBtnText.textContent = '处理中...';\n // submitBtnLoading.classList.remove('d-none');\n\t\t\t//\n // try {\n // // --- 修改点 2: 调用后端 AI 翻译服务 ---\n // const currentSentence = this.passageSentences[currentIndexBeforeSubmit];\n // const aiTranslation = await this.getAITranslation(currentSentence);\n // // --- 修改点 2 结束 ---\n\t\t\t//\n // // --- 新增点 3: 保存翻译记录到后端 ---\n // await this.saveTranslationRecord(\n // this.currentPassageId, // 短文ID\n // currentSentence, // 英文原句\n // userInput, // 用户翻译\n // aiTranslation // AI翻译\n // );\n // // --- 新增点 3 结束 ---\n\t\t\t//\n // // 显示AI翻译结果\n // aiTranslationElement.textContent = aiTranslation;\n // aiTranslationElement.classList.remove('translation-placeholder');\n // aiTranslationElement.classList.add('text-dark');\n\t\t\t//\n // } catch (error) {\n // console.error('[翻译特训] 处理翻译或保存记录失败:', error);\n // aiTranslationElement.textContent = `处理失败: ${error.message}`;\n // aiTranslationElement.classList.remove('translation-placeholder');\n // aiTranslationElement.classList.add('text-danger');\n // } finally {\n // // 恢复按钮状态\n // setTimeout(() => {\n // submitBtn.disabled = false;\n // submitBtnText.textContent = '提交翻译';\n // submitBtnLoading.classList.add('d-none');\n // document.getElementById('user-translation').value = '';\n // }, 500);\n // }\n //}\n\n // --- 提交用户翻译 (确保保存翻译记录) ---\n async submitTranslation() {\n const userInput = document.getElementById('user-translation').value.trim();\n const submitBtn = document.getElementById('submit-translation-btn');\n const submitBtnText = document.getElementById('submit-btn-text');\n const submitBtnLoading = document.getElementById('submit-btn-loading');\n const aiTranslationElement = document.getElementById('ai-translation');\n\n if (!userInput) {\n alert('请输入您的翻译。');\n return;\n }\n\n // --- 修改点 1: 获取提交前的句子信息 ---\n // 在加载下一个句子之前,保存当前句子的信息,用于保存记录\n const currentIndexBeforeSubmit = this.currentIndex;\n const currentSentenceBeforeSubmit = this.passageSentences[currentIndexBeforeSubmit];\n const currentPassageIdBeforeSubmit = this.currentPassageId;\n // --- 修改点 1 结束 ---\n\n // --- 修改点 2: 立即加载下一个句子 ---\n this.nextSentence();\n this.updateProgress();\n // --- 修改点 2 结束 ---\n\n // 显示加载状态 (用于 AI 翻译和保存记录)\n submitBtn.disabled = true;\n submitBtnText.textContent = '处理中...';\n submitBtnLoading.classList.remove('d-none');\n\n try {\n // --- 修改点 3: 调用后端 AI 翻译服务 ---\n // 使用提交前的句子\n const aiTranslation = await this.getAITranslation(currentSentenceBeforeSubmit);\n // --- 修改点 3 结束 ---\n\n // --- 新增点 4: 保存翻译记录到后端 ---\n // 使用提交前保存的信息\n await this.saveTranslationRecord(\n currentPassageIdBeforeSubmit,\n currentSentenceBeforeSubmit,\n userInput,\n aiTranslation\n );\n // --- 新增点 4 结束 ---\n\n // 显示AI翻译结果\n aiTranslationElement.textContent = aiTranslation;\n aiTranslationElement.classList.remove('translation-placeholder');\n aiTranslationElement.classList.add('text-dark');\n\n } catch (error) {\n console.error('[翻译特训] 处理翻译或保存记录失败:', error);\n aiTranslationElement.textContent = `处理失败: ${error.message}`;\n aiTranslationElement.classList.remove('translation-placeholder');\n aiTranslationElement.classList.add('text-danger');\n } finally {\n // 恢复按钮状态\n setTimeout(() => {\n submitBtn.disabled = false;\n submitBtnText.textContent = '提交翻译';\n submitBtnLoading.classList.add('d-none');\n // 清空用户输入框 (在加载新句子后)\n document.getElementById('user-translation').value = '';\n }, 500); // 稍微延迟恢复,让用户感知到处理过程\n }\n }\n \n // --- 保存翻译记录到后端 ---\n //async saveTranslationRecord(passageId, sentence, userTranslation, aiTranslation) {\n // const token = localStorage.getItem('token');\n // if (!token) {\n // console.warn(\"用户未登录,无法保存翻译记录。\");\n // return;\n // }\n\t\t\t//\n // try {\n // const response = await fetch(`${this.apiBaseUrl}/translation-records`, {\n // method: 'POST',\n // headers: {\n // 'Authorization': `Bearer ${token}`,\n // 'Content-Type': 'application/json'\n // },\n // body: JSON.stringify({\n // passageId,\n // sentence,\n // userTranslation,\n // aiTranslation\n // })\n // });\n\t\t\t//\n // if (!response.ok) {\n // const errorData = await response.json().catch(() => ({}));\n // throw new Error(errorData.error || `HTTP error! status: ${response.status}`);\n // }\n\t\t\t//\n // const result = await response.json();\n // console.log(\"翻译记录保存成功:\", result.message);\n // } catch (error) {\n // // 记录保存失败不应中断主流程,仅记录错误\n // console.error(\"保存翻译记录到后端失败:\", error);\n // // 可以选择性地通知用户,但这通常不是关键错误\n // // showMessage('翻译已提交,但保存记录失败: ' + error.message, 'warning');\n // }\n //}\n\n // --- 保存翻译记录到后端 (如果尚不存在) ---\n async saveTranslationRecord(passageId, sentence, userTranslation, aiTranslation) {\n //--const token = localStorage.getItem('token');\n\t\t\t\tconsole.log(\"[DEBUG] saveTranslationRecord called with:\", { passageId, sentence, userTranslation, aiTranslation }); // <-- 添加\n\n const token = localStorage.getItem('token');\n console.log(\"[DEBUG] Retrieved token:\", token ? `${token.substring(0, 10)}...` : 'None'); // <-- 添加\n \n if (!token) {\n console.warn(\"用户未登录,无法保存翻译记录。\");\n // 可以选择性地显示消息\n // this.showMessage('请先登录以保存翻译记录。', 'warning');\n return;\n }\n\n try {\n console.log(\"[DEBUG] About to fetch POST /api/translation-records\"); // <-- 添加\n \n\t\t\t\t\tconst response = await fetch(`${this.apiBaseUrl}/translation-records`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n passageId,\n sentence,\n userTranslation,\n aiTranslation\n })\n });\n\t\t\t\t\t\n\t\t\t\t\tconsole.log(\"[DEBUG] Fetch response received:\", response.status, response.statusText); // <-- 添加\n\n\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({}));\n throw new Error(errorData.error || `HTTP error! status: ${response.status}`);\n }\n\n const result = await response.json();\n\t\t\t\t\tconsole.log(\"[DEBUG] Save successful, server response:\", result); // <-- 添加\n \n console.log(\"翻译记录保存成功:\", result.message);\n // 可选:显示成功消息\n // this.showMessage('翻译记录已保存。', 'success');\n } catch (error) {\n // 记录保存失败不应中断主流程,仅记录错误\n console.error(\"保存翻译记录到后端失败:\", error);\n // 可以选择性地通知用户,但这通常不是关键错误\n // this.showMessage('翻译已提交,但保存记录失败: ' + error.message, 'warning');\n }\n }\n\t\t\t\n //// 模拟AI翻译API调用\n //async getAITranslation(sentence) {\n // // 模拟网络延迟\n // await new Promise(resolve => setTimeout(resolve, 1000));\n // // 模拟翻译结果\n // const mockTranslations = {\n // \"Last Sunday, my family and I went to the Central Park.\": \"上个星期天,我和我的家人去了中央公园。\",\n // \"The weather was sunny and warm.\": \"天气晴朗而温暖。\",\n // \"There were many people in the park.\": \"公园里有很多人。\",\n // \"Some children were flying kites.\": \"一些孩子在放风筝。\",\n // \"Some old people were dancing.\": \"一些老人在跳舞。\",\n // \"My sister and I played on the slide.\": \"我和我妹妹在滑梯上玩。\",\n // \"My parents sat on the bench and watched us.\": \"我的父母坐在长椅上看着我们。\",\n // \"We bought ice cream from a vendor.\": \"我们从一个小贩那里买了冰淇淋。\",\n // \"It was a wonderful day!\": \"这是美好的一天!\"\n // };\n // return mockTranslations[sentence] || `(模拟翻译)${sentence}`;\n //}\n\n////=======================调用真实DS API==========================================////\n\t\t/*\n // --- 调用 DeepSeek API 获取翻译 ---\n async getAITranslation(sentence) {\n // 1. 输入验证\n if (!sentence || typeof sentence !== 'string') {\n throw new Error('Invalid sentence provided for translation.');\n }\n\n // 2. 检查 API Key\n if (!DEEPSEEK_API_KEY || DEEPSEEK_API_KEY === 'YOUR_DEEPSEEK_API_KEY_HERE') {\n console.error('DeepSeek API Key 未配置或为默认值。');\n // 可以选择回退到模拟翻译或抛出错误\n // throw new Error('DeepSeek API Key 未配置。');\n // --- 回退到模拟翻译 (开发/测试用) ---\n await new Promise(resolve => setTimeout(resolve, 500)); // 模拟网络延迟\n const mockTranslations = {\n \"Last Sunday, my family and I went to the Central Park.\": \"上个星期天,我和我的家人去了中央公园。\",\n \"The weather was sunny and warm.\": \"天气晴朗而温暖。\",\n \"There were many people in the park.\": \"公园里有很多人。\",\n \"Some children were flying kites.\": \"一些孩子在放风筝。\",\n \"Some old people were dancing.\": \"一些老人在跳舞。\",\n \"My sister and I played on the slide.\": \"我和我妹妹在滑梯上玩。\",\n \"My parents sat on the bench and watched us.\": \"我的父母坐在长椅上看着我们。\",\n \"We bought ice cream from a vendor.\": \"我们从一个小贩那里买了冰淇淋。\",\n \"It was a wonderful day!\": \"这是美好的一天!\"\n // 添加更多模拟翻译...\n };\n const mockResult = mockTranslations[sentence];\n if (mockResult) {\n console.warn('使用模拟翻译结果,因为 DeepSeek API Key 未配置。');\n return mockResult;\n } else {\n return `(模拟) ${sentence}`; // 默认模拟\n }\n // --- 回退结束 ---\n }\n\n // 3. 构造请求载荷 (Payload)\n const payload = {\n model: \"deepseek-chat\", // 或者 \"deepseek-coder\" 根据需要选择\n messages: [\n // 系统角色消息 (可选,用于设定 AI 的行为)\n // { role: \"system\", content: \"You are a helpful English-Chinese translator. Translate the following English sentence into Chinese naturally and accurately.\" },\n // 用户角色消息 (包含要翻译的句子)\n { role: \"user\", content: `Translate the following English sentence into Chinese: \"${sentence}\"` }\n // 或者更简洁的指令:\n // { role: \"user\", content: `Please translate this English sentence to Chinese: ${sentence}` }\n ],\n // temperature: 0.7, // 控制生成文本的随机性 (0-2)\n // max_tokens: 100, // 限制生成文本的最大 token 数\n stream: false // 我们不使用流式传输\n };\n\n // 4. 发起 API 请求\n try {\n console.log(`[DeepSeek API] 正在翻译句子: \"${sentence}\"`);\n const response = await fetch(DEEPSEEK_API_URL, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`, // 使用 Bearer Token\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(payload)\n });\n\n // 5. 处理响应\n if (!response.ok) {\n // 尝试解析错误信息\n let errorMsg = `DeepSeek API 请求失败: ${response.status} ${response.statusText}`;\n try {\n const errorData = await response.json();\n errorMsg = errorData.error?.message || errorMsg;\n } catch (e) {\n // 如果无法解析 JSON 错误,忽略\n }\n console.error('[DeepSeek API] 错误响应:', response.status, response.statusText);\n throw new Error(errorMsg);\n }\n\n const data = await response.json();\n console.log(\"[DeepSeek API] 响应数据:\", data);\n\n // 6. 提取翻译结果\n // 假设 API 返回格式为 { choices: [{ message: { content: \"翻译结果\" } }] }\n const translatedText = data.choices?.[0]?.message?.content?.trim();\n\n if (translatedText) {\n console.log(`[DeepSeek API] 翻译成功: \"${sentence}\" -> \"${translatedText}\"`);\n return translatedText;\n } else {\n console.error('[DeepSeek API] 响应中未找到翻译结果:', data);\n throw new Error('DeepSeek API 返回了无效的响应格式。');\n }\n\n } catch (error) {\n console.error('[DeepSeek API] 翻译请求过程中发生错误:', error);\n // 可以选择抛出错误,让上层处理,或者返回一个默认值/错误信息\n throw error; // 推荐抛出,让 submitTranslation 处理\n // 或者返回错误信息作为翻译结果:\n // return `[翻译错误: ${error.message}]`;\n }\n }\n\t\t\t*/\n////=========================模拟API 调试用============================================////\n // --- 修改/替换:调用 DeepSeek API 获取翻译 (带错误处理和模拟回退) ---\n async getAITranslation(sentence) {\n // 1. 输入验证\n if (!sentence || typeof sentence !== 'string') {\n throw new Error('Invalid sentence provided for translation.');\n }\n\n // 2. 检查 API Key 是否已配置且非默认值\n if (DEEPSEEK_API_KEY && DEEPSEEK_API_KEY !== 'YOUR_DEEPSEEK_API_KEY_HERE') {\n // 3. 构造请求载荷 (Payload)\n const payload = {\n model: \"deepseek-chat\", // 选择合适的模型\n messages: [\n // 用户角色消息 (包含要翻译的句子)\n // 使用更清晰的指令,要求只返回翻译结果\n { role: \"user\", content: `Translate the following English sentence into Chinese directly, providing only the translation without any additional explanation or formatting: \"${sentence}\"` }\n ],\n stream: false // 不使用流式传输\n };\n\n // 4. 发起 API 请求\n try {\n console.log(`[DeepSeek API] 正在翻译句子: \"${sentence}\"`);\n const response = await fetch(DEEPSEEK_API_URL, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`, // 使用 Bearer Token\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify(payload)\n });\n\n // 5. 处理响应\n if (!response.ok) {\n // 尝试解析错误信息\n let errorMsg = `DeepSeek API 请求失败: ${response.status} ${response.statusText}`;\n try {\n const errorData = await response.json();\n errorMsg = errorData.error?.message || errorMsg;\n } catch (e) {\n // 如果无法解析 JSON 错误,忽略\n }\n console.error('[DeepSeek API] 错误响应:', response.status, response.statusText);\n // --- 关键修改:抛出错误,让上层决定是否回退 ---\n throw new Error(errorMsg);\n // --- 关键修改结束 ---\n }\n\n const data = await response.json();\n console.log(\"[DeepSeek API] 响应数据:\", data);\n\n // 6. 提取翻译结果\n // 假设 API 返回格式为 { choices: [{ message: { content: \"翻译结果\" } }] }\n let translatedText = data.choices?.[0]?.message?.content?.trim();\n\n if (translatedText) {\n // --- 新增:清理翻译结果 ---\n // 移除可能的 Markdown 代码块标记\n translatedText = translatedText.replace(/^`+|`+$/g, '');\n // --- 新增结束 ---\n console.log(`[DeepSeek API] 翻译成功: \"${sentence}\" -> \"${translatedText}\"`);\n return translatedText;\n } else {\n console.error('[DeepSeek API] 响应中未找到翻译结果:', data);\n throw new Error('DeepSeek API 返回了无效的响应格式。');\n }\n\n } catch (error) {\n // --- 关键修改:记录 DeepSeek API 错误并回退 ---\n console.error('[DeepSeek API] 翻译请求过程中发生错误:', error);\n console.warn('[Fallback] DeepSeek API 调用失败,回退到模拟翻译。');\n // 不立即抛出错误,而是继续执行到模拟翻译部分\n // --- 关键修改结束 ---\n }\n } else {\n console.warn('[Config] DeepSeek API Key 未配置或为默认值,跳过 API 调用。');\n }\n\n // --- 新增点:模拟翻译 (作为后备方案或 API 失败时的回退) ---\n // 如果 API Key 无效、未配置或 API 调用失败,则执行此块\n console.log('[Mock Translation] 使用模拟翻译结果。');\n await new Promise(resolve => setTimeout(resolve, 500)); // 模拟网络延迟\n const mockTranslations = {\n \"Last Sunday, my family and I went to the Central Park.\": \"上个星期天,我和我的家人去了中央公园。\",\n \"The weather was sunny and warm.\": \"天气晴朗而温暖。\",\n \"There were many people in the park.\": \"公园里有很多人。\",\n \"Some children were flying kites.\": \"一些孩子在放风筝。\",\n \"Some old people were dancing.\": \"一些老人在跳舞。\",\n \"My sister and I played on the slide.\": \"我和我妹妹在滑梯上玩。\",\n \"My parents sat on the bench and watched us.\": \"我的父母坐在长椅上看着我们。\",\n \"We bought ice cream from a vendor.\": \"我们从一个小贩那里买了冰淇淋。\",\n \"It was a wonderful day!\": \"这是美好的一天!\"\n // 你可以在这里添加更多常用的模拟翻译\n };\n const mockResult = mockTranslations[sentence];\n if (mockResult) {\n return mockResult;\n } else {\n // 如果没有预设的模拟翻译,返回一个通用格式\n return `(模拟翻译) ${sentence}`;\n }\n // --- 新增点结束 ---\n }\n////=========================================================////\n \n updateProgress() {\n const progressText = document.getElementById('progress-text');\n const progressBar = document.getElementById('progress-bar');\n if (this.passageSentences.length > 0) {\n const progress = Math.round(((this.currentIndex + 1) / this.passageSentences.length) * 100);\n progressText.textContent = `${this.currentIndex + 1}/${this.passageSentences.length}`;\n progressBar.style.width = `${progress}%`;\n progressBar.setAttribute('aria-valuenow', progress);\n } else {\n progressText.textContent = '0/0';\n progressBar.style.width = '0%';\n progressBar.setAttribute('aria-valuenow', 0);\n }\n }\n\n // --- 新增功能:管理选中的单词列表 ---\n\n // 1. 从 localStorage 加载选中的单词\n loadSelectedWordsFromStorage() {\n try {\n const stored = localStorage.getItem(this.localStorageKey);\n if (stored) {\n this.selectedWords = JSON.parse(stored);\n }\n } catch (e) {\n console.error(\"从 localStorage 加载选中单词失败:\", e);\n this.selectedWords = [];\n }\n }\n\n // 2. 将选中的单词保存到 localStorage\n saveSelectedWordsToStorage() {\n try {\n localStorage.setItem(this.localStorageKey, JSON.stringify(this.selectedWords));\n } catch (e) {\n console.error(\"保存选中单词到 localStorage 失败:\", e);\n }\n }\n\n // 3. 渲染选中的单词到表格\n renderSelectedWords() {\n const tbody = document.getElementById('selected-words-tbody');\n const noWordsRow = document.getElementById('no-words-row');\n const saveBtn = document.getElementById('save-to-vocab-btn');\n\n // 增加检查,确保 DOM 元素存在\n if (!tbody || !noWordsRow || !saveBtn) {\n console.error(\"renderSelectedWords: 无法找到必要的 DOM 元素\");\n return;\n }\n\n // 总是先清空现有内容\n tbody.innerHTML = '';\n\n if (this.selectedWords.length === 0) {\n // 如果没有单词,显示提示行并禁用保存按钮\n noWordsRow.style.display = ''; // '' 等同于 'table-row' 对于 tbody 内的 tr\n saveBtn.disabled = true;\n console.log(\"renderSelectedWords: 选词列表为空\");\n return;\n }\n\n // 如果有单词,隐藏提示行并启用保存按钮\n noWordsRow.style.display = 'none';\n saveBtn.disabled = false;\n\n // 遍历选中的单词并添加到表格\n this.selectedWords.forEach((wordObj, index) => {\n const row = document.createElement('tr');\n // 截断长句子以适应表格\n const truncatedSentence = wordObj.sentence.length > 50 ? wordObj.sentence.substring(0, 50) + '...' : wordObj.sentence;\n row.innerHTML = `\n <th scope=\"row\">${index + 1}</th>\n <td>${this.escapeHtml(wordObj.text)}</td>\n <td title=\"${this.escapeHtml(wordObj.sentence)}\">${this.escapeHtml(truncatedSentence)}</td>\n <td>\n <button class=\"btn btn-sm btn-outline-danger remove-word-btn\" data-index=\"${index}\">\n <i class=\"bi bi-trash\"></i>\n </button>\n </td>\n `;\n tbody.appendChild(row);\n });\n\n // 为新添加的删除按钮绑定事件监听器\n // 使用事件委托可能更健壮,但直接绑定也有效\n document.querySelectorAll('.remove-word-btn').forEach(button => {\n // 移除可能已存在的监听器,防止重复绑定 (可选,但更安全)\n button.removeEventListener('click', this.handleRemoveWordClick);\n button.addEventListener('click', this.handleRemoveWordClick.bind(this));\n });\n\n console.log(\"renderSelectedWords: 表格已更新,显示 \" + this.selectedWords.length + \" 个单词\");\n }\n\n // --- 辅助函数:防止 XSS ---\n escapeHtml(unsafe) {\n if (typeof unsafe !== 'string') return '';\n return unsafe\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n }\n\n // --- 辅助函数:处理删除按钮点击 ---\n handleRemoveWordClick(event) {\n event.stopPropagation();\n const index = parseInt(event.currentTarget.getAttribute('data-index'), 10);\n if (!isNaN(index) && index >= 0 && index < this.selectedWords.length) {\n this.removeWord(index);\n } else {\n console.error(\"handleRemoveWordClick: 无效的索引\", index);\n }\n }\n\n // ... (其他方法保持不变) ...\n\n //// 确保 removeWord 方法也更新 UI\n //removeWord(index) {\n // if (index >= 0 && index < this.selectedWords.length) {\n // const removedWord = this.selectedWords.splice(index, 1)[0];\n // this.saveSelectedWordsToStorage();\n // this.renderSelectedWords(); // 重新渲染列表\n\t\t\t//\n // // --- 关键修改:同时移除短文中的高亮 ---\n // // 注意:这里需要移除所有匹配的单词高亮\n // document.querySelectorAll(`.word-token[data-word=\"${this.escapeHtml(removedWord.text)}\"][data-sentence=\"${this.escapeHtml(removedWord.sentence)}\"]`).forEach(token => {\n // token.classList.remove('selected');\n // });\n\t\t\t//\n // this.showMessage(`单词 \"${removedWord.text}\" 已从列表中移除。`, 'info');\n // }\n //},\n\t\t\t//\n //// 确保 clearSelectedWords 方法也更新 UI\n //clearSelectedWords() {\n // if (this.selectedWords.length === 0) {\n // this.showMessage('列表已经是空的。', 'info');\n // return;\n // }\n // if (confirm('确定要清空所有选中的单词吗?')) {\n // this.selectedWords = [];\n // this.saveSelectedWordsToStorage();\n // this.renderSelectedWords(); // 重新渲染列表\n\t\t\t//\n // // --- 关键修改:清除短文中的所有高亮 ---\n // document.querySelectorAll('.word-token.selected').forEach(token => {\n // token.classList.remove('selected');\n // });\n\t\t\t//\n // this.showMessage('选词列表已清空。', 'success');\n // }\n //},\n\n\t\t\t/*\n //renderSelectedWords() {\n // const tbody = document.getElementById('selected-words-tbody');\n // const noWordsRow = document.getElementById('no-words-row');\n // const saveBtn = document.getElementById('save-to-vocab-btn');\n\t\t\t//\n // tbody.innerHTML = ''; // 清空现有内容\n\t\t\t//\n // if (this.selectedWords.length === 0) {\n // noWordsRow.style.display = '';\n // saveBtn.disabled = true;\n // return;\n // }\n\t\t\t//\n // noWordsRow.style.display = 'none';\n // saveBtn.disabled = false;\n\t\t\t//\n // this.selectedWords.forEach((wordObj, index) => {\n // const row = document.createElement('tr');\n // // 截断长句子\n // const truncatedSentence = wordObj.sentence.length > 50 ? wordObj.sentence.substring(0, 50) + '...' : wordObj.sentence;\n // row.innerHTML = `\n // <th scope=\"row\">${index + 1}</th>\n // <td>${wordObj.text}</td>\n // <td title=\"${wordObj.sentence}\">${truncatedSentence}</td> <!-- 使用 title 显示完整句子 -->\n // <td>\n // <button class=\"btn btn-sm btn-outline-danger remove-word-btn\" data-index=\"${index}\">\n // <i class=\"bi bi-trash\"></i>\n // </button>\n // </td>\n // `;\n // tbody.appendChild(row);\n // });\n\t\t\t//\n // // 为删除按钮添加事件监听器\n // document.querySelectorAll('.remove-word-btn').forEach(button => {\n // button.addEventListener('click', (e) => {\n // e.stopPropagation(); // 防止事件冒泡\n // const index = parseInt(e.currentTarget.getAttribute('data-index'));\n // this.removeWord(index);\n // });\n // });\n //} */\n\n // 4. 从列表中移除单词\n removeWord(index) {\n if (index >= 0 && index < this.selectedWords.length) {\n const removedWord = this.selectedWords.splice(index, 1)[0];\n this.saveSelectedWordsToStorage();\n this.renderSelectedWords();\n \n // --- 关键修改:同时移除短文中的高亮 ---\n document.querySelectorAll(`.word-token[data-word=\"${removedWord.text}\"][data-sentence=\"${removedWord.sentence}\"]`).forEach(token => {\n token.classList.remove('selected');\n });\n \n this.showMessage(`单词 \"${removedWord.text}\" 已从列表中移除。`, 'info');\n }\n }\n\n // 5. 清空所有选中的单词\n clearSelectedWords() {\n if (this.selectedWords.length === 0) {\n this.showMessage('列表已经是空的。', 'info');\n return;\n }\n if (confirm('确定要清空所有选中的单词吗?')) {\n this.selectedWords = [];\n this.saveSelectedWordsToStorage();\n this.renderSelectedWords();\n \n // --- 关键修改:清除短文中的所有高亮 ---\n document.querySelectorAll('.word-token.selected').forEach(token => {\n token.classList.remove('selected');\n });\n\t\t\t\t\t\n\t\t\t\t\t//// 移除之前句子的高亮\n\t\t\t\t\t//document.querySelectorAll('.sentence-highlight').forEach(el => {\n\t\t\t\t\t//\tel.classList.remove('sentence-highlight');\n\t\t\t\t\t//});\n \n this.showMessage('选词列表已清空。', 'success');\n }\n }\n\n // 6. 将所有选中的单词保存到后端生词表\n async saveAllToVocabulary() {\n if (this.selectedWords.length === 0) {\n this.showMessage('没有单词需要保存。', 'info');\n return;\n }\n\n const token = localStorage.getItem('token');\n if (!token) {\n this.showMessage('请先登录以保存单词。', 'warning');\n return;\n }\n\n const saveBtn = document.getElementById('save-to-vocab-btn');\n const originalText = saveBtn.innerHTML;\n saveBtn.innerHTML = `<span class=\"spinner-border spinner-border-sm\" role=\"status\" aria-hidden=\"true\"></span> 保存中...`;\n saveBtn.disabled = true;\n\n let successCount = 0;\n let failCount = 0;\n const failedWords = [];\n\t\t\t\t\n\t\t\t\t// 准备要发送到后端的数据,包含所在句子\n const vocabularyData = this.selectedWords.map(wordObj => ({\n word: wordObj.text,\n translation: '(待补充)', // 初始翻译可以设为空或提示用户填写\n partOfSpeech: '', // 词性可选\n // --- 关键修改:source 包含句子 ---\n //source: `翻译特训 - 短文ID:${wordObj.passageId} - 句子:\"${wordObj.sentence}\"`\n\t\t\t\t\tsource: `翻译特训 - 短文ID:${wordObj.passageId} - 句子:\"${wordObj.sentence}\"`\n // --- 关键修改结束 ---\n }));\n\n\t\t\t\t//// 准备要发送到后端的数据\n\t\t\t\t//const vocabularyData = this.selectedWords.map(wordObj => ({\n\t\t\t\t//\tword: wordObj.text,\n\t\t\t\t//\ttranslation: '(待补充)',\n\t\t\t\t//\tpartOfSpeech: '',\n\t\t\t\t//\t// --- 确认这里已修改 ---\n\t\t\t\t//\tsource: `翻译特训 - 短文ID:${wordObj.passageId} - 句子:\"${wordObj.sentence}\"`\n\t\t\t\t//\t// --- 确认这里已修改 ---\n\t\t\t\t//}));\n\n for (const wordObj of this.selectedWords) {\n try {\n // 调用后端 API 添加生词\n //const response = await fetch(`${this.apiBaseUrl}/vocabulary`, {\n // method: 'POST',\n // headers: {\n // 'Authorization': `Bearer ${token}`,\n // 'Content-Type': 'application/json'\n // },\n // body: JSON.stringify({\n // word: wordObj.text,\n // translation: '(待补充)', // 初始翻译可以设为空或提示用户填写\n // partOfSpeech: '', // 词性可选\n // //source: `翻译特训 - 短文ID:${wordObj.passageId}` // 记录来源\n\t\t\t\t\t\t//\t\tsource: `翻译特训 - 短文ID:${wordObj.passageId} - 句子:\"${wordObj.sentence}\"`\n // })\n //});\n\t\t\t\t\t\t\n const response = await fetch(`${this.apiBaseUrl}/vocabulary`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n word: wordObj.text,\n translation: '(待补充)',\n partOfSpeech: '',\n // --- 关键修改:source 包含句子 ---\n source: `翻译特训 - 短文ID:${wordObj.passageId} - 句子:\"${wordObj.sentence}\"`\n // --- 关键修改结束 ---\n })\n });\t\t\t\t\t\t\n\n if (response.ok) {\n const result = await response.json();\n console.log(`单词 \"${wordObj.text}\" 添加成功:`, result.message);\n successCount++;\n } else {\n const errorResult = await response.json();\n console.error(`添加单词 \"${wordObj.text}\" 失败:`, errorResult.error);\n failCount++;\n failedWords.push({ word: wordObj.text, error: errorResult.error || '未知错误' });\n }\n } catch (error) {\n console.error(`添加单词 \"${wordObj.text}\" 时发生网络错误:`, error);\n failCount++;\n failedWords.push({ word: wordObj.text, error: '网络错误' });\n }\n }\n\n // 恢复按钮状态\n saveBtn.innerHTML = originalText;\n saveBtn.disabled = false;\n\n // 处理结果\n if (successCount > 0) {\n // 保存成功后,清空本地列表和高亮\n this.selectedWords = [];\n this.saveSelectedWordsToStorage();\n this.renderSelectedWords();\n // 清除短文中的所有高亮\n document.querySelectorAll('.word-token.selected').forEach(token => {\n token.classList.remove('selected');\n });\n this.showMessage(`成功将 ${successCount} 个单词添加到生词表。`, 'success');\n }\n if (failCount > 0) {\n let failMsg = `有 ${failCount} 个单词添加失败。`;\n if (failedWords.length > 0) {\n failMsg += \" 失败列表: \" + failedWords.map(fw => `${fw.word} (${fw.error})`).join(', ');\n }\n this.showMessage(failMsg, 'danger');\n }\n }\n\n // --- 辅助函数 ---\n\n // 显示消息\n showMessage(text, type) { // type: success, info, warning, danger\n const messageArea = document.getElementById('message-area');\n messageArea.textContent = text;\n messageArea.className = `alert alert-${type} d-block`;\n // 5秒后自动隐藏\n setTimeout(() => {\n messageArea.classList.add('d-none');\n }, 5000);\n }\n }\n\n // 页面加载完成后初始化\n document.addEventListener('DOMContentLoaded', function () {\n window.translationTrainer = new TranslationTrainer();\n\n // 登出按钮事件\n document.getElementById('logout-btn')?.addEventListener('click', () => {\n localStorage.removeItem('token');\n window.location.href = 'index.html';\n });\n\n // 登录按钮事件\n document.getElementById('login-btn')?.addEventListener('click', () => {\n window.location.href = 'login.html';\n });\n });\n </script>\n</body>\n</html>"}}}}