Spaces:
Running
Running
Update index.html
Browse files- index.html +88 -80
index.html
CHANGED
|
@@ -798,112 +798,113 @@
|
|
| 798 |
|
| 799 |
// [新增] 初始化手寫板功能 (修復無反應問題)
|
| 800 |
function initHandwritingBoard() {
|
| 801 |
-
if (handwritingCanvasObj) return; // 避免重複初始化
|
| 802 |
-
|
| 803 |
// 建立 handwriting.js 實例 (參數: canvas元素, 筆畫粗細)
|
| 804 |
const canvasEl = document.getElementById('handwriting-canvas');
|
|
|
|
| 805 |
// 確保 canvas 寬度正確 (解決有些手機上寬度不對的問題)
|
| 806 |
const rect = canvasEl.parentElement.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
canvasEl.width = rect.width || 300;
|
| 808 |
-
canvasEl.height = 250;
|
| 809 |
|
| 810 |
-
// [重要修復] 強制註冊 passive: false
|
| 811 |
-
// 這是為了解決在 iOS Safari / 新版 Chrome 上,手指滑動會觸發頁面捲動而非畫線的問題
|
| 812 |
-
// 雖然 CSS 設了 touch-action: none,但在某些舊版 library 實作上仍需此修正
|
| 813 |
const preventDefaultTouch = (e) => {
|
| 814 |
if (e.cancelable) e.preventDefault();
|
| 815 |
};
|
| 816 |
canvasEl.addEventListener('touchstart', preventDefaultTouch, { passive: false });
|
| 817 |
canvasEl.addEventListener('touchmove', preventDefaultTouch, { passive: false });
|
| 818 |
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
-
|
| 823 |
-
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
|
| 827 |
-
|
| 828 |
-
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
-
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
|
|
|
|
|
|
| 838 |
|
| 839 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 840 |
handwritingCanvasObj.setLineWidth(5);
|
| 841 |
handwritingCanvasObj.setPenColor("#333");
|
| 842 |
-
|
| 843 |
-
// 讓 undo/redo 功能生效
|
| 844 |
handwritingCanvasObj.setOptions({
|
| 845 |
language: "en",
|
| 846 |
numOfReturn: 1
|
| 847 |
});
|
| 848 |
-
|
| 849 |
-
// 綁定按鈕事件
|
| 850 |
-
document.getElementById('hw-clear-btn').addEventListener('click', () => {
|
| 851 |
-
handwritingCanvasObj.erase();
|
| 852 |
-
answerInput.value = '';
|
| 853 |
-
answerInput.focus(); // 保持焦點
|
| 854 |
-
});
|
| 855 |
-
|
| 856 |
-
document.getElementById('hw-undo-btn').addEventListener('click', () => {
|
| 857 |
-
handwritingCanvasObj.undo();
|
| 858 |
-
});
|
| 859 |
-
|
| 860 |
-
document.getElementById('hw-recognize-btn').addEventListener('click', () => {
|
| 861 |
-
handwritingCanvasObj.recognize();
|
| 862 |
-
});
|
| 863 |
}
|
| 864 |
|
| 865 |
-
// [新增]
|
| 866 |
-
|
|
|
|
| 867 |
const hwContainer = document.getElementById('handwriting-container');
|
| 868 |
|
| 869 |
-
//
|
| 870 |
-
|
| 871 |
|
| 872 |
-
if (
|
| 873 |
-
// ---
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
// 1. 顯示手寫板
|
| 877 |
hwContainer.classList.remove('hidden');
|
| 878 |
hwContainer.classList.add('flex');
|
| 879 |
|
| 880 |
-
// 2. 初始化手寫板 (
|
| 881 |
-
|
| 882 |
-
setTimeout(() => initHandwritingBoard(), 100);
|
| 883 |
|
| 884 |
-
// 3. 鎖定輸入框
|
| 885 |
answerInput.readOnly = true;
|
| 886 |
-
answerInput.placeholder = "
|
| 887 |
-
answerInput.classList.add('handwriting-mode');
|
| 888 |
|
| 889 |
-
// 4.
|
| 890 |
if (window.google && window.google.ime) {
|
| 891 |
window.google.ime.setOptions({ ime: 'none', trigger: null });
|
| 892 |
}
|
| 893 |
|
| 894 |
} else {
|
| 895 |
-
// --- PC
|
| 896 |
-
console.log("切換為打字模式");
|
| 897 |
-
|
| 898 |
// 1. 隱藏手寫板
|
| 899 |
hwContainer.classList.add('hidden');
|
| 900 |
hwContainer.classList.remove('flex');
|
| 901 |
|
| 902 |
// 2. 恢復輸入框
|
| 903 |
answerInput.readOnly = false;
|
| 904 |
-
answerInput.placeholder = "
|
| 905 |
answerInput.classList.remove('handwriting-mode');
|
| 906 |
}
|
|
|
|
|
|
|
| 907 |
}
|
| 908 |
|
| 909 |
// --- 視圖管理 ---
|
|
@@ -1096,31 +1097,29 @@
|
|
| 1096 |
frontDisplay.classList.add('hidden');
|
| 1097 |
clozeQuestionContainer.classList.add('hidden');
|
| 1098 |
|
| 1099 |
-
// [新增] 每次顯示卡片時,呼叫設定確保介面正確
|
| 1100 |
-
applyVersionSettings();
|
| 1101 |
-
|
| 1102 |
let frontText, backText;
|
| 1103 |
-
|
|
|
|
| 1104 |
if (isSpeed) {
|
| 1105 |
-
//
|
| 1106 |
switch (currentSpeedQuestionType) {
|
| 1107 |
case 'zh-en':
|
| 1108 |
frontText = card.chinese;
|
| 1109 |
backText = card.english;
|
| 1110 |
-
|
| 1111 |
break;
|
| 1112 |
case 'en-zh':
|
| 1113 |
frontText = card.english;
|
| 1114 |
backText = card.chinese;
|
| 1115 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1116 |
-
|
| 1117 |
break;
|
| 1118 |
case 'listen':
|
| 1119 |
frontText = '請聽發音';
|
| 1120 |
backText = card.english;
|
| 1121 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1122 |
speakWord(card);
|
| 1123 |
-
|
| 1124 |
break;
|
| 1125 |
}
|
| 1126 |
} else {
|
|
@@ -1132,22 +1131,24 @@
|
|
| 1132 |
progressDisplay.textContent = `第 ${currentCardIndex + 1}/${wordsForCurrentMode.length} 張`;
|
| 1133 |
prevBtn.disabled = currentCardIndex === 0;
|
| 1134 |
nextBtn.disabled = currentCardIndex === wordsForCurrentMode.length - 1;
|
| 1135 |
-
|
|
|
|
|
|
|
| 1136 |
break;
|
| 1137 |
case 'zh-en': case 'hard':
|
| 1138 |
frontText = card.chinese; backText = card.english;
|
| 1139 |
-
|
| 1140 |
break;
|
| 1141 |
case 'en-zh':
|
| 1142 |
frontText = card.english; backText = card.chinese;
|
| 1143 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1144 |
-
|
| 1145 |
break;
|
| 1146 |
case 'listen':
|
| 1147 |
frontText = '請聽發音'; backText = card.english;
|
| 1148 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1149 |
speakWord(card);
|
| 1150 |
-
|
| 1151 |
break;
|
| 1152 |
case 'sentence-cloze':
|
| 1153 |
clozeQuestionContainer.classList.remove('hidden');
|
|
@@ -1155,11 +1156,15 @@
|
|
| 1155 |
sentenceZhDisplay.textContent = `(${card.sentence.zh})`;
|
| 1156 |
sentenceZhDisplay.classList.add('hidden');
|
| 1157 |
toggleTranslationBtn.textContent = '顯示翻譯';
|
| 1158 |
-
backText = card.sentence.answer || card.english;
|
| 1159 |
-
|
| 1160 |
break;
|
| 1161 |
}
|
| 1162 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1163 |
if (frontText) {
|
| 1164 |
frontDisplay.classList.remove('hidden');
|
| 1165 |
frontDisplay.textContent = frontText;
|
|
@@ -1911,8 +1916,11 @@
|
|
| 1911 |
if (currentMode === 'review') {
|
| 1912 |
flashcardContainer.classList.toggle('flipped');
|
| 1913 |
const isFlipped = flashcardContainer.classList.contains('flipped');
|
| 1914 |
-
|
| 1915 |
-
|
|
|
|
|
|
|
|
|
|
| 1916 |
feedbackDisplay.textContent = '';
|
| 1917 |
setTimeout(() => answerInput.focus(), 100);
|
| 1918 |
}
|
|
@@ -1974,7 +1982,7 @@
|
|
| 1974 |
passwordModal.classList.add('hidden');
|
| 1975 |
renderWordList();
|
| 1976 |
// 在進入管理畫面時也更新一下設定狀態
|
| 1977 |
-
|
| 1978 |
showView('manage');
|
| 1979 |
} else {
|
| 1980 |
passwordError.textContent = '密碼錯誤!';
|
|
|
|
| 798 |
|
| 799 |
// [新增] 初始化手寫板功能 (修復無反應問題)
|
| 800 |
function initHandwritingBoard() {
|
|
|
|
|
|
|
| 801 |
// 建立 handwriting.js 實例 (參數: canvas元素, 筆畫粗細)
|
| 802 |
const canvasEl = document.getElementById('handwriting-canvas');
|
| 803 |
+
|
| 804 |
// 確保 canvas 寬度正確 (解決有些手機上寬度不對的問題)
|
| 805 |
const rect = canvasEl.parentElement.getBoundingClientRect();
|
| 806 |
+
|
| 807 |
+
// [關鍵修復]:如果寬度是 0 (表示元素隱藏中),則不進行初始化,避免狀態錯誤
|
| 808 |
+
if (rect.width === 0) return;
|
| 809 |
+
|
| 810 |
+
// 設定 canvas 屬性寬高 (這是繪圖解析度)
|
| 811 |
canvasEl.width = rect.width || 300;
|
| 812 |
+
canvasEl.height = 250;
|
| 813 |
|
| 814 |
+
// [重要修復] 強制註冊 passive: false 的事件監聽器,防止畫面捲動
|
|
|
|
|
|
|
| 815 |
const preventDefaultTouch = (e) => {
|
| 816 |
if (e.cancelable) e.preventDefault();
|
| 817 |
};
|
| 818 |
canvasEl.addEventListener('touchstart', preventDefaultTouch, { passive: false });
|
| 819 |
canvasEl.addEventListener('touchmove', preventDefaultTouch, { passive: false });
|
| 820 |
|
| 821 |
+
// 如果物件已經存在,不需要重新 new,但需要確保屬性設定正確
|
| 822 |
+
if (!handwritingCanvasObj) {
|
| 823 |
+
handwritingCanvasObj = new handwriting.Canvas(canvasEl, 3);
|
| 824 |
+
|
| 825 |
+
// 設定辨識後的回呼函式
|
| 826 |
+
handwritingCanvasObj.setCallBack(function(data, err) {
|
| 827 |
+
if (err) {
|
| 828 |
+
console.error(err);
|
| 829 |
+
alert("辨識失敗,請檢查網路連線"); // handwriting.js 需要連網
|
| 830 |
+
return;
|
| 831 |
+
}
|
| 832 |
+
// data 是候選字陣列,取第一個
|
| 833 |
+
if (data && data.length > 0) {
|
| 834 |
+
const result = data[0];
|
| 835 |
+
answerInput.value = result; // 填入答案框
|
| 836 |
+
|
| 837 |
+
// 視覺回饋
|
| 838 |
+
answerInput.classList.add('border-green-500', 'bg-green-50');
|
| 839 |
+
setTimeout(() => answerInput.classList.remove('border-green-500', 'bg-green-50'), 500);
|
| 840 |
+
}
|
| 841 |
+
});
|
| 842 |
|
| 843 |
+
// 綁定按鈕事件 (只需綁定一次)
|
| 844 |
+
document.getElementById('hw-clear-btn').addEventListener('click', () => {
|
| 845 |
+
handwritingCanvasObj.erase();
|
| 846 |
+
answerInput.value = '';
|
| 847 |
+
answerInput.focus(); // 保持焦點
|
| 848 |
+
});
|
| 849 |
+
|
| 850 |
+
document.getElementById('hw-undo-btn').addEventListener('click', () => {
|
| 851 |
+
handwritingCanvasObj.undo();
|
| 852 |
+
});
|
| 853 |
+
|
| 854 |
+
document.getElementById('hw-recognize-btn').addEventListener('click', () => {
|
| 855 |
+
handwritingCanvasObj.recognize();
|
| 856 |
+
});
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
// [關鍵] 每次 resize 或是重啟時,都要重新設定筆畫樣式,以免因為 canvas 重設寬高而遺失
|
| 860 |
handwritingCanvasObj.setLineWidth(5);
|
| 861 |
handwritingCanvasObj.setPenColor("#333");
|
|
|
|
|
|
|
| 862 |
handwritingCanvasObj.setOptions({
|
| 863 |
language: "en",
|
| 864 |
numOfReturn: 1
|
| 865 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
}
|
| 867 |
|
| 868 |
+
// [新增] 更新輸入介面 (控制手寫板顯示與輸入法)
|
| 869 |
+
// expectedLang: 'en' (預期輸入英文) 或 'zh' (預期輸入中文)
|
| 870 |
+
function updateInputInterface(expectedLang) {
|
| 871 |
const hwContainer = document.getElementById('handwriting-container');
|
| 872 |
|
| 873 |
+
// 只有在行動裝置 且 預期輸入英文時,才開啟手寫板
|
| 874 |
+
const shouldUseHandwriting = (effectiveVersion === 'mobile' && expectedLang === 'en');
|
| 875 |
|
| 876 |
+
if (shouldUseHandwriting) {
|
| 877 |
+
// --- 行動版 + 輸入英文 (開啟手寫) ---
|
| 878 |
+
// 1. 顯示手寫板容器
|
|
|
|
|
|
|
| 879 |
hwContainer.classList.remove('hidden');
|
| 880 |
hwContainer.classList.add('flex');
|
| 881 |
|
| 882 |
+
// 2. 初始化手寫板 (使用 setTimeout 確保 flex 佈局生效後有正確寬度)
|
| 883 |
+
setTimeout(() => initHandwritingBoard(), 50);
|
|
|
|
| 884 |
|
| 885 |
+
// 3. 鎖定輸入框 (變成顯示結果用)
|
| 886 |
answerInput.readOnly = true;
|
| 887 |
+
answerInput.placeholder = "請在下方手寫英文...";
|
| 888 |
+
answerInput.classList.add('handwriting-mode');
|
| 889 |
|
| 890 |
+
// 4. 嘗試移除原生輸入法干擾
|
| 891 |
if (window.google && window.google.ime) {
|
| 892 |
window.google.ime.setOptions({ ime: 'none', trigger: null });
|
| 893 |
}
|
| 894 |
|
| 895 |
} else {
|
| 896 |
+
// --- PC 版 或 行動版輸入中文 (關閉手寫,用鍵盤) ---
|
|
|
|
|
|
|
| 897 |
// 1. 隱藏手寫板
|
| 898 |
hwContainer.classList.add('hidden');
|
| 899 |
hwContainer.classList.remove('flex');
|
| 900 |
|
| 901 |
// 2. 恢復輸入框
|
| 902 |
answerInput.readOnly = false;
|
| 903 |
+
answerInput.placeholder = (expectedLang === 'zh') ? "輸入中文答案 (按 Enter 提交)" : "輸入英文答案 (按 Enter 提交)";
|
| 904 |
answerInput.classList.remove('handwriting-mode');
|
| 905 |
}
|
| 906 |
+
|
| 907 |
+
console.log(`輸入模式: ${shouldUseHandwriting ? '手寫 (Handwriting)' : '鍵盤 (Keyboard)'}, 語言: ${expectedLang}`);
|
| 908 |
}
|
| 909 |
|
| 910 |
// --- 視圖管理 ---
|
|
|
|
| 1097 |
frontDisplay.classList.add('hidden');
|
| 1098 |
clozeQuestionContainer.classList.add('hidden');
|
| 1099 |
|
|
|
|
|
|
|
|
|
|
| 1100 |
let frontText, backText;
|
| 1101 |
+
let targetLanguage = 'en'; // 預設輸入英文
|
| 1102 |
+
|
| 1103 |
if (isSpeed) {
|
| 1104 |
+
// 極速挑戰
|
| 1105 |
switch (currentSpeedQuestionType) {
|
| 1106 |
case 'zh-en':
|
| 1107 |
frontText = card.chinese;
|
| 1108 |
backText = card.english;
|
| 1109 |
+
targetLanguage = 'en';
|
| 1110 |
break;
|
| 1111 |
case 'en-zh':
|
| 1112 |
frontText = card.english;
|
| 1113 |
backText = card.chinese;
|
| 1114 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1115 |
+
targetLanguage = 'zh';
|
| 1116 |
break;
|
| 1117 |
case 'listen':
|
| 1118 |
frontText = '請聽發音';
|
| 1119 |
backText = card.english;
|
| 1120 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1121 |
speakWord(card);
|
| 1122 |
+
targetLanguage = 'en';
|
| 1123 |
break;
|
| 1124 |
}
|
| 1125 |
} else {
|
|
|
|
| 1131 |
progressDisplay.textContent = `第 ${currentCardIndex + 1}/${wordsForCurrentMode.length} 張`;
|
| 1132 |
prevBtn.disabled = currentCardIndex === 0;
|
| 1133 |
nextBtn.disabled = currentCardIndex === wordsForCurrentMode.length - 1;
|
| 1134 |
+
// 複習模式下:沒翻面(看英文)=輸入中文,翻面(看中文)=輸入英文
|
| 1135 |
+
const isFlipped = flashcardContainer.classList.contains('flipped');
|
| 1136 |
+
targetLanguage = isFlipped ? 'en' : 'zh';
|
| 1137 |
break;
|
| 1138 |
case 'zh-en': case 'hard':
|
| 1139 |
frontText = card.chinese; backText = card.english;
|
| 1140 |
+
targetLanguage = 'en';
|
| 1141 |
break;
|
| 1142 |
case 'en-zh':
|
| 1143 |
frontText = card.english; backText = card.chinese;
|
| 1144 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1145 |
+
targetLanguage = 'zh';
|
| 1146 |
break;
|
| 1147 |
case 'listen':
|
| 1148 |
frontText = '請聽發音'; backText = card.english;
|
| 1149 |
[speakBtn, speakSlowBtn].forEach(btn => btn.classList.remove('hidden'));
|
| 1150 |
speakWord(card);
|
| 1151 |
+
targetLanguage = 'en';
|
| 1152 |
break;
|
| 1153 |
case 'sentence-cloze':
|
| 1154 |
clozeQuestionContainer.classList.remove('hidden');
|
|
|
|
| 1156 |
sentenceZhDisplay.textContent = `(${card.sentence.zh})`;
|
| 1157 |
sentenceZhDisplay.classList.add('hidden');
|
| 1158 |
toggleTranslationBtn.textContent = '顯示翻譯';
|
| 1159 |
+
backText = card.sentence.answer || card.english;
|
| 1160 |
+
targetLanguage = 'en';
|
| 1161 |
break;
|
| 1162 |
}
|
| 1163 |
}
|
| 1164 |
+
|
| 1165 |
+
// [新增] 根據目標語言決定是否顯示手寫板
|
| 1166 |
+
updateInputInterface(targetLanguage);
|
| 1167 |
+
|
| 1168 |
if (frontText) {
|
| 1169 |
frontDisplay.classList.remove('hidden');
|
| 1170 |
frontDisplay.textContent = frontText;
|
|
|
|
| 1916 |
if (currentMode === 'review') {
|
| 1917 |
flashcardContainer.classList.toggle('flipped');
|
| 1918 |
const isFlipped = flashcardContainer.classList.contains('flipped');
|
| 1919 |
+
|
| 1920 |
+
// [智慧輸入切換] 複習模式下:翻面後看到中文->要輸入英文
|
| 1921 |
+
const targetLanguage = isFlipped ? 'en' : 'zh';
|
| 1922 |
+
updateInputInterface(targetLanguage);
|
| 1923 |
+
|
| 1924 |
feedbackDisplay.textContent = '';
|
| 1925 |
setTimeout(() => answerInput.focus(), 100);
|
| 1926 |
}
|
|
|
|
| 1982 |
passwordModal.classList.add('hidden');
|
| 1983 |
renderWordList();
|
| 1984 |
// 在進入管理畫面時也更新一下設定狀態
|
| 1985 |
+
updateInputInterface('pc'); // 管理介面不需要手寫
|
| 1986 |
showView('manage');
|
| 1987 |
} else {
|
| 1988 |
passwordError.textContent = '密碼錯誤!';
|