|
|
@@ -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() {
|
|
|
}
|
|
|
|
|
|
|
|
|
+
|