Kaynağa Gözat

fix: 优化陪练排课状态判断

彭峰 2 ay önce
ebeveyn
işleme
66fdb62c40

+ 84 - 9
src/app/features/course-center/course-center-page.component.ts

@@ -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 {