|
|
@@ -1,3 +1,5 @@
|
|
|
+import { LearningDraft } from '../../core/learning-draft';
|
|
|
+import { ReviewPage, reviewCalendar } from '../../core/review-page';
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
Volume2,
|
|
|
X,
|
|
|
} from 'lucide-angular';
|
|
|
-import { catchError, finalize, forkJoin, Observable, of, switchMap, throwError } from 'rxjs';
|
|
|
+import { catchError, finalize, forkJoin, Observable, of, Subscription, switchMap, throwError } from 'rxjs';
|
|
|
import { GAME_AUDIO } from '../../core/app.constants';
|
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
|
import {
|
|
|
@@ -124,6 +126,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
readonly unknown = signal<Word[]>([]);
|
|
|
readonly loading = signal(false);
|
|
|
readonly submitting = signal(false);
|
|
|
+ readonly selectionTotal=signal(0);readonly selectionPage=signal(1);readonly selectionLoading=signal(false);private selectionRequest?:Subscription;
|
|
|
readonly error = signal('');
|
|
|
|
|
|
readonly studyStep = signal<1 | 2 | 3>(1);
|
|
|
@@ -216,6 +219,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
: 0);
|
|
|
readonly wordPreviewRangeEnd = computed(() => Math.min(this.wordPreviewPage() * this.wordPreviewPageSize, this.wordPreviewTotal()));
|
|
|
readonly memoryStudents = computed(() => {
|
|
|
+ if(this.reviewPageData()?.students)return this.reviewPageData()!.students!.map(s=>({id:String(s.id),name:s.name,taskCount:s.taskCount}));
|
|
|
const students = new Map<string, MemoryStudentOption>();
|
|
|
for (const item of this.calendarItems()) {
|
|
|
const id = String(item['yhid'] ?? '').trim();
|
|
|
@@ -227,17 +231,14 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
return [...students.values()].sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'));
|
|
|
});
|
|
|
readonly memorySelectedStudentName = computed(() => this.memoryStudents().find((student) => student.id === this.memoryStudentId())?.name || '全部学员');
|
|
|
+ readonly reviewPageData=signal<ReviewPage|null>(null); readonly reviewListPage=signal(1);
|
|
|
readonly memoryFilteredItems = computed(() => {
|
|
|
const studentId = this.memoryStudentId();
|
|
|
return studentId ? this.calendarItems().filter((item) => String(item['yhid'] ?? '') === studentId) : this.calendarItems();
|
|
|
});
|
|
|
readonly memoryView = computed(() => buildAntiForgetView(this.memoryFilteredItems()));
|
|
|
readonly memorySummary = computed(() => this.memoryView().summary);
|
|
|
- readonly memoryCalendar = computed(() => buildAntiForgetCalendar(
|
|
|
- this.memoryFilteredItems(),
|
|
|
- this.memoryMonth(),
|
|
|
- this.memorySelectedDate(),
|
|
|
- ));
|
|
|
+ readonly memoryCalendar=computed(()=>reviewCalendar(this.reviewPageData()?.dates||[],this.memoryMonth(),this.memorySelectedDate()));
|
|
|
readonly memorySelectedTasks = computed(() => antiForgetTasksOnDate(this.memoryFilteredItems(), this.memorySelectedDate()));
|
|
|
readonly memorySelectedLabel = computed(() => {
|
|
|
const value = this.memorySelectedDate();
|
|
|
@@ -245,11 +246,9 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
const [year, month, day] = value.split('-').map(Number);
|
|
|
return new Intl.DateTimeFormat('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' }).format(new Date(year, month - 1, day, 12));
|
|
|
});
|
|
|
- readonly memoryCompletedCount = computed(() => this.memoryFilteredItems().filter(antiForgetCompleted).length);
|
|
|
- readonly memoryDueCount = computed(() => this.memoryFilteredItems().filter((item) => !antiForgetCompleted(item) && antiForgetDate(item) <= this.dateOnly()).length);
|
|
|
- readonly memoryProgress = computed(() => this.memoryFilteredItems().length
|
|
|
- ? Math.round((this.memoryCompletedCount() / this.memoryFilteredItems().length) * 100)
|
|
|
- : 0);
|
|
|
+ readonly memoryCompletedCount = computed(() => this.reviewPageData()?.summary.completed||0);
|
|
|
+ readonly memoryDueCount = computed(() => this.reviewPageData()?.summary.due||0);
|
|
|
+ readonly memoryProgress=computed(()=>{const s=this.reviewPageData()?.summary;return s?.total?Math.round(s.completed/s.total*100):0;});
|
|
|
readonly progress = computed(() => {
|
|
|
const total = this.isMatchingPage ? this.words().length : this.words().length || this.questions().length;
|
|
|
if (!total) return 0;
|
|
|
@@ -280,6 +279,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
|
+ this.calendarRequestId++;this.selectionRequest?.unsubscribe();
|
|
|
this.stopExamTimer();
|
|
|
this.platform.stopAudio();
|
|
|
}
|
|
|
@@ -393,6 +393,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
load(): void {
|
|
|
+ this.selectionRequest?.unsubscribe();this.selectionPage.set(1);this.selectionTotal.set(0);
|
|
|
this.resetPageState();
|
|
|
this.loading.set(true);
|
|
|
this.error.set('');
|
|
|
@@ -485,6 +486,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
queryParams: {
|
|
|
nids: [...this.selectedNodes()].join(','),
|
|
|
pnid: this.query('nid'),
|
|
|
+ bindingId: this.query('bindingId'),
|
|
|
orderId: this.query('orderId'),
|
|
|
},
|
|
|
});
|
|
|
@@ -798,7 +800,6 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.submitPracticeEnd();
|
|
|
return;
|
|
|
}
|
|
|
- this.updateUnitRecord();
|
|
|
this.saveLearningRecord();
|
|
|
}
|
|
|
|
|
|
@@ -866,6 +867,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
endRecord(): void {
|
|
|
+ if(this.submitting())return;
|
|
|
const now = this.dateTime();
|
|
|
this.submitting.set(true);
|
|
|
this.api.post('content_update', {
|
|
|
@@ -954,6 +956,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
selectMemoryDay(day: AntiForgetCalendarDay): void {
|
|
|
this.memorySelectedDate.set(day.date);
|
|
|
+ this.reviewListPage.set(1);this.loadCalendar();
|
|
|
if (!day.inCurrentMonth) {
|
|
|
const [year, month] = day.date.split('-').map(Number);
|
|
|
this.memoryMonth.set(new Date(year, month - 1, 1, 12));
|
|
|
@@ -965,8 +968,9 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
const next = new Date(current.getFullYear(), current.getMonth() + amount, 1, 12);
|
|
|
this.memoryMonth.set(next);
|
|
|
const monthKey = `${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}`;
|
|
|
- const firstTask = this.calendarItems().map(antiForgetDate).filter((date) => date.startsWith(monthKey)).sort()[0];
|
|
|
+ const firstTask = (this.reviewPageData()?.dates||[]).map(row=>row.date).filter((date) => date.startsWith(monthKey)).sort()[0];
|
|
|
this.memorySelectedDate.set(firstTask || `${monthKey}-01`);
|
|
|
+ this.reviewListPage.set(1);this.loadCalendar();
|
|
|
}
|
|
|
|
|
|
showCurrentMemoryMonth(): void {
|
|
|
@@ -974,14 +978,11 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
const [year, month] = today.split('-').map(Number);
|
|
|
this.memoryMonth.set(new Date(year, month - 1, 1, 12));
|
|
|
this.memorySelectedDate.set(today);
|
|
|
+ this.reviewListPage.set(1);this.loadCalendar();
|
|
|
}
|
|
|
|
|
|
selectMemoryStudent(event: Event): void {
|
|
|
- this.memoryStudentId.set((event.target as HTMLSelectElement).value);
|
|
|
- const selectedDate = nearestAntiForgetDate(this.memoryFilteredItems());
|
|
|
- const [year, month] = selectedDate.split('-').map(Number);
|
|
|
- this.memorySelectedDate.set(selectedDate);
|
|
|
- this.memoryMonth.set(new Date(year, month - 1, 1, 12));
|
|
|
+ this.memoryStudentId.set((event.target as HTMLSelectElement).value);this.reviewListPage.set(1);this.loadCalendar();
|
|
|
}
|
|
|
|
|
|
memoryTaskStatus(item: SourceItem): string {
|
|
|
@@ -1039,6 +1040,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
submitExam(): void {
|
|
|
+ if(this.submitting())return;
|
|
|
this.stopExamTimer();
|
|
|
const ask = this.askInfo();
|
|
|
const user = this.session.user();
|
|
|
@@ -1113,21 +1115,13 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private loadSelection(): void {
|
|
|
+ loadMoreSelection():void{if(!this.selectionLoading()&&this.words().length<this.selectionTotal()){this.selectionPage.update(p=>p+1);this.loadSelection(true);}}
|
|
|
+ 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 failure=(error:ApiError)=>{if(more){this.selectionPage.update(p=>p-1);this.platform.toast(error.message,'error');}else this.error.set(error.message);};
|
|
|
if (this.meta.path === 'pages/stu/stu_abc') {
|
|
|
- const student = this.session.student() ?? this.session.user();
|
|
|
- this.api.get<unknown>('e_ck_list', {
|
|
|
- checkwords: 2,
|
|
|
- nids: this.query('nids') || this.query('nid'),
|
|
|
- modelId: 52,
|
|
|
- cpage: 1,
|
|
|
- psize: 300,
|
|
|
- uid: student?.userId,
|
|
|
- }).pipe(finalize(() => this.loading.set(false))).subscribe({
|
|
|
- next: ({ result }) => this.words.set(this.findList(result).map((item) => this.toWord(item)).filter((word) => word.word)),
|
|
|
- error: (error: ApiError) => this.error.set(error.message),
|
|
|
- });
|
|
|
- return;
|
|
|
+ const student = this.session.student() ?? this.session.user();this.selectionLoading.set(true);
|
|
|
+ this.selectionRequest=this.api.get<unknown>('e_ck_list',{checkwords:2,nids:this.query('nids')||this.query('nid'),modelId:52,cpage:this.selectionPage(),psize:100,uid:student?.userId}).pipe(finalize(()=>{this.selectionLoading.set(false);this.loading.set(false);})).subscribe({next:({result,page})=>accept(result,page?.itemCount),error:failure});return;
|
|
|
}
|
|
|
|
|
|
if (this.meta.path === 'pages/his/his_words') {
|
|
|
@@ -1145,10 +1139,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
const owner = Number(this.session.user()?.groupId) === 3 ? this.session.student() : this.session.user();
|
|
|
- this.api.get<unknown>('e_words_list', { uid: owner?.userId, cpage: 1, psize: 300 }).pipe(finalize(() => this.loading.set(false))).subscribe({
|
|
|
- next: ({ result }) => this.words.set(this.findList(result).map((item) => this.toNewWord(item)).filter((word) => word.word)),
|
|
|
- error: (error: ApiError) => this.error.set(error.message),
|
|
|
- });
|
|
|
+ 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});
|
|
|
}
|
|
|
|
|
|
private loadStageWords(): void {
|
|
|
@@ -1301,32 +1293,31 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.error.set('请先选择本次学习的单词');
|
|
|
return;
|
|
|
}
|
|
|
- this.updateUnitRecord();
|
|
|
const user = this.session.user();
|
|
|
this.api.get<unknown>('content_list', {
|
|
|
modelId: 56,
|
|
|
psize: 2,
|
|
|
- myfield2: `UserId=${user?.userId}|dqrq=${this.compactDate()}`,
|
|
|
+ myfield2: `UserId=${(this.canStartPointReading ? this.session.student() : user)?.userId}|dqrq=${this.compactDate()}`,
|
|
|
}).subscribe({
|
|
|
next: ({ result }) => this.learnedRecord.set(this.findList(result)[0] ?? {}),
|
|
|
- error: () => undefined,
|
|
|
+ error: (error:ApiError) => this.error.set(error.message),
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private updateUnitRecord(): void {
|
|
|
+ private updateUnitRecord(): Observable<unknown> {
|
|
|
const nids = this.query('nids');
|
|
|
const bookId = this.query('pnid');
|
|
|
- if (!nids || !bookId) return;
|
|
|
+ if (!nids || !bookId) return of(null);
|
|
|
const wordIds: Record<string, string> = {};
|
|
|
for (const nodeId of nids.split(',')) {
|
|
|
wordIds[nodeId] = this.words().filter((word) => word.nodeId === nodeId).map((word) => word.wordId).join(',');
|
|
|
}
|
|
|
- this.api.post('unit_record_update', {
|
|
|
+ return this.api.post('unit_record_update', {
|
|
|
nids,
|
|
|
- userId: this.session.user()?.userId,
|
|
|
+ userId: (this.canStartPointReading ? this.session.student() : this.session.user())?.userId,
|
|
|
bookId,
|
|
|
wordIds: JSON.stringify(wordIds),
|
|
|
- }).subscribe({ error: () => undefined });
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
private saveLearningRecord(): void {
|
|
|
@@ -1371,6 +1362,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
if (!existing['xxqs']) addon['fxrl'] = '';
|
|
|
this.submitting.set(true);
|
|
|
this.persistWordsAsLearned(this.words(), owner).pipe(
|
|
|
+ switchMap(()=>this.updateUnitRecord()),
|
|
|
switchMap(() => this.api.post(action, { content: JSON.stringify(content), addon: JSON.stringify(addon) })),
|
|
|
finalize(() => this.submitting.set(false)),
|
|
|
).subscribe({
|
|
|
@@ -1542,24 +1534,12 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ turnReviewPage(delta:number):void{this.reviewListPage.update(p=>Math.max(1,Math.min(this.reviewPageData()?.page.pageCount||1,p+delta)));this.loadCalendar();}
|
|
|
private loadCalendar(): void {
|
|
|
- const uid = this.session.user()?.userId;
|
|
|
- const coachMode = Number(this.session.groupId()) === 3;
|
|
|
- this.api.post<unknown>('e_get_21list', { uid, shouru: coachMode ? 1 : undefined, cpage: 1, psize: 500 })
|
|
|
- .pipe(finalize(() => this.loading.set(false)))
|
|
|
- .subscribe({
|
|
|
- next: ({ result }) => {
|
|
|
- const items = mergeAntiForgetItems(this.findList(result), []);
|
|
|
- this.calendarItems.set(items);
|
|
|
- const selectedDate = nearestAntiForgetDate(items);
|
|
|
- const [year, month] = selectedDate.split('-').map(Number);
|
|
|
- this.memorySelectedDate.set(selectedDate);
|
|
|
- this.memoryMonth.set(new Date(year, month - 1, 1, 12));
|
|
|
- },
|
|
|
- error: () => this.error.set('抗遗忘数据暂时无法加载,请稍后重试'),
|
|
|
- });
|
|
|
- this.api.post('e_get_21list_tj', { uid }).subscribe({ error: () => undefined });
|
|
|
+ const requestId=++this.calendarRequestId;this.loading.set(true);
|
|
|
+ this.api.post<ReviewPage>('app_review_page',{uid:this.session.user()?.userId,studentId:this.memoryStudentId()||undefined,date:this.memorySelectedDate()||this.dateOnly(),page:this.reviewListPage(),pageSize:20}).pipe(finalize(()=>{if(requestId===this.calendarRequestId)this.loading.set(false);})).subscribe({next:({result})=>{if(requestId!==this.calendarRequestId)return;this.reviewPageData.set(result);this.calendarItems.set(result.items);if(!this.memorySelectedDate())this.memorySelectedDate.set(this.dateOnly());},error:(e:ApiError)=>{if(requestId===this.calendarRequestId)this.error.set(e.message);}});
|
|
|
}
|
|
|
+ private calendarRequestId=0;
|
|
|
|
|
|
private loadAntiForget(): void {
|
|
|
const user = this.session.user();
|
|
|
@@ -1744,13 +1724,14 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
this.storageSet('_page_current_learn', 1);
|
|
|
this.storageSet('_page_total_page', pages.length);
|
|
|
this.storageSet('_page_word_orilist', pages);
|
|
|
- sessionStorage.setItem('xiaoshu.selectedWords', JSON.stringify(pages[0] ?? chosen));
|
|
|
+ sessionStorage.setItem(this.learningStorageKey('selectedWords'), JSON.stringify(pages[0] ?? chosen));
|
|
|
void this.router.navigate(['/pages/stu/stu_con'], {
|
|
|
replaceUrl: true,
|
|
|
state: { learningReturnUrl: this.router.url },
|
|
|
queryParams: {
|
|
|
nids: this.query('nids') || this.query('nid'),
|
|
|
pnid: this.query('pnid'),
|
|
|
+ bindingId: this.query('bindingId'),
|
|
|
orderId: this.query('orderId'),
|
|
|
},
|
|
|
});
|
|
|
@@ -1764,6 +1745,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
nid: this.query('nid'),
|
|
|
nids: this.query('nids'),
|
|
|
pnid: this.query('pnid'),
|
|
|
+ bindingId: this.query('bindingId'),
|
|
|
orderId: this.query('orderId') || this.query('id'),
|
|
|
},
|
|
|
});
|
|
|
@@ -1778,7 +1760,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
const mode = String(this.storageGet('learn_mode') ?? '');
|
|
|
const key = mode === 'newword' ? 'choose_newword_learn' : mode === 'history_word' ? 'choose_hisword_learn' : 'choose_word_learn';
|
|
|
this.storageSet(key, page);
|
|
|
- sessionStorage.setItem('xiaoshu.selectedWords', JSON.stringify(page));
|
|
|
+ sessionStorage.setItem(this.learningStorageKey('selectedWords'), JSON.stringify(page));
|
|
|
return page.map((item) => this.toWord(item as SourceItem));
|
|
|
}
|
|
|
return [];
|
|
|
@@ -1787,7 +1769,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
const key = mode === 'newword' ? 'choose_newword_learn' : mode === 'history_word' ? 'choose_hisword_learn' : 'choose_word_learn';
|
|
|
const stored = this.parseArray(this.storageGet(key));
|
|
|
if (stored.length) return stored.map((item) => this.toWord(item));
|
|
|
- return this.parseArray(sessionStorage.getItem('xiaoshu.selectedWords')).map((item) => this.toWord(item));
|
|
|
+ return this.parseArray(sessionStorage.getItem(this.learningStorageKey('selectedWords'))).map((item) => this.toWord(item));
|
|
|
}
|
|
|
|
|
|
private readAllLearningWords(): Word[] {
|
|
|
@@ -1798,12 +1780,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
return this.readLearningWords();
|
|
|
}
|
|
|
|
|
|
- private clearLearningStorage(): void {
|
|
|
- for (const key of ['learn_mode', 'choose_word_learn', 'choose_newword_learn', 'choose_hisword_learn', '_page_word_orilist', '_page_total_page', '_page_current_learn']) {
|
|
|
- localStorage.removeItem(`xiaoshu.learning.${key}`);
|
|
|
- }
|
|
|
- sessionStorage.removeItem('xiaoshu.selectedWords');
|
|
|
- }
|
|
|
+ private clearLearningStorage(): void { this.draft().clear(); }
|
|
|
|
|
|
private resetPageState(): void {
|
|
|
this.stopExamTimer();
|
|
|
@@ -1917,6 +1894,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
wordsId: words.map((word) => word.wordId).join(','),
|
|
|
kcid: this.query('pnid') || this.query('kcid') || this.orderInfo()['kcid'],
|
|
|
save: 1,
|
|
|
+ requestKey:this.draft().command('learn:'+words.map(w=>w.wordId).sort().join(',')),
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -1959,15 +1937,13 @@ export class LearningPageComponent implements OnInit, OnDestroy {
|
|
|
return copy;
|
|
|
}
|
|
|
|
|
|
- private storageGet(key: string): unknown {
|
|
|
- const raw = localStorage.getItem(`xiaoshu.learning.${key}`);
|
|
|
- if (raw === null) return null;
|
|
|
- try { return JSON.parse(raw) as unknown; } catch { return raw; }
|
|
|
- }
|
|
|
-
|
|
|
- private storageSet(key: string, value: unknown): void {
|
|
|
- localStorage.setItem(`xiaoshu.learning.${key}`, JSON.stringify(value));
|
|
|
+ private draft():LearningDraft {
|
|
|
+ const learner=this.canStartPointReading?this.session.student():this.session.user();
|
|
|
+ return new LearningDraft(`xiaoshu.learning.${this.session.user()?.userId}.${learner?.userId}.${this.query('orderId')||this.query('bindingId')||'practice'}`);
|
|
|
}
|
|
|
+ private learningStorageKey(key:string):string{return this.draft().key(key);}
|
|
|
+ private storageGet(key:string):unknown{return this.draft().get(key);}
|
|
|
+ private storageSet(key:string,value:unknown):void{this.draft().set(key,value);}
|
|
|
|
|
|
private query(name: string): string {
|
|
|
return this.route.snapshot.queryParamMap.get(name) ?? '';
|