Spaces:
Running
Running
Update index.html
Browse files- index.html +283 -46
index.html
CHANGED
|
@@ -14,6 +14,8 @@
|
|
| 14 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
|
| 15 |
<!-- 載入 PDF.js 函式庫 -->
|
| 16 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
|
|
|
|
|
|
|
| 17 |
|
| 18 |
<!-- 【新】Firebase SDK -->
|
| 19 |
<script type="module">
|
|
@@ -120,6 +122,29 @@
|
|
| 120 |
#ai-progress-bar-inner, #audio-preload-bar {
|
| 121 |
transition: width 0.3s ease-in-out;
|
| 122 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
</style>
|
| 124 |
</head>
|
| 125 |
<body class="bg-gray-50 min-h-screen flex flex-col items-center p-4 sm:p-8 font-sans">
|
|
@@ -212,6 +237,23 @@
|
|
| 212 |
</button>
|
| 213 |
</div>
|
| 214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
<!-- 標題設定區塊 -->
|
| 216 |
<div class="mb-6 p-4 bg-gray-50 rounded-2xl">
|
| 217 |
<h3 class="text-xl font-semibold mb-4 text-gray-700">標題設定</h3>
|
|
@@ -330,20 +372,43 @@
|
|
| 330 |
</div>
|
| 331 |
</div>
|
| 332 |
|
| 333 |
-
<!--
|
| 334 |
<div id="hint-section" class="w-full max-w-md mt-4 text-center hidden">
|
| 335 |
-
|
| 336 |
-
|
| 337 |
</div>
|
| 338 |
|
| 339 |
-
<!--
|
| 340 |
<div id="quiz-container" class="w-full max-w-md mt-4">
|
| 341 |
-
<
|
| 342 |
-
<input id="answer-input"
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
<div id="feedback-display" class="text-center mt-3 h-6 font-semibold"></div>
|
| 348 |
</div>
|
| 349 |
|
|
@@ -561,6 +626,10 @@
|
|
| 561 |
// The main logic is in the script tag below
|
| 562 |
</script>
|
| 563 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 564 |
document.addEventListener('DOMContentLoaded', () => {
|
| 565 |
// DOM 元素
|
| 566 |
const mainMenu = document.getElementById('main-menu');
|
|
@@ -601,6 +670,10 @@
|
|
| 601 |
const parseStatus = document.getElementById('parse-status');
|
| 602 |
const aiProgressContainer = document.getElementById('ai-progress-container');
|
| 603 |
const aiProgressBarInner = document.getElementById('ai-progress-bar-inner');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 604 |
|
| 605 |
// 學習介面
|
| 606 |
const flashcardContainer = document.getElementById('flashcard-container');
|
|
@@ -622,15 +695,16 @@
|
|
| 622 |
|
| 623 |
// 測驗相關
|
| 624 |
const quizContainer = document.getElementById('quiz-container');
|
| 625 |
-
|
| 626 |
const answerInput = document.getElementById('answer-input');
|
| 627 |
-
const
|
| 628 |
const feedbackDisplay = document.getElementById('feedback-display');
|
| 629 |
const wrongAnswerFeedback = document.getElementById('wrong-answer-feedback');
|
| 630 |
const confirmWrongBtn = document.getElementById('confirm-wrong-btn');
|
| 631 |
-
const hintSection = document.getElementById('hint-section');
|
| 632 |
const hintBtn = document.getElementById('hint-btn');
|
| 633 |
-
const hintDisplay = document.getElementById('hint-display');
|
|
|
|
| 634 |
const quizCompletionMessage = document.getElementById('quiz-completion-message');
|
| 635 |
|
| 636 |
// 複習模式導覽
|
|
@@ -728,6 +802,135 @@
|
|
| 728 |
'sentence-cloze': { title: '情境克漏字' },
|
| 729 |
};
|
| 730 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 731 |
// --- 視圖管理 ---
|
| 732 |
const showView = (view) => {
|
| 733 |
mainMenu.classList.toggle('hidden', view !== 'menu');
|
|
@@ -909,7 +1112,7 @@
|
|
| 909 |
feedbackDisplay.textContent = '';
|
| 910 |
answerInput.value = '';
|
| 911 |
answerInput.disabled = false;
|
| 912 |
-
|
| 913 |
wrongAnswerFeedback.classList.add('hidden');
|
| 914 |
quizContainer.classList.remove('hidden');
|
| 915 |
speedResultView.classList.add('hidden');
|
|
@@ -917,6 +1120,9 @@
|
|
| 917 |
hintBtn.disabled = false;
|
| 918 |
frontDisplay.classList.add('hidden');
|
| 919 |
clozeQuestionContainer.classList.add('hidden');
|
|
|
|
|
|
|
|
|
|
| 920 |
|
| 921 |
let frontText, backText;
|
| 922 |
|
|
@@ -926,7 +1132,7 @@
|
|
| 926 |
case 'zh-en':
|
| 927 |
frontText = card.chinese;
|
| 928 |
backText = card.english;
|
| 929 |
-
answerInput.placeholder = "請輸入英文答案...";
|
| 930 |
break;
|
| 931 |
case 'en-zh':
|
| 932 |
frontText = card.english;
|
|
@@ -939,7 +1145,7 @@
|
|
| 939 |
backText = card.english;
|
| 940 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 941 |
speakWord(card);
|
| 942 |
-
answerInput.placeholder = "請輸入英文答案...";
|
| 943 |
break;
|
| 944 |
}
|
| 945 |
} else {
|
|
@@ -955,7 +1161,7 @@
|
|
| 955 |
break;
|
| 956 |
case 'zh-en': case 'hard':
|
| 957 |
frontText = card.chinese; backText = card.english;
|
| 958 |
-
answerInput.placeholder = "請輸入英文答案...";
|
| 959 |
break;
|
| 960 |
case 'en-zh':
|
| 961 |
frontText = card.english; backText = card.chinese;
|
|
@@ -966,7 +1172,7 @@
|
|
| 966 |
frontText = '請聽發音'; backText = card.english;
|
| 967 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 968 |
speakWord(card);
|
| 969 |
-
answerInput.placeholder = "請輸入英文答案...";
|
| 970 |
break;
|
| 971 |
case 'sentence-cloze':
|
| 972 |
clozeQuestionContainer.classList.remove('hidden');
|
|
@@ -975,7 +1181,7 @@
|
|
| 975 |
sentenceZhDisplay.classList.add('hidden');
|
| 976 |
toggleTranslationBtn.textContent = '顯示翻譯';
|
| 977 |
backText = card.sentence.answer || card.english; // 【修改】背面顯示正確答案
|
| 978 |
-
answerInput.placeholder = "請填入空格中的單字...";
|
| 979 |
break;
|
| 980 |
}
|
| 981 |
}
|
|
@@ -984,7 +1190,11 @@
|
|
| 984 |
frontDisplay.textContent = frontText;
|
| 985 |
}
|
| 986 |
backDisplay.textContent = backText;
|
| 987 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 988 |
};
|
| 989 |
|
| 990 |
if (flashcardContainer.classList.contains('flipped')) {
|
|
@@ -1136,12 +1346,16 @@
|
|
| 1136 |
if (isCorrect) {
|
| 1137 |
correctSound.play();
|
| 1138 |
answerInput.disabled = true;
|
| 1139 |
-
|
| 1140 |
feedbackDisplay.textContent = '答對了!';
|
| 1141 |
feedbackDisplay.classList.remove('text-red-500');
|
| 1142 |
feedbackDisplay.classList.add('text-green-600');
|
| 1143 |
triggerConfetti();
|
| 1144 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1145 |
if (!flashcardContainer.classList.contains('flipped')) {
|
| 1146 |
flashcardContainer.classList.add('flipped');
|
| 1147 |
}
|
|
@@ -1182,7 +1396,10 @@
|
|
| 1182 |
updateCompletionUI();
|
| 1183 |
}
|
| 1184 |
|
| 1185 |
-
[flashcardContainer, quizContainer, progressBarContainer
|
|
|
|
|
|
|
|
|
|
| 1186 |
quizCompletionMessage.classList.remove('hidden');
|
| 1187 |
|
| 1188 |
setTimeout(() => showView('menu'), 3000);
|
|
@@ -1190,7 +1407,7 @@
|
|
| 1190 |
}, 1500);
|
| 1191 |
} else { // Review mode
|
| 1192 |
answerInput.disabled = false;
|
| 1193 |
-
|
| 1194 |
}
|
| 1195 |
} else { // Incorrect answer
|
| 1196 |
wrongSound.play();
|
|
@@ -1216,7 +1433,7 @@
|
|
| 1216 |
setTimeout(() => { answerInput.classList.remove('shake'); }, 820);
|
| 1217 |
} else { // Other quiz modes
|
| 1218 |
answerInput.disabled = true;
|
| 1219 |
-
|
| 1220 |
flashcardContainer.classList.add('flipped');
|
| 1221 |
quizContainer.classList.add('hidden');
|
| 1222 |
wrongAnswerFeedback.classList.remove('hidden');
|
|
@@ -1229,13 +1446,19 @@
|
|
| 1229 |
|
| 1230 |
const updateHintButtonVisibility = () => {
|
| 1231 |
const isQuiz = !['review', 'speed'].includes(currentMode);
|
| 1232 |
-
|
|
|
|
|
|
|
| 1233 |
};
|
| 1234 |
|
| 1235 |
confirmWrongBtn.addEventListener('click', () => {
|
| 1236 |
const wrongCard = quizQueue.shift();
|
| 1237 |
const reinsertIndex = Math.min(quizQueue.length, 3);
|
| 1238 |
quizQueue.splice(reinsertIndex, 0, wrongCard);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1239 |
displayCard();
|
| 1240 |
});
|
| 1241 |
|
|
@@ -1272,7 +1495,7 @@
|
|
| 1272 |
|
| 1273 |
const endSpeedChallenge = () => {
|
| 1274 |
clearInterval(timerInterval);
|
| 1275 |
-
answerInput.disabled = true;
|
| 1276 |
quizContainer.classList.add('hidden'); speedResultView.classList.remove('hidden');
|
| 1277 |
finalScore.textContent = currentScore;
|
| 1278 |
|
|
@@ -1727,7 +1950,17 @@
|
|
| 1727 |
|
| 1728 |
prevBtn.addEventListener('click', () => { if (currentCardIndex > 0) { currentCardIndex--; displayCard(); }});
|
| 1729 |
nextBtn.addEventListener('click', () => { if (currentCardIndex < wordsForCurrentMode.length - 1) { currentCardIndex++; displayCard(); }});
|
| 1730 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1731 |
|
| 1732 |
speakBtn.addEventListener('click', (e) => {
|
| 1733 |
e.stopPropagation();
|
|
@@ -1740,29 +1973,33 @@
|
|
| 1740 |
if(word) speakWithBrowserTTS(word.english, 0.2);
|
| 1741 |
});
|
| 1742 |
|
| 1743 |
-
hintBtn
|
| 1744 |
-
|
| 1745 |
-
|
| 1746 |
-
|
| 1747 |
-
|
| 1748 |
-
|
| 1749 |
-
|
| 1750 |
-
|
| 1751 |
-
|
| 1752 |
-
|
| 1753 |
-
|
| 1754 |
-
|
| 1755 |
-
|
| 1756 |
-
|
| 1757 |
-
|
| 1758 |
-
|
| 1759 |
-
|
|
|
|
|
|
|
| 1760 |
|
| 1761 |
passwordForm.addEventListener('submit', (e) => {
|
| 1762 |
e.preventDefault();
|
| 1763 |
if (passwordInput.value === MANAGE_PASSWORD) {
|
| 1764 |
passwordModal.classList.add('hidden');
|
| 1765 |
renderWordList();
|
|
|
|
|
|
|
| 1766 |
showView('manage');
|
| 1767 |
} else {
|
| 1768 |
passwordError.textContent = '密碼錯誤!';
|
|
@@ -2130,4 +2367,4 @@
|
|
| 2130 |
});
|
| 2131 |
</script>
|
| 2132 |
</body>
|
| 2133 |
-
</html>
|
|
|
|
| 14 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
|
| 15 |
<!-- 載入 PDF.js 函式庫 -->
|
| 16 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
|
| 17 |
+
<!-- [第一步] 引入手寫辨識函式庫 -->
|
| 18 |
+
<script src="https://cdn.jsdelivr.net/gh/ChenYuHo/handwriting.js/handwriting.js"></script>
|
| 19 |
|
| 20 |
<!-- 【新】Firebase SDK -->
|
| 21 |
<script type="module">
|
|
|
|
| 122 |
#ai-progress-bar-inner, #audio-preload-bar {
|
| 123 |
transition: width 0.3s ease-in-out;
|
| 124 |
}
|
| 125 |
+
|
| 126 |
+
/* [第二步] 新增手寫板樣式 */
|
| 127 |
+
#handwriting-container {
|
| 128 |
+
transition: all 0.3s ease;
|
| 129 |
+
}
|
| 130 |
+
#handwriting-canvas {
|
| 131 |
+
width: 100%;
|
| 132 |
+
max-width: 100%; /* 確保不超出容器 */
|
| 133 |
+
height: 250px; /* 手寫區域高度 */
|
| 134 |
+
background-color: #fff;
|
| 135 |
+
border: 2px dashed #cbd5e1; /* 虛線邊框更有「畫布」的感覺 */
|
| 136 |
+
border-radius: 0.75rem;
|
| 137 |
+
cursor: crosshair;
|
| 138 |
+
touch-action: none; /* [重要] 防止手指寫字時觸發網頁捲動 */
|
| 139 |
+
}
|
| 140 |
+
/* 讓答案框在唯讀模式下看起來像「顯示區」 */
|
| 141 |
+
input[readonly].handwriting-mode {
|
| 142 |
+
background-color: #f3f4f6;
|
| 143 |
+
color: #4f46e5;
|
| 144 |
+
font-weight: bold;
|
| 145 |
+
border-color: #818cf8;
|
| 146 |
+
cursor: not-allowed;
|
| 147 |
+
}
|
| 148 |
</style>
|
| 149 |
</head>
|
| 150 |
<body class="bg-gray-50 min-h-screen flex flex-col items-center p-4 sm:p-8 font-sans">
|
|
|
|
| 237 |
</button>
|
| 238 |
</div>
|
| 239 |
|
| 240 |
+
<!-- [新增] 版本鎖定設定區塊 -->
|
| 241 |
+
<div class="mb-6 p-4 bg-yellow-50 rounded-2xl border-2 border-yellow-100">
|
| 242 |
+
<h3 class="text-xl font-semibold mb-4 text-yellow-800">🔒 版本鎖定設定</h3>
|
| 243 |
+
<p class="text-sm text-yellow-700 mb-3">選擇學生進行測驗時的輸入方式:</p>
|
| 244 |
+
<div class="flex items-center gap-6">
|
| 245 |
+
<label class="flex items-center cursor-pointer">
|
| 246 |
+
<input type="radio" name="version-lock" value="pc" class="h-5 w-5 text-yellow-600 focus:ring-yellow-500">
|
| 247 |
+
<span class="ml-2 text-gray-800 font-medium">鎖定 PC 版 (鍵盤打字)</span>
|
| 248 |
+
</label>
|
| 249 |
+
<label class="flex items-center cursor-pointer">
|
| 250 |
+
<input type="radio" name="version-lock" value="mobile" class="h-5 w-5 text-yellow-600 focus:ring-yellow-500">
|
| 251 |
+
<span class="ml-2 text-gray-800 font-medium">鎖定 行動版 (手寫輸入)</span>
|
| 252 |
+
</label>
|
| 253 |
+
<button id="save-version-btn" class="ml-auto bg-yellow-600 text-white font-bold py-2 px-6 rounded-full hover:bg-yellow-700 transition-colors">儲存設定</button>
|
| 254 |
+
</div>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
<!-- 標題設定區塊 -->
|
| 258 |
<div class="mb-6 p-4 bg-gray-50 rounded-2xl">
|
| 259 |
<h3 class="text-xl font-semibold mb-4 text-gray-700">標題設定</h3>
|
|
|
|
| 372 |
</div>
|
| 373 |
</div>
|
| 374 |
|
| 375 |
+
<!-- 舊的提示功能區塊 (保留但預設隱藏,新版已整合到下方) -->
|
| 376 |
<div id="hint-section" class="w-full max-w-md mt-4 text-center hidden">
|
| 377 |
+
<!-- 已整合到下方 -->
|
| 378 |
+
<p id="hint-display" class="mt-2 text-gray-600 font-semibold h-6"></p>
|
| 379 |
</div>
|
| 380 |
|
| 381 |
+
<!-- [第三步] 插入手寫板介面 (取代舊的 input form) -->
|
| 382 |
<div id="quiz-container" class="w-full max-w-md mt-4">
|
| 383 |
+
<div class="flex flex-col gap-4">
|
| 384 |
+
<input id="answer-input" placeholder="輸入你的答案 (按 Enter 提交)" class="w-full p-4 border-2 border-gray-300 rounded-xl text-2xl focus:border-indigo-500 focus:ring-indigo-500 transition-colors" autocomplete="off">
|
| 385 |
+
|
| 386 |
+
<div id="handwriting-container" class="hidden flex-col gap-2">
|
| 387 |
+
<div class="relative w-full">
|
| 388 |
+
<canvas id="handwriting-canvas" width="500" height="250"></canvas>
|
| 389 |
+
<p class="absolute bottom-2 right-3 text-xs text-gray-400 pointer-events-none">請在此區域手寫</p>
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
<div class="flex gap-2 w-full">
|
| 393 |
+
<button id="hw-clear-btn" class="flex-1 py-3 bg-gray-200 text-gray-700 rounded-xl font-semibold hover:bg-gray-300 transition-colors flex items-center justify-center gap-1">
|
| 394 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm4 0a1 1 0 012 0v6a1 1 0 11-2 0V8z" clip-rule="evenodd" /></svg>
|
| 395 |
+
重寫
|
| 396 |
+
</button>
|
| 397 |
+
<button id="hw-undo-btn" class="flex-1 py-3 bg-yellow-100 text-yellow-700 rounded-xl font-semibold hover:bg-yellow-200 transition-colors flex items-center justify-center gap-1">
|
| 398 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" /></svg>
|
| 399 |
+
復原
|
| 400 |
+
</button>
|
| 401 |
+
<button id="hw-recognize-btn" class="flex-[2] py-3 bg-indigo-600 text-white rounded-xl font-bold hover:bg-indigo-700 shadow-md transition-colors flex items-center justify-center gap-1">
|
| 402 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" /></svg>
|
| 403 |
+
辨識輸入
|
| 404 |
+
</button>
|
| 405 |
+
</div>
|
| 406 |
+
</div>
|
| 407 |
+
|
| 408 |
+
<button id="submit-answer-btn" class="w-full bg-indigo-500 text-white p-4 rounded-xl text-xl font-bold hover:bg-indigo-600 transition-colors">提交答案</button>
|
| 409 |
+
|
| 410 |
+
<button id="hint-btn" class="w-full bg-yellow-500 text-white p-3 rounded-xl text-lg font-bold hover:bg-yellow-600 transition-colors hidden">顯示提示</button>
|
| 411 |
+
</div>
|
| 412 |
<div id="feedback-display" class="text-center mt-3 h-6 font-semibold"></div>
|
| 413 |
</div>
|
| 414 |
|
|
|
|
| 626 |
// The main logic is in the script tag below
|
| 627 |
</script>
|
| 628 |
<script>
|
| 629 |
+
// [第四步] JavaScript 邏輯 (Script)
|
| 630 |
+
let effectiveVersion = 'pc'; // 預設為 PC 版
|
| 631 |
+
let handwritingCanvasObj = null; // [新增] 存放手寫板實例
|
| 632 |
+
|
| 633 |
document.addEventListener('DOMContentLoaded', () => {
|
| 634 |
// DOM 元素
|
| 635 |
const mainMenu = document.getElementById('main-menu');
|
|
|
|
| 670 |
const parseStatus = document.getElementById('parse-status');
|
| 671 |
const aiProgressContainer = document.getElementById('ai-progress-container');
|
| 672 |
const aiProgressBarInner = document.getElementById('ai-progress-bar-inner');
|
| 673 |
+
|
| 674 |
+
// [新增] 版本控制按鈕
|
| 675 |
+
const saveVersionBtn = document.getElementById('save-version-btn');
|
| 676 |
+
const versionRadios = document.getElementsByName('version-lock');
|
| 677 |
|
| 678 |
// 學習介面
|
| 679 |
const flashcardContainer = document.getElementById('flashcard-container');
|
|
|
|
| 695 |
|
| 696 |
// 測驗相關
|
| 697 |
const quizContainer = document.getElementById('quiz-container');
|
| 698 |
+
// 注意:原本的 quizForm 已經被移除,改用直接監聽按鈕
|
| 699 |
const answerInput = document.getElementById('answer-input');
|
| 700 |
+
const submitAnswerBtn = document.getElementById('submit-answer-btn'); // [新增]
|
| 701 |
const feedbackDisplay = document.getElementById('feedback-display');
|
| 702 |
const wrongAnswerFeedback = document.getElementById('wrong-answer-feedback');
|
| 703 |
const confirmWrongBtn = document.getElementById('confirm-wrong-btn');
|
| 704 |
+
// const hintSection = document.getElementById('hint-section'); // 舊的提示區塊,不再使用
|
| 705 |
const hintBtn = document.getElementById('hint-btn');
|
| 706 |
+
const hintDisplay = document.getElementById('hint-display'); // 這裡暫時共用舊的 display 元素或需修改
|
| 707 |
+
// 為了配合新版���面,建議將提示顯示區域也移動到新區塊內,但為了最小修改,我們這裡用 JS 控制
|
| 708 |
const quizCompletionMessage = document.getElementById('quiz-completion-message');
|
| 709 |
|
| 710 |
// 複習模式導覽
|
|
|
|
| 802 |
'sentence-cloze': { title: '情境克漏字' },
|
| 803 |
};
|
| 804 |
|
| 805 |
+
// [新增] 初始化手寫板功能
|
| 806 |
+
function initHandwritingBoard() {
|
| 807 |
+
if (handwritingCanvasObj) return; // 避免重複初始化
|
| 808 |
+
|
| 809 |
+
// 建立 handwriting.js 實例 (參數: canvas元素, 筆畫粗細)
|
| 810 |
+
const canvasEl = document.getElementById('handwriting-canvas');
|
| 811 |
+
// 確保 canvas 寬度正確 (解決有些手機上寬度不對的問題)
|
| 812 |
+
canvasEl.width = canvasEl.parentElement.clientWidth || 300;
|
| 813 |
+
|
| 814 |
+
handwritingCanvasObj = new handwriting.Canvas(canvasEl, 3);
|
| 815 |
+
|
| 816 |
+
// 設定辨識後的回呼函式
|
| 817 |
+
handwritingCanvasObj.setCallBack(function(data, err) {
|
| 818 |
+
if (err) {
|
| 819 |
+
console.error(err);
|
| 820 |
+
alert("辨識失敗,請檢查網路連線"); // handwriting.js 需要連網
|
| 821 |
+
return;
|
| 822 |
+
}
|
| 823 |
+
// data 是候選字陣列,取第一個
|
| 824 |
+
if (data && data.length > 0) {
|
| 825 |
+
const result = data[0];
|
| 826 |
+
answerInput.value = result; // 填入答案框
|
| 827 |
+
|
| 828 |
+
// 視覺回饋
|
| 829 |
+
answerInput.classList.add('border-green-500', 'bg-green-50');
|
| 830 |
+
setTimeout(() => answerInput.classList.remove('border-green-500', 'bg-green-50'), 500);
|
| 831 |
+
}
|
| 832 |
+
});
|
| 833 |
+
|
| 834 |
+
// 設定筆畫樣式
|
| 835 |
+
handwritingCanvasObj.setLineWidth(5);
|
| 836 |
+
handwritingCanvasObj.setPenColor("#333");
|
| 837 |
+
|
| 838 |
+
// 讓 undo/redo 功能生效
|
| 839 |
+
handwritingCanvasObj.setOptions({
|
| 840 |
+
language: "en",
|
| 841 |
+
numOfReturn: 1
|
| 842 |
+
});
|
| 843 |
+
|
| 844 |
+
// 綁定按鈕事件
|
| 845 |
+
document.getElementById('hw-clear-btn').addEventListener('click', () => {
|
| 846 |
+
handwritingCanvasObj.erase();
|
| 847 |
+
answerInput.value = '';
|
| 848 |
+
answerInput.focus(); // 保持焦點
|
| 849 |
+
});
|
| 850 |
+
|
| 851 |
+
document.getElementById('hw-undo-btn').addEventListener('click', () => {
|
| 852 |
+
handwritingCanvasObj.undo();
|
| 853 |
+
});
|
| 854 |
+
|
| 855 |
+
document.getElementById('hw-recognize-btn').addEventListener('click', () => {
|
| 856 |
+
handwritingCanvasObj.recognize();
|
| 857 |
+
});
|
| 858 |
+
}
|
| 859 |
+
|
| 860 |
+
// [新增] 應用版本設定 (控制手寫板顯示)
|
| 861 |
+
function applyVersionSettings() {
|
| 862 |
+
const hwContainer = document.getElementById('handwriting-container');
|
| 863 |
+
const versionLockSetting = localStorage.getItem('flashcardsVersionLock');
|
| 864 |
+
|
| 865 |
+
// 優先使用儲存的設定,否則預設為 pc
|
| 866 |
+
if (versionLockSetting) {
|
| 867 |
+
effectiveVersion = versionLockSetting;
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
// 更新管理介面的 Radio 狀態
|
| 871 |
+
for(let radio of versionRadios) {
|
| 872 |
+
if(radio.value === effectiveVersion) {
|
| 873 |
+
radio.checked = true;
|
| 874 |
+
}
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
// 如果手寫容器還沒生成(避免報錯),先跳過
|
| 878 |
+
if (!hwContainer) return;
|
| 879 |
+
|
| 880 |
+
if (effectiveVersion === 'mobile') {
|
| 881 |
+
// --- 行動版模式 (手寫) ---
|
| 882 |
+
console.log("切換為手寫模式");
|
| 883 |
+
|
| 884 |
+
// 1. 顯示手寫板
|
| 885 |
+
hwContainer.classList.remove('hidden');
|
| 886 |
+
hwContainer.classList.add('flex');
|
| 887 |
+
|
| 888 |
+
// 2. 初始化手寫板 (如果還沒建立,且元素可見時才初始化以取得正確寬度)
|
| 889 |
+
// 使用 setTimeout 確保 display:flex 生效後才抓得到寬度
|
| 890 |
+
setTimeout(() => initHandwritingBoard(), 100);
|
| 891 |
+
|
| 892 |
+
// 3. 鎖定輸入框
|
| 893 |
+
answerInput.readOnly = true;
|
| 894 |
+
answerInput.placeholder = "請在下方手寫板書寫...";
|
| 895 |
+
answerInput.classList.add('handwriting-mode'); // 套用我們剛寫的 CSS
|
| 896 |
+
|
| 897 |
+
// 4. 移除之前可能加上的原生輸入法設定
|
| 898 |
+
if (window.google && window.google.ime) {
|
| 899 |
+
window.google.ime.setOptions({ ime: 'none', trigger: null });
|
| 900 |
+
}
|
| 901 |
+
|
| 902 |
+
} else {
|
| 903 |
+
// --- PC 版模式 (打字) ---
|
| 904 |
+
console.log("切換為打字模式");
|
| 905 |
+
|
| 906 |
+
// 1. 隱藏手寫板
|
| 907 |
+
hwContainer.classList.add('hidden');
|
| 908 |
+
hwContainer.classList.remove('flex');
|
| 909 |
+
|
| 910 |
+
// 2. 恢復輸入框
|
| 911 |
+
answerInput.readOnly = false;
|
| 912 |
+
answerInput.placeholder = "輸入你的答案 (按 Enter 提交)";
|
| 913 |
+
answerInput.classList.remove('handwriting-mode');
|
| 914 |
+
}
|
| 915 |
+
}
|
| 916 |
+
|
| 917 |
+
// [新增] 儲存版本設定
|
| 918 |
+
saveVersionBtn.addEventListener('click', () => {
|
| 919 |
+
let selectedValue = 'pc';
|
| 920 |
+
for(let radio of versionRadios) {
|
| 921 |
+
if(radio.checked) selectedValue = radio.value;
|
| 922 |
+
}
|
| 923 |
+
localStorage.setItem('flashcardsVersionLock', selectedValue);
|
| 924 |
+
effectiveVersion = selectedValue;
|
| 925 |
+
applyVersionSettings();
|
| 926 |
+
|
| 927 |
+
const originalText = saveVersionBtn.textContent;
|
| 928 |
+
saveVersionBtn.textContent = '已儲存!';
|
| 929 |
+
setTimeout(() => {
|
| 930 |
+
saveVersionBtn.textContent = originalText;
|
| 931 |
+
}, 2000);
|
| 932 |
+
});
|
| 933 |
+
|
| 934 |
// --- 視圖管理 ---
|
| 935 |
const showView = (view) => {
|
| 936 |
mainMenu.classList.toggle('hidden', view !== 'menu');
|
|
|
|
| 1112 |
feedbackDisplay.textContent = '';
|
| 1113 |
answerInput.value = '';
|
| 1114 |
answerInput.disabled = false;
|
| 1115 |
+
submitAnswerBtn.disabled = false; // [修改]
|
| 1116 |
wrongAnswerFeedback.classList.add('hidden');
|
| 1117 |
quizContainer.classList.remove('hidden');
|
| 1118 |
speedResultView.classList.add('hidden');
|
|
|
|
| 1120 |
hintBtn.disabled = false;
|
| 1121 |
frontDisplay.classList.add('hidden');
|
| 1122 |
clozeQuestionContainer.classList.add('hidden');
|
| 1123 |
+
|
| 1124 |
+
// [新增] 每次顯示卡片時,呼叫設定確保介面正確
|
| 1125 |
+
applyVersionSettings();
|
| 1126 |
|
| 1127 |
let frontText, backText;
|
| 1128 |
|
|
|
|
| 1132 |
case 'zh-en':
|
| 1133 |
frontText = card.chinese;
|
| 1134 |
backText = card.english;
|
| 1135 |
+
answerInput.placeholder = effectiveVersion === 'mobile' ? "請在下方手寫板書寫..." : "請輸入英文答案...";
|
| 1136 |
break;
|
| 1137 |
case 'en-zh':
|
| 1138 |
frontText = card.english;
|
|
|
|
| 1145 |
backText = card.english;
|
| 1146 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1147 |
speakWord(card);
|
| 1148 |
+
answerInput.placeholder = effectiveVersion === 'mobile' ? "請在下方手寫板書寫..." : "請輸入英文答案...";
|
| 1149 |
break;
|
| 1150 |
}
|
| 1151 |
} else {
|
|
|
|
| 1161 |
break;
|
| 1162 |
case 'zh-en': case 'hard':
|
| 1163 |
frontText = card.chinese; backText = card.english;
|
| 1164 |
+
answerInput.placeholder = effectiveVersion === 'mobile' ? "請在下方手寫板書寫..." : "請輸入英文答案...";
|
| 1165 |
break;
|
| 1166 |
case 'en-zh':
|
| 1167 |
frontText = card.english; backText = card.chinese;
|
|
|
|
| 1172 |
frontText = '請聽發音'; backText = card.english;
|
| 1173 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1174 |
speakWord(card);
|
| 1175 |
+
answerInput.placeholder = effectiveVersion === 'mobile' ? "請在下方手寫板書寫..." : "請輸入英文答案...";
|
| 1176 |
break;
|
| 1177 |
case 'sentence-cloze':
|
| 1178 |
clozeQuestionContainer.classList.remove('hidden');
|
|
|
|
| 1181 |
sentenceZhDisplay.classList.add('hidden');
|
| 1182 |
toggleTranslationBtn.textContent = '顯示翻譯';
|
| 1183 |
backText = card.sentence.answer || card.english; // 【修改】背面顯示正確答案
|
| 1184 |
+
answerInput.placeholder = effectiveVersion === 'mobile' ? "請填入空格中的單字..." : "請填入空格中的單字...";
|
| 1185 |
break;
|
| 1186 |
}
|
| 1187 |
}
|
|
|
|
| 1190 |
frontDisplay.textContent = frontText;
|
| 1191 |
}
|
| 1192 |
backDisplay.textContent = backText;
|
| 1193 |
+
|
| 1194 |
+
// 如果不是唯讀(手寫模式),才聚焦
|
| 1195 |
+
if(!answerInput.readOnly) {
|
| 1196 |
+
setTimeout(() => answerInput.focus(), 100);
|
| 1197 |
+
}
|
| 1198 |
};
|
| 1199 |
|
| 1200 |
if (flashcardContainer.classList.contains('flipped')) {
|
|
|
|
| 1346 |
if (isCorrect) {
|
| 1347 |
correctSound.play();
|
| 1348 |
answerInput.disabled = true;
|
| 1349 |
+
submitAnswerBtn.disabled = true; // [修改]
|
| 1350 |
feedbackDisplay.textContent = '答對了!';
|
| 1351 |
feedbackDisplay.classList.remove('text-red-500');
|
| 1352 |
feedbackDisplay.classList.add('text-green-600');
|
| 1353 |
triggerConfetti();
|
| 1354 |
|
| 1355 |
+
if(handwritingCanvasObj) {
|
| 1356 |
+
handwritingCanvasObj.erase(); // 答對時清空手寫板
|
| 1357 |
+
}
|
| 1358 |
+
|
| 1359 |
if (!flashcardContainer.classList.contains('flipped')) {
|
| 1360 |
flashcardContainer.classList.add('flipped');
|
| 1361 |
}
|
|
|
|
| 1396 |
updateCompletionUI();
|
| 1397 |
}
|
| 1398 |
|
| 1399 |
+
[flashcardContainer, quizContainer, progressBarContainer].forEach(el => el.classList.add('hidden'));
|
| 1400 |
+
// 提示功能已經移入 quizContainer,所以不需要單獨隱藏
|
| 1401 |
+
// if (hintSection) hintSection.classList.add('hidden');
|
| 1402 |
+
|
| 1403 |
quizCompletionMessage.classList.remove('hidden');
|
| 1404 |
|
| 1405 |
setTimeout(() => showView('menu'), 3000);
|
|
|
|
| 1407 |
}, 1500);
|
| 1408 |
} else { // Review mode
|
| 1409 |
answerInput.disabled = false;
|
| 1410 |
+
submitAnswerBtn.disabled = false; // [修改]
|
| 1411 |
}
|
| 1412 |
} else { // Incorrect answer
|
| 1413 |
wrongSound.play();
|
|
|
|
| 1433 |
setTimeout(() => { answerInput.classList.remove('shake'); }, 820);
|
| 1434 |
} else { // Other quiz modes
|
| 1435 |
answerInput.disabled = true;
|
| 1436 |
+
submitAnswerBtn.disabled = true; // [修改]
|
| 1437 |
flashcardContainer.classList.add('flipped');
|
| 1438 |
quizContainer.classList.add('hidden');
|
| 1439 |
wrongAnswerFeedback.classList.remove('hidden');
|
|
|
|
| 1446 |
|
| 1447 |
const updateHintButtonVisibility = () => {
|
| 1448 |
const isQuiz = !['review', 'speed'].includes(currentMode);
|
| 1449 |
+
if (hintBtn) {
|
| 1450 |
+
hintBtn.classList.toggle('hidden', !(isQuiz && quizIncorrectCount >= 2));
|
| 1451 |
+
}
|
| 1452 |
};
|
| 1453 |
|
| 1454 |
confirmWrongBtn.addEventListener('click', () => {
|
| 1455 |
const wrongCard = quizQueue.shift();
|
| 1456 |
const reinsertIndex = Math.min(quizQueue.length, 3);
|
| 1457 |
quizQueue.splice(reinsertIndex, 0, wrongCard);
|
| 1458 |
+
// 答錯後繼續,需清空手寫板
|
| 1459 |
+
if(handwritingCanvasObj) {
|
| 1460 |
+
handwritingCanvasObj.erase();
|
| 1461 |
+
}
|
| 1462 |
displayCard();
|
| 1463 |
});
|
| 1464 |
|
|
|
|
| 1495 |
|
| 1496 |
const endSpeedChallenge = () => {
|
| 1497 |
clearInterval(timerInterval);
|
| 1498 |
+
answerInput.disabled = true; submitAnswerBtn.disabled = true;
|
| 1499 |
quizContainer.classList.add('hidden'); speedResultView.classList.remove('hidden');
|
| 1500 |
finalScore.textContent = currentScore;
|
| 1501 |
|
|
|
|
| 1950 |
|
| 1951 |
prevBtn.addEventListener('click', () => { if (currentCardIndex > 0) { currentCardIndex--; displayCard(); }});
|
| 1952 |
nextBtn.addEventListener('click', () => { if (currentCardIndex < wordsForCurrentMode.length - 1) { currentCardIndex++; displayCard(); }});
|
| 1953 |
+
|
| 1954 |
+
// [重要修改] 表單移除後,改用按鈕監聽
|
| 1955 |
+
// quizForm.addEventListener('submit', (e) => { e.preventDefault(); checkAnswer(); });
|
| 1956 |
+
submitAnswerBtn.addEventListener('click', checkAnswer);
|
| 1957 |
+
// 允許在 PC 模式下按 Enter 送出
|
| 1958 |
+
answerInput.addEventListener('keydown', (e) => {
|
| 1959 |
+
if(e.key === 'Enter') {
|
| 1960 |
+
e.preventDefault();
|
| 1961 |
+
if(!submitAnswerBtn.disabled) checkAnswer();
|
| 1962 |
+
}
|
| 1963 |
+
});
|
| 1964 |
|
| 1965 |
speakBtn.addEventListener('click', (e) => {
|
| 1966 |
e.stopPropagation();
|
|
|
|
| 1973 |
if(word) speakWithBrowserTTS(word.english, 0.2);
|
| 1974 |
});
|
| 1975 |
|
| 1976 |
+
if(hintBtn) {
|
| 1977 |
+
hintBtn.addEventListener('click', () => {
|
| 1978 |
+
const card = currentMode === 'speed' ? currentSpeedCard : quizQueue[0];
|
| 1979 |
+
if (!card) return;
|
| 1980 |
+
const questionType = currentMode === 'speed' ? currentSpeedQuestionType : (currentMode === 'hard' ? 'zh-en' : currentMode);
|
| 1981 |
+
let correctAnswer;
|
| 1982 |
+
// 【修改】提示功能現在也使用正確的答案來源
|
| 1983 |
+
switch (questionType) {
|
| 1984 |
+
case 'sentence-cloze': correctAnswer = card.sentence.answer; break;
|
| 1985 |
+
case 'zh-en': case 'listen': correctAnswer = card.english.replace(/[\((\[【].*?[\))\]】]/g, "").trim(); break;
|
| 1986 |
+
case 'en-zh': correctAnswer = card.chinese.replace(/[\((\[【].*?[\))\]】]/g, "").trim(); break;
|
| 1987 |
+
default: return;
|
| 1988 |
+
}
|
| 1989 |
+
if (correctAnswer) {
|
| 1990 |
+
hintDisplay.textContent = `提示:答案以 '${correctAnswer[0]}' 開頭。`;
|
| 1991 |
+
hintBtn.disabled = true;
|
| 1992 |
+
}
|
| 1993 |
+
});
|
| 1994 |
+
}
|
| 1995 |
|
| 1996 |
passwordForm.addEventListener('submit', (e) => {
|
| 1997 |
e.preventDefault();
|
| 1998 |
if (passwordInput.value === MANAGE_PASSWORD) {
|
| 1999 |
passwordModal.classList.add('hidden');
|
| 2000 |
renderWordList();
|
| 2001 |
+
// 在進入管理畫面時也更新一下設定狀態
|
| 2002 |
+
applyVersionSettings();
|
| 2003 |
showView('manage');
|
| 2004 |
} else {
|
| 2005 |
passwordError.textContent = '密碼錯誤!';
|
|
|
|
| 2367 |
});
|
| 2368 |
</script>
|
| 2369 |
</body>
|
| 2370 |
+
</html>
|