s202226701051 пре 6 месеци
родитељ
комит
2e093b7e6b

+ 29 - 20
word-app/src/app/word-match/word-match.page.html

@@ -196,25 +196,31 @@
           {{gameState.matchedPairs === getTotalPairs() ? '挑战成功!' : '游戏结束!'}}
         </h2>
         
-        <div class="victory-stars">
+        <div class="victory-stars" [class.stars-three]="gameState.victoryStarCount >= 1">
           <div class="victory-stars-row">
-            <div class="cartoon-star cartoon-star--arch-left" style="--star-tilt: -10deg">
-              <svg class="cartoon-star-svg" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
-                <path *ngIf="isVictoryStarEarned(0)" [attr.d]="victoryStarSolidPath" [attr.fill]="victoryStarSolidFillColor()"/>
-                <path *ngIf="!isVictoryStarEarned(0)" [attr.d]="victoryStarHollowPath" [attr.fill]="victoryStarHollowFillColor()"/>
-              </svg>
+            <div class="cartoon-star cartoon-star--arch-left" [class.star-earned]="isVictoryStarEarned(0)" style="--star-tilt: -10deg">
+              <div class="star-spin-inner">
+                <svg class="cartoon-star-svg" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
+                  <path *ngIf="isVictoryStarEarned(0)" [attr.d]="victoryStarSolidPath" [attr.fill]="victoryStarSolidFillColor()"/>
+                  <path *ngIf="!isVictoryStarEarned(0)" [attr.d]="victoryStarHollowPath" [attr.fill]="victoryStarHollowFillColor()"/>
+                </svg>
+              </div>
             </div>
-            <div class="cartoon-star cartoon-star--arch-mid" style="--star-tilt: 0deg">
-              <svg class="cartoon-star-svg" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
-                <path *ngIf="isVictoryStarEarned(1)" [attr.d]="victoryStarSolidPath" [attr.fill]="victoryStarSolidFillColor()"/>
-                <path *ngIf="!isVictoryStarEarned(1)" [attr.d]="victoryStarHollowPath" [attr.fill]="victoryStarHollowFillColor()"/>
-              </svg>
+            <div class="cartoon-star cartoon-star--arch-mid" [class.star-earned]="isVictoryStarEarned(1)" style="--star-tilt: 0deg">
+              <div class="star-spin-inner">
+                <svg class="cartoon-star-svg" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
+                  <path *ngIf="isVictoryStarEarned(1)" [attr.d]="victoryStarSolidPath" [attr.fill]="victoryStarSolidFillColor()"/>
+                  <path *ngIf="!isVictoryStarEarned(1)" [attr.d]="victoryStarHollowPath" [attr.fill]="victoryStarHollowFillColor()"/>
+                </svg>
+              </div>
             </div>
-            <div class="cartoon-star cartoon-star--arch-right" style="--star-tilt: 10deg">
-              <svg class="cartoon-star-svg" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
-                <path *ngIf="isVictoryStarEarned(2)" [attr.d]="victoryStarSolidPath" [attr.fill]="victoryStarSolidFillColor()"/>
-                <path *ngIf="!isVictoryStarEarned(2)" [attr.d]="victoryStarHollowPath" [attr.fill]="victoryStarHollowFillColor()"/>
-              </svg>
+            <div class="cartoon-star cartoon-star--arch-right" [class.star-earned]="isVictoryStarEarned(2)" style="--star-tilt: 10deg">
+              <div class="star-spin-inner">
+                <svg class="cartoon-star-svg" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
+                  <path *ngIf="isVictoryStarEarned(2)" [attr.d]="victoryStarSolidPath" [attr.fill]="victoryStarSolidFillColor()"/>
+                  <path *ngIf="!isVictoryStarEarned(2)" [attr.d]="victoryStarHollowPath" [attr.fill]="victoryStarHollowFillColor()"/>
+                </svg>
+              </div>
             </div>
           </div>
         </div>
@@ -254,16 +260,19 @@
   <!-- 退出确认弹窗(复用串词成文页面样式) -->
   <div class="wizard-modal-overlay" *ngIf="showExitModal">
     <div class="wizard-modal-container exit-modal-container" (click)="$event.stopPropagation()">
