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.
1 line
83 KiB
JSON
1 line
83 KiB
JSON
{"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\">© 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, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\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>"}}}} |