主要功能: - 前后端分离架构,支持远程访问 - 试卷上传和管理系统 - 学科分类统计和数据可视化 - 统一配置管理,支持环境变量 - 响应式设计,优化的用户界面 技术栈: - 后端: FastAPI + SQLAlchemy + SQLite - 前端: Vue 3 + Element Plus + Vite - 配置: 环境变量 + 统一配置中心 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1583 lines
56 KiB
HTML
1583 lines
56 KiB
HTML
<!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> |