|
@@ -1,5 +1,6 @@
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
|
import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core';
|
|
|
|
|
+import { FormsModule } from '@angular/forms';
|
|
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
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 } from 'lucide-angular';
|
|
|
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
|
import { catchError, finalize, forkJoin, of } from 'rxjs';
|
|
@@ -29,13 +30,14 @@ interface StudentOption {
|
|
|
@Component({
|
|
@Component({
|
|
|
selector: 'app-course-center-page',
|
|
selector: 'app-course-center-page',
|
|
|
standalone: true,
|
|
standalone: true,
|
|
|
- imports: [CommonModule, LucideAngularModule],
|
|
|
|
|
|
|
+ imports: [CommonModule, FormsModule, LucideAngularModule],
|
|
|
templateUrl: './course-center-page.component.html',
|
|
templateUrl: './course-center-page.component.html',
|
|
|
styleUrl: './course-center-page.component.scss',
|
|
styleUrl: './course-center-page.component.scss',
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
})
|
|
})
|
|
|
export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
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 isCoach = ['pages/home/shop', 'pages/member/ke_xiao'].includes(this.meta.path);
|
|
|
readonly isHistory = this.meta.path === 'pages/member/ke_xiao';
|
|
readonly isHistory = this.meta.path === 'pages/member/ke_xiao';
|
|
|
readonly hasScheduleCalendar = !this.isHistory;
|
|
readonly hasScheduleCalendar = !this.isHistory;
|
|
@@ -70,8 +72,11 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
private readonly session = inject(SessionService);
|
|
private readonly session = inject(SessionService);
|
|
|
private readonly platform = inject(PlatformService);
|
|
private readonly platform = inject(PlatformService);
|
|
|
private nowTimer?: ReturnType<typeof setInterval>;
|
|
private nowTimer?: ReturnType<typeof setInterval>;
|
|
|
|
|
+ private restoredScheduleState = false;
|
|
|
|
|
+ private syncingQuery = false;
|
|
|
|
|
|
|
|
ngOnInit(): void {
|
|
ngOnInit(): void {
|
|
|
|
|
+ this.restoredScheduleState = this.restoreScheduleState();
|
|
|
this.load();
|
|
this.load();
|
|
|
if (this.hasScheduleCalendar) this.nowTimer = setInterval(() => this.now.set(Date.now()), 60_000);
|
|
if (this.hasScheduleCalendar) this.nowTimer = setInterval(() => this.now.set(Date.now()), 60_000);
|
|
|
}
|
|
}
|
|
@@ -95,7 +100,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
.subscribe({
|
|
.subscribe({
|
|
|
next: ({ result }) => {
|
|
next: ({ result }) => {
|
|
|
this.schedules.set(this.asList(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),
|
|
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 }) => {
|
|
}).pipe(finalize(() => this.loading.set(false))).subscribe(({ courses, schedules }) => {
|
|
|
this.courses.set(this.asList(courses.result));
|
|
this.courses.set(this.asList(courses.result));
|
|
|
this.schedules.set(this.asList(schedules.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);
|
|
const failures = [courses.retmsg, schedules.retmsg].filter(Boolean);
|
|
|
if (failures.length) this.error.set(`部分数据加载失败:${failures.join(';')}`);
|
|
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; }
|
|
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.selectedStudent.set(value || 'all');
|
|
|
|
|
+ this.syncScheduleState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
shiftCalendarMonth(amount: number): void {
|
|
shiftCalendarMonth(amount: number): void {
|
|
|
const month = this.calendarMonth();
|
|
const month = this.calendarMonth();
|
|
|
this.calendarMonth.set(new Date(month.getFullYear(), month.getMonth() + amount, 1, 12));
|
|
this.calendarMonth.set(new Date(month.getFullYear(), month.getMonth() + amount, 1, 12));
|
|
|
|
|
+ this.syncScheduleState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
selectCalendarDay(day: CalendarDay): void { this.selectDate(day.date); }
|
|
selectCalendarDay(day: CalendarDay): void { this.selectDate(day.date); }
|
|
@@ -185,9 +194,13 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
showWeek(): void {
|
|
showWeek(): void {
|
|
|
this.scheduleRange.set('week');
|
|
this.scheduleRange.set('week');
|
|
|
this.ensureSelectedDate();
|
|
this.ensureSelectedDate();
|
|
|
|
|
+ this.syncScheduleState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- showAllSchedules(): void { this.scheduleRange.set('all'); }
|
|
|
|
|
|
|
+ showAllSchedules(): void {
|
|
|
|
|
+ this.scheduleRange.set('all');
|
|
|
|
|
+ this.syncScheduleState();
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
scheduleEmptyText(): string {
|
|
scheduleEmptyText(): string {
|
|
|
const target = this.isCoach ? '排课' : '伴学记录';
|
|
const target = this.isCoach ? '排课' : '伴学记录';
|
|
@@ -199,7 +212,8 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
open(item: Item): void {
|
|
open(item: Item): void {
|
|
|
if (!this.isCoach) {
|
|
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;
|
|
return;
|
|
|
}
|
|
}
|
|
|
if (this.isFutureUnfinished(item)) {
|
|
if (this.isFutureUnfinished(item)) {
|
|
@@ -211,6 +225,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
report(item: Item, event: Event): void {
|
|
report(item: Item, event: Event): void {
|
|
|
event.stopPropagation();
|
|
event.stopPropagation();
|
|
|
|
|
+ this.syncScheduleState();
|
|
|
if (this.rawStatus(item) > 10) {
|
|
if (this.rawStatus(item) > 10) {
|
|
|
void this.router.navigate(['/pages/shop/baobiao'], { queryParams: { sid: this.scheduleId(item) } });
|
|
void this.router.navigate(['/pages/shop/baobiao'], { queryParams: { sid: this.scheduleId(item) } });
|
|
|
return;
|
|
return;
|
|
@@ -220,17 +235,22 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
|
|
private loadStudentAndNavigate(item: Item, path: string, queryKey: 'id' | 'sid'): void {
|
|
private loadStudentAndNavigate(item: Item, path: string, queryKey: 'id' | 'sid'): void {
|
|
|
const studentId = Number(item['szyh'] ?? 0);
|
|
const studentId = Number(item['szyh'] ?? 0);
|
|
|
|
|
+ this.syncScheduleState();
|
|
|
|
|
+ this.storeAppointmentSnapshot(item);
|
|
|
if (!studentId) {
|
|
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;
|
|
return;
|
|
|
}
|
|
}
|
|
|
this.platform.loading.set(true);
|
|
this.platform.loading.set(true);
|
|
|
this.api.post<AppUser>('user_get', { uid: studentId }).pipe(finalize(() => this.platform.loading.set(false))).subscribe({
|
|
this.api.post<AppUser>('user_get', { uid: studentId }).pipe(finalize(() => this.platform.loading.set(false))).subscribe({
|
|
|
next: ({ result }) => {
|
|
next: ({ result }) => {
|
|
|
this.session.setStudent(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');
|
|
this.scheduleRange.set('day');
|
|
|
const value = this.parseDate(date);
|
|
const value = this.parseDate(date);
|
|
|
this.calendarMonth.set(new Date(value.getFullYear(), value.getMonth(), 1, 12));
|
|
this.calendarMonth.set(new Date(value.getFullYear(), value.getMonth(), 1, 12));
|
|
|
|
|
+ this.syncScheduleState();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private ensureSelectedDate(): void {
|
|
private ensureSelectedDate(): void {
|
|
@@ -437,4 +458,78 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
if (Number.isNaN(date.getTime())) return '';
|
|
if (Number.isNaN(date.getTime())) return '';
|
|
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
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);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|