|
|
@@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { Router, NavigationStart } from '@angular/router';
|
|
|
import { WordStoryService, StoryResponse } from './word-story.service';
|
|
|
+import { CloudObject, CloudUser } from '../recite-words/ncloud';
|
|
|
import {
|
|
|
IonContent,
|
|
|
IonFooter,
|
|
|
@@ -20,7 +21,8 @@ import {
|
|
|
chevronForwardOutline,
|
|
|
createOutline,
|
|
|
saveOutline,
|
|
|
- addOutline
|
|
|
+ addOutline,
|
|
|
+ timeOutline
|
|
|
} from 'ionicons/icons';
|
|
|
|
|
|
// 单词数据接口
|
|
|
@@ -39,6 +41,16 @@ interface FlashWord {
|
|
|
chineseMeaning: string;
|
|
|
}
|
|
|
|
|
|
+interface HistoryRecordState {
|
|
|
+ titleEn: string;
|
|
|
+ titleZh: string;
|
|
|
+ paragraphs: { en: string; zh: string }[];
|
|
|
+ answers: string[];
|
|
|
+ distractors: string[];
|
|
|
+ userBlanks: string[];
|
|
|
+ score: number;
|
|
|
+}
|
|
|
+
|
|
|
/** 段落中的片段:文本或挖空 */
|
|
|
export type ParagraphSegment = { type: 'text'; value: string } | { type: 'blank'; index: number };
|
|
|
|
|
|
@@ -98,6 +110,7 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
|
|
|
// 做题页状态
|
|
|
showExerciseView = false;
|
|
|
+ isHistoryReplay = false;
|
|
|
exerciseStory: StoryResponse | null = null;
|
|
|
/** 每个空当前填写的词,null 表示未填 */
|
|
|
exerciseBlanks: (string | null)[] = [];
|
|
|
@@ -140,6 +153,10 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
|
|
|
// 退出确认弹窗状态
|
|
|
showExitModal: boolean = false;
|
|
|
+ // 保存确认弹窗状态
|
|
|
+ showSaveModal: boolean = false;
|
|
|
+ // 保存中状态,防重复点击
|
|
|
+ isSavingStory: boolean = false;
|
|
|
|
|
|
// 预设选项数据
|
|
|
styleOptions: string[] = ['童话', '青春', '悬疑', '励志', '科幻', '日常', '历史', '浪漫'];
|
|
|
@@ -201,7 +218,8 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
chevronForwardOutline,
|
|
|
createOutline,
|
|
|
saveOutline,
|
|
|
- addOutline
|
|
|
+ addOutline,
|
|
|
+ timeOutline
|
|
|
});
|
|
|
// 从其他页通过 state 进入做题页时恢复故事
|
|
|
const nav = this.router.getCurrentNavigation();
|
|
|
@@ -209,12 +227,39 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
this.exerciseStory = nav.extras.state['story'] as StoryResponse;
|
|
|
this.showExerciseView = true;
|
|
|
}
|
|
|
+ if (nav?.extras?.state?.['historyRecord']) {
|
|
|
+ this.isHistoryReplay = true;
|
|
|
+ const history = nav.extras.state['historyRecord'] as HistoryRecordState;
|
|
|
+ this.exerciseStory = {
|
|
|
+ titleEn: history.titleEn || '',
|
|
|
+ titleZh: history.titleZh || '',
|
|
|
+ paragraphs: history.paragraphs || [],
|
|
|
+ answers: history.answers || [],
|
|
|
+ distractors: history.distractors || []
|
|
|
+ };
|
|
|
+ this.showExerciseView = true;
|
|
|
+ this.exerciseBlanks = (history.userBlanks || []).map((x) => x ?? '');
|
|
|
+ this.availableWords = [];
|
|
|
+ this.focusedBlankIndex = null;
|
|
|
+ const total = this.exerciseStory.answers.length;
|
|
|
+ const filled = this.exerciseBlanks.filter((x) => !!String(x).trim()).length;
|
|
|
+ const correct = this.exerciseStory.answers.reduce((acc, ans, idx) => {
|
|
|
+ const user = (this.exerciseBlanks[idx] || '').trim().toLowerCase();
|
|
|
+ return acc + (user && user === String(ans || '').trim().toLowerCase() ? 1 : 0);
|
|
|
+ }, 0);
|
|
|
+ this.scoreResult = {
|
|
|
+ correct,
|
|
|
+ total,
|
|
|
+ filled,
|
|
|
+ percent: Number(history.score ?? 0)
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
// 与 currentTab 保持一致
|
|
|
this.tabSwipeProgress = this.currentTab === 'learned' ? 1 : 0;
|
|
|
- if (this.exerciseStory) this.initExerciseState();
|
|
|
+ if (this.exerciseStory && !this.isHistoryReplay) this.initExerciseState();
|
|
|
}
|
|
|
|
|
|
/** 仅在“选择单词列表页”时禁用页面整体滚动,让列表区域单独滚动 */
|
|
|
@@ -288,6 +333,7 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
|
|
|
/** 点击挖空区域:若已有词则退回词池并选中该空;若无词则仅选中该空 */
|
|
|
selectBlank(index: number): void {
|
|
|
+ if (this.scoreResult) return;
|
|
|
const current = this.exerciseBlanks[index];
|
|
|
if (current != null) {
|
|
|
this.availableWords = [...this.availableWords, current];
|
|
|
@@ -817,13 +863,15 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
|
|
|
// 选择难度
|
|
|
selectDifficulty(difficulty: string) {
|
|
|
- this.wizardData.difficulty = difficulty;
|
|
|
+ this.wizardData.difficulty =
|
|
|
+ this.wizardData.difficulty === difficulty ? '' : difficulty;
|
|
|
this.showOtherDifficultyInput = false;
|
|
|
}
|
|
|
|
|
|
// 选择体裁
|
|
|
selectGenre(genre: string) {
|
|
|
- this.wizardData.genre = genre;
|
|
|
+ this.wizardData.genre =
|
|
|
+ this.wizardData.genre === genre ? '' : genre;
|
|
|
this.showOtherGenreInput = false;
|
|
|
}
|
|
|
|
|
|
@@ -993,6 +1041,12 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
|
|
|
// 返回上一页
|
|
|
goBack() {
|
|
|
+ // 历史回放模式:返回直接回历史列表,不弹框
|
|
|
+ if (this.isHistoryReplay) {
|
|
|
+ this.router.navigate(['/word-story-history']);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
// 评分结果页面点击返回,弹出确认弹窗
|
|
|
if (this.showExerciseView && this.scoreResult) {
|
|
|
this.showExitModal = true;
|
|
|
@@ -1027,16 +1081,66 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
|
|
|
// 保存故事 - 存入数据库(先不实现),然后重置状态回到tab2页面
|
|
|
saveStory() {
|
|
|
- // TODO: 将数据存入数据库
|
|
|
- console.log('保存故事:', {
|
|
|
- story: this.exerciseStory,
|
|
|
- score: this.scoreResult,
|
|
|
- answers: this.exerciseBlanks
|
|
|
+ // 评分页点击保存图标时,先弹确认框
|
|
|
+ if (!this.showExerciseView || !this.scoreResult || !this.exerciseStory) return;
|
|
|
+ this.showSaveModal = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ closeSaveModal() {
|
|
|
+ if (this.isSavingStory) return;
|
|
|
+ this.showSaveModal = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ async confirmSaveStory() {
|
|
|
+ if (this.isSavingStory) return;
|
|
|
+ this.isSavingStory = true;
|
|
|
+ try {
|
|
|
+ const ok = await this.persistStoryHistory();
|
|
|
+ if (!ok) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.showSaveModal = false;
|
|
|
+ // 保存成功后停留在当前页面(评分页)
|
|
|
+ } finally {
|
|
|
+ this.isSavingStory = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private async persistStoryHistory(): Promise<boolean> {
|
|
|
+ if (!this.exerciseStory || !this.scoreResult) {
|
|
|
+ console.warn('[WordStory] 保存失败:评分数据不完整');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const user = await CloudUser.getCurrent();
|
|
|
+ if (!user?.id) {
|
|
|
+ console.warn('[WordStory] 保存失败:当前未登录');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ const row = new CloudObject('word_story_history');
|
|
|
+ row.set({
|
|
|
+ user: { __type: 'Pointer', className: '_User', objectId: user.id },
|
|
|
+ titleEn: this.exerciseStory.titleEn || '',
|
|
|
+ titleZh: this.exerciseStory.titleZh || '',
|
|
|
+ paragraphs: this.exerciseStory.paragraphs || [],
|
|
|
+ answers: this.exerciseStory.answers || [],
|
|
|
+ userBlanks: this.exerciseBlanks.map((x) => x ?? ''),
|
|
|
+ distractors: this.exerciseStory.distractors || [],
|
|
|
+ score: this.scoreResult.percent,
|
|
|
+ difficulty: this.wizardData.difficulty || '',
|
|
|
+ genre: this.wizardData.genre || '',
|
|
|
+ styles: this.wizardData.styles || []
|
|
|
});
|
|
|
-
|
|
|
- // 重置状态并回到tab2页面
|
|
|
- this.resetState();
|
|
|
- this.router.navigate(['/tabs/tab2']);
|
|
|
+
|
|
|
+ await row.save();
|
|
|
+ console.log('[WordStory] 保存成功', row.id);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ onHistoryClick(event: Event): void {
|
|
|
+ event.stopPropagation();
|
|
|
+ this.router.navigate(['/word-story-history']);
|
|
|
}
|
|
|
|
|
|
// 重置页面状态
|
|
|
@@ -1054,6 +1158,8 @@ listSwipeDirection: 'left' | 'right' | null = null;
|
|
|
this.currentFlashIndex = 0;
|
|
|
this.showStyleModal = false;
|
|
|
this.showExitModal = false;
|
|
|
+ this.showSaveModal = false;
|
|
|
+ this.isSavingStory = false;
|
|
|
// 重置单词选中状态
|
|
|
this.learningWords.forEach(word => word.selected = false);
|
|
|
this.learnedWords.forEach(word => word.selected = false);
|