ソースを参照

完成单词匹配的选择难度页面

s202226701051 1 週間 前
コミット
4f1cd6d65e

+ 5 - 0
word-app/src/app/app.routes.ts

@@ -59,6 +59,11 @@ export const routes: Routes = [
     path: 'word-story',
     loadComponent: () => import('./word-story/word-story.page').then( m => m.WordStoryPage)
   },
+  {
+    path: 'word-match',
+    loadComponent: () => import('./word-match/word-match.page').then( m => m.WordMatchPage)
+  },
+
 
 
 

+ 0 - 1
word-app/src/app/edit-profile/hwobs.service.ts

@@ -167,7 +167,6 @@ export class HwobsProvider {
   
 }
 
-// ... 原有的代码保持不变 ...
 
 /**
  * 简化的华为OBS服务 - 只用于测试上传功能

+ 5 - 5
word-app/src/app/recite-words/word-group.service.ts

@@ -6,9 +6,9 @@ import { Word, WordGroup } from './word-interfaces';
   providedIn: 'root'
 })
 export class WordGroupService {
-  private currentGroup: WordGroup | null = null;
-  private groupHistory: WordGroup[] = [];
-  private isActive = new BehaviorSubject<boolean>(false);
+  private currentGroup: WordGroup | null = null; // 当前单词组
+  private groupHistory: WordGroup[] = []; // 单词组历史记录
+  private isActive = new BehaviorSubject<boolean>(false); // 当前是否在学习单词组
   
   // 每组单词数量
   private readonly WORDS_PER_GROUP = 5;
@@ -99,7 +99,7 @@ export class WordGroupService {
     
     // 找到下一个未完成的单词
     let found = false;
-    let attempts = 0;
+    let attempts = 0; // 防止无限循环
     const maxAttempts = this.currentGroup.words.length;
     
     while (!found && attempts < maxAttempts) {
@@ -177,7 +177,7 @@ export class WordGroupService {
   }
   
   /**
-   * 获取组完成状态
+   * 获取组完成状态 是否可以学习
    */
   isGroupActive(): boolean {
     return !!this.currentGroup && !this.currentGroup.completed;

+ 33 - 2
word-app/src/app/recite-words/word-processing.service.ts

@@ -350,7 +350,7 @@ export class WordProcessingService {
   }
   
   /**
-   * 按标点符号分割文本为词
+   * 将释义按标点符号分割文本为词
    */
   private splitWordsByPunctuation(text: string): string[] {
     if (!text) return [];
@@ -568,4 +568,35 @@ export class WordProcessingService {
   getAllDefinitionsCount(): number {
     return this.allDefinitions.length;
   }
-}
+  
+  /**
+   * 获取随机单词
+   * @param count 需要的单词数量
+   * @returns 随机单词数组
+   */
+  getRandomWords(count: number): { english: string; chinese: string }[] {
+    if (this.allDefinitions.length === 0) {
+      console.error('没有可用的单词定义');
+      return [];
+    }
+    
+    const result: { english: string; chinese: string }[] = [];
+    const usedIndices = new Set<number>();
+    
+    while (result.length < count && usedIndices.size < this.allDefinitions.length) {
+      const randomIndex = Math.floor(Math.random() * this.allDefinitions.length);
+      if (usedIndices.has(randomIndex)) continue;
+      
+      usedIndices.add(randomIndex);
+      const def = this.allDefinitions[randomIndex];
+      result.push({
+        english: def.word,
+        chinese: def.definition
+      });
+    }
+    
+    return result;
+  }
+}
+
+export { Word };

+ 1 - 1
word-app/src/app/spelling-practice/spelling-practice.page.html

@@ -132,7 +132,7 @@
       color="medium"
       (click)="studyAnotherGroup()"
       class="action-button">
-      学一组
+      学一组
     </ion-button>
     
   </div>

+ 2 - 2
word-app/src/app/tab2/tab2.page.html

@@ -90,11 +90,11 @@
       <span class="card-title">看义选英</span>
     </div>
 
-    <div class="training-card vip" >
+    <div class="training-card vip" (click)="goToWordMatch()">
       <div class="card-icon">
         <img src="assets/icon/例句填空.svg" alt="例句填空">
       </div>
-      <span class="card-title">例句填空</span>
+      <span class="card-title">单词消消乐</span>
     </div>
 
     <div class="training-card "  (click)="goToAIWord()">

+ 4 - 0
word-app/src/app/tab2/tab2.page.ts

@@ -79,4 +79,8 @@ goToWordStory(){
   this.router.navigate(["word-story"]);
 }
 
+goToWordMatch(){
+  this.router.navigate(["word-match"]);
+}
+
 }

+ 243 - 0
word-app/src/app/word-match/word-match.page.html

