Ver código fonte

Show reading history article count

彭峰 3 semanas atrás
pai
commit
84cc6b6566

+ 13 - 2
projects/xiaoshu-mobile/src/app/features/english-reading/english-reading.service.ts

@@ -27,14 +27,25 @@ export class EnglishReadingService {
     private readonly ai: FmodeAiService,
   ) {}
 
-  async findArticles(wordList: string[], limit = 100): Promise<ReadingArticle[]> {
+  async findArticles(wordList: string[], limit = 100, skip = 0): Promise<ReadingArticle[]> {
     const where: Record<string, unknown> = {
       'createOptions.tpl': ARTICLE_TEMPLATE,
       user: this.cloud.userPointer,
       isDeleted: { '$ne': true },
     };
     if (wordList.length) where['createOptions.params.wordListKey'] = [...new Set(wordList.map(w=>w.trim().toLowerCase()))].sort().join('|');
-    return this.cloud.query<ReadingArticle>('SurveyItem', where, { order: '-createdAt', limit });
+    return this.cloud.query<ReadingArticle>('SurveyItem', where, { order: '-createdAt', limit, skip });
+  }
+
+  async countArticles(wordList: string[]): Promise<number> {
+    const pageSize = 100;
+    let count = 0;
+    while (count < 5000) {
+      const articles = await this.findArticles(wordList, pageSize, count);
+      count += articles.length;
+      if (articles.length < pageSize) return count;
+    }
+    return count;
   }
 
   async duplicateExists(params: ReadingRouteParams): Promise<boolean> {

+ 2 - 2
projects/xiaoshu-mobile/src/app/features/english-reading/reading-setup.component.html

@@ -2,7 +2,7 @@
   <header class="setup-heading">
     <button type="button" class="back-button" aria-label="返回" title="返回" (click)="goBack()"><lucide-icon [img]="icons.ArrowLeft" [size]="24" /></button>
     <div><span class="eyebrow">AI ENGLISH READING</span><h2>生词来读一读</h2><p>用正在学习的词汇生成分级阅读与练习题</p></div>
-    <button type="button" class="history-button" (click)="openHistory()"><lucide-icon [img]="icons.History" [size]="18" />历史文章</button>
+    <button type="button" class="history-button" (click)="openHistory()">历史文章 @if (historyCount() !== null) { <span class="history-count">{{ historyCount() }}</span> }</button>
   </header>
 
   <form (ngSubmit)="start()">
@@ -19,7 +19,7 @@
       <div class="form-grid">
         <label class="field"><span>年级难度</span><select name="level" [(ngModel)]="level" (change)="saveLevel()"><option value="">请选择年级</option>@for (item of levels; track item) { <option [value]="item">{{ item }}</option> }</select></label>
         <label class="field"><span>语篇类型</span><select name="type" [(ngModel)]="type"><option value="随机">随机</option>@for (item of types; track item) { <option [value]="item">{{ item }}</option> }</select></label>
-        <label class="field full"><span>生词列表</span><textarea name="vocabulary" [(ngModel)]="vocabulary" rows="4" placeholder="请输入生词,用逗号或分号分隔,例如 apple, banana, car"></textarea><small>当前 {{ vocabularyCount }} 个词</small></label>
+        <label class="field full"><span>生词列表</span><textarea name="vocabulary" [(ngModel)]="vocabulary" (ngModelChange)="onVocabularyChange()" rows="4" placeholder="请输入生词,用逗号或分号分隔,例如 apple, banana, car"></textarea><small>当前 {{ vocabularyCount }} 个词</small></label>
         <label class="field full"><span>内容题材</span><select name="theme" [(ngModel)]="theme"><option value="随机">随机</option>@for (item of themes; track item) { <option [value]="item">{{ item }}</option> }</select></label>
       </div>
       <button class="primary-button start-button" type="submit" [disabled]="busy() || nameDirty() || !level || !vocabulary"><lucide-icon [img]="busy() ? icons.LoaderCircle : icons.Sparkles" [class.spin]="busy()" [size]="19" />{{ busy() ? '正在检查学习记录' : '开始 AI 生成练读文章' }}</button>

+ 1 - 0
projects/xiaoshu-mobile/src/app/features/english-reading/reading-setup.component.scss

@@ -8,6 +8,7 @@
 .setup-heading p { margin: 0; font-size: 13px; opacity: .82; }
 .eyebrow { font-size: 10px; font-weight: 700; letter-spacing: .08em; opacity: .72; }
 .history-button { display: inline-flex; min-height: 40px; align-items: center; padding: 8px 12px; color: white; background: rgb(255 255 255 / 12%); border: 1px solid rgb(255 255 255 / 35%); border-radius: var(--radius-sm); white-space: nowrap; gap: 7px; }
+.history-count { display: inline-grid; min-width: 20px; height: 20px; padding-inline: 5px; align-items: center; justify-content: center; color: var(--brand-strong); background: white; border-radius: 999px; font-size: 12px; font-weight: 700; }
 .form-section { padding: 22px; border-bottom: 1px solid var(--line); }
 .form-section h3 { margin: 0 0 17px; font-size: 17px; letter-spacing: 0; }
 .form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }

+ 33 - 4
projects/xiaoshu-mobile/src/app/features/english-reading/reading-setup.component.ts

@@ -1,9 +1,9 @@
 import { DropdownDirective } from '../../shared/dropdown.directive';
 import { CommonModule } from '@angular/common';
-import { ChangeDetectionStrategy, Component, inject, OnInit, signal } from '@angular/core';
+import { ChangeDetectionStrategy, Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
 import { FormsModule } from '@angular/forms';
 import { ActivatedRoute, Router } from '@angular/router';
-import { ArrowLeft, BookOpen, ChevronRight, Clock3, History, LoaderCircle, LucideAngularModule, Sparkles, X } from 'lucide-angular';
+import { ArrowLeft, BookOpen, ChevronRight, Clock3, LoaderCircle, LucideAngularModule, Sparkles, X } from 'lucide-angular';
 import { PlatformService } from '../../core/platform.service';
 import { SessionService } from '../../core/session.service';
 import { EnglishReadingService } from './english-reading.service';
@@ -20,17 +20,18 @@ import { normalizeReadingParams, normalizeReadingReturnUrl, READING_LEVELS, READ
   styleUrl: './reading-setup.component.scss',
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
-export class ReadingSetupComponent implements OnInit {
+export class ReadingSetupComponent implements OnInit, OnDestroy {
   readonly levels = READING_LEVELS;
   readonly types = READING_TYPES;
   readonly themes = READING_THEMES;
-  readonly icons = { ArrowLeft, BookOpen, ChevronRight, Clock3, History, LoaderCircle, Sparkles, X };
+  readonly icons = { ArrowLeft, BookOpen, ChevronRight, Clock3, LoaderCircle, Sparkles, X };
   readonly busy = signal(false);
   readonly historyOpen = signal(false);
   readonly historyLoading = signal(false);
   readonly historyLoadingMore = signal(false);
   readonly historyHasMore = signal(true);
   readonly historyItems = signal<ReadingHistoryEntry[]>([]);
+  readonly historyCount = signal<number | null>(null);
   readonly nameDirty = signal(false);
 
   nickname = '';
@@ -41,6 +42,8 @@ export class ReadingSetupComponent implements OnInit {
   theme = '随机';
   private historyPageIndex = 0;
   private readonly historyPageSize = 10;
+  private historyCountTimer?: ReturnType<typeof setTimeout>;
+  private historyCountRequestId = 0;
   private routeParams!: ReadingRouteParams;
 
   private readonly route = inject(ActivatedRoute);
@@ -74,6 +77,31 @@ export class ReadingSetupComponent implements OnInit {
     this.type = params.type || '随机';
     this.theme = params.theme || '随机';
     this.level = params.level || this.level;
+    void this.refreshHistoryCount();
+  }
+
+  ngOnDestroy(): void {
+    clearTimeout(this.historyCountTimer);
+    this.historyCountRequestId += 1;
+  }
+
+  onVocabularyChange(): void {
+    clearTimeout(this.historyCountTimer);
+    this.historyCountRequestId += 1;
+    this.historyCount.set(null);
+    this.historyCountTimer = setTimeout(() => void this.refreshHistoryCount(), 350);
+  }
+
+  private async refreshHistoryCount(): Promise<void> {
+    const requestId = ++this.historyCountRequestId;
+    const words = splitWords(this.vocabulary);
+    try {
+      await this.ensureUser();
+      const count = await this.reading.countArticles(words);
+      if (requestId === this.historyCountRequestId) this.historyCount.set(count);
+    } catch {
+      if (requestId === this.historyCountRequestId) this.historyCount.set(null);
+    }
   }
 
   markNameDirty(): void { this.nameDirty.set(true); }
@@ -143,6 +171,7 @@ export class ReadingSetupComponent implements OnInit {
       this.historyItems.set(items);
       this.historyHasMore.set(items.length === this.historyPageSize);
       this.historyPageIndex = 1;
+      void this.refreshHistoryCount();
     } catch (error) { this.showError(error); }
     finally { this.historyLoading.set(false); }
   }