|
|
@@ -28,7 +28,7 @@ import {
|
|
|
X,
|
|
|
} from 'lucide-angular';
|
|
|
import { catchError, finalize, forkJoin, map, Observable, of, Subscription, switchMap, tap, throwError } from 'rxjs';
|
|
|
-import { GAME_AUDIO } from '../../core/app.constants';
|
|
|
+import { listeningChoices, shuffledStudy } from '../../core/learning-random';
|
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
|
import {
|
|
|
AntiForgetCalendarDay,
|
|
|
@@ -149,6 +149,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly matchRight = signal<Word[]>([]);
|
|
|
readonly leftChoice = signal('');
|
|
|
readonly rightChoice = signal('');
|
|
|
+ readonly choicesLoading = signal(false);
|
|
|
readonly choiceOptions = signal<Word[]>([]);
|
|
|
readonly letters = signal<LetterChoice[]>([]);
|
|
|
readonly assembled = signal('');
|
|
|
@@ -287,6 +288,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
private calendarRequest?: Subscription;
|
|
|
private choiceOptionsRequest?: Subscription;
|
|
|
private choiceOptionsVersion = 0;
|
|
|
+ private outsideChoicePool: Word[] | null = null;
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
if (isTeacherLedLearningPath(this.meta.path) && !this.isReadOnlyWordView && !this.isReadOnlySelectionPage && !this.canStartPointReading && !this.isNewWordSelfStudyStage) {
|
|
|
@@ -733,7 +735,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
shuffleStudyWords(): void {
|
|
|
- this.words.set(this.shuffle(this.words()));
|
|
|
+ this.words.set(shuffledStudy(this.words()));
|
|
|
this.repetitions.set({});
|
|
|
}
|
|
|
|
|
|
@@ -757,7 +759,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.rightChoice.set('');
|
|
|
return;
|
|
|
}
|
|
|
- this.platform.play(this.api.assetUrl(GAME_AUDIO.success));
|
|
|
+ this.platform.playEffect('success');
|
|
|
this.matchLeft.update((items) => items.filter((item) => item.id !== left));
|
|
|
this.matchRight.update((items) => items.filter((item) => item.id !== right));
|
|
|
this.leftChoice.set('');
|
|
|
@@ -766,16 +768,17 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
chooseMeaning(option: Word): void {
|
|
|
+ if (this.choicesLoading() || this.choiceOptions().length !== 4 || this.completed() || !this.choiceOptions().some(w => w.id === option.id)) return;
|
|
|
const word = this.current();
|
|
|
if (!word) return;
|
|
|
if (option.id !== word.id) {
|
|
|
- this.platform.play(this.api.assetUrl(GAME_AUDIO.error));
|
|
|
+ this.platform.playEffect('error');
|
|
|
this.platform.toast('答错了,请重新听一遍', 'error');
|
|
|
this.play(word);
|
|
|
return;
|
|
|
}
|
|
|
this.correct.update((value) => value + 1);
|
|
|
- this.platform.play(this.api.assetUrl(GAME_AUDIO.success));
|
|
|
+ this.platform.playEffect('success');
|
|
|
if (this.currentIndex() >= this.words().length - 1) {
|
|
|
this.completed.set(true);
|
|
|
return;
|
|
|
@@ -795,11 +798,11 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
if (next.toLowerCase() === word.word.toLowerCase()) {
|
|
|
this.spellingCorrect.set(true);
|
|
|
this.correct.update((value) => value + 1);
|
|
|
- this.platform.play(this.api.assetUrl(GAME_AUDIO.success));
|
|
|
+ this.platform.playEffect('success');
|
|
|
this.platform.toast('拼写正确', 'success');
|
|
|
return;
|
|
|
}
|
|
|
- this.platform.play(this.api.assetUrl(GAME_AUDIO.error));
|
|
|
+ this.platform.playEffect('error');
|
|
|
this.platform.toast('拼写错了,再试一次', 'error');
|
|
|
this.prepareSpelling();
|
|
|
}
|
|
|
@@ -1393,7 +1396,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
if (this.studyStep() < 3) {
|
|
|
const next = (this.studyStep() + 1) as 2 | 3;
|
|
|
this.studyStep.set(next);
|
|
|
- this.words.set(next === 2 ? [...this.words()].reverse() : this.shuffle(this.words()));
|
|
|
+ this.words.set(shuffledStudy(this.words()));
|
|
|
this.repetitions.set({});
|
|
|
this.platform.toast(next === 2 ? '进入乱序学习' : '进入中文挑战', 'success');
|
|
|
return;
|
|
|
@@ -1401,6 +1404,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.navigateStage('/pages/stu/stu3');
|
|
|
}
|
|
|
|
|
|
+ retryListeningChoices(): void { this.outsideChoicePool = null; this.prepareChoiceOptions(); }
|
|
|
+
|
|
|
private prepareChoiceOptions(): void {
|
|
|
this.choiceOptionsRequest?.unsubscribe();
|
|
|
const version = ++this.choiceOptionsVersion;
|
|
|
@@ -1409,25 +1414,26 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.choiceOptions.set([]);
|
|
|
return;
|
|
|
}
|
|
|
- const distractors = this.shuffle(this.words().filter((word) => word.id !== current.id && word.meaning !== current.meaning)).slice(0, 3);
|
|
|
- this.choiceOptions.set(this.shuffle([current, ...distractors]));
|
|
|
- if (distractors.length < 3) {
|
|
|
- this.choiceOptionsRequest = this.api.get<unknown>('content_list', { modelId: 52, orders: 'NEWID()', cpage: 1, psize: 8 }).subscribe({
|
|
|
- next: ({ result }) => {
|
|
|
- if (version !== this.choiceOptionsVersion || this.current() !== current || this.completed()) return;
|
|
|
- const usedIds = new Set([current.id, ...distractors.map((word) => word.id)]);
|
|
|
- const usedMeanings = new Set([current.meaning, ...distractors.map((word) => word.meaning)]);
|
|
|
- const extras = this.shuffle(this.findList(result).map((item) => this.toWord(item))).filter((word) => {
|
|
|
- if (!word.id || !word.meaning || usedIds.has(word.id) || usedMeanings.has(word.meaning)) return false;
|
|
|
- usedIds.add(word.id);
|
|
|
- usedMeanings.add(word.meaning);
|
|
|
- return true;
|
|
|
- }).slice(0, 3 - distractors.length);
|
|
|
- this.choiceOptions.set(this.shuffle([current, ...distractors, ...extras]));
|
|
|
- },
|
|
|
- error: () => undefined,
|
|
|
- });
|
|
|
- }
|
|
|
+ const round = [...this.readAllLearningWords(), ...this.words()];
|
|
|
+ const apply = (pool: Word[]) => {
|
|
|
+ if (version !== this.choiceOptionsVersion || this.current() !== current || this.completed()) return;
|
|
|
+ this.choiceOptions.set(listeningChoices(current, round, pool, items => this.shuffle(items)));
|
|
|
+ this.choicesLoading.set(false);
|
|
|
+ };
|
|
|
+ if (this.outsideChoicePool !== null) { apply(this.outsideChoicePool); return; }
|
|
|
+ this.choiceOptions.set([]);
|
|
|
+ this.choicesLoading.set(true);
|
|
|
+ this.choiceOptionsRequest = this.api.get<unknown>('content_list', { modelId: 52, orders: 'NEWID()', cpage: 1, psize: 60, excludeWordIds: [...new Set(round.map(w => w.id))].join(',') }).subscribe({
|
|
|
+ next: ({ result }) => {
|
|
|
+ if (version !== this.choiceOptionsVersion || this.current() !== current || this.completed()) return;
|
|
|
+ this.outsideChoicePool = this.findList(result).map(item => this.toWord(item));
|
|
|
+ apply(this.outsideChoicePool);
|
|
|
+ },
|
|
|
+ error: () => {
|
|
|
+ apply([]);
|
|
|
+ this.platform.toast('拓展选项暂未加载,本题使用已选词汇', 'info');
|
|
|
+ },
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
private prepareSpelling(): void {
|
|
|
@@ -2039,6 +2045,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
private clearLearningStorage(): void { this.draft().clear(); }
|
|
|
|
|
|
private resetPageState(): void {
|
|
|
+ this.outsideChoicePool = null;
|
|
|
+ this.choicesLoading.set(false);
|
|
|
this.choiceOptionsVersion++;
|
|
|
this.choiceOptionsRequest?.unsubscribe();
|
|
|
this.stopExamTimer();
|