Просмотр исходного кода

优化生成,做题,评分页面

s202226701051 7 месяцев назад
Родитель
Сommit
2f33c60749

+ 16 - 17
word-app/src/app/word-story/word-story.page.html

@@ -25,7 +25,7 @@
         <div class="exercise-title-container" *ngIf="exerciseStory.titleEn">
           <div class="exercise-title-main">
             <h2 class="exercise-title-en">{{ exerciseStory.titleEn }}</h2>
-            <p class="exercise-title-zh" *ngIf="exerciseStory.titleZh">
+            <p class="exercise-title-zh" *ngIf="scoreResult && exerciseStory.titleZh">
               {{ exerciseStory.titleZh }}
             </p>
           </div>
@@ -85,14 +85,19 @@
     </div>
 
     <!-- 标签切换 - 仅在非生成且未出结果且非做题时显示 -->
-    <div class="tab-section" *ngIf="!isGenerating && !storyResponse && !showExerciseView">
-      <div class="tab-item" 
+    <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" 
+      <div class="tab-item"
            [class.active]="currentTab === 'learned'"
            (click)="switchTab('learned')">
         <span>已学习</span>
@@ -155,6 +160,12 @@
         </div>
 
       </div>
+
+      <!-- 去做题按钮 - 滑动提示下方 -->
+      <button class="go-exercise-btn" *ngIf="storyResponse && !isGenerating" (click)="goToExercise()">
+        <ion-icon name="create-outline"></ion-icon>
+        <span>去做题</span>
+      </button>
     </div>
 
     <!-- 风格选择弹窗 - 多步骤向导 -->
@@ -249,11 +260,7 @@
 
   <!-- 非做题模式:底部操作栏 -->
   <div class="bottom-bar" *ngIf="!isGenerating && !storyResponse && !showExerciseView">
-    <div class="bottom-wave">
-      <svg viewBox="0 0 1440 120" preserveAspectRatio="none">
-          <path fill="#ffffff" d="M0,64L80,69.3C160,75,320,85,480,80C640,75,800,53,960,48C1120,43,1280,53,1360,58.7L1440,64L1440,120L1360,120C1280,120,1120,120,960,120C800,120,640,120,480,120C320,120,160,120,80,120L0,120Z"></path>
-        </svg>
-    </div>
+    <div class="bottom-wave"><svg viewBox="0 0 1440 120" preserveAspectRatio="none"><path fill="#ffffff" d="M0,64L80,69.3C160,75,320,85,480,80C640,75,800,53,960,48C1120,43,1280,53,1360,58.7L1440,64L1440,120L1360,120C1280,120,1120,120,960,120C800,120,640,120,480,120C320,120,160,120,80,120L0,120Z"></path></svg></div>
     <div class="bottom-content">
       <div class="hint-section">
         <ion-icon name="information-circle-outline" class="hint-icon"></ion-icon>
@@ -272,14 +279,6 @@
     </div>
   </div>
 
-  <!-- 已生成 & 非做题:去做题按钮(固定在底部,不遮挡内容) -->
-  <div class="go-exercise-footer" *ngIf="storyResponse && !isGenerating && !showExerciseView">
-    <button class="go-exercise-btn" (click)="goToExercise()">
-      <ion-icon name="create-outline"></ion-icon>
-      <span>去做题</span>
-    </button>
-  </div>
-
   <!-- 做题模式:底部选项池 / 去评分 -->
   <div class="exercise-bottom-bar" *ngIf="showExerciseView && exerciseStory && availableWords.length > 0 && !isAllBlanksFilled() && !scoreResult">
     <div class="exercise-bottom-inner">

+ 50 - 40
word-app/src/app/word-story/word-story.page.scss

@@ -115,6 +115,14 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     padding: 0 16px;
     margin-bottom: 16px;
     gap: 40px;
+    // 添加滑动相关的样式
+    cursor: grab;
+    user-select: none;
+    touch-action: pan-y; // 允许垂直滚动,禁止水平滚动
+
+    &:active {
+      cursor: grabbing;
+    }
 
     .tab-item {
       display: flex;
@@ -462,6 +470,38 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
         font-size: 1.2rem;
       }
     }
+
+    // 去做题按钮 - 在滑动提示下方
+    .go-exercise-btn {
+      width: 88vw;
+      max-width: 420px;
+      min-width: 280px;
+      box-sizing: border-box;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 10px;
+      padding: 14px 24px;
+      border-radius: 999px;
+      border: none;
+      font-size: 1.1rem;
+      font-weight: 600;
+      color: $pearl;
+      background: linear-gradient(135deg, $ocean-mid 0%, $ocean-deep 100%);
+      box-shadow: 0 4px 16px rgba(2, 136, 209, 0.35);
+      cursor: pointer;
+      transition: all 0.25s ease;
+      margin-top: 80px; // 与滑动提示保持距离
+
+      ion-icon {
+        font-size: 1.4rem;
+      }
+
+      &:active {
+        transform: scale(0.96);
+        box-shadow: 0 2px 10px rgba(2, 136, 209, 0.3);
+      }
+    }
   }
 
   // 故事已生成标题