-      <!-- 3 秒倒计时:圆圈 + 数字 -->
-      <div class="exit-modal-countdown" *ngIf="continueCountdown !== null">
+      <!-- 3 秒倒计时:圆圈 + 数字,显示完 1 后再显示 GO -->
+      <div class="exit-modal-countdown" *ngIf="continueCountdown !== null || showGo">
         <div class="countdown-circle">
-          <span class="countdown-number">{{ continueCountdown }}</span>
+          <ng-container *ngIf="continueCountdown !== null">
+            <span *ngFor="let n of getCountdownArray(); trackBy: countdownTrackBy" class="countdown-number">{{ n }}</span>
+          </ng-container>
+          <span *ngIf="showGo" class="countdown-number countdown-go">GO</span>
         </div>
         <p class="countdown-hint">即将继续游戏</p>
       </div>
 
       <!-- 是否退出 + 退出/继续 按钮 -->
-      <ng-container *ngIf="continueCountdown === null">
+      <ng-container *ngIf="continueCountdown === null && !showGo">
         <div class="wizard-header">
           <h3>是否退出游戏</h3>
         </div>

+ 63 - 2
word-app/src/app/word-match/word-match.page.scss

@@ -73,6 +73,12 @@ ion-content {
     font-weight: 800;
     color: $ocean-deep;
     line-height: 1;
+    animation: countdown-pop 0.45s ease-out;
+
+    &.countdown-go {
+      font-size: 2.2rem;
+      letter-spacing: 0.08em;
+    }
   }
 
   .countdown-hint {
@@ -83,6 +89,38 @@ ion-content {
   }
 }
 
+@keyframes countdown-pop {
+  0% { transform: scale(1); }
+  45% { transform: scale(1.35); }
+  100% { transform: scale(1); }
+}
+
+// 星星进入动画:由小变大带轻微弹跳,左→中→右错开
+@keyframes star-enter-left {
+  0% { opacity: 0; transform: translateY(14px) rotate(var(--star-tilt, -10deg)) scale(0); }
+  55% { opacity: 1; transform: translateY(14px) rotate(var(--star-tilt, -10deg)) scale(1.05); }
+  100% { opacity: 1; transform: translateY(14px) rotate(var(--star-tilt, -10deg)) scale(0.92); }
+}
+@keyframes star-enter-mid {
+  0% { opacity: 0; transform: translateY(-6px) rotate(var(--star-tilt, 0deg)) scale(0); }
+  55% { opacity: 1; transform: translateY(-6px) rotate(var(--star-tilt, 0deg)) scale(1.18); }
+  100% { opacity: 1; transform: translateY(-6px) rotate(var(--star-tilt, 0deg)) scale(1.08); }
+}
+@keyframes star-enter-right {
+  0% { opacity: 0; transform: translateY(14px) rotate(var(--star-tilt, 10deg)) scale(0); }
+  55% { opacity: 1; transform: translateY(14px) rotate(var(--star-tilt, 10deg)) scale(1.05); }
+  100% { opacity: 1; transform: translateY(14px) rotate(var(--star-tilt, 10deg)) scale(0.92); }
+}
+
+// 点亮星庆祝:放大 → 绕中心竖轴旋转 → 还原(做得更明显)
+@keyframes star-spin-inner {
+  0%   { transform: scale(1) rotateY(0deg); }
+  15%  { transform: scale(1.35) rotateY(0deg); }
+  50%  { transform: scale(1.35) rotateY(360deg); }
+  88%  { transform: scale(1.08) rotateY(360deg); }
+  100% { transform: scale(1) rotateY(360deg); }
+}
+
 .wizard-modal-overlay .wizard-header {
   padding: 0 0 10px 0;
   text-align: center;
@@ -978,10 +1016,23 @@ ion-content {
         }
       }
 