@@ -0,0 +1,243 @@
+
+<ion-content class="word-story-content" [class.has-fixed-bottom-bar]="!gameState.isGameOver">
+  <div class="ocean-bg"></div>
+  
+  <!-- 漂浮的海洋生物装饰 -->
+  <div class="floating-creatures">
+    <div class="creature fish-1">
+      <img src="assets/icon/鲨鱼.svg" alt="fish" class="fish-svg">
+    </div>
+    <div class="creature fish-2">
+      <img src="assets/icon/鲨鱼.svg" alt="fish" class="fish-svg">
+    </div>
+    <div class="creature jellyfish">
+      <svg viewBox="0 0 100 100" width="40" height="40">
+        <circle cx="50" cy="30" r="15" fill="#FF80AB" opacity="0.6"/>
+        <path d="M35,40 Q50,70 65,40" stroke="#FF80AB" stroke-width="8" fill="none" opacity="0.5"/>
+        <path d="M30,45 Q45,80 60,45" stroke="#FF80AB" stroke-width="6" fill="none" opacity="0.5"/>
+        <path d="M40,45 Q50,85 70,45" stroke="#FF80AB" stroke-width="7" fill="none" opacity="0.5"/>
+      </svg>
+    </div>
+    <div class="creature bubble-1"></div>
+    <div class="creature bubble-2"></div>
+    <div class="creature bubble-3"></div>
+    <div class="creature starfish">
+      <svg viewBox="0 0 100 100" width="30" height="30">
+        <path d="M50,10 L60,35 L85,35 L65,50 L75,80 L50,65 L25,80 L35,50 L15,35 L40,35 Z" fill="#FFB74D" opacity="0.7"/>
+      </svg>
+    </div>
+  </div>
+  
+  <div class="word-story-container">
+    <!-- 顶部导航 - 完全套用串词成文页面的船图标 -->
+    <div class="nav-header">
+      <div class="back-btn" (click)="goBack()">
+        <ion-icon name="arrow-back-outline"></ion-icon>
+      </div>
+      <h1 class="page-title">
+        <ion-icon name="boat-outline" class="title-icon"></ion-icon>
+        单词配对消消乐
+      </h1>
+      <div class="placeholder"></div>
+    </div>
+
+    <!-- 游戏进行中 -->
+    <ng-container *ngIf="!gameState.isGameOver">
+      <!-- 难度选择器包装容器 - 垂直居中 -->
+      <div class="difficulty-selector-wrapper" *ngIf="gameState.cards.length === 0">
+        <!-- 难度选择器 - 海洋风格波浪装饰 -->
+        <div class="difficulty-selector">
+          <div class="wave-decoration">
+            <svg viewBox="0 0 1200 120" preserveAspectRatio="none">
+              <path d="M0,0V46.29c47.79,22.2,103.59,32.17,158,28,70.36-5.37,136.33-33.31,206.8-37.5C438.64,32.43,512.34,53.67,583,72.05c69.27,18,138.3,24.88,209.4,13.08,36.15-6,69.85-17.84,104.45-29.34C989.49,25,1113-14.29,1200,52.47V0Z" opacity=".25" fill="#81D4FA"></path>
+              <path d="M0,0V15.81C13,21.25,27.93,25.67,44.24,28.45c69.76,11.65,139.79-5,208.74-16.68C364.06,0,440.45,-6.64,521.46,6.68c55.82,9.13,108.61,27.27,162.23,40.29C741.57,62.54,812.19,72,884.66,66.82c42.43-3.13,84.16-12.18,125.26-23.88C1064.12,29.27,1137.54,7.47,1200,27.38V0Z" fill="#E1F5FE"></path>
+            </svg>
+          </div>
+          
+          <!-- 添加锚点装饰 -->
+          <div class="anchor-decoration">
+            <svg width="40" height="40" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M12 2C10.9 2 10 2.9 10 4V4.29C7.12 5.14 5 7.82 5 11V13L3 15V17H21V15L19 13V11C19 7.82 16.88 5.14 14 4.29V4C14 2.9 13.1 2 12 2ZM12 6C14.21 6 16 7.79 16 10V13H8V10C8 7.79 9.79 6 12 6ZM7 19V20C7 21.1 7.9 22 9 22H15C16.1 22 17 21.1 17 20V19H7Z" fill="#0288D1"/>
+            </svg>
+          </div>
+          
+          <p class="difficulty-title">选择难度开始游戏</p>
+          
+          <div class="difficulty-buttons-row">
+            <div 
+              class="difficulty-option"
+              [class.selected]="selectedDifficulty === 'easy'"
+              (click)="selectedDifficulty = 'easy'; $event.stopPropagation()"
+            >
+              <span class="option-icon">🐟</span>
+              <span class="option-text">简单</span>
+            </div>
+            <div 
+              class="difficulty-option"
+              [class.selected]="selectedDifficulty === 'medium'"
+              (click)="selectedDifficulty = 'medium'; $event.stopPropagation()"
+            >
+              <span class="option-icon">🐠</span>
+              <span class="option-text">中等</span>
+            </div>
+            <div 
+              class="difficulty-option"
+              [class.selected]="selectedDifficulty === 'hard'"
+              (click)="selectedDifficulty = 'hard'; $event.stopPropagation()"
+            >
+              <span class="option-icon">🐡</span>
+              <span class="option-text">困难</span>
+            </div>
+          </div>
+          
+          <ion-button 
+            (click)="selectedDifficulty && startGame(selectedDifficulty)" 
+            class="select-btn"
+            [class.active]="selectedDifficulty !== null"
+            [disabled]="selectedDifficulty === null"
+            expand="block"
+          >
+            <ion-icon name="play-outline" slot="start"></ion-icon>
+            开始游戏
+          </ion-button>
+          
+          <!-- 底部海星装饰 -->
+          <div class="starfish-decoration">
+            <svg viewBox="0 0 100 100" width="30" height="30">
+              <path d="M50,10 L60,35 L85,35 L65,50 L75,80 L50,65 L25,80 L35,50 L15,35 L40,35 Z" fill="#FFB74D"/>
+            </svg>
+          </div>
+        </div>
+      </div>
+
+      <!-- 游戏信息栏 - 仅在游戏开始后显示 -->
+      <div class="game-header shell-card" *ngIf="gameState.cards.length > 0">
+        <div class="stat-item">
+          <ion-icon name="time-outline" class="stat-icon"></ion-icon>
+          <span class="stat-label">时间</span>
+          <span class="stat-value">{{gameState.timeLeft}}s</span>
+        </div>
+        <div class="stat-divider"></div>
+        <div class="stat-item">
+          <ion-icon name="star-outline" class="stat-icon"></ion-icon>
+          <span class="stat-label">分数</span>
+          <span class="stat-value">{{gameState.score}}</span>
+        </div>
+        <div class="stat-divider"></div>
+        <div class="stat-item">
+          <ion-icon name="checkmark-circle-outline" class="stat-icon"></ion-icon>
+          <span class="stat-label">匹配</span>
+          <span class="stat-value">{{gameState.matchedPairs}}/{{getTotalPairs()}}</span>
+        </div>
+      </div>
+
+      <!-- 卡片网格 - 添加海浪效果 -->
+      <div class="cards-grid-container" *ngIf="gameState.cards.length > 0">
+        <div class="wave-overlay"></div>
+        <div class="cards-grid" [style.--grid-cols]="gameState.cards.length <= 8 ? 3 : 4">
+          <div 
+            class="card-wrapper"
+            *ngFor="let card of gameState.cards; let i = index"
+          >
+            <div 
+              class="card"
+              [class.flipped]="card.isFlipped"
+              [class.matched]="card.isMatched"
+              (click)="onCardClick(i)"
+            >
+              <div class="card-inner">
+                <!-- 卡片正面:鲨鱼图片 -->
+                <div class="card-front">
+                  <div class="shark-container">
+                    <img src="assets/icon/鲨鱼.svg" alt="shark" class="shark-image">
+                  </div>
+                </div>
+                <!-- 卡片背面:单词/释义 -->
+                <div class="card-back">
+                  <span class="card-text">{{card.content}}</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 底部提示信息 -->
+      <div class="game-tip" *ngIf="gameState.cards.length > 0">
+        <ion-icon name="information-circle-outline" class="tip-icon"></ion-icon>
+        <span class="tip-text">点击卡片配对英文和中文</span>
+      </div>
+    </ng-container>
+
+    <!-- 游戏结束 -->
+    <div class="game-over" *ngIf="gameState.isGameOver">
+      <div class="result-card shell-card">
+        <!-- 添加胜利/失败动画 -->
+        <div class="confetti" *ngIf="gameState.matchedPairs === getTotalPairs()">
+          <div class="confetti-piece"></div>
+          <div class="confetti-piece"></div>
+          <div class="confetti-piece"></div>
+          <div class="confetti-piece"></div>
+        </div>
+        
+        <div class="result-icon" [class.win]="gameState.matchedPairs === getTotalPairs()">
+          <div class="bubble-animation"></div>
+          <img *ngIf="gameState.matchedPairs === getTotalPairs()" src="assets/icon/鲨鱼.svg" alt="win" class="result-shark">
+          <span *ngIf="gameState.matchedPairs !== getTotalPairs()">⏰</span>
+        </div>
+        
+        <h2 class="result-title ocean-title">
+          {{gameState.matchedPairs === getTotalPairs() ? '恭喜获胜!' : '游戏结束!'}}
+        </h2>
+        
+        <div class="treasure-chest" *ngIf="gameState.matchedPairs === getTotalPairs()">
+          <svg width="60" height="60" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+            <rect x="4" y="8" width="16" height="12" rx="2" fill="#FFB74D"/>
+            <rect x="6" y="6" width="12" height="4" fill="#FF9800"/>
+            <circle cx="12" cy="14" r="2" fill="#FFD54F"/>
+            <circle cx="12" cy="14" r="1" fill="#FFF9C4"/>
+            <path d="M9 19L8 22H16L15 19H9Z" fill="#FFB74D"/>
+          </svg>
+          <span class="treasure-text">获得宝藏!</span>
+        </div>
+        
+        <div class="result-stats">
+          <div class="stat-item">
+            <ion-icon name="trophy-outline" class="stat-icon"></ion-icon>
+            <span class="stat-label">最终分数</span>
+            <span class="stat-value ocean-number">{{gameState.score}}</span>
+          </div>
+          <div class="stat-item">
+            <ion-icon name="fish-outline" class="stat-icon"></ion-icon>
+            <span class="stat-label">匹配对数</span>
+            <span class="stat-value ocean-number">{{gameState.matchedPairs}}/{{getTotalPairs()}}</span>
+          </div>
+          <div class="stat-item">
+            <ion-icon name="hourglass-outline" class="stat-icon"></ion-icon>
+            <span class="stat-label">用时</span>
+            <span class="stat-value ocean-number">{{90 - gameState.timeLeft}}s</span>
+          </div>
+        </div>
+        
+        <div class="button-group">
+          <ion-button 
+            (click)="startGame(gameState.difficulty)" 
+            class="ocean-btn primary"
+            expand="block"
+          >
+            <ion-icon name="refresh-outline" slot="start"></ion-icon>
+            再玩一次
+          </ion-button>
+          <ion-button 
+            (click)="resetToDifficultySelect()" 
+            class="ocean-btn secondary"
+            expand="block"
+          >
+            <ion-icon name="options-outline" slot="start"></ion-icon>
+            选择难度
+          </ion-button>
+        </div>
+      </div>
+    </div>
+  </div>
+</ion-content>

