|
@@ -1,6 +1,7 @@
|
|
|
import { LessonRecordDetailComponent } from './lesson-record-detail.component';
|
|
import { LessonRecordDetailComponent } from './lesson-record-detail.component';
|
|
|
import { AnchoredMenuDirective } from '../shared/anchored-menu.directive';
|
|
import { AnchoredMenuDirective } from '../shared/anchored-menu.directive';
|
|
|
import { DropdownDirective } from '../shared/dropdown.directive';
|
|
import { DropdownDirective } from '../shared/dropdown.directive';
|
|
|
|
|
+import { skip, Subscription } from 'rxjs';
|
|
|
import { ActivatedRoute } from '@angular/router';
|
|
import { ActivatedRoute } from '@angular/router';
|
|
|
import { CommonModule } from '@angular/common';
|
|
import { CommonModule } from '@angular/common';
|
|
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core';
|
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, inject, signal } from '@angular/core';
|
|
@@ -166,10 +167,53 @@ export class OperationsScheduleComponent implements OnInit, OnDestroy {
|
|
|
}catch(error){this.detailError.set(error instanceof Error?error.message:'取消排课失败');}
|
|
}catch(error){this.detailError.set(error instanceof Error?error.message:'取消排课失败');}
|
|
|
finally{this.saving.set(false);}
|
|
finally{this.saving.set(false);}
|
|
|
}
|
|
}
|
|
|
- ngOnInit():void{void this.load().then(()=>{const id=Number(this.route.snapshot.queryParamMap.get('studentId'));const member=this.data()?.members.find(m=>m.userId===id);if(member){this.openCreate();this.chooseMember(member);}});this.stopLiveRefresh=this.liveRefresh.watch(['courses','appointments','account'],()=>{if(this.loading()||this.saving()||this.searching())return false;void this.load(true);return true;});}
|
|
|
|
|
- ngOnDestroy():void{this.stopLiveRefresh?.();}
|
|
|
|
|
|
|
+ private readonly navigationSubscriptions = new Subscription();
|
|
|
|
|
+ private dataRevision = 0;
|
|
|
|
|
+ private loadedRevision = -1;
|
|
|
|
|
+ private activationQueued = false;
|
|
|
|
|
+ private attachedIntent = false;
|
|
|
|
|
+ private handledStudentId = 0;
|
|
|
|
|
+ private destroyed = false;
|
|
|
|
|
+ private activationRequest = 0;
|
|
|
|
|
+
|
|
|
|
|
+ ngOnInit():void {
|
|
|
|
|
+ // Detached menu views must remember writes made on the member/credit pages.
|
|
|
|
|
+ this.navigationSubscriptions.add(this.operations.dataChanges$?.subscribe(() => this.dataRevision++));
|
|
|
|
|
+ this.navigationSubscriptions.add(this.route.queryParamMap?.pipe(skip(1)).subscribe(() => this.queueActivation()));
|
|
|
|
|
+ void this.activateSchedule();
|
|
|
|
|
+ this.stopLiveRefresh=this.liveRefresh.watch(['courses','appointments','account'],()=>{if(this.loading()||this.saving()||this.searching())return false;void this.load(true);return true;});
|
|
|
|
|
+ }
|
|
|
|
|
+ onOperationsRouteAttach():void {
|
|
|
|
|
+ this.attachedIntent = true;
|
|
|
|
|
+ this.queueActivation();
|
|
|
|
|
+ }
|
|
|
|
|
+ private queueActivation():void {
|
|
|
|
|
+ if (this.activationQueued || this.destroyed) return;
|
|
|
|
|
+ this.activationQueued = true;
|
|
|
|
|
+ // The router installs the new query parameters after retrieving a cached view.
|
|
|
|
|
+ queueMicrotask(() => {
|
|
|
|
|
+ this.activationQueued = false;
|
|
|
|
|
+ if (!this.destroyed) void this.activateSchedule();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ private async activateSchedule():Promise<void> {
|
|
|
|
|
+ const request = ++this.activationRequest;
|
|
|
|
|
+ const id = Number(this.route.snapshot.queryParamMap.get('studentId'));
|
|
|
|
|
+ const selectMember = id > 0 && (this.attachedIntent || id !== this.handledStudentId);
|
|
|
|
|
+ this.attachedIntent = false;
|
|
|
|
|
+ if (!this.data() || this.loadedRevision !== this.dataRevision || selectMember) await this.load();
|
|
|
|
|
+ if (this.destroyed || request !== this.activationRequest || this.error()) return;
|
|
|
|
|
+ this.handledStudentId = id;
|
|
|
|
|
+ if (!selectMember) return;
|
|
|
|
|
+ const member = this.data()?.members.find(row => row.userId === id);
|
|
|
|
|
+ if (!member) { this.error.set('未找到该学员的最新排课资料,请刷新后重试'); return; }
|
|
|
|
|
+ this.openCreate();
|
|
|
|
|
+ this.chooseMember(member);
|
|
|
|
|
+ }
|
|
|
|
|
+ ngOnDestroy():void {this.destroyed=true;this.navigationSubscriptions.unsubscribe();this.stopLiveRefresh?.();}
|
|
|
|
|
+
|
|
|
range():{from:string;to:string}{const from=new Date(`${this.date()}T00:00:00`);const to=new Date(from);if(this.view()==='week')to.setDate(to.getDate()+6);return{from:localDateKey(from),to:localDateKey(to)};}
|
|
range():{from:string;to:string}{const from=new Date(`${this.date()}T00:00:00`);const to=new Date(from);if(this.view()==='week')to.setDate(to.getDate()+6);return{from:localDateKey(from),to:localDateKey(to)};}
|
|
|
- async load(background=false):Promise<void>{const request=++this.loadRequest;if(!background)this.loading.set(true);this.error.set('');try{const range=this.range();const result=await this.operations.schedule(range.from,range.to,this.coachFilter(),!background);if(request!==this.loadRequest)return;if(!background&&this.initialRange&&!result.appointments.length&&result.dataDateTo&&result.dataDateTo!==this.date()){this.initialRange=false;this.date.set(result.dataDateTo);const latestRange=this.range(),latest=await this.operations.schedule(latestRange.from,latestRange.to,this.coachFilter(),true);if(request!==this.loadRequest)return;this.data.set(latest);this.message.set(`当前日期没有排课,已自动定位到最近有数据的 ${result.dataDateTo}`);}else{this.initialRange=false;this.data.set(result);}}catch(error){if(request===this.loadRequest)this.error.set(error instanceof Error?error.message:'排课数据加载失败');}finally{if(!background&&request===this.loadRequest)this.loading.set(false);}}
|
|
|
|
|
|
|
+ async load(background=false):Promise<void>{const request=++this.loadRequest,revision=this.dataRevision;if(!background)this.loading.set(true);this.error.set('');try{const range=this.range();const result=await this.operations.schedule(range.from,range.to,this.coachFilter(),!background);if(request!==this.loadRequest)return;if(!background&&this.initialRange&&!result.appointments.length&&result.dataDateTo&&result.dataDateTo!==this.date()){this.initialRange=false;this.date.set(result.dataDateTo);const latestRange=this.range(),latest=await this.operations.schedule(latestRange.from,latestRange.to,this.coachFilter(),true);if(request!==this.loadRequest)return;this.data.set(latest);this.loadedRevision=revision;this.message.set(`当前日期没有排课,已自动定位到最近有数据的 ${result.dataDateTo}`);}else{this.initialRange=false;this.data.set(result);this.loadedRevision=revision;}}catch(error){if(request===this.loadRequest)this.error.set(error instanceof Error?error.message:'排课数据加载失败');}finally{if(!background&&request===this.loadRequest)this.loading.set(false);}}
|
|
|
move(days:number):void{const value=new Date(`${this.date()}T00:00:00`);value.setDate(value.getDate()+days);this.date.set(localDateKey(value));void this.load();}
|
|
move(days:number):void{const value=new Date(`${this.date()}T00:00:00`);value.setDate(value.getDate()+days);this.date.set(localDateKey(value));void this.load();}
|
|
|
showLatest():void{const latest=this.data()?.dataDateTo;if(!latest)return;this.date.set(latest);void this.load();}
|
|
showLatest():void{const latest=this.data()?.dataDateTo;if(!latest)return;this.date.set(latest);void this.load();}
|
|
|
async applyScheduleFilters():Promise<void>{if(this.searching())return;this.searching.set(true);this.scheduleSearch.set(this.scheduleSearchInput().trim());this.statusFilter.set(this.statusFilterInput());await new Promise(resolve=>setTimeout(resolve,220));this.searching.set(false);}
|
|
async applyScheduleFilters():Promise<void>{if(this.searching())return;this.searching.set(true);this.scheduleSearch.set(this.scheduleSearchInput().trim());this.statusFilter.set(this.statusFilterInput());await new Promise(resolve=>setTimeout(resolve,220));this.searching.set(false);}
|