Explorar el Código

fix: 恢复排课列表状态并兜底详情异常

彭峰 hace 2 meses
padre
commit
da8060a810

+ 1 - 0
src/app/features/appointment/appointment-page.component.html

@@ -8,6 +8,7 @@
       <div><span>{{ status() > 20 ? '课程陪练报告' : '预约课程' }}</span><strong>{{ statusText() }}</strong></div>
       <lucide-icon [img]="status() >= 30 ? icons.Flag : icons.Clock3" [size]="28" />
     </header>
+    @if (detailWarning()) { <p class="detail-warning">{{ detailWarning() }}</p> }
 
     <dl class="appointment-info">
       <div><dt>学生姓名</dt><dd>{{ field('student') || '-' }}</dd></div>

+ 1 - 0
src/app/features/appointment/appointment-page.component.scss

@@ -3,6 +3,7 @@
 .appointment-status > div { display: flex; flex-direction: column; gap: 5px; }
 .appointment-status span { font-size: 12px; opacity: .82; }
 .appointment-status strong { font-size: 20px; }
+.detail-warning { margin: 0; padding: 9px 16px; color: #8a5b12; background: #fff3d9; font-size: 12px; }
 .appointment-info { margin: 0; padding: 5px 16px; background: white; }
 .appointment-info div { display: grid; min-height: 46px; grid-template-columns: 92px minmax(0, 1fr); align-items: center; border-bottom: 1px solid var(--line); gap: 10px; }
 .appointment-info div:last-child { border-bottom: 0; }

+ 47 - 12
src/app/features/appointment/appointment-page.component.ts

@@ -32,6 +32,7 @@ export class AppointmentPageComponent implements OnInit {
   readonly loading = signal(true);
   readonly submitting = signal(false);
   readonly error = signal('');
+  readonly detailWarning = signal('');
   readonly icons = { BookCheck, Check, ChevronRight, CircleHelp, Clock3, Flag, GraduationCap, Play, Save, Star, X };
   readonly status = computed(() => Number(this.order()['dszt'] ?? this.order()['status'] ?? 0));
 
@@ -62,27 +63,29 @@ export class AppointmentPageComponent implements OnInit {
     }
     this.loading.set(true);
     this.error.set('');
+    this.detailWarning.set('');
+    const fallback = this.appointmentSnapshot();
     this.api.get<unknown>('e_order_detail', { id: this.orderId }).pipe(finalize(() => this.loading.set(false))).subscribe({
       next: ({ result }) => {
-        const order = this.findList(result)[0] ?? this.asRecord(result);
-        this.order.set(order);
-        this.selectedCourse = String(order['kcid'] ?? '');
-        this.coachComment = String(order['pldp'] ?? '');
-        this.loadPracticeRecord();
-        if (!this.isStudentView) {
-          this.loadCourses();
-          this.loadLearningRecord();
-          if (this.status() <= 10 && this.selectedCourse) this.loadUnits();
+        const detail = this.findList(result)[0] ?? this.asRecord(result);
+        this.applyOrder({ ...fallback, ...detail });
+      },
+      error: (error: ApiError) => {
+        if (Object.keys(fallback).length) {
+          this.applyOrder(fallback);
+          this.detailWarning.set('详情接口暂时返回异常,当前显示排课列表里的课程信息。你可以返回列表后重试。');
+          this.loading.set(false);
+          return;
         }
+        this.error.set(this.friendlyDetailError(error.message));
       },
-      error: (error: ApiError) => this.error.set(error.message),
     });
   }
 
   field(label: string): string {
     const aliases: Record<string, string[]> = {
-      student: ['HoneyName', 'honeyName', 'xymz_t'],
-      course: ['yykcmc', 'kcmc', 'Title'],
+      student: ['HoneyName', 'honeyName', 'honeyname', 'xymz_t'],
+      course: ['yykcmc', 'kcmc', 'Subtitle', 'ctitle', 'Title'],
       type: ['dslx', 'dslx_t', 'kclx'],
       date: ['bxrq', 'yydate'],
       time: ['sdsd', 'startTime'],
@@ -98,6 +101,18 @@ export class AppointmentPageComponent implements OnInit {
     return String(this.order()['status_txt'] ?? ({ 0: '待开始', 10: '学习中', 11: '等待学员确认', 20: '学员已确认', 30: '已完成' } as Record<number, string>)[this.status()] ?? `状态 ${this.status()}`);
   }
 
+  private applyOrder(order: SourceItem): void {
+    this.order.set(order);
+    this.selectedCourse = String(order['kcid'] ?? '');
+    this.coachComment = String(order['pldp'] ?? '');
+    this.loadPracticeRecord();
+    if (!this.isStudentView) {
+      this.loadCourses();
+      this.loadLearningRecord();
+      if (this.status() <= 10 && this.selectedCourse) this.loadUnits();
+    }
+  }
+
   chooseCourse(): void {
     const selected = this.courses().find((course) => String(course['kcid'] ?? course['kcId']) === this.selectedCourse);
     if (!selected) return;
@@ -372,6 +387,26 @@ export class AppointmentPageComponent implements OnInit {
     return value && typeof value === 'object' && !Array.isArray(value) ? value as SourceItem : {};
   }
 
+  private appointmentSnapshot(): SourceItem {
+    const state = history.state as { appointmentSnapshot?: unknown } | null;
+    if (state?.appointmentSnapshot && typeof state.appointmentSnapshot === 'object' && !Array.isArray(state.appointmentSnapshot)) {
+      return state.appointmentSnapshot as SourceItem;
+    }
+    try {
+      const stored = sessionStorage.getItem(`xiaoshu.appointment.snapshot.${this.orderId}`);
+      const parsed = stored ? JSON.parse(stored) as unknown : null;
+      return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as SourceItem : {};
+    } catch {
+      return {};
+    }
+  }
+
+  private friendlyDetailError(message: string): string {
+    return message.includes('Object reference not set to an instance of an object')
+      ? '课程详情暂时无法加载,请返回排课列表重试。'
+      : message;
+  }
+
   private dateOnly(): string {
     const now = new Date();
     return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;

+ 1 - 1
src/app/features/course-center/course-center-page.component.html

@@ -31,7 +31,7 @@
             @if (isCoach && studentOptions().length) {
               <label class="student-filter">
                 <span>学员</span>
-                <select [value]="selectedStudent()" (change)="setStudentFilter($event)">
+                <select [ngModel]="selectedStudent()" (ngModelChange)="setStudentFilter($event)">
                   <option value="all">全部学员({{ schedules().length }})</option>
                   @for (student of studentOptions(); track student.id) {
                     <option [value]="student.id">{{ student.name }}({{ student.count }})</option>

+ 107 - 12
src/app/features/course-center/course-center-page.component.ts

@@ -1,5 +1,6 @@
 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 { catchError, finalize, forkJoin, of } from 'rxjs';
@@ -29,13 +30,14 @@ interface StudentOption {
 @Component({
   selector: 'app-course-center-page',
   standalone: true,
-  imports: [CommonModule, LucideAngularModule],
+  imports: [CommonModule, FormsModule, LucideAngularModule],
   templateUrl: './course-center-page.component.html',
   styleUrl: './course-center-page.component.scss',
   changeDetection: ChangeDetectionStrategy.OnPush,
 })
 export class CourseCenterPageComponent implements OnInit, OnDestroy {
-  readonly meta = inject(ActivatedRoute).snapshot.data['meta'] as PageMeta;
+  private readonly route = inject(ActivatedRoute);
+  readonly meta = this.route.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';
   readonly hasScheduleCalendar = !this.isHistory;
@@ -70,8 +72,11 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
   private readonly session = inject(SessionService);
   private readonly platform = inject(PlatformService);
   private nowTimer?: ReturnType<typeof setInterval>;
+  private restoredScheduleState = false;
+  private syncingQuery = false;
 
   ngOnInit(): void {
+    this.restoredScheduleState = this.restoreScheduleState();
     this.load();
     if (this.hasScheduleCalendar) this.nowTimer = setInterval(() => this.now.set(Date.now()), 60_000);
   }
@@ -95,7 +100,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
         .subscribe({
           next: ({ result }) => {
             this.schedules.set(this.asList(result));
-            if (!this.isHistory) this.selectDate(today);
+            if (!this.isHistory && !this.restoredScheduleState) this.selectDate(today);
           },
           error: (error: ApiError) => this.error.set(error.message),
         });
@@ -110,7 +115,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
     }).pipe(finalize(() => this.loading.set(false))).subscribe(({ courses, schedules }) => {
       this.courses.set(this.asList(courses.result));
       this.schedules.set(this.asList(schedules.result));
-      this.selectDate(today);
+      if (!this.restoredScheduleState) this.selectDate(today);
       const failures = [courses.retmsg, schedules.retmsg].filter(Boolean);
       if (failures.length) this.error.set(`部分数据加载失败:${failures.join(';')}`);
     });
@@ -164,16 +169,20 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
 
   isFinished(item: Item): boolean { return this.rawStatus(item) >= 30; }
 
-  setScheduleFilter(filter: ScheduleFilter): void { this.scheduleFilter.set(filter); }
+  setScheduleFilter(filter: ScheduleFilter): void {
+    this.scheduleFilter.set(filter);
+    this.syncScheduleState();
+  }
 
-  setStudentFilter(event: Event): void {
-    const value = event.target instanceof HTMLSelectElement ? event.target.value : 'all';
+  setStudentFilter(value: string): void {
     this.selectedStudent.set(value || 'all');
+    this.syncScheduleState();
   }
 
   shiftCalendarMonth(amount: number): void {
     const month = this.calendarMonth();
     this.calendarMonth.set(new Date(month.getFullYear(), month.getMonth() + amount, 1, 12));
+    this.syncScheduleState();
   }
 
   selectCalendarDay(day: CalendarDay): void { this.selectDate(day.date); }
@@ -185,9 +194,13 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
   showWeek(): void {
     this.scheduleRange.set('week');
     this.ensureSelectedDate();
+    this.syncScheduleState();
   }
 
-  showAllSchedules(): void { this.scheduleRange.set('all'); }
+  showAllSchedules(): void {
+    this.scheduleRange.set('all');
+    this.syncScheduleState();
+  }
 
   scheduleEmptyText(): string {
     const target = this.isCoach ? '排课' : '伴学记录';
@@ -199,7 +212,8 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
 
   open(item: Item): void {
     if (!this.isCoach) {
-      void this.router.navigate(['/pages/shop/stu_get_order'], { queryParams: { id: this.scheduleId(item) } });
+      this.storeAppointmentSnapshot(item);
+      void this.router.navigate(['/pages/shop/stu_get_order'], { queryParams: { id: this.scheduleId(item) }, state: { appointmentSnapshot: item } });
       return;
     }
     if (this.isFutureUnfinished(item)) {
@@ -211,6 +225,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
 
   report(item: Item, event: Event): void {
     event.stopPropagation();
+    this.syncScheduleState();
     if (this.rawStatus(item) > 10) {
       void this.router.navigate(['/pages/shop/baobiao'], { queryParams: { sid: this.scheduleId(item) } });
       return;
@@ -220,17 +235,22 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
 
   private loadStudentAndNavigate(item: Item, path: string, queryKey: 'id' | 'sid'): void {
     const studentId = Number(item['szyh'] ?? 0);
+    this.syncScheduleState();
+    this.storeAppointmentSnapshot(item);
     if (!studentId) {
-      void this.router.navigate([path], { queryParams: { [queryKey]: this.scheduleId(item) } });
+      void this.router.navigate([path], { queryParams: { [queryKey]: this.scheduleId(item) }, state: { appointmentSnapshot: item } });
       return;
     }
     this.platform.loading.set(true);
     this.api.post<AppUser>('user_get', { uid: studentId }).pipe(finalize(() => this.platform.loading.set(false))).subscribe({
       next: ({ result }) => {
         this.session.setStudent(result);
-        void this.router.navigate([path], { queryParams: { [queryKey]: this.scheduleId(item) } });
+        void this.router.navigate([path], { queryParams: { [queryKey]: this.scheduleId(item) }, state: { appointmentSnapshot: item } });
+      },
+      error: () => {
+        this.platform.toast('学员信息暂时无法加载,已使用排课信息打开详情', 'error');
+        void this.router.navigate([path], { queryParams: { [queryKey]: this.scheduleId(item) }, state: { appointmentSnapshot: item } });
       },
-      error: (error: ApiError) => this.platform.toast(error.message, 'error'),
     });
   }
 
@@ -374,6 +394,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
     this.scheduleRange.set('day');
     const value = this.parseDate(date);
     this.calendarMonth.set(new Date(value.getFullYear(), value.getMonth(), 1, 12));
+    this.syncScheduleState();
   }
 
   private ensureSelectedDate(): void {
@@ -437,4 +458,78 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
     if (Number.isNaN(date.getTime())) return '';
     return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
   }
+
+  private restoreScheduleState(): boolean {
+    if (!this.hasScheduleCalendar) return false;
+    const query = this.route.snapshot.queryParamMap;
+    let restored = false;
+    const status = query.get('csStatus') as ScheduleFilter | null;
+    const range = query.get('csRange') as ScheduleRange | null;
+    const date = query.get('csDate');
+    const student = query.get('csStudent');
+    const month = query.get('csMonth');
+    if (status && ['all', 'pending', 'finished'].includes(status)) {
+      this.scheduleFilter.set(status);
+      restored = true;
+    }
+    if (range && ['day', 'week', 'all'].includes(range)) {
+      this.scheduleRange.set(range);
+      restored = true;
+    }
+    if (date && this.dateKey(date)) {
+      this.selectedDate.set(this.dateKey(date));
+      restored = true;
+    }
+    if (student) {
+      this.selectedStudent.set(student);
+      restored = true;
+    }
+    if (month) {
+      const restoredMonth = this.parseMonth(month);
+      if (restoredMonth) {
+        this.calendarMonth.set(restoredMonth);
+        restored = true;
+      }
+    } else if (date && this.dateKey(date)) {
+      this.calendarMonth.set(this.firstOfMonth(this.parseDate(this.dateKey(date))));
+    }
+    return restored;
+  }
+
+  private syncScheduleState(): void {
+    if (!this.hasScheduleCalendar || this.syncingQuery) return;
+    this.syncingQuery = true;
+    void this.router.navigate([], {
+      relativeTo: this.route,
+      queryParams: {
+        csRange: this.scheduleRange(),
+        csDate: this.selectedDate(),
+        csStudent: this.selectedStudent(),
+        csStatus: this.scheduleFilter(),
+        csMonth: this.monthKey(this.calendarMonth()),
+      },
+      queryParamsHandling: 'merge',
+      replaceUrl: true,
+    }).finally(() => { this.syncingQuery = false; });
+  }
+
+  private storeAppointmentSnapshot(item: Item): void {
+    const id = this.scheduleId(item);
+    if (id === undefined || id === null || id === '') return;
+    try {
+      sessionStorage.setItem(`xiaoshu.appointment.snapshot.${id}`, JSON.stringify(item));
+    } catch {
+      // Storage can be unavailable in restricted browser modes; navigation state still carries the snapshot.
+    }
+  }
+
+  private monthKey(date: Date): string {
+    return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
+  }
+
+  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);
+  }
 }