Files
Claude CodeandClaude b701c22c78 feat: 完整的WordMaster Pro学习管理系统
主要功能:
- 前后端分离架构,支持远程访问
- 试卷上传和管理系统
- 学科分类统计和数据可视化
- 统一配置管理,支持环境变量
- 响应式设计,优化的用户界面

技术栈:
- 后端: FastAPI + SQLAlchemy + SQLite
- 前端: Vue 3 + Element Plus + Vite
- 配置: 环境变量 + 统一配置中心

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2025-10-05 22:12:16 +08:00

1583 lines
56 KiB
HTML
Raw Permalink 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能单词记忆大师 - WordMaster Pro</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
color: white;
}
.header h1 {
font-size: 2.5em;
font-weight: 700;
margin-bottom: 10px;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.2em;
opacity: 0.9;
}
.nav-tabs {
display: flex;
justify-content: center;
margin-bottom: 30px;
background: rgba(255,255,255,0.1);
border-radius: 15px;
padding: 5px;
backdrop-filter: blur(10px);
}
.nav-tab {
background: none;
border: none;
color: white;
padding: 12px 24px;
border-radius: 10px;
cursor: pointer;
transition: all 0.3s ease;
font-size: 1em;
font-weight: 500;
}
.nav-tab.active {
background: rgba(255,255,255,0.2);
transform: scale(1.05);
}
.nav-tab:hover {
background: rgba(255,255,255,0.1);
}
.content-section {
display: none;
background: white;
border-radius: 20px;
padding: 30px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
backdrop-filter: blur(10px);
}
.content-section.active {
display: block;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.learning-card {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
border-radius: 20px;
padding: 40px;
text-align: center;
color: white;
margin-bottom: 30px;
min-height: 400px;
display: flex;
flex-direction: column;
justify-content: center;
position: relative;
overflow: hidden;
}
.learning-card::before {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
animation: rotate 20s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.learning-card .content {
position: relative;
z-index: 1;
}
.word-display {
font-size: 3em;
font-weight: 700;
margin-bottom: 20px;
text-shadow: 0 2px 10px rgba(0,0,0,0.3);
}
.meaning-display {
font-size: 1.5em;
margin-bottom: 30px;
opacity: 0.9;
}
.example-display {
font-size: 1.1em;
font-style: italic;
opacity: 0.8;
margin-bottom: 30px;
}
.controls {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
flex-wrap: wrap;
}
.btn {
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
border: none;
color: white;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
font-size: 1em;
font-weight: 500;
transition: all 0.3s ease;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.3);
}
.btn.danger {
background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);
}
.btn.success {
background: linear-gradient(135deg, #a8edea 0%, #fed6e3 100%);
}
.rating-buttons {
display: flex;
gap: 10px;
justify-content: center;
margin-top: 20px;
}
.rating-btn {
width: 40px;
height: 40px;
border-radius: 50%;
border: none;
color: white;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
}
.rating-btn:nth-child(1) { background: #ff4757; }
.rating-btn:nth-child(2) { background: #ff6b6b; }
.rating-btn:nth-child(3) { background: #ffa502; }
.rating-btn:nth-child(4) { background: #2ed573; }
.rating-btn:nth-child(5) { background: #1e90ff; }
.rating-btn:nth-child(6) { background: #5352ed; }
.input-mode {
margin-top: 20px;
}
.answer-input {
width: 100%;
max-width: 400px;
padding: 15px;
border: 2px solid rgba(255,255,255,0.3);
border-radius: 15px;
font-size: 1.2em;
text-align: center;
background: rgba(255,255,255,0.1);
color: white;
margin-bottom: 15px;
}
.answer-input::placeholder {
color: rgba(255,255,255,0.7);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stats-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 15px;
text-align: center;
box-shadow: 0 10px 25px rgba(0,0,0,0.1);
}
.stats-card h3 {
font-size: 2em;
margin-bottom: 10px;
}
.stats-card p {
opacity: 0.9;
}
.progress-bar {
width: 100%;
height: 10px;
background: rgba(255,255,255,0.2);
border-radius: 5px;
overflow: hidden;
margin: 20px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
border-radius: 5px;
transition: width 0.3s ease;
}
.chart-container {
background: white;
padding: 20px;
border-radius: 15px;
margin-bottom: 20px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
.leech-words {
background: linear-gradient(135deg, #ff9a9e 0%, #fecfef 100%);
padding: 20px;
border-radius: 15px;
margin-top: 20px;
}
.mode-selector {
display: flex;
gap: 10px;
justify-content: center;
margin-bottom: 20px;
}
.mode-btn {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 10px 20px;
border-radius: 15px;
cursor: pointer;
transition: all 0.3s ease;
}
.mode-btn.active {
background: rgba(255,255,255,0.4);
transform: scale(1.05);
}
.settings-grid {
display: grid;
gap: 20px;
margin-bottom: 20px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
}
.toggle {
position: relative;
width: 50px;
height: 25px;
background: #ccc;
border-radius: 25px;
cursor: pointer;
transition: all 0.3s ease;
}
.toggle.active {
background: #4facfe;
}
.toggle::after {
content: '';
position: absolute;
width: 21px;
height: 21px;
background: white;
border-radius: 50%;
top: 2px;
left: 2px;
transition: all 0.3s ease;
}
.toggle.active::after {
left: 27px;
}
.hidden {
display: none !important;
}
.word-list {
max-height: 300px;
overflow-y: auto;
background: #f8f9fa;
border-radius: 10px;
padding: 15px;
}
.word-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #dee2e6;
}
.word-item:last-child {
border-bottom: none;
}
.pronunciation-btn {
background: none;
border: none;
font-size: 1.5em;
cursor: pointer;
margin-left: 10px;
transition: transform 0.3s ease;
}
.pronunciation-btn:hover {
transform: scale(1.2);
}
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2em;
}
.word-display {
font-size: 2em;
}
.nav-tabs {
flex-wrap: wrap;
}
.controls {
flex-direction: column;
align-items: center;
}
.stats-grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧠 WordMaster Pro</h1>
<p>智能单词记忆大师 - 10种高效学习方法集成</p>
</div>
<div class="nav-tabs">
<button class="nav-tab active" onclick="showSection('learning')">🎯 学习</button>
<button class="nav-tab" onclick="showSection('stats')">📊 统计</button>
<button class="nav-tab" onclick="showSection('words')">📚 词库</button>
<button class="nav-tab" onclick="showSection('settings')">⚙️ 设置</button>
</div>
<!-- 学习界面 -->
<div id="learning" class="content-section active">
<div class="mode-selector">
<button class="mode-btn active" onclick="setLearningMode('flashcard')">🃏 抽认卡</button>
<button class="mode-btn" onclick="setLearningMode('typing')">⌨️ 输入</button>
<button class="mode-btn" onclick="setLearningMode('choice')">🎯 选择</button>
<button class="mode-btn" onclick="setLearningMode('cloze')">📝 完形填空</button>
</div>
<div class="learning-card">
<div class="content">
<div id="learning-content">
<div class="word-display" id="word-display">准备开始学习</div>
<button class="pronunciation-btn" onclick="speakWord()" id="speak-btn">🔊</button>
<div class="meaning-display" id="meaning-display">点击开始按钮开始学习之旅</div>
<div class="example-display" id="example-display"></div>
<!-- 输入模式 -->
<div id="input-mode" class="input-mode hidden">
<input type="text" class="answer-input" id="answer-input" placeholder="请输入答案...">
<button class="btn" onclick="checkAnswer()">检查答案</button>
</div>
<!-- 选择模式 -->
<div id="choice-mode" class="hidden">
<div class="controls" id="choices-container"></div>
</div>
<!-- 评分按钮 -->
<div class="rating-buttons" id="rating-buttons">
<button class="rating-btn" onclick="rateWord(0)" title="完全不记得">0</button>
<button class="rating-btn" onclick="rateWord(1)" title="有印象">1</button>
<button class="rating-btn" onclick="rateWord(2)" title="想起来了">2</button>
<button class="rating-btn" onclick="rateWord(3)" title="记得">3</button>
<button class="rating-btn" onclick="rateWord(4)" title="很容易">4</button>
<button class="rating-btn" onclick="rateWord(5)" title="太容易">5</button>
</div>
</div>
</div>
</div>
<div class="controls">
<button class="btn" onclick="startLearning()">🚀 开始学习</button>
<button class="btn" onclick="nextWord()">⏭️ 下一个</button>
<button class="btn" onclick="toggleDirection()">🔄 切换方向</button>
<button class="btn" onclick="startMicroSession(3)">⏱️ 3分钟</button>
<button class="btn" onclick="startMicroSession(5)">⏱️ 5分钟</button>
<button class="btn" onclick="startMicroSession(10)">⏱️ 10分钟</button>
</div>
<div class="progress-bar">
<div class="progress-fill" id="session-progress" style="width: 0%"></div>
</div>
</div>
<!-- 统计界面 -->
<div id="stats" class="content-section">
<div class="stats-grid">
<div class="stats-card">
<h3 id="total-words">0</h3>
<p>总词汇量</p>
</div>
<div class="stats-card">
<h3 id="due-today">0</h3>
<p>今日待复习</p>
</div>
<div class="stats-card">
<h3 id="completed-today">0</h3>
<p>今日已完成</p>
</div>
<div class="stats-card">
<h3 id="accuracy-rate">0%</h3>
<p>准确率</p>
</div>
</div>
<div class="chart-container">
<h3>未来7天复习预测</h3>
<canvas id="forecast-chart" width="400" height="200"></canvas>
</div>
<div class="leech-words">
<h3>🐛 顽固词汇 (需要特别关注)</h3>
<div id="leech-list" class="word-list"></div>
</div>
</div>
<!-- 词库管理 -->
<div id="words" class="content-section">
<div class="controls">
<button class="btn" onclick="importWords()">📥 导入词汇</button>
<button class="btn" onclick="exportWords()">📤 导出词汇</button>
<button class="btn" onclick="addNewWord()">➕ 添加单词</button>
</div>
<div id="word-list" class="word-list"></div>
</div>
<!-- 设置界面 -->
<div id="settings" class="content-section">
<div class="settings-grid">
<div class="setting-item">
<span>使用SM-2算法</span>
<div class="toggle active" onclick="toggleSetting('useSM2')" id="toggle-sm2"></div>
</div>
<div class="setting-item">
<span>自动发音</span>
<div class="toggle active" onclick="toggleSetting('autoSpeak')" id="toggle-speak"></div>
</div>
<div class="setting-item">
<span>模糊匹配</span>
<div class="toggle active" onclick="toggleSetting('fuzzyMatch')" id="toggle-fuzzy"></div>
</div>
<div class="setting-item">
<span>自动调整新词上限</span>
<div class="toggle active" onclick="toggleSetting('autoTune')" id="toggle-tune"></div>
</div>
<div class="setting-item">
<span>每日新词数量</span>
<input type="number" id="daily-new" value="20" min="5" max="100" onchange="updateDailyNew(this.value)">
</div>
<div class="setting-item">
<span>模糊匹配容错率</span>
<input type="range" id="fuzzy-tolerance" min="10" max="50" value="20" onchange="updateFuzzyTolerance(this.value)">
<span id="fuzzy-value">20%</span>
</div>
</div>
</div>
</div>
<script>
// 全局变量
let words = [];
let currentWord = null;
let currentIndex = 0;
let sessionWords = [];
let sessionStartTime = null;
let sessionTimeLimit = null;
let learningMode = 'flashcard';
let direction = 'en2zh';
let directionStep = 0;
// 设置
let settings = {
useSM2: true,
autoSpeak: true,
fuzzyMatch: true,
autoTune: true,
dailyNew: 20,
fuzzyTolerance: 0.2
};
// 初始化示例词汇
const sampleWords = [
{
id: 1,
word: "algorithm",
meaning: "算法",
example: "The sorting algorithm is very efficient.",
phrases: ["machine learning algorithm", "search algorithm", "algorithm complexity"],
EF: 2.5,
interval: 0,
reps: 0,
due: Date.now(),
stats: { ok: 0, wrong: 0, streakWrong: 0 },
leech: false
},
{
id: 2,
word: "comprehensive",
meaning: "全面的,综合的",
example: "The report provides a comprehensive analysis.",
phrases: ["comprehensive study", "comprehensive plan", "comprehensive review"],
EF: 2.5,
interval: 0,
reps: 0,
due: Date.now(),
stats: { ok: 0, wrong: 0, streakWrong: 0 },
leech: false
},
{
id: 3,
word: "sophisticated",
meaning: "复杂的,精密的",
example: "The new software has sophisticated features.",
phrases: ["sophisticated technology", "sophisticated approach", "sophisticated analysis"],
EF: 2.5,
interval: 0,
reps: 0,
due: Date.now(),
stats: { ok: 0, wrong: 0, streakWrong: 0 },
leech: false
},
{
id: 4,
word: "efficiency",
meaning: "效率",
example: "We need to improve the efficiency of our system.",
phrases: ["energy efficiency", "work efficiency", "improve efficiency"],
EF: 2.5,
interval: 0,
reps: 0,
due: Date.now(),
stats: { ok: 0, wrong: 0, streakWrong: 0 },
leech: false
},
{
id: 5,
word: "implement",
meaning: "实施,执行",
example: "We will implement the new policy next month.",
phrases: ["implement strategy", "implement changes", "implement solution"],
EF: 2.5,
interval: 0,
reps: 0,
due: Date.now(),
stats: { ok: 0, wrong: 0, streakWrong: 0 },
leech: false
}
];
// 初始化
function init() {
loadData();
if (words.length === 0) {
words = [...sampleWords];
saveData();
}
updateStats();
loadSettings();
}
// 数据持久化
function saveData() {
localStorage.setItem('wordmaster_words', JSON.stringify(words));
localStorage.setItem('wordmaster_settings', JSON.stringify(settings));
}
function loadData() {
const savedWords = localStorage.getItem('wordmaster_words');
const savedSettings = localStorage.getItem('wordmaster_settings');
if (savedWords) {
words = JSON.parse(savedWords);
}
if (savedSettings) {
settings = { ...settings, ...JSON.parse(savedSettings) };
}
}
function loadSettings() {
document.getElementById('daily-new').value = settings.dailyNew;
document.getElementById('fuzzy-tolerance').value = settings.fuzzyTolerance * 100;
document.getElementById('fuzzy-value').textContent = `${Math.round(settings.fuzzyTolerance * 100)}%`;
document.getElementById('toggle-sm2').classList.toggle('active', settings.useSM2);
document.getElementById('toggle-speak').classList.toggle('active', settings.autoSpeak);
document.getElementById('toggle-fuzzy').classList.toggle('active', settings.fuzzyMatch);
document.getElementById('toggle-tune').classList.toggle('active', settings.autoTune);
}
// SM-2 算法实现
function sm2Schedule(word, quality) {
if (!word.EF) word.EF = 2.5;
if (!word.interval) word.interval = 0;
if (!word.reps) word.reps = 0;
if (!word.due) word.due = Date.now();
word.EF = Math.max(1.3, word.EF + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02)));
if (quality < 3) {
word.reps = 0;
word.interval = 1;
} else {
word.reps += 1;
if (word.reps === 1) {
word.interval = 1;
} else if (word.reps === 2) {
word.interval = 3;
} else {
word.interval = Math.round(word.interval * word.EF);
}
}
word.due = Date.now() + word.interval * 24 * 3600 * 1000;
return word;
}
// 字符串标准化
function normalize(str) {
return str.toLowerCase().trim().replace(/[^a-z]/g, '');
}
// Levenshtein距离算法
function levenshteinDistance(a, b) {
const dp = Array.from({length: a.length + 1}, () => Array(b.length + 1).fill(0));
for (let i = 0; i <= a.length; i++) dp[i][0] = i;
for (let j = 0; j <= b.length; j++) dp[0][j] = j;
for (let i = 1; i <= a.length; i++) {
for (let j = 1; j <= b.length; j++) {
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
dp[i][j] = Math.min(
dp[i - 1][j] + 1, // 删除
dp[i][j - 1] + 1, // 插入
dp[i - 1][j - 1] + cost // 替换
);
}
}
return dp[a.length][b.length];
}
// 模糊匹配检查
function isCorrectAnswer(input, target) {
const normalizedInput = normalize(input);
const normalizedTarget = normalize(target);
if (normalizedInput === normalizedTarget) return true;
if (!settings.fuzzyMatch) return false;
const distance = levenshteinDistance(normalizedInput, normalizedTarget);
const tolerance = Math.max(1, Math.floor(normalizedTarget.length * settings.fuzzyTolerance));
return distance <= tolerance;
}
// 方向切换
function nextDirection() {
directionStep++;
return (directionStep % 4 === 0) ? 'zh2en' : 'en2zh';
}
// 生成相似干扰项
function similarityScore(a, b) {
const normalizedA = a.toLowerCase();
const normalizedB = b.toLowerCase();
const commonChars = [...new Set(normalizedA)].filter(ch => normalizedB.includes(ch)).length;
return commonChars / Math.max(normalizedA.length, normalizedB.length);
}
function pickDistractors(targetWord, count = 3) {
const pool = words.filter(w => w.id !== targetWord.id);
return pool
.map(w => ({
word: w,
score: 0.6 * similarityScore(w.word, targetWord.word) +
0.4 * similarityScore(w.meaning, targetWord.meaning)
}))
.sort((x, y) => y.score - x.score)
.slice(0, count)
.map(x => x.word);
}
// 处理学习结果和顽固词检测
function processLearningResult(word, isCorrect, quality = null) {
if (!word.stats) {
word.stats = { ok: 0, wrong: 0, streakWrong: 0 };
}
if (isCorrect) {
word.stats.ok++;
word.stats.streakWrong = 0;
if (word.leech) {
// 顽固词答对后逐步恢复
if (word.stats.ok % 3 === 0) {
word.leech = false;
word.EF = Math.min(2.5, word.EF + 0.1);
}
}
} else {
word.stats.wrong++;
word.stats.streakWrong++;
// 连续错误3次标记为顽固词
if (word.stats.streakWrong >= 3) {
word.leech = true;
word.EF = Math.max(1.3, word.EF - 0.2);
word.interval = 1;
}
}
// 使用SM-2算法调度
if (settings.useSM2 && quality !== null) {
sm2Schedule(word, quality);
}
saveData();
}
// 生成完形填空
function generateCloze(example, word) {
if (!example) return null;
const regex = new RegExp(`\\b${word}\\b`, 'i');
if (!regex.test(example)) return null;
return example.replace(regex, '____');
}
// 语块问题生成
function generatePhraseQuestion(word) {
if (!word.phrases || !word.phrases.length) return null;
const phrase = word.phrases[Math.floor(Math.random() * word.phrases.length)];
const cloze = phrase.replace(new RegExp(word.word, 'i'), '____');
return {
prompt: cloze,
answer: word.word,
fullPhrase: phrase
};
}
// TTS发音
function speakWord(text = null) {
if (!settings.autoSpeak && !text) return;
const textToSpeak = text || (currentWord ? currentWord.word : '');
if (!textToSpeak) return;
const utterance = new SpeechSynthesisUtterance(textToSpeak);
utterance.lang = 'en-US';
utterance.rate = 1;
utterance.pitch = 1;
speechSynthesis.speak(utterance);
}
// 预测未来到期词汇量
function forecastDue(daysAhead = 7) {
const buckets = Array.from({length: daysAhead + 1}, () => 0);
const start = new Date();
start.setHours(0, 0, 0, 0);
const startTime = start.getTime();
words.forEach(word => {
const dayIndex = Math.floor(((word.due || startTime) - startTime) / (24 * 3600 * 1000));
if (dayIndex >= 0 && dayIndex <= daysAhead) {
buckets[dayIndex]++;
}
});
return buckets;
}
// 自适应调整每日新词数量
function autoTuneDailyNew() {
if (!settings.autoTune) return;
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayTime = today.getTime();
const todayWords = words.filter(w => w.due <= todayTime + 24 * 3600 * 1000);
const completedToday = todayWords.filter(w => w.stats && w.stats.ok > 0);
const completionRate = todayWords.length ? completedToday.length / todayWords.length : 1;
if (completionRate < 0.6) {
settings.dailyNew = Math.max(5, settings.dailyNew - 5);
} else if (completionRate > 0.9 && settings.dailyNew < 60) {
settings.dailyNew += 5;
}
saveData();
}
// 学习界面控制函数
function showSection(sectionName) {
// 隐藏所有内容区域
document.querySelectorAll('.content-section').forEach(section => {
section.classList.remove('active');
});
// 移除所有导航标签的激活状态
document.querySelectorAll('.nav-tab').forEach(tab => {
tab.classList.remove('active');
});
// 显示指定区域
document.getElementById(sectionName).classList.add('active');
// 激活对应的导航标签
event.target.classList.add('active');
// 如果切换到统计页面,更新统计信息
if (sectionName === 'stats') {
updateStats();
drawForecastChart();
updateLeechList();
} else if (sectionName === 'words') {
updateWordList();
}
}
function setLearningMode(mode) {
learningMode = mode;
// 更新模式按钮状态
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// 重新显示当前单词
if (currentWord) {
displayWord(currentWord);
}
}
function startLearning() {
// 获取今日待学习的单词
const today = Date.now();
const dueWords = words.filter(w => (w.due || 0) <= today);
if (dueWords.length === 0) {
alert('今日没有需要复习的单词!');
return;
}
sessionWords = [...dueWords];
currentIndex = 0;
sessionStartTime = Date.now();
// 自适应调整
autoTuneDailyNew();
nextWord();
}
function nextWord() {
if (sessionWords.length === 0) {
alert('请先开始学习!');
return;
}
// 检查微时段学习是否结束
if (sessionTimeLimit && Date.now() - sessionStartTime > sessionTimeLimit) {
alert(`时间到!本次学习完成,共学习了 ${currentIndex} 个单词。`);
sessionTimeLimit = null;
updateStats();
return;
}
if (currentIndex < sessionWords.length) {
currentWord = sessionWords[currentIndex];
direction = nextDirection();
displayWord(currentWord);
currentIndex++;
// 更新进度条
const progress = (currentIndex / sessionWords.length) * 100;
document.getElementById('session-progress').style.width = `${progress}%`;
} else {
alert('本轮学习完成!');
updateStats();
}
}
function displayWord(word) {
const wordDisplay = document.getElementById('word-display');
const meaningDisplay = document.getElementById('meaning-display');
const exampleDisplay = document.getElementById('example-display');
// 隐藏所有模式
document.getElementById('input-mode').classList.add('hidden');
document.getElementById('choice-mode').classList.add('hidden');
document.getElementById('rating-buttons').classList.remove('hidden');
// 根据学习模式和方向显示内容
switch (learningMode) {
case 'flashcard':
displayFlashcard(word);
break;
case 'typing':
displayTypingMode(word);
break;
case 'choice':
displayChoiceMode(word);
break;
case 'cloze':
displayClozeMode(word);
break;
}
// 自动发音
if (settings.autoSpeak && (direction === 'en2zh' || learningMode === 'flashcard')) {
setTimeout(() => speakWord(word.word), 500);
}
}
function displayFlashcard(word) {
const wordDisplay = document.getElementById('word-display');
const meaningDisplay = document.getElementById('meaning-display');
const exampleDisplay = document.getElementById('example-display');
if (direction === 'en2zh') {
wordDisplay.textContent = word.word;
meaningDisplay.textContent = word.meaning;
} else {
wordDisplay.textContent = word.meaning;
meaningDisplay.textContent = word.word;
}
exampleDisplay.textContent = word.example || '';
}
function displayTypingMode(word) {
const wordDisplay = document.getElementById('word-display');
const meaningDisplay = document.getElementById('meaning-display');
const exampleDisplay = document.getElementById('example-display');
document.getElementById('input-mode').classList.remove('hidden');
document.getElementById('rating-buttons').classList.add('hidden');
if (direction === 'en2zh') {
wordDisplay.textContent = word.word;
meaningDisplay.textContent = '请输入中文意思:';
} else {
wordDisplay.textContent = word.meaning;
meaningDisplay.textContent = '请输入英文单词:';
}
exampleDisplay.textContent = word.example || '';
document.getElementById('answer-input').value = '';
document.getElementById('answer-input').focus();
}
function displayChoiceMode(word) {
const wordDisplay = document.getElementById('word-display');
const meaningDisplay = document.getElementById('meaning-display');
const choicesContainer = document.getElementById('choices-container');
document.getElementById('choice-mode').classList.remove('hidden');
document.getElementById('rating-buttons').classList.add('hidden');
const distractors = pickDistractors(word, 3);
const allChoices = [word, ...distractors];
// 随机打乱选项
for (let i = allChoices.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[allChoices[i], allChoices[j]] = [allChoices[j], allChoices[i]];
}
if (direction === 'en2zh') {
wordDisplay.textContent = word.word;
meaningDisplay.textContent = '请选择正确的中文意思:';
choicesContainer.innerHTML = '';
allChoices.forEach(choice => {
const btn = document.createElement('button');
btn.className = 'btn';
btn.textContent = choice.meaning;
btn.onclick = () => selectChoice(choice.id === word.id);
choicesContainer.appendChild(btn);
});
} else {
wordDisplay.textContent = word.meaning;
meaningDisplay.textContent = '请选择正确的英文单词:';
choicesContainer.innerHTML = '';
allChoices.forEach(choice => {
const btn = document.createElement('button');
btn.className = 'btn';
btn.textContent = choice.word;
btn.onclick = () => selectChoice(choice.id === word.id);
choicesContainer.appendChild(btn);
});
}
}
function displayClozeMode(word) {
const wordDisplay = document.getElementById('word-display');
const meaningDisplay = document.getElementById('meaning-display');
const exampleDisplay = document.getElementById('example-display');
// 尝试生成完形填空
const cloze = generateCloze(word.example, word.word);
if (cloze) {
document.getElementById('input-mode').classList.remove('hidden');
document.getElementById('rating-buttons').classList.add('hidden');
wordDisplay.textContent = '完形填空';
meaningDisplay.textContent = word.meaning;
exampleDisplay.textContent = cloze;
document.getElementById('answer-input').value = '';
document.getElementById('answer-input').focus();
} else {
// 如果无法生成完形填空,使用语块模式
const phraseQuestion = generatePhraseQuestion(word);
if (phraseQuestion) {
document.getElementById('input-mode').classList.remove('hidden');
document.getElementById('rating-buttons').classList.add('hidden');
wordDisplay.textContent = '完成短语';
meaningDisplay.textContent = word.meaning;
exampleDisplay.textContent = phraseQuestion.prompt;
document.getElementById('answer-input').value = '';
document.getElementById('answer-input').focus();
} else {
// 备用方案:使用普通抽认卡模式
displayFlashcard(word);
}
}
}
function checkAnswer() {
const input = document.getElementById('answer-input').value.trim();
if (!input) return;
let isCorrect = false;
let targetAnswer = '';
if (learningMode === 'typing') {
targetAnswer = direction === 'en2zh' ? currentWord.meaning : currentWord.word;
isCorrect = isCorrectAnswer(input, targetAnswer);
} else if (learningMode === 'cloze') {
targetAnswer = currentWord.word;
isCorrect = isCorrectAnswer(input, targetAnswer);
}
// 显示结果
const meaningDisplay = document.getElementById('meaning-display');
if (isCorrect) {
meaningDisplay.innerHTML = `<span style="color: #2ed573;">✓ 正确!</span> 答案是:${targetAnswer}`;
processLearningResult(currentWord, true, 4);
} else {
meaningDisplay.innerHTML = `<span style="color: #ff4757;">✗ 错误!</span> 正确答案是:${targetAnswer}`;
processLearningResult(currentWord, false, 1);
}
// 2秒后自动下一题
setTimeout(() => {
nextWord();
}, 2000);
}
function selectChoice(isCorrect) {
const meaningDisplay = document.getElementById('meaning-display');
if (isCorrect) {
meaningDisplay.innerHTML = `<span style="color: #2ed573;">✓ 正确!</span>`;
processLearningResult(currentWord, true, 4);
} else {
meaningDisplay.innerHTML = `<span style="color: #ff4757;">✗ 错误!</span>`;
processLearningResult(currentWord, false, 1);
}
// 1.5秒后自动下一题
setTimeout(() => {
nextWord();
}, 1500);
}
function rateWord(quality) {
processLearningResult(currentWord, quality >= 3, quality);
// 显示反馈
const meaningDisplay = document.getElementById('meaning-display');
const ratings = ['完全不记得', '有印象', '想起来了', '记得', '很容易', '太容易'];
meaningDisplay.innerHTML = `评分:${quality} - ${ratings[quality]}`;
// 1秒后自动下一题
setTimeout(() => {
nextWord();
}, 1000);
}
function toggleDirection() {
direction = direction === 'en2zh' ? 'zh2en' : 'en2zh';
if (currentWord) {
displayWord(currentWord);
}
}
function startMicroSession(minutes) {
sessionTimeLimit = minutes * 60 * 1000;
startLearning();
}
// 统计功能
function updateStats() {
const totalWords = words.length;
const today = Date.now();
const todayWords = words.filter(w => (w.due || 0) <= today);
const completedToday = words.filter(w => {
if (!w.stats) return false;
const lastStudy = w.due - (w.interval || 1) * 24 * 3600 * 1000;
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
return lastStudy >= todayStart.getTime();
});
const totalCorrect = words.reduce((sum, w) => sum + (w.stats?.ok || 0), 0);
const totalAttempts = words.reduce((sum, w) => sum + ((w.stats?.ok || 0) + (w.stats?.wrong || 0)), 0);
const accuracyRate = totalAttempts > 0 ? Math.round((totalCorrect / totalAttempts) * 100) : 0;
document.getElementById('total-words').textContent = totalWords;
document.getElementById('due-today').textContent = todayWords.length;
document.getElementById('completed-today').textContent = completedToday.length;
document.getElementById('accuracy-rate').textContent = `${accuracyRate}%`;
}
function drawForecastChart() {
const canvas = document.getElementById('forecast-chart');
const ctx = canvas.getContext('2d');
const forecast = forecastDue(7);
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
const width = canvas.width;
const height = canvas.height;
const margin = 40;
const chartWidth = width - 2 * margin;
const chartHeight = height - 2 * margin;
const maxValue = Math.max(...forecast, 1);
const days = ['今天', '明天', '后天', '第4天', '第5天', '第6天', '第7天'];
// 绘制背景
ctx.fillStyle = '#f8f9fa';
ctx.fillRect(0, 0, width, height);
// 绘制网格线
ctx.strokeStyle = '#dee2e6';
ctx.lineWidth = 1;
for (let i = 0; i <= 5; i++) {
const y = margin + (chartHeight * i) / 5;
ctx.beginPath();
ctx.moveTo(margin, y);
ctx.lineTo(width - margin, y);
ctx.stroke();
}
// 绘制柱状图
const barWidth = chartWidth / forecast.length * 0.8;
const barSpacing = chartWidth / forecast.length * 0.2;
forecast.forEach((value, index) => {
const barHeight = (value / maxValue) * chartHeight;
const x = margin + index * (barWidth + barSpacing) + barSpacing / 2;
const y = height - margin - barHeight;
// 渐变色
const gradient = ctx.createLinearGradient(0, y, 0, y + barHeight);
gradient.addColorStop(0, '#4facfe');
gradient.addColorStop(1, '#00f2fe');
ctx.fillStyle = gradient;
ctx.fillRect(x, y, barWidth, barHeight);
// 数值标签
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(value.toString(), x + barWidth / 2, y - 5);
// 日期标签
ctx.fillText(days[index], x + barWidth / 2, height - margin + 20);
});
// 标题
ctx.fillStyle = '#333';
ctx.font = 'bold 14px Arial';
ctx.textAlign = 'center';
ctx.fillText('未来7天复习量预测', width / 2, 20);
}
function updateLeechList() {
const leechWords = words.filter(w => w.leech);
const leechList = document.getElementById('leech-list');
if (leechWords.length === 0) {
leechList.innerHTML = '<p style="text-align: center; color: #6c757d;">暂无顽固词汇,学习状态良好!</p>';
return;
}
leechList.innerHTML = '';
leechWords.forEach(word => {
const wordItem = document.createElement('div');
wordItem.className = 'word-item';
wordItem.innerHTML = `
<div>
<strong>${word.word}</strong> - ${word.meaning}
<div style="font-size: 0.8em; color: #6c757d;">
错误次数: ${word.stats?.wrong || 0} | 连续错误: ${word.stats?.streakWrong || 0}
</div>
</div>
<button class="btn" onclick="reviewLeechWord(${word.id})" style="padding: 5px 10px; font-size: 0.8em;">
复习
</button>
`;
leechList.appendChild(wordItem);
});
}
function reviewLeechWord(wordId) {
const word = words.find(w => w.id === wordId);
if (word) {
currentWord = word;
showSection('learning');
displayWord(word);
}
}
function updateWordList() {
const wordList = document.getElementById('word-list');
wordList.innerHTML = '';
words.forEach(word => {
const wordItem = document.createElement('div');
wordItem.className = 'word-item';
wordItem.innerHTML = `
<div>
<strong>${word.word}</strong> - ${word.meaning}
<div style="font-size: 0.8em; color: #6c757d;">
${word.example || ''}
${word.leech ? '<span style="color: #ff4757;">🐛</span>' : ''}
</div>
</div>
<div>
<button class="btn" onclick="editWord(${word.id})" style="padding: 5px 10px; font-size: 0.8em; margin-right: 5px;">
编辑
</button>
<button class="btn danger" onclick="deleteWord(${word.id})" style="padding: 5px 10px; font-size: 0.8em;">
删除
</button>
</div>
`;
wordList.appendChild(wordItem);
});
}
// 词汇管理功能
function importWords() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.csv,.json';
input.onchange = function(event) {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = function(e) {
try {
let importedWords = [];
if (file.name.endsWith('.json')) {
importedWords = JSON.parse(e.target.result);
} else if (file.name.endsWith('.csv')) {
const lines = e.target.result.split('\n');
const headers = lines[0].split(',');
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim()) {
const values = lines[i].split(',');
const word = {
id: Date.now() + i,
word: values[0]?.trim() || '',
meaning: values[1]?.trim() || '',
example: values[2]?.trim() || '',
phrases: values[3] ? values[3].split(';').map(p => p.trim()) : [],
EF: 2.5,
interval: 0,
reps: 0,
due: Date.now(),
stats: { ok: 0, wrong: 0, streakWrong: 0 },
leech: false
};
importedWords.push(word);
}
}
}
words.push(...importedWords);
saveData();
updateWordList();
alert(`成功导入 ${importedWords.length} 个单词!`);
} catch (error) {
alert('导入失败:文件格式错误');
}
};
reader.readAsText(file);
};
input.click();
}
function exportWords() {
const csvContent = [
'word,meaning,example,phrases',
...words.map(w => `"${w.word}","${w.meaning}","${w.example || ''}","${(w.phrases || []).join(';')}"`)
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'wordmaster_export.csv';
link.click();
}
function addNewWord() {
const word = prompt('请输入英文单词:');
if (!word) return;
const meaning = prompt('请输入中文意思:');
if (!meaning) return;
const example = prompt('请输入例句(可选):') || '';
const newWord = {
id: Date.now(),
word: word.trim(),
meaning: meaning.trim(),
example: example.trim(),
phrases: [],
EF: 2.5,
interval: 0,
reps: 0,
due: Date.now(),
stats: { ok: 0, wrong: 0, streakWrong: 0 },
leech: false
};
words.push(newWord);
saveData();
updateWordList();
alert('单词添加成功!');
}
function editWord(wordId) {
const word = words.find(w => w.id === wordId);
if (!word) return;
const newWord = prompt('修改英文单词:', word.word);
if (newWord === null) return;
const newMeaning = prompt('修改中文意思:', word.meaning);
if (newMeaning === null) return;
const newExample = prompt('修改例句:', word.example || '');
if (newExample === null) return;
word.word = newWord.trim();
word.meaning = newMeaning.trim();
word.example = newExample.trim();
saveData();
updateWordList();
alert('单词修改成功!');
}
function deleteWord(wordId) {
if (!confirm('确定要删除这个单词吗?')) return;
const index = words.findIndex(w => w.id === wordId);
if (index !== -1) {
words.splice(index, 1);
saveData();
updateWordList();
alert('单词删除成功!');
}
}
// 设置功能
function toggleSetting(settingName) {
settings[settingName] = !settings[settingName];
const toggle = document.getElementById(`toggle-${settingName.replace(/([A-Z])/g, '-$1').toLowerCase()}`);
toggle.classList.toggle('active', settings[settingName]);
saveData();
}
function updateDailyNew(value) {
settings.dailyNew = parseInt(value);
saveData();
}
function updateFuzzyTolerance(value) {
settings.fuzzyTolerance = value / 100;
document.getElementById('fuzzy-value').textContent = `${value}%`;
saveData();
}
// 键盘事件处理
document.addEventListener('keydown', function(event) {
if (event.target.tagName === 'INPUT') return;
switch(event.key) {
case 'Enter':
case ' ':
if (learningMode === 'typing' || learningMode === 'cloze') {
checkAnswer();
} else {
nextWord();
}
event.preventDefault();
break;
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
if (learningMode === 'flashcard') {
rateWord(parseInt(event.key) - 1);
event.preventDefault();
}
break;
case 'ArrowRight':
nextWord();
event.preventDefault();
break;
case 'ArrowLeft':
toggleDirection();
event.preventDefault();
break;
case 's':
speakWord();
event.preventDefault();
break;
}
});
// Enter键提交答案
document.addEventListener('DOMContentLoaded', function() {
const answerInput = document.getElementById('answer-input');
answerInput.addEventListener('keypress', function(event) {
if (event.key === 'Enter') {
checkAnswer();
}
});
});
// 页面加载完成后初始化
window.addEventListener('load', function() {
init();
});
</script>
</body>
</html>