|
@@ -123,6 +123,9 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly selectionRevealed = signal<Set<string>>(new Set());
|
|
readonly selectionRevealed = signal<Set<string>>(new Set());
|
|
|
readonly selectionAssessment = signal<Record<string, 'known' | 'unknown'>>({});
|
|
readonly selectionAssessment = signal<Record<string, 'known' | 'unknown'>>({});
|
|
|
readonly activeSelectionBucket = signal<'pending' | 'completed'>('pending');
|
|
readonly activeSelectionBucket = signal<'pending' | 'completed'>('pending');
|
|
|
|
|
+ readonly newWordCounts = signal({ pending: 0, completed: 0 });
|
|
|
|
|
+ private readonly newWordPageSize = 30;
|
|
|
|
|
+ private readonly newWordSelectionCache = new Map<string, Word>();
|
|
|
readonly currentIndex = signal(0);
|
|
readonly currentIndex = signal(0);
|
|
|
readonly revealed = signal(false);
|
|
readonly revealed = signal(false);
|
|
|
readonly completed = signal(false);
|
|
readonly completed = signal(false);
|
|
@@ -208,6 +211,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly learnedNewWords = computed(() => this.words().filter((word) => this.newWordLearned(word)));
|
|
readonly learnedNewWords = computed(() => this.words().filter((word) => this.newWordLearned(word)));
|
|
|
readonly unlearnedNewWords = computed(() => this.words().filter((word) => !this.newWordLearned(word)));
|
|
readonly unlearnedNewWords = computed(() => this.words().filter((word) => !this.newWordLearned(word)));
|
|
|
readonly visibleNewWords = computed(() => this.activeSelectionBucket() === 'completed' ? this.learnedNewWords() : this.unlearnedNewWords());
|
|
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 allVisibleNewWordsSelected = computed(() => this.visibleNewWords().length > 0 && this.visibleNewWords().every((word) => this.selected().has(word.id)));
|
|
|
readonly newlyKnownWords = computed(() => this.words().filter((word) => !this.wordHistoricallyLearned(word) && this.selectionAssessment()[word.id] === 'known'));
|
|
readonly newlyKnownWords = computed(() => this.words().filter((word) => !this.wordHistoricallyLearned(word) && this.selectionAssessment()[word.id] === 'known'));
|
|
|
readonly selectionActionLabel = computed(() => {
|
|
readonly selectionActionLabel = computed(() => {
|
|
@@ -433,7 +437,9 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
get selectionLimit(): number {
|
|
get selectionLimit(): number {
|
|
|
- return this.meta.path === 'pages/stu/stu_abc' ? 30 : this.words().length;
|
|
|
|
|
|
|
+ if (this.meta.path === 'pages/stu/stu_abc') return 30;
|
|
|
|
|
+ if (this.isNewWordSelectionPage) return 100;
|
|
|
|
|
+ return this.words().length;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
load(): void {
|
|
load(): void {
|
|
@@ -620,7 +626,10 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
const next = new Set(this.selected());
|
|
const next = new Set(this.selected());
|
|
|
for (const word of visible) {
|
|
for (const word of visible) {
|
|
|
if (clearVisible) next.delete(word.id);
|
|
if (clearVisible) next.delete(word.id);
|
|
|
- else next.add(word.id);
|
|
|
|
|
|
|
+ else if (next.size < this.selectionLimit) next.add(word.id);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!clearVisible && next.size === this.selectionLimit && visible.some((word) => !next.has(word.id))) {
|
|
|
|
|
+ this.platform.toast(`本次最多选择 ${this.selectionLimit} 个单词`, 'error');
|
|
|
}
|
|
}
|
|
|
this.selected.set(next);
|
|
this.selected.set(next);
|
|
|
return;
|
|
return;
|
|
@@ -633,7 +642,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
startSelected(): void {
|
|
startSelected(): void {
|
|
|
- const chosen = (this.isRefineSelectionPage ? this.pendingSelectionWords() : this.words())
|
|
|
|
|
|
|
+ const available = this.isNewWordSelectionPage ? [...this.newWordSelectionCache.values()] : this.isRefineSelectionPage ? this.pendingSelectionWords() : this.words();
|
|
|
|
|
+ const chosen = available
|
|
|
.filter((word) => this.selected().has(word.id));
|
|
.filter((word) => this.selected().has(word.id));
|
|
|
const newlyKnown = this.newlyKnownWords();
|
|
const newlyKnown = this.newlyKnownWords();
|
|
|
if (!chosen.length && !newlyKnown.length) {
|
|
if (!chosen.length && !newlyKnown.length) {
|
|
@@ -666,7 +676,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
deleteSelectedNewWords(): void {
|
|
deleteSelectedNewWords(): void {
|
|
|
- const chosen = this.words().filter((word) => this.selected().has(word.id));
|
|
|
|
|
|
|
+ const chosen = [...this.newWordSelectionCache.values()].filter((word) => this.selected().has(word.id));
|
|
|
if (!chosen.length) {
|
|
if (!chosen.length) {
|
|
|
this.platform.toast('请选择单词', 'error');
|
|
this.platform.toast('请选择单词', 'error');
|
|
|
return;
|
|
return;
|
|
@@ -1181,6 +1191,22 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
loadMoreSelection():void{if(!this.selectionLoading()&&this.words().length<this.selectionTotal()){this.selectionPage.update(p=>p+1);this.loadSelection(true);}}
|
|
loadMoreSelection():void{if(!this.selectionLoading()&&this.words().length<this.selectionTotal()){this.selectionPage.update(p=>p+1);this.loadSelection(true);}}
|
|
|
|
|
+ selectNewWordBucket(bucket: 'pending' | 'completed'): void {
|
|
|
|
|
+ if (this.activeSelectionBucket() === bucket) return;
|
|
|
|
|
+ this.selectionRequest?.unsubscribe();
|
|
|
|
|
+ this.activeSelectionBucket.set(bucket);
|
|
|
|
|
+ this.selectionPage.set(1);
|
|
|
|
|
+ this.selectionTotal.set(this.newWordCounts()[bucket]);
|
|
|
|
|
+ this.words.set([]);
|
|
|
|
|
+ this.loadSelection();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ goToNewWordPage(page: number): void {
|
|
|
|
|
+ if (this.selectionLoading() || page < 1 || page > this.newWordPageCount() || page === this.selectionPage()) return;
|
|
|
|
|
+ this.selectionPage.set(page);
|
|
|
|
|
+ this.loadSelection();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private loadSelection(more=false): void {
|
|
private loadSelection(more=false): void {
|
|
|
const accept=(result:unknown,total:unknown)=>{const incoming=this.findList(result).map(item=>this.meta.path==='pages/member/starshop'?this.toNewWord(item):this.toWord(item)).filter(w=>w.word);this.words.set(more?[...this.words(),...incoming]:incoming);this.selectionTotal.set(Number(total)||this.words().length);};
|
|
const accept=(result:unknown,total:unknown)=>{const incoming=this.findList(result).map(item=>this.meta.path==='pages/member/starshop'?this.toNewWord(item):this.toWord(item)).filter(w=>w.word);this.words.set(more?[...this.words(),...incoming]:incoming);this.selectionTotal.set(Number(total)||this.words().length);};
|
|
|
const failure=(error:ApiError)=>{if(more){this.selectionPage.update(p=>p-1);this.platform.toast(error.message,'error');}else this.error.set(error.message);};
|
|
const failure=(error:ApiError)=>{if(more){this.selectionPage.update(p=>p-1);this.platform.toast(error.message,'error');}else this.error.set(error.message);};
|
|
@@ -1206,7 +1232,18 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
const owner = Number(this.session.user()?.groupId) === 3 ? this.session.student() : this.session.user();
|
|
const owner = Number(this.session.user()?.groupId) === 3 ? this.session.student() : this.session.user();
|
|
|
this.selectionLoading.set(true);
|
|
this.selectionLoading.set(true);
|
|
|
- this.selectionRequest=this.api.get<unknown>('e_words_list',{uid:owner?.userId,cpage:this.selectionPage(),psize:100}).pipe(finalize(()=>{this.selectionLoading.set(false);this.loading.set(false);})).subscribe({next:({result,page})=>accept(result,page?.itemCount),error:failure});
|
|
|
|
|
|
|
+ this.selectionRequest=this.api.get<unknown>('e_words_list',{uid:owner?.userId,cpage:this.selectionPage(),psize:this.newWordPageSize,learningStatus:this.activeSelectionBucket()}).pipe(finalize(()=>{this.selectionLoading.set(false);this.loading.set(false);})).subscribe({
|
|
|
|
|
+ next: ({result,page}) => {
|
|
|
|
|
+ const incoming = this.findList(result).map((item) => this.toNewWord(item)).filter((word) => word.word);
|
|
|
|
|
+ this.words.set(incoming);
|
|
|
|
|
+ for (const word of incoming) this.newWordSelectionCache.set(word.id, word);
|
|
|
|
|
+ const counts = {pending: Number(page?.['pendingCount'] ?? 0), completed: Number(page?.['completedCount'] ?? 0)};
|
|
|
|
|
+ this.newWordCounts.set(counts);
|
|
|
|
|
+ this.selectionTotal.set(Number(page?.itemCount ?? counts[this.activeSelectionBucket()]));
|
|
|
|
|
+ this.error.set('');
|
|
|
|
|
+ },
|
|
|
|
|
+ error: failure,
|
|
|
|
|
+ });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private loadStageWords(): void {
|
|
private loadStageWords(): void {
|
|
@@ -1938,6 +1975,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.calendarLoading.set(false);
|
|
this.calendarLoading.set(false);
|
|
|
this.calendarError.set('');
|
|
this.calendarError.set('');
|
|
|
this.selected.set(new Set());
|
|
this.selected.set(new Set());
|
|
|
|
|
+ this.newWordSelectionCache.clear();
|
|
|
|
|
+ this.newWordCounts.set({ pending: 0, completed: 0 });
|
|
|
this.selectionRevealed.set(new Set());
|
|
this.selectionRevealed.set(new Set());
|
|
|
this.selectionAssessment.set({});
|
|
this.selectionAssessment.set({});
|
|
|
this.activeSelectionBucket.set('pending');
|
|
this.activeSelectionBucket.set('pending');
|