feat: add mastery feature to recitation wheel
This commit is contained in:
@@ -1,18 +1,21 @@
|
||||
// 背诵排序功能脚本
|
||||
// 背诵排序功能脚本
|
||||
|
||||
let extractedItems = [];
|
||||
let sortedItems = [];
|
||||
let sortedItems = []; // 原始排序后的所有项目
|
||||
let availableItems = []; // 当前可用于转盘的项目(排除已掌握的)
|
||||
let masteredItems = []; // 已掌握的项目列表
|
||||
let currentSpinIndex = 0;
|
||||
let currentSelectedItem = null; // 当前转盘选中的项目
|
||||
let isSpinning = false;
|
||||
|
||||
// 颜色配置 - 转盘使用不同颜色
|
||||
// 颜色配置 - 转盘使用不同颜色
|
||||
const colors = [
|
||||
'#667eea', '#764ba2', '#f093fb', '#f5576c',
|
||||
'#4facfe', '#00f2fe', '#43e97b', '#38f9d7',
|
||||
'#fa709a', '#fee140', '#30cfd0', '#330867'
|
||||
];
|
||||
|
||||
// DOM元素
|
||||
// DOM元素
|
||||
const textInput = document.getElementById('textInput');
|
||||
const extractBtn = document.getElementById('extractBtn');
|
||||
const extractedSection = document.getElementById('extractedSection');
|
||||
@@ -29,18 +32,23 @@ const resetBtn = document.getElementById('resetBtn');
|
||||
const exportTxtBtn = document.getElementById('exportTxtBtn');
|
||||
const exportJsonBtn = document.getElementById('exportJsonBtn');
|
||||
const exportCsvBtn = document.getElementById('exportCsvBtn');
|
||||
const masteredBtn = document.getElementById('masteredBtn');
|
||||
const forgotBtn = document.getElementById('forgotBtn');
|
||||
const masteryButtons = document.getElementById('masteryButtons');
|
||||
const remainingNum = document.getElementById('remainingNum');
|
||||
|
||||
// 本地存储键名
|
||||
// 本地存储键
|
||||
const STORAGE_KEY_EXTRACTED = 'recitation_extracted_items';
|
||||
const STORAGE_KEY_SORTED = 'recitation_sorted_items';
|
||||
const STORAGE_KEY_ORIGINAL_TEXT = 'recitation_original_text';
|
||||
const STORAGE_KEY_MASTERED = 'recitation_mastered_items';
|
||||
|
||||
// 页面加载时恢复数据
|
||||
// 页面加载时恢复数据
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
restoreFromStorage();
|
||||
});
|
||||
|
||||
// 保存到本地存储
|
||||
// 保存到本地存储
|
||||
function saveToStorage() {
|
||||
try {
|
||||
if (extractedItems.length > 0) {
|
||||
@@ -52,17 +60,21 @@ function saveToStorage() {
|
||||
if (textInput.value.trim()) {
|
||||
localStorage.setItem(STORAGE_KEY_ORIGINAL_TEXT, textInput.value);
|
||||
}
|
||||
if (masteredItems.length > 0) {
|
||||
localStorage.setItem(STORAGE_KEY_MASTERED, JSON.stringify(masteredItems));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存到本地存储失败:', e);
|
||||
console.error('保存到本地存储失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地存储恢复
|
||||
// 从本地存储恢复
|
||||
function restoreFromStorage() {
|
||||
try {
|
||||
const savedExtracted = localStorage.getItem(STORAGE_KEY_EXTRACTED);
|
||||
const savedSorted = localStorage.getItem(STORAGE_KEY_SORTED);
|
||||
const savedText = localStorage.getItem(STORAGE_KEY_ORIGINAL_TEXT);
|
||||
const savedMastered = localStorage.getItem(STORAGE_KEY_MASTERED);
|
||||
|
||||
if (savedText) {
|
||||
textInput.value = savedText;
|
||||
@@ -75,36 +87,54 @@ function restoreFromStorage() {
|
||||
textInput.disabled = true;
|
||||
}
|
||||
|
||||
if (savedMastered) {
|
||||
masteredItems = JSON.parse(savedMastered);
|
||||
}
|
||||
|
||||
if (savedSorted) {
|
||||
sortedItems = JSON.parse(savedSorted);
|
||||
updateAvailableItems(); // 更新可用项目列表
|
||||
displaySortedItems(sortedItems);
|
||||
createWheel(sortedItems);
|
||||
createWheel(availableItems);
|
||||
wheelSection.style.display = 'block';
|
||||
resultSection.style.display = 'block';
|
||||
updateMasteryInfo();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('从本地存储恢复失败:', e);
|
||||
console.error('从本地存储恢复失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除本地存储
|
||||
// 清除本地存储
|
||||
function clearStorage() {
|
||||
localStorage.removeItem(STORAGE_KEY_EXTRACTED);
|
||||
localStorage.removeItem(STORAGE_KEY_SORTED);
|
||||
localStorage.removeItem(STORAGE_KEY_ORIGINAL_TEXT);
|
||||
localStorage.removeItem(STORAGE_KEY_MASTERED);
|
||||
}
|
||||
|
||||
// 提取知识点
|
||||
// 更新可用项目列表(排除已掌握的)
|
||||
function updateAvailableItems() {
|
||||
availableItems = sortedItems.filter(item => !masteredItems.includes(item));
|
||||
updateMasteryInfo();
|
||||
}
|
||||
|
||||
// 更新掌握情况信息
|
||||
function updateMasteryInfo() {
|
||||
remainingNum.textContent = availableItems.length;
|
||||
}
|
||||
|
||||
// 提取知识点
|
||||
extractBtn.addEventListener('click', async () => {
|
||||
const text = textInput.value.trim();
|
||||
|
||||
if (!text) {
|
||||
alert('请输入要处理的文本');
|
||||
alert('请输入需要识别的文本');
|
||||
return;
|
||||
}
|
||||
|
||||
extractBtn.disabled = true;
|
||||
extractBtn.textContent = '识别中...';
|
||||
extractBtn.textContent = '识别中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/extract', {
|
||||
@@ -122,20 +152,20 @@ extractBtn.addEventListener('click', async () => {
|
||||
displayExtractedItems(extractedItems);
|
||||
extractedSection.style.display = 'block';
|
||||
textInput.disabled = true;
|
||||
saveToStorage(); // 保存到本地存储
|
||||
saveToStorage(); // 保存到本地存储
|
||||
} else {
|
||||
alert(data.message || '提取失败');
|
||||
alert(data.message || '提取失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提取失败:', error);
|
||||
alert('提取失败,请检查网络连接');
|
||||
console.error('提取失败:', error);
|
||||
alert('提取失败,请检查网络连接');
|
||||
} finally {
|
||||
extractBtn.disabled = false;
|
||||
extractBtn.textContent = '识别知识点';
|
||||
extractBtn.textContent = '识别知识点';
|
||||
}
|
||||
});
|
||||
|
||||
// 显示提取的项目
|
||||
// 显示提取到的项目
|
||||
function displayExtractedItems(items) {
|
||||
itemCount.textContent = items.length;
|
||||
itemsList.innerHTML = '';
|
||||
@@ -148,15 +178,15 @@ function displayExtractedItems(items) {
|
||||
});
|
||||
}
|
||||
|
||||
// 随机排序
|
||||
// 开始排序
|
||||
sortBtn.addEventListener('click', async () => {
|
||||
if (extractedItems.length === 0) {
|
||||
alert('请先提取知识点');
|
||||
alert('请先提取知识点');
|
||||
return;
|
||||
}
|
||||
|
||||
sortBtn.disabled = true;
|
||||
sortBtn.textContent = '排序中...';
|
||||
sortBtn.textContent = '排序中...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/sort', {
|
||||
@@ -171,36 +201,45 @@ sortBtn.addEventListener('click', async () => {
|
||||
|
||||
if (data.success) {
|
||||
sortedItems = data.items;
|
||||
masteredItems = []; // 重新排序时重置已掌握列表
|
||||
updateAvailableItems(); // 更新可用项目
|
||||
displaySortedItems(sortedItems);
|
||||
createWheel(sortedItems);
|
||||
createWheel(availableItems);
|
||||
wheelSection.style.display = 'block';
|
||||
resultSection.style.display = 'block';
|
||||
currentSpinIndex = 0;
|
||||
saveToStorage(); // 保存到本地存储
|
||||
currentSelectedItem = null;
|
||||
masteryButtons.style.display = 'none';
|
||||
saveToStorage(); // 保存到本地存储
|
||||
} else {
|
||||
alert(data.message || '排序失败');
|
||||
alert(data.message || '排序失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('排序失败:', error);
|
||||
alert('排序失败,请检查网络连接');
|
||||
console.error('排序失败:', error);
|
||||
alert('排序失败,请检查网络连接');
|
||||
} finally {
|
||||
sortBtn.disabled = false;
|
||||
sortBtn.textContent = '开始随机排序';
|
||||
sortBtn.textContent = '开始随机排序';
|
||||
}
|
||||
});
|
||||
|
||||
// 创建转盘 - 使用SVG实现更真实的转盘效果
|
||||
// 创建转盘 - 使用SVG实现真实转盘效果
|
||||
function createWheel(items) {
|
||||
wheel.innerHTML = '';
|
||||
|
||||
if (items.length === 0) return;
|
||||
if (items.length === 0) {
|
||||
currentItem.textContent = '所有知识点已掌握!';
|
||||
spinBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
spinBtn.disabled = false;
|
||||
const anglePerItem = 360 / items.length;
|
||||
const radius = 190; // 转盘半径(考虑边框)
|
||||
const radius = 190; // 转盘半径,考虑边框
|
||||
const centerX = 200;
|
||||
const centerY = 200;
|
||||
|
||||
// 创建SVG转盘
|
||||
// 创建SVG转盘
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.setAttribute('width', '400');
|
||||
svg.setAttribute('height', '400');
|
||||
@@ -210,6 +249,7 @@ function createWheel(items) {
|
||||
svg.style.left = '0';
|
||||
svg.style.width = '100%';
|
||||
svg.style.height = '100%';
|
||||
svg.style.transition = 'transform 3s cubic-bezier(0.17, 0.67, 0.12, 0.99)';
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const startAngle = (index * anglePerItem - 90) * Math.PI / 180;
|
||||
@@ -238,7 +278,7 @@ function createWheel(items) {
|
||||
|
||||
svg.appendChild(path);
|
||||
|
||||
// 添加文本
|
||||
// 添加文本
|
||||
const midAngle = (startAngle + endAngle) / 2;
|
||||
const textRadius = radius * 0.7;
|
||||
const textX = centerX + textRadius * Math.cos(midAngle);
|
||||
@@ -262,41 +302,47 @@ function createWheel(items) {
|
||||
wheel.appendChild(svg);
|
||||
}
|
||||
|
||||
// 转动转盘
|
||||
// 转动转盘
|
||||
spinBtn.addEventListener('click', () => {
|
||||
if (isSpinning || sortedItems.length === 0) return;
|
||||
if (isSpinning || availableItems.length === 0) return;
|
||||
|
||||
isSpinning = true;
|
||||
spinBtn.disabled = true;
|
||||
currentItem.textContent = '转盘中...';
|
||||
currentItem.textContent = '转盘中...';
|
||||
masteryButtons.style.display = 'none'; // 隐藏按钮,等待转盘停止
|
||||
currentSelectedItem = null;
|
||||
|
||||
// 随机选择一个索引(添加多圈旋转效果)
|
||||
const randomIndex = Math.floor(Math.random() * sortedItems.length);
|
||||
const spins = 3; // 转3圈
|
||||
const anglePerItem = 360 / sortedItems.length;
|
||||
// 计算目标角度:多转几圈 + 指向选中项
|
||||
// 随机选择一个项目,并增加多圈旋转效果
|
||||
const randomIndex = Math.floor(Math.random() * availableItems.length);
|
||||
const spins = 3; // 转3圈
|
||||
const anglePerItem = 360 / availableItems.length;
|
||||
// 计算目标角度:转多圈 + 指向选中项
|
||||
const targetAngle = spins * 360 + (360 - (randomIndex * anglePerItem) - anglePerItem / 2);
|
||||
|
||||
// 获取当前角度
|
||||
// 获取当前角度
|
||||
const svg = wheel.querySelector('svg');
|
||||
if (!svg) return;
|
||||
const currentAngle = getCurrentRotation(svg);
|
||||
|
||||
// 计算总旋转角度(考虑当前角度)
|
||||
// 计算总旋转角度(考虑当前角度)
|
||||
const totalRotation = currentAngle + targetAngle;
|
||||
|
||||
svg.style.transform = `rotate(${totalRotation}deg)`;
|
||||
|
||||
// 转盘停止后显示结果
|
||||
// 转盘停止后显示结果
|
||||
setTimeout(() => {
|
||||
currentItem.textContent = `${randomIndex + 1}. ${sortedItems[randomIndex]}`;
|
||||
currentSelectedItem = availableItems[randomIndex];
|
||||
currentItem.textContent = `${randomIndex + 1}. ${currentSelectedItem}`;
|
||||
currentSpinIndex = randomIndex;
|
||||
isSpinning = false;
|
||||
spinBtn.disabled = false;
|
||||
masteryButtons.style.display = 'flex'; // 显示掌握按钮
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// 获取当前旋转角度
|
||||
// 获取当前旋转角度
|
||||
function getCurrentRotation(element) {
|
||||
if (!element) return 0;
|
||||
const style = window.getComputedStyle(element);
|
||||
const transform = style.transform;
|
||||
if (transform === 'none') return 0;
|
||||
@@ -306,7 +352,51 @@ function getCurrentRotation(element) {
|
||||
return angle;
|
||||
}
|
||||
|
||||
// 显示排序结果
|
||||
// 背会了按钮
|
||||
masteredBtn.addEventListener('click', () => {
|
||||
if (!currentSelectedItem) return;
|
||||
|
||||
// 添加到已掌握列表
|
||||
if (!masteredItems.includes(currentSelectedItem)) {
|
||||
masteredItems.push(currentSelectedItem);
|
||||
}
|
||||
|
||||
// 更新可用项目列表
|
||||
updateAvailableItems();
|
||||
|
||||
// 重新创建转盘(因为项目数量可能改变)
|
||||
createWheel(availableItems);
|
||||
|
||||
// 隐藏按钮和当前项目显示
|
||||
masteryButtons.style.display = 'none';
|
||||
currentItem.textContent = '';
|
||||
currentSelectedItem = null;
|
||||
|
||||
// 重置转盘角度
|
||||
const svg = wheel.querySelector('svg');
|
||||
if (svg) {
|
||||
svg.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
|
||||
// 保存状态
|
||||
saveToStorage();
|
||||
|
||||
// 更新排序结果列表显示(标记已掌握的)
|
||||
displaySortedItems(sortedItems);
|
||||
});
|
||||
|
||||
// 忘记了按钮
|
||||
forgotBtn.addEventListener('click', () => {
|
||||
if (!currentSelectedItem) return;
|
||||
|
||||
// 忘记了的项目保留在列表中,不做任何操作
|
||||
// 隐藏按钮,可以继续转动转盘
|
||||
masteryButtons.style.display = 'none';
|
||||
currentItem.textContent = '';
|
||||
currentSelectedItem = null;
|
||||
});
|
||||
|
||||
// 显示排序结果
|
||||
function displaySortedItems(items) {
|
||||
sortedList.innerHTML = '';
|
||||
|
||||
@@ -314,6 +404,11 @@ function displaySortedItems(items) {
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.className = 'sorted-item';
|
||||
|
||||
// 如果已掌握,添加特殊样式
|
||||
if (masteredItems.includes(item)) {
|
||||
itemDiv.classList.add('sorted-item-mastered');
|
||||
}
|
||||
|
||||
const numberSpan = document.createElement('span');
|
||||
numberSpan.className = 'sorted-item-number';
|
||||
numberSpan.textContent = index + 1;
|
||||
@@ -322,20 +417,30 @@ function displaySortedItems(items) {
|
||||
textSpan.className = 'sorted-item-text';
|
||||
textSpan.textContent = item;
|
||||
|
||||
if (masteredItems.includes(item)) {
|
||||
const masteredIcon = document.createElement('span');
|
||||
masteredIcon.className = 'mastered-icon';
|
||||
masteredIcon.textContent = '✓';
|
||||
textSpan.appendChild(masteredIcon);
|
||||
}
|
||||
|
||||
itemDiv.appendChild(numberSpan);
|
||||
itemDiv.appendChild(textSpan);
|
||||
sortedList.appendChild(itemDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// 导出功能
|
||||
// 导出功能
|
||||
exportTxtBtn.addEventListener('click', () => exportData('txt'));
|
||||
exportJsonBtn.addEventListener('click', () => exportData('json'));
|
||||
exportCsvBtn.addEventListener('click', () => exportData('csv'));
|
||||
|
||||
function exportData(format) {
|
||||
if (sortedItems.length === 0) {
|
||||
alert('没有可导出的数据');
|
||||
// 导出时只导出当前可用的项目(未掌握的)
|
||||
const itemsToExport = availableItems.length > 0 ? availableItems : sortedItems;
|
||||
|
||||
if (itemsToExport.length === 0) {
|
||||
alert('没有可导出的数据');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -345,13 +450,13 @@ function exportData(format) {
|
||||
'Content-Type': 'application/json; charset=utf-8'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
items: sortedItems,
|
||||
items: itemsToExport,
|
||||
format: format
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('导出失败');
|
||||
throw new Error('导出失败');
|
||||
}
|
||||
return response.blob();
|
||||
})
|
||||
@@ -360,23 +465,26 @@ function exportData(format) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
|
||||
a.download = `背诵排序结果_${timestamp}.${format}`;
|
||||
a.download = `背诵排序结果_${timestamp}.${format}`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('导出失败:', error);
|
||||
alert('导出失败,请重试');
|
||||
console.error('导出失败:', error);
|
||||
alert('导出失败,请稍后重试');
|
||||
});
|
||||
}
|
||||
|
||||
// 重置
|
||||
// 重置
|
||||
resetBtn.addEventListener('click', () => {
|
||||
extractedItems = [];
|
||||
sortedItems = [];
|
||||
availableItems = [];
|
||||
masteredItems = [];
|
||||
currentSpinIndex = 0;
|
||||
currentSelectedItem = null;
|
||||
|
||||
textInput.value = '';
|
||||
textInput.disabled = false;
|
||||
@@ -391,10 +499,11 @@ resetBtn.addEventListener('click', () => {
|
||||
svg.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
currentItem.textContent = '';
|
||||
masteryButtons.style.display = 'none';
|
||||
|
||||
isSpinning = false;
|
||||
spinBtn.disabled = false;
|
||||
updateMasteryInfo();
|
||||
|
||||
clearStorage(); // 清除本地存储
|
||||
clearStorage(); // 清除本地存储
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user