flashcards_test / index.html
Lashtw's picture
Update index.html
7767ac9 verified
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>單字閃卡</title>
<!-- 載入 Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- 載入彩帶效果庫 -->
<script src="https://cdn.jsdelivr.net/npm/canvas-confetti@1.9.2/dist/confetti.browser.min.js"></script>
<!-- 載入 QR Code 產生器 -->
<script src="https://cdn.jsdelivr.net/npm/qrcode-generator/qrcode.js"></script>
<!-- 載入 Pako 壓縮函式庫 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js"></script>
<!-- 載入 PDF.js 函式庫 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.11.338/pdf.min.js"></script>
<!-- 【新增】載入 Google Handwriting Input API (IME) -->
<script src="https://www.google.com/inputtools/request?ime=handwriting&app=gws&cs=1&oe=UTF-8&ss=1&v=3" async></script>
<!-- 【新】Firebase SDK -->
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-app.js";
import { getFirestore, collection, addDoc, getDoc, doc } from "https://www.gstatic.com/firebasejs/9.15.0/firebase-firestore.js";
// 您的 Firebase 設定金鑰
const firebaseConfig = {
apiKey: "AIzaSyAIRUONOLmA1pe42cmH_MNPgGC3oHKuceo",
authDomain: "englishflashcardbackstage.firebaseapp.com",
projectId: "englishflashcardbackstage",
storageBucket: "englishflashcardbackstage.firebasestorage.app",
messagingSenderId: "911755356145",
appId: "1:911755356145:web:a4a5b0245b3bb9f15d4650"
};
// 初始化 Firebase
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// 將 db 實例暴露到全域,以便主腳本可以存取
window.firebaseDB = db;
window.firebaseFirestore = { collection, addDoc, getDoc, doc };
</script>
<style>
/* 定義閃卡的翻轉效果 */
.flip-container {
perspective: 1000px;
}
.flipper {
transition: transform 0.6s;
transform-style: preserve-3d;
position: relative;
}
.flip-container.flipped .flipper {
transform: rotateY(180deg);
}
.front, .back {
backface-visibility: hidden;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
border-radius: 1.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
color: white;
text-align: center;
background: linear-gradient(135deg, var(--tw-gradient-from), var(--tw-gradient-to));
}
.front {
z-index: 2;
transform: rotateY(0deg);
--tw-gradient-from: #6366f1; /* Indigo-500 */
--tw-gradient-to: #4f46e5; /* Indigo-600 */
}
.back {
transform: rotateY(180deg);
--tw-gradient-from: #a78bfa; /* Violet-400 */
--tw-gradient-to: #c4b5fd; /* Violet-300 */
}
/* 答錯時的晃動動畫 */
@keyframes shake {
10%, 90% { transform: translate3d(-1px, 0, 0); }
20%, 80% { transform: translate3d(2px, 0, 0); }
30%, 50%, 70% { transform: translate3d(-4px, 0, 0); }
40%, 60% { transform: translate3d(4px, 0, 0); }
}
.shake {
animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both;
}
.timer-highlight {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
50% { transform: scale(1.1); }
}
.speak-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* 列表動畫 */
.list-item {
transition: all 0.3s ease-out;
opacity: 1;
transform: translateX(0);
}
.list-item.removing {
opacity: 0;
transform: translateX(100%);
}
#qrcode-container img {
margin: auto;
border: 6px solid white;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
/* 進度條動畫 */
#ai-progress-bar-inner, #audio-preload-bar {
transition: width 0.3s ease-in-out;
}
/* 【新增】手寫輸入框樣式 */
.handwriting-input {
font-size: 1.5rem; /* 放大字體 */
padding: 1rem; /* 增加內邊距 */
text-align: center;
border: 3px solid #4f46e5; /* 醒目的邊框 */
box-shadow: 0 0 10px rgba(79, 70, 229, 0.5); /* 陰影 */
}
</style>
</head>
<body class="bg-gray-50 min-h-screen flex flex-col items-center p-4 sm:p-8 font-sans">
<!-- 主選單:新增單字 & 模式選擇 -->
<div id="main-menu" class="w-full max-w-2xl bg-white p-6 rounded-3xl shadow-2xl transition-all duration-500">
<h1 id="main-title" class="text-4xl font-extrabold text-center text-gray-900 mb-6"></h1>
<!-- 模式選擇區塊 -->
<div id="mode-selection-section">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-gray-800">選擇學習模式</h2>
<div class="text-right">
<p class="text-sm text-gray-500">極速挑戰最高分</p>
<p id="highscore-display" class="text-lg font-bold text-amber-500">0</p>
</div>
</div>
<!-- 單字範圍與隨機出題選擇 -->
<div id="settings-container" class="my-4 p-4 bg-gray-100 rounded-2xl space-y-3">
<div class="flex items-center justify-center gap-4">
<label for="start-range" class="font-semibold text-gray-700">單字範圍:</label>
<input type="number" id="start-range" min="1" class="w-20 p-2 border border-gray-300 rounded-lg text-center" placeholder="從">
<span class="font-semibold text-gray-700">-</span>
<input type="number" id="end-range" min="1" class="w-20 p-2 border border-gray-300 rounded-lg text-center" placeholder="到">
</div>
<div class="flex items-center justify-center gap-4">
<input type="checkbox" id="random-questions-checkbox" class="h-5 w-5 rounded text-indigo-600 focus:ring-indigo-500 border-gray-300">
<label for="random-questions-checkbox" class="font-semibold text-gray-700">隨機出題:</label>
<input type="number" id="random-questions-count" min="1" class="w-20 p-2 border border-gray-300 rounded-lg text-center disabled:bg-gray-200 disabled:cursor-not-allowed" placeholder="題數" disabled>
<span class="font-semibold text-gray-700"></span>
</div>
</div>
<!-- 主要功能 -->
<div class="mb-4">
<div class="relative mb-3 border-b pb-2">
<h3 class="text-lg font-semibold text-gray-600 text-center">主要功能</h3>
<button id="reset-crowns-btn" class="absolute right-0 top-1/2 -translate-y-1/2 text-xs bg-gray-200 hover:bg-gray-300 text-gray-700 font-semibold py-1 px-3 rounded-full transition-colors disabled:bg-gray-300">重置進度</button>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button data-mode="review" class="mode-btn bg-sky-500 hover:bg-sky-600">複習模式</button>
<button data-mode="zh-en" class="mode-btn bg-teal-500 hover:bg-teal-600 relative">
中翻英測驗
<span class="crown-icon hidden absolute -top-2 -right-1 text-4xl" style="transform: rotate(15deg);">👑</span>
</button>
<button data-mode="en-zh" class="mode-btn bg-amber-500 hover:bg-amber-600 relative">
英翻中測驗
<span class="crown-icon hidden absolute -top-2 -right-1 text-4xl" style="transform: rotate(15deg);">👑</span>
</button>
<button data-mode="listen" class="mode-btn bg-rose-500 hover:bg-rose-600 relative">
聽力測驗
<span class="crown-icon hidden absolute -top-2 -right-1 text-4xl" style="transform: rotate(15deg);">👑</span>
</button>
</div>
</div>
<!-- 補充功能 -->
<div class="mb-4">
<h3 class="text-lg font-semibold text-gray-600 mb-3 text-center border-b pb-2">補充功能</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<button data-mode="hard" class="mode-btn bg-red-700 hover:bg-red-800">🧠 困難單字複習</button>
<button data-mode="speed" class="mode-btn bg-slate-700 hover:bg-slate-800">⚡ 極速挑戰 ⚡</button>
<button data-mode="sentence-cloze" class="mode-btn bg-cyan-600 hover:bg-cyan-700">📝 情境克漏字</button>
</div>
</div>
<!-- Audio Preload Progress -->
<div id="audio-preload-container" class="hidden mt-4">
<p id="audio-preload-text" class="text-sm text-center text-gray-500 mb-1">正在預載語音檔案...</p>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="audio-preload-bar" class="bg-sky-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
<!-- 教師工具 -->
<div id="teacher-tools" class="mt-8 pt-4 border-t flex justify-end items-center gap-3">
<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>
<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>
<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>
</div>
</div>
</div>
<!-- 單字管理介面 (預設隱藏) -->
<div id="word-management-view" class="hidden w-full max-w-4xl bg-white p-6 rounded-3xl shadow-2xl">
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-bold text-gray-800">單字列表管理</h2>
<button id="back-to-modes-from-manage-btn" class="bg-gray-200 text-gray-800 p-3 rounded-full hover:bg-gray-300 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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>
</button>
</div>
<!-- 標題設定區塊 -->
<div class="mb-6 p-4 bg-gray-50 rounded-2xl">
<h3 class="text-xl font-semibold mb-4 text-gray-700">標題設定</h3>
<input type="text" id="title-input" placeholder="輸入您的單字本標題" class="w-full p-3 border border-gray-300 rounded-lg focus:ring-indigo-500 focus:border-indigo-500 text-lg mb-3">
<button id="save-title-btn" class="w-full bg-indigo-500 text-white p-3 rounded-lg font-semibold hover:bg-indigo-600 transition-colors">儲存標題</button>
</div>
<!-- 【新增】版本鎖定設定區塊 -->
<div class="mb-6 p-4 bg-indigo-50 rounded-2xl">
<h3 class="text-xl font-semibold mb-4 text-gray-700">版本鎖定設定 (分享時生效)</h3>
<div class="flex items-center space-x-4">
<label class="flex items-center">
<input type="radio" name="version-lock" value="auto" id="lock-auto" class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500" checked>
<span class="ml-2 text-gray-700">自動判斷 (預設)</span>
</label>
<label class="flex items-center">
<input type="radio" name="version-lock" value="pc" id="lock-pc" class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
<span class="ml-2 text-gray-700">鎖定 PC 版 (打字)</span>
</label>
<label class="flex items-center">
<input type="radio" name="version-lock" value="mobile" id="lock-mobile" class="h-4 w-4 text-indigo-600 border-gray-300 focus:ring-indigo-500">
<span class="ml-2 text-gray-700">鎖定 行動版 (手寫)</span>
</label>
</div>
</div>
<!-- 單字新增區塊 -->
<div class="mb-6 p-4 bg-gray-50 rounded-2xl">
<h3 class="text-xl font-semibold mb-4 text-gray-700">手動新增單字</h3>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
<input type="text" id="new-english" placeholder="英文單字 (含詞性)" class="p-3 border border-gray-300 rounded-lg">
<input type="text" id="new-chinese" placeholder="中文翻譯" class="p-3 border border-gray-300 rounded-lg">
</div>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-4">
<input type="text" id="new-sentence-en" placeholder="英文例句 (用___代替單字)" class="p-3 border border-gray-300 rounded-lg col-span-2">
<input type="text" id="new-sentence-answer" placeholder="克漏字答案" class="p-3 border border-gray-300 rounded-lg">
</div>
<input type="text" id="new-sentence-zh" placeholder="中文例句翻譯" class="w-full p-3 border border-gray-300 rounded-lg mb-4">
<button id="add-word-btn" class="w-full bg-green-500 text-white p-3 rounded-lg font-semibold hover:bg-green-600 transition-colors">新增單字</button>
</div>
<!-- PDF/文字解析區塊 -->
<div class="mb-6 p-4 bg-gray-50 rounded-2xl">
<h3 class="text-xl font-semibold mb-4 text-gray-700">PDF/文字 AI 解析新增</h3>
<div class="flex items-center space-x-4 mb-4">
<input type="text" id="api-key-input" placeholder="輸入您的 Gemini API Key" class="flex-1 p-3 border border-gray-300 rounded-lg">
<button id="save-api-key-btn" class="bg-blue-500 text-white p-3 rounded-lg font-semibold hover:bg-blue-600 transition-colors">儲存 Key</button>
</div>
<input type="file" id="pdf-upload-input" accept=".pdf" class="w-full p-3 border border-gray-300 rounded-lg mb-4 bg-white">
<textarea id="text-input" placeholder="或直接貼上文字內容" rows="5" class="w-full p-3 border border-gray-300 rounded-lg mb-4"></textarea>
<button id="parse-pdf-btn" class="w-full bg-purple-500 text-white p-3 rounded-lg font-semibold hover:bg-purple-600 transition-colors">開始 AI 解析</button>
<p id="parse-status" class="mt-3 text-center text-sm text-gray-600"></p>
<!-- AI 進度條 -->
<div id="ai-progress-container" class="hidden mt-4">
<p class="text-sm text-center text-gray-500 mb-1">AI 正在努力解析中...</p>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="ai-progress-bar-inner" class="bg-purple-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
</div>
</div>
<!-- 單字列表 -->
<div class="mb-6">
<h3 class="text-xl font-semibold mb-4 text-gray-700">已儲存單字列表 (<span id="word-count">0</span> 個)</h3>
<div id="word-list" class="space-y-3">
<!-- 單字列表將由 JS 渲染 -->
</div>
</div>
<!-- 清除所有單字按鈕 -->
<button id="clear-all-words-btn" class="w-full bg-red-500 text-white p-3 rounded-lg font-semibold hover:bg-red-600 transition-colors">清除所有單字和紀錄</button>
</div>
<!-- AI 解析確認 Modal (預設隱藏) -->
<div id="parse-confirm-modal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
<h3 class="text-2xl font-bold mb-4 text-gray-800">AI 解析結果確認</h3>
<p class="text-gray-600 mb-4">請檢查以下單字列表,取消勾選不需要新增的項目。</p>
<div class="flex items-center mb-4">
<input type="checkbox" id="select-all-checkbox" class="h-5 w-5 rounded text-indigo-600 focus:ring-indigo-500 border-gray-300">
<label for="select-all-checkbox" class="ml-2 font-semibold text-gray-700">全選/全不選</label>
</div>
<div id="parse-results-container" class="space-y-3 mb-6">
<!-- AI 解析結果將由 JS 渲染 -->
</div>
<div class="flex justify-end gap-4 mt-6">
<button type="button" id="cancel-parse-btn" class="px-6 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors">取消並放棄</button>
<button type="button" id="confirm-parse-btn" class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">確認新增所選單字</button>
</div>
</div>
</div>
<!-- 學習介面 (預設隱藏) -->
<div id="learning-view" class="hidden w-full max-w-2xl">
<!-- 極速挑戰 HUD -->
<div id="speed-hud" class="hidden fixed top-0 left-0 right-0 bg-white shadow-lg p-4 z-10 flex justify-between items-center">
<div class="text-xl font-bold text-gray-800">分數: <span id="speed-score">0</span></div>
<div class="text-2xl font-extrabold text-red-600">時間: <span id="speed-timer">60</span>s</div>
</div>
<div id="hud-placeholder" class="h-16 hidden"></div> <!-- 佔位符 -->
<!-- 進度條 -->
<div id="progress-bar-container" class="w-full mb-6">
<div class="flex justify-between items-center mb-2">
<span id="progress-text" class="text-lg font-semibold text-gray-700">進度: 0/0</span>
<span id="mistake-count" class="text-lg font-semibold text-red-500">錯誤: 0</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-3">
<div id="progress-bar" class="bg-indigo-500 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
</div>
</div>
<!-- 閃卡區塊 -->
<div id="flashcard-container" class="flip-container w-full h-96 mb-6">
<div class="flipper w-full h-full">
<!-- 正面 (中文) -->
<div id="card-front" class="front text-4xl font-bold bg-indigo-500 text-white">
<span id="card-zh">中文翻譯</span>
</div>
<!-- 背面 (英文) -->
<div id="card-back" class="back text-4xl font-bold bg-violet-400 text-white">
<span id="card-en">English Word</span>
</div>
</div>
</div>
<!-- 測驗區塊 -->
<div id="quiz-container" class="w-full max-w-md mx-auto">
<div id="quiz-completion-message" class="hidden text-center p-8 bg-green-100 rounded-xl shadow-lg">
<p class="text-3xl font-bold text-green-700 mb-4">測驗完成!🎉</p>
<p id="quiz-summary" class="text-xl text-gray-700"></p>
</div>
<!-- 題目顯示 -->
<div id="question-display" class="text-center mb-6 p-4 bg-white rounded-xl shadow-md">
<p id="question-text" class="text-2xl font-semibold text-gray-800">題目</p>
<button id="speak-btn" class="speak-btn mt-2 p-2 bg-indigo-100 text-indigo-600 rounded-full hover:bg-indigo-200 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1V9a1 1 0 011-1h1.586l4.707-4.707A1 1 0 0112 3v18a1 1 0 01-1.707.707L5.586 15z" /></svg>
</button>
</div>
<!-- 答案輸入區塊 -->
<div class="flex flex-col gap-4">
<!-- 【修改】答案輸入框,移除 type="text" 以便手寫辨識 API 介入 -->
<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">
<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>
<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>
</div>
</div>
<!-- 複習模式導航 -->
<div id="review-nav-container" class="hidden w-full max-w-md mx-auto mt-6 flex justify-between gap-4">
<button id="prev-card-btn" class="flex-1 bg-gray-500 text-white p-3 rounded-xl text-lg font-bold hover:bg-gray-600 transition-colors">上一張</button>
<button id="flip-card-btn" class="flex-1 bg-indigo-500 text-white p-3 rounded-xl text-lg font-bold hover:bg-indigo-600 transition-colors">翻轉</button>
<button id="next-card-btn" class="flex-1 bg-green-500 text-white p-3 rounded-xl text-lg font-bold hover:bg-green-600 transition-colors">下一張</button>
</div>
<!-- 返回主選單 -->
<button id="back-to-menu-btn" class="mt-8 px-6 py-3 bg-gray-200 text-gray-800 rounded-full font-semibold hover:bg-gray-300 transition-colors">返回主選單</button>
</div>
<!-- 成績報告介面 (預設隱藏) -->
<div id="grade-report-view" class="hidden w-full max-w-4xl bg-white p-6 rounded-3xl shadow-2xl">
<div class="flex justify-between items-center mb-6">
<h2 class="text-3xl font-bold text-gray-800">成績報告</h2>
<button id="back-to-modes-from-report-btn" class="bg-gray-200 text-gray-800 p-3 rounded-full hover:bg-gray-300 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" 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>
</button>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Report for zh-en -->
<div class="p-4 bg-teal-50 rounded-lg border border-teal-200">
<h4 class="text-xl font-bold text-teal-800 mb-4">中翻英測驗</h4>
<div id="report-zh-en" class="space-y-3 text-left text-gray-700 max-h-80 overflow-y-auto pr-2">
<!-- Content will be generated by JS -->
</div>
</div>
<!-- Report for en-zh -->
<div class="p-4 bg-amber-50 rounded-lg border border-amber-200">
<h4 class="text-xl font-bold text-amber-800 mb-4">英翻中測驗</h4>
<div id="report-en-zh" class="space-y-3 text-left text-gray-700 max-h-80 overflow-y-auto pr-2">
<!-- Content will be generated by JS -->
</div>
</div>
<!-- Report for listen -->
<div class="p-4 bg-rose-50 rounded-lg border border-rose-200">
<h4 class="text-xl font-bold text-rose-800 mb-4">聽力測驗</h4>
<div id="report-listen" class="space-y-3 text-left text-gray-700 max-h-80 overflow-y-auto pr-2">
<!-- Content will be generated by JS -->
</div>
</div>
</div>
</div>
<!-- 清除確認 Modal -->
<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">
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-sm">
<h3 class="text-2xl font-bold mb-4 text-gray-800">確認操作</h3>
<p class="text-gray-600 mb-6">您確定要清除所有單字和成績紀錄嗎?此操作無法復原。</p>
<div class="flex justify-end gap-4 mt-6">
<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>
<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>
</div>
</div>
</div>
<!-- 單字刪除確認 Modal -->
<div id="confirm-delete-modal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-sm">
<h3 class="text-2xl font-bold mb-4 text-gray-800">確認刪除</h3>
<p class="text-gray-600 mb-6">您確定要刪除單字 "<span id="word-to-delete" class="font-bold"></span>" 嗎?此操作無法復原。</p>
<div class="flex justify-end gap-4 mt-6">
<button type="button" id="cancel-delete-btn" class="px-6 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors">取消</button>
<button type="button" id="confirm-delete-btn" class="px-6 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors">確認刪除</button>
</div>
</div>
</div>
<!-- 分享 Modal -->
<div id="share-modal" class="hidden fixed inset-0 bg-gray-900 bg-opacity-75 flex items-center justify-center z-50 p-4">
<div class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-md">
<h3 class="text-2xl font-bold mb-4 text-gray-800">分享您的單字本</h3>
<p class="text-gray-600 mb-6">將以下連結或 QR Code 分享給您的學生。</p>
<div id="qrcode-container" class="mb-6 p-4 bg-gray-100 rounded-lg">
<!-- QR Code 將由 JS 渲染 -->
</div>
<div class="flex items-center border border-gray-300 rounded-lg p-3 mb-6 bg-gray-50">
<input type="text" id="share-link-input" readonly class="flex-1 bg-transparent text-gray-700 truncate">
<button id="copy-link-btn" class="ml-3 bg-indigo-500 text-white p-2 rounded-lg text-sm font-semibold hover:bg-indigo-600 transition-colors">複製連結</button>
</div>
<div class="flex justify-end">
<button type="button" id="close-share-modal-btn" class="px-6 py-2 bg-gray-200 text-gray-800 rounded-lg hover:bg-gray-300 transition-colors">關閉</button>
</div>
</div>
</div>
<script>
// --- 全域變數 ---
let words = [];
let quizQueue = [];
let wordsForCurrentMode = [];
let currentCardIndex = 0;
let currentMode = '';
let quizIncorrectCount = 0;
let quizStartTime = 0;
let completionStatus = {}; // { 'zh-en': true, 'en-zh': false, 'listen': true }
let highScore = 0;
let gradeReports = {}; // { 'zh-en': [], 'en-zh': [], 'listen': [] }
let parsedWordsFromAI = [];
let timerInterval;
let timeLeft = 60;
let speedScore = 0;
let currentSpeedCard = null;
let currentSpeedQuestionType = '';
let isMobileDevice = /Mobi|Android/i.test(navigator.userAgent); // 簡易判斷是否為行動裝置
let versionLock = 'auto'; // 【新增】版本鎖定設定: 'auto', 'pc', 'mobile'
let effectiveVersion = 'pc'; // 【新增】實際運行的版本: 'pc' (打字) 或 'mobile' (手寫)
// --- DOM 元素 ---
const mainMenu = document.getElementById('main-menu');
const mainTitle = document.getElementById('main-title');
const wordManagementView = document.getElementById('word-management-view');
const learningView = document.getElementById('learning-view');
const flashcardContainer = document.getElementById('flashcard-container');
const cardZh = document.getElementById('card-zh');
const cardEn = document.getElementById('card-en');
const flipCardBtn = document.getElementById('flip-card-btn');
const prevCardBtn = document.getElementById('prev-card-btn');
const nextCardBtn = document.getElementById('next-card-btn');
const reviewNavContainer = document.getElementById('review-nav-container');
const quizContainer = document.getElementById('quiz-container');
const questionText = document.getElementById('question-text');
const answerInput = document.getElementById('answer-input');
const submitAnswerBtn = document.getElementById('submit-answer-btn');
const backToMenuBtn = document.getElementById('back-to-menu-btn');
const progressBar = document.getElementById('progress-bar');
const progressText = document.getElementById('progress-text');
const mistakeCount = document.getElementById('mistake-count');
const progressBarContainer = document.getElementById('progress-bar-container');
const quizCompletionMessage = document.getElementById('quiz-completion-message');
const quizSummary = document.getElementById('quiz-summary');
const speakBtn = document.getElementById('speak-btn');
const wordList = document.getElementById('word-list');
const wordCount = document.getElementById('word-count');
const newEnglish = document.getElementById('new-english');
const newChinese = document.getElementById('new-chinese');
const newSentenceEn = document.getElementById('new-sentence-en');
const newSentenceZh = document.getElementById('new-sentence-zh');
const newSentenceAnswer = document.getElementById('new-sentence-answer');
const addWordBtn = document.getElementById('add-word-btn');
const clearAllWordsBtn = document.getElementById('clear-all-words-btn');
const confirmClearModal = document.getElementById('confirm-clear-modal');
const cancelClearBtn = document.getElementById('cancel-clear-btn');
const confirmClearBtn = document.getElementById('confirm-clear-btn');
const confirmDeleteModal = document.getElementById('confirm-delete-modal');
const cancelDeleteBtn = document.getElementById('cancel-delete-btn');
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
const wordToDeleteSpan = document.getElementById('word-to-delete');
const manageWordsBtn = document.getElementById('manage-words-btn');
const backToModesFromManageBtn = document.getElementById('back-to-modes-from-manage-btn');
const titleInput = document.getElementById('title-input');
const saveTitleBtn = document.getElementById('save-title-btn');
const highscoreDisplay = document.getElementById('highscore-display');
const speedHud = document.getElementById('speed-hud');
const speedScoreDisplay = document.getElementById('speed-score');
const speedTimerDisplay = document.getElementById('speed-timer');
const hudPlaceholder = document.getElementById('hud-placeholder');
const hintBtn = document.getElementById('hint-btn');
const resetCrownsBtn = document.getElementById('reset-crowns-btn');
const shareGameBtn = document.getElementById('share-game-btn');
const shareModal = document.getElementById('share-modal');
const closeShareModalBtn = document.getElementById('close-share-modal-btn');
const shareLinkInput = document.getElementById('share-link-input');
const copyLinkBtn = document.getElementById('copy-link-btn');
const qrcodeContainer = document.getElementById('qrcode-container');
const gradeReportBtn = document.getElementById('grade-report-btn');
const gradeReportView = document.getElementById('grade-report-view');
const backToModesFromReportBtn = document.getElementById('back-to-modes-from-report-btn');
const reportZhEn = document.getElementById('report-zh-en');
const reportEnZh = document.getElementById('report-en-zh');
const reportListen = document.getElementById('report-listen');
const startRangeInput = document.getElementById('start-range');
const endRangeInput = document.getElementById('end-range');
const randomQuestionsCheckbox = document.getElementById('random-questions-checkbox');
const randomQuestionsCountInput = document.getElementById('random-questions-count');
const audioPreloadContainer = document.getElementById('audio-preload-container');
const audioPreloadBar = document.getElementById('audio-preload-bar');
const audioPreloadText = document.getElementById('audio-preload-text');
// AI 解析相關
const apiKeyInput = document.getElementById('api-key-input');
const saveApiKeyBtn = document.getElementById('save-api-key-btn');
const pdfUploadInput = document.getElementById('pdf-upload-input');
const textInput = document.getElementById('text-input');
const parsePdfBtn = document.getElementById('parse-pdf-btn');
const parseStatus = document.getElementById('parse-status');
const aiProgressContainer = document.getElementById('ai-progress-container');
const aiProgressBarInner = document.getElementById('ai-progress-bar-inner');
const parseConfirmModal = document.getElementById('parse-confirm-modal');
const parseResultsContainer = document.getElementById('parse-results-container');
const selectAllCheckbox = document.getElementById('select-all-checkbox');
const confirmParseBtn = document.getElementById('confirm-parse-btn');
const cancelParseBtn = document.getElementById('cancel-parse-btn');
// 【新增】版本鎖定 DOM
const lockAuto = document.getElementById('lock-auto');
const lockPc = document.getElementById('lock-pc');
const lockMobile = document.getElementById('lock-mobile');
// --- 音效 ---
const correctAudio = new Audio('correct.mp3');
const wrongAudio = new Audio('wrong.mp3');
// --- 輔助函式 ---
// 【新增】初始化版本設定
function initializeVersion() {
// 1. 檢查 URL 參數是否有版本鎖定
const urlParams = new URLSearchParams(window.location.search);
const urlLock = urlParams.get('version');
if (urlLock === 'pc' || urlLock === 'mobile') {
versionLock = urlLock;
} else {
// 2. 檢查 localStorage 是否有教師設定
const storedLock = localStorage.getItem('versionLock');
if (storedLock === 'pc' || storedLock === 'mobile') {
versionLock = storedLock;
}
}
// 3. 根據鎖定和裝置類型決定實際版本
if (versionLock === 'pc') {
effectiveVersion = 'pc';
} else if (versionLock === 'mobile') {
effectiveVersion = 'mobile';
} else {
// auto 模式
effectiveVersion = isMobileDevice ? 'mobile' : 'pc';
}
// 4. 應用版本設定
applyVersionSettings();
// 5. 更新管理介面的 radio button 狀態
if (versionLock === 'pc') lockPc.checked = true;
else if (versionLock === 'mobile') lockMobile.checked = true;
else lockAuto.checked = true;
}
// 【新增】應用版本設定
function applyVersionSettings() {
if (effectiveVersion === 'mobile') {
// 行動版:強制手寫辨識
answerInput.classList.add('handwriting-input');
answerInput.setAttribute('inputmode', 'none'); // 禁用原生鍵盤
answerInput.setAttribute('lang', 'en'); // 設置語言為英文
answerInput.setAttribute('data-ime', 'handwriting'); // 標記為手寫輸入
// 嘗試啟用 Google Handwriting Input API
if (window.google && window.google.ime) {
window.google.ime.setOptions({
ime: 'handwriting',
language: 'en',
// 確保在行動裝置上點擊輸入框時,手寫板會彈出
trigger: answerInput
});
} else {
// 如果 API 載入失敗,給予提示
answerInput.placeholder = '手寫輸入載入中... (請點擊輸入框)';
}
} else {
// PC 版:標準打字輸入
answerInput.classList.remove('handwriting-input');
answerInput.removeAttribute('inputmode');
answerInput.removeAttribute('lang');
answerInput.removeAttribute('data-ime');
answerInput.placeholder = '輸入你的答案 (按 Enter 提交)';
// 嘗試禁用 Google Handwriting Input API (如果已啟用)
if (window.google && window.google.ime) {
window.google.ime.setOptions({
ime: 'none',
trigger: null
});
}
}
// 提示使用者當前版本
const versionText = effectiveVersion === 'mobile' ? '行動版 (手寫)' : 'PC 版 (打字)';
console.log(`當前版本: ${versionText} (鎖定設定: ${versionLock})`);
}
// 【新增】儲存版本鎖定設定
function saveVersionLock() {
const selectedLock = document.querySelector('input[name="version-lock"]:checked').value;
versionLock = selectedLock;
if (versionLock === 'auto') {
localStorage.removeItem('versionLock');
} else {
localStorage.setItem('versionLock', versionLock);
}
// 重新應用設定以更新當前介面
initializeVersion();
const originalText = saveTitleBtn.textContent;
saveTitleBtn.textContent = '設定已儲存!';
setTimeout(() => {
saveTitleBtn.textContent = originalText;
}, 1500);
}
// 【新增】版本鎖定事件監聽
document.querySelectorAll('input[name="version-lock"]').forEach(radio => {
radio.addEventListener('change', saveVersionLock);
});
// 確保在頁面載入後初始化版本設定
window.addEventListener('load', initializeVersion);
// 由於 Google Handwriting Input API 是異步載入,我們需要一個回調函數
window.googleImeLoaded = function() {
console.log('Google Handwriting Input API 載入完成。');
// 重新應用設定以確保手寫功能正確啟用
applyVersionSettings();
};
// 檢查 Google Handwriting Input API 是否已載入
if (window.google && window.google.ime) {
window.googleImeLoaded();
} else {
// 監聽 API 載入事件 (如果 API 支援)
// 由於我們是使用 <script async> 載入,這裡可能需要一個更可靠的監聽機制
// 暫時依賴 window.googleImeLoaded 函數,並在 applyVersionSettings 中處理未載入的情況
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
function showView(viewId) {
[mainMenu, wordManagementView, learningView, gradeReportView].forEach(view => {
view.classList.add('hidden');
});
document.getElementById(viewId).classList.remove('hidden');
// 確保在切換到學習介面時,版本設定被正確應用
if (viewId === 'learning-view') {
applyVersionSettings();
}
}
function saveWordsToStorage() {
localStorage.setItem('flashcardsWords', JSON.stringify(words));
localStorage.setItem('flashcardsReports', JSON.stringify(gradeReports));
localStorage.setItem('flashcardsTitle', mainTitle.textContent);
localStorage.setItem('flashcardsHighScore', highScore);
localStorage.setItem('completionStatus', JSON.stringify(completionStatus));
}
function loadWordsFromStorage() {
const storedWords = localStorage.getItem('flashcardsWords');
if (storedWords) {
words = JSON.parse(storedWords);
}
const storedReports = localStorage.getItem('flashcardsReports');
if (storedReports) {
gradeReports = JSON.parse(storedReports);
}
const storedTitle = localStorage.getItem('flashcardsTitle');
if (storedTitle) {
mainTitle.textContent = storedTitle;
titleInput.value = storedTitle;
} else {
mainTitle.textContent = '單字閃卡';
titleInput.value = '單字閃卡';
}
const storedHighScore = localStorage.getItem('flashcardsHighScore');
if (storedHighScore) {
highScore = parseInt(storedHighScore, 10);
highscoreDisplay.textContent = highScore;
}
const storedCompletion = localStorage.getItem('completionStatus');
if (storedCompletion) {
completionStatus = JSON.parse(storedCompletion);
}
// 【新增】載入版本鎖定設定
const storedLock = localStorage.getItem('versionLock');
if (storedLock === 'pc' || storedLock === 'mobile') {
versionLock = storedLock;
} else {
versionLock = 'auto';
}
// 處理 URL 參數中的單字資料
const urlParams = new URLSearchParams(window.location.search);
const encodedData = urlParams.get('data');
const urlTitle = urlParams.get('title');
if (encodedData) {
try {
const compressed = atob(encodedData);
const decompressed = pako.inflate(compressed.split('').map(c => c.charCodeAt(0)), { to: 'string' });
const urlWords = JSON.parse(decompressed);
if (urlWords.length > 0) {
// 僅在本地沒有單字時才載入 URL 中的單字
if (words.length === 0) {
words = urlWords;
saveWordsToStorage(); // 儲存從 URL 載入的單字
}
if (urlTitle) {
mainTitle.textContent = decodeURIComponent(urlTitle);
titleInput.value = decodeURIComponent(urlTitle);
localStorage.setItem('flashcardsTitle', mainTitle.textContent);
}
}
} catch (e) {
console.error("解碼 URL 資料失敗:", e);
}
}
renderWordList();
updateCompletionUI();
preloadAudioFiles();
}
function updateCompletionUI() {
document.querySelectorAll('.mode-btn').forEach(btn => {
const mode = btn.dataset.mode;
const crown = btn.querySelector('.crown-icon');
if (crown) {
if (completionStatus[mode]) {
crown.classList.remove('hidden');
} else {
crown.classList.add('hidden');
}
}
});
}
function renderWordList() {
wordList.innerHTML = '';
wordCount.textContent = words.length;
words.forEach((word, index) => {
const item = document.createElement('div');
item.className = 'list-item p-4 bg-white rounded-xl shadow-md flex justify-between items-center transition-all duration-300';
item.innerHTML = `
<div class="flex-1 mr-4">
<p class="text-lg font-bold text-gray-800">${index + 1}. ${word.english}</p>
<p class="text-gray-600">${word.chinese}</p>
${word.sentence ? `<p class="text-sm text-blue-500 mt-1">例句: ${word.sentence.en.replace('___', `(${word.sentence.answer})`)}</p>` : ''}
<p class="text-xs text-gray-400 mt-1">熟練度: ${word.proficiency} | 錯: ${word.incorrectCount}</p>
</div>
<button data-index="${index}" class="delete-word-btn bg-red-500 text-white p-2 rounded-full hover:bg-red-600 transition-colors flex-shrink-0">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button>
`;
wordList.appendChild(item);
});
document.querySelectorAll('.delete-word-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const index = parseInt(e.currentTarget.dataset.index, 10);
showDeleteConfirmation(index);
});
});
}
function showDeleteConfirmation(index) {
const word = words[index];
wordToDeleteSpan.textContent = `${word.english} (${word.chinese})`;
confirmDeleteModal.classList.remove('hidden');
confirmDeleteBtn.onclick = () => {
// 執行刪除動畫
const itemToRemove = wordList.children[index];
itemToRemove.classList.add('removing');
setTimeout(() => {
words.splice(index, 1);
saveWordsToStorage();
renderWordList();
confirmDeleteModal.classList.add('hidden');
preloadAudioFiles(); // 刪除後重新預載音檔
}, 300);
};
cancelDeleteBtn.onclick = () => {
confirmDeleteModal.classList.add('hidden');
};
}
function updateProgressBar() {
const total = wordsForCurrentMode.length;
const remaining = quizQueue.length;
const completed = total - remaining;
const percentage = total > 0 ? (completed / total) * 100 : 0;
progressBar.style.width = `${percentage}%`;
progressText.textContent = `進度: ${completed}/${total}`;
mistakeCount.textContent = `錯誤: ${quizIncorrectCount}`;
}
function updateHintButtonVisibility() {
const isQuiz = !['review', 'speed', ''].includes(currentMode);
const isCloze = currentMode === 'sentence-cloze';
if (isQuiz && !isCloze) {
hintBtn.classList.remove('hidden');
} else {
hintBtn.classList.add('hidden');
}
}
function displayCard() {
answerInput.value = '';
answerInput.focus();
const isReview = currentMode === 'review';
const isSpeed = currentMode === 'speed';
let card;
if (isReview) {
card = wordsForCurrentMode[currentCardIndex];
reviewNavContainer.classList.remove('hidden');
quizContainer.classList.add('hidden');
flashcardContainer.classList.remove('flipped');
prevCardBtn.disabled = currentCardIndex === 0;
nextCardBtn.disabled = currentCardIndex === wordsForCurrentMode.length - 1;
cardZh.textContent = card.chinese;
cardEn.textContent = card.english;
// 複習模式不顯示問題區塊
questionText.textContent = '';
speakBtn.classList.add('hidden');
} else if (isSpeed) {
// 極速挑戰模式
card = currentSpeedCard;
reviewNavContainer.classList.add('hidden');
quizContainer.classList.remove('hidden');
flashcardContainer.classList.add('hidden');
// 隨機決定題目類型
const questionTypes = ['zh-en', 'en-zh', 'listen'];
currentSpeedQuestionType = questionTypes[Math.floor(Math.random() * questionTypes.length)];
let question;
let isListen = false;
switch (currentSpeedQuestionType) {
case 'zh-en':
question = `中翻英: ${card.chinese}`;
break;
case 'en-zh':
question = `英翻中: ${card.english}`;
break;
case 'listen':
question = `聽力測驗 (請聽音檔)`;
isListen = true;
break;
}
questionText.textContent = question;
speakBtn.classList.toggle('hidden', !isListen);
if (isListen) {
speakWord(card.english);
}
} else {
// 測驗模式
card = quizQueue[0];
reviewNavContainer.classList.add('hidden');
quizContainer.classList.remove('hidden');
flashcardContainer.classList.add('hidden');
let question;
let isListen = false;
switch (currentMode) {
case 'zh-en':
case 'hard':
question = `中翻英: ${card.chinese}`;
break;
case 'en-zh':
question = `英翻中: ${card.english}`;
break;
case 'listen':
question = `聽力測驗 (請聽音檔)`;
isListen = true;
break;
case 'sentence-cloze':
question = `克漏字: ${card.sentence.en.replace('___', ' (___) ')} (${card.sentence.zh})`;
break;
}
questionText.textContent = question;
speakBtn.classList.toggle('hidden', !isListen);
if (isListen) {
speakWord(card.english);
}
updateProgressBar();
}
// 確保在顯示新卡片時,版本設定被正確應用
applyVersionSettings();
}
function setupLearningView(mode) {
if (words.length === 0) {
alert("請先新增單字!");
return;
}
currentMode = mode;
quizCompletionMessage.classList.add('hidden');
// 確保輸入框在開始測驗前是可用的
answerInput.disabled = false;
submitAnswerBtn.disabled = false;
const isReview = mode === 'review';
const isSpeed = mode === 'speed';
const isQuiz = !isReview && !isSpeed;
quizContainer.classList.remove('hidden');
reviewNavContainer.classList.toggle('hidden', !isReview);
progressBarContainer.classList.toggle('hidden', isReview || isSpeed);
speedHud.classList.toggle('hidden', !isSpeed);
hudPlaceholder.classList.toggle('hidden', isSpeed);
quizCompletionMessage.classList.add('hidden');
const start = parseInt(startRangeInput.value, 10);
const end = parseInt(endRangeInput.value, 10);
let wordPool;
if (!isNaN(start) && !isNaN(end) && start > 0 && end >= start && end <= words.length) {
wordPool = words.slice(start - 1, end);
} else {
wordPool = [...words];
}
// 【修改】過濾克漏字模式的單字,必須包含 sentence 和 answer
if (mode === 'sentence-cloze') {
wordsForCurrentMode = wordPool.filter(w => w.sentence && w.sentence.en && w.sentence.zh && w.sentence.answer);
if (wordsForCurrentMode.length === 0) {
alert("題庫中沒有附帶完整例句(包含克漏字答案)的單字可供此模式使用。");
return;
}
} else if (mode === 'review') {
wordsForCurrentMode = wordPool;
} else if (randomQuestionsCheckbox.checked) {
const randomCount = parseInt(randomQuestionsCountInput.value, 10);
if (!isNaN(randomCount) && randomCount > 0 && randomCount <= wordPool.length) {
wordsForCurrentMode = shuffleArray([...wordPool]).slice(0, randomCount);
} else {
alert('請輸入有效的隨機題數。題數不能為零或超過所選範圍的單字總數。');
return;
}
} else {
wordsForCurrentMode = wordPool;
}
if (wordsForCurrentMode.length === 0) {
alert("選定的範圍內沒有單字,請重新選擇或新增。");
return;
}
if (mode === 'hard') {
const hardWords = words.filter(w => w.incorrectCount > 0)
.sort((a, b) => b.incorrectCount - a.incorrectCount).slice(0, 10);
if (hardWords.length === 0) {
alert('太棒了!目前沒有需要特別複習的困難單字。'); return;
}
wordsForCurrentMode = hardWords;
quizQueue = shuffleArray([...wordsForCurrentMode]);
} else if (isQuiz) {
quizQueue = shuffleArray([...wordsForCurrentMode]);
}
if (isQuiz) {
quizStartTime = Date.now();
}
quizIncorrectCount = 0;
updateHintButtonVisibility();
if (isSpeed) startSpeedChallenge(wordsForCurrentMode);
else { currentCardIndex = 0; displayCard(); }
showView('learning-view');
}
const checkAnswer = () => {
const userAnswer = answerInput.value.trim();
if (!userAnswer) return;
const isSpeed = currentMode === 'speed';
const isReview = currentMode === 'review';
if (isSpeed && timeLeft <= 0) return;
const card = isReview ? wordsForCurrentMode[currentCardIndex] : (isSpeed ? currentSpeedCard : quizQueue[0]);
if (!card) return;
const wordIndex = words.findIndex(w => w.english === card.english && w.chinese === card.chinese);
let correctAnswer, answerLang;
if (isReview) {
const isFlipped = flashcardContainer.classList.contains('flipped');
let rawAnswer = isFlipped ? card.english : card.chinese;
correctAnswer = rawAnswer.replace(/[\((\[【].*?[\))\]】]/g, "").trim();
answerLang = isFlipped ? 'en' : 'zh';
} else {
let effectiveMode = isSpeed ? currentSpeedQuestionType : (currentMode === 'hard' ? 'zh-en' : currentMode);
let rawAnswer;
switch (effectiveMode) {
// 【修改】此處是核心邏輯修改
case 'sentence-cloze':
// 直接使用 sentence.answer 作為正確答案
rawAnswer = card.sentence.answer;
answerLang = 'en';
break;
case 'zh-en': case 'listen':
rawAnswer = card.english;
answerLang = 'en';
break;
case 'en-zh':
rawAnswer = card.chinese;
answerLang = 'zh';
break;
}
// 如果 rawAnswer 存在才進行處理
if (rawAnswer) {
correctAnswer = rawAnswer.replace(/[\((\[【].*?[\))\]】]/g, "").trim();
} else {
// 預防性程式碼,如果找不到答案就判定為錯
correctAnswer = `__NO_ANSWER_DEFINED__${Date.now()}`;
}
}
let isCorrect;
if (answerLang === 'en') {
const normalize = (str) => {
return str.toLowerCase()
.replace(/\s*\.{3}\s*/g, '') // 處理 '...',移除點及周圍空格
.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, "") // 移除標點符號
.replace(/\s+/g, ''); // 移除所有空格
};
isCorrect = normalize(userAnswer) === normalize(correctAnswer);
} else if (answerLang === 'zh') {
const possibleAnswers = correctAnswer.split(/[;;]/).map(a => a.trim()).filter(a => a);
isCorrect = possibleAnswers.some(ans => ans === userAnswer);
} else {
isCorrect = userAnswer === correctAnswer;
}
if (isCorrect) {
correctAudio.play();
answerInput.classList.remove('border-red-500');
answerInput.classList.add('border-green-500');
if (!isReview) {
// 更新熟練度
if (wordIndex !== -1) {
words[wordIndex].proficiency = (words[wordIndex].proficiency || 0) + 1;
words[wordIndex].incorrectCount = Math.max(0, (words[wordIndex].incorrectCount || 0) - 1);
}
if (isSpeed) {
speedScore++;
speedScoreDisplay.textContent = speedScore;
startNextSpeedCard();
} else {
quizQueue.shift();
if (quizQueue.length === 0) {
endQuiz();
} else {
displayCard();
}
}
}
} else {
wrongAudio.play();
answerInput.classList.remove('border-green-500');
answerInput.classList.add('border-red-500');
// 晃動效果
quizContainer.classList.add('shake');
setTimeout(() => quizContainer.classList.remove('shake'), 820);
if (!isReview) {
quizIncorrectCount++;
// 更新錯誤次數
if (wordIndex !== -1) {
words[wordIndex].incorrectCount = (words[wordIndex].incorrectCount || 0) + 1;
}
if (isSpeed) {
// 極速挑戰答錯不扣分,但會換下一題
startNextSpeedCard();
} else {
// 測驗模式答錯,將題目移到隊列尾部
const failedCard = quizQueue.shift();
quizQueue.push(failedCard);
displayCard();
}
}
}
// 清空輸入框
answerInput.value = '';
answerInput.focus();
if (wordIndex !== -1) {
saveWordsToStorage();
}
};
function endQuiz() {
const total = wordsForCurrentMode.length;
const mistakes = quizIncorrectCount;
const elapsedTime = Math.round((Date.now() - quizStartTime) / 1000);
answerInput.disabled = true;
submitAnswerBtn.disabled = true;
quizSummary.innerHTML = `
<p>總題數: ${total}</p>
<p>錯誤次數: <span class="font-bold text-red-500">${mistakes}</span></p>
<p>花費時間: ${elapsedTime} 秒</p>
`;
quizCompletionMessage.classList.remove('hidden');
// 儲存報告
const reportData = {
total: total,
attempted: total,
mistakes: mistakes,
time: elapsedTime,
date: new Date().toISOString(),
status: 'completed'
};
const history = gradeReports[currentMode] || [];
history.push(reportData);
gradeReports[currentMode] = history;
saveWordsToStorage();
// 檢查是否完成
if (mistakes === 0) {
completionStatus[currentMode] = true;
updateCompletionUI();
confetti({
particleCount: 100,
spread: 70,
origin: { y: 0.6 }
});
}
}
function startSpeedChallenge(pool) {
speedScore = 0;
timeLeft = 60;
speedScoreDisplay.textContent = speedScore;
speedTimerDisplay.textContent = timeLeft;
// 隨機選取第一張卡片
currentSpeedCard = pool[Math.floor(Math.random() * pool.length)];
displayCard();
// 啟動計時器
clearInterval(timerInterval);
timerInterval = setInterval(() => {
timeLeft--;
speedTimerDisplay.textContent = timeLeft;
if (timeLeft <= 10) {
speedTimerDisplay.classList.add('timer-highlight');
} else {
speedTimerDisplay.classList.remove('timer-highlight');
}
if (timeLeft <= 0) {
clearInterval(timerInterval);
endSpeedChallenge();
}
}, 1000);
}
function startNextSpeedCard() {
// 隨機選取下一張卡片
currentSpeedCard = wordsForCurrentMode[Math.floor(Math.random() * wordsForCurrentMode.length)];
displayCard();
}
function endSpeedChallenge() {
answerInput.disabled = true;
submitAnswerBtn.disabled = true;
if (speedScore > highScore) {
highScore = speedScore;
localStorage.setItem('flashcardsHighScore', highScore);
highscoreDisplay.textContent = highScore;
confetti({
particleCount: 150,
spread: 100,
origin: { y: 0.6 }
});
}
quizSummary.innerHTML = `
<p>最終得分: <span class="text-3xl font-bold text-amber-500">${speedScore}</span></p>
<p class="mt-2">歷史最高分: ${highScore}</p>
`;
quizCompletionMessage.classList.remove('hidden');
// 儲存報告
const reportData = {
total: wordsForCurrentMode.length, // 這裡 total 意義不大,用 pool size
attempted: speedScore, // 答對題數
mistakes: 0, // 極速挑戰不計入錯誤
time: 60,
score: speedScore,
date: new Date().toISOString(),
status: 'completed'
};
const history = gradeReports[currentMode] || [];
history.push(reportData);
gradeReports[currentMode] = history;
saveWordsToStorage();
}
function speakWord(word) {
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(word);
utterance.lang = 'en-US';
utterance.rate = 0.8; // 稍微慢一點
speakBtn.disabled = true;
utterance.onend = () => {
speakBtn.disabled = false;
};
utterance.onerror = () => {
speakBtn.disabled = false;
};
window.speechSynthesis.speak(utterance);
} else {
alert("您的瀏覽器不支援語音合成。");
}
}
// --- PDF/AI 解析相關函式 ---
function updateAIProgressBar(percentage) {
aiProgressBarInner.style.width = `${percentage}%`;
}
function loadApiKey() {
const storedKey = localStorage.getItem('geminiApiKey');
if (storedKey) {
apiKeyInput.value = storedKey;
}
}
function saveApiKey() {
const key = apiKeyInput.value.trim();
if (key) {
localStorage.setItem('geminiApiKey', key);
alert('API Key 已儲存!');
} else {
localStorage.removeItem('geminiApiKey');
alert('API Key 已清除!');
}
}
async function callGeminiToParseText(text) {
const apiKey = apiKeyInput.value.trim();
if (!apiKey) {
parseStatus.textContent = '錯誤:請先輸入您的 Gemini API 金鑰並儲存。';
parsePdfBtn.disabled = false;
aiProgressContainer.classList.add('hidden');
return;
}
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:generateContent?key=${apiKey}`;
const prompt = `
請分析以下從英文單字學習講義擷取的文字。你的任務是辨識出每一個單字條目,並為每個條目提取以下資訊:
1. "english": 完整的英文單字,包含詞性標註,例如 "yesterday (adv.)" 或 "study (v.)"。
2. "chinese": 對應的中文翻譯。
3. "sentence": 一個包含例句的物件。這個物件應該有以下三個屬性:
- "en": 英文例句。請將該條目的主要英文單字(不含詞性)替換成 "___"。
- "zh": 完整的中文例句翻譯。
- "answer": 在 "en" 屬性的 "___" 空格中,文法正確的答案。例如,如果單字是 "watch",例句是 "He ___ TV.",那 "answer" 就應該是 "watches"。
規則:
- 如果一個單字條目有多個例句,請只選擇第一個或最能代表該單字用法的例句。
- 如果同一個單字因為詞性不同而有多個條目(例如 "study (v.)" 和 "study (n.)"),請將它們視為獨立的條目,並各自尋找一個代表性的例句。
- 如果一個單字條目沒有提供例句,則省略 "sentence" 物件。
請將所有解析出的單字條目,以一個 JSON 陣列的格式回傳。每一個陣列中的物件都應該符合上述的結構。
這是你要分析的文字內容:
---
${text}
---
`;
const payload = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
responseMimeType: "application/json",
responseSchema: {
type: "ARRAY",
items: {
type: "OBJECT",
properties: {
english: { type: "STRING" },
chinese: { type: "STRING" },
sentence: {
type: "OBJECT",
properties: {
en: { type: "STRING" },
zh: { type: "STRING" },
answer: { type: "STRING" } // 新增 answer 欄位
}
}
}
}
}
}
};
try {
const response = await fetch(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorBody = await response.json();
console.error('API Error:', errorBody);
throw new Error(`API 請求失敗,狀態碼:${response.status}. 錯誤訊息: ${errorBody.error.message}`);
}
const result = await response.json();
if (result.candidates && result.candidates.length > 0 && result.candidates[0].content?.parts?.[0]?.text) {
const jsonText = result.candidates[0].content.parts[0].text;
parsedWordsFromAI = JSON.parse(jsonText);
updateAIProgressBar(100);
parseStatus.textContent = 'AI 分析完成!請確認結果。';
showParseConfirmation(parsedWordsFromAI);
setTimeout(() => {
aiProgressContainer.classList.add('hidden');
parseStatus.textContent = '';
}, 2000);
} else {
throw new Error("從 AI 收到無效的回應格式。");
}
} catch (error) {
console.error('AI 解析失敗:', error);
parseStatus.textContent = `AI 解析失敗: ${error.message}`;
aiProgressContainer.classList.add('hidden');
} finally {
parsePdfBtn.disabled = false;
}
}
function showParseConfirmation(parsedWords) {
parseResultsContainer.innerHTML = '';
selectAllCheckbox.checked = true;
parsedWords.forEach((word, index) => {
const item = document.createElement('div');
item.className = 'p-3 bg-gray-50 rounded-md border flex items-start';
item.innerHTML = `
<input type="checkbox" checked id="word-checkbox-${index}" data-index="${index}" class="word-checkbox h-5 w-5 rounded text-indigo-600 focus:ring-indigo-500 border-gray-300 mr-4 mt-1">
<div class="flex-1">
<p><strong>英文:</strong> ${word.english}</p>
<p><strong>中文:</strong> ${word.chinese}</p>
${word.sentence ? `
<div class="mt-2 pt-2 border-t border-gray-200">
<p class="text-sm text-gray-600"><strong>例句 (英):</strong> ${word.sentence.en}</p>
<p class="text-sm text-gray-600"><strong>例句 (中):</strong> ${word.sentence.zh}</p>
${word.sentence.answer ? `<p class="text-sm text-blue-700 font-semibold"><strong>克漏字答案:</strong> ${word.sentence.answer}</p>` : ''}
</div>
` : ''}
</div>
`;
parseResultsContainer.appendChild(item);
});
parseConfirmModal.classList.remove('hidden');
}
// --- 音檔預載相關函式 ---
let audioPreloadQueue = [];
let preloadedAudio = {};
let totalAudioCount = 0;
let loadedAudioCount = 0;
function preloadAudioFiles() {
audioPreloadQueue = [];
preloadedAudio = {};
// 收集所有需要語音的單字
const wordsToSpeak = new Set();
words.forEach(word => {
if (word.english) {
wordsToSpeak.add(word.english.replace(/[\((\[【].*?[\))\]】]/g, "").trim());
}
});
audioPreloadQueue = Array.from(wordsToSpeak);
totalAudioCount = audioPreloadQueue.length;
loadedAudioCount = 0;
if (totalAudioCount === 0) {
audioPreloadContainer.classList.add('hidden');
return;
}
audioPreloadContainer.classList.remove('hidden');
audioPreloadText.textContent = `正在預載語音檔案... (0/${totalAudioCount})`;
audioPreloadBar.style.width = '0%';
preloadNextAudio();
}
function preloadNextAudio() {
if (audioPreloadQueue.length === 0) {
audioPreloadText.textContent = `語音檔案預載完成! (${totalAudioCount}/${totalAudioCount})`;
setTimeout(() => audioPreloadContainer.classList.add('hidden'), 2000);
return;
}
const word = audioPreloadQueue.shift();
const url = `https://api.dictionaryapi.dev/api/v2/entries/en/${word}`;
fetch(url)
.then(response => response.json())
.then(data => {
let audioUrl = null;
if (Array.isArray(data) && data.length > 0) {
for (const meaning of data) {
if (meaning.phonetics && meaning.phonetics.length > 0) {
for (const phonetic of meaning.phonetics) {
if (phonetic.audio) {
audioUrl = phonetic.audio;
break;
}
}
}
if (audioUrl) break;
}
}
if (audioUrl) {
const audio = new Audio(audioUrl);
audio.preload = 'auto';
audio.oncanplaythrough = () => {
preloadedAudio[word] = audio;
loadedAudioCount++;
updateAudioPreloadProgress();
preloadNextAudio();
};
audio.onerror = () => {
// 載入失敗也算完成,繼續下一個
loadedAudioCount++;
updateAudioPreloadProgress();
preloadNextAudio();
};
} else {
// 找不到音檔也算完成,繼續下一個
loadedAudioCount++;
updateAudioPreloadProgress();
preloadNextAudio();
}
})
.catch(error => {
console.error(`預載 ${word} 音檔失敗:`, error);
loadedAudioCount++;
updateAudioPreloadProgress();
preloadNextAudio();
});
}
function updateAudioPreloadProgress() {
const percentage = totalAudioCount > 0 ? (loadedAudioCount / totalAudioCount) * 100 : 100;
audioPreloadBar.style.width = `${percentage}%`;
audioPreloadText.textContent = `正在預載語音檔案... (${loadedAudioCount}/${totalAudioCount})`;
}
function speakWord(word) {
const cleanWord = word.replace(/[\((\[【].*?[\))\]】]/g, "").trim();
if (preloadedAudio[cleanWord]) {
preloadedAudio[cleanWord].currentTime = 0;
preloadedAudio[cleanWord].play();
speakBtn.disabled = true;
preloadedAudio[cleanWord].onended = () => {
speakBtn.disabled = false;
};
preloadedAudio[cleanWord].onerror = () => {
speakBtn.disabled = false;
};
return;
}
// 如果沒有預載,則嘗試即時獲取
const url = `https://api.dictionaryapi.dev/api/v2/entries/en/${cleanWord}`;
speakBtn.disabled = true;
fetch(url)
.then(response => response.json())
.then(data => {
let audioUrl = null;
if (Array.isArray(data) && data.length > 0) {
for (const meaning of data) {
if (meaning.phonetics && meaning.phonetics.length > 0) {
for (const phonetic of meaning.phonetics) {
if (phonetic.audio) {
audioUrl = phonetic.audio;
break;
}
}
}
if (audioUrl) break;
}
}
if (audioUrl) {
const audio = new Audio(audioUrl);
audio.play();
audio.onended = () => { speakBtn.disabled = false; };
audio.onerror = () => { speakBtn.disabled = false; };
} else {
// 如果找不到音檔,使用瀏覽器內建的語音合成
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(cleanWord);
utterance.lang = 'en-US';
utterance.rate = 0.8;
window.speechSynthesis.speak(utterance);
utterance.onend = () => { speakBtn.disabled = false; };
utterance.onerror = () => { speakBtn.disabled = false; };
} else {
alert("找不到音檔且瀏覽器不支援語音合成。");
speakBtn.disabled = false;
}
}
})
.catch(error => {
console.error('即時獲取音檔失敗:', error);
// 失敗後嘗試使用瀏覽器內建的語音合成
if ('speechSynthesis' in window) {
const utterance = new SpeechSynthesisUtterance(cleanWord);
utterance.lang = 'en-US';
utterance.rate = 0.8;
window.speechSynthesis.speak(utterance);
utterance.onend = () => { speakBtn.disabled = false; };
utterance.onerror = () => { speakBtn.disabled = false; };
} else {
alert("即時獲取音檔失敗且瀏覽器不支援語音合成。");
speakBtn.disabled = false;
}
});
}
// --- 事件監聽 ---
// 確保在頁面載入後初始化版本設定
window.addEventListener('load', initializeVersion);
document.querySelectorAll('.mode-btn[data-mode]').forEach(btn => {
btn.addEventListener('click', () => setupLearningView(btn.dataset.mode));
});
resetCrownsBtn.addEventListener('click', () => {
completionStatus = {};
localStorage.removeItem('completionStatus');
updateCompletionUI();
highScore = 0;
localStorage.removeItem('flashcardsHighScore');
highscoreDisplay.textContent = highScore;
const originalText = resetCrownsBtn.textContent;
resetCrownsBtn.textContent = '已重置!';
resetCrownsBtn.disabled = true;
setTimeout(() => {
resetCrownsBtn.textContent = originalText;
resetCrownsBtn.disabled = false;
}, 2000);
});
backToMenuBtn.addEventListener('click', () => {
clearInterval(timerInterval);
const isQuiz = !['review', 'speed', ''].includes(currentMode);
if (isQuiz && quizStartTime > 0) {
const attempted = wordsForCurrentMode.length - quizQueue.length;
if (attempted > 0) {
const elapsedTime = Math.round((Date.now() - quizStartTime) / 1000);
const reportData = {
total: wordsForCurrentMode.length,
attempted: attempted,
mistakes: quizIncorrectCount,
time: elapsedTime,
date: new Date().toISOString(),
status: 'abandoned'
};
const history = gradeReports[currentMode] || [];
history.push(reportData);
gradeReports[currentMode] = history;
saveWordsToStorage();
}
}
currentMode = '';
showView('main-menu');
});
flipCardBtn.addEventListener('click', () => {
flashcardContainer.classList.toggle('flipped');
});
prevCardBtn.addEventListener('click', () => {
if (currentCardIndex > 0) {
currentCardIndex--;
flashcardContainer.classList.remove('flipped');
displayCard();
}
});
nextCardBtn.addEventListener('click', () => {
if (currentCardIndex < wordsForCurrentMode.length - 1) {
currentCardIndex++;
flashcardContainer.classList.remove('flipped');
displayCard();
}
});
submitAnswerBtn.addEventListener('click', checkAnswer);
// 允許按 Enter 提交答案
answerInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
checkAnswer();
}
});
hintBtn.addEventListener('click', () => {
const card = quizQueue[0];
if (!card) return;
let answer;
switch (currentMode) {
case 'zh-en':
case 'listen':
case 'hard':
answer = card.english.replace(/[\((\[【].*?[\))\]】]/g, "").trim();
break;
case 'en-zh':
answer = card.chinese.replace(/[\((\[【].*?[\))\]】]/g, "").trim();
break;
default:
return;
}
if (answer.length > 0) {
const hint = answer.charAt(0) + '...';
answerInput.value = hint;
answerInput.focus();
}
});
// --- 單字管理事件 ---
manageWordsBtn.addEventListener('click', () => {
loadApiKey(); // 載入 API Key
showView('word-management-view');
});
backToModesFromManageBtn.addEventListener('click', () => {
showView('main-menu');
});
addWordBtn.addEventListener('click', () => {
const english = newEnglish.value.trim();
const chinese = newChinese.value.trim();
const sentenceEn = newSentenceEn.value.trim();
const sentenceZh = newSentenceZh.value.trim();
const sentenceAnswer = newSentenceAnswer.value.trim();
if (!english || !chinese) {
alert("英文單字和中文翻譯為必填項。");
return;
}
const newWord = {
english: english,
chinese: chinese,
proficiency: 0,
incorrectCount: 0
};
if (sentenceEn && sentenceZh && sentenceAnswer) {
newWord.sentence = {
en: sentenceEn,
zh: sentenceZh,
answer: sentenceAnswer
};
}
words.push(newWord);
saveWordsToStorage();
renderWordList();
preloadAudioFiles(); // 新增單字後重新預載音檔
// 清空輸入框
newEnglish.value = '';
newChinese.value = '';
newSentenceEn.value = '';
newSentenceZh.value = '';
newSentenceAnswer.value = '';
alert("單字新增成功!");
});
clearAllWordsBtn.addEventListener('click', () => {
confirmClearModal.classList.remove('hidden');
});
cancelClearBtn.addEventListener('click', () => {
confirmClearModal.classList.add('hidden');
});
confirmClearBtn.addEventListener('click', () => {
words = [];
gradeReports = {};
completionStatus = {};
highScore = 0;
localStorage.removeItem('flashcardsWords');
localStorage.removeItem('flashcardsReports');
localStorage.removeItem('flashcardsTitle');
localStorage.removeItem('flashcardsHighScore');
localStorage.removeItem('completionStatus');
mainTitle.textContent = '單字閃卡';
titleInput.value = '單字閃卡';
highscoreDisplay.textContent = 0;
renderWordList();
updateCompletionUI();
confirmClearModal.classList.add('hidden');
preloadAudioFiles(); // 清除後重置音檔預載
alert("所有單字和紀錄已清除!");
});
saveTitleBtn.addEventListener('click', () => {
const newTitle = titleInput.value.trim() || '單字閃卡';
mainTitle.textContent = newTitle;
localStorage.setItem('flashcardsTitle', newTitle);
const originalText = saveTitleBtn.textContent;
saveTitleBtn.textContent = '標題已儲存!';
setTimeout(() => {
saveTitleBtn.textContent = originalText;
}, 1500);
});
// --- 分享功能事件 ---
shareGameBtn.addEventListener('click', async () => {
if (words.length === 0) {
alert("請先新增單字才能分享!");
return;
}
// 1. 壓縮單字資料
const jsonString = JSON.stringify(words);
const compressed = pako.deflate(jsonString, { to: 'string' });
const encodedData = btoa(compressed);
// 2. 取得標題
const title = encodeURIComponent(mainTitle.textContent);
// 3. 取得版本鎖定設定
const lockSetting = document.querySelector('input[name="version-lock"]:checked').value;
let versionParam = '';
if (lockSetting !== 'auto') {
versionParam = `&version=${lockSetting}`;
}
// 4. 組合分享連結
const shareLink = `${window.location.origin}${window.location.pathname}?title=${title}&data=${encodedData}${versionParam}`;
shareLinkInput.value = shareLink;
// 5. 產生 QR Code
qrcodeContainer.innerHTML = '';
const qr = qrcode(0, 'M');
qr.addData(shareLink);
qr.make();
const imgTag = qr.createImgTag(6, 10); // 6: 模組大小, 10: 邊界
qrcodeContainer.innerHTML = imgTag;
shareModal.classList.remove('hidden');
});
closeShareModalBtn.addEventListener('click', () => {
shareModal.classList.add('hidden');
});
copyLinkBtn.addEventListener('click', () => {
shareLinkInput.select();
shareLinkInput.setSelectionRange(0, 99999); // For mobile devices
document.execCommand('copy');
const originalText = copyLinkBtn.textContent;
copyLinkBtn.textContent = '已複製!';
setTimeout(() => {
copyLinkBtn.textContent = originalText;
}, 1500);
});
// --- 成績報告事件 ---
gradeReportBtn.addEventListener('click', () => {
renderGradeReport();
showView('grade-report-view');
});
backToModesFromReportBtn.addEventListener('click', () => {
showView('main-menu');
});
function renderGradeReport() {
const formatTime = (seconds) => {
const min = Math.floor(seconds / 60);
const sec = seconds % 60;
return `${min}${sec}秒`;
};
const renderModeReport = (container, mode, title) => {
container.innerHTML = '';
const reports = gradeReports[mode] || [];
if (reports.length === 0) {
container.innerHTML = `<p class="text-center text-gray-500">尚無 ${title} 測驗紀錄。</p>`;
return;
}
reports.sort((a, b) => new Date(b.date) - new Date(a.date));
reports.forEach((report, index) => {
const date = new Date(report.date).toLocaleString('zh-TW', {
year: 'numeric', month: 'numeric', day: 'numeric',
hour: '2-digit', minute: '2-digit'
});
let statusClass = 'bg-green-100 text-green-800';
let statusText = '完成';
let summary = '';
if (mode === 'speed') {
summary = `得分: <span class="font-bold text-amber-600">${report.score}</span>`;
statusText = '極速挑戰';
statusClass = 'bg-slate-100 text-slate-800';
} else {
if (report.status === 'abandoned') {
statusClass = 'bg-yellow-100 text-yellow-800';
statusText = '中途放棄';
summary = `已作答 ${report.attempted} 題,錯誤 ${report.mistakes} 次`;
} else {
summary = `總題數 ${report.total} 題,錯誤 <span class="font-bold text-red-500">${report.mistakes}</span> 次`;
if (report.mistakes === 0) {
statusClass = 'bg-green-100 text-green-800';
summary += ' (👑 完美)';
} else {
statusClass = 'bg-red-100 text-red-800';
}
}
summary += `, 耗時 ${formatTime(report.time)}`;
}
const item = document.createElement('div');
item.className = 'p-3 border rounded-lg bg-white shadow-sm';
item.innerHTML = `
<div class="flex justify-between items-center mb-1">
<span class="text-xs text-gray-500">${date}</span>
<span class="text-xs font-semibold px-2 py-0.5 rounded-full ${statusClass}">${statusText}</span>
</div>
<p class="text-sm text-gray-700">${summary}</p>
`;
container.appendChild(item);
});
};
renderModeReport(reportZhEn, 'zh-en', '中翻英');
renderModeReport(reportEnZh, 'en-zh', '英翻中');
renderModeReport(reportListen, 'listen', '聽力測驗');
renderModeReport(document.createElement('div'), 'speed', '極速挑戰'); // 極速挑戰的報告不顯示在主報告區,但會被儲存
}
// --- AI 解析流程事件 ---
saveApiKeyBtn.addEventListener('click', saveApiKey);
parsePdfBtn.addEventListener('click', async () => {
parsePdfBtn.disabled = true;
aiProgressContainer.classList.remove('hidden');
updateAIProgressBar(0);
parseStatus.textContent = '正在準備解析...';
let textToParse = textInput.value.trim();
if (pdfUploadInput.files.length > 0) {
const file = pdfUploadInput.files[0];
parseStatus.textContent = `正在讀取 PDF 檔案: ${file.name}...`;
updateAIProgressBar(10);
try {
const pdf = await pdfjsLib.getDocument(URL.createObjectURL(file)).promise;
let fullText = '';
for (let i = 1; i <= pdf.numPages; i++) {
const page = await pdf.getPage(i);
const textContent = await page.getTextContent();
fullText += textContent.items.map(item => item.str).join(' ') + '\n\n';
updateAIProgressBar(10 + (i / pdf.numPages) * 40); // 讀取進度到 50%
}
textToParse = fullText.trim();
parseStatus.textContent = 'PDF 讀取完成,開始 AI 分析...';
updateAIProgressBar(50);
} catch (error) {
console.error('PDF 讀取失敗:', error);
parseStatus.textContent = `PDF 讀取失敗: ${error.message}`;
parsePdfBtn.disabled = false;
aiProgressContainer.classList.add('hidden');
return;
}
}
if (textToParse) {
await callGeminiToParseText(textToParse);
} else {
parseStatus.textContent = '請上傳 PDF 檔案或貼上文字內容。';
parsePdfBtn.disabled = false;
aiProgressContainer.classList.add('hidden');
}
});
selectAllCheckbox.addEventListener('change', (e) => {
const isChecked = e.target.checked;
parseResultsContainer.querySelectorAll('.word-checkbox').forEach(checkbox => {
checkbox.checked = isChecked;
});
});
confirmParseBtn.addEventListener('click', () => {
const selectedWords = [];
const checkboxes = parseResultsContainer.querySelectorAll('.word-checkbox:checked');
checkboxes.forEach(checkbox => {
const index = parseInt(checkbox.dataset.index, 10);
selectedWords.push(parsedWordsFromAI[index]);
});
const newWords = selectedWords.map(word => ({
...word,
proficiency: 0,
incorrectCount: 0
}));
words.push(...newWords);
saveWordsToStorage();
renderWordList();
preloadAudioFiles(); // 解析完後也預載新的音檔
parseConfirmModal.classList.add('hidden');
parseStatus.textContent = `成功新增 ${selectedWords.length} 個單字!`;
pdfUploadInput.value = '';
textInput.value = '';
parsedWordsFromAI = [];
});
cancelParseBtn.addEventListener('click', () => {
parseConfirmModal.classList.add('hidden');
parseStatus.textContent = '操作已取消。';
pdfUploadInput.value = '';
textInput.value = '';
parsedWordsFromAI = [];
});
// --- 範圍選擇與隨機題數連動 ---
randomQuestionsCheckbox.addEventListener('change', (e) => {
randomQuestionsCountInput.disabled = !e.target.checked;
if (e.target.checked) {
randomQuestionsCountInput.focus();
}
});
// --- 初始化 ---
loadWordsFromStorage();
</script>
</body>
</html>