Bladeren bron

完成消消乐游戏页面

s202226701051 7 maanden geleden
bovenliggende
commit
ad7327c4e3

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

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

+ 24 - 21
word-app/src/app/word-match/word-match.page.html

@@ -30,12 +30,12 @@
   
   <div class="word-story-container">
     <!-- 顶部导航 - 完全套用串词成文页面的船图标 -->
-    <div class="nav-header">
+<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>
+        <img src="assets/icon/鲨鱼头部.svg" alt="shark head" class="title-icon">
         单词配对消消乐
       </h1>
       <div class="placeholder"></div>
@@ -139,25 +139,28 @@
             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 
+  class="card"
+  [class.flipped]="card.isFlipped || card.isMatched"
+  [class.matched]="card.isMatched"
+  [class.animating]="card.isAnimating" 
+  (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 class="check-mark" *ngIf="card.isMatched">✓</div>
+    </div>
+  </div>
+</div>
           </div>
         </div>
       </div>

+ 146 - 82
word-app/src/app/word-match/word-match.page.scss

@@ -179,10 +179,10 @@ $seaweed: #4CAF50;
       align-items: center;
       gap: 8px;
 
-      .title-icon {
-        color: $ocean-mid;
-        font-size: 2rem;
-        font-weight: 700;
+.title-icon {
+        width: 2.3rem;
+        height: 2.3rem;
+        object-fit: contain;
       }
     }
 
@@ -477,106 +477,170 @@ $seaweed: #4CAF50;
     perspective: 1000px;
   }
 
-  .card {
+ // 卡片样式
+.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 {
+    pointer-events: none;  // 禁止点击
+  }
+
+  // 放大动画
+  &.animating {
+    animation: cardPop 0.3s ease-out;
+  }
+
+  .card-inner {
     width: 100%;
     height: 100%;
     position: relative;
-    cursor: pointer;
     transform-style: preserve-3d;
-    transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+    transition: all 0.3s ease;
+  }
 
-    &.flipped {
-      transform: rotateY(180deg);
-    }
+  .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;
+    transition: all 0.3s ease;
+  }
 
