文件结构
文件夹
word.html
words.js
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>日語動詞 · 入力練習</title>
<style>
/* ===== 全局样式 ===== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
}
body {
background: linear-gradient(145deg, #f7f3f0 0%, #e8e0da 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
}
.container {
max-width: 1200px;
width: 100%;
background: rgba(255, 247, 242, 0.75);
backdrop-filter: blur(4px);
border-radius: 3.5rem;
padding: 2rem;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15), 0 6px 12px rgba(0, 0, 0, 0.05);
border: 1px solid rgba(255, 235, 225, 0.5);
display: flex;
gap: 2rem;
flex-wrap: wrap;
position: relative;
}
/* ===== 完成庆祝横幅 ===== */
#completionBanner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
justify-content: center;
align-items: center;
background: rgba(255, 215, 0, 0.15);
backdrop-filter: blur(6px);
border-radius: 3.5rem;
z-index: 10;
pointer-events: none;
animation: bannerFadeIn 0.6s ease;
}
#completionBanner.show {
display: flex;
pointer-events: auto;
}
@keyframes bannerFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.banner-content {
text-align: center;
padding: 2rem 3rem;
background: rgba(255, 255, 255, 0.85);
border-radius: 3rem;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
animation: celebratePulse 1.2s infinite alternate;
}
@keyframes celebratePulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 215, 0, 0.4);
}
100% {
transform: scale(1.02);
box-shadow: 0 0 40px 10px rgba(255, 215, 0, 0.2);
}
}
.banner-content h2 {
font-size: 3.5rem;
margin-bottom: 0.3rem;
background: linear-gradient(135deg, #f7971e, #ffd200);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: rainbow 2s linear infinite;
}
@keyframes rainbow {
0% {
filter: hue-rotate(0deg);
}
100% {
filter: hue-rotate(360deg);
}
}
.banner-content p {
font-size: 1.5rem;
color: #3e2e24;
margin-top: 0.5rem;
}
.banner-emoji {
font-size: 4rem;
display: block;
animation: floatEmoji 1.8s ease-in-out infinite;
}
@keyframes floatEmoji {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-15px) rotate(10deg);
}
}
/* ===== 左侧:主学习卡片 ===== */
.main-card {
flex: 1 1 60%;
min-width: 320px;
}
.card {
background: #fffcf9;
border-radius: 2.8rem;
padding: 2rem 1.8rem 1.8rem;
box-shadow: inset 0 1px 4px rgba(255, 255, 255, 0.8), 0 12px 24px -8px rgba(60, 40, 30, 0.2);
border: 1px solid #f2e3da;
transition: 0.2s;
}
h1 {
font-size: 2rem;
font-weight: 600;
color: #3e2e24;
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.4rem;
flex-wrap: wrap;
}
h1 small {
font-size: 0.9rem;
font-weight: 400;
color: #7f6b5c;
margin-left: auto;
background: #ede3db;
padding: 0.2rem 1.2rem;
border-radius: 40px;
}
.progress-badge {
display: flex;
justify-content: space-between;
color: #5b4a3c;
font-weight: 500;
padding: 0.2rem 0 0.8rem;
border-bottom: 2px dashed #dacfc7;
margin-bottom: 1.2rem;
font-size: 0.95rem;
}
.chinese-meaning {
font-size: 2.4rem;
font-weight: 600;
color: #2b1f18;
text-align: center;
padding-bottom: 0.8rem;
border-bottom: 3px dotted #dccfc6;
margin-bottom: 1.8rem;
min-height: 4rem;
}
.sentence-block {
background: #f5ede7;
border-radius: 2rem;
padding: 1.8rem 1.5rem;
margin-bottom: 1.5rem;
}
.sentence-line {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.3rem 0.6rem;
font-size: 1.6rem;
font-weight: 500;
color: #2a1f18;
}
.sentence-line .noun,
.sentence-line .particle {
background: #e7dbd2;
padding: 0.1rem 0.8rem;
border-radius: 60px;
font-size: 1.4rem;
}
.sentence-line .particle {
background: #dacbc0;
}
.verb-input-wrapper {
display: inline-flex;
align-items: center;
border-bottom: 4px solid #b6a093;
background: rgba(235, 215, 200, 0.15);
border-radius: 8px 8px 0 0;
padding: 0 0.2rem;
min-width: 130px;
transition: 0.2s;
}
.verb-input-wrapper.correct {
border-bottom-color: #4caf50;
background: #e8f5e9;
}
.verb-input-wrapper.wrong {
border-bottom-color: #e57373;
background: #ffebee;
}
#verbInput {
border: none;
background: transparent;
font-size: 1.8rem;
font-weight: 600;
color: #2b1f18;
padding: 0.1rem 0.3rem;
width: 120px;
outline: none;
}
#verbInput::placeholder {
color: #b6a093;
font-weight: 400;
font-size: 1.1rem;
}
.input-hint {
font-size: 0.8rem;
color: #7f6b5c;
margin-left: 0.2rem;
}
.feedback-message {
margin-top: 0.6rem;
font-size: 1rem;
font-weight: 500;
min-height: 2rem;
}
.feedback-message .correct-text {
color: #2e7d32;
}
.feedback-message .wrong-text {
color: #c62828;
}
.extra-phrase {
margin-top: 1rem;
padding-top: 0.8rem;
border-top: 1px solid #dccfc6;
font-size: 1.1rem;
color: #4f3c2e;
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
align-items: center;
}
.extra-phrase .label {
background: #e1d2c6;
padding: 0.1rem 1rem;
border-radius: 60px;
}
.extra-phrase .phrase-content {
font-weight: 500;
color: #2b1f18;
transition: all 0.3s;
}
.action-group {
display: flex;
flex-wrap: wrap;
gap: 0.6rem 1rem;
justify-content: center;
margin: 1.2rem 0 0.3rem;
}
.btn {
background: #f2e5dc;
border: none;
padding: 0.5rem 1.4rem;
border-radius: 60px;
font-size: 1rem;
font-weight: 550;
color: #3a2c22;
box-shadow: 0 4px 0 #cbb8ab, 0 6px 12px rgba(0, 0, 0, 0.05);
cursor: pointer;
transition: all 0.07s;
display: inline-flex;
align-items: center;
gap: 0.3rem;
border: 1px solid #efe1d6;
}
.btn:active {
transform: translateY(4px);
box-shadow: 0 1px 0 #b6a194;
}
.btn-primary {
background: #cfb7a8;
color: #2b1e16;
box-shadow: 0 4px 0 #9d887b;
}
.btn-success {
background: #8fb08a;
color: #1f2c1a;
box-shadow: 0 4px 0 #64815e;
}
.btn-outline {
background: transparent;
box-shadow: none;
border: 2px solid #cbb8ab;
color: #4b382b;
}
.btn:disabled {
opacity: 0.5;
pointer-events: none;
}
.counter-info {
display: flex;
justify-content: center;
gap: 1.5rem;
margin: 1.2rem 0 0.5rem;
font-size: 0.95rem;
color: #5e4b3b;
}
.counter-info span {
background: #e7dbd2;
padding: 0.1rem 1rem;
border-radius: 40px;
}
.footer-actions {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.6rem 1rem;
margin-top: 0.8rem;
}
.btn-icon {
background: #f0e3da;
padding: 0.3rem 1.2rem;
border-radius: 60px;
font-size: 0.9rem;
}
/* ===== 右侧:答题卡 ===== */
.answer-sheet-wrapper {
flex: 0 0 280px;
min-width: 200px;
background: #fffcf9;
border-radius: 2.8rem;
padding: 1.8rem 1.2rem;
box-shadow: inset 0 1px 4px rgba(255, 255, 255, 0.8), 0 12px 24px -8px rgba(60, 40, 30, 0.2);
border: 1px solid #f2e3da;
align-self: flex-start;
max-height: 80vh;
overflow-y: auto;
}
.answer-sheet-title {
font-size: 1rem;
font-weight: 600;
color: #5b4a3c;
margin-bottom: 0.8rem;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.3rem;
}
.answer-sheet-title small {
font-weight: 400;
font-size: 0.8rem;
color: #7f6b5c;
}
.answer-grid {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
justify-content: flex-start;
margin-bottom: 0.8rem;
}
.answer-dot {
width: 34px;
height: 34px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.7rem;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
border: 2px solid #dccfc6;
background: #f5ede7;
color: #5b4a3c;
user-select: none;
flex-shrink: 0;
}
.answer-dot:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
.answer-dot.correct {
background: #a5d6a7;
border-color: #4caf50;
color: #1e3a1e;
}
.answer-dot.wrong {
background: #ef9a9a;
border-color: #e57373;
color: #6d1a1a;
}
.answer-dot.current {
border-color: #3e2e24;
border-width: 3px;
box-shadow: 0 0 0 3px rgba(62, 46, 36, 0.2);
transform: scale(1.05);
}
.answer-dot.unanswered {
background: #f5ede7;
border-color: #dccfc6;
color: #b6a093;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 0.8rem;
margin-top: 0.6rem;
font-size: 0.9rem;
color: #5b4a3c;
}
.pagination button {
background: #ede3db;
border: none;
padding: 0.2rem 0.8rem;
border-radius: 30px;
cursor: pointer;
font-weight: 500;
color: #3e2e24;
transition: 0.1s;
border: 1px solid #dccfc6;
}
.pagination button:hover {
background: #dccfc6;
}
.pagination button:disabled {
opacity: 0.4;
pointer-events: none;
}
.pagination .page-info {
font-weight: 500;
min-width: 60px;
text-align: center;
}
/* ===== 响应式 ===== */
@media (max-width: 820px) {
.container {
flex-direction: column;
align-items: stretch;
}
.main-card {
flex: 1 1 auto;
}
.answer-sheet-wrapper {
flex: 1 1 auto;
max-height: none;
overflow-y: visible;
}
}
@media (max-width: 480px) {
.container {
padding: 1rem;
}
.card {
padding: 1.2rem;
}
.chinese-meaning {
font-size: 1.8rem;
}
.sentence-line {
font-size: 1.3rem;
}
#verbInput {
font-size: 1.4rem;
width: 90px;
}
.verb-input-wrapper {
min-width: 100px;
}
.answer-dot {
width: 28px;
height: 28px;
font-size: 0.6rem;
}
.banner-content h2 {
font-size: 2.5rem;
}
}
</style>
</head>
<body>
<div class="container">
<!-- 完成庆祝横幅 -->
<div id="completionBanner">
<div class="banner-content">
<span class="banner-emoji">🎉</span>
<h2>✨ 全部背完啦! ✨</h2>
<p>おめでとう! 素晴らしい! 🎊</p>
<div style="margin-top:1rem; font-size:2rem;">🏆🌟🎇</div>
<button class="btn btn-primary" id="restartFromBannerBtn" style="margin-top:1.5rem; pointer-events:auto;">🔄
もう一度やる</button>
</div>
</div>
<!-- 左侧:主卡片 -->
<div class="main-card">
<div class="card">
<header
style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; margin-bottom:0.2rem;">
<h1>📘 動詞・入力 <small>タイプ&チェック</small></h1>
</header>
<div class="progress-badge">
<span>🔖 今日のカード</span>
<span id="cardCounter">1 / 100</span>
</div>
<div class="chinese-meaning" id="chineseDisplay">〜 を 食べる</div>
<div class="sentence-block">
<div class="sentence-line">
<span class="noun" id="nounDisplay">私</span>
<span class="particle" id="particleDisplay">は</span>
<span class="verb-input-wrapper" id="inputWrapper">
<input type="text" id="verbInput" placeholder="動詞" autocomplete="off" autofocus>
<span class="input-hint">⏎</span>
</span>
<span style="margin-left:0.2rem; font-weight:300; color:#7f6757;">( 入力 )</span>
</div>
<div class="feedback-message" id="feedbackMessage">
<span style="color:#7f6b5c;">💡 動詞を入力して Enter を押してください</span>
</div>
<div class="extra-phrase">
<span class="label">📎 用例</span>
<span class="phrase-content" id="phraseContent">---</span>
</div>
</div>
<div class="action-group">
<button class="btn btn-outline" id="prevCardBtn">◀ 前へ</button>
<button class="btn btn-success" id="nextCardBtn">次へ ▶</button>
<button class="btn btn-outline" id="resetCardBtn">↺ リセット</button>
<button class="btn btn-primary" id="showAnswerBtn">🔍 答え</button>
</div>
<div class="counter-info">
<span id="knownCount">✅ 正解: 0</span>
<span id="totalCount">📚 総語数: 100</span>
</div>
<div class="footer-actions">
<button class="btn btn-icon" id="shuffleBtn">🔀 打乱顺序</button>
<button class="btn btn-icon" id="resetProgressBtn">🔄 重新开始</button>
</div>
</div>
</div>
<!-- 右侧:答题卡 -->
<div class="answer-sheet-wrapper">
<div class="answer-sheet-title">
<span>📋 解答一覧</span>
<small>クリックでジャンプ</small>
</div>
<div class="answer-grid" id="answerGrid"></div>
<div class="pagination" id="pagination">
<button id="prevPageBtn">◀</button>
<span class="page-info" id="pageInfo">1 / 1</span>
<button id="nextPageBtn">▶</button>
</div>
</div>
</div>
<!-- ===== 外部数据导入(words.js) ===== -->
<script src="./words.js"></script>
<script>
// ============================================================
// 数据加载:优先使用 words.js 中的 window.wordsData
// 若未定义则使用内置 fallback(100个N3动词)
// ============================================================
const FALLBACK_WORDS = [
{ "id": 1, "chinese": "吃", "noun": "ご飯", "particle": "を", "verb": "食べる", "phrase": "毎日 ご飯 を 食べる" },
{ "id": 2, "chinese": "喝", "noun": "水", "particle": "を", "verb": "飲む", "phrase": "朝 水 を 飲む" },
{ "id": 3, "chinese": "去", "noun": "学校", "particle": "へ", "verb": "行く", "phrase": "バス で 学校 へ 行く" },
{ "id": 4, "chinese": "来", "noun": "家", "particle": "に", "verb": "来る", "phrase": "友達 が 家 に 来る" },
{ "id": 5, "chinese": "看", "noun": "テレビ", "particle": "を", "verb": "見る", "phrase": "夜 テレビ を 見る" }
];
// ============================================================
// 核心逻辑
// ============================================================
let words = [];
let currentIndex = 0;
let correctCount = 0;
let cardStatus = [];
let cardInputs = [];
const ITEMS_PER_PAGE = 50;
let currentPage = 0;
const chineseDisplay = document.getElementById('chineseDisplay');
const nounDisplay = document.getElementById('nounDisplay');
const particleDisplay = document.getElementById('particleDisplay');
const verbInput = document.getElementById('verbInput');
const inputWrapper = document.getElementById('inputWrapper');
const phraseContent = document.getElementById('phraseContent');
const feedbackMessage = document.getElementById('feedbackMessage');
const cardCounter = document.getElementById('cardCounter');
const knownCountSpan = document.getElementById('knownCount');
const totalCountSpan = document.getElementById('totalCount');
const nextCardBtn = document.getElementById('nextCardBtn');
const prevCardBtn = document.getElementById('prevCardBtn');
const resetCardBtn = document.getElementById('resetCardBtn');
const showAnswerBtn = document.getElementById('showAnswerBtn');
const shuffleBtn = document.getElementById('shuffleBtn');
const resetProgressBtn = document.getElementById('resetProgressBtn');
const answerGrid = document.getElementById('answerGrid');
const prevPageBtn = document.getElementById('prevPageBtn');
const nextPageBtn = document.getElementById('nextPageBtn');
const pageInfo = document.getElementById('pageInfo');
const completionBanner = document.getElementById('completionBanner');
const restartFromBannerBtn = document.getElementById('restartFromBannerBtn');
let isAnswered = false;
// ---------- 完成检测 ----------
function checkCompletion() {
if (!words.length) return false;
const allDone = cardStatus.every(s => s !== 'unanswered');
if (allDone) {
completionBanner.classList.add('show');
} else {
completionBanner.classList.remove('show');
}
return allDone;
}
// ---------- 渲染主卡片 ----------
function renderCard() {
if (!words.length) return;
const word = words[currentIndex];
chineseDisplay.textContent = word.chinese || '';
nounDisplay.textContent = word.noun || '';
particleDisplay.textContent = word.particle || '';
const status = cardStatus[currentIndex] || 'unanswered';
if (status === 'correct') {
phraseContent.textContent = word.phrase || `${word.noun} ${word.particle} ${word.verb}`;
phraseContent.style.color = '#2e7d32';
} else {
phraseContent.textContent = '---';
phraseContent.style.color = '#b6a093';
}
if (status === 'correct') {
verbInput.value = word.verb || '';
inputWrapper.className = 'verb-input-wrapper correct';
feedbackMessage.innerHTML = `<span class="correct-text">✅ 正解! 「${word.verb}」 (Enterで次へ)</span>`;
isAnswered = true;
verbInput.disabled = false;
} else if (status === 'wrong') {
verbInput.value = cardInputs[currentIndex] || '';
inputWrapper.className = 'verb-input-wrapper wrong';
feedbackMessage.innerHTML = `<span class="wrong-text">❌ 不正解。正解は 「${word.verb}」 です。</span>`;
isAnswered = true;
verbInput.disabled = true;
} else {
verbInput.value = cardInputs[currentIndex] || '';
inputWrapper.className = 'verb-input-wrapper';
feedbackMessage.innerHTML = `<span style="color:#7f6b5c;">💡 動詞を入力して Enter を押してください</span>`;
isAnswered = false;
verbInput.disabled = false;
verbInput.focus();
}
cardCounter.textContent = `${currentIndex + 1} / ${words.length}`;
totalCountSpan.textContent = `📚 総語数: ${words.length}`;
updateCorrectCount();
renderAnswerGrid();
checkCompletion(); // 每次渲染后检测是否全部完成
}
function updateCorrectCount() {
correctCount = cardStatus.filter(s => s === 'correct').length;
knownCountSpan.textContent = `✅ 正解: ${correctCount}`;
}
// ---------- 渲染答题卡(分页) ----------
function renderAnswerGrid() {
answerGrid.innerHTML = '';
const totalPages = Math.ceil(words.length / ITEMS_PER_PAGE);
if (currentPage >= totalPages) currentPage = Math.max(0, totalPages - 1);
const start = currentPage * ITEMS_PER_PAGE;
const end = Math.min(start + ITEMS_PER_PAGE, words.length);
for (let i = start; i < end; i++) {
const dot = document.createElement('div');
dot.className = 'answer-dot';
const status = cardStatus[i] || 'unanswered';
if (status === 'correct') dot.classList.add('correct');
else if (status === 'wrong') dot.classList.add('wrong');
else dot.classList.add('unanswered');
if (i === currentIndex) dot.classList.add('current');
dot.textContent = i + 1;
dot.addEventListener('click', () => jumpToCard(i));
answerGrid.appendChild(dot);
}
pageInfo.textContent = `${currentPage + 1} / ${totalPages || 1}`;
prevPageBtn.disabled = currentPage === 0;
nextPageBtn.disabled = currentPage >= totalPages - 1;
}
// ---------- 分页控制 ----------
function goToPage(page) {
const totalPages = Math.ceil(words.length / ITEMS_PER_PAGE);
if (page < 0 || page >= totalPages) return;
currentPage = page;
renderAnswerGrid();
}
// ---------- 跳转到指定卡片 ----------
function jumpToCard(index) {
if (index < 0 || index >= words.length) return;
cardInputs[currentIndex] = verbInput.value;
const targetPage = Math.floor(index / ITEMS_PER_PAGE);
if (targetPage !== currentPage) {
currentPage = targetPage;
}
currentIndex = index;
renderCard();
verbInput.focus();
verbInput.setSelectionRange(verbInput.value.length, verbInput.value.length);
}
// ---------- 答案检查 ----------
function checkAnswer() {
if (!words.length) return;
const status = cardStatus[currentIndex] || 'unanswered';
if (status === 'correct') { nextCard(); return; }
if (status === 'wrong') {
feedbackMessage.innerHTML = `<span style="color:#7f6b5c;">⏳ このカードは不正解です。リセットして再挑戦してください。</span>`;
return;
}
const word = words[currentIndex];
const userInput = verbInput.value.trim();
if (userInput === '') {
feedbackMessage.innerHTML = `<span style="color:#c62828;">⚠️ 動詞を入力してください。</span>`;
inputWrapper.className = 'verb-input-wrapper';
return;
}
cardInputs[currentIndex] = userInput;
const isMatch = userInput.normalize('NFKC') === (word.verb || '').normalize('NFKC');
if (isMatch) {
cardStatus[currentIndex] = 'correct';
inputWrapper.className = 'verb-input-wrapper correct';
feedbackMessage.innerHTML = `<span class="correct-text">✅ 正解! 「${word.verb}」 (Enterで次へ)</span>`;
phraseContent.textContent = word.phrase || `${word.noun} ${word.particle} ${word.verb}`;
phraseContent.style.color = '#2e7d32';
updateCorrectCount();
} else {
cardStatus[currentIndex] = 'wrong';
inputWrapper.className = 'verb-input-wrapper wrong';
feedbackMessage.innerHTML = `<span class="wrong-text">❌ 不正解。正解は 「${word.verb}」 です。</span>`;
phraseContent.textContent = '---';
phraseContent.style.color = '#b6a093';
verbInput.disabled = true;
}
renderAnswerGrid();
checkCompletion(); // 答案变化后检测完成
}
// ---------- 卡片导航 ----------
function nextCard() {
if (!words.length) return;
cardInputs[currentIndex] = verbInput.value;
currentIndex = (currentIndex + 1) % words.length;
const targetPage = Math.floor(currentIndex / ITEMS_PER_PAGE);
if (targetPage !== currentPage) {
currentPage = targetPage;
}
renderCard();
verbInput.focus();
}
function prevCard() {
if (!words.length) return;
cardInputs[currentIndex] = verbInput.value;
currentIndex = (currentIndex - 1 + words.length) % words.length;
const targetPage = Math.floor(currentIndex / ITEMS_PER_PAGE);
if (targetPage !== currentPage) {
currentPage = targetPage;
}
renderCard();
verbInput.focus();
}
function resetCurrentCard() {
if (!words.length) return;
cardStatus[currentIndex] = 'unanswered';
cardInputs[currentIndex] = '';
renderCard();
verbInput.focus();
checkCompletion();
}
function showAnswer() {
if (!words.length) return;
const word = words[currentIndex];
verbInput.value = word.verb || '';
inputWrapper.className = 'verb-input-wrapper correct';
feedbackMessage.innerHTML = `<span style="color:#2e7d32;">🔍 答え: 「${word.verb}」</span>`;
if (cardStatus[currentIndex] === 'unanswered') {
phraseContent.textContent = word.phrase || `${word.noun} ${word.particle} ${word.verb}`;
phraseContent.style.color = '#2e7d32';
}
verbInput.disabled = true;
cardInputs[currentIndex] = word.verb;
}
// ---------- 打乱顺序(洗牌 + 重置进度) ----------
function shuffleAndReset() {
if (!words.length) return;
// 保存当前输入
cardInputs[currentIndex] = verbInput.value;
// 组合数据
const combined = words.map((w, i) => ({ word: w, status: cardStatus[i] || 'unanswered', input: cardInputs[i] || '' }));
// 洗牌
for (let i = combined.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[combined[i], combined[j]] = [combined[j], combined[i]];
}
words = combined.map(item => item.word);
cardStatus = combined.map(() => 'unanswered'); // 重置所有状态为未答
cardInputs = combined.map(() => ''); // 清空输入
currentIndex = 0;
currentPage = 0;
correctCount = 0;
isAnswered = false;
completionBanner.classList.remove('show');
renderCard();
verbInput.focus();
}
// ---------- 重新开始(不改变顺序,仅重置进度) ----------
function resetProgress() {
if (!words.length) return;
cardStatus = words.map(() => 'unanswered');
cardInputs = words.map(() => '');
correctCount = 0;
currentIndex = 0;
currentPage = 0;
isAnswered = false;
completionBanner.classList.remove('show');
renderCard();
knownCountSpan.textContent = `✅ 正解: 0`;
verbInput.focus();
}
// ---------- 事件绑定 ----------
verbInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); checkAnswer(); }
if (e.key === ' ' && !e.repeat && cardStatus[currentIndex] === 'correct') {
e.preventDefault();
nextCard();
}
});
verbInput.addEventListener('input', () => {
if (cardStatus[currentIndex] === 'unanswered') {
cardInputs[currentIndex] = verbInput.value;
if (isAnswered) {
isAnswered = false;
inputWrapper.className = 'verb-input-wrapper';
feedbackMessage.innerHTML = `<span style="color:#7f6b5c;">✏️ 再入力中... Enter でチェック</span>`;
verbInput.disabled = false;
}
}
});
nextCardBtn.addEventListener('click', nextCard);
prevCardBtn.addEventListener('click', prevCard);
resetCardBtn.addEventListener('click', resetCurrentCard);
showAnswerBtn.addEventListener('click', showAnswer);
shuffleBtn.addEventListener('click', shuffleAndReset);
resetProgressBtn.addEventListener('click', resetProgress);
prevPageBtn.addEventListener('click', () => goToPage(currentPage - 1));
nextPageBtn.addEventListener('click', () => goToPage(currentPage + 1));
restartFromBannerBtn.addEventListener('click', resetProgress);
// ---------- 数据加载 ----------
function initData() {
if (window.wordsData && Array.isArray(window.wordsData) && window.wordsData.length) {
words = window.wordsData;
console.log('✅ words.js 加载成功,单词数:', words.length);
} else {
console.warn('⚠️ words.js 未定义或无效,使用内置 fallback 数据');
words = FALLBACK_WORDS.slice();
}
cardStatus = words.map(() => 'unanswered');
cardInputs = words.map(() => '');
currentIndex = 0;
currentPage = 0;
renderCard();
verbInput.focus();
}
if (document.readyState === 'complete') {
initData();
} else {
window.addEventListener('load', initData);
}
</script>
</body>
</html>
javascript
// ============================================================
// words.js - 日语动词数据(N3级别,100个)
// 赋值给全局变量 window.wordsData,供 HTML 使用
// ============================================================
window.wordsData = [
{ "id": 1, "chinese": "吃", "noun": "ご飯", "particle": "を", "verb": "食べる", "phrase": "毎日 ご飯 を 食べる" },
{ "id": 2, "chinese": "喝", "noun": "水", "particle": "を", "verb": "飲む", "phrase": "朝 水 を 飲む" },
{ "id": 3, "chinese": "去", "noun": "学校", "particle": "へ", "verb": "行く", "phrase": "バス で 学校 へ 行く" },
{ "id": 4, "chinese": "来", "noun": "家", "particle": "に", "verb": "来る", "phrase": "友達 が 家 に 来る" },
{ "id": 5, "chinese": "看", "noun": "テレビ", "particle": "を", "verb": "見る", "phrase": "夜 テレビ を 見る" },
{ "id": 6, "chinese": "听", "noun": "音楽", "particle": "を", "verb": "聞く", "phrase": "よく 音楽 を 聞く" },
{ "id": 7, "chinese": "说", "noun": "日本語", "particle": "を", "verb": "話す", "phrase": "友達 と 日本語 を 話す" },
{ "id": 8, "chinese": "读", "noun": "本", "particle": "を", "verb": "読む", "phrase": "図書館 で 本 を 読む" },
{ "id": 9, "chinese": "写", "noun": "手紙", "particle": "を", "verb": "書く", "phrase": "ペン で 手紙 を 書く" },
{ "id": 10, "chinese": "买", "noun": "パン", "particle": "を", "verb": "買う", "phrase": "スーパー で パン を 買う" },
{ "id": 11, "chinese": "卖", "noun": "本", "particle": "を", "verb": "売る", "phrase": "店 で 本 を 売る" },
{ "id": 12, "chinese": "使用", "noun": "パソコン", "particle": "を", "verb": "使う", "phrase": "仕事 で パソコン を 使う" },
{ "id": 13, "chinese": "做", "noun": "料理", "particle": "を", "verb": "作る", "phrase": "毎日 料理 を 作る" },
{ "id": 14, "chinese": "拿", "noun": "かばん", "particle": "を", "verb": "持つ", "phrase": "手 に かばん を 持つ" },
{ "id": 15, "chinese": "取", "noun": "本", "particle": "を", "verb": "取る", "phrase": "棚 から 本 を 取る" },
{ "id": 16, "chinese": "放置", "noun": "本", "particle": "を", "verb": "置く", "phrase": "机 の 上 に 本 を 置く" },
{ "id": 17, "chinese": "进入", "noun": "部屋", "particle": "に", "verb": "入る", "phrase": "ドア を 開けて 部屋 に 入る" },
{ "id": 18, "chinese": "出去", "noun": "部屋", "particle": "から", "verb": "出る", "phrase": "教室 から 出る" },
{ "id": 19, "chinese": "打开", "noun": "窓", "particle": "を", "verb": "開ける", "phrase": "朝 窓 を 開ける" },
{ "id": 20, "chinese": "关闭", "noun": "ドア", "particle": "を", "verb": "閉める", "phrase": "夜 ドア を 閉める" },
{ "id": 21, "chinese": "教", "noun": "学生", "particle": "に", "verb": "教える", "phrase": "先生 が 学生 に 教える" },
{ "id": 22, "chinese": "学习", "noun": "日本語", "particle": "を", "verb": "学ぶ", "phrase": "大学 で 日本語 を 学ぶ" },
{ "id": 23, "chinese": "记住", "noun": "単語", "particle": "を", "verb": "覚える", "phrase": "毎日 単語 を 覚える" },
{ "id": 24, "chinese": "忘记", "noun": "約束", "particle": "を", "verb": "忘れる", "phrase": "時々 約束 を 忘れる" },
{ "id": 25, "chinese": "想", "noun": "こと", "particle": "を", "verb": "思う", "phrase": "いろいろ な こと を 思う" },
{ "id": 26, "chinese": "考虑", "noun": "問題", "particle": "を", "verb": "考える", "phrase": "真剣 に 問題 を 考える" },
{ "id": 27, "chinese": "决定", "noun": "計画", "particle": "を", "verb": "決める", "phrase": "会議 で 計画 を 決める" },
{ "id": 28, "chinese": "开始", "noun": "授業", "particle": "を", "verb": "始める", "phrase": "9時 に 授業 を 始める" },
{ "id": 29, "chinese": "结束", "noun": "仕事", "particle": "を", "verb": "終わる", "phrase": "5時 に 仕事 が 終わる" },
{ "id": 30, "chinese": "继续", "noun": "勉強", "particle": "を", "verb": "続ける", "phrase": "毎日 勉強 を 続ける" },
{ "id": 31, "chinese": "停止", "noun": "車", "particle": "を", "verb": "止める", "phrase": "信号 で 車 を 止める" },
{ "id": 32, "chinese": "改变", "noun": "予定", "particle": "を", "verb": "変える", "phrase": "急に 予定 を 変える" },
{ "id": 33, "chinese": "修理", "noun": "時計", "particle": "を", "verb": "直す", "phrase": "店 で 時計 を 直す" },
{ "id": 34, "chinese": "弄坏", "noun": "花瓶", "particle": "を", "verb": "壊す", "phrase": "子供 が 花瓶 を 壊す" },
{ "id": 35, "chinese": "修好", "noun": "機械", "particle": "が", "verb": "直る", "phrase": "機械 が 直る" },
{ "id": 36, "chinese": "坏掉", "noun": "テレビ", "particle": "が", "verb": "壊れる", "phrase": "古い テレビ が 壊れる" },
{ "id": 37, "chinese": "见面", "noun": "友達", "particle": "に", "verb": "会う", "phrase": "駅 で 友達 に 会う" },
{ "id": 38, "chinese": "打电话", "noun": "母", "particle": "に", "verb": "電話する", "phrase": "毎日 母 に 電話する" },
{ "id": 39, "chinese": "散步", "noun": "公園", "particle": "を", "verb": "散歩する", "phrase": "朝 公園 を 散歩する" },
{ "id": 40, "chinese": "旅行", "noun": "京都", "particle": "へ", "verb": "旅行する", "phrase": "夏 京都 へ 旅行する" },
{ "id": 41, "chinese": "担心", "noun": "試験", "particle": "を", "verb": "心配する", "phrase": "試験 を 心配する" },
{ "id": 42, "chinese": "努力", "noun": "目標", "particle": "に", "verb": "努力する", "phrase": "目標 に 向かって 努力する" },
{ "id": 43, "chinese": "失败", "noun": "試験", "particle": "に", "verb": "失敗する", "phrase": "大切 な 試験 に 失敗する" },
{ "id": 44, "chinese": "成功", "noun": "計画", "particle": "が", "verb": "成功する", "phrase": "計画 が 成功する" },
{ "id": 45, "chinese": "吃惊", "noun": "ニュース", "particle": "に", "verb": "驚く", "phrase": "突然 の ニュース に 驚く" },
{ "id": 46, "chinese": "笑", "noun": "話", "particle": "を", "verb": "笑う", "phrase": "面白い 話 を 笑う" },
{ "id": 47, "chinese": "哭", "noun": "悲しい", "particle": "に", "verb": "泣く", "phrase": "悲しい 映画 に 泣く" },
{ "id": 48, "chinese": "生气", "noun": "友達", "particle": "に", "verb": "怒る", "phrase": "遅刻 し た 友達 に 怒る" },
{ "id": 49, "chinese": "道歉", "noun": "先生", "particle": "に", "verb": "謝る", "phrase": "遅刻 し て 先生 に 謝る" },
{ "id": 50, "chinese": "感谢", "noun": "友達", "particle": "に", "verb": "感謝する", "phrase": "手伝っ て くれて 友達 に 感謝する" },
{ "id": 51, "chinese": "相信", "noun": "友達", "particle": "を", "verb": "信じる", "phrase": "彼 は 友達 を 信じる" },
{ "id": 52, "chinese": "怀疑", "noun": "彼", "particle": "を", "verb": "疑う", "phrase": "彼 の 言う こと を 疑う" },
{ "id": 53, "chinese": "理解", "noun": "内容", "particle": "を", "verb": "理解する", "phrase": "難し い 内容 を 理解する" },
{ "id": 54, "chinese": "说明", "noun": "ルール", "particle": "を", "verb": "説明する", "phrase": "新し い ルール を 説明する" },
{ "id": 55, "chinese": "翻译", "noun": "文章", "particle": "を", "verb": "翻訳する", "phrase": "英語 の 文章 を 翻訳する" },
{ "id": 56, "chinese": "练习", "noun": "発音", "particle": "を", "verb": "練習する", "phrase": "毎日 発音 を 練習する" },
{ "id": 57, "chinese": "复习", "noun": "単語", "particle": "を", "verb": "復習する", "phrase": "試験 の 前 に 単語 を 復習する" },
{ "id": 58, "chinese": "预习", "noun": "教科書", "particle": "を", "verb": "予習する", "phrase": "授業 の 前 に 教科書 を 予習する" },
{ "id": 59, "chinese": "迟到", "noun": "授業", "particle": "に", "verb": "遅刻する", "phrase": "寝坊 し て 授業 に 遅刻する" },
{ "id": 60, "chinese": "早退", "noun": "会社", "particle": "を", "verb": "早退する", "phrase": "体調 が 悪く て 会社 を 早退する" },
{ "id": 61, "chinese": "休息", "noun": "休憩", "particle": "を", "verb": "休む", "phrase": "昼 休み に 休む" },
{ "id": 62, "chinese": "工作", "noun": "会社", "particle": "で", "verb": "働く", "phrase": "大手 会社 で 働く" },
{ "id": 63, "chinese": "居住", "noun": "東京", "particle": "に", "verb": "住む", "phrase": "東京 に 住む" },
{ "id": 64, "chinese": "出生", "noun": "大阪", "particle": "で", "verb": "生まれる", "phrase": "大阪 で 生まれる" },
{ "id": 65, "chinese": "死亡", "noun": "年", "particle": "に", "verb": "死ぬ", "phrase": "80歳 で 死ぬ" },
{ "id": 66, "chinese": "结婚", "noun": "彼女", "particle": "と", "verb": "結婚する", "phrase": "来年 彼女 と 結婚する" },
{ "id": 67, "chinese": "离婚", "noun": "夫", "particle": "と", "verb": "離婚する", "phrase": "去年 夫 と 離婚する" },
{ "id": 68, "chinese": "开始(自动)", "noun": "会議", "particle": "が", "verb": "始まる", "phrase": "10時 に 会議 が 始まる" },
{ "id": 69, "chinese": "结束(自动)", "noun": "授業", "particle": "が", "verb": "終わる", "phrase": "3時 に 授業 が 終わる" },
{ "id": 70, "chinese": "增加", "noun": "人口", "particle": "が", "verb": "増える", "phrase": "都市 の 人口 が 増える" },
{ "id": 71, "chinese": "减少", "noun": "貯金", "particle": "が", "verb": "減る", "phrase": "使っ て 貯金 が 減る" },
{ "id": 72, "chinese": "上升", "noun": "物価", "particle": "が", "verb": "上がる", "phrase": "毎年 物価 が 上がる" },
{ "id": 73, "chinese": "下降", "noun": "気温", "particle": "が", "verb": "下がる", "phrase": "冬 に 気温 が 下がる" },
{ "id": 74, "chinese": "提高(他)", "noun": "レベル", "particle": "を", "verb": "上げる", "phrase": "勉強 し て レベル を 上げる" },
{ "id": 75, "chinese": "降低(他)", "noun": "値段", "particle": "を", "verb": "下げる", "phrase": "セール で 値段 を 下げる" },
{ "id": 76, "chinese": "收集", "noun": "切手", "particle": "を", "verb": "集める", "phrase": "趣味 で 切手 を 集める" },
{ "id": 77, "chinese": "聚集(自动)", "noun": "人", "particle": "が", "verb": "集まる", "phrase": "広場 に 人 が 集まる" },
{ "id": 78, "chinese": "摇动(他)", "noun": "木", "particle": "を", "verb": "揺らす", "phrase": "風 が 木 を 揺らす" },
{ "id": 79, "chinese": "摇动(自动)", "noun": "電車", "particle": "が", "verb": "揺れる", "phrase": "電車 が 揺れる" },
{ "id": 80, "chinese": "沸腾(自动)", "noun": "お湯", "particle": "が", "verb": "沸く", "phrase": "やかん の お湯 が 沸く" },
{ "id": 81, "chinese": "煮沸(他)", "noun": "お湯", "particle": "を", "verb": "沸かす", "phrase": "お湯 を 沸かす" },
{ "id": 82, "chinese": "干燥(自动)", "noun": "洗濯物", "particle": "が", "verb": "乾く", "phrase": "外 に 干し て 洗濯物 が 乾く" },
{ "id": 83, "chinese": "晾干(他)", "noun": "洗濯物", "particle": "を", "verb": "乾かす", "phrase": "乾燥機 で 洗濯物 を 乾かす" },
{ "id": 84, "chinese": "弄湿(他)", "noun": "服", "particle": "を", "verb": "濡らす", "phrase": "雨 で 服 を 濡らす" },
{ "id": 85, "chinese": "湿(自动)", "noun": "服", "particle": "が", "verb": "濡れる", "phrase": "雨 で 服 が 濡れる" },
{ "id": 86, "chinese": "弄脏(他)", "noun": "服", "particle": "を", "verb": "汚す", "phrase": "泥 で 服 を 汚す" },
{ "id": 87, "chinese": "脏(自动)", "noun": "服", "particle": "が", "verb": "汚れる", "phrase": "着 て い たら 服 が 汚れる" },
{ "id": 88, "chinese": "打开(自动)", "noun": "ドア", "particle": "が", "verb": "開く", "phrase": "自動 で ドア が 開く" },
{ "id": 89, "chinese": "关闭(自动)", "noun": "ドア", "particle": "が", "verb": "閉まる", "phrase": "時間 に なると ドア が 閉まる" },
{ "id": 90, "chinese": "连接(他)", "noun": "ケーブル", "particle": "を", "verb": "繋ぐ", "phrase": "パソコン に ケーブル を 繋ぐ" },
{ "id": 91, "chinese": "连接(自动)", "noun": "線", "particle": "が", "verb": "繋がる", "phrase": "電話 の 線 が 繋がる" },
{ "id": 92, "chinese": "放入", "noun": "箱", "particle": "に", "verb": "入れる", "phrase": "本 を 箱 に 入れる" },
{ "id": 93, "chinese": "取出", "noun": "箱", "particle": "から", "verb": "出す", "phrase": "本 を 箱 から 出す" },
{ "id": 94, "chinese": "选择", "noun": "プレゼント", "particle": "を", "verb": "選ぶ", "phrase": "友達 の ため に プレゼント を 選ぶ" },
{ "id": 95, "chinese": "穿(裤子)", "noun": "ズボン", "particle": "を", "verb": "履く", "phrase": "朝 ズボン を 履く" },
{ "id": 96, "chinese": "戴(眼镜)", "noun": "メガネ", "particle": "を", "verb": "かける", "phrase": "本 を 読む とき メガネ を かける" },
{ "id": 97, "chinese": "脱(衣服)", "noun": "コート", "particle": "を", "verb": "脱ぐ", "phrase": "暖かく なっ て コート を 脱ぐ" },
{ "id": 98, "chinese": "穿(上衣)", "noun": "シャツ", "particle": "を", "verb": "着る", "phrase": "今日 は 青い シャツ を 着る" },
{ "id": 99, "chinese": "等待", "noun": "バス", "particle": "を", "verb": "待つ", "phrase": "毎朝 バス を 待つ" },
{ "id": 100, "chinese": "请求", "noun": "助け", "particle": "を", "verb": "頼む", "phrase": "困っ た とき 友達 に 助け を 頼む" }
];