Browse Source

优化单词列表,可以滑动切换,单词列表共享,优化页面

s202226701051 1 tuần trước cách đây
mục cha
commit
4ddf656aaa

+ 74 - 34
word-app/src/app/word-story/word-story.page.html

@@ -1,9 +1,12 @@
 <ion-content class="word-story-content"
              [class.has-bottom-bar]="showExerciseView"
-             [class.has-fixed-bottom-bar]="!isGenerating && !storyResponse && !showExerciseView">
+             [class.has-fixed-bottom-bar]="!isGenerating && !storyResponse && !showExerciseView"
+             [scrollY]="!isWordListMode">
   <div class="ocean-bg"></div>
   
-  <div class="word-story-container" [class.exercise-mode]="showExerciseView">
+  <div class="word-story-container"
+       [class.exercise-mode]="showExerciseView"
+       [class.word-list-mode]="isWordListMode">
     
     <!-- 顶部导航 - 添加船图标 -->
     <div class="nav-header">
@@ -84,43 +87,80 @@
       <h2 class="generating-title story-done" *ngIf="storyResponse && !isGenerating">故事已生成</h2>
     </div>
 
-    <!-- 标签切换 - 仅在非生成且未出结果且非做题时显示 -->
-    <div class="tab-section"
-         *ngIf="!isGenerating && !storyResponse && !showExerciseView"
-         (touchstart)="onTabTouchStart($event)"
-         (touchend)="onTabTouchEnd($event)"
-         (mousedown)="onTabMouseDown($event)"
-         (mouseup)="onTabMouseUp($event)">
-      <div class="tab-item"
-           [class.active]="currentTab === 'learning'"
-           (click)="switchTab('learning')">
-        <span>待学习</span>
-        <ion-icon name="help-circle-outline" class="help-icon"></ion-icon>
-      </div>
-      <div class="tab-item"
-           [class.active]="currentTab === 'learned'"
-           (click)="switchTab('learned')">
-        <span>已学习</span>
-        <ion-icon name="help-circle-outline" class="help-icon"></ion-icon>
+<!-- 标签切换 - 仅在非生成且未出结果且非做题时显示 -->
+<div class="tab-section"
+     *ngIf="!isGenerating && !storyResponse && !showExerciseView">
+  <div class="tab-item"
+       [class.active]="tabSwipeProgress < 0.5"
+       (click)="switchTab('learning')">
+    <span>待学习</span>
+    <ion-icon name="help-circle-outline" class="help-icon"></ion-icon>
+  </div>
+  <div class="tab-item"
+       [class.active]="tabSwipeProgress >= 0.5"
+       (click)="switchTab('learned')">
+    <span>已学习</span>
+    <ion-icon name="help-circle-outline" class="help-icon"></ion-icon>
+  </div>
+</div>
+
+<!-- 单词列表(丝滑拖拽预览双页) -->
+<div class="word-list-scroll" *ngIf="!isGenerating && !storyResponse && !showExerciseView">
+  <div class="word-list-viewport"
+       [class.dragging]="isListDragging"
+       [attr.data-swipe-direction]="listSwipeDirection"
+       (touchstart)="onListTouchStart($event)"
+       (touchmove)="onListTouchMove($event)"
+       (touchend)="onListTouchEnd($event)"
+       (mousedown)="onListMouseDown($event)"
+       (mousemove)="onListMouseMove($event)"
+       (mouseup)="onListMouseUp($event)"
+       (mouseleave)="onListMouseLeave($event)">
+
+  <div class="word-list-track"
+       [style.transform]="wordListTransform"
+       [style.transition]="wordListTransition">
+
+    <!-- Page 1: 待学习 -->
+    <div class="word-list-page">
+      <div class="word-list">
+        <div class="word-card"
+             *ngFor="let word of learningWords; let i = index"
+             [class.selected]="word.selected"
+             (click)="toggleSelect(word)">
+          <div class="select-circle" [class.checked]="word.selected">
+            <ion-icon name="checkmark" *ngIf="word.selected"></ion-icon>
+          </div>
+          <div class="word-info">
+            <h3 class="word-text">{{ word.word }}</h3>
+            <p class="word-meaning">{{ word.meaning }}</p>
+          </div>
+        </div>
       </div>
     </div>
 
-    <!-- 单词列表 - 仅在非生成且未出结果且非做题时显示 -->
-    <div class="word-list" *ngIf="!isGenerating && !storyResponse && !showExerciseView">
-      <div class="word-card" 
-           *ngFor="let word of currentWordList; let i = index"
-           [class.selected]="word.selected"
-           (click)="toggleSelect(word)">
-        <div class="select-circle" [class.checked]="word.selected">
-          <ion-icon name="checkmark" *ngIf="word.selected"></ion-icon>
-        </div>
-        <div class="word-info">
-          <h3 class="word-text">{{ word.word }}</h3>
-          <p class="word-meaning">{{ word.meaning }}</p>
+    <!-- Page 2: 已学习 -->
+    <div class="word-list-page">
+      <div class="word-list">
+        <div class="word-card"
+             *ngFor="let word of learnedWords; let i = index"
+             [class.selected]="word.selected"
+             (click)="toggleSelect(word)">
+          <div class="select-circle" [class.checked]="word.selected">
+            <ion-icon name="checkmark" *ngIf="word.selected"></ion-icon>
+          </div>
+          <div class="word-info">
+            <h3 class="word-text">{{ word.word }}</h3>
+            <p class="word-meaning">{{ word.meaning }}</p>
+          </div>
         </div>
       </div>
     </div>
 
+  </div>
+</div>
+</div>
+
     <!-- 单词卡片滑动区域 - 生成中或已生成且非做题时显示 -->
     <div class="word-slider-section" *ngIf="(isGenerating || storyResponse) && !showExerciseView">
       <div class="word-slider-container" 
