Spaces:
Running
Running
Update index.html
Browse files- index.html +163 -47
index.html
CHANGED
|
@@ -151,6 +151,7 @@
|
|
| 151 |
|
| 152 |
<!-- 教師工具 -->
|
| 153 |
<div id="teacher-tools" class="mt-8 pt-4 border-t flex justify-end items-center gap-3">
|
|
|
|
| 154 |
<button id="share-game-btn" class="text-white font-semibold py-2 px-5 rounded-full transition-transform hover:scale-105 bg-violet-500 hover:bg-violet-600 text-sm">🔗 分享</button>
|
| 155 |
<button id="manage-words-btn" class="text-white font-semibold py-2 px-5 rounded-full transition-transform hover:scale-105 bg-gray-500 hover:bg-gray-600 text-sm">⚙️ 管理</button>
|
| 156 |
</div>
|
|
@@ -289,6 +290,12 @@
|
|
| 289 |
</button>
|
| 290 |
</div>
|
| 291 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
<!-- 複習模式導覽 -->
|
| 293 |
<div id="review-nav-container" class="flex items-center justify-between w-full max-w-md mt-10 space-x-4">
|
| 294 |
<button id="prev-btn" class="nav-btn">
|
|
@@ -371,12 +378,46 @@
|
|
| 371 |
</div>
|
| 372 |
</div>
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
|
| 375 |
<!-- 清除確認 Modal -->
|
| 376 |
<div id="confirm-clear-modal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 p-4">
|
| 377 |
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-sm">
|
| 378 |
<h3 class="text-2xl font-bold mb-4 text-gray-800">確認操作</h3>
|
| 379 |
-
<p class="text-gray-600 mb-6"
|
| 380 |
<div class="flex justify-end gap-4 mt-6">
|
| 381 |
<button type="button" id="cancel-clear-btn" class="px-6 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors">取消</button>
|
| 382 |
<button type="button" id="confirm-clear-btn" class="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">確認清除</button>
|
|
@@ -418,6 +459,7 @@
|
|
| 418 |
const teacherTools = document.getElementById('teacher-tools');
|
| 419 |
const manageWordsBtn = document.getElementById('manage-words-btn');
|
| 420 |
const shareGameBtn = document.getElementById('share-game-btn');
|
|
|
|
| 421 |
const randomQuestionsCheckbox = document.getElementById('random-questions-checkbox');
|
| 422 |
const randomQuestionsCountInput = document.getElementById('random-questions-count');
|
| 423 |
|
|
@@ -459,6 +501,7 @@
|
|
| 459 |
const hintSection = document.getElementById('hint-section');
|
| 460 |
const hintBtn = document.getElementById('hint-btn');
|
| 461 |
const hintDisplay = document.getElementById('hint-display');
|
|
|
|
| 462 |
|
| 463 |
// 複習模式導覽
|
| 464 |
const reviewNavContainer = document.getElementById('review-nav-container');
|
|
@@ -475,7 +518,7 @@
|
|
| 475 |
const finalScore = document.getElementById('final-score');
|
| 476 |
const newHighscoreMsg = document.getElementById('new-highscore-msg');
|
| 477 |
const playAgainBtn = document.getElementById('play-again-btn');
|
| 478 |
-
|
| 479 |
// Modals
|
| 480 |
const passwordModal = document.getElementById('password-modal');
|
| 481 |
const passwordForm = document.getElementById('password-form');
|
|
@@ -497,6 +540,11 @@
|
|
| 497 |
const copyFeedback = document.getElementById('copy-feedback');
|
| 498 |
const closeShareModalBtn = document.getElementById('close-share-modal-btn');
|
| 499 |
const qrcodeContainer = document.getElementById('qrcode-container');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
const confirmClearModal = document.getElementById('confirm-clear-modal');
|
| 501 |
const cancelClearBtn = document.getElementById('cancel-clear-btn');
|
| 502 |
const confirmClearBtn = document.getElementById('confirm-clear-btn');
|
|
@@ -514,6 +562,7 @@
|
|
| 514 |
let words = [];
|
| 515 |
let quizQueue = [];
|
| 516 |
let wordsForCurrentMode = [];
|
|
|
|
| 517 |
let quizIncorrectCount = 0;
|
| 518 |
let currentCardIndex = 0;
|
| 519 |
let currentMode = '';
|
|
@@ -528,6 +577,7 @@
|
|
| 528 |
let timeLeft = 60;
|
| 529 |
let currentSpeedCard = null;
|
| 530 |
let currentSpeedQuestionType = '';
|
|
|
|
| 531 |
|
| 532 |
const modeDetails = {
|
| 533 |
'review': { title: '複習模式' }, 'zh-en': { title: '中翻英測驗' },
|
|
@@ -565,7 +615,6 @@
|
|
| 565 |
// --- 單字管理功能 ---
|
| 566 |
const renderWordList = () => {
|
| 567 |
wordListContainer.innerHTML = '';
|
| 568 |
-
// 移除字母排序,直接按照陣列順序渲染
|
| 569 |
words.forEach((word, index) => {
|
| 570 |
const item = document.createElement('div');
|
| 571 |
item.className = 'list-item p-3 bg-gray-100 rounded-lg flex items-center justify-between';
|
|
@@ -683,6 +732,8 @@
|
|
| 683 |
}
|
| 684 |
|
| 685 |
flashcardContainer.style.cursor = 'default';
|
|
|
|
|
|
|
| 686 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.add('hidden'));
|
| 687 |
feedbackDisplay.textContent = '';
|
| 688 |
answerInput.value = '';
|
|
@@ -752,7 +803,6 @@
|
|
| 752 |
|
| 753 |
if (flashcardContainer.classList.contains('flipped')) {
|
| 754 |
flashcardContainer.classList.remove('flipped');
|
| 755 |
-
// Wait for the flip animation (0.6s) to mostly complete before changing content
|
| 756 |
setTimeout(updateCardContent, 600);
|
| 757 |
} else {
|
| 758 |
updateCardContent();
|
|
@@ -771,21 +821,19 @@
|
|
| 771 |
progressBarContainer.classList.toggle('hidden', isReview || isSpeed);
|
| 772 |
speedHud.classList.toggle('hidden', !isSpeed);
|
| 773 |
hudPlaceholder.classList.toggle('hidden', isSpeed);
|
|
|
|
| 774 |
|
| 775 |
const start = parseInt(startRangeInput.value, 10);
|
| 776 |
const end = parseInt(endRangeInput.value, 10);
|
| 777 |
|
| 778 |
let wordPool;
|
| 779 |
-
// 1. First, determine the pool of words based on the range.
|
| 780 |
if (!isNaN(start) && !isNaN(end) && start > 0 && end >= start && end <= words.length) {
|
| 781 |
wordPool = words.slice(start - 1, end);
|
| 782 |
} else {
|
| 783 |
wordPool = [...words];
|
| 784 |
}
|
| 785 |
|
| 786 |
-
// 2. Then, determine the final word set for the mode.
|
| 787 |
if (mode === 'review') {
|
| 788 |
-
// For review mode, ALWAYS use the full pool, ignoring random settings.
|
| 789 |
wordsForCurrentMode = wordPool;
|
| 790 |
} else if (randomQuestionsCheckbox.checked) {
|
| 791 |
const randomCount = parseInt(randomQuestionsCountInput.value, 10);
|
|
@@ -796,7 +844,6 @@
|
|
| 796 |
return;
|
| 797 |
}
|
| 798 |
} else {
|
| 799 |
-
// For other modes without random, use the entire pool.
|
| 800 |
wordsForCurrentMode = wordPool;
|
| 801 |
}
|
| 802 |
|
|
@@ -817,6 +864,10 @@
|
|
| 817 |
quizQueue = shuffleArray([...wordsForCurrentMode]);
|
| 818 |
}
|
| 819 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 820 |
quizIncorrectCount = 0;
|
| 821 |
updateHintButtonVisibility();
|
| 822 |
|
|
@@ -840,22 +891,14 @@
|
|
| 840 |
|
| 841 |
const wordIndex = words.findIndex(w => w.english === card.english && w.chinese === card.chinese);
|
| 842 |
|
| 843 |
-
let correctAnswer;
|
| 844 |
-
let answerLang;
|
| 845 |
|
| 846 |
if (isReview) {
|
| 847 |
const isFlipped = flashcardContainer.classList.contains('flipped');
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
answerLang = 'en';
|
| 851 |
-
} else { // English side showing, user should type Chinese
|
| 852 |
-
correctAnswer = card.chinese.split('(')[0].trim();
|
| 853 |
-
answerLang = 'zh';
|
| 854 |
-
}
|
| 855 |
} else {
|
| 856 |
-
let effectiveMode = currentMode;
|
| 857 |
-
if (isSpeed) effectiveMode = currentSpeedQuestionType;
|
| 858 |
-
if (currentMode === 'hard') effectiveMode = 'zh-en';
|
| 859 |
switch (effectiveMode) {
|
| 860 |
case 'zh-en': case 'listen':
|
| 861 |
correctAnswer = card.english.split('(')[0].trim();
|
|
@@ -903,19 +946,36 @@
|
|
| 903 |
if (quizQueue.length > 0) {
|
| 904 |
displayCard();
|
| 905 |
} else {
|
| 906 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 907 |
if (currentMode !== 'hard') {
|
| 908 |
completionStatus[currentMode] = true;
|
| 909 |
localStorage.setItem('completionStatus', JSON.stringify(completionStatus));
|
| 910 |
updateCompletionUI();
|
| 911 |
}
|
| 912 |
-
|
| 913 |
-
|
|
|
|
|
|
|
|
|
|
| 914 |
}
|
| 915 |
}, 1500);
|
| 916 |
-
}
|
| 917 |
-
|
| 918 |
-
if (isReview) {
|
| 919 |
answerInput.disabled = false;
|
| 920 |
submitBtn.disabled = false;
|
| 921 |
}
|
|
@@ -955,9 +1015,8 @@
|
|
| 955 |
|
| 956 |
|
| 957 |
const updateHintButtonVisibility = () => {
|
| 958 |
-
const isQuiz =
|
| 959 |
-
|
| 960 |
-
hintSection.classList.toggle('hidden', !shouldShow);
|
| 961 |
};
|
| 962 |
|
| 963 |
confirmWrongBtn.addEventListener('click', () => {
|
|
@@ -1052,6 +1111,42 @@
|
|
| 1052 |
});
|
| 1053 |
};
|
| 1054 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1055 |
const generateQRCode = (text) => {
|
| 1056 |
qrcodeContainer.innerHTML = '';
|
| 1057 |
try {
|
|
@@ -1071,7 +1166,33 @@
|
|
| 1071 |
document.querySelectorAll('.mode-btn[data-mode]').forEach(btn => {
|
| 1072 |
btn.addEventListener('click', () => setupLearningView(btn.dataset.mode));
|
| 1073 |
});
|
| 1074 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1075 |
|
| 1076 |
flashcardContainer.addEventListener('click', () => {
|
| 1077 |
if (currentMode === 'review') {
|
|
@@ -1152,6 +1273,12 @@
|
|
| 1152 |
shareModal.classList.remove('hidden');
|
| 1153 |
});
|
| 1154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1155 |
generateLinkBtn.addEventListener('click', async () => {
|
| 1156 |
generateLinkBtn.textContent = '產生中...';
|
| 1157 |
generateLinkBtn.disabled = true;
|
|
@@ -1179,21 +1306,17 @@
|
|
| 1179 |
|
| 1180 |
const longUrl = url.toString();
|
| 1181 |
|
| 1182 |
-
// 使用TinyURL API縮短網址
|
| 1183 |
const response = await fetch(`https://tinyurl.com/api-create.php?url=${encodeURIComponent(longUrl)}`);
|
| 1184 |
if (response.ok) {
|
| 1185 |
const shortUrl = await response.text();
|
| 1186 |
shareLinkInput.value = shortUrl;
|
| 1187 |
generateQRCode(shortUrl);
|
| 1188 |
} else {
|
| 1189 |
-
// 如果API失敗,則退回使用長網址
|
| 1190 |
shareLinkInput.value = longUrl;
|
| 1191 |
generateQRCode(longUrl);
|
| 1192 |
copyFeedback.textContent = '縮短網址失敗,已產生原始連結。';
|
| 1193 |
}
|
| 1194 |
-
|
| 1195 |
shareResultContainer.classList.remove('hidden');
|
| 1196 |
-
|
| 1197 |
} catch (error) {
|
| 1198 |
console.error("產生分享連結時發生錯誤:", error);
|
| 1199 |
alert("產生分享連結時發生錯誤,可能是網路問題或單字列表過大。");
|
|
@@ -1206,7 +1329,7 @@
|
|
| 1206 |
|
| 1207 |
copyLinkBtn.addEventListener('click', () => {
|
| 1208 |
shareLinkInput.select();
|
| 1209 |
-
shareLinkInput.setSelectionRange(0, 99999);
|
| 1210 |
try {
|
| 1211 |
document.execCommand('copy');
|
| 1212 |
copyFeedback.textContent = '已成功複製!';
|
|
@@ -1230,7 +1353,9 @@
|
|
| 1230 |
|
| 1231 |
confirmClearBtn.addEventListener('click', () => {
|
| 1232 |
words = [];
|
|
|
|
| 1233 |
saveWordsToStorage();
|
|
|
|
| 1234 |
renderWordList();
|
| 1235 |
confirmClearModal.classList.add('hidden');
|
| 1236 |
});
|
|
@@ -1242,17 +1367,8 @@
|
|
| 1242 |
|
| 1243 |
confirmDeleteBtn.addEventListener('click', () => {
|
| 1244 |
if (indexToDelete > -1) {
|
| 1245 |
-
const
|
| 1246 |
-
|
| 1247 |
-
allDeleteButtons.forEach(btn => {
|
| 1248 |
-
if(parseInt(btn.dataset.index, 10) === indexToDelete) {
|
| 1249 |
-
itemElement = btn.closest('.list-item');
|
| 1250 |
-
}
|
| 1251 |
-
});
|
| 1252 |
-
|
| 1253 |
-
if (itemElement) {
|
| 1254 |
-
itemElement.classList.add('removing');
|
| 1255 |
-
}
|
| 1256 |
|
| 1257 |
setTimeout(() => {
|
| 1258 |
if (indexToDelete > -1) {
|
|
@@ -1329,6 +1445,7 @@
|
|
| 1329 |
lessonTitle = localStorage.getItem('flashcardsLessonTitle') || 'L1';
|
| 1330 |
updateMainTitle();
|
| 1331 |
|
|
|
|
| 1332 |
completionStatus = JSON.parse(localStorage.getItem('completionStatus')) || {};
|
| 1333 |
updateCompletionUI();
|
| 1334 |
highScore = parseInt(localStorage.getItem('flashcardsHighScore') || '0', 10);
|
|
@@ -1364,7 +1481,6 @@
|
|
| 1364 |
};
|
| 1365 |
initializeApp();
|
| 1366 |
|
| 1367 |
-
// Append global styles
|
| 1368 |
const style = document.createElement('style');
|
| 1369 |
style.textContent = `
|
| 1370 |
.mode-btn { color: white; font-weight: bold; padding: 1rem 1.5rem; border-radius: 1.5rem; transition: all 0.3s; transform: translateY(0); display: flex; justify-content: center; align-items: center; text-align: center; height: 100%;}
|
|
|
|
| 151 |
|
| 152 |
<!-- 教師工具 -->
|
| 153 |
<div id="teacher-tools" class="mt-8 pt-4 border-t flex justify-end items-center gap-3">
|
| 154 |
+
<button id="grade-report-btn" class="text-white font-semibold py-2 px-5 rounded-full transition-transform hover:scale-105 bg-blue-500 hover:bg-blue-600 text-sm">📊 成績報告</button>
|
| 155 |
<button id="share-game-btn" class="text-white font-semibold py-2 px-5 rounded-full transition-transform hover:scale-105 bg-violet-500 hover:bg-violet-600 text-sm">🔗 分享</button>
|
| 156 |
<button id="manage-words-btn" class="text-white font-semibold py-2 px-5 rounded-full transition-transform hover:scale-105 bg-gray-500 hover:bg-gray-600 text-sm">⚙️ 管理</button>
|
| 157 |
</div>
|
|
|
|
| 290 |
</button>
|
| 291 |
</div>
|
| 292 |
|
| 293 |
+
<!-- 測驗完成提示訊息 -->
|
| 294 |
+
<div id="quiz-completion-message" class="hidden w-full max-w-md mt-4 flex flex-col items-center text-center p-6 bg-white rounded-2xl shadow-lg">
|
| 295 |
+
<h2 class="text-3xl font-bold text-green-600">測驗完成!</h2>
|
| 296 |
+
<p class="mt-4 text-lg text-gray-700">成績已記錄,即將返回主選單...</p>
|
| 297 |
+
</div>
|
| 298 |
+
|
| 299 |
<!-- 複習模式導覽 -->
|
| 300 |
<div id="review-nav-container" class="flex items-center justify-between w-full max-w-md mt-10 space-x-4">
|
| 301 |
<button id="prev-btn" class="nav-btn">
|
|
|
|
| 378 |
</div>
|
| 379 |
</div>
|
| 380 |
|
| 381 |
+
<!-- 成績報告 Modal -->
|
| 382 |
+
<div id="grade-report-modal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 p-4">
|
| 383 |
+
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-4xl">
|
| 384 |
+
<div class="flex justify-between items-center mb-6">
|
| 385 |
+
<h3 class="text-3xl font-bold text-gray-800">成績報告</h3>
|
| 386 |
+
<button type="button" id="close-report-modal-btn" class="p-2 rounded-full hover:bg-gray-200 transition-colors">
|
| 387 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /></svg>
|
| 388 |
+
</button>
|
| 389 |
+
</div>
|
| 390 |
+
<div id="report-content" class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 391 |
+
<!-- Report for zh-en -->
|
| 392 |
+
<div class="p-4 bg-teal-50 rounded-lg border border-teal-200">
|
| 393 |
+
<h4 class="text-xl font-bold text-teal-800 mb-4">中翻英測驗</h4>
|
| 394 |
+
<div id="report-zh-en" class="space-y-3 text-left text-gray-700 max-h-80 overflow-y-auto pr-2">
|
| 395 |
+
<!-- Content will be generated by JS -->
|
| 396 |
+
</div>
|
| 397 |
+
</div>
|
| 398 |
+
<!-- Report for en-zh -->
|
| 399 |
+
<div class="p-4 bg-amber-50 rounded-lg border border-amber-200">
|
| 400 |
+
<h4 class="text-xl font-bold text-amber-800 mb-4">英翻中測驗</h4>
|
| 401 |
+
<div id="report-en-zh" class="space-y-3 text-left text-gray-700 max-h-80 overflow-y-auto pr-2">
|
| 402 |
+
<!-- Content will be generated by JS -->
|
| 403 |
+
</div>
|
| 404 |
+
</div>
|
| 405 |
+
<!-- Report for listen -->
|
| 406 |
+
<div class="p-4 bg-rose-50 rounded-lg border border-rose-200">
|
| 407 |
+
<h4 class="text-xl font-bold text-rose-800 mb-4">聽力測驗</h4>
|
| 408 |
+
<div id="report-listen" class="space-y-3 text-left text-gray-700 max-h-80 overflow-y-auto pr-2">
|
| 409 |
+
<!-- Content will be generated by JS -->
|
| 410 |
+
</div>
|
| 411 |
+
</div>
|
| 412 |
+
</div>
|
| 413 |
+
</div>
|
| 414 |
+
</div>
|
| 415 |
|
| 416 |
<!-- 清除確認 Modal -->
|
| 417 |
<div id="confirm-clear-modal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 p-4">
|
| 418 |
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-sm">
|
| 419 |
<h3 class="text-2xl font-bold mb-4 text-gray-800">確認操作</h3>
|
| 420 |
+
<p class="text-gray-600 mb-6">您確定要清除所有單字和成績紀錄嗎?此操作無法復原。</p>
|
| 421 |
<div class="flex justify-end gap-4 mt-6">
|
| 422 |
<button type="button" id="cancel-clear-btn" class="px-6 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors">取消</button>
|
| 423 |
<button type="button" id="confirm-clear-btn" class="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">確認清除</button>
|
|
|
|
| 459 |
const teacherTools = document.getElementById('teacher-tools');
|
| 460 |
const manageWordsBtn = document.getElementById('manage-words-btn');
|
| 461 |
const shareGameBtn = document.getElementById('share-game-btn');
|
| 462 |
+
const gradeReportBtn = document.getElementById('grade-report-btn');
|
| 463 |
const randomQuestionsCheckbox = document.getElementById('random-questions-checkbox');
|
| 464 |
const randomQuestionsCountInput = document.getElementById('random-questions-count');
|
| 465 |
|
|
|
|
| 501 |
const hintSection = document.getElementById('hint-section');
|
| 502 |
const hintBtn = document.getElementById('hint-btn');
|
| 503 |
const hintDisplay = document.getElementById('hint-display');
|
| 504 |
+
const quizCompletionMessage = document.getElementById('quiz-completion-message');
|
| 505 |
|
| 506 |
// 複習模式導覽
|
| 507 |
const reviewNavContainer = document.getElementById('review-nav-container');
|
|
|
|
| 518 |
const finalScore = document.getElementById('final-score');
|
| 519 |
const newHighscoreMsg = document.getElementById('new-highscore-msg');
|
| 520 |
const playAgainBtn = document.getElementById('play-again-btn');
|
| 521 |
+
|
| 522 |
// Modals
|
| 523 |
const passwordModal = document.getElementById('password-modal');
|
| 524 |
const passwordForm = document.getElementById('password-form');
|
|
|
|
| 540 |
const copyFeedback = document.getElementById('copy-feedback');
|
| 541 |
const closeShareModalBtn = document.getElementById('close-share-modal-btn');
|
| 542 |
const qrcodeContainer = document.getElementById('qrcode-container');
|
| 543 |
+
const gradeReportModal = document.getElementById('grade-report-modal');
|
| 544 |
+
const closeReportModalBtn = document.getElementById('close-report-modal-btn');
|
| 545 |
+
const reportZhEn = document.getElementById('report-zh-en');
|
| 546 |
+
const reportEnZh = document.getElementById('report-en-zh');
|
| 547 |
+
const reportListen = document.getElementById('report-listen');
|
| 548 |
const confirmClearModal = document.getElementById('confirm-clear-modal');
|
| 549 |
const cancelClearBtn = document.getElementById('cancel-clear-btn');
|
| 550 |
const confirmClearBtn = document.getElementById('confirm-clear-btn');
|
|
|
|
| 562 |
let words = [];
|
| 563 |
let quizQueue = [];
|
| 564 |
let wordsForCurrentMode = [];
|
| 565 |
+
let gradeReports = {};
|
| 566 |
let quizIncorrectCount = 0;
|
| 567 |
let currentCardIndex = 0;
|
| 568 |
let currentMode = '';
|
|
|
|
| 577 |
let timeLeft = 60;
|
| 578 |
let currentSpeedCard = null;
|
| 579 |
let currentSpeedQuestionType = '';
|
| 580 |
+
let quizStartTime = 0;
|
| 581 |
|
| 582 |
const modeDetails = {
|
| 583 |
'review': { title: '複習模式' }, 'zh-en': { title: '中翻英測驗' },
|
|
|
|
| 615 |
// --- 單字管理功能 ---
|
| 616 |
const renderWordList = () => {
|
| 617 |
wordListContainer.innerHTML = '';
|
|
|
|
| 618 |
words.forEach((word, index) => {
|
| 619 |
const item = document.createElement('div');
|
| 620 |
item.className = 'list-item p-3 bg-gray-100 rounded-lg flex items-center justify-between';
|
|
|
|
| 732 |
}
|
| 733 |
|
| 734 |
flashcardContainer.style.cursor = 'default';
|
| 735 |
+
flashcardContainer.classList.remove('hidden');
|
| 736 |
+
quizCompletionMessage.classList.add('hidden');
|
| 737 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.add('hidden'));
|
| 738 |
feedbackDisplay.textContent = '';
|
| 739 |
answerInput.value = '';
|
|
|
|
| 803 |
|
| 804 |
if (flashcardContainer.classList.contains('flipped')) {
|
| 805 |
flashcardContainer.classList.remove('flipped');
|
|
|
|
| 806 |
setTimeout(updateCardContent, 600);
|
| 807 |
} else {
|
| 808 |
updateCardContent();
|
|
|
|
| 821 |
progressBarContainer.classList.toggle('hidden', isReview || isSpeed);
|
| 822 |
speedHud.classList.toggle('hidden', !isSpeed);
|
| 823 |
hudPlaceholder.classList.toggle('hidden', isSpeed);
|
| 824 |
+
quizCompletionMessage.classList.add('hidden');
|
| 825 |
|
| 826 |
const start = parseInt(startRangeInput.value, 10);
|
| 827 |
const end = parseInt(endRangeInput.value, 10);
|
| 828 |
|
| 829 |
let wordPool;
|
|
|
|
| 830 |
if (!isNaN(start) && !isNaN(end) && start > 0 && end >= start && end <= words.length) {
|
| 831 |
wordPool = words.slice(start - 1, end);
|
| 832 |
} else {
|
| 833 |
wordPool = [...words];
|
| 834 |
}
|
| 835 |
|
|
|
|
| 836 |
if (mode === 'review') {
|
|
|
|
| 837 |
wordsForCurrentMode = wordPool;
|
| 838 |
} else if (randomQuestionsCheckbox.checked) {
|
| 839 |
const randomCount = parseInt(randomQuestionsCountInput.value, 10);
|
|
|
|
| 844 |
return;
|
| 845 |
}
|
| 846 |
} else {
|
|
|
|
| 847 |
wordsForCurrentMode = wordPool;
|
| 848 |
}
|
| 849 |
|
|
|
|
| 864 |
quizQueue = shuffleArray([...wordsForCurrentMode]);
|
| 865 |
}
|
| 866 |
|
| 867 |
+
if (isQuiz) {
|
| 868 |
+
quizStartTime = Date.now();
|
| 869 |
+
}
|
| 870 |
+
|
| 871 |
quizIncorrectCount = 0;
|
| 872 |
updateHintButtonVisibility();
|
| 873 |
|
|
|
|
| 891 |
|
| 892 |
const wordIndex = words.findIndex(w => w.english === card.english && w.chinese === card.chinese);
|
| 893 |
|
| 894 |
+
let correctAnswer, answerLang;
|
|
|
|
| 895 |
|
| 896 |
if (isReview) {
|
| 897 |
const isFlipped = flashcardContainer.classList.contains('flipped');
|
| 898 |
+
correctAnswer = isFlipped ? card.english.split('(')[0].trim() : card.chinese.split('(')[0].trim();
|
| 899 |
+
answerLang = isFlipped ? 'en' : 'zh';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 900 |
} else {
|
| 901 |
+
let effectiveMode = isSpeed ? currentSpeedQuestionType : (currentMode === 'hard' ? 'zh-en' : currentMode);
|
|
|
|
|
|
|
| 902 |
switch (effectiveMode) {
|
| 903 |
case 'zh-en': case 'listen':
|
| 904 |
correctAnswer = card.english.split('(')[0].trim();
|
|
|
|
| 946 |
if (quizQueue.length > 0) {
|
| 947 |
displayCard();
|
| 948 |
} else {
|
| 949 |
+
// 測驗完成
|
| 950 |
+
const elapsedTime = Math.round((Date.now() - quizStartTime) / 1000);
|
| 951 |
+
const reportData = {
|
| 952 |
+
total: wordsForCurrentMode.length,
|
| 953 |
+
mistakes: quizIncorrectCount,
|
| 954 |
+
time: elapsedTime,
|
| 955 |
+
date: new Date().toISOString(),
|
| 956 |
+
status: 'completed'
|
| 957 |
+
};
|
| 958 |
+
|
| 959 |
+
const history = gradeReports[currentMode] || [];
|
| 960 |
+
history.push(reportData);
|
| 961 |
+
gradeReports[currentMode] = history;
|
| 962 |
+
localStorage.setItem('gradeReports', JSON.stringify(gradeReports));
|
| 963 |
+
|
| 964 |
+
quizStartTime = 0; // Reset timer
|
| 965 |
+
|
| 966 |
if (currentMode !== 'hard') {
|
| 967 |
completionStatus[currentMode] = true;
|
| 968 |
localStorage.setItem('completionStatus', JSON.stringify(completionStatus));
|
| 969 |
updateCompletionUI();
|
| 970 |
}
|
| 971 |
+
|
| 972 |
+
[flashcardContainer, quizContainer, progressBarContainer, hintSection].forEach(el => el.classList.add('hidden'));
|
| 973 |
+
quizCompletionMessage.classList.remove('hidden');
|
| 974 |
+
|
| 975 |
+
setTimeout(() => showView('menu'), 3000);
|
| 976 |
}
|
| 977 |
}, 1500);
|
| 978 |
+
} else { // Review mode
|
|
|
|
|
|
|
| 979 |
answerInput.disabled = false;
|
| 980 |
submitBtn.disabled = false;
|
| 981 |
}
|
|
|
|
| 1015 |
|
| 1016 |
|
| 1017 |
const updateHintButtonVisibility = () => {
|
| 1018 |
+
const isQuiz = !['review', 'speed'].includes(currentMode);
|
| 1019 |
+
hintSection.classList.toggle('hidden', !(isQuiz && quizIncorrectCount >= 2));
|
|
|
|
| 1020 |
};
|
| 1021 |
|
| 1022 |
confirmWrongBtn.addEventListener('click', () => {
|
|
|
|
| 1111 |
});
|
| 1112 |
};
|
| 1113 |
|
| 1114 |
+
const renderGradeReport = () => {
|
| 1115 |
+
const reportTargets = { 'zh-en': reportZhEn, 'en-zh': reportEnZh, 'listen': reportListen };
|
| 1116 |
+
for (const mode in reportTargets) {
|
| 1117 |
+
const history = gradeReports[mode] || [];
|
| 1118 |
+
const targetDiv = reportTargets[mode];
|
| 1119 |
+
targetDiv.innerHTML = ''; // Clear previous content
|
| 1120 |
+
|
| 1121 |
+
if (history.length === 0) {
|
| 1122 |
+
targetDiv.innerHTML = `<p class="text-gray-500 p-4 text-center">尚未有紀錄</p>`;
|
| 1123 |
+
} else {
|
| 1124 |
+
history.slice().reverse().forEach(report => {
|
| 1125 |
+
const minutes = Math.floor(report.time / 60);
|
| 1126 |
+
const seconds = report.time % 60;
|
| 1127 |
+
const isCompleted = report.status === 'completed';
|
| 1128 |
+
|
| 1129 |
+
const reportElement = document.createElement('div');
|
| 1130 |
+
reportElement.className = 'p-3 mb-3 bg-white rounded-lg shadow-sm border';
|
| 1131 |
+
reportElement.innerHTML = `
|
| 1132 |
+
<div class="flex justify-between items-center mb-2">
|
| 1133 |
+
<p class="text-sm font-semibold">${new Date(report.date).toLocaleString('zh-TW', { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' })}</p>
|
| 1134 |
+
<span class="px-2 py-1 text-xs font-bold rounded-full ${isCompleted ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'}">
|
| 1135 |
+
${isCompleted ? '已完成' : '未完成'}
|
| 1136 |
+
</span>
|
| 1137 |
+
</div>
|
| 1138 |
+
<div class="text-sm space-y-1">
|
| 1139 |
+
<p><strong>題目:</strong> ${isCompleted ? report.total : `${report.attempted || 0}/${report.total}`} 題</p>
|
| 1140 |
+
<p><strong>答錯:</strong> <span class="font-bold text-red-500">${report.mistakes} 次</span></p>
|
| 1141 |
+
<p><strong>時間:</strong> ${minutes} 分 ${seconds} 秒</p>
|
| 1142 |
+
</div>
|
| 1143 |
+
`;
|
| 1144 |
+
targetDiv.appendChild(reportElement);
|
| 1145 |
+
});
|
| 1146 |
+
}
|
| 1147 |
+
}
|
| 1148 |
+
};
|
| 1149 |
+
|
| 1150 |
const generateQRCode = (text) => {
|
| 1151 |
qrcodeContainer.innerHTML = '';
|
| 1152 |
try {
|
|
|
|
| 1166 |
document.querySelectorAll('.mode-btn[data-mode]').forEach(btn => {
|
| 1167 |
btn.addEventListener('click', () => setupLearningView(btn.dataset.mode));
|
| 1168 |
});
|
| 1169 |
+
|
| 1170 |
+
backToMenuBtn.addEventListener('click', () => {
|
| 1171 |
+
clearInterval(timerInterval);
|
| 1172 |
+
const isQuiz = !['review', 'speed', ''].includes(currentMode);
|
| 1173 |
+
if (isQuiz && quizStartTime > 0) {
|
| 1174 |
+
const attempted = wordsForCurrentMode.length - quizQueue.length;
|
| 1175 |
+
// Only save if at least one question was attempted
|
| 1176 |
+
if (attempted > 0) {
|
| 1177 |
+
const elapsedTime = Math.round((Date.now() - quizStartTime) / 1000);
|
| 1178 |
+
const reportData = {
|
| 1179 |
+
total: wordsForCurrentMode.length,
|
| 1180 |
+
attempted: attempted,
|
| 1181 |
+
mistakes: quizIncorrectCount,
|
| 1182 |
+
time: elapsedTime,
|
| 1183 |
+
date: new Date().toISOString(),
|
| 1184 |
+
status: 'abandoned'
|
| 1185 |
+
};
|
| 1186 |
+
const history = gradeReports[currentMode] || [];
|
| 1187 |
+
history.push(reportData);
|
| 1188 |
+
gradeReports[currentMode] = history;
|
| 1189 |
+
localStorage.setItem('gradeReports', JSON.stringify(gradeReports));
|
| 1190 |
+
}
|
| 1191 |
+
}
|
| 1192 |
+
quizStartTime = 0;
|
| 1193 |
+
currentMode = '';
|
| 1194 |
+
showView('menu');
|
| 1195 |
+
});
|
| 1196 |
|
| 1197 |
flashcardContainer.addEventListener('click', () => {
|
| 1198 |
if (currentMode === 'review') {
|
|
|
|
| 1273 |
shareModal.classList.remove('hidden');
|
| 1274 |
});
|
| 1275 |
|
| 1276 |
+
gradeReportBtn.addEventListener('click', () => {
|
| 1277 |
+
renderGradeReport();
|
| 1278 |
+
gradeReportModal.classList.remove('hidden');
|
| 1279 |
+
});
|
| 1280 |
+
closeReportModalBtn.addEventListener('click', () => gradeReportModal.classList.add('hidden'));
|
| 1281 |
+
|
| 1282 |
generateLinkBtn.addEventListener('click', async () => {
|
| 1283 |
generateLinkBtn.textContent = '產生中...';
|
| 1284 |
generateLinkBtn.disabled = true;
|
|
|
|
| 1306 |
|
| 1307 |
const longUrl = url.toString();
|
| 1308 |
|
|
|
|
| 1309 |
const response = await fetch(`https://tinyurl.com/api-create.php?url=${encodeURIComponent(longUrl)}`);
|
| 1310 |
if (response.ok) {
|
| 1311 |
const shortUrl = await response.text();
|
| 1312 |
shareLinkInput.value = shortUrl;
|
| 1313 |
generateQRCode(shortUrl);
|
| 1314 |
} else {
|
|
|
|
| 1315 |
shareLinkInput.value = longUrl;
|
| 1316 |
generateQRCode(longUrl);
|
| 1317 |
copyFeedback.textContent = '縮短網址失敗,已產生原始連結。';
|
| 1318 |
}
|
|
|
|
| 1319 |
shareResultContainer.classList.remove('hidden');
|
|
|
|
| 1320 |
} catch (error) {
|
| 1321 |
console.error("產生分享連結時發生錯誤:", error);
|
| 1322 |
alert("產生分享連結時發生錯誤,可能是網路問題或單字列表過大。");
|
|
|
|
| 1329 |
|
| 1330 |
copyLinkBtn.addEventListener('click', () => {
|
| 1331 |
shareLinkInput.select();
|
| 1332 |
+
shareLinkInput.setSelectionRange(0, 99999);
|
| 1333 |
try {
|
| 1334 |
document.execCommand('copy');
|
| 1335 |
copyFeedback.textContent = '已成功複製!';
|
|
|
|
| 1353 |
|
| 1354 |
confirmClearBtn.addEventListener('click', () => {
|
| 1355 |
words = [];
|
| 1356 |
+
gradeReports = {};
|
| 1357 |
saveWordsToStorage();
|
| 1358 |
+
localStorage.removeItem('gradeReports');
|
| 1359 |
renderWordList();
|
| 1360 |
confirmClearModal.classList.add('hidden');
|
| 1361 |
});
|
|
|
|
| 1367 |
|
| 1368 |
confirmDeleteBtn.addEventListener('click', () => {
|
| 1369 |
if (indexToDelete > -1) {
|
| 1370 |
+
const itemElement = wordListContainer.querySelector(`.list-item button[data-index="${indexToDelete}"]`)?.closest('.list-item');
|
| 1371 |
+
if (itemElement) itemElement.classList.add('removing');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1372 |
|
| 1373 |
setTimeout(() => {
|
| 1374 |
if (indexToDelete > -1) {
|
|
|
|
| 1445 |
lessonTitle = localStorage.getItem('flashcardsLessonTitle') || 'L1';
|
| 1446 |
updateMainTitle();
|
| 1447 |
|
| 1448 |
+
gradeReports = JSON.parse(localStorage.getItem('gradeReports')) || {};
|
| 1449 |
completionStatus = JSON.parse(localStorage.getItem('completionStatus')) || {};
|
| 1450 |
updateCompletionUI();
|
| 1451 |
highScore = parseInt(localStorage.getItem('flashcardsHighScore') || '0', 10);
|
|
|
|
| 1481 |
};
|
| 1482 |
initializeApp();
|
| 1483 |
|
|
|
|
| 1484 |
const style = document.createElement('style');
|
| 1485 |
style.textContent = `
|
| 1486 |
.mode-btn { color: white; font-weight: bold; padding: 1rem 1.5rem; border-radius: 1.5rem; transition: all 0.3s; transform: translateY(0); display: flex; justify-content: center; align-items: center; text-align: center; height: 100%;}
|