-    &.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-front {
+    background: linear-gradient(135deg, $ocean-mid, $ocean-deep);
+    transform: rotateY(0deg);
 
-    .card-inner {
-      width: 100%;
-      height: 100%;
-      position: relative;
-      transform-style: preserve-3d;
-      transition: all 0.3s ease;
+    .shark-container {
+      width: 80%;
+      height: 80%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      animation: swim 3s ease-in-out infinite;
     }
 
-    .card-front,
-    .card-back {
-      position: absolute;
+    .shark-image {
       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;
+      object-fit: contain;
+      filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
     }
+  }
 
-    .card-front {
-      background: linear-gradient(135deg, $ocean-mid, $ocean-deep);
-      transform: rotateY(0deg);
+  .card-back {
+    background: linear-gradient(135deg, $pearl, $ocean-foam);
+    transform: rotateY(180deg);
+    padding: 8px;
+    position: relative;
 
-      .shark-container {
-        width: 80%;
-        height: 80%;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        animation: swim 3s ease-in-out infinite;
+    .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;
       }
+    }
 
-      .shark-image {
-        width: 100%;
-        height: 100%;
-        object-fit: contain;
-        filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
+    // 匹配成功时的勾勾
+    .check-mark {
+      position: absolute;
+      top: -8px;
+      right: -8px;
+      width: 28px;
+      height: 28px;
+      background: #4CAF50;
+      color: white;
+      border-radius: 50%;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 1.2rem;
+      font-weight: bold;
+      z-index: 20;
+      box-shadow: 0 2px 8px rgba(76, 175, 80, 0.4);
+      animation: popIn 0.3s ease-out;
+      
+      // 光晕效果
+      &::after {
+        content: '';
+        position: absolute;
+        top: -2px;
+        left: -2px;
+        right: -2px;
+        bottom: -2px;
+        border-radius: 50%;
+        background: rgba(76, 175, 80, 0.2);
+        z-index: -1;
+        animation: pulse 1.5s ease-in-out infinite;
       }
     }
+  }
+}
 
-    .card-back {
-      background: linear-gradient(135deg, $pearl, $ocean-foam);
-      transform: rotateY(180deg);
-      padding: 8px;
+// 卡片放大动画
+@keyframes cardPop {
+  0% {
+    transform: scale(1) rotateY(180deg);
+  }
+  50% {
+    transform: scale(1.1) rotateY(180deg);
+  }
+  100% {
+    transform: scale(1) rotateY(180deg);
+  }
+}
 
-      .card-text {
-        font-size: 1rem;
-        font-weight: 600;
-        color: $ocean-deep;
-        text-align: center;
-        word-break: break-word;
+// 脉冲动画
+@keyframes pulse {
+  0% {
+    transform: scale(1);
+    opacity: 0.6;
+  }
+  50% {
+    transform: scale(1.2);
+    opacity: 0.2;
+  }
+  100% {
+    transform: scale(1);
+    opacity: 0.6;
+  }
+}
 
-        @media (min-width: 768px) {
-          font-size: 1.2rem;
-        }
-      }
-    }
+// 勾勾出现动画
+@keyframes popIn {
+  0% {
+    transform: scale(0);
+  }
+  50% {
+    transform: scale(1.2);
   }
+  100% {
+    transform: scale(1);
+  }
+}
 
   // 游戏提示
   .game-tip {

+ 136 - 89
word-app/src/app/word-match/word-match.page.ts

@@ -50,6 +50,7 @@ type Card = {
   isFlipped: boolean;
   isMatched: boolean;
   pairId: number;
+  isAnimating?: boolean;  // 添加这一行
 };
 
 type Difficulty = 'easy' | 'medium' | 'hard';
@@ -71,12 +72,25 @@ export class WordMatchPage {
     cards: [] as Card[],
     flippedIndices: [] as number[],
     matchedPairs: 0,
-    timeLeft: 90,
+    timeLeft: 60,
     isGameOver: false,
     score: 0,
     timer: null as any,
     difficulty: 'medium' as Difficulty
   };
+
+  // 在类顶部添加时间奖励常量
+private readonly SCORE_RULES = {
+  BASE_MATCH: 10,          // 每对匹配基础分
+  COMPLETION_BONUS: 5,     // 完成游戏基础奖励
+  TIME_BONUS: [            // 时间奖励阶梯
+    { threshold: 10, bonus: 20 },
+    { threshold: 15, bonus: 15 },
+    { threshold: 20, bonus: 10 },
+    { threshold: 30, bonus: 8 },
+    { threshold: Infinity, bonus: 0 }
+  ]
+};
   
   selectedDifficulty: Difficulty | null = null;
   private flipTimeout: any = null;
@@ -143,36 +157,39 @@ export class WordMatchPage {
   }
 
   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);
-  }
+  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,
+    isAnimating: false  // 添加这一行
+  }));
+  
+  // 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,
+    isAnimating: false  // 添加这一行
+  }));
+  
+  // Combine and shuffle
+  this.gameState.cards = [...wordCards, ...definitionCards]
+    .sort(() => Math.random() - 0.5);
+}
+
 
   private startTimer() {
     this.gameState.timer = setInterval(() => {
@@ -214,72 +231,102 @@ export class WordMatchPage {
     }
   }
 
-  private checkMatch() {
-    const [firstIndex, secondIndex] = this.gameState.flippedIndices;
-    const firstCard = this.gameState.cards[firstIndex];
-    const secondCard = this.gameState.cards[secondIndex];
+ private checkMatch() {
+  const [firstIndex, secondIndex] = this.gameState.flippedIndices;
+  const firstCard = this.gameState.cards[firstIndex];
+  const secondCard = this.gameState.cards[secondIndex];
+  
+  // 检查是否匹配
+  if (firstCard.pairId === secondCard.pairId && firstCard.isWord !== secondCard.isWord) {
+    // 匹配成功 - 先等待两张卡片都完成翻转动画
+    this.canFlip = false;
     
-    // 检查是否匹配(一个英文一个中文,且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 = [];
+    // 延迟400ms确保动画完成
+    setTimeout(() => {
+      this.ngZone.run(() => {
+        firstCard.isMatched = true;
+        secondCard.isMatched = true;
+        
+        // 添加匹配成功动画
+        firstCard.isAnimating = true;
+        secondCard.isAnimating = true;
+        
+        this.gameState.matchedPairs++;
+        this.gameState.score += this.SCORE_RULES.BASE_MATCH;
+        this.gameState.flippedIndices = [];
+        
+        // 300ms后移除动画
+        setTimeout(() => {
+          firstCard.isAnimating = false;
+          secondCard.isAnimating = false;
           this.canFlip = true;
-          this.flipTimeout = null;
-        });
-      }, 800); // 800ms后翻回
-    }
+          
+          // 检查是否获胜
+          if (this.gameState.matchedPairs === this.getTotalPairs()) {
+             // 立即停止计时器
+  clearInterval(this.gameState.timer);
+  this.gameState.timer = null;
+  
+  // 延迟1.5秒后进入结算(仅用于动画效果)
+  setTimeout(() => this.endGame(true), 1500);
+          }
+        }, 300);
+      });
+    }, 400);
+  }  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);
   }
+}
 
-  endGame(isWin = false) {
-    clearInterval(this.gameState.timer);
-    this.gameState.isGameOver = true;
-    
-    if (isWin) {
-      // 获胜奖励:剩余时间加分
-      this.gameState.score += Math.floor(this.gameState.timeLeft * 2);
-    }
-  }
+endGame(isWin = false) {
+  clearInterval(this.gameState.timer);
+  this.gameState.isGameOver = true;
+  
+  // 计算时间奖励(剩余时间 = 总时间90秒 - 已用时间)
+  const timeUsed = 90 - this.gameState.timeLeft;
+  let timeBonus = 0;
 
-  getCardCountByDifficulty(): number {
-    switch (this.gameState.difficulty) {
-      case 'easy': return 8;   // 4对
-      case 'medium': return 12; // 6对
-      case 'hard': return 16;   // 8对
-      default: return 12;
+  if (isWin) {
+    // 应用时间阶梯奖励
+    for (const rule of this.SCORE_RULES.TIME_BONUS) {
+      if (timeUsed <= rule.threshold) {
+        timeBonus = rule.bonus;
+        break;
+      }
     }
+    
+    // 总得分 = 匹配分 + 完成奖励 + 时间奖励
+    this.gameState.score += this.SCORE_RULES.COMPLETION_BONUS + timeBonus;
   }
+}
 
+private getCardCountByDifficulty(): number {
+  switch (this.gameState.difficulty) {
+    case 'easy': return 6;   // 3对 (6张卡片)
+    case 'medium': return 10; // 5对 (10张卡片)
+    case 'hard': return 12;   // 6对 (12张卡片)
+    default: return 10;
+  }
+}
   getTotalPairs(): number {
     return this.getCardCountByDifficulty() / 2;
   }

File diff suppressed because it is too large
+ 0 - 0
word-app/src/assets/icon/单词消消乐.svg


File diff suppressed because it is too large
+ 0 - 0
word-app/src/assets/icon/鲨鱼头部.svg


Some files were not shown because too many files changed in this diff