@@ -169,7 +209,7 @@
     </div>
 
     <!-- 风格选择弹窗 - 多步骤向导 -->
-    <div class="wizard-modal-overlay" *ngIf="showStyleModal" (click)="closeStyleModal()">
+    <div class="wizard-modal-overlay" *ngIf="showStyleModal">
       <div class="wizard-modal-container" (click)="$event.stopPropagation()">
         
         <!-- 弹窗头部 -->
@@ -256,7 +296,7 @@
   </div>
 </ion-content>
 
-<ion-footer class="word-story-footer">
+<ion-footer class="word-story-footer" [class.modal-open]="showStyleModal">
 
   <!-- 非做题模式:底部操作栏 -->
   <div class="bottom-bar" *ngIf="!isGenerating && !storyResponse && !showExerciseView">

+ 376 - 74
word-app/src/app/word-story/word-story.page.scss

@@ -45,6 +45,7 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     pointer-events: none;
   }
 
+
   .word-story-container {
     position: relative;
     z-index: 1;
@@ -107,7 +108,7 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     }
   }
 
-  // 标签切换 - 居中对齐
+  // 标签切换 - 居中对齐(使用每个 tab 自己的下划线指示器)
   .tab-section {
     display: flex;
     justify-content: center;
@@ -115,7 +116,6 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     padding: 0 16px;
     margin-bottom: 16px;
     gap: 40px;
-    // 添加滑动相关的样式
     cursor: grab;
     user-select: none;
     touch-action: pan-y; // 允许垂直滚动,禁止水平滚动
@@ -155,19 +155,6 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
         .help-icon {
           color: $ocean-light;
         }
-
-        &::after {
-          content: '';
-          position: absolute;
-          bottom: 0;
-          left: 50%;
-          transform: translateX(-50%);
-          width: 70%;
-          height: 3px;
-          background: $ocean-mid;
-          border-radius: 2px;
-          box-shadow: 0 0 8px rgba(41, 182, 246, 0.4);
-        }
       }
 
       &:active {
@@ -176,81 +163,313 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     }
   }
 
