|
|
@@ -46,6 +46,7 @@ interface CompanionOverview {
|
|
|
refreshedAt: string;
|
|
|
version?: string;
|
|
|
syncStatus?: LiveSyncStatus;
|
|
|
+ legacyAppointmentSync?: LiveSyncStatus & { complete?: boolean };
|
|
|
}
|
|
|
|
|
|
@Component({
|
|
|
@@ -80,6 +81,7 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
readonly liveChecking = signal(false);
|
|
|
readonly refreshedAt = signal('');
|
|
|
readonly liveSyncStatus = signal<LiveSyncStatus>({ state: 'current' });
|
|
|
+ readonly syncNoticeVisible = signal(false);
|
|
|
readonly error = signal('');
|
|
|
readonly studentOptions = computed(() => this.serverStudents().map(student => ({...student, id: String(student.id), name: String(student.name || '').trim() || `学员 ${student.id}`})));
|
|
|
readonly historyMonthOptions = computed(() => this.buildHistoryMonthOptions());
|
|
|
@@ -111,6 +113,8 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
private initialScheduleLoaded = false;
|
|
|
private historyFilterInitialized = false;
|
|
|
private stopLiveRefresh?: () => void;
|
|
|
+ private syncNoticeTimer?: ReturnType<typeof setTimeout>;
|
|
|
+ private hasFreshLegacyAppointments = false;
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
this.restoredScheduleState = this.restoreScheduleState();
|
|
|
@@ -119,13 +123,14 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
scopes: ['courses', 'appointments'],
|
|
|
studentId: () => Number(this.selectedStudent() === 'all' ? this.session.user()?.userId : this.selectedStudent()) || undefined,
|
|
|
onRefresh: () => this.load(true, true, true),
|
|
|
- onState: (state) => { this.liveChecking.set(state.checking); this.liveSyncStatus.set(state.syncStatus); },
|
|
|
+ onState: (state) => { this.liveChecking.set(state.checking); this.updateSyncStatus(state.syncStatus); },
|
|
|
});
|
|
|
if (this.hasScheduleCalendar) this.nowTimer = setInterval(() => this.now.set(Date.now()), 60_000);
|
|
|
}
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
|
this.stopLiveRefresh?.();
|
|
|
+ if (this.syncNoticeTimer) clearTimeout(this.syncNoticeTimer);
|
|
|
this.loadId++;this.loadRequest?.unsubscribe();
|
|
|
if (this.nowTimer) clearInterval(this.nowTimer);
|
|
|
}
|
|
|
@@ -160,7 +165,10 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
this.courses.set(Array.isArray(result?.courses) ? result.courses : []);
|
|
|
this.schedules.set(Array.isArray(result?.schedules) ? result.schedules : []);
|
|
|
this.refreshedAt.set(result.refreshedAt || new Date().toISOString());
|
|
|
- if (result.syncStatus) this.liveSyncStatus.set(result.syncStatus);
|
|
|
+ if (!this.isCoach) this.hasFreshLegacyAppointments = result.legacyAppointmentSync?.state === 'current' && result.legacyAppointmentSync.complete !== false;
|
|
|
+ if (result.syncStatus || this.hasFreshLegacyAppointments) {
|
|
|
+ this.updateSyncStatus(result.syncStatus || { state: 'current' }, refresh && !background);
|
|
|
+ }
|
|
|
this.loadedRange = range;
|
|
|
this.loadedAll = !range;
|
|
|
if (!this.initialScheduleLoaded && !this.isHistory && !this.restoredScheduleState) this.selectedDate.set(today);
|
|
|
@@ -169,12 +177,34 @@ export class CourseCenterPageComponent implements OnInit, OnDestroy {
|
|
|
},
|
|
|
error: (error: ApiError) => {
|
|
|
if (requestId !== this.loadId) return;
|
|
|
- if (background) this.liveSyncStatus.set({ state: 'unavailable' });
|
|
|
+ if (background) {
|
|
|
+ this.hasFreshLegacyAppointments = false;
|
|
|
+ this.updateSyncStatus({ state: 'unavailable' });
|
|
|
+ }
|
|
|
else this.error.set(error.message);
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+ private updateSyncStatus(status: LiveSyncStatus, forceNotice = false): void {
|
|
|
+ // A successful direct appointment pull is fresher than the separate legacy bridge checkpoint.
|
|
|
+ const next = this.hasFreshLegacyAppointments && status.state !== 'current' ? { state: 'current' as const } : status;
|
|
|
+ const previous = this.liveSyncStatus().state;
|
|
|
+ this.liveSyncStatus.set(next);
|
|
|
+ if (next.state === 'current') {
|
|
|
+ this.syncNoticeVisible.set(false);
|
|
|
+ if (this.syncNoticeTimer) clearTimeout(this.syncNoticeTimer);
|
|
|
+ this.syncNoticeTimer = undefined;
|
|
|
+ } else if (previous !== next.state || forceNotice) {
|
|
|
+ this.syncNoticeVisible.set(true);
|
|
|
+ if (this.syncNoticeTimer) clearTimeout(this.syncNoticeTimer);
|
|
|
+ this.syncNoticeTimer = setTimeout(() => {
|
|
|
+ this.syncNoticeVisible.set(false);
|
|
|
+ this.syncNoticeTimer = undefined;
|
|
|
+ }, 6_000);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
liveStatusText(): string {
|
|
|
const status = this.liveSyncStatus();
|
|
|
if (status.state === 'delayed') return status.lagSeconds == null ? '数据同步延迟,正在追平' : `数据同步延迟 ${status.lagSeconds} 秒,正在追平`;
|