@@ -581,7 +621,7 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     -webkit-hyphens: auto;
     -moz-hyphens: auto;
     hyphenate-limit-chars: 4 2 2;
-    text-indent: 4ch;
+    text-indent: 3.5ch;
   }
 
   .blank-slot {
@@ -974,11 +1014,20 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     .bottom-wave {
       height: 30px;
       overflow: hidden;
+      line-height: 0;
+      font-size: 0;
+      display: block;
+      // SVG上半部分保持透明,让页面背景色透过来
+      background: transparent;
 
       svg {
         width: 100%;
         height: 100%;
         display: block;
+        vertical-align: top;
+        margin: 0;
+        padding: 0;
+        border: 0;
       }
     }
 
@@ -1057,45 +1106,6 @@ $seaweed: #4CAF50;          // 海藻绿(点缀)
     }
   }
 
-  // 已生成 & 非做题:去做题按钮(固定在底部,不遮挡内容)
-  .go-exercise-footer {
-    background: $pearl;
-    padding: 12px 16px 16px;
-    border-top: 3px solid $ocean-deep;
-    display: flex;
-    justify-content: center;
-  }
-
-  .go-exercise-btn {
-    width: 88vw;
-    max-width: 420px;
-    min-width: 280px;
-    box-sizing: border-box;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    gap: 10px;
-    padding: 14px 24px;
-    border-radius: 999px;
-    border: none;
-    font-size: 1.1rem;
-    font-weight: 600;
-    color: $pearl;
-    background: linear-gradient(135deg, $ocean-mid 0%, $ocean-deep 100%);
-    box-shadow: 0 4px 16px rgba(2, 136, 209, 0.35);
-    cursor: pointer;
-    transition: all 0.25s ease;
-
-    ion-icon {
-      font-size: 1.4rem;
-    }
-
-    &:active {
-      transform: scale(0.96);
-      box-shadow: 0 2px 10px rgba(2, 136, 209, 0.3);
-    }
-  }
-
   // 做题模式:底部选项池
   .exercise-bottom-bar {
     background: $pearl;

+ 91 - 22
word-app/src/app/word-story/word-story.page.ts

@@ -1,12 +1,12 @@
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { Router, NavigationStart } from '@angular/router';
 import { WordStoryService, StoryResponse } from './word-story.service';
 import { 
   IonContent, 
-  IonIcon,
-  IonFooter
+  IonFooter, 
+  IonIcon 
 } from '@ionic/angular/standalone';
 import { addIcons } from 'ionicons';
 import { 
@@ -50,17 +50,24 @@ export type ParagraphSegment = { type: 'text'; value: string } | { type: 'blank'
     FormsModule,
     IonContent,
     IonIcon,
-    IonFooter
+    IonFooter,
   ]
 })
 export class WordStoryPage implements OnInit {
-  
+
+  // IonContent 引用,用于滚动控制
+  @ViewChild(IonContent) content!: IonContent;
+
   // 当前标签页
   currentTab: 'learning' | 'learned' = 'learning';
   private isSwiped: boolean = false;
- swipeDirection: 'left' | 'right' | null = null;
- isAnimating: boolean = false; // 防止重复动画
-animationClass: string = '';   // 当前动画类,如 'slide-out-left', 'slide-in-right' 等
+  swipeDirection: 'left' | 'right' | null = null;
+  isAnimating: boolean = false; // 防止重复动画
+  animationClass: string = '';   // 当前动画类,如 'slide-out-left', 'slide-in-right' 等
+
+  // 标签页滑动相关
+  private tabTouchStartX: number = 0;
+  private tabTouchEndX: number = 0;
   // 新增属性
   storyResponse: StoryResponse | null = null;
   storyError: string | null = null;
@@ -153,12 +160,23 @@ animationClass: string = '';   // 当前动画类,如 'slide-out-left', 'slide
     return this.getParagraphSegments(paragraphEn, startBlankIndex).segments;
   }
 
+  /** 打乱数组顺序(Fisher-Yates 洗牌算法) */
+  private shuffleArray<T>(array: T[]): T[] {
+    const result = [...array];
+    for (let i = result.length - 1; i > 0; i--) {
+      const j = Math.floor(Math.random() * (i + 1));
+      [result[i], result[j]] = [result[j], result[i]];
+    }
+    return result;
+  }
+
   /** 初始化做题状态:统计空、初始化空白与词池 */
   initExerciseState(): void {
     if (!this.exerciseStory) return;
     const n = this.getTotalBlankCount();
     this.exerciseBlanks = new Array<null>(n).fill(null);
-    this.availableWords = [...this.exerciseStory.answers, ...this.exerciseStory.distractors];
+    // 打乱选项顺序
+    this.availableWords = this.shuffleArray([...this.exerciseStory.answers, ...this.exerciseStory.distractors]);
     this.focusedBlankIndex = null;
     this.scoreResult = null;
   }
