|
|
@@ -1,5 +1,5 @@
|
|
|
import { CommonModule } from '@angular/common';
|
|
|
-import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
|
|
|
+import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
|
import { AlertCircle, BarChart3, BookOpen, CalendarDays, ChevronRight, GraduationCap, LoaderCircle, LucideAngularModule } from 'lucide-angular';
|
|
|
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
|
|
@@ -34,7 +34,7 @@ interface StudentOption {
|
|
|
styleUrl: './course-center-page.component.scss',
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
})
|
|
|
-export class CourseCenterPageComponent implements OnInit {
|
|
|
+export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
readonly meta = inject(ActivatedRoute).snapshot.data['meta'] as PageMeta;
|
|
|
readonly isCoach = ['pages/home/shop', 'pages/member/ke_xiao'].includes(this.meta.path);
|
|
|
readonly isHistory = this.meta.path === 'pages/member/ke_xiao';
|
|
|
@@ -47,6 +47,7 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
readonly selectedDate = signal(this.today());
|
|
|
readonly selectedStudent = signal('all');
|
|
|
readonly calendarMonth = signal(this.firstOfMonth(new Date()));
|
|
|
+ readonly now = signal(Date.now());
|
|
|
readonly loading = signal(false);
|
|
|
readonly error = signal('');
|
|
|
readonly studentOptions = computed(() => this.buildStudentOptions());
|
|
|
@@ -68,8 +69,16 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
private readonly router = inject(Router);
|
|
|
private readonly session = inject(SessionService);
|
|
|
private readonly platform = inject(PlatformService);
|
|
|
+ private nowTimer?: ReturnType<typeof setInterval>;
|
|
|
|
|
|
- ngOnInit(): void { this.load(); }
|
|
|
+ ngOnInit(): void {
|
|
|
+ this.load();
|
|
|
+ if (this.hasScheduleCalendar) this.nowTimer = setInterval(() => this.now.set(Date.now()), 60_000);
|
|
|
+ }
|
|
|
+
|
|
|
+ ngOnDestroy(): void {
|
|
|
+ if (this.nowTimer) clearInterval(this.nowTimer);
|
|
|
+ }
|
|
|
|
|
|
load(): void {
|
|
|
const uid = this.session.user()?.userId;
|
|
|
@@ -147,12 +156,13 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
scheduleId(item: Item): unknown { return item['GeneralID'] ?? item['ID'] ?? item['id']; }
|
|
|
|
|
|
statusText(item: Item): string {
|
|
|
- return ({ 0: '未开始', 10: '已开始', 11: '已上课', 20: '已评价', 30: '已结课' } as Record<number, string>)[Number(item['dszt'] ?? 0)] ?? '--';
|
|
|
+ if (this.isOverdueUnfinished(item)) return '未完课';
|
|
|
+ return ({ 0: '未开始', 10: '已开始', 11: '已上课', 20: '已评价', 30: '已结课' } as Record<number, string>)[this.rawStatus(item)] ?? '--';
|
|
|
}
|
|
|
|
|
|
- statusClass(item: Item): string { return `status-${Number(item['dszt'] ?? 0)}`; }
|
|
|
+ statusClass(item: Item): string { return this.isOverdueUnfinished(item) ? 'status-11' : `status-${this.rawStatus(item)}`; }
|
|
|
|
|
|
- isFinished(item: Item): boolean { return Number(item['dszt'] ?? 0) >= 30; }
|
|
|
+ isFinished(item: Item): boolean { return this.rawStatus(item) >= 30; }
|
|
|
|
|
|
setScheduleFilter(filter: ScheduleFilter): void { this.scheduleFilter.set(filter); }
|
|
|
|
|
|
@@ -192,7 +202,7 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
void this.router.navigate(['/pages/shop/stu_get_order'], { queryParams: { id: this.scheduleId(item) } });
|
|
|
return;
|
|
|
}
|
|
|
- if (Number(item['dszt'] ?? 0) <= 10 && String(item['bxrq'] ?? '').slice(0, 10) !== this.today()) {
|
|
|
+ if (this.isFutureUnfinished(item)) {
|
|
|
this.platform.toast('课程未开始', 'error');
|
|
|
return;
|
|
|
}
|
|
|
@@ -201,7 +211,7 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
|
|
|
report(item: Item, event: Event): void {
|
|
|
event.stopPropagation();
|
|
|
- if (Number(item['dszt'] ?? 0) > 10) {
|
|
|
+ if (this.rawStatus(item) > 10) {
|
|
|
void this.router.navigate(['/pages/shop/baobiao'], { queryParams: { sid: this.scheduleId(item) } });
|
|
|
return;
|
|
|
}
|
|
|
@@ -291,7 +301,72 @@ export class CourseCenterPageComponent implements OnInit {
|
|
|
private sortedVisibleSchedules(): Item[] {
|
|
|
const list = [...this.visibleSchedules()];
|
|
|
if (this.isHistory) return list;
|
|
|
- return list.sort((left, right) => `${this.itemDate(left)} ${this.scheduleTime(left)}`.localeCompare(`${this.itemDate(right)} ${this.scheduleTime(right)}`));
|
|
|
+ return list.sort((left, right) => this.lessonStartTime(left) - this.lessonStartTime(right));
|
|
|
+ }
|
|
|
+
|
|
|
+ private rawStatus(item: Item): number { return Number(item['dszt'] ?? 0); }
|
|
|
+
|
|
|
+ private isPendingRaw(item: Item): boolean { return this.rawStatus(item) <= 10; }
|
|
|
+
|
|
|
+ private isFutureUnfinished(item: Item): boolean {
|
|
|
+ if (!this.isPendingRaw(item)) return false;
|
|
|
+ const start = this.lessonStart(item);
|
|
|
+ return Boolean(start && start.getTime() > this.now());
|
|
|
+ }
|
|
|
+
|
|
|
+ private isOverdueUnfinished(item: Item): boolean {
|
|
|
+ if (!this.isPendingRaw(item)) return false;
|
|
|
+ const end = this.lessonEnd(item);
|
|
|
+ if (end) return end.getTime() < this.now();
|
|
|
+ const date = this.itemDate(item);
|
|
|
+ return Boolean(date && date < this.today());
|
|
|
+ }
|
|
|
+
|
|
|
+ private lessonStartTime(item: Item): number {
|
|
|
+ return this.lessonStart(item)?.getTime() ?? Number.MAX_SAFE_INTEGER;
|
|
|
+ }
|
|
|
+
|
|
|
+ private lessonStart(item: Item): Date | null {
|
|
|
+ const rawDateTime = this.dateTimeFromRaw(item['yysj'] ?? item['bxrq'] ?? item['yydate']);
|
|
|
+ const date = this.itemDate(item);
|
|
|
+ if (!date) return rawDateTime;
|
|
|
+ const time = this.timeParts(item['sdsd'] ?? item['kssj'] ?? item['yysj']);
|
|
|
+ if (!time) return rawDateTime ?? this.parseDate(date);
|
|
|
+ const [year, month, day] = date.split('-').map(Number);
|
|
|
+ return new Date(year, month - 1, day, time.hour, time.minute);
|
|
|
+ }
|
|
|
+
|
|
|
+ private lessonEnd(item: Item): Date | null {
|
|
|
+ const start = this.lessonStart(item);
|
|
|
+ if (!start) return null;
|
|
|
+ return new Date(start.getTime() + this.lessonMinutes(item) * 60_000);
|
|
|
+ }
|
|
|
+
|
|
|
+ private lessonMinutes(item: Item): number {
|
|
|
+ const text = `${item['Title'] ?? ''} ${item['Title2'] ?? ''} ${item['Subtitle'] ?? ''}`;
|
|
|
+ const matched = text.match(/(\d{1,3})\s*分钟/);
|
|
|
+ if (matched) return Number(matched[1]);
|
|
|
+ const type = Number(item['dslx'] ?? 0);
|
|
|
+ if (type === 1) return 30;
|
|
|
+ if (type === 2) return 60;
|
|
|
+ if (type === 3) return 30;
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ private timeParts(value: unknown): { hour: number; minute: number } | null {
|
|
|
+ const matched = String(value ?? '').match(/(?:^|\s)(\d{1,2}):(\d{2})/);
|
|
|
+ if (!matched) return null;
|
|
|
+ const hour = Number(matched[1]);
|
|
|
+ const minute = Number(matched[2]);
|
|
|
+ if (hour > 23 || minute > 59) return null;
|
|
|
+ return { hour, minute };
|
|
|
+ }
|
|
|
+
|
|
|
+ private dateTimeFromRaw(value: unknown): Date | null {
|
|
|
+ const raw = String(value ?? '');
|
|
|
+ const matched = raw.match(/^(\d{4})-(\d{1,2})-(\d{1,2})(?:[ T](\d{1,2}):(\d{2}))?/);
|
|
|
+ if (!matched) return null;
|
|
|
+ return new Date(Number(matched[1]), Number(matched[2]) - 1, Number(matched[3]), Number(matched[4] ?? 0), Number(matched[5] ?? 0));
|
|
|
}
|
|
|
|
|
|
private selectDate(date: string): void {
|