-      // 通关卡通胖星星(替代宝箱)
+      // 通关卡通胖星星(替代宝箱)+ 进入动画
       .victory-stars {
         margin: 8px 0 14px;
         text-align: center;
+
+        &.stars-three {
+          perspective: 240px;
+          .cartoon-star--arch-left.star-earned .star-spin-inner {
+            animation: star-spin-inner 1.5s ease-in-out 0.8s both;
+          }
+          .cartoon-star--arch-mid.star-earned .star-spin-inner {
+            animation: star-spin-inner 1.5s ease-in-out 0.98s both;
+          }
+          .cartoon-star--arch-right.star-earned .star-spin-inner {
+            animation: star-spin-inner 1.5s ease-in-out 1.16s both;
+          }
+        }
       }
 
       .victory-stars-row {
@@ -1000,17 +1051,27 @@ ion-content {
         transform: rotate(var(--star-tilt, 0deg));
         position: relative;
         transition: transform 0.2s ease;
-
         filter: drop-shadow(0 3px 6px rgba(30, 80, 120, 0.15));
 
         &--arch-left {
           transform: translateY(14px) rotate(var(--star-tilt, 0deg)) scale(0.92);
+          animation: star-enter-left 0.5s ease-out both;
         }
         &--arch-mid {
           transform: translateY(-6px) rotate(var(--star-tilt, 0deg)) scale(1.08);
+          animation: star-enter-mid 0.5s ease-out 0.12s both;
         }
         &--arch-right {
           transform: translateY(14px) rotate(var(--star-tilt, 0deg)) scale(0.92);
+          animation: star-enter-right 0.5s ease-out 0.24s both;
+        }
+
+        .star-spin-inner {
+          width: 100%;
+          height: 100%;
+          display: block;
+          transform-style: preserve-3d;
+          transform-origin: center center;
         }
 
         .cartoon-star-svg {

+ 28 - 3
word-app/src/app/word-match/word-match.page.ts

@@ -116,7 +116,18 @@ private readonly SCORE_RULES = {
   showExitModal: boolean = false;
   /** 点击「继续」后的倒计时 3、2、1,null 表示未在倒计时 */
   continueCountdown: number | null = null;
+  /** 显示完 1 后显示 GO,再关闭弹窗 */
+  showGo: boolean = false;
   private continueCountdownInterval: any = null;
+  private goTimeout: any = null;
+
+  /** 供模板 *ngFor 用,数字变化时重建节点以重播动画 */
+  getCountdownArray(): number[] {
+    return this.continueCountdown !== null ? [this.continueCountdown] : [];
+  }
+  countdownTrackBy(_index: number, value: number): number {
+    return value;
+  }
   private flipTimeout: any = null;
   private canFlip: boolean = true;
 
@@ -189,21 +200,35 @@ private readonly SCORE_RULES = {
 
   private clearContinueCountdown() {
     this.continueCountdown = null;
+    this.showGo = false;
     if (this.continueCountdownInterval) {
       clearInterval(this.continueCountdownInterval);
       this.continueCountdownInterval = null;
     }
+    if (this.goTimeout) {
+      clearTimeout(this.goTimeout);
+      this.goTimeout = null;
+    }
   }
 
-  // 点击「继续」:先 3 秒倒计时,结束后再关闭弹窗并恢复游戏
+  // 点击「继续」:先 3、2、1 倒计时,显示 1 后再显示 GO,然后关闭弹窗并恢复游戏
   startContinueCountdown() {
     this.clearContinueCountdown();
     this.continueCountdown = 3;
     this.continueCountdownInterval = setInterval(() => {
       if (this.continueCountdown === null) return;
       if (this.continueCountdown <= 1) {
-        this.clearContinueCountdown();
-        this.closeExitModal();
+        if (this.continueCountdownInterval) {
+          clearInterval(this.continueCountdownInterval);
+          this.continueCountdownInterval = null;
+        }
+        this.continueCountdown = null;
+        this.showGo = true;
+        this.goTimeout = setTimeout(() => {
+          this.goTimeout = null;
+          this.showGo = false;
+          this.closeExitModal();
+        }, 500);
         return;
       }
       this.continueCountdown = this.continueCountdown! - 1;