+ 911 - 0
word-app/src/app/word-match/word-match.page.scss

@@ -0,0 +1,911 @@
+/* word-match.page.scss */
+// 完全套用串词成文页面的样式变量
+$ocean-deep: #01579B;
+$ocean-mid: #0288D1;
+$ocean-light: #29B6F6;
+$ocean-surface: #81D4FA;
+$ocean-foam: #E1F5FE;
+$sand: #F5F5F5;
+$pearl: #FFFFFF;
+$coral: #FF7043;
+$seaweed: #4CAF50;
+
+:host {
+  background: $ocean-foam;
+  --ion-background-color: #{$ocean-foam};
+}
+
+// 完全套用串词成文页面的基础样式
+.word-story-content {
+  --background: #{$ocean-foam};
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  overflow: hidden;
+
+  &.has-fixed-bottom-bar {
+    --padding-bottom: 130px;
+  }
+
+  .ocean-bg {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    height: 300px;
+    background: linear-gradient(180deg, $ocean-surface 0%, $ocean-foam 100%);
+    border-radius: 0 0 50% 50% / 0 0 30% 30%;
+    z-index: 0;
+    pointer-events: none;
+  }
+
+  // 漂浮的海洋生物
+  .floating-creatures {
+    position: fixed;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    pointer-events: none;
+    z-index: 1;
+
+    .creature {
+      position: absolute;
+      
+      &.fish-1 {
+        top: 15%;
+        right: 5%;
+        animation: swimRight 8s ease-in-out infinite;
+        
+        .fish-svg {
+          width: 50px;
+          height: 50px;
+          opacity: 0.3;
+          transform: scaleX(-1);
+        }
+      }
+      
+      &.fish-2 {
+        bottom: 20%;
+        left: 5%;
+        animation: swimLeft 10s ease-in-out infinite;
+        
+        .fish-svg {
+          width: 40px;
+          height: 40px;
+          opacity: 0.3;
+        }
+      }
+      
+      &.jellyfish {
+        top: 30%;
+        left: 10%;
+        animation: float 6s ease-in-out infinite;
+      }
+      
+      &.bubble-1 {
+        width: 20px;
+        height: 20px;
+        border-radius: 50%;
+        background: rgba(255, 255, 255, 0.6);
+        top: 20%;
+        left: 20%;
+        animation: bubbleUp 4s ease-in-out infinite;
+      }
+      
+      &.bubble-2 {
+        width: 15px;
+        height: 15px;
+        border-radius: 50%;
+        background: rgba(255, 255, 255, 0.5);
+        top: 40%;
+        right: 15%;
+        animation: bubbleUp 5s ease-in-out infinite 1s;
+      }
+      
+      &.bubble-3 {
+        width: 25px;
+        height: 25px;
+        border-radius: 50%;
+        background: rgba(255, 255, 255, 0.4);
+        bottom: 30%;
+        left: 25%;
+        animation: bubbleUp 6s ease-in-out infinite 0.5s;
+      }
+      
+      &.starfish {
+        bottom: 15%;
+        right: 10%;
+        animation: rotate 12s linear infinite;
+      }
+    }
+  }
+
+  .word-story-container {
+    position: relative;
+    z-index: 2;
+    display: flex;
+    flex-direction: column;
+    min-height: 100%;
+    padding: 0 16px 20px;
+  }
+
+  // 游戏进行中的容器 - 让难度选择器垂直居中
+  .word-story-container > ng-container:first-of-type {
+    display: flex;
+    flex-direction: column;
+    flex: 1;
+    justify-content: center;
+    align-items: center;
+  }
+
+  // 顶部导航 - 完全套用串词成文页面
+  .nav-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 8% 5%;
+
+    .back-btn {
+      width: 40px;
+      height: 40px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      background: $pearl;
+      border-radius: 12px;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      box-shadow: 0 2px 8px rgba(2, 136, 209, 0.15);
+
+      &:active {
+        transform: scale(0.95);
+        background: $ocean-foam;
+      }
+
+      ion-icon {
+        font-size: 1.7rem;
+        color: $ocean-mid;
+      }
+    }
+
+    .page-title {
+      font-size: 1.6rem;
+      font-weight: 600;
+      color: $ocean-deep;
+      margin: 0;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      .title-icon {
+        color: $ocean-mid;
+        font-size: 2rem;
+        font-weight: 700;
+      }
+    }
+
+    .placeholder {
+      width: 40px;
+    }
+  }
+
+  // 贝壳卡片样式
+  .shell-card {
+    background: $pearl;
+    border-radius: 20px;
+    padding: 16px 20px;
+    box-shadow: 0 8px 20px rgba(2, 136, 209, 0.1);
+    border: 2px solid rgba($ocean-surface, 0.5);
+    backdrop-filter: blur(5px);
+    position: relative;
+    overflow: hidden;
+
+    &::before {
+      content: '';
+      position: absolute;
+      top: -10px;
+      right: -10px;
+      width: 60px;
+      height: 60px;
+      background: radial-gradient(circle, rgba($ocean-surface, 0.3) 0%, transparent 70%);
+      border-radius: 50%;
+      z-index: 0;
+    }
+  }
+
+  // 游戏信息栏
+  .game-header {
+    display: flex;
+    justify-content: space-around;
+    align-items: center;
+    margin-bottom: 24px;
+    padding: 16px;
+
+    .stat-item {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 4px;
+      position: relative;
+      z-index: 1;
+
+      .stat-icon {
+        font-size: 1.5rem;
+        color: $ocean-mid;
+        margin-bottom: 4px;
+      }
+
+      .stat-label {
+        font-size: 0.8rem;
+        color: $ocean-mid;
+        font-weight: 500;
+      }
+
+      .stat-value {
+        font-size: 1.4rem;
+        font-weight: 700;
+        color: $ocean-deep;
+      }
+    }
+
+    .stat-divider {
+      width: 1px;
+      height: 30px;
+      background: linear-gradient(to bottom, transparent, $ocean-surface, transparent);
+    }
+  }
+
+  // 难度选择器包装容器 - 垂直居中
+  .difficulty-selector-wrapper {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    flex: 1;
+    width: 100%;
+    padding: 20px 0;
+  }
+
+  // 难度选择器
+  .difficulty-selector {
+    position: relative;
+    background: $pearl;
+    border-radius: 24px;
+    padding: 30px 20px 20px;
+    box-shadow: 0 8px 20px rgba(2, 136, 209, 0.15);
+    border: 2px solid rgba($ocean-surface, 0.5);
+    max-width: 500px;
+    width: 100%;
+
+    .wave-decoration {
+      position: absolute;
+      top: -20px;
+      left: 0;
+      right: 0;
+      height: 40px;
+      overflow: hidden;
+      
+      svg {
+        width: 100%;
+        height: 100%;
+      }
+    }
+
+    .anchor-decoration {
+      position: absolute;
+      top: 20px;
+      right: 20px;
+      opacity: 0.5;
+      animation: float 3s ease-in-out infinite;
+    }
+
+    .starfish-decoration {
+      position: absolute;
+      bottom: 10px;
+      left: 10px;
+      opacity: 0.5;
+      animation: rotate 10s linear infinite;
+    }
+
+    .difficulty-title {
+      text-align: center;
+      font-size: 1.6rem;
+      font-weight: 600;
+      color: $ocean-deep;
+      margin: 0 0 15% 0;
+    }
+
+    .difficulty-buttons-row {
+      display: flex;
+      justify-content: space-around;
+      gap: 12px;
+      margin-bottom: 24px;
+      width: 100%;
+    }
+
+    .difficulty-option {
+      padding: 14px 20px;
+      border-radius: 12px;
+      border: 2px solid #B3E5FC;
+      background: #E1F5FE;
+      color: #01579B;
+      font-size: 1.2rem;
+      font-weight: 600;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      display: inline-flex;
+      flex-direction: column;
+      align-items: center;
+      gap: 8px;
+      flex: 1;
+      min-width: 0;
+
+      .option-icon {
+        font-size: 2rem;
+      }
+
+      .option-text {
+        font-size: 1.1rem;
+        font-weight: 600;
+        color: #01579B;
+      }
+
+      &:hover {
+        border-color: #29B6F6;
+        background: #29B6F6;
+        color: white;
+        transform: translateY(-1px);
+
+        .option-text {
+          color: white;
+        }
+      }
+
+      &:active {
+        transform: scale(0.96);
+      }
+
+      &.selected {
+        background: #4FC3F7;
+        color: white;
+        border-color: #4FC3F7;
+        box-shadow: 0 2px 8px rgba(41, 182, 246, 0.25);
+
+        .option-text {
+          color: white;
+        }
+      }
+    }
+
+    .select-btn {
+      flex: 1;
+      padding: 14px 24px;
+      border: none;
+      border-radius: 15px;
+      font-size: 1.3rem;
+      font-weight: 700;
+      cursor: pointer;
+      transition: all 0.2s ease;
+      outline: none;
+      height: 50px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      margin: 15% auto ;
+      width: 100%;
+      max-width: 300px;
+      
+      // 未选择难度时的样式(禁用状态)
+      opacity: 0.5;
+      cursor: not-allowed;
+      background-color: #11b1f6 !important;
+      color: white !important;
+      border: 3px solid #11b1f6 !important;
+      transform: none !important;
+      box-shadow: none !important;
+      --background: #11b1f6;
+      --color: white;
+      --border-width: 3px;
+      --border-style: solid;
+      --border-color: #11b1f6;
+
+      &::part(native) {
+        letter-spacing: 0.5px;
+      }
+      
+      // 选择难度后的样式(激活状态)
+      &.active {
+        opacity: 1;
+        cursor: pointer;
+        background-color: #11b1f6 !important;
+        color: white !important;
+        border: 3px solid #11b1f6 !important;
+        --background: #11b1f6;
+        --color: white;
+        
+        &:hover {
+          background-color: #0fa3e0 !important;
+          box-shadow: 0 4px 12px rgba(17, 177, 246, 0.3);
+          --background: #0fa3e0;
+        }
+        
+        &:active {
+          transform: scale(0.98) translateY(0);
+        }
+      }
+    }
+  }
+
+  // 卡片网格容器
+  .cards-grid-container {
+    position: relative;
+    margin-top: 20px;
+    
+    .wave-overlay {
+      position: absolute;
+      top: -10px;
+      left: 0;
+      right: 0;
+      height: 20px;
+      background: repeating-linear-gradient(
+        45deg,
+        transparent,
+        transparent 10px,
+        rgba($ocean-surface, 0.3) 10px,
+        rgba($ocean-surface, 0.3) 20px
+      );
+      opacity: 0.3;
+      pointer-events: none;
+    }
+  }
+
+  // 卡片网格
+  .cards-grid {
+    display: grid;
+    grid-template-columns: repeat(3, 1fr);
+    gap: 12px;
+
+    @media (min-width: 768px) {
+      grid-template-columns: repeat(4, 1fr);
+    }
+  }
+
+  .card-wrapper {
+    aspect-ratio: 1;
+    perspective: 1000px;
+  }
+
+  .card {
+    width: 100%;
+    height: 100%;
+    position: relative;
+    cursor: pointer;
+    transform-style: preserve-3d;
+    transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+
+    &.flipped {
+      transform: rotateY(180deg);
+    }
+
+    &.matched {
+      .card-inner {
+        filter: brightness(1.1);
+      }
+      
+      &::after {
+        content: '✓';
+        position: absolute;
+        top: -8px;
+        right: -8px;
+        width: 24px;
+        height: 24px;
+        background: $seaweed;
+        color: white;
+        border-radius: 50%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 1rem;
+        font-weight: bold;
+        z-index: 10;
+        box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4);
+        animation: popIn 0.3s ease-out;
+      }
+    }
+
+    .card-inner {
+      width: 100%;
+      height: 100%;
+      position: relative;
+      transform-style: preserve-3d;
+      transition: all 0.3s ease;
+    }
+
+    .card-front,
+    .card-back {
+      position: absolute;
+      width: 100%;
+      height: 100%;
+      backface-visibility: hidden;
+      -webkit-backface-visibility: hidden;
+      border-radius: 16px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      box-shadow: 0 4px 12px rgba(2, 136, 209, 0.2);
+      border: 2px solid $ocean-surface;
+    }
+
+    .card-front {
+      background: linear-gradient(135deg, $ocean-mid, $ocean-deep);
+      transform: rotateY(0deg);
+
+      .shark-container {
+        width: 80%;
+        height: 80%;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        animation: swim 3s ease-in-out infinite;
+      }
+
+      .shark-image {
+        width: 100%;
+        height: 100%;
+        object-fit: contain;
+        filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
+      }
+    }
+
+    .card-back {
+      background: linear-gradient(135deg, $pearl, $ocean-foam);
+      transform: rotateY(180deg);
+      padding: 8px;
+
+      .card-text {
+        font-size: 1rem;
+        font-weight: 600;
+        color: $ocean-deep;
+        text-align: center;
+        word-break: break-word;
+
+        @media (min-width: 768px) {
+          font-size: 1.2rem;
+        }
+      }
+    }
+  }
+
+  // 游戏提示
+  .game-tip {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    gap: 8px;
+    margin-top: 20px;
+    padding: 12px;
+    background: rgba($pearl, 0.8);
+    border-radius: 30px;
+    border: 1px solid rgba($ocean-surface, 0.5);
+    backdrop-filter: blur(5px);
+
+    .tip-icon {
+      font-size: 1.2rem;
+      color: $ocean-mid;
+    }
+
+    .tip-text {
+      font-size: 0.9rem;
+      color: $ocean-deep;
+      font-weight: 500;
+    }
+  }
+
+  // 游戏结束页面
+  .game-over {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    min-height: 100%;
+    padding: 20px;
+
+    .result-card {
+      max-width: 400px;
+      width: 100%;
+      text-align: center;
+      position: relative;
+
+      .confetti {
+        position: absolute;
+        top: 0;
+        left: 0;
+        right: 0;
+        pointer-events: none;
+        
+        .confetti-piece {
+          position: absolute;
+          width: 10px;
+          height: 20px;
+          background: $coral;
+          top: 0;
+          opacity: 0;
+          
+          &:nth-child(1) {
+            left: 10%;
+            animation: confetti 2s ease-in-out infinite;
+            background: #FFB74D;
+          }
+          
+          &:nth-child(2) {
+            left: 30%;
+            animation: confetti 2s ease-in-out infinite 0.3s;
+            background: #4FC3F7;
+          }
+          
+          &:nth-child(3) {
+            left: 60%;
+            animation: confetti 2s ease-in-out infinite 0.6s;
+            background: #81C784;
+          }
+          
+          &:nth-child(4) {
+            left: 80%;
+            animation: confetti 2s ease-in-out infinite 0.9s;
+            background: #FF8A80;
+          }
+        }
+      }
+
+      .result-icon {
+        width: 100px;
+        height: 100px;
+        border-radius: 50%;
+        background: linear-gradient(135deg, $ocean-surface, $ocean-mid);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        font-size: 3rem;
+        margin: -50px auto 20px;
+        box-shadow: 0 8px 20px rgba(2, 136, 209, 0.3);
+        position: relative;
+        overflow: hidden;
+
+        &.win {
+          background: linear-gradient(135deg, #FFB74D, #FF9800);
+        }
+
+        .bubble-animation {
+          position: absolute;
+          top: 0;
+          left: 0;
+          right: 0;
+          bottom: 0;
+          background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.8) 0%, transparent 50%);
+          animation: bubble 2s ease-in-out infinite;
+        }
+
+        .result-shark {
+          width: 60%;
+          height: 60%;
+          object-fit: contain;
+          z-index: 1;
+          filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2));
+          animation: jump 1s ease-in-out infinite;
+        }
+      }
+
+      .treasure-chest {
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        gap: 8px;
+        margin: 10px 0;
+        
+        .treasure-text {
+          font-size: 1.2rem;
+          font-weight: 600;
+          color: #FFB74D;
+          animation: glow 1.5s ease-in-out infinite;
+        }
+      }
+
+      .result-title {
+        font-size: 1.8rem;
+        font-weight: 700;
+        margin: 16px 0;
+        background: linear-gradient(135deg, $ocean-deep, $ocean-mid);
+        -webkit-background-clip: text;
+        -webkit-text-fill-color: transparent;
+        background-clip: text;
+      }
+
+      .result-stats {
+        display: grid;
+        grid-template-columns: repeat(3, 1fr);
+        gap: 12px;
+        margin: 20px 0;
+
+        .stat-item {
+          .stat-icon {
+            font-size: 1.2rem;
+            color: $ocean-mid;
+            margin-bottom: 4px;
+          }
+
+          .stat-label {
+            font-size: 0.8rem;
+            color: #78909C;
+            display: block;
+            margin-bottom: 4px;
+          }
+
+          .stat-value {
+            font-size: 1.2rem;
+            font-weight: 700;
+            color: $ocean-deep;
+
+            &.ocean-number {
+              color: $ocean-mid;
+            }
+          }
+        }
+      }
+
+      .button-group {
+        display: flex;
+        gap: 12px;
+        margin-top: 20px;
+      }
+
+      .ocean-btn {
+        --border-radius: 16px;
+        --padding-top: 14px;
+        --padding-bottom: 14px;
+        height: auto;
+        margin: 0;
+        font-weight: 600;
+
+        &.primary {
+          --background: linear-gradient(135deg, $ocean-mid, $ocean-deep);
+          --color: white;
+        }
+
+        &.secondary {
+          --background: transparent;
+          --color: $ocean-mid;
+          --border-color: $ocean-mid;
+          --border-width: 2px;
+          --border-style: solid;
+        }
+      }
+    }
+  }
+
+  // 动画
+  @keyframes swim {
+    0%, 100% {
+      transform: translateY(0) rotate(0deg);
+    }
+    25% {
+      transform: translateY(-3px) rotate(-2deg);
+    }
+    75% {
+      transform: translateY(3px) rotate(2deg);
+    }
+  }
+
+  @keyframes swimRight {
+    0%, 100% {
+      transform: translateX(0) translateY(0);
+    }
+    25% {
+      transform: translateX(-20px) translateY(-10px);
+    }
+    75% {
+      transform: translateX(20px) translateY(10px);
+    }
+  }
+
+  @keyframes swimLeft {
+    0%, 100% {
+      transform: translateX(0) translateY(0) scaleX(-1);
+    }
+    25% {
+      transform: translateX(20px) translateY(10px) scaleX(-1);
+    }
+    75% {
+      transform: translateX(-20px) translateY(-10px) scaleX(-1);
+    }
+  }
+
+  @keyframes float {
+    0%, 100% {
+      transform: translateY(0);
+    }
+    50% {
+      transform: translateY(-15px);
+    }
+  }
+
+  @keyframes bubbleUp {
+    0% {
+      transform: translateY(0) scale(1);
+      opacity: 0.6;
+    }
+    50% {
+      transform: translateY(-30px) scale(1.2);
+      opacity: 0.8;
+    }
+    100% {
+      transform: translateY(-60px) scale(1.5);
+      opacity: 0;
+    }
+  }
+
+  @keyframes rotate {
+    from {
+      transform: rotate(0deg);
+    }
+    to {
+      transform: rotate(360deg);
+    }
+  }
+
+  @keyframes popIn {
+    0% {
+      transform: scale(0);
+    }
+    50% {
+      transform: scale(1.2);
+    }
+    100% {
+      transform: scale(1);
+    }
+  }
+
+  @keyframes bubble {
+    0% {
+      transform: scale(0.8);
+      opacity: 0.8;
+    }
+    50% {
+      transform: scale(1.2);
+      opacity: 0.4;
+    }
+    100% {
+      transform: scale(0.8);
+      opacity: 0.8;
+    }
+  }
+
+  @keyframes jump {
+    0%, 100% {
+      transform: translateY(0);
+    }
+    50% {
+      transform: translateY(-5px);
+    }
+  }
+
+  @keyframes glow {
+    0%, 100% {
+      filter: drop-shadow(0 0 2px #FFB74D);
+    }
+    50% {
+      filter: drop-shadow(0 0 10px #FFB74D);
+    }
+  }
+
+  @keyframes confetti {
+    0% {
+      transform: translateY(0) rotate(0deg);
+      opacity: 1;
+    }
+    100% {
+      transform: translateY(100px) rotate(720deg);
+      opacity: 0;
+    }
+  }
+}

+ 17 - 0
word-app/src/app/word-match/word-match.page.spec.ts

@@ -0,0 +1,17 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { WordMatchPage } from './word-match.page';
+
+describe('WordMatchPage', () => {
+  let component: WordMatchPage;
+  let fixture: ComponentFixture<WordMatchPage>;
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(WordMatchPage);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 286 - 0
word-app/src/app/word-match/word-match.page.ts

@@ -0,0 +1,286 @@
+// word-match.page.ts (保持不变,但需要确保导入正确的图标)
+import { Component, NgZone } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { Router } from '@angular/router';
+import { 
+  IonButton, 
+  IonContent,
+  IonIcon
+} from '@ionic/angular/standalone';
+import { addIcons } from 'ionicons';
+import { 
+  arrowBackOutline, 
+  boatOutline, 
+  refreshOutline, 
+  optionsOutline,
+  timeOutline,
+  starOutline,
+  checkmarkCircleOutline,
+  informationCircleOutline,
+  trophyOutline,
+  fishOutline,
+  hourglassOutline,
+  playOutline
+} from 'ionicons/icons';
+
+// 静态单词数据 - 英文-中文配对
+const STATIC_WORDS = [
+  { english: 'apple', chinese: '苹果' },
+  { english: 'banana', chinese: '香蕉' },
+  { english: 'cat', chinese: '猫' },
+  { english: 'dog', chinese: '狗' },
+  { english: 'book', chinese: '书' },
+  { english: 'car', chinese: '汽车' },
+  { english: 'house', chinese: '房子' },
+  { english: 'water', chinese: '水' },
+  { english: 'fire', chinese: '火' },
+  { english: 'tree', chinese: '树' },
+  { english: 'sun', chinese: '太阳' },
+  { english: 'moon', chinese: '月亮' },
+  { english: 'star', chinese: '星星' },
+  { english: 'bird', chinese: '鸟' },
+  { english: 'fish', chinese: '鱼' },
+  { english: 'flower', chinese: '花' }
+];
+
+type Card = {
+  id: number;
+  content: string;
+  isWord: boolean;
+  isFlipped: boolean;
+  isMatched: boolean;
+  pairId: number;
+};
+
+type Difficulty = 'easy' | 'medium' | 'hard';
+
+@Component({
+  selector: 'app-word-match',
+  templateUrl: './word-match.page.html',
+  styleUrls: ['./word-match.page.scss'],
+  standalone: true,
+  imports: [
+    CommonModule,
+    IonButton, 
+    IonContent,
+    IonIcon
+  ]
+})
+export class WordMatchPage {
+  gameState = {
+    cards: [] as Card[],
+    flippedIndices: [] as number[],
+    matchedPairs: 0,
+    timeLeft: 90,
+    isGameOver: false,
+    score: 0,
+    timer: null as any,
+    difficulty: 'medium' as Difficulty
+  };
+  
+  selectedDifficulty: Difficulty | null = null;
+  private flipTimeout: any = null;
+  private canFlip: boolean = true;
+
+  constructor(
+    private router: Router,
+    private ngZone: NgZone
+  ) {
+    addIcons({ 
+      arrowBackOutline, 
+      boatOutline, 
+      refreshOutline, 
+      optionsOutline,
+      timeOutline,
+      starOutline,
+      checkmarkCircleOutline,
+      informationCircleOutline,
+      trophyOutline,
+      fishOutline,
+      hourglassOutline,
+      playOutline
+    });
+  }
+
+  // 返回上一页
+  goBack() {
+    this.router.navigate(['/tabs/tab2']);
+  }
+
+  // 重置到难度选择
+  resetToDifficultySelect() {
+    this.resetGame();
+  }
+
+  startGame(difficulty: Difficulty) {
+    this.selectedDifficulty = difficulty;
+    this.gameState.difficulty = difficulty;
+    this.resetGame();
+    this.setupCards();
+    this.startTimer();
+  }
+
+  private resetGame() {
+    if (this.gameState.timer) {
+      clearInterval(this.gameState.timer);
+    }
+    if (this.flipTimeout) {
+      clearTimeout(this.flipTimeout);
+      this.flipTimeout = null;
+    }
+    
+    this.gameState = {
+      cards: [],
+      flippedIndices: [],
+      matchedPairs: 0,
+      timeLeft: 90,
+      isGameOver: false,
+      score: 0,
+      difficulty: this.gameState.difficulty,
+      timer: null
+    };
+    this.canFlip = true;
+  }
+
+  private setupCards() {
+    const pairCount = this.getCardCountByDifficulty() / 2;
+    
+    // 从静态数据中随机选取指定数量的单词对
+    const shuffledWords = [...STATIC_WORDS].sort(() => Math.random() - 0.5);
+    const selectedWords = shuffledWords.slice(0, pairCount);
+    
+    // Create word cards
+    const wordCards: Card[] = selectedWords.map((word, index) => ({
+      id: index * 2,
+      content: word.english,
+      isWord: true,
+      isFlipped: false,
+      isMatched: false,
+      pairId: index
+    }));
+    
+    // Create definition cards
+    const definitionCards: Card[] = selectedWords.map((word, index) => ({
+      id: index * 2 + 1,
+      content: word.chinese,
+      isWord: false,
+      isFlipped: false,
+      isMatched: false,
+      pairId: index
+    }));
+    
+    // Combine and shuffle
+    this.gameState.cards = [...wordCards, ...definitionCards]
+      .sort(() => Math.random() - 0.5);
+  }
+
+  private startTimer() {
+    this.gameState.timer = setInterval(() => {
+      this.ngZone.run(() => {
+        this.gameState.timeLeft--;
+        if (this.gameState.timeLeft <= 0) {
+          this.endGame();
+        }
+      });
+    }, 1000);
+  }
+
+  onCardClick(index: number) {
+    this.ngZone.run(() => {
+      this.flipCard(index);
+    });
+  }
+
+  flipCard(index: number) {
+    const card = this.gameState.cards[index];
+    
+    // 检查是否可以翻转
+    if (!this.canFlip || 
+        card.isFlipped || 
+        card.isMatched || 
+        this.gameState.flippedIndices.length >= 2 ||
+        this.gameState.isGameOver) {
+      return;
+    }
+    
+    // 翻转卡片
+    card.isFlipped = true;
+    this.gameState.flippedIndices.push(index);
+    
+    // 如果已经有两张卡片翻转,检查匹配
+    if (this.gameState.flippedIndices.length === 2) {
+      this.canFlip = false; // 禁止继续翻转
+      this.checkMatch();
+    }
+  }
+
+  private checkMatch() {
+    const [firstIndex, secondIndex] = this.gameState.flippedIndices;
+    const firstCard = this.gameState.cards[firstIndex];
+    const secondCard = this.gameState.cards[secondIndex];
+    
+    // 检查是否匹配(一个英文一个中文,且pairId相同)
+    if (firstCard.pairId === secondCard.pairId && firstCard.isWord !== secondCard.isWord) {
+      // 匹配成功
+      firstCard.isMatched = true;
+      secondCard.isMatched = true;
+      this.gameState.matchedPairs++;
+      
+      // 加分
+      this.gameState.score += 10;
+      
+      // 清空翻转索引
+      this.gameState.flippedIndices = [];
+      this.canFlip = true;
+      
+      // 检查是否获胜
+      if (this.gameState.matchedPairs === this.getTotalPairs()) {
+        this.endGame(true);
+      }
+    } else {
+      // 匹配失败,延迟后翻回
+      this.flipTimeout = setTimeout(() => {
+        this.ngZone.run(() => {
+          // 翻回卡片
+          if (firstIndex !== undefined && this.gameState.cards[firstIndex]) {
+            this.gameState.cards[firstIndex].isFlipped = false;
+          }
+          if (secondIndex !== undefined && this.gameState.cards[secondIndex]) {
+            this.gameState.cards[secondIndex].isFlipped = false;
+          }
+          
+          // 扣分
+          this.gameState.score = Math.max(0, this.gameState.score - 2);
+          
+          // 清空翻转索引并允许继续翻转
+          this.gameState.flippedIndices = [];
+          this.canFlip = true;
+          this.flipTimeout = null;
+        });
+      }, 800); // 800ms后翻回
+    }
+  }
+
+  endGame(isWin = false) {
+    clearInterval(this.gameState.timer);
+    this.gameState.isGameOver = true;
+    
+    if (isWin) {
+      // 获胜奖励:剩余时间加分
+      this.gameState.score += Math.floor(this.gameState.timeLeft * 2);
+    }
+  }
+
+  getCardCountByDifficulty(): number {
+    switch (this.gameState.difficulty) {
+      case 'easy': return 8;   // 4对
+      case 'medium': return 12; // 6对
+      case 'hard': return 16;   // 8对
+      default: return 12;
+    }
+  }
+
+  getTotalPairs(): number {
+    return this.getCardCountByDifficulty() / 2;
+  }
+}

ファイルの差分が大きいため隠しています
+ 0 - 0
word-app/src/assets/icon/鲨鱼.svg


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません