|
|
@@ -1,11 +1,12 @@
|
|
|
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 { Router, RouterLink } from '@angular/router';
|
|
|
-import { Award, BookMarked, CalendarDays, CheckCircle2, ChevronRight, CircleHelp, History, LogOut, LucideAngularModule, MessageSquare, RefreshCw, Settings, ShieldCheck, UserRound, UsersRound } from 'lucide-angular';
|
|
|
+import { Award, BookMarked, CalendarDays, CheckCircle2, ChevronRight, CircleHelp, History, LoaderCircle, LogOut, LucideAngularModule, MessageSquare, RefreshCw, Settings, ShieldCheck, UserRound, UsersRound } from 'lucide-angular';
|
|
|
import { finalize } from 'rxjs';
|
|
|
import { APP_VERSION } from '../../core/app.constants';
|
|
|
import { ApiService } from '../../core/api.service';
|
|
|
import { AntiForgetHistorySummary } from '../../core/anti-forget';
|
|
|
+import { LiveDataRefreshService, LiveSyncStatus } from '../../core/live-data-refresh.service';
|
|
|
import { AppUser } from '../../core/models';
|
|
|
import { SessionService } from '../../core/session.service';
|
|
|
import { buildAccountMembership, CompanionLearningSummary } from './account-membership';
|
|
|
@@ -28,6 +29,8 @@ interface AccountOverview {
|
|
|
companionSummary?: Pick<CompanionLearningSummary, 'completedSessions' | 'completedMinutes'>;
|
|
|
antiForgetSummary?: AntiForgetHistorySummary;
|
|
|
refreshedAt: string;
|
|
|
+ version?: string;
|
|
|
+ syncStatus?: LiveSyncStatus;
|
|
|
}
|
|
|
|
|
|
@Component({
|
|
|
@@ -38,7 +41,7 @@ interface AccountOverview {
|
|
|
styleUrl: './account-page.component.scss',
|
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
|
})
|
|
|
-export class AccountPageComponent implements OnInit {
|
|
|
+export class AccountPageComponent implements OnInit, OnDestroy {
|
|
|
readonly session = inject(SessionService);
|
|
|
readonly version = APP_VERSION;
|
|
|
readonly coachPanel = signal<CoachPanelSummary>({ todaySchedules: 0, completedLessons: 0, reviewIncome: 0, reviewRecords: 0, totalLessons: 0, totalHours: 0, course30: 0, course60: 0, trialLessons: 0 });
|
|
|
@@ -48,10 +51,13 @@ export class AccountPageComponent implements OnInit {
|
|
|
readonly antiForgetError = signal('');
|
|
|
readonly avatarFailed = signal(false);
|
|
|
readonly companionLoading = signal(false);
|
|
|
+ readonly liveChecking = signal(false);
|
|
|
+ readonly refreshedAt = signal('');
|
|
|
+ readonly liveSyncStatus = signal<LiveSyncStatus>({ state: 'current' });
|
|
|
readonly antiForgetSummary = signal<AntiForgetHistorySummary>({ total: 0, completed: 0, progress: 0, todayTotal: 0, todayCompleted: 0, duePending: 0, scheduled: 0, scheduledDays: 0 });
|
|
|
readonly membership = computed(() => buildAccountMembership(this.session.user()));
|
|
|
readonly companionSummary = signal<CompanionLearningSummary>({ completedSessions: 0, completedMinutes: 0, durationLabel: '0 小时' });
|
|
|
- readonly icons = { Award, BookMarked, CalendarDays, CheckCircle2, ChevronRight, CircleHelp, History, LogOut, MessageSquare, RefreshCw, Settings, ShieldCheck, UserRound, UsersRound };
|
|
|
+ readonly icons = { Award, BookMarked, CalendarDays, CheckCircle2, ChevronRight, CircleHelp, History, LoaderCircle, LogOut, MessageSquare, RefreshCw, Settings, ShieldCheck, UserRound, UsersRound };
|
|
|
readonly services = computed(() => Number(this.session.groupId()) === 3 ? [
|
|
|
{ path: '/pages/home/shop', label: '陪练排课', icon: CalendarDays },
|
|
|
{ path: '/pages/member/ke_xiao', label: '销课记录', icon: CheckCircle2 },
|
|
|
@@ -65,35 +71,52 @@ export class AccountPageComponent implements OnInit {
|
|
|
]);
|
|
|
|
|
|
private readonly api = inject(ApiService);
|
|
|
+ private readonly liveData = inject(LiveDataRefreshService);
|
|
|
private readonly router = inject(Router);
|
|
|
+ private stopLiveRefresh?: () => void;
|
|
|
+ private overviewRequestId = 0;
|
|
|
|
|
|
ngOnInit(): void {
|
|
|
const uid = this.session.user()?.userId;
|
|
|
if (!uid) return;
|
|
|
this.loadAccountOverview(false);
|
|
|
+ this.stopLiveRefresh = this.liveData.watch({
|
|
|
+ scopes: ['appointments', 'review', 'account'],
|
|
|
+ studentId: () => Number(this.session.user()?.userId || 0) || undefined,
|
|
|
+ onRefresh: () => this.loadAccountOverview(true, true),
|
|
|
+ onState: (state) => { this.liveChecking.set(state.checking); this.liveSyncStatus.set(state.syncStatus); },
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
+ ngOnDestroy(): void { this.stopLiveRefresh?.(); }
|
|
|
+
|
|
|
loadAntiForget(): void { this.loadAccountOverview(true); }
|
|
|
retryOverview(): void { this.loadAccountOverview(true); }
|
|
|
|
|
|
- private loadAccountOverview(refresh: boolean): void {
|
|
|
+ private loadAccountOverview(refresh: boolean, background = false): void {
|
|
|
const uid = this.session.user()?.userId;
|
|
|
if (!uid) return;
|
|
|
+ const requestId = ++this.overviewRequestId;
|
|
|
const coach = Number(this.session.groupId()) === 3;
|
|
|
- this.coachPanelLoading.set(coach);
|
|
|
- this.companionLoading.set(!coach);
|
|
|
- this.antiForgetLoading.set(!coach);
|
|
|
- this.antiForgetError.set('');
|
|
|
- this.overviewError.set('');
|
|
|
+ if (!background) {
|
|
|
+ this.coachPanelLoading.set(coach);
|
|
|
+ this.companionLoading.set(!coach);
|
|
|
+ this.antiForgetLoading.set(!coach);
|
|
|
+ this.antiForgetError.set('');
|
|
|
+ this.overviewError.set('');
|
|
|
+ }
|
|
|
this.api.post<AccountOverview>('app_account_overview', { uid, refresh }).pipe(
|
|
|
finalize(() => {
|
|
|
+ if (background || requestId !== this.overviewRequestId) return;
|
|
|
this.coachPanelLoading.set(false);
|
|
|
this.companionLoading.set(false);
|
|
|
this.antiForgetLoading.set(false);
|
|
|
}),
|
|
|
).subscribe({
|
|
|
next: ({ result }) => {
|
|
|
- if (!result) return;
|
|
|
+ if (!result || requestId !== this.overviewRequestId) return;
|
|
|
+ this.refreshedAt.set(result.refreshedAt || new Date().toISOString());
|
|
|
+ if (result.syncStatus) this.liveSyncStatus.set(result.syncStatus);
|
|
|
this.session.patchUser(result.profile ?? {});
|
|
|
if (result.coachPanel) this.coachPanel.set(result.coachPanel);
|
|
|
if (result.companionSummary) {
|
|
|
@@ -103,6 +126,11 @@ export class AccountPageComponent implements OnInit {
|
|
|
if (result.antiForgetSummary) this.antiForgetSummary.set(result.antiForgetSummary);
|
|
|
},
|
|
|
error: () => {
|
|
|
+ if (requestId !== this.overviewRequestId) return;
|
|
|
+ if (background) {
|
|
|
+ this.liveSyncStatus.set({ state: 'unavailable' });
|
|
|
+ return;
|
|
|
+ }
|
|
|
this.overviewError.set(coach ? '老师工作数据暂时无法加载' : '账号数据暂时无法加载');
|
|
|
if (!coach) this.antiForgetError.set('抗遗忘数据暂时无法加载');
|
|
|
},
|
|
|
@@ -111,6 +139,12 @@ export class AccountPageComponent implements OnInit {
|
|
|
|
|
|
logout(): void { this.session.logout(); void this.router.navigateByUrl('/pages/member/wxauth'); }
|
|
|
avatarUrl(): string { return this.api.assetUrl(this.session.user()?.userFace); }
|
|
|
+ liveStatusText(): string {
|
|
|
+ const status = this.liveSyncStatus();
|
|
|
+ if (status.state === 'delayed') return '数据同步延迟,正在追平';
|
|
|
+ if (status.state === 'unavailable') return '实时同步暂时不可用';
|
|
|
+ return this.refreshedAt() ? `更新于 ${new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(new Date(this.refreshedAt()))}` : '正在核对最新数据';
|
|
|
+ }
|
|
|
|
|
|
private formatMinutes(minutes: number): string {
|
|
|
if (!minutes) return '0 小时';
|