|
|
@@ -132,7 +132,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly knownSaveRetry = signal(false);
|
|
|
private knownSaveContinuation: { continueLearning: boolean; openKnownPass: boolean } | null = null;
|
|
|
private courseSelectionNeedsRebase = false;
|
|
|
- readonly bulkCandidates = computed(() => this.visibleCourseWords().filter(word => !this.wordHistoricallyLearned(word)));
|
|
|
+ readonly bulkCandidates = computed(() => this.visibleCourseWords().filter(word => this.canRelearnCourseWord(word) && this.selectionAssessment()[word.id] !== 'known'));
|
|
|
readonly activeSelectionBucket = signal<'pending' | 'completed'>('pending');
|
|
|
readonly coursePassTabs = ['未学习', '已学一遍', '已学第二遍', '已学第三遍'];
|
|
|
readonly coursePass = signal(0);
|
|
|
@@ -140,7 +140,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly bookPassCounts = signal<number[] | null>(null);
|
|
|
private readonly courseSelectionCache = signal<Word[]>([]);
|
|
|
readonly visibleCourseWords = computed(() => {
|
|
|
- const candidates = this.coursePass() === 1 ? [...this.words(), ...this.newlyKnownWords()] : this.words();
|
|
|
+ const candidates = [...this.words(), ...this.newlyKnownWords()];
|
|
|
return [...new Map(candidates.map(word => [word.id, word])).values()]
|
|
|
.filter(word => this.courseWordPass(word) === this.coursePass());
|
|
|
});
|
|
|
@@ -261,7 +261,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly visibleNewWords = computed(() => this.activeSelectionBucket() === 'completed' ? this.learnedNewWords() : this.unlearnedNewWords());
|
|
|
readonly newWordPageCount = computed(() => Math.max(1, Math.ceil(this.selectionTotal() / this.newWordPageSize)));
|
|
|
readonly allVisibleNewWordsSelected = computed(() => this.visibleNewWords().length > 0 && this.visibleNewWords().every((word) => this.selected().has(word.id)));
|
|
|
- readonly newlyKnownWords = computed(() => (this.isRefineSelectionPage ? this.courseSelectionCache() : this.words()).filter((word) => !this.wordHistoricallyLearned(word) && this.selectionAssessment()[word.id] === 'known'));
|
|
|
+ readonly newlyKnownWords = computed(() => (this.isRefineSelectionPage ? this.courseSelectionCache() : this.words()).filter((word) => this.selectionAssessment()[word.id] === 'known'));
|
|
|
readonly selectionActionLabel = computed(() => {
|
|
|
if (this.knownSaveRetry()) return '重试保存';
|
|
|
const hasLearningWords = this.selected().size > 0;
|
|
|
@@ -667,14 +667,21 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
courseWordPass(word: Word): number {
|
|
|
- return Math.min(3, Math.max(this.selectionAssessment()[word.id] === 'known' ? 1 : 0, Math.floor(Number(word['w_learned']) || 0)));
|
|
|
+ return Math.min(3, this.savedCoursePass(word) + (this.selectionAssessment()[word.id] === 'known' ? 1 : 0));
|
|
|
+ }
|
|
|
+
|
|
|
+ private savedCoursePass(word: Word): number {
|
|
|
+ return Math.min(3, Math.max(0, Math.floor(Number(word['w_learned']) || 0)));
|
|
|
}
|
|
|
|
|
|
coursePassCount(pass: number): number {
|
|
|
const counts = this.coursePassCounts();
|
|
|
if (!counts) return 0;
|
|
|
- const known = this.newlyKnownWords().length;
|
|
|
- return Math.max(0, counts[pass] + (pass === 0 ? -known : pass === 1 ? known : 0));
|
|
|
+ const adjustment = this.newlyKnownWords().reduce((total, word) => {
|
|
|
+ const before = this.savedCoursePass(word), after = Math.min(3, before + 1);
|
|
|
+ return total + Number(after === pass) - Number(before === pass);
|
|
|
+ }, 0);
|
|
|
+ return Math.max(0, counts[pass] + adjustment);
|
|
|
}
|
|
|
|
|
|
canRelearnCourseWord(word: Word): boolean {
|
|
|
@@ -685,7 +692,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
const pass = this.coursePass();
|
|
|
if (pass === 0) return this.isReadOnlySelectionPage ? '这些单词尚未完成第一遍学习,点击可查看释义。' : '可直接选择“认识”或“不认识”,也可全选本页后批量判断。';
|
|
|
if (pass === 3) return '这些单词已完成三遍学习,可查看和听读音。';
|
|
|
- return this.isReadOnlySelectionPage ? `这些单词已学${pass}遍,可由老师选词继续学习。` : `选择单词开始第${pass === 1 ? '二' : '三'}遍学习。`;
|
|
|
+ return this.isReadOnlySelectionPage ? `这些单词已学${pass}遍,可由老师选词继续学习。` : `选择单词开始第${pass === 1 ? '二' : '三'}遍学习,也可全选本页批量判断。`;
|
|
|
}
|
|
|
|
|
|
selectCoursePass(pass: number): void {
|
|
|
@@ -715,14 +722,14 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
markWordKnown(word: Word): void {
|
|
|
- if (this.selectionLocked() || this.isReadOnlySelectionPage || this.wordHistoricallyLearned(word)) return;
|
|
|
+ if (this.selectionLocked() || !this.canRelearnCourseWord(word) || this.selectionAssessment()[word.id] === 'known') return;
|
|
|
this.setWordSelected(word, false);
|
|
|
this.setSelectionAssessment(word, 'known');
|
|
|
this.bulkSelection.update(current => current === null ? null : new Set([...current].filter(id => id !== word.id)));
|
|
|
}
|
|
|
|
|
|
markWordUnknown(word: Word): void {
|
|
|
- if (this.selectionLocked() || this.isReadOnlySelectionPage || this.wordHistoricallyLearned(word)) return;
|
|
|
+ if (this.selectionLocked() || this.isReadOnlySelectionPage || this.savedCoursePass(word) >= 3) return;
|
|
|
if (this.setWordSelected(word, true)) {
|
|
|
this.setSelectionAssessment(word, 'unknown');
|
|
|
this.bulkSelection.update(current => current === null ? null : new Set([...current].filter(id => id !== word.id)));
|
|
|
@@ -734,12 +741,12 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
toggleBulkSelection(): void {
|
|
|
- if (this.selectionLocked() || this.selectionLoading() || this.isReadOnlySelectionPage || this.coursePass() !== 0) return;
|
|
|
+ if (this.selectionLocked() || this.selectionLoading() || this.isReadOnlySelectionPage || this.coursePass() >= 3) return;
|
|
|
this.bulkSelection.set(this.bulkSelection() === null ? new Set(this.bulkCandidates().map(word => word.id)) : null);
|
|
|
}
|
|
|
|
|
|
applyBulkAssessment(assessment: 'known' | 'unknown'): void {
|
|
|
- if (this.selectionLocked() || this.isReadOnlySelectionPage || this.coursePass() !== 0) return;
|
|
|
+ if (this.selectionLocked() || this.isReadOnlySelectionPage || this.coursePass() >= 3) return;
|
|
|
const checked = this.bulkSelection();
|
|
|
if (!checked?.size) return;
|
|
|
const candidates = this.bulkCandidates();
|
|
|
@@ -817,7 +824,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.beginSelectedFlow(chosen, '');
|
|
|
return;
|
|
|
}
|
|
|
- if (openKnownPass) this.selectCoursePass(1);
|
|
|
+ if (openKnownPass) this.selectCoursePass(Math.min(3, this.coursePass() + 1));
|
|
|
this.platform.toast('识词结果已保存', 'success');
|
|
|
},
|
|
|
error: (error: ApiError) => {
|
|
|
@@ -2433,17 +2440,20 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
wordsId: batch.map((word) => word.wordId).join(','),
|
|
|
kcid: (this.meta.path === 'pages/stu/pl_end' ? this.orderInfo()['learningCourseId'] : '') || this.query('pnid') || this.query('kcid') || this.orderInfo()['kcid'],
|
|
|
save: 1,
|
|
|
- requestKey:this.draft().command('learn:'+batch.map(w=>w.wordId).sort().join(',')),
|
|
|
+ requestKey:this.draft().command('learn:'+batch.map(w=>w.wordId+'@'+this.savedCoursePass(w)).sort().join(',')),
|
|
|
}).pipe(tap(() => onBatchSaved?.(batch)))), toArray());
|
|
|
}
|
|
|
|
|
|
private commitKnownWords(words: Word[]): void {
|
|
|
- if (this.isRefineSelectionPage && this.coursePass() === 0) this.courseSelectionNeedsRebase = true;
|
|
|
- const ids = new Set(words.map(word => word.id));
|
|
|
- const update = (current: Word[]) => current.map(word => ids.has(word.id) ? {...word, w_learned: 1} : word);
|
|
|
+ if (this.isRefineSelectionPage) this.courseSelectionNeedsRebase = true;
|
|
|
+ const nextPass = new Map(words.map(word => [word.id, Math.min(3, this.savedCoursePass(word) + 1)]));
|
|
|
+ const ids = new Set(nextPass.keys());
|
|
|
+ const update = (current: Word[]) => current.map(word => ids.has(word.id) ? {...word, w_learned: nextPass.get(word.id)!} : word);
|
|
|
this.words.update(update);
|
|
|
this.courseSelectionCache.update(update);
|
|
|
- const advance = (counts: number[] | null) => counts ? [Math.max(0,counts[0]-ids.size),counts[1]+ids.size,counts[2],counts[3]] : null;
|
|
|
+ const changes = [0,0,0,0];
|
|
|
+ for (const word of words) { changes[this.savedCoursePass(word)]--; changes[nextPass.get(word.id)!]++; }
|
|
|
+ const advance = (counts: number[] | null) => counts ? counts.map((count, pass) => Math.max(0, count + changes[pass])) : null;
|
|
|
this.coursePassCounts.update(advance);
|
|
|
this.bookPassCounts.update(advance);
|
|
|
this.selectionAssessment.update(current => Object.fromEntries(Object.entries(current).filter(([id]) => !ids.has(id))) as Record<string, 'known' | 'unknown'>);
|