Files
recommend/static/js/recitation.js
赵杰 Jie Zhao (雄狮汽车科技) cad03268f3 feat: add mastery feature to recitation wheel
2025-11-02 23:46:11 +08:00

510 lines
16 KiB
JavaScript

// 背诵排序功能脚本
let extractedItems = [];
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元素
const textInput = document.getElementById('textInput');
const extractBtn = document.getElementById('extractBtn');
const extractedSection = document.getElementById('extractedSection');
const itemsList = document.getElementById('itemsList');
const itemCount = document.getElementById('itemCount');
const sortBtn = document.getElementById('sortBtn');
const wheelSection = document.getElementById('wheelSection');
const wheel = document.getElementById('wheel');
const spinBtn = document.getElementById('spinBtn');
const currentItem = document.getElementById('currentItem');
const resultSection = document.getElementById('resultSection');
const sortedList = document.getElementById('sortedList');
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) {
localStorage.setItem(STORAGE_KEY_EXTRACTED, JSON.stringify(extractedItems));
}
if (sortedItems.length > 0) {
localStorage.setItem(STORAGE_KEY_SORTED, JSON.stringify(sortedItems));
}
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);
}
}
// 从本地存储恢复
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;
}
if (savedExtracted) {
extractedItems = JSON.parse(savedExtracted);
displayExtractedItems(extractedItems);
extractedSection.style.display = 'block';
textInput.disabled = true;
}
if (savedMastered) {
masteredItems = JSON.parse(savedMastered);
}
if (savedSorted) {
sortedItems = JSON.parse(savedSorted);
updateAvailableItems(); // 更新可用项目列表
displaySortedItems(sortedItems);
createWheel(availableItems);
wheelSection.style.display = 'block';
resultSection.style.display = 'block';
updateMasteryInfo();
}
} catch (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('请输入需要识别的文本');
return;
}
extractBtn.disabled = true;
extractBtn.textContent = '识别中...';
try {
const response = await fetch('/api/extract', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text })
});
const data = await response.json();
if (data.success) {
extractedItems = data.items;
displayExtractedItems(extractedItems);
extractedSection.style.display = 'block';
textInput.disabled = true;
saveToStorage(); // 保存到本地存储
} else {
alert(data.message || '提取失败');
}
} catch (error) {
console.error('提取失败:', error);
alert('提取失败,请检查网络连接');
} finally {
extractBtn.disabled = false;
extractBtn.textContent = '识别知识点';
}
});
// 显示提取到的项目
function displayExtractedItems(items) {
itemCount.textContent = items.length;
itemsList.innerHTML = '';
items.forEach((item, index) => {
const tag = document.createElement('span');
tag.className = 'item-tag';
tag.textContent = `${index + 1}. ${item}`;
itemsList.appendChild(tag);
});
}
// 开始排序
sortBtn.addEventListener('click', async () => {
if (extractedItems.length === 0) {
alert('请先提取知识点');
return;
}
sortBtn.disabled = true;
sortBtn.textContent = '排序中...';
try {
const response = await fetch('/api/sort', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ items: extractedItems })
});
const data = await response.json();
if (data.success) {
sortedItems = data.items;
masteredItems = []; // 重新排序时重置已掌握列表
updateAvailableItems(); // 更新可用项目
displaySortedItems(sortedItems);
createWheel(availableItems);
wheelSection.style.display = 'block';
resultSection.style.display = 'block';
currentSpinIndex = 0;
currentSelectedItem = null;
masteryButtons.style.display = 'none';
saveToStorage(); // 保存到本地存储
} else {
alert(data.message || '排序失败');
}
} catch (error) {
console.error('排序失败:', error);
alert('排序失败,请检查网络连接');
} finally {
sortBtn.disabled = false;
sortBtn.textContent = '开始随机排序';
}
});
// 创建转盘 - 使用SVG实现真实转盘效果
function createWheel(items) {
wheel.innerHTML = '';
if (items.length === 0) {
currentItem.textContent = '所有知识点已掌握!';
spinBtn.disabled = true;
return;
}
spinBtn.disabled = false;
const anglePerItem = 360 / items.length;
const radius = 190; // 转盘半径,考虑边框
const centerX = 200;
const centerY = 200;
// 创建SVG转盘
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', '400');
svg.setAttribute('height', '400');
svg.setAttribute('viewBox', '0 0 400 400');
svg.style.position = 'absolute';
svg.style.top = '0';
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;
const endAngle = ((index + 1) * anglePerItem - 90) * Math.PI / 180;
const x1 = centerX + radius * Math.cos(startAngle);
const y1 = centerY + radius * Math.sin(startAngle);
const x2 = centerX + radius * Math.cos(endAngle);
const y2 = centerY + radius * Math.sin(endAngle);
const largeArcFlag = anglePerItem > 180 ? 1 : 0;
const pathData = [
`M ${centerX} ${centerY}`,
`L ${x1} ${y1}`,
`A ${radius} ${radius} 0 ${largeArcFlag} 1 ${x2} ${y2}`,
'Z'
].join(' ');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', pathData);
const colorIndex = index % colors.length;
path.setAttribute('fill', colors[colorIndex]);
path.setAttribute('stroke', '#fff');
path.setAttribute('stroke-width', '2');
svg.appendChild(path);
// 添加文本
const midAngle = (startAngle + endAngle) / 2;
const textRadius = radius * 0.7;
const textX = centerX + textRadius * Math.cos(midAngle);
const textY = centerY + textRadius * Math.sin(midAngle);
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', textX);
text.setAttribute('y', textY);
text.setAttribute('text-anchor', 'middle');
text.setAttribute('fill', 'white');
text.setAttribute('font-size', items.length > 8 ? '12' : '14');
text.setAttribute('font-weight', 'bold');
text.setAttribute('transform', `rotate(${(midAngle * 180 / Math.PI + 90)}, ${textX}, ${textY})`);
const displayText = item.length > 12 ? item.substring(0, 12) + '...' : item;
text.textContent = displayText;
svg.appendChild(text);
});
wheel.appendChild(svg);
}
// 转动转盘
spinBtn.addEventListener('click', () => {
if (isSpinning || availableItems.length === 0) return;
isSpinning = true;
spinBtn.disabled = true;
currentItem.textContent = '转盘中...';
masteryButtons.style.display = 'none'; // 隐藏按钮,等待转盘停止
currentSelectedItem = null;
// 随机选择一个项目,并增加多圈旋转效果
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(() => {
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;
const matrix = new DOMMatrix(transform);
const angle = Math.atan2(matrix.b, matrix.a) * (180 / Math.PI);
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 = '';
items.forEach((item, index) => {
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;
const textSpan = document.createElement('span');
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) {
// 导出时只导出当前可用的项目(未掌握的)
const itemsToExport = availableItems.length > 0 ? availableItems : sortedItems;
if (itemsToExport.length === 0) {
alert('没有可导出的数据');
return;
}
fetch('/api/export/sorted', {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify({
items: itemsToExport,
format: format
})
})
.then(response => {
if (!response.ok) {
throw new Error('导出失败');
}
return response.blob();
})
.then(blob => {
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
a.download = `背诵排序结果_${timestamp}.${format}`;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
document.body.removeChild(a);
})
.catch(error => {
console.error('导出失败:', error);
alert('导出失败,请稍后重试');
});
}
// 重置
resetBtn.addEventListener('click', () => {
extractedItems = [];
sortedItems = [];
availableItems = [];
masteredItems = [];
currentSpinIndex = 0;
currentSelectedItem = null;
textInput.value = '';
textInput.disabled = false;
extractedSection.style.display = 'none';
wheelSection.style.display = 'none';
resultSection.style.display = 'none';
wheel.innerHTML = '';
const svg = wheel.querySelector('svg');
if (svg) {
svg.style.transform = 'rotate(0deg)';
}
currentItem.textContent = '';
masteryButtons.style.display = 'none';
isSpinning = false;
spinBtn.disabled = false;
updateMasteryInfo();
clearStorage(); // 清除本地存储
});