-  // 单词列表 - 贝壳卡片风格
-  .word-list {
-    padding: 0 16px;
+// 单词列表(双页拖拽视图)
+.word-list-viewport {
+  overflow: hidden;
+  width: 100%;
+  height: auto;
+  min-height: 100%;
+}
+
+// 只有列表区域滚动:顶部导航/标签固定,列表内部滚动
+.word-story-container.word-list-mode {
+  height: 100%;
+}
+
+.word-list-scroll {
+  flex: 1;
+  min-height: 0; // 关键:允许子元素在 flex 容器内滚动
+  overflow-y: auto;
+  -webkit-overflow-scrolling: touch;
+  padding-bottom: 10px;
+  height: auto;
+}
+
+.word-list-track {
+  display: flex;
+  width: 200%;
+  min-height: 100%;
+  height: auto;
+  will-change: transform;
+}
+
+.word-list-page {
+  width: 50%;
+  flex: 0 0 50%;
+  min-height: 100%;
+}
+
+// 单词列表 - 贝壳卡片风格(每一页内部依旧使用原列表样式)
+.word-list {
+  padding: 0 16px;
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  justify-content: flex-start;
+  align-content: flex-start;
+  min-height: auto;
+  height: auto;
+  
+  // 取消滑动动画:不再对列表本身做 transform/opacity 过渡
+  transition: none;
+  cursor: default;
+  user-select: none;
+  touch-action: pan-y; // 允许垂直滚动,禁止水平滚动
+  
+  &.dragging {
+    cursor: grabbing;
+    // 不做缩放/透明变化,避免“拖拽动画”
+  }
+  
+  // 添加滑动方向的视觉提示
+  &[data-swipe-direction="left"] {
+    &::after {
+      content: '';
+      position: absolute;
+      right: 10px;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 4px;
+      height: 40px;
+      background: linear-gradient(to bottom, $ocean-light, $ocean-mid);
+      border-radius: 4px;
+      opacity: 0.3;
+      // 保留指示器,移除动画
+    }
+  }
+  
+  &[data-swipe-direction="right"] {
+    &::before {
+      content: '';
+      position: absolute;
+      left: 10px;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 4px;
+      height: 40px;
+      background: linear-gradient(to bottom, $ocean-light, $ocean-mid);
+      border-radius: 4px;
+      opacity: 0.3;
+      // 保留指示器,移除动画
+    }
+  }
+
+  .word-card {
     display: flex;
-    flex-direction: column;
-    gap: 12px;
+    align-items: center;
+    gap: 16px;
+    background: $pearl;
+    border-radius: 16px;
+    padding: 16px 20px;
+    border: 2px solid #E3F2FD;
+    cursor: pointer;
+    transition: all 0.2s ease;
+    box-shadow: 0 2px 8px rgba(2, 136, 209, 0.06);
+    
+    // 新增:拖动时的卡片效果
+    .dragging & {
+      transform: none !important; // 防止卡片独立动画干扰整体拖动
+    }
 
-    .word-card {
+    &:active {
+      transform: scale(0.98);
+    }
+
+    &.selected {
+      border-color: $ocean-light;
+      background: linear-gradient(135deg, #E1F5FE 0%, $pearl 100%);
+      box-shadow: 
+        0 4px 12px rgba(41, 182, 246, 0.15),
+        inset 0 0 0 1px rgba(41, 182, 246, 0.1);
+    }
+
+    .select-circle {
+      width: 24px;
+      height: 24px;
+      border-radius: 50%;
+      border: 2px solid #B3E5FC;
       display: flex;
       align-items: center;
-      gap: 16px;
-      background: $pearl;
-      border-radius: 16px;
-      padding: 16px 20px;
-      border: 2px solid #E3F2FD;
-      cursor: pointer;
+      justify-content: center;
+      flex-shrink: 0;
       transition: all 0.2s ease;
-      box-shadow: 0 2px 8px rgba(2, 136, 209, 0.06);
+      background: $pearl;
 
-      &:active {
-        transform: scale(0.98);
+      &.checked {
+        background: $ocean-mid;
+        border-color: $ocean-mid;
+        box-shadow: 0 2px 8px rgba(2, 136, 209, 0.3);
       }
 
-      &.selected {
-        border-color: $ocean-light;
-        background: linear-gradient(135deg, #E1F5FE 0%, $pearl 100%);
-        box-shadow: 
-          0 4px 12px rgba(41, 182, 246, 0.15),
-          inset 0 0 0 1px rgba(41, 182, 246, 0.1);
+      ion-icon {
+        color: $pearl;
+        font-size: 1rem;
+        font-weight: bold;
       }
+    }
 
-      .select-circle {
-        width: 24px;
-        height: 24px;
-        border-radius: 50%;
-        border: 2px solid #B3E5FC;
-        display: flex;
-        align-items: center;
-        justify-content: center;
-        flex-shrink: 0;
-        transition: all 0.2s ease;
-        background: $pearl;
-
-        &.checked {
-          background: $ocean-mid;
-          border-color: $ocean-mid;
-          box-shadow: 0 2px 8px rgba(2, 136, 209, 0.3);
-        }
+    .word-info {
+      flex: 1;
 
-        ion-icon {
-          color: $pearl;
-          font-size: 1rem;
-          font-weight: bold;
-        }
+      .word-text {
+        font-size: 1.4rem;
+        font-weight: 700;
+        color: $ocean-deep;
+        margin: 0 0 4px 0;
+        font-family: 'Georgia', serif;
       }
 
-      .word-info {
-        flex: 1;
+      .word-meaning {
+        font-size: 0.95rem;
+        color: #546E7A;
+        margin: 0;
+      }
+    }
+  }
+}
 
-        .word-text {
-          font-size: 1.4rem;
-          font-weight: 700;
-          color: $ocean-deep;
-          margin: 0 0 4px 0;
-          font-family: 'Georgia', serif;
-        }
+// 在标签切换部分也添加相应的滑动样式
+.tab-section {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 0 16px;
+  margin-bottom: 16px;
+  gap: 40px;
+  
+  // 添加滑动相关的样式
+  cursor: grab;
+  user-select: none;
+  touch-action: pan-y; // 允许垂直滚动,禁止水平滚动
+  transition: opacity 0.1s ease;
+  
+  &:active {
+    cursor: grabbing;
+  }
+  
+  // 滑动时的视觉反馈
+  &.dragging {
+    opacity: 0.9;
+    
+    .tab-item {
+      pointer-events: none; // 拖动时禁止点击标签
+    }
+  }
+  
+  // 添加滑动方向的视觉提示
+  &[data-swipe-direction="left"] {
+    &::after {
+      content: '';
+      position: absolute;
+      right: 20px;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 30px;
+      height: 30px;
+      background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%230288D1'%3E%3Cpath d='M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z'/%3E%3C/svg%3E") no-repeat center;
+      background-size: contain;
+      opacity: 0.3;
+      animation: slideLeftHint 0.5s ease;
+    }
+  }
+  
+  &[data-swipe-direction="right"] {
+    &::before {
+      content: '';
+      position: absolute;
+      left: 20px;
+      top: 50%;
+      transform: translateY(-50%);
+      width: 30px;
+      height: 30px;
+      background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%230288D1'%3E%3Cpath d='M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z'/%3E%3C/svg%3E") no-repeat center;
+      background-size: contain;
+      opacity: 0.3;
+      animation: slideRightHint 0.5s ease;
+    }
+  }
+  
+  @keyframes slideLeftHint {
+    0% {
+      opacity: 0;
+      transform: translateY(-50%) translateX(-10px);
+    }
+    50% {
+      opacity: 0.6;
+      transform: translateY(-50%) translateX(0);
+    }
+    100% {
+      opacity: 0.3;
+      transform: translateY(-50%) translateX(0);
+    }
+  }
+  
+  @keyframes slideRightHint {
+    0% {
+      opacity: 0;
+      transform: translateY(-50%) translateX(10px);
+    }
+    50% {
+      opacity: 0.6;
+      transform: translateY(-50%) translateX(0);
+    }
+    100% {
+      opacity: 0.3;
+      transform: translateY(-50%) translateX(0);
+    }
+  }
 
-        .word-meaning {
-          font-size: 0.95rem;
-          color: #546E7A;
-          margin: 0;
-        }
+  .tab-item {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    padding: 8px 16px;
+    cursor: pointer;
+    position: relative;
+    transition: all 0.3s ease;
+
+    span {
+      font-size: 1.1rem;
+      color: #90A4AE;
+      font-weight: 500;
+      transition: all 0.3s ease;
+    }
+
+    .help-icon {
+      font-size: 1.5rem;
+      color: #B0BEC5;
+      transition: all 0.3s ease;
+    }
+
+    &.active {
+      span {
+        color: $ocean-deep;
+        font-weight: 600;
+      }
+
+      .help-icon {
+        color: $ocean-light;
+      }
+
+      &::after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        left: 50%;
+        transform: translateX(-50%);
+        width: 70%;
+        height: 3px;
+        background: $ocean-mid;
+        border-radius: 2px;
+        box-shadow: 0 0 8px rgba(41, 182, 246, 0.4);
       }
     }
+
+    &:active {
+      opacity: 0.7;
+    }
   }
+}
 
   // 生成中状态
   .generating-section {
@@ -522,7 +741,7 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
 
   .exercise-title-container {
     background: $pearl;
-    padding: 16px 20px;
+    padding: 16px 15px;
     border-radius: 16px;
     box-shadow: 0 2px 12px rgba(2, 136, 209, 0.1);
     flex: 1;
@@ -582,7 +801,7 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     justify-content: center;
     gap: 4px;
     min-width: 80px;
-    padding: 12px 16px;
+    padding: 10px 12px;
     background: $pearl;
     border-radius: 16px;
     box-shadow: 0 2px 12px rgba(2, 136, 209, 0.1);
@@ -1178,4 +1397,87 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
       transform: scale(0.96);
     }
   }
+}
+
+// 在文件末尾添加一个简单的滑动指示器
+.swipe-indicator {
+  position: fixed;
+  top: 50%;
+  transform: translateY(-50%);
+  width: 40px;
+  height: 40px;
+  border-radius: 50%;
+  background: rgba($ocean-mid, 0.2);
+  backdrop-filter: blur(5px);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  z-index: 1000;
+  pointer-events: none;
+  transition: opacity 0.2s ease;
+  
+  &.left {
+    left: 10px;
+    
+    ion-icon {
+      color: $ocean-deep;
+      font-size: 24px;
+      animation: bounceLeft 1s infinite;
+    }
+  }
+  
+  &.right {
+    right: 10px;
+    
+    ion-icon {
+      color: $ocean-deep;
+      font-size: 24px;
+      animation: bounceRight 1s infinite;
+    }
+  }
+  
+  @keyframes bounceLeft {
+    0%, 100% {
+      transform: translateX(0);
+    }
+    50% {
+      transform: translateX(-5px);
+    }
+  }
+  
+  @keyframes bounceRight {
+    0%, 100% {
+      transform: translateX(0);
+    }
+    50% {
+      transform: translateX(5px);
+    }
+  }
+}
+
+// 模态框弹出时底部变暗且禁用点击
+.word-story-footer {
+  &.modal-open {
+    // 添加黑色半透明遮罩
+    &::before {
+      content: '';
+      position: absolute;
+      top: 0;
+      left: 0;
+      right: 0;
+      bottom: 0;
+      background-color: rgba(0, 0, 0, 0.5);
+      z-index: 10;
+      pointer-events: auto;
+    }
+    
+    // 底部内容变暗且禁用点击
+    .bottom-bar,
+    .exercise-bottom-bar,
+    .exercise-score-bar {
+      opacity: 0.6;
+      filter: brightness(0.5);
+      pointer-events: none;
+    }
+  }
 }

+ 373 - 186
word-app/src/app/word-story/word-story.page.ts

@@ -57,18 +57,40 @@ export class WordStoryPage implements OnInit {
 
   // IonContent 引用,用于滚动控制
   @ViewChild(IonContent) content!: IonContent;
+  // 在类属性中添加
+listSwipeDirection: 'left' | 'right' | null = null;
 
   // 当前标签页
   currentTab: 'learning' | 'learned' = 'learning';
-  private isSwiped: boolean = false;
-  swipeDirection: 'left' | 'right' | null = null;
-  isAnimating: boolean = false; // 防止重复动画
-  animationClass: string = '';   // 当前动画类,如 'slide-out-left', 'slide-in-right' 等
+  /** 0=待学习,1=已学习;用于“拖拽中预览另一页”和指示器跟随 */
+  tabSwipeProgress: number = 0;
+  /** 列表 track 的 transition(拖拽时为 none,松手吸附时为带动画) */
+  wordListTransition: string = 'transform 220ms cubic-bezier(0.4, 0, 0.2, 1)';
+  private listViewportWidth: number = 0;
+  private listDragStartProgress: number = 0;
 
   // 标签页滑动相关
   private tabTouchStartX: number = 0;
   private tabTouchEndX: number = 0;
-  // 新增属性
+  private isTabDragging: boolean = false;
+
+  // 单词列表滑动相关
+  private listTouchStartX: number = 0;
+  private listTouchStartY: number = 0;
+  private listTouchEndX: number = 0;
+  /** 仅在“确实发生水平拖拽”时才为 true,用于展示拖拽态(而不是按下就算拖拽) */
+  isListDragging: boolean = false;
+  /** 按下后是否发生过水平移动(用于决定是否触发 swipe/屏蔽点击) */
+  private listHasHorizontalMove: boolean = false;
+  /** swipe 后短时间屏蔽 click,避免 touchend 后触发卡片点击 */
+  private suppressWordClickUntil: number = 0;
+
+  // 闪卡滑动相关
+  swipeDirection: 'left' | 'right' | null = null;
+  isAnimating: boolean = false;
+  animationClass: string = '';
+  
+  // 故事相关
   storyResponse: StoryResponse | null = null;
   storyError: string | null = null;
 
@@ -84,7 +106,6 @@ export class WordStoryPage implements OnInit {
   /** 评分结果:null 未评分 */
   scoreResult: { correct: number; total: number; filled: number; percent: number } | null = null;
 
-
   // 待学习单词列表
   learningWords: WordItem[] = [
     { id: '1', word: 'add', meaning: 'v. 增加;相加', selected: false },
@@ -113,7 +134,6 @@ export class WordStoryPage implements OnInit {
 
   // 风格选择弹窗状态
   showStyleModal: boolean = false;
-  styleInput: string = '';
   defaultStyles: string = '比如:童话、青春、悬疑、励志、科幻...';
 
   // 生成状态
@@ -123,15 +143,33 @@ export class WordStoryPage implements OnInit {
   flashWords: FlashWord[] = [];
   currentFlashIndex: number = 0;
 
-  // 触摸/鼠标滑动相关
+  // 闪卡触摸/鼠标滑动相关
   private touchStartX: number = 0;
   private touchEndX: number = 0;
   private isDragging: boolean = false;
-  private currentTranslateX: number = 0;
   isSliding: boolean = false;
   cardTransform: string = '';
 
-  constructor(private router: Router , private wordStoryService: WordStoryService) {
+  // 向导相关
+  wizardSteps = [
+    { key: 'style', label: '风格' },
+    { key: 'difficulty', label: '难度' },
+    { key: 'genre', label: '体裁' }
+  ];
+  
+  currentWizardStep: number = 0;
+  
+  wizardData: {
+    style: string;
+    difficulty: string;
+    genre: string;
+  } = {
+    style: '',
+    difficulty: '',
+    genre: ''
+  };
+
+  constructor(private router: Router, private wordStoryService: WordStoryService) {
     addIcons({
       arrowBackOutline,
       helpCircleOutline,
@@ -152,9 +190,18 @@ export class WordStoryPage implements OnInit {
   }
 
   ngOnInit(): void {
+    // 与 currentTab 保持一致
+    this.tabSwipeProgress = this.currentTab === 'learned' ? 1 : 0;
     if (this.exerciseStory) this.initExerciseState();
   }
 
+  /** 仅在“选择单词列表页”时禁用页面整体滚动,让列表区域单独滚动 */
+  get isWordListMode(): boolean {
+    return !this.isGenerating && !this.storyResponse && !this.showExerciseView;
+  }
+
+  /** ========== 做题页相关方法 ========== */
+  
   /** 模板用:获取某段的片段列表 */
   getParagraphSegmentsForView(paragraphEn: string, startBlankIndex: number): ParagraphSegment[] {
     return this.getParagraphSegments(paragraphEn, startBlankIndex).segments;
@@ -293,6 +340,13 @@ export class WordStoryPage implements OnInit {
     }, 100);
   }
 
+  /** 判断是否所有空都已填 */
+  isAllBlanksFilled(): boolean {
+    return this.exerciseBlanks.every(blank => blank !== null && blank !== '');
+  }
+
+  /** ========== 单词列表相关方法 ========== */
+
   // 获取当前显示的单词列表
   get currentWordList(): WordItem[] {
     return this.currentTab === 'learning' ? this.learningWords : this.learnedWords;
@@ -303,34 +357,200 @@ export class WordStoryPage implements OnInit {
     return [...this.learningWords, ...this.learnedWords].filter(word => word.selected).length;
   }
 
-  // 获取当前闪卡单词
-  get currentFlashWord(): FlashWord | null {
-    return this.flashWords[this.currentFlashIndex] || null;
-  }
-
   // 切换标签页
   switchTab(tab: 'learning' | 'learned') {
     this.currentTab = tab;
+    // 吸附到目标页
+    this.wordListTransition = 'transform 220ms cubic-bezier(0.4, 0, 0.2, 1)';
+    this.tabSwipeProgress = tab === 'learned' ? 1 : 0;
+  }
+
+  // 切换单词选中状态
+  toggleSelect(word: WordItem) {
+    // 如果刚刚发生过滑动切换(或正在拖拽),不要把它当作点击选中
+    if (this.isListDragging || Date.now() < this.suppressWordClickUntil) return;
+    word.selected = !word.selected;
+  }
+
+  /** 列表横向位移(百分比),跟随 tabSwipeProgress */
+  get wordListTransform(): string {
+    // track 宽度为 200%,单页宽度为 50%,因此切换一页只需要移动 50%
+    return `translate3d(${-this.tabSwipeProgress * 50}%, 0, 0)`;
+  }
+
+  /** ========== 单词列表滑动切换标签页 ========== */
+
+  // 单词列表区域的触摸事件
+  onListTouchStart(event: TouchEvent) {
+    this.listTouchStartX = event.touches[0].clientX;
+    this.listTouchStartY = event.touches[0].clientY;
+    this.isListDragging = false;
+    this.listHasHorizontalMove = false;
+    this.wordListTransition = 'none';
+    const el = event.currentTarget as HTMLElement | null;
+    this.listViewportWidth = el?.getBoundingClientRect().width || 0;
+    this.listDragStartProgress = this.tabSwipeProgress;
+  }
+
+  onListTouchMove(event: TouchEvent) {
+    // 只在明显滑动时才阻止滚动
+    const diffX = Math.abs(event.touches[0].clientX - this.listTouchStartX);
+    const diffY = Math.abs(event.touches[0].clientY - this.listTouchStartY);
+    if (diffX > diffY && diffX > 10) {
+      this.isListDragging = true;
+      this.listHasHorizontalMove = true;
+      event.preventDefault(); // 只有水平滑动才阻止页面滚动
+
+      const currentX = event.touches[0].clientX;
+      const deltaX = currentX - this.listTouchStartX;
+      const width = this.listViewportWidth || 1;
+      // 再减小进度变化速度:需要拖动更长距离才切换完整一页
+      const next = this.listDragStartProgress + (-deltaX / (width * 1.5));
+      this.tabSwipeProgress = Math.max(0, Math.min(1, next));
+    }
+  }
+
+  onListTouchEnd(event: TouchEvent) {
+    this.listTouchEndX = event.changedTouches[0].clientX;
+    if (this.listHasHorizontalMove) {
+      // swipe 发生后,短时间屏蔽 click(移动端常见:touchend 后仍会触发 click)
+      this.suppressWordClickUntil = Date.now() + 250;
+      this.snapToNearestTab();
+    }
+    this.listTouchStartX = 0;
+    this.listTouchStartY = 0;
+    this.listTouchEndX = 0;
+    this.isListDragging = false;
+    this.listHasHorizontalMove = false;
+  }
+
+  // 单词列表区域的鼠标事件
+  onListMouseDown(event: MouseEvent) {
+    this.listTouchStartX = event.clientX;
+    this.listTouchStartY = event.clientY;
+    this.isListDragging = false;
+    this.listHasHorizontalMove = false;
+    this.wordListTransition = 'none';
+    const el = event.currentTarget as HTMLElement | null;
+    this.listViewportWidth = el?.getBoundingClientRect().width || 0;
+    this.listDragStartProgress = this.tabSwipeProgress;
+  }
+
+  onListMouseMove(event: MouseEvent) {
+    // 只有在按键按下时才计算拖拽
+    if (event.buttons !== 1) return;
+    const diffX = Math.abs(event.clientX - this.listTouchStartX);
+    const diffY = Math.abs(event.clientY - this.listTouchStartY);
+    if (diffX > diffY && diffX > 10) {
+      this.isListDragging = true;
+      this.listHasHorizontalMove = true;
+      const deltaX = event.clientX - this.listTouchStartX;
+      const width = this.listViewportWidth || 1;
+      const next = this.listDragStartProgress + (-deltaX / (width * 2.4));
+      this.tabSwipeProgress = Math.max(0, Math.min(1, next));
+    }
+  }
+
+  onListMouseUp(event: MouseEvent) {
+    this.listTouchEndX = event.clientX;
+    if (this.listHasHorizontalMove) {
+      this.suppressWordClickUntil = Date.now() + 250;
+      this.snapToNearestTab();
+    }
+    this.listTouchStartX = 0;
+    this.listTouchStartY = 0;
+    this.listTouchEndX = 0;
+    this.isListDragging = false;
+    this.listHasHorizontalMove = false;
+  }
+
+  onListMouseLeave(event: MouseEvent) {
+    if (this.isListDragging) {
+      this.isListDragging = false;
+      this.listTouchStartX = 0;
+      this.listTouchEndX = 0;
+    }
+  }
+
+  /** 松手后吸附到最近的 tab,并给出方向提示(不做动画脉冲,只保留指示器本身) */
+  private snapToNearestTab() {
+    // 提高吸附阈值:需要超过 60% 才切到另一页,避免轻微滑动就切换
+    const targetTab: 'learning' | 'learned' = this.tabSwipeProgress >= 0.6 ? 'learned' : 'learning';
+    const fromTab = this.currentTab;
+
+    this.wordListTransition = 'transform 220ms cubic-bezier(0.4, 0, 0.2, 1)';
+    this.currentTab = targetTab;
+    this.tabSwipeProgress = targetTab === 'learned' ? 1 : 0;
+
+    // 仅在确实切换时展示方向指示器
+    if (fromTab !== targetTab) {
+      this.listSwipeDirection = targetTab === 'learned' ? 'left' : 'right';
+      if (window.navigator && window.navigator.vibrate) {
+        window.navigator.vibrate(10);
+      }
+      setTimeout(() => {
+        this.listSwipeDirection = null;
+      }, 350);
+    } else {
+      this.listSwipeDirection = null;
+    }
   }
 
+  // 列表滚动由 .word-list-scroll 承担,不需要动态测量高度
+
+  /** ========== 标签页切换区域滑动(保留原有功能) ========== */
+
   // 标签页滑动事件 - 触摸
   onTabTouchStart(event: TouchEvent) {
     this.tabTouchStartX = event.touches[0].clientX;
+    this.isTabDragging = true;
+  }
+
+  onTabTouchMove(event: TouchEvent) {
+    if (!this.isTabDragging) return;
+
+    const diff = event.touches[0].clientX - this.tabTouchStartX;
+    // 只有明显的水平滑动才阻止默认行为
+    if (Math.abs(diff) > 10) {
+      event.preventDefault();
+    }
   }
 
   onTabTouchEnd(event: TouchEvent) {
+    if (!this.isTabDragging) return;
+
     this.tabTouchEndX = event.changedTouches[0].clientX;
     this.handleTabSwipe();
+    this.tabTouchStartX = 0;
+    this.tabTouchEndX = 0;
+    this.isTabDragging = false;
   }
 
   // 标签页滑动事件 - 鼠标
   onTabMouseDown(event: MouseEvent) {
     this.tabTouchStartX = event.clientX;
+    this.isTabDragging = true;
+    // 不阻止默认行为,允许点击
+  }
+
+  onTabMouseMove(event: MouseEvent) {
+    if (!this.isTabDragging) return;
+
+    const diff = event.clientX - this.tabTouchStartX;
+    // 只有明显的水平滑动才阻止默认行为
+    if (Math.abs(diff) > 10) {
+      event.preventDefault();
+    }
   }
 
   onTabMouseUp(event: MouseEvent) {
+    if (!this.isTabDragging) return;
+
     this.tabTouchEndX = event.clientX;
     this.handleTabSwipe();
+    this.tabTouchStartX = 0;
+    this.tabTouchEndX = 0;
+    this.isTabDragging = false;
   }
 
   // 处理标签页滑动
@@ -349,32 +569,15 @@ export class WordStoryPage implements OnInit {
     }
   }
 
-  // 切换单词选中状态
-  toggleSelect(word: WordItem) {
-    word.selected = !word.selected;
-  }
+  /** ========== 闪卡相关方法 ========== */
 
-  // 返回上一页
-  goBack() {
-    if (this.showExerciseView) {
-      this.showExerciseView = false;
-      this.exerciseStory = null;
-      return;
-    }
-    if (this.isGenerating) {
-      this.isGenerating = false;
-      this.flashWords = [];
-      this.currentFlashIndex = 0;
-    } else {
-      this.router.navigate(['/tabs/tab2']);
-    }
+  // 获取当前闪卡单词
+  get currentFlashWord(): FlashWord | null {
+    return this.flashWords[this.currentFlashIndex] || null;
   }
 
-
-  
   // 解析单词为闪卡格式
   private parseWordItem(wordText: string): FlashWord {
-    // 这里简化处理,实际应从词库获取完整信息
     const wordMap: { [key: string]: FlashWord } = {
       'add': { word: 'add', phonetic: '/æd/', partOfSpeech: 'v.', chineseMeaning: '增加;相加' },
       'addition': { word: 'addition', phonetic: '/əˈdɪʃn/', partOfSpeech: 'n.', chineseMeaning: '增添;添加物' },
@@ -404,32 +607,31 @@ export class WordStoryPage implements OnInit {
     };
   }
 
-// 触摸移动 - 优化跟随手感
-onTouchMove(event: TouchEvent) {
-  if (!this.isDragging) return;
-  
-  const currentX = event.touches[0].clientX;
-  const diff = currentX - this.touchStartX;
-  
-  // 阻力系数:越往外拉越费劲
-  const resistance = 0.8;
-  const resistedDiff = diff * resistance;
-  
-  // 旋转角度随拖动距离增加
-  const rotate = resistedDiff * 0.1;
-  
-  // 轻微缩放模拟抬起效果
-  const scale = 1 - Math.abs(resistedDiff) * 0.0002;
-  
-  this.cardTransform = `translateX(${resistedDiff}px) rotate(${rotate}deg) scale(${scale})`;
-  
-  if (diff > 0) {
-    this.swipeDirection = 'right';
-  } else if (diff < 0) {
-    this.swipeDirection = 'left';
+  // 触摸移动 - 优化跟随手感
+  onTouchMove(event: TouchEvent) {
+    if (!this.isDragging) return;
+    
+    const currentX = event.touches[0].clientX;
+    const diff = currentX - this.touchStartX;
+    
+    // 阻力系数:越往外拉越费劲
+    const resistance = 0.8;
+    const resistedDiff = diff * resistance;
+    
+    // 旋转角度随拖动距离增加
+    const rotate = resistedDiff * 0.1;
+    
+    // 轻微缩放模拟抬起效果
+    const scale = 1 - Math.abs(resistedDiff) * 0.0002;
+    
+    this.cardTransform = `translateX(${resistedDiff}px) rotate(${rotate}deg) scale(${scale})`;
+    
+    if (diff > 0) {
+      this.swipeDirection = 'right';
+    } else if (diff < 0) {
+      this.swipeDirection = 'left';
+    }
   }
-}
-
 
   // 触摸事件处理
   onTouchStart(event: TouchEvent) {
@@ -438,8 +640,6 @@ onTouchMove(event: TouchEvent) {
     this.isSliding = true;
   }
 
-
-
   // 鼠标事件处理
   onMouseDown(event: MouseEvent) {
     this.touchStartX = event.clientX;
@@ -447,134 +647,116 @@ onTouchMove(event: TouchEvent) {
     this.isSliding = true;
   }
 
-onMouseUp(event: MouseEvent) {
-  if (!this.isDragging) return;
-  this.touchEndX = event.clientX;
-  this.handleSwipe();
-}
+  onMouseUp(event: MouseEvent) {
+    if (!this.isDragging) return;
+    this.touchEndX = event.clientX;
+    this.handleSwipe();
+  }
 
-onTouchEnd(event: TouchEvent) {
-  this.touchEndX = event.changedTouches[0].clientX;
-  this.handleSwipe();
-}
+  onTouchEnd(event: TouchEvent) {
+    this.touchEndX = event.changedTouches[0].clientX;
+    this.handleSwipe();
+  }
 
-// 鼠标移动同理
-onMouseMove(event: MouseEvent) {
-  if (!this.isDragging) return;
-  
-  const currentX = event.clientX;
-  const diff = currentX - this.touchStartX;
-  
-  const resistance = 0.8;
-  const resistedDiff = diff * resistance;
-  const rotate = resistedDiff * 0.1;
-  const scale = 1 - Math.abs(resistedDiff) * 0.0002;
-  
-  this.cardTransform = `translateX(${resistedDiff}px) rotate(${rotate}deg) scale(${scale})`;
-  
-  if (diff > 0) {
-    this.swipeDirection = 'right';
-  } else if (diff < 0) {
-    this.swipeDirection = 'left';
+  // 鼠标移动
+  onMouseMove(event: MouseEvent) {
+    if (!this.isDragging) return;
+    
+    const currentX = event.clientX;
+    const diff = currentX - this.touchStartX;
+    
+    const resistance = 0.8;
+    const resistedDiff = diff * resistance;
+    const rotate = resistedDiff * 0.1;
+    const scale = 1 - Math.abs(resistedDiff) * 0.0002;
+    
+    this.cardTransform = `translateX(${resistedDiff}px) rotate(${rotate}deg) scale(${scale})`;
+    
+    if (diff > 0) {
+      this.swipeDirection = 'right';
+    } else if (diff < 0) {
+      this.swipeDirection = 'left';
+    }
   }
-}
 
   // 处理滑动
-private handleSwipe() {
-  if (this.isAnimating) return;
-  
-  const diff = this.touchEndX - this.touchStartX;
-  const threshold = 60;
+  private handleSwipe() {
+    if (this.isAnimating) return;
+    
+    const diff = this.touchEndX - this.touchStartX;
+    const threshold = 60;
 
-  if (Math.abs(diff) > threshold) {
-    this.isAnimating = true;
-    if (diff > 0) {
-      // 向右滑动 -> 上一个单词
-      this.playSlideOut('right');
+    if (Math.abs(diff) > threshold) {
+      this.isAnimating = true;
+      if (diff > 0) {
+        // 向右滑动 -> 上一个单词
+        this.playSlideOut('right');
+      } else {
+        // 向左滑动 -> 下一个单词
+        this.playSlideOut('left');
+      }
     } else {
-      // 向左滑动 -> 下一个单词
-      this.playSlideOut('left');
+      // 未超过阈值,弹性回弹
+      this.cardTransform = '';
+      this.isDragging = false;
+      this.isSliding = false;
     }
-  } else {
-    // 未超过阈值,弹性回弹
-    this.cardTransform = '';
-    this.isDragging = false;
-    this.isSliding = false;
   }
-}
 
-// 播放滑出动画
-playSlideOut(direction: 'left' | 'right') {
-  const slideOutClass = direction === 'left' ? 'slide-out-left' : 'slide-out-right';
-  this.animationClass = slideOutClass;
-  
-  // 0.15s 滑出
-  setTimeout(() => {
-    if (direction === 'left') {
-      this.nextWord();
-    } else {
-      this.prevWord();
-    }
-    
-    this.animationClass = '';
+  // 播放滑出动画
+  playSlideOut(direction: 'left' | 'right') {
+    const slideOutClass = direction === 'left' ? 'slide-out-left' : 'slide-out-right';
+    this.animationClass = slideOutClass;
     
-    requestAnimationFrame(() => {
+    // 0.15s 滑出
+    setTimeout(() => {
+      if (direction === 'left') {
+        this.nextWord();
+      } else {
+        this.prevWord();
+      }
+      
+      this.animationClass = '';
+      
       requestAnimationFrame(() => {
-        this.animationClass = 'enter-from-back';
-        
-        // 0.25s 后清理
-        setTimeout(() => {
-          this.animationClass = '';
-          this.isAnimating = false;
-          this.isDragging = false;
-          this.isSliding = false;
-          this.cardTransform = '';
-          this.swipeDirection = null;
-        }, 250);
+        requestAnimationFrame(() => {
+          this.animationClass = 'enter-from-back';
+          
+          // 0.25s 后清理
+          setTimeout(() => {
+            this.animationClass = '';
+            this.isAnimating = false;
+            this.isDragging = false;
+            this.isSliding = false;
+            this.cardTransform = '';
+            this.swipeDirection = null;
+          }, 250);
+        });
       });
-    });
-  }, 150);
-}
+    }, 150);
+  }
 
   // 下一个单词
-nextWord() {
-  if (this.currentFlashIndex < this.flashWords.length - 1) {
-    this.currentFlashIndex++;
-  } else {
-    // 循环到第一个
-    this.currentFlashIndex = 0;
+  nextWord() {
+    if (this.currentFlashIndex < this.flashWords.length - 1) {
+      this.currentFlashIndex++;
+    } else {
+      // 循环到第一个
+      this.currentFlashIndex = 0;
+    }
   }
-}
 
-// 上一个单词
-prevWord() {
-  if (this.currentFlashIndex > 0) {
-    this.currentFlashIndex--;
-  } else {
-    // 循环到最后一个
-    this.currentFlashIndex = this.flashWords.length - 1;
+  // 上一个单词
+  prevWord() {
+    if (this.currentFlashIndex > 0) {
+      this.currentFlashIndex--;
+    } else {
+      // 循环到最后一个
+      this.currentFlashIndex = this.flashWords.length - 1;
+    }
   }
-}
 
- wizardSteps = [
-    { key: 'style', label: '风格' },
-    { key: 'difficulty', label: '难度' },
-    { key: 'genre', label: '体裁' }
-  ];
-  
-  // 当前向导步骤
-  currentWizardStep: number = 0;
-  
-  // 向导数据
-  wizardData: {
-    style: string;
-    difficulty: string;
-    genre: string;
-  } = {
-    style: '',
-    difficulty: '',
-    genre: ''
-  };
+  /** ========== 向导相关方法 ========== */
 
   // 检查当前步骤是否可以继续
   get canProceedWizard(): boolean {
@@ -620,7 +802,8 @@ prevWord() {
     this.currentWizardStep = 0;
   }
 
-    confirmStyle() {
+  // 确认风格并生成故事
+  confirmStyle() {
     // 从两个列表中获取所有选中的单词
     const selectedWords = [...this.learningWords, ...this.learnedWords]
       .filter(word => word.selected)
@@ -643,12 +826,7 @@ prevWord() {
       next: (response) => {
         this.storyResponse = response;
         console.log('生成的故事:', response);
-
-        // 这里可以将response传递给故事展示页面或组件
-        // 例如:this.router.navigate(['/story-result'], { state: { story: response } });
-
-        // 暂时保留在生成界面,实际可以跳转到结果页
-        this.isGenerating = false; // 测试用,实际应该跳转
+        this.isGenerating = false;
       },
       error: (error) => {
         console.error('生成失败:', error);
@@ -658,15 +836,24 @@ prevWord() {
     });
   }
 
-  // 添加重试方法
+  // 重试生成
   retryGeneration() {
     this.confirmStyle();
   }
 
-  isAllBlanksFilled(): boolean {
-    return this.exerciseBlanks.every(blank => blank !== null && blank !== '');
+  // 返回上一页
+  goBack() {
+    if (this.showExerciseView) {
+      this.showExerciseView = false;
+      this.exerciseStory = null;
+      return;
+    }
+    if (this.isGenerating) {
+      this.isGenerating = false;
+      this.flashWords = [];
+      this.currentFlashIndex = 0;
+    } else {
+      this.router.navigate(['/tabs/tab2']);
+    }
   }
-}
-
-
-
+}