<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>记忆曲线单词学习</title>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background: #f0f0f0;
}
.container {
display: flex;
gap: 20px;
height: calc(100vh - 40px);
}
.grid-container {
flex: 1;
overflow: auto;
background: rgb(3, 77, 36);
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
position: relative;
}
.grid {
position: absolute;
top: 0;
left: 0;
transform-origin: 0 0;
transition: transform 0.1s;
}
.cell {
position: absolute;
width: 20px;
height: 20px;
border: 1px solid #ddd;
cursor: pointer;
transition: background-color 0.3s;
}
.cell:hover {
border-color: #666;
}
.cell.gray { background-color: #ccc; }
.cell.green { background-color: #90EE90; }
.cell.yellow { background-color: #FFD700; }
.cell.red { background-color: #FF6B6B; }
.sidebar {
width: 300px;
background: rgb(13, 56, 8);
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
gap: 20px;
color: white;
}
.word-detail {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 30px;
border: 1px solid #ddd;
border-radius: 8px;
display: none;
background: rgba(13, 56, 8, 0.95);
color: white;
z-index: 1000;
min-width: 300px;
max-width: 80%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
}
.word-detail.active {
display: block;
}
.word-detail h3 {
margin-top: 0;
color: #4CAF50;
}
.word-detail p {
margin: 10px 0;
line-height: 1.6;
}
.word-detail textarea {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid rgba(255,255,255,0.2);
background: rgba(255, 255, 255, 0.1);
color: white;
margin: 10px 0;
min-height: 100px;
resize: vertical;
font-family: inherit;
}
.word-detail textarea:focus {
outline: none;
border-color: #4CAF50;
}
.word-detail .button-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
.word-detail .notes-display {
margin: 10px 0;
padding: 10px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.05);
white-space: pre-wrap;
display: none;
min-height: 50px;
word-wrap: break-word;
}
.word-detail .notes-display.active {
display: block;
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: none;
z-index: 999;
}
.overlay.active {
display: block;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
flex-wrap: wrap;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #4CAF50;
color: white;
cursor: pointer;
}
button:hover {
background: #45a049;
}
.zoom-controls {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 10px;
}
.zoom-controls button {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
#notes {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
background: rgba(255, 255, 255, 0.1);
color: white;
}
.settings {
margin-top: 20px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: rgba(255, 255, 255, 0.1);
}
.settings label {
display: block;
margin-bottom: 10px;
color: white;
}
.sync-status {
display: none;
}
.sync-status.success {
display: none;
}
.sync-status.error {
display: none;
}
.sync-button {
display: none;
}
.sync-button:hover {
display: none;
}
.sync-button:disabled {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="grid-container">
<div class="grid" id="grid"></div>
</div>
<div class="sidebar">
<div class="controls">
<button onclick="addNewWord()">添加新单词</button>
<button onclick="exportData()">导出JSON文件</button>
<button onclick="importData()">导入JSON文件</button>
<button onclick="exportToClipboard()">导出到剪切板</button>
<button onclick="importFromText()">从文本框导入</button>
<button onclick="showBatchImport()">批量导入</button>
</div>
</div>
<div class="settings">
<h3>设置</h3>
<label>
<input type="checkbox" id="requirePhonetic" checked> 要求输入音标
</label>
</div>
</div>
</div>
<div class="zoom-controls">
<button onclick="zoomIn()">+</button>
<button onclick="zoomOut()">-</button>
</div>
<div class="overlay" id="overlay"></div>
<div class="word-detail" id="wordDetail">
<h3>单词详情</h3>
<div class="display-toggle">
<label>
<input type="checkbox" id="displayToggle" onchange="toggleDisplayMode()"> 显示英文
</label>
</div>
<p id="wordText"></p>
<p id="wordMeaning"></p>
<p id="wordPhonetic"></p>
<textarea id="notes" placeholder="在这里添加学习笔记..."></textarea>
<div class="button-group">
<button onclick="saveNotes()">保存笔记</button>
<button onclick="deleteWord(parseInt(document.getElementById('wordDetail').dataset.currentIndex))">删除单词</button>
<button onclick="closeWordDetail()">关闭</button>
</div>
</div>
<textarea id="importTextArea" placeholder="在这里粘贴JSON数据以导入" style="width: 100%; height: 100px; margin-top: 20px;"></textarea>
<script>
// 修改存储单词数据的结构
let words = [];
let scale = 1;
let notes = {};
let lastViewTimes = {};
let viewHistory = {}; // 新增:存储每个单词的所有查看时间
let showEnglish = true; // 控制显示英文还是中文
let currentWordIndex = null; // 当前显示的单词索引
// 初始化
function init() {
loadData();
createGrid();
updateColors();
initEventListeners();
setInterval(updateColors, 30000); // 每分钟更新一次颜色
}
// 创建网格
function createGrid() {
const grid = document.getElementById('grid');
const wordCount = words.length;
const gridSize = Math.ceil(Math.sqrt(wordCount));
grid.style.width = `${gridSize * 20}px`;
grid.style.height = `${gridSize * 20}px`;
for (let i = 0; i < wordCount; i++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.style.left = `${(i % gridSize) * 20}px`;
cell.style.top = `${Math.floor(i / gridSize) * 20}px`;
cell.onclick = () => showWordDetail(i);
grid.appendChild(cell);
}
}
// 修改 showWordDetail 函数
function showWordDetail(index) {
currentWordIndex = index;
const word = words[index];
const detail = document.getElementById('wordDetail');
const overlay = document.getElementById('overlay');
const wordText = document.getElementById('wordText');
const wordMeaning = document.getElementById('wordMeaning');
const wordPhonetic = document.getElementById('wordPhonetic');
const notesTextarea = document.getElementById('notes');
detail.dataset.currentIndex = index;
// 根据显示模式设置内容
if (showEnglish) {
wordText.textContent = word.text;
wordMeaning.textContent = '';
} else {
wordText.textContent = '';
wordMeaning.textContent = word.meaning;
}
wordPhonetic.textContent = word.phonetic;
notesTextarea.value = notes[index] || '';
detail.classList.add('active');
overlay.classList.add('active');
if (!viewHistory[index]) {
viewHistory[index] = [];
}
viewHistory[index].push(Date.now());
lastViewTimes[index] = Date.now();
saveData();
updateColors();
}
// 关闭单词详情
function closeWordDetail() {
const detail = document.getElementById('wordDetail');
const overlay = document.getElementById('overlay');
detail.classList.remove('active');
overlay.classList.remove('active');
}
// 修改 updateColors 函数来使用复习次数
function updateColors() {
const cells = document.querySelectorAll('.cell');
const now = Date.now();
cells.forEach((cell, index) => {
const history = viewHistory[index] || [];
const reviewCount = history.length;
const lastView = lastViewTimes[index] || 0;
const timeDiff = now - lastView;
// 根据复习次数和时间间隔设置不同的颜色
if (reviewCount >= 9) { // 9次以上复习
if (timeDiff < 5184000000) { // 30天内
cell.className = 'cell green';
} else {
cell.className = 'cell yellow';
}
} else if (reviewCount == 8) { // 8次复习
if (timeDiff < 2592000000) { // 15天内
cell.className = 'cell green';
} else {
cell.className = 'cell yellow';
}
} else if (reviewCount == 7) { // 7次复习
if (timeDiff < 1209600000) { // 7天内
cell.className = 'cell green';
} else {
cell.className = 'cell yellow';
}
} else if (reviewCount == 6) { // 6次复习
if (timeDiff < 518400000) { // 3天内
cell.className = 'cell green';
} else {
cell.className = 'cell yellow';
}
} else if (reviewCount == 5) { // 5次复习
if (timeDiff <172800000 ) { // 1天内
cell.className = 'cell green';
} else {
cell.className = 'cell yellow';
}
} else if (reviewCount == 4) { // 4次复习
if (timeDiff < 43200000) { // 12小时
cell.className = 'cell green';
} else if (timeDiff < 50400000) { // 2小时内
cell.className = 'cell yellow';
} else {
cell.className = 'cell red';
}
} else if (reviewCount == 3) { // 3次复习
if (timeDiff < 7200000) { // 2小时
cell.className = 'cell green';
} else if (timeDiff < 14400000) { // 2小时内
cell.className = 'cell yellow';
} else {
cell.className = 'cell red';
}
} else if (reviewCount == 2) { // 2次复习
if (timeDiff < 1800000) { // 30分钟
cell.className = 'cell green';
} else if (timeDiff < 9000000) { // 2小时内
cell.className = 'cell yellow';
} else {
cell.className = 'cell red';
}
} else { // 原有的时间间隔逻辑 1次复习
if (timeDiff < 300000) { // 5分钟内
cell.className = 'cell green';
} else if (timeDiff < 1800000) { // 30分钟内
cell.className = 'cell yellow';
} else if (timeDiff < 7200000) { // 2小时内
cell.className = 'cell red';
} else {
cell.className = 'cell gray';
}
}
});
}
// 添加新单词
function addNewWord() {
const text = prompt('输入单词:');
if (!text) return;
const meaning = prompt('输入中文含义:');
if (!meaning) return;
let phonetic = '';
if (document.getElementById('requirePhonetic').checked) {
phonetic = prompt('输入音标(可选):');
}
const index = words.length;
words.push({ text, meaning, phonetic });
notes[index] = ''; // 初始化该单词的笔记
createGrid();
saveData();
}
// 批量导入单词
function showBatchImport() {
const text = prompt('请输入单词列表(格式: "单词": "含义"):');
if (!text) return;
try {
const lines = text.split('\n');
lines.forEach(line => {
const match = line.match(/"([^"]+)":\s*"([^"]+)"/);
if (match) {
const [_, text, meaning] = match;
const index = words.length;
words.push({ text, meaning, phonetic: '' });
notes[index] = ''; // 初始化该单词的笔记
}
});
createGrid();
saveData();
alert('批量导入成功!');
} catch (error) {
alert('导入失败,请检查格式是否正确');
}
}
// 修改 saveData 函数
function saveData() {
const data = {
words,
lastViewTimes,
notes,
viewHistory // 添加 viewHistory 到保存数据中
};
localStorage.setItem('memoryCurveData', JSON.stringify(data));
}
// 修改 loadData 函数,仅从本地加载数据
function loadData() {
const localData = localStorage.getItem('memoryCurveData');
if (localData) {
try {
const parsed = JSON.parse(localData);
words = parsed.words || [];
lastViewTimes = parsed.lastViewTimes || {};
notes = parsed.notes || {};
viewHistory = parsed.viewHistory || {}; // 加载历史记录
} catch (error) {
console.error('本地数据加载失败:', error);
words = [];
lastViewTimes = {};
notes = {};
viewHistory = {};
}
}
}
// 导出数据
function exportData() {
const data = {
words,
lastViewTimes,
notes,
viewHistory
};
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'memory_curve_data.json';
a.click();
URL.revokeObjectURL(url);
}
// 导入数据
function importData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
const data = JSON.parse(event.target.result);
words = data.words || [];
lastViewTimes = data.lastViewTimes || {};
notes = data.notes || {};
viewHistory = data.viewHistory || {};
createGrid();
updateColors();
};
reader.readAsText(file);
};
input.click();
}
// 保存笔记
function saveNotes() {
const detail = document.getElementById('wordDetail');
const index = parseInt(detail.dataset.currentIndex);
const noteContent = document.getElementById('notes').value;
if (!isNaN(index)) {
notes[index] = noteContent;
saveData();
alert('笔记已保存');
}
}
// 缩放控制
function zoomIn() {
scale *= 1.2;
updateZoom();
}
function zoomOut() {
scale /= 1.2;
updateZoom();
}
function updateZoom() {
const grid = document.getElementById('grid');
grid.style.transform = `scale(${scale})`;
}
// 鼠标滚轮缩放
document.querySelector('.grid-container').addEventListener('wheel', (e) => {
e.preventDefault();
if (e.deltaY < 0) {
zoomIn();
} else {
zoomOut();
}
});
// 初始化事件监听
function initEventListeners() {
// ESC键关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeWordDetail();
} else if (e.key === 'z') {
selectRandomYellowWord();
} else if (e.key === 'v') {
selectRandomRedWord();
} else if (e.key === 'x') {
showEnglish = true;
toggleContent();
} else if (e.key === 'c') {
showEnglish = false;
toggleContent();
} else if (e.key === ' ' && document.querySelector('.word-detail.active')) {
e.preventDefault();
toggleContent();
}
});
// 点击遮罩层关闭
document.getElementById('overlay').addEventListener('click', closeWordDetail);
// 添加鼠标点击事件
document.querySelector('.word-detail').addEventListener('click', (e) => {
if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA') {
toggleContent();
}
});
}
// 添加显示模式切换函数
function toggleDisplayMode() {
showEnglish = document.getElementById('displayToggle').checked;
if (currentWordIndex !== null) {
showWordDetail(currentWordIndex);
}
}
// 修改 deleteWord 函数为全新的实现
function deleteWord(index) {
if (confirm('确定要删除这个单词吗?')) {
// 保存所有数据的临时备份 - 注意原始索引对应关系
const oldNotes = {...notes};
const oldLastViewTimes = {...lastViewTimes};
const oldViewHistory = {...viewHistory};
// 移除要删除的单词
words.splice(index, 1);
// 完全清空原有的对象
notes = {};
lastViewTimes = {};
viewHistory = {};
// 重建新的映射关系
words.forEach((word, newIndex) => {
// 如果当前索引小于被删除的索引,保持原来的数据
if (newIndex < index) {
notes[newIndex] = oldNotes[newIndex] || '';
lastViewTimes[newIndex] = oldLastViewTimes[newIndex] || null;
viewHistory[newIndex] = oldViewHistory[newIndex] || [];
}
// 如果当前索引大于等于被删除的索引,则使用原来索引+1的数据
else {
notes[newIndex] = oldNotes[newIndex + 1] || '';
lastViewTimes[newIndex] = oldLastViewTimes[newIndex + 1] || null;
viewHistory[newIndex] = oldViewHistory[newIndex + 1] || [];
}
});
createGrid();
updateColors();
saveData();
closeWordDetail();
}
}
// 添加切换显示内容函数
function toggleContent() {
if (currentWordIndex === null) return;
const word = words[currentWordIndex];
const wordText = document.getElementById('wordText');
const wordMeaning = document.getElementById('wordMeaning');
if (showEnglish) {
wordText.textContent = '';
wordMeaning.textContent = word.meaning;
} else {
wordText.textContent = word.text;
wordMeaning.textContent = '';
}
}
// 添加随机选择黄色单词功能
function selectRandomYellowWord() {
const yellowCells = Array.from(document.querySelectorAll('.cell.yellow'));
if (yellowCells.length > 0) {
const randomIndex = Math.floor(Math.random() * yellowCells.length);
const cellIndex = Array.from(document.querySelectorAll('.cell')).indexOf(yellowCells[randomIndex]);
showWordDetail(cellIndex);
}
}
// 添加随机选择红单词功能
function selectRandomRedWord() {
const redCells = Array.from(document.querySelectorAll('.cell.red'));
if (redCells.length > 0) {
const randomIndex = Math.floor(Math.random() * redCells.length);
const cellIndex = Array.from(document.querySelectorAll('.cell')).indexOf(redCells[randomIndex]);
showWordDetail(cellIndex);
}
}
// 初始化页面
init();
// 导出到剪切板
function exportToClipboard() {
const data = {
words,
lastViewTimes,
notes,
viewHistory
};
const jsonData = JSON.stringify(data);
navigator.clipboard.writeText(jsonData).then(() => {
alert('数据已复制到剪切板');
}).catch(err => {
console.error('复制到剪切板失败:', err);
});
}
// 从文本框导入
function importFromText() {
const textArea = document.getElementById('importTextArea');
const jsonData = textArea.value;
try {
const data = JSON.parse(jsonData);
words = data.words || [];
lastViewTimes = data.lastViewTimes || {};
notes = data.notes || {};
viewHistory = data.viewHistory || {};
createGrid();
updateColors();
alert('数据导入成功');
} catch (error) {
alert('导入失败,请检查JSON格式');
console.error('导入失败:', error);
}
}
</script>
</body>
</html>