|
@@ -2,7 +2,7 @@ import { CommonModule } from '@angular/common';
|
|
|
import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
|
|
import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
|
|
|
import { FormsModule } from '@angular/forms';
|
|
import { FormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
|
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
|
|
-import { AlertCircle, ArrowRight, BookOpen, CalendarDays, CheckCircle2, ChevronLeft, ChevronRight, Clock3, LoaderCircle, LucideAngularModule, RotateCcw, Sparkles } from 'lucide-angular';
|
|
|
|
|
|
|
+import { AlertCircle, ArrowRight, BookOpen, CalendarDays, CheckCircle2, ChevronRight, Clock3, LoaderCircle, LucideAngularModule, RotateCcw, Sparkles } from 'lucide-angular';
|
|
|
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
|
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
import { ApiError, ApiService } from '../../core/api.service';
|
|
|
import { extractReadingWords, readNcloudSessionToken } from '../../core/external-reading';
|
|
import { extractReadingWords, readNcloudSessionToken } from '../../core/external-reading';
|
|
@@ -11,14 +11,6 @@ import { SessionService } from '../../core/session.service';
|
|
|
|
|
|
|
|
type Item = Record<string, unknown>;
|
|
type Item = Record<string, unknown>;
|
|
|
|
|
|
|
|
-interface CalendarDay {
|
|
|
|
|
- date: string;
|
|
|
|
|
- day: number;
|
|
|
|
|
- inMonth: boolean;
|
|
|
|
|
- isToday: boolean;
|
|
|
|
|
- count: number;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-study-records-page',
|
|
selector: 'app-study-records-page',
|
|
|
standalone: true,
|
|
standalone: true,
|
|
@@ -36,50 +28,20 @@ export class StudyRecordsPageComponent implements OnInit {
|
|
|
readonly error = signal('');
|
|
readonly error = signal('');
|
|
|
readonly totalIncome = signal(0);
|
|
readonly totalIncome = signal(0);
|
|
|
readonly activeTab = signal(0);
|
|
readonly activeTab = signal(0);
|
|
|
- readonly calendarMonth = signal(this.firstOfMonth(new Date()));
|
|
|
|
|
- readonly calendarSelectedDate = signal('');
|
|
|
|
|
- readonly visibleItems = computed(() => {
|
|
|
|
|
- const selectedDate = this.calendarSelectedDate();
|
|
|
|
|
- if (this.meta.path !== 'pages/his/his' || !selectedDate) return this.items();
|
|
|
|
|
- return this.items().filter((item) => this.itemDate(item) === selectedDate);
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ readonly visibleItems = computed(() => this.items());
|
|
|
readonly latestReadingRecord = computed(() => this.items().find((item) => extractReadingWords(item['xxqs']).length > 0));
|
|
readonly latestReadingRecord = computed(() => this.items().find((item) => extractReadingWords(item['xxqs']).length > 0));
|
|
|
readonly latestReadingWordCount = computed(() => {
|
|
readonly latestReadingWordCount = computed(() => {
|
|
|
const item = this.latestReadingRecord();
|
|
const item = this.latestReadingRecord();
|
|
|
return item ? extractReadingWords(item['xxqs']).length : 0;
|
|
return item ? extractReadingWords(item['xxqs']).length : 0;
|
|
|
});
|
|
});
|
|
|
- readonly selectedCalendarLabel = computed(() => {
|
|
|
|
|
- const selectedDate = this.calendarSelectedDate();
|
|
|
|
|
|
|
+ readonly selectedCalendarLabel = (): string => {
|
|
|
|
|
+ const selectedDate = this.selectedDate;
|
|
|
if (!selectedDate) return '全部记录';
|
|
if (!selectedDate) return '全部记录';
|
|
|
const [year, month, day] = selectedDate.split('-').map(Number);
|
|
const [year, month, day] = selectedDate.split('-').map(Number);
|
|
|
return new Intl.DateTimeFormat('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })
|
|
return new Intl.DateTimeFormat('zh-CN', { month: 'long', day: 'numeric', weekday: 'short' })
|
|
|
.format(new Date(year, month - 1, day, 12));
|
|
.format(new Date(year, month - 1, day, 12));
|
|
|
- });
|
|
|
|
|
- readonly calendarLabel = computed(() => new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: 'long' }).format(this.calendarMonth()));
|
|
|
|
|
- readonly calendarDays = computed<CalendarDay[]>(() => {
|
|
|
|
|
- const month = this.calendarMonth();
|
|
|
|
|
- const year = month.getFullYear();
|
|
|
|
|
- const monthIndex = month.getMonth();
|
|
|
|
|
- const counts = new Map<string, number>();
|
|
|
|
|
- for (const item of this.items()) {
|
|
|
|
|
- const date = this.itemDate(item);
|
|
|
|
|
- if (date) counts.set(date, (counts.get(date) ?? 0) + 1);
|
|
|
|
|
- }
|
|
|
|
|
- const offset = (new Date(year, monthIndex, 1, 12).getDay() + 6) % 7;
|
|
|
|
|
- const start = new Date(year, monthIndex, 1 - offset, 12);
|
|
|
|
|
- const today = this.dateKey(new Date());
|
|
|
|
|
- return Array.from({ length: 42 }, (_, index) => {
|
|
|
|
|
- const date = new Date(start);
|
|
|
|
|
- date.setDate(start.getDate() + index);
|
|
|
|
|
- const key = this.dateKey(date);
|
|
|
|
|
- return { date: key, day: date.getDate(), inMonth: date.getMonth() === monthIndex, isToday: key === today, count: counts.get(key) ?? 0 };
|
|
|
|
|
- });
|
|
|
|
|
- });
|
|
|
|
|
- readonly calendarSummary = computed(() => {
|
|
|
|
|
- const days = this.calendarDays().filter((day) => day.inMonth && day.count > 0);
|
|
|
|
|
- return { learnedDays: days.length, records: days.reduce((sum, day) => sum + day.count, 0) };
|
|
|
|
|
- });
|
|
|
|
|
- readonly icons = { AlertCircle, ArrowRight, BookOpen, CalendarDays, CheckCircle2, ChevronLeft, ChevronRight, Clock3, LoaderCircle, RotateCcw, Sparkles };
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+ readonly icons = { AlertCircle, ArrowRight, BookOpen, CalendarDays, CheckCircle2, ChevronRight, Clock3, LoaderCircle, RotateCcw, Sparkles };
|
|
|
studentId = 0;
|
|
studentId = 0;
|
|
|
selectedDate = '';
|
|
selectedDate = '';
|
|
|
|
|
|
|
@@ -87,7 +49,6 @@ export class StudyRecordsPageComponent implements OnInit {
|
|
|
private readonly router = inject(Router);
|
|
private readonly router = inject(Router);
|
|
|
private readonly api = inject(ApiService);
|
|
private readonly api = inject(ApiService);
|
|
|
readonly session = inject(SessionService);
|
|
readonly session = inject(SessionService);
|
|
|
- private calendarInitialized = false;
|
|
|
|
|
private syncingHistoryQuery = false;
|
|
private syncingHistoryQuery = false;
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
|
ngOnInit(): void {
|
|
@@ -118,7 +79,6 @@ export class StudyRecordsPageComponent implements OnInit {
|
|
|
next: ({ result }) => {
|
|
next: ({ result }) => {
|
|
|
const list = this.asList(result);
|
|
const list = this.asList(result);
|
|
|
this.items.set(list);
|
|
this.items.set(list);
|
|
|
- if (this.meta.path === 'pages/his/his') this.initializeCalendar(list);
|
|
|
|
|
},
|
|
},
|
|
|
error: (error: ApiError) => this.error.set(error.message),
|
|
error: (error: ApiError) => this.error.set(error.message),
|
|
|
});
|
|
});
|
|
@@ -129,41 +89,14 @@ export class StudyRecordsPageComponent implements OnInit {
|
|
|
onFilterChange(): void {
|
|
onFilterChange(): void {
|
|
|
this.studentId = Number(this.studentId || 0);
|
|
this.studentId = Number(this.studentId || 0);
|
|
|
this.selectedDate = this.dateKey(this.selectedDate);
|
|
this.selectedDate = this.dateKey(this.selectedDate);
|
|
|
- this.calendarInitialized = false;
|
|
|
|
|
- if (this.selectedDate) {
|
|
|
|
|
- this.calendarSelectedDate.set(this.selectedDate);
|
|
|
|
|
- this.calendarMonth.set(this.firstOfMonth(this.parseDate(this.selectedDate)));
|
|
|
|
|
- this.calendarInitialized = true;
|
|
|
|
|
- }
|
|
|
|
|
this.syncHistoryState();
|
|
this.syncHistoryState();
|
|
|
this.load();
|
|
this.load();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- shiftCalendarMonth(amount: number): void {
|
|
|
|
|
- const month = this.calendarMonth();
|
|
|
|
|
- this.calendarMonth.set(new Date(month.getFullYear(), month.getMonth() + amount, 1, 12));
|
|
|
|
|
- this.calendarSelectedDate.set('');
|
|
|
|
|
- this.syncHistoryState();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- showCurrentMonth(): void {
|
|
|
|
|
- this.calendarMonth.set(this.firstOfMonth(new Date()));
|
|
|
|
|
- this.calendarSelectedDate.set('');
|
|
|
|
|
- this.syncHistoryState();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- selectCalendarDay(day: CalendarDay): void {
|
|
|
|
|
- this.calendarSelectedDate.set(day.date);
|
|
|
|
|
- if (!day.inMonth) {
|
|
|
|
|
- const [year, month] = day.date.split('-').map(Number);
|
|
|
|
|
- this.calendarMonth.set(new Date(year, month - 1, 1, 12));
|
|
|
|
|
- }
|
|
|
|
|
- this.syncHistoryState();
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
showAllRecords(): void {
|
|
showAllRecords(): void {
|
|
|
- this.calendarSelectedDate.set('');
|
|
|
|
|
|
|
+ this.selectedDate = '';
|
|
|
this.syncHistoryState();
|
|
this.syncHistoryState();
|
|
|
|
|
+ this.load();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
courseId(item: Item): string {
|
|
courseId(item: Item): string {
|
|
@@ -264,7 +197,6 @@ export class StudyRecordsPageComponent implements OnInit {
|
|
|
const list = this.asList(records.result);
|
|
const list = this.asList(records.result);
|
|
|
this.items.set(list);
|
|
this.items.set(list);
|
|
|
this.courses.set(this.asList(courses.result));
|
|
this.courses.set(this.asList(courses.result));
|
|
|
- this.initializeCalendar(list);
|
|
|
|
|
},
|
|
},
|
|
|
error: (error: ApiError) => this.error.set(error.message),
|
|
error: (error: ApiError) => this.error.set(error.message),
|
|
|
});
|
|
});
|
|
@@ -303,14 +235,7 @@ export class StudyRecordsPageComponent implements OnInit {
|
|
|
if (this.meta.path !== 'pages/his/his') return;
|
|
if (this.meta.path !== 'pages/his/his') return;
|
|
|
const query = this.route.snapshot.queryParamMap;
|
|
const query = this.route.snapshot.queryParamMap;
|
|
|
this.studentId = Number(query.get('uid') ?? 0);
|
|
this.studentId = Number(query.get('uid') ?? 0);
|
|
|
- this.selectedDate = this.dateKey(query.get('date') ?? '');
|
|
|
|
|
- const selectedCalendarDate = this.dateKey(query.get('hisDay') ?? '') || this.selectedDate;
|
|
|
|
|
- const restoredMonth = this.parseMonth(query.get('hisMonth') ?? '');
|
|
|
|
|
-
|
|
|
|
|
- if (selectedCalendarDate) this.calendarSelectedDate.set(selectedCalendarDate);
|
|
|
|
|
- if (restoredMonth) this.calendarMonth.set(restoredMonth);
|
|
|
|
|
- else if (selectedCalendarDate) this.calendarMonth.set(this.firstOfMonth(this.parseDate(selectedCalendarDate)));
|
|
|
|
|
- if (restoredMonth || selectedCalendarDate) this.calendarInitialized = true;
|
|
|
|
|
|
|
+ this.selectedDate = this.dateKey(query.get('date') ?? query.get('hisDay') ?? '');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private syncHistoryState(): void {
|
|
private syncHistoryState(): void {
|
|
@@ -323,29 +248,14 @@ export class StudyRecordsPageComponent implements OnInit {
|
|
|
uid: teacherStudentId,
|
|
uid: teacherStudentId,
|
|
|
name: null,
|
|
name: null,
|
|
|
date: this.selectedDate || null,
|
|
date: this.selectedDate || null,
|
|
|
- hisDay: this.calendarSelectedDate() || null,
|
|
|
|
|
- hisMonth: this.monthKey(this.calendarMonth()),
|
|
|
|
|
|
|
+ hisDay: null,
|
|
|
|
|
+ hisMonth: null,
|
|
|
},
|
|
},
|
|
|
queryParamsHandling: 'merge',
|
|
queryParamsHandling: 'merge',
|
|
|
replaceUrl: true,
|
|
replaceUrl: true,
|
|
|
}).finally(() => { this.syncingHistoryQuery = false; });
|
|
}).finally(() => { this.syncingHistoryQuery = false; });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private initializeCalendar(items: Item[]): void {
|
|
|
|
|
- if (this.calendarInitialized) return;
|
|
|
|
|
- this.calendarInitialized = true;
|
|
|
|
|
- const currentMonth = this.monthKey(this.calendarMonth());
|
|
|
|
|
- const dates = items.map((item) => this.itemDate(item)).filter(Boolean).sort();
|
|
|
|
|
- if (!dates.some((date) => date.startsWith(currentMonth)) && dates.length) {
|
|
|
|
|
- const [year, month] = dates.at(-1)!.split('-').map(Number);
|
|
|
|
|
- this.calendarMonth.set(new Date(year, month - 1, 1, 12));
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- private itemDate(item: Item): string {
|
|
|
|
|
- return this.dateKey(item['CreateTime'] ?? item['ctime'] ?? item['learned_date'] ?? item['PubAddTime']);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
private dateKey(value: unknown): string {
|
|
private dateKey(value: unknown): string {
|
|
|
const raw = String(value ?? '');
|
|
const raw = String(value ?? '');
|
|
|
const matched = raw.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
|
|
const matched = raw.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
|
|
@@ -356,15 +266,4 @@ export class StudyRecordsPageComponent implements OnInit {
|
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private firstOfMonth(date: Date): Date { return new Date(date.getFullYear(), date.getMonth(), 1, 12); }
|
|
|
|
|
- private monthKey(date: Date): string { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; }
|
|
|
|
|
- private parseDate(value: string): Date {
|
|
|
|
|
- const [year, month, day] = value.split('-').map(Number);
|
|
|
|
|
- return new Date(year, month - 1, day, 12);
|
|
|
|
|
- }
|
|
|
|
|
- private parseMonth(value: string): Date | null {
|
|
|
|
|
- const matched = value.match(/^(\d{4})-(\d{1,2})$/);
|
|
|
|
|
- if (!matched) return null;
|
|
|
|
|
- return new Date(Number(matched[1]), Number(matched[2]) - 1, 1, 12);
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|