|
|
@@ -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']);
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
+}
|