日语单词 Web Page

文件结构

文件夹

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": "困っ た とき 友達 に 助け を 頼む" }
];
相关推荐
禅思院3 小时前
AI对话前端从入门到崩溃:一个长对话引发的五层优化战争【引子】
前端·面试·架构
TrisighT3 小时前
Electron 鸿蒙 PC 上点外链唤醒应用,我试了 6 种写法只有 1 种能跑
前端·electron·harmonyos
天才熊猫君4 小时前
配置与数据分离:一种可视化搭建的属性编辑方案
前端·javascript
林希_Rachel_傻希希4 小时前
web性能之相关路径——AI总结
前端·javascript·面试
竹林8184 小时前
用 wagmi v2 踩坑两天,我终于搞懂了多链钱包切换在 DeFi 前端中的正确姿势
前端·javascript
用户2136610035724 小时前
Vue项目搜索功能与面包屑导航
前端·javascript
星栈5 小时前
LiveView 的实时通信,爽是爽,但 PubSub 和广播也最容易把自己绕晕
前端·前端框架·elixir
用户2930750976695 小时前
告别关键词匹配,拥抱向量语义 —— RAG 搜索从零到一
前端
独孤留白5 小时前
从C到Rust:告别 C 的"指针 + 长度"手动模式
前端·rust