@@ -228,14 +246,22 @@ animationClass: string = '';   // 当前动画类,如 'slide-out-left', 'slide
     for (let i = 0; i < answers.length; i++) {
       const user = (this.exerciseBlanks[i] || '').trim();
       const ans = (answers[i] || '').trim();
-      if (user) filled++;
+      if (user) {
+        filled++;
+      }
       if (user.toLowerCase() === ans.toLowerCase()) {
         correct++;
       }
     }
-    if (filled === 0) filled = answers.length;
+    if (filled === 0) {
+      filled = answers.length;
+    }
     const percent = Math.ceil((correct / filled) * 100);
     this.scoreResult = { correct, total: answers.length, filled, percent };
+    // 滚动到顶部查看评分结果
+    setTimeout(() => {
+      this.content.scrollToTop();
+    }, 100);
   }
 
   /** 某个空是否答对(仅评分后生效) */
@@ -261,6 +287,10 @@ animationClass: string = '';   // 当前动画类,如 'slide-out-left', 'slide
     this.showExerciseView = true;
     this.initExerciseState();
     this.router.navigate(['/word-story'], { queryParams: { mode: 'exercise' } });
+    // 滚动到顶部
+    setTimeout(() => {
+      this.content.scrollToTop();
+    }, 100);
   }
 
   // 获取当前显示的单词列表
@@ -268,9 +298,9 @@ animationClass: string = '';   // 当前动画类,如 'slide-out-left', 'slide
     return this.currentTab === 'learning' ? this.learningWords : this.learnedWords;
   }
 
-  // 获取已选中的单词数量
+  // 获取已选中的单词数量(包括待学习和已学习)
   get selectedCount(): number {
-    return this.currentWordList.filter(word => word.selected).length;
+    return [...this.learningWords, ...this.learnedWords].filter(word => word.selected).length;
   }
 
   // 获取当前闪卡单词
@@ -283,6 +313,42 @@ animationClass: string = '';   // 当前动画类,如 'slide-out-left', 'slide
     this.currentTab = tab;
   }
 
+  // 标签页滑动事件 - 触摸
+  onTabTouchStart(event: TouchEvent) {
+    this.tabTouchStartX = event.touches[0].clientX;
+  }
+
+  onTabTouchEnd(event: TouchEvent) {
+    this.tabTouchEndX = event.changedTouches[0].clientX;
+    this.handleTabSwipe();
+  }
+
+  // 标签页滑动事件 - 鼠标
+  onTabMouseDown(event: MouseEvent) {
+    this.tabTouchStartX = event.clientX;
+  }
+
+  onTabMouseUp(event: MouseEvent) {
+    this.tabTouchEndX = event.clientX;
+    this.handleTabSwipe();
+  }
+
+  // 处理标签页滑动
+  private handleTabSwipe() {
+    const diff = this.tabTouchEndX - this.tabTouchStartX;
+    const threshold = 50; // 滑动阈值
+
+    if (Math.abs(diff) > threshold) {
+      if (diff > 0 && this.currentTab === 'learned') {
+        // 向右滑动,切换到待学习
+        this.switchTab('learning');
+      } else if (diff < 0 && this.currentTab === 'learning') {
+        // 向左滑动,切换到已学习
+        this.switchTab('learned');
+      }
+    }
+  }
+
   // 切换单词选中状态
   toggleSelect(word: WordItem) {
     word.selected = !word.selected;
@@ -533,14 +599,15 @@ prevWord() {
 
   // 打开风格选择弹窗(重置向导)
   generateStory() {
-    const selectedWords = this.currentWordList
+    // 从两个列表中获取所有选中的单词
+    const selectedWords = [...this.learningWords, ...this.learnedWords]
       .filter(word => word.selected)
       .map(word => word.word);
-    
+
     if (selectedWords.length < 5 || selectedWords.length > 10) {
       return;
     }
-    
+
     // 重置向导状态
     this.currentWizardStep = 0;
     this.wizardData = { style: '', difficulty: '', genre: '' };
@@ -554,18 +621,19 @@ prevWord() {
   }
 
     confirmStyle() {
-    const selectedWords = this.currentWordList
+    // 从两个列表中获取所有选中的单词
+    const selectedWords = [...this.learningWords, ...this.learnedWords]
       .filter(word => word.selected)
       .map(word => word.word);
-    
+
     // 构建闪卡数据(用于滑动复习)
     this.flashWords = selectedWords.map(word => this.parseWordItem(word));
     this.currentFlashIndex = 0;
-    
+
     this.closeStyleModal();
     this.isGenerating = true;
     this.storyError = null;
-    
+
     // 调用AI生成故事
     this.wordStoryService.generateStory(
       selectedWords,
@@ -575,10 +643,10 @@ prevWord() {
       next: (response) => {
         this.storyResponse = response;
         console.log('生成的故事:', response);
-        
+
         // 这里可以将response传递给故事展示页面或组件
         // 例如:this.router.navigate(['/story-result'], { state: { story: response } });
-        
+
         // 暂时保留在生成界面,实际可以跳转到结果页
         this.isGenerating = false; // 测试用,实际应该跳转
       },
@@ -601,3 +669,4 @@ prevWord() {
 }
 
 
+