|
@@ -9,7 +9,7 @@ import { antiForgetCompleted, antiForgetDate, buildAntiForgetView, findAntiForge
|
|
|
import { ApiService } from '../../core/api.service';
|
|
import { ApiService } from '../../core/api.service';
|
|
|
import { PlatformService } from '../../core/platform.service';
|
|
import { PlatformService } from '../../core/platform.service';
|
|
|
import { SessionService } from '../../core/session.service';
|
|
import { SessionService } from '../../core/session.service';
|
|
|
-import { LiveDataRefreshService } from '../../core/live-data-refresh.service';
|
|
|
|
|
|
|
+import { HomeDailyCacheService } from '../../core/home-daily-cache.service';
|
|
|
|
|
|
|
|
type Item = Record<string, unknown>;
|
|
type Item = Record<string, unknown>;
|
|
|
|
|
|
|
@@ -64,62 +64,56 @@ export class HomePageComponent implements OnInit, OnDestroy {
|
|
|
private readonly api = inject(ApiService);
|
|
private readonly api = inject(ApiService);
|
|
|
private readonly router = inject(Router);
|
|
private readonly router = inject(Router);
|
|
|
private readonly platform = inject(PlatformService);
|
|
private readonly platform = inject(PlatformService);
|
|
|
- private readonly liveData = inject(LiveDataRefreshService);
|
|
|
|
|
|
|
+ private readonly dailyCache = inject(HomeDailyCacheService);
|
|
|
|
|
+ private readonly requests = new Subscription();
|
|
|
private readonly zone=inject(NgZone);
|
|
private readonly zone=inject(NgZone);
|
|
|
private readonly todayReviews = inject(TeacherTodayReviewService);
|
|
private readonly todayReviews = inject(TeacherTodayReviewService);
|
|
|
private reviewRequest?: Subscription;
|
|
private reviewRequest?: Subscription;
|
|
|
private midnightTimer?: ReturnType<typeof setTimeout>;
|
|
private midnightTimer?: ReturnType<typeof setTimeout>;
|
|
|
- private readonly resumeToday=()=>{
|
|
|
|
|
- if(document.visibilityState==='visible') {
|
|
|
|
|
- const uid=this.session.user()?.userId;
|
|
|
|
|
- if(uid && (!this.isCoach() || this.reviewLoadedDate !== beijingToday())) this.loadAntiForget(uid);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ private homeLoadedDate = '';
|
|
|
|
|
+ private readonly resumeToday = () => {
|
|
|
|
|
+ if (document.visibilityState === 'visible' && this.homeLoadedDate !== beijingToday()) this.loadDailyHome();
|
|
|
};
|
|
};
|
|
|
- private reviewLoadedDate = '';
|
|
|
|
|
- private scheduleTodayRefresh():void{
|
|
|
|
|
- if(this.midnightTimer)clearTimeout(this.midnightTimer);
|
|
|
|
|
- const now=Date.now(),next=(Math.floor((now+8*3600000)/86400000)+1)*86400000-8*3600000;
|
|
|
|
|
- this.midnightTimer=this.zone.runOutsideAngular(()=>setTimeout(()=>this.zone.run(()=>{const uid=this.session.user()?.userId;if(uid)this.loadAntiForget(uid);this.retryCoachStats();this.scheduleTodayRefresh();}),next-now+100));
|
|
|
|
|
- }
|
|
|
|
|
- private stopLiveRefresh?: () => void;
|
|
|
|
|
|
|
|
|
|
- ngOnInit(): void {
|
|
|
|
|
|
|
+ private loadDailyHome(): void {
|
|
|
const uid = this.session.user()?.userId;
|
|
const uid = this.session.user()?.userId;
|
|
|
- if (uid) {
|
|
|
|
|
|
|
+ if (!uid) return;
|
|
|
|
|
+ this.homeLoadedDate = beijingToday();
|
|
|
|
|
+ this.loadStats(uid);
|
|
|
|
|
+ if (!this.isCoach()) this.loadBalances(uid);
|
|
|
|
|
+ this.loadAntiForget(uid);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private scheduleTodayRefresh(): void {
|
|
|
|
|
+ if (this.midnightTimer) clearTimeout(this.midnightTimer);
|
|
|
|
|
+ const now = Date.now(), next = (Math.floor((now + 8 * 3600000) / 86400000) + 1) * 86400000 - 8 * 3600000;
|
|
|
|
|
+ this.midnightTimer = this.zone.runOutsideAngular(() => setTimeout(() => this.zone.run(() => {
|
|
|
|
|
+ this.resumeToday();
|
|
|
this.scheduleTodayRefresh();
|
|
this.scheduleTodayRefresh();
|
|
|
- document.addEventListener('visibilitychange',this.resumeToday);
|
|
|
|
|
- this.loadStats(uid);
|
|
|
|
|
- if(!this.isCoach())this.loadBalances(uid);
|
|
|
|
|
- this.loadAntiForget(uid);
|
|
|
|
|
- this.stopLiveRefresh = this.liveData.watch({
|
|
|
|
|
- scopes: ['appointments', 'review', 'account'],
|
|
|
|
|
- studentId: () => Number(this.session.user()?.userId) || undefined,
|
|
|
|
|
- onRefresh: () => {
|
|
|
|
|
- const currentId = this.session.user()?.userId;
|
|
|
|
|
- if (!currentId) return;
|
|
|
|
|
- this.loadStats(currentId, true);
|
|
|
|
|
- if(!this.isCoach())this.loadBalances(currentId);
|
|
|
|
|
- if (!this.isCoach()) this.loadAntiForget(currentId, true);
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ }), next - now + 100));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ngOnInit(): void {
|
|
|
|
|
+ this.loadDailyHome();
|
|
|
|
|
+ this.scheduleTodayRefresh();
|
|
|
|
|
+ document.addEventListener('visibilitychange', this.resumeToday);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
ngOnDestroy(): void {
|
|
|
- this.stopLiveRefresh?.();
|
|
|
|
|
|
|
+ this.requests.unsubscribe();
|
|
|
this.reviewRequest?.unsubscribe();
|
|
this.reviewRequest?.unsubscribe();
|
|
|
- if(this.midnightTimer)clearTimeout(this.midnightTimer);
|
|
|
|
|
- document.removeEventListener('visibilitychange',this.resumeToday);
|
|
|
|
|
|
|
+ if (this.midnightTimer) clearTimeout(this.midnightTimer);
|
|
|
|
|
+ document.removeEventListener('visibilitychange', this.resumeToday);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
retryCoachStats(): void {
|
|
retryCoachStats(): void {
|
|
|
const uid = this.session.user()?.userId;
|
|
const uid = this.session.user()?.userId;
|
|
|
- if (uid) this.loadStats(uid);
|
|
|
|
|
|
|
+ if (uid) this.loadStats(uid, true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
retryAntiForget(): void {
|
|
retryAntiForget(): void {
|
|
|
const uid = this.session.user()?.userId;
|
|
const uid = this.session.user()?.userId;
|
|
|
- if (uid) this.loadAntiForget(uid, false, true);
|
|
|
|
|
|
|
+ if (uid) this.loadAntiForget(uid, true);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
coachReviewStudent(item: Item): string {
|
|
coachReviewStudent(item: Item): string {
|
|
@@ -167,44 +161,58 @@ export class HomePageComponent implements OnInit, OnDestroy {
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private loadStats(uid: string | number, background = false): void {
|
|
|
|
|
- if (!background) {
|
|
|
|
|
- this.statsLoading.set(true);
|
|
|
|
|
- this.coachStatsError.set('');
|
|
|
|
|
- }
|
|
|
|
|
- this.api.post<Record<string, number>>('e_order_tongji', { uid }).pipe(
|
|
|
|
|
- finalize(() => { if (!background) this.statsLoading.set(false); }),
|
|
|
|
|
- ).subscribe({
|
|
|
|
|
- next: ({ result }) => {this.stats.set(result || {});this.statsAvailable.set(Boolean(result));this.coachStatsError.set('');},
|
|
|
|
|
- error: () => {
|
|
|
|
|
- this.coachStatsError.set(this.statsAvailable()?'统计暂时无法更新,当前显示上次结果':'统计暂时无法加载');
|
|
|
|
|
- },
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ private loadStats(uid: string | number, force = false): void {
|
|
|
|
|
+ this.statsLoading.set(true);
|
|
|
|
|
+ this.coachStatsError.set('');
|
|
|
|
|
+ this.requests.add(this.dailyCache.load('stats', () =>
|
|
|
|
|
+ this.api.post<Record<string, number>>('e_order_tongji', { uid }).pipe(map(({ result }) => result)), force,
|
|
|
|
|
+ ).pipe(finalize(() => this.statsLoading.set(false))).subscribe({
|
|
|
|
|
+ next: ({ value }) => { this.stats.set(value); this.statsAvailable.set(true); this.coachStatsError.set(''); },
|
|
|
|
|
+ error: () => this.coachStatsError.set(this.statsAvailable() ? '统计暂时无法更新,当前显示上次结果' : '统计暂时无法加载'),
|
|
|
|
|
+ }));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private loadAntiForget(uid: string | number, background = false, force = false): void {
|
|
|
|
|
|
|
+ private loadAntiForget(uid: string | number, force = false): void {
|
|
|
this.reviewRequest?.unsubscribe();
|
|
this.reviewRequest?.unsubscribe();
|
|
|
- if (!background) {
|
|
|
|
|
- this.antiForgetLoading.set(true);
|
|
|
|
|
- this.antiForgetError.set('');
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ this.antiForgetLoading.set(true);
|
|
|
|
|
+ this.antiForgetError.set('');
|
|
|
const request = this.isCoach() ? this.todayReviews.load(force) :
|
|
const request = this.isCoach() ? this.todayReviews.load(force) :
|
|
|
- this.api.post<ReviewPage>('app_review_page', { uid, page: 1, pageSize: 20, status: 'pending' }).pipe(map(({ result }) => result));
|
|
|
|
|
- this.reviewRequest = request.pipe(
|
|
|
|
|
- finalize(() => { if (!background) this.antiForgetLoading.set(false); }),
|
|
|
|
|
- ).subscribe({
|
|
|
|
|
- next: (result) => {
|
|
|
|
|
|
|
+ this.dailyCache.load('review', () => this.api.post<ReviewPage>('app_review_page', {
|
|
|
|
|
+ uid, page: 1, pageSize: 20, status: 'pending',
|
|
|
|
|
+ }).pipe(map(({ result }) => result)), force).pipe(map(snapshot => snapshot.value));
|
|
|
|
|
+ this.reviewRequest = request.pipe(finalize(() => this.antiForgetLoading.set(false))).subscribe({
|
|
|
|
|
+ next: result => {
|
|
|
this.reviewData.set(result);
|
|
this.reviewData.set(result);
|
|
|
this.antiForgetItems.set(result.items);
|
|
this.antiForgetItems.set(result.items);
|
|
|
- this.reviewLoadedDate = beijingToday();
|
|
|
|
|
this.antiForgetError.set('');
|
|
this.antiForgetError.set('');
|
|
|
},
|
|
},
|
|
|
- error: () => { this.antiForgetError.set('抗遗忘计划暂时无法加载'); },
|
|
|
|
|
|
|
+ error: () => this.antiForgetError.set('抗遗忘计划暂时无法加载'),
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- retryStudentStats():void { const uid=this.session.user()?.userId;if(uid){this.loadStats(uid);this.loadBalances(uid);} }
|
|
|
|
|
- private loadBalances(uid:string|number):void {this.balanceLoading.set(true);this.api.get<{profile:Record<string,unknown>}>('app_account_overview',{uid}).pipe(finalize(()=>this.balanceLoading.set(false))).subscribe({next:({result})=>{this.balances.set(result.profile);this.balanceError.set('');this.balanceRefreshedAt.set(new Date().toLocaleTimeString('zh-CN',{timeZone:'Asia/Shanghai',hour:'2-digit',minute:'2-digit'}));},error:()=>this.balanceError.set('课时余额暂时无法更新')});}
|
|
|
|
|
|
|
+ retryStudentHome(): void {
|
|
|
|
|
+ this.retryStudentStats();
|
|
|
|
|
+ this.retryAntiForget();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ retryStudentStats(): void {
|
|
|
|
|
+ const uid = this.session.user()?.userId;
|
|
|
|
|
+ if (uid) { this.loadStats(uid, true); this.loadBalances(uid, true); }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private loadBalances(uid: string | number, force = false): void {
|
|
|
|
|
+ this.balanceLoading.set(true);
|
|
|
|
|
+ this.requests.add(this.dailyCache.load('balances', () =>
|
|
|
|
|
+ this.api.get<{ profile: Record<string, unknown> }>('app_account_overview', { uid, refresh: true }).pipe(map(({ result }) => result.profile)), force,
|
|
|
|
|
+ ).pipe(finalize(() => this.balanceLoading.set(false))).subscribe({
|
|
|
|
|
+ next: ({ value, updatedAt }) => {
|
|
|
|
|
+ this.balances.set(value);
|
|
|
|
|
+ this.balanceError.set('');
|
|
|
|
|
+ this.balanceRefreshedAt.set(new Date(updatedAt).toLocaleTimeString('zh-CN', { timeZone: 'Asia/Shanghai', hour: '2-digit', minute: '2-digit' }));
|
|
|
|
|
+ },
|
|
|
|
|
+ error: () => this.balanceError.set('课时余额暂时无法更新'),
|
|
|
|
|
+ }));
|
|
|
|
|
+ }
|
|
|
stat(key:string):string|number {const value=this.stats()[key];return value!=null&&Number.isFinite(Number(value))?Number(value):'—';}
|
|
stat(key:string):string|number {const value=this.stats()[key];return value!=null&&Number.isFinite(Number(value))?Number(value):'—';}
|
|
|
balance(...keys:string[]):string|number {const p=this.balances();if(!p)return '—';for(const key of keys)if(p[key]!=null&&p[key]!==''&&Number.isFinite(Number(p[key])))return Number(p[key]);return '—';}
|
|
balance(...keys:string[]):string|number {const p=this.balances();if(!p)return '—';for(const key of keys)if(p[key]!=null&&p[key]!==''&&Number.isFinite(Number(p[key])))return Number(p[key]);return '—';}
|
|
|
|
|
|