Просмотр исходного кода

Handle legacy wordbook responses in paginated view

彭峰 1 день назад
Родитель
Сommit
3e310dd9e2

+ 38 - 0
projects/xiaoshu-mobile/src/app/core/home-learning-modules.integration.spec.ts

@@ -519,6 +519,44 @@ describe('home learning modules', () => {
     expect(view().querySelector('.selection-header strong')?.textContent).toContain('已选 1 个单词');
     expect(view().querySelector('.selection-header strong')?.textContent).toContain('已选 1 个单词');
   });
   });
 
 
+  it('keeps wordbook counts and rows correct when the server only returns the old mixed list', async () => {
+    const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
+    const get = api.get.bind(api);
+    const all = Array.from({ length: 139 }, (_, index) => ({
+      GeneralID: 601 + index,
+      xxcs: index < 45 ? 1 : 0,
+      detail: [{ GeneralID: 701 + index, Title: `word-${index + 1}` }],
+    }));
+    const requests: Record<string, unknown>[] = [];
+    spyOn(api, 'get').and.callFake((action, params) => {
+      if (action !== 'e_words_list') return get(action, params);
+      requests.push(params ?? {});
+      const page = Number(params?.['cpage']);
+      const size = Number(params?.['psize']);
+      return of({ retcode: 1, result: all.slice((page - 1) * size, page * size),
+        page: { itemCount: all.length, pageCount: Math.ceil(all.length / size), pageIndex: page, pageSize: size } } as ApiEnvelope<any>);
+    });
+    const harness = await RouterTestingHarness.create();
+    await harness.navigateByUrl('/pages/member/starshop');
+    harness.fixture.detectChanges();
+    const view = () => harness.routeNativeElement!;
+    expect(requests.length).toBe(2);
+    expect(requests[0]).toEqual(jasmine.objectContaining({ learningStatus: 'pending', psize: 30 }));
+    expect(requests[1]).toEqual(jasmine.objectContaining({ cpage: 1, psize: 500 }));
+    expect(view().querySelectorAll('.word-selection-tabs b')[0].textContent).toContain('94');
+    expect(view().querySelectorAll('.word-selection-tabs b')[1].textContent).toContain('45');
+    expect(view().querySelectorAll('.pending-column .word-select-list > button').length).toBe(30);
+    expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 1 / 4 页 · 共 94 个单词');
+    (view().querySelectorAll('.mobile-pagination button')[1] as HTMLButtonElement).click();
+    harness.fixture.detectChanges();
+    expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 2 / 4 页');
+    (view().querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
+    harness.fixture.detectChanges();
+    expect(view().querySelectorAll('.completed-column .word-select-list > button').length).toBe(30);
+    expect(view().querySelector('.mobile-pagination')?.textContent).toContain('第 1 / 2 页 · 共 45 个单词');
+    expect(requests.length).toBe(2);
+  });
+
   it('saves completed wordbook study before showing the word in the learned tab', async () => {
   it('saves completed wordbook study before showing the word in the learned tab', async () => {
     const prefix = 'xiaoshu.learning.100.100.practice.';
     const prefix = 'xiaoshu.learning.100.100.practice.';
     const word = { GeneralID: 701, Title: 'tend', sy: '照顾' };
     const word = { GeneralID: 701, Title: 'tend', sy: '照顾' };

+ 67 - 4
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.ts

@@ -126,6 +126,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   readonly newWordCounts = signal({ pending: 0, completed: 0 });
   readonly newWordCounts = signal({ pending: 0, completed: 0 });
   private readonly newWordPageSize = 30;
   private readonly newWordPageSize = 30;
   private readonly newWordSelectionCache = new Map<string, Word>();
   private readonly newWordSelectionCache = new Map<string, Word>();
+  private legacyNewWords: Word[] | null = null;
   readonly currentIndex = signal(0);
   readonly currentIndex = signal(0);
   readonly revealed = signal(false);
   readonly revealed = signal(false);
   readonly completed = signal(false);
   readonly completed = signal(false);
@@ -1198,15 +1199,60 @@ export class LearningPageComponent implements OnInit, OnDestroy {
     this.selectionPage.set(1);
     this.selectionPage.set(1);
     this.selectionTotal.set(this.newWordCounts()[bucket]);
     this.selectionTotal.set(this.newWordCounts()[bucket]);
     this.words.set([]);
     this.words.set([]);
+    if (this.legacyNewWords) { this.showLegacyNewWordPage(); return; }
     this.loadSelection();
     this.loadSelection();
   }
   }
 
 
   goToNewWordPage(page: number): void {
   goToNewWordPage(page: number): void {
     if (this.selectionLoading() || page < 1 || page > this.newWordPageCount() || page === this.selectionPage()) return;
     if (this.selectionLoading() || page < 1 || page > this.newWordPageCount() || page === this.selectionPage()) return;
     this.selectionPage.set(page);
     this.selectionPage.set(page);
+    if (this.legacyNewWords) { this.showLegacyNewWordPage(); return; }
     this.loadSelection();
     this.loadSelection();
   }
   }
 
 
+  private showLegacyNewWordPage(): void {
+    const all = this.legacyNewWords ?? [];
+    const pending = all.filter((word) => !this.newWordLearned(word));
+    const completed = all.filter((word) => this.newWordLearned(word));
+    const bucket = this.activeSelectionBucket() === 'completed' ? completed : pending;
+    const start = (this.selectionPage() - 1) * this.newWordPageSize;
+    this.newWordCounts.set({ pending: pending.length, completed: completed.length });
+    this.selectionTotal.set(bucket.length);
+    this.words.set(bucket.slice(start, start + this.newWordPageSize));
+    for (const word of all) this.newWordSelectionCache.set(word.id, word);
+    this.error.set('');
+    this.selectionLoading.set(false);
+    this.loading.set(false);
+  }
+
+  private loadLegacyNewWords(uid: number | undefined, page = 1, collected: Word[] = []): void {
+    const size = 500;
+    this.selectionRequest = this.api.get<unknown>('e_words_list', { uid, cpage: page, psize: size }).subscribe({
+      next: ({ result, page: pageInfo }) => {
+        const incoming = this.findList(result).map((item) => this.toNewWord(item)).filter((word) => word.word);
+        const all = [...collected, ...incoming];
+        const total = Number(pageInfo?.itemCount ?? all.length);
+        if (!Number.isFinite(total) || (!incoming.length && all.length < total)) {
+          this.error.set('生词本数据未加载完整,请重试');
+          this.selectionLoading.set(false);
+          this.loading.set(false);
+          return;
+        }
+        if (all.length < total) {
+          this.loadLegacyNewWords(uid, page + 1, all);
+          return;
+        }
+        this.legacyNewWords = all;
+        this.showLegacyNewWordPage();
+      },
+      error: (error: ApiError) => {
+        this.error.set(error.message || '生词本加载失败,请重试');
+        this.selectionLoading.set(false);
+        this.loading.set(false);
+      },
+    });
+  }
+
   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);};
@@ -1232,17 +1278,33 @@ 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:this.newWordPageSize,learningStatus:this.activeSelectionBucket()}).pipe(finalize(()=>{this.selectionLoading.set(false);this.loading.set(false);})).subscribe({
+    this.selectionRequest=this.api.get<unknown>('e_words_list',{uid:owner?.userId,cpage:this.selectionPage(),psize:this.newWordPageSize,learningStatus:this.activeSelectionBucket()}).subscribe({
       next: ({result,page}) => {
       next: ({result,page}) => {
         const incoming = this.findList(result).map((item) => this.toNewWord(item)).filter((word) => word.word);
         const incoming = this.findList(result).map((item) => this.toNewWord(item)).filter((word) => word.word);
+        const pendingCount = Number(page?.['pendingCount']);
+        const completedCount = Number(page?.['completedCount']);
+        const total = Number(page?.itemCount ?? incoming.length);
+        const validCounts = Number.isFinite(pendingCount) && Number.isFinite(completedCount)
+          && pendingCount >= 0 && completedCount >= 0 && pendingCount + completedCount >= total
+          && (incoming.length > 0 || total === 0 || this.selectionPage() > Math.ceil(total / this.newWordPageSize));
+        if (!validCounts) {
+          this.loadLegacyNewWords(owner?.userId);
+          return;
+        }
         this.words.set(incoming);
         this.words.set(incoming);
         for (const word of incoming) this.newWordSelectionCache.set(word.id, word);
         for (const word of incoming) this.newWordSelectionCache.set(word.id, word);
-        const counts = {pending: Number(page?.['pendingCount'] ?? 0), completed: Number(page?.['completedCount'] ?? 0)};
+        const counts = {pending: pendingCount, completed: completedCount};
         this.newWordCounts.set(counts);
         this.newWordCounts.set(counts);
-        this.selectionTotal.set(Number(page?.itemCount ?? counts[this.activeSelectionBucket()]));
+        this.selectionTotal.set(total);
         this.error.set('');
         this.error.set('');
+        this.selectionLoading.set(false);
+        this.loading.set(false);
+      },
+      error: (error: ApiError) => {
+        failure(error);
+        this.selectionLoading.set(false);
+        this.loading.set(false);
       },
       },
-      error: failure,
     });
     });
   }
   }
 
 
@@ -1977,6 +2039,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
     this.selected.set(new Set());
     this.selected.set(new Set());
     this.newWordSelectionCache.clear();
     this.newWordSelectionCache.clear();
     this.newWordCounts.set({ pending: 0, completed: 0 });
     this.newWordCounts.set({ pending: 0, completed: 0 });
+    this.legacyNewWords = null;
     this.selectionRevealed.set(new Set());
     this.selectionRevealed.set(new Set());
     this.selectionAssessment.set({});
     this.selectionAssessment.set({});
     this.activeSelectionBucket.set('pending');
     this.activeSelectionBucket.set('pending');