|
|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
|
|
|
import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
|
-import { AlertCircle, BarChart3, BookOpen, CalendarDays, ChevronRight, GraduationCap, LoaderCircle, LucideAngularModule } from 'lucide-angular';
|
|
|
+import { AlertCircle, BarChart3, BookOpen, CalendarDays, ChevronRight, GraduationCap, LoaderCircle, LucideAngularModule, Search, X } from 'lucide-angular';
|
|
|
import { finalize } from 'rxjs';
|
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
|
import { canStartTeacherLedLearning } from '../../core/learning-access';
|
|
|
@@ -31,6 +31,12 @@ interface StudentOption {
|
|
|
count: number;
|
|
|
}
|
|
|
|
|
|
+interface MonthOption {
|
|
|
+ value: string;
|
|
|
+ label: string;
|
|
|
+ count: number;
|
|
|
+}
|
|
|
+
|
|
|
interface CompanionOverview {
|
|
|
courses: Item[];
|
|
|
schedules: Item[];
|
|
|
@@ -58,14 +64,18 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
readonly scheduleRange = signal<ScheduleRange>('day');
|
|
|
readonly selectedDate = signal(this.today());
|
|
|
readonly selectedStudent = signal('all');
|
|
|
+ readonly historyMonth = signal('all');
|
|
|
+ readonly historyStudentQuery = signal('');
|
|
|
readonly calendarMonth = signal(this.firstOfMonth(new Date()));
|
|
|
readonly now = signal(Date.now());
|
|
|
readonly loading = signal(false);
|
|
|
readonly error = signal('');
|
|
|
readonly studentOptions = computed(() => this.buildStudentOptions());
|
|
|
+ readonly historyMonthOptions = computed(() => this.buildHistoryMonthOptions());
|
|
|
+ readonly historyFilterActive = computed(() => this.historyMonth() !== 'all' || Boolean(this.historyStudentQuery().trim()));
|
|
|
readonly scopedSchedules = computed(() => this.schedules().filter((item) => this.matchesStudent(item)));
|
|
|
readonly filteredSchedulePool = computed(() => this.scopedSchedules().filter((item) => this.matchesStatus(item)));
|
|
|
- readonly visibleSchedules = computed(() => this.filteredSchedulePool().filter((item) => this.matchesRange(item)));
|
|
|
+ readonly visibleSchedules = computed(() => this.filteredSchedulePool().filter((item) => this.matchesRange(item) && this.matchesHistoryFilters(item)));
|
|
|
readonly pendingCount = computed(() => this.scopedSchedules().filter((item) => this.isOverdueUnfinished(item)).length);
|
|
|
readonly finishedCount = computed(() => this.scopedSchedules().filter((item) => this.isFinished(item)).length);
|
|
|
readonly calendarDays = computed(() => this.buildCalendarDays());
|
|
|
@@ -75,7 +85,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
if (this.scheduleRange() === 'week') return `${this.selectedDate()} 所在周`;
|
|
|
return this.selectedDate();
|
|
|
});
|
|
|
- readonly icons = { AlertCircle, BarChart3, BookOpen, CalendarDays, ChevronRight, GraduationCap, LoaderCircle };
|
|
|
+ readonly icons = { AlertCircle, BarChart3, BookOpen, CalendarDays, ChevronRight, GraduationCap, LoaderCircle, Search, X };
|
|
|
|
|
|
private readonly api = inject(ApiService);
|
|
|
private readonly router = inject(Router);
|
|
|
@@ -87,6 +97,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
private loadedRange: { start: string; end: string } | null = null;
|
|
|
private loadedAll = false;
|
|
|
private initialScheduleLoaded = false;
|
|
|
+ private historyFilterInitialized = false;
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
this.restoredScheduleState = this.restoreScheduleState();
|
|
|
@@ -117,6 +128,10 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
next: ({ result }) => {
|
|
|
this.courses.set(Array.isArray(result?.courses) ? result.courses : []);
|
|
|
this.schedules.set(Array.isArray(result?.schedules) ? result.schedules : []);
|
|
|
+ if (this.isHistory && !this.historyFilterInitialized) {
|
|
|
+ this.historyMonth.set(this.historyMonthOptions()[0]?.value ?? 'all');
|
|
|
+ this.historyFilterInitialized = true;
|
|
|
+ }
|
|
|
this.loadedRange = range;
|
|
|
this.loadedAll = !range;
|
|
|
if (!this.initialScheduleLoaded && !this.isHistory && !this.restoredScheduleState) this.selectDate(today);
|
|
|
@@ -213,6 +228,15 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
this.syncScheduleState();
|
|
|
}
|
|
|
|
|
|
+ setHistoryMonth(value: string): void { this.historyMonth.set(value || 'all'); }
|
|
|
+
|
|
|
+ setHistoryStudentQuery(value: string): void { this.historyStudentQuery.set(value); }
|
|
|
+
|
|
|
+ clearHistoryFilters(): void {
|
|
|
+ this.historyMonth.set('all');
|
|
|
+ this.historyStudentQuery.set('');
|
|
|
+ }
|
|
|
+
|
|
|
shiftCalendarMonth(amount: number): void {
|
|
|
const month = this.calendarMonth();
|
|
|
this.calendarMonth.set(new Date(month.getFullYear(), month.getMonth() + amount, 1, 12));
|
|
|
@@ -303,6 +327,22 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
return [...options.values()].sort((left, right) => left.name.localeCompare(right.name, 'zh-CN'));
|
|
|
}
|
|
|
|
|
|
+ private buildHistoryMonthOptions(): MonthOption[] {
|
|
|
+ if (!this.isHistory) return [];
|
|
|
+ const counts = new Map<string, number>();
|
|
|
+ for (const item of this.schedules()) {
|
|
|
+ const month = this.itemDate(item).slice(0, 7);
|
|
|
+ if (!/^\d{4}-\d{2}$/.test(month)) continue;
|
|
|
+ counts.set(month, (counts.get(month) ?? 0) + 1);
|
|
|
+ }
|
|
|
+ return [...counts.entries()]
|
|
|
+ .sort(([left], [right]) => right.localeCompare(left))
|
|
|
+ .map(([value, count]) => {
|
|
|
+ const [year, month] = value.split('-').map(Number);
|
|
|
+ return { value, count, label: `${year}年${month}月` };
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
private assignedUnits(item: Item): Array<{ id: string; name: string }> {
|
|
|
const raw = item['syjd'] ?? item['assignedUnits'];
|
|
|
let source: unknown = raw;
|
|
|
@@ -387,6 +427,14 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
return date === this.selectedDate();
|
|
|
}
|
|
|
|
|
|
+ private matchesHistoryFilters(item: Item): boolean {
|
|
|
+ if (!this.isHistory) return true;
|
|
|
+ const selectedMonth = this.historyMonth();
|
|
|
+ if (selectedMonth !== 'all' && !this.itemDate(item).startsWith(`${selectedMonth}-`)) return false;
|
|
|
+ const query = this.historyStudentQuery().trim().toLocaleLowerCase('zh-CN');
|
|
|
+ return !query || this.studentName(item).toLocaleLowerCase('zh-CN').includes(query);
|
|
|
+ }
|
|
|
+
|
|
|
private sortedVisibleSchedules(): Item[] {
|
|
|
const list = [...this.visibleSchedules()];
|
|
|
if (this.isHistory) return list;
|