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

feat(mobile): filter coach memory tasks by student

彭峰 1 месяц назад
Родитель
Сommit
32383d15da

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

@@ -172,6 +172,9 @@ class LearningModulesApiService {
         { GeneralID: 100, Title: '张雅茜@历史任务', kc_title: '历史复习', kywsj: '2026-05-15 09:00', learned_at: '2026-05-01T01:15:00.000Z', fxzt: 1, yhid: 100, xxjlid: 900 },
         { GeneralID: 101, Title: '张雅茜@任务', kc_title: '初中英语复习', kywsj: '2026-08-01 09:00', learned_at: '2026-07-20T02:20:00.000Z', fxzt: 1, yhid: 100, xxjlid: 900 },
         { GeneralID: 102, Title: '张雅茜@任务', kc_title: '高中英语复习', kywsj: '2026-08-02 09:00', learned_at: '2026-07-20T02:20:00.000Z', fxzt: 0, yhid: 100, xxjlid: 900 },
+        ...(Number(params['shouru']) === 1
+          ? [{ GeneralID: 103, Title: '李同学@任务', kc_title: '小学英语复习', kywsj: '2026-08-02 10:00', learned_at: '2026-07-21T02:20:00.000Z', fxzt: 0, yhid: 101, xxjlid: 901 }]
+          : []),
       ]);
     }
     if (action === 'e_record_detail') {
@@ -292,6 +295,7 @@ describe('home learning modules', () => {
 
     await harness.navigateByUrl('/pages/stu/stu_calendar');
     expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();
+    expect(harness.routeNativeElement?.querySelector('.memory-student-filter')).toBeNull();
     expect(harness.routeNativeElement?.querySelectorAll('.memory-date').length).toBe(42);
     expect(harness.routeNativeElement?.querySelectorAll('.memory-date.completed, .memory-date.due').length).toBe(2);
     expect(harness.routeNativeElement?.querySelector('.memory-overview')?.textContent).toContain('2 / 3');
@@ -521,6 +525,14 @@ describe('home learning modules', () => {
     expect(harness.routeNativeElement?.querySelector('.memory-copy')?.textContent).toContain('关联学员抗遗忘任务');
     expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('张雅茜');
     expect(harness.routeNativeElement?.querySelector('.memory-date-tasks button')?.textContent).toContain('带学生复习');
+    const memoryStudentFilter = harness.routeNativeElement?.querySelector('[aria-label="筛选抗遗忘学员"] select') as HTMLSelectElement;
+    expect(memoryStudentFilter.options.length).toBe(3);
+    memoryStudentFilter.value = '100';
+    memoryStudentFilter.dispatchEvent(new Event('change'));
+    harness.fixture.detectChanges();
+    expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).toContain('张雅茜');
+    expect(harness.routeNativeElement?.querySelector('.memory-date-tasks')?.textContent).not.toContain('李同学');
+    expect(harness.routeNativeElement?.querySelector('.memory-stats')?.textContent).toContain('2 / 3');
 
     await harness.navigateByUrl('/pages/member/ke_xiao');
     await harness.fixture.whenStable();

+ 8 - 1
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.html

@@ -342,12 +342,19 @@
         </div>
       </figure>
       <div class="memory-stats">
-        <span><strong>{{ memoryCompletedCount() }} / {{ calendarItems().length }}</strong><small>任务完成</small></span>
+        <span><strong>{{ memoryCompletedCount() }} / {{ memoryFilteredItems().length }}</strong><small>任务完成</small></span>
         <span><strong>{{ memoryDueCount() }}</strong><small>到期待复习</small></span>
         <span><strong>{{ memoryCalendar().scheduledDays }}</strong><small>本月安排天数</small></span>
       </div>
     </section>
 
+    @if (canStartPointReading) {
+      <section class="memory-student-filter" aria-label="筛选抗遗忘学员">
+        <div><span>当前查看</span><strong>{{ memorySelectedStudentName() }}</strong></div>
+        <label><span>学员</span><select aria-label="筛选抗遗忘学员" [value]="memoryStudentId()" (change)="selectMemoryStudent($event)"><option value="">全部学员({{ memoryStudents().length }}人)</option>@for (student of memoryStudents(); track student.id) { <option [value]="student.id">{{ student.name }}({{ student.taskCount }}项)</option> }</select></label>
+      </section>
+    }
+
     <section class="memory-calendar" aria-label="复习日历">
       <header>
         <div><span>复习日历</span><strong>{{ memoryCalendar().monthLabel }}</strong></div>

+ 39 - 7
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.ts

@@ -80,6 +80,12 @@ interface ExamAnswer {
   result: 0 | 1 | 2;
 }
 
+interface MemoryStudentOption {
+  id: string;
+  name: string;
+  taskCount: number;
+}
+
 const WORD_CATALOG_CATEGORIES = [
   { label: '小学', pid: 8 },
   { label: '初中', pid: 9 },
@@ -133,6 +139,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   readonly seen = signal<Set<string>>(new Set());
   readonly recordRevealed = signal<Set<string>>(new Set());
   readonly calendarItems = signal<SourceItem[]>([]);
+  readonly memoryStudentId = signal('');
   readonly memoryMonth = signal(new Date(new Date().getFullYear(), new Date().getMonth(), 1, 12));
   readonly memorySelectedDate = signal('');
   readonly orderInfo = signal<SourceItem>({});
@@ -208,24 +215,40 @@ export class LearningPageComponent implements OnInit, OnDestroy {
     ? ((this.wordPreviewPage() - 1) * this.wordPreviewPageSize) + 1
     : 0);
   readonly wordPreviewRangeEnd = computed(() => Math.min(this.wordPreviewPage() * this.wordPreviewPageSize, this.wordPreviewTotal()));
-  readonly memoryView = computed(() => buildAntiForgetView(this.calendarItems()));
+  readonly memoryStudents = computed(() => {
+    const students = new Map<string, MemoryStudentOption>();
+    for (const item of this.calendarItems()) {
+      const id = String(item['yhid'] ?? '').trim();
+      if (!id) continue;
+      const current = students.get(id);
+      if (current) current.taskCount += 1;
+      else students.set(id, { id, name: this.calendarStudent(item) || `学员 ${id}`, taskCount: 1 });
+    }
+    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 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.calendarItems(),
+    this.memoryFilteredItems(),
     this.memoryMonth(),
     this.memorySelectedDate(),
   ));
-  readonly memorySelectedTasks = computed(() => antiForgetTasksOnDate(this.calendarItems(), this.memorySelectedDate()));
+  readonly memorySelectedTasks = computed(() => antiForgetTasksOnDate(this.memoryFilteredItems(), this.memorySelectedDate()));
   readonly memorySelectedLabel = computed(() => {
     const value = this.memorySelectedDate();
     if (!value) return '';
     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.calendarItems().filter(antiForgetCompleted).length);
-  readonly memoryDueCount = computed(() => this.calendarItems().filter((item) => !antiForgetCompleted(item) && antiForgetDate(item) <= this.dateOnly()).length);
-  readonly memoryProgress = computed(() => this.calendarItems().length
-    ? Math.round((this.memoryCompletedCount() / this.calendarItems().length) * 100)
+  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 progress = computed(() => {
     const total = this.isMatchingPage ? this.words().length : this.words().length || this.questions().length;
@@ -912,6 +935,14 @@ export class LearningPageComponent implements OnInit, OnDestroy {
     this.memorySelectedDate.set(today);
   }
 
+  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));
+  }
+
   memoryTaskStatus(item: SourceItem): string {
     if (this.calendarCompleted(item)) return '已完成';
     return this.calendarReady(item) ? '待复习' : '未到期';
@@ -1734,6 +1765,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
     this.seen.set(new Set());
     this.recordRevealed.set(new Set());
     this.calendarItems.set([]);
+    this.memoryStudentId.set('');
     this.examStarted.set(false);
     this.examSubmitted.set(false);
   }

+ 23 - 0
projects/xiaoshu-mobile/src/styles.scss

@@ -399,6 +399,29 @@ app-learning-page .node-list > button.selected .node-check { background: var(--b
 
 app-learning-page .node-list .node-copy { flex: 1; }
 
+app-learning-page .memory-student-filter {
+  display: grid;
+  align-items: end;
+  margin: var(--panel-gap) var(--page-gutter) 0;
+  padding: 14px;
+  background: var(--surface);
+  border: 1px solid var(--line);
+  border-radius: var(--panel-radius);
+  box-shadow: var(--shadow-sm);
+  grid-template-columns: minmax(0, 1fr) minmax(220px, .8fr);
+  gap: 14px;
+}
+
+app-learning-page .memory-student-filter > div,
+app-learning-page .memory-student-filter label { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
+app-learning-page .memory-student-filter span { color: var(--ink-soft); font-size: 11px; }
+app-learning-page .memory-student-filter strong { color: var(--ink); font-size: 18px; }
+app-learning-page .memory-student-filter select { min-height: 42px; padding-block: 7px; }
+
+@media (max-width: 520px) {
+  app-learning-page .memory-student-filter { grid-template-columns: 1fr; gap: 10px; }
+}
+
 app-learning-page .node-list .unit-progress-ring {
   display: grid;
   width: 44px;