|
@@ -11,8 +11,11 @@ import { EnrollmentOptions } from '../member-enrollment.models';
|
|
|
@if(options();as value){<div class="form-grid"><label>课程分类<select [ngModel]="category" (ngModelChange)="changeCategory($event)" [disabled]="saving()">@for(c of value.categories;track c.key){<option [value]="c.key">{{c.name}}</option>}</select></label><label>搜索课程<input [disabled]="saving()" [ngModel]="search()" (ngModelChange)="search.set($event)" placeholder="课程名称或 ID" /></label><label>学阶<select [disabled]="saving() || !!categoryStage()" [ngModel]="stage()" (ngModelChange)="stage.set($event)"><option value="">全部</option><option value="primary">小学</option><option value="middle">初中</option><option value="high">高中</option><option value="other">其他</option></select></label></div>
|
|
@if(options();as value){<div class="form-grid"><label>课程分类<select [ngModel]="category" (ngModelChange)="changeCategory($event)" [disabled]="saving()">@for(c of value.categories;track c.key){<option [value]="c.key">{{c.name}}</option>}</select></label><label>搜索课程<input [disabled]="saving()" [ngModel]="search()" (ngModelChange)="search.set($event)" placeholder="课程名称或 ID" /></label><label>学阶<select [disabled]="saving() || !!categoryStage()" [ngModel]="stage()" (ngModelChange)="stage.set($event)"><option value="">全部</option><option value="primary">小学</option><option value="middle">初中</option><option value="high">高中</option><option value="other">其他</option></select></label></div>
|
|
|
@if(value.reason){<p class="error">{{value.reason}}</p>}
|
|
@if(value.reason){<p class="error">{{value.reason}}</p>}
|
|
|
<div class="rules">@for(r of value.rules;track r.objectId){<span>{{r.durationMinutes}}分钟 · {{r.deliveryLabel}}:@for(c of r.creditCosts;track c.account){ {{c.label}} {{c.amount}} }({{r.affordable?'课时足够':'课时不足'}})</span>}</div>
|
|
<div class="rules">@for(r of value.rules;track r.objectId){<span>{{r.durationMinutes}}分钟 · {{r.deliveryLabel}}:@for(c of r.creditCosts;track c.account){ {{c.label}} {{c.amount}} }({{r.affordable?'课时足够':'课时不足'}})</span>}</div>
|
|
|
-<div class="scroll"><table><thead><tr><th>选择</th><th>课程内容</th><th>学阶</th><th>单词数量</th><th>状态</th></tr></thead><tbody>@for(c of filtered();track c.courseId){<tr><td><input type="checkbox" [attr.aria-label]="'选择'+c.courseName" [checked]="selected().includes(c.courseId)" (change)="toggle(c.courseId)" [disabled]="saving()||loading()||!c.eligible||c.alreadyOpened" /></td><td>{{c.courseName}}<small>ID {{c.courseId}}</small></td><td>{{c.stageName}}</td><td>{{c.wordCount}}</td><td>{{c.alreadyOpened?'该分类已开课':c.reason||'可开课'}}</td></tr>}@empty{<tr><td colspan="5">该分类下暂无匹配内容,请检查课程目录和学阶。</td></tr>}</tbody></table></div>
|
|
|
|
|
|
|
+<div class="selection-bar"><span>最新课程在前 · 点击课程行可选择</span><span aria-live="polite">已选 {{selected().length}} 项</span><button type="button" (click)="clearSelection()" [disabled]="saving()||!selected().length">清空选择</button></div>
|
|
|
|
|
+@if(selectedCourses().length){<div class="selected-courses" aria-label="已选课程">@for(c of selectedCourses();track c.courseId){<button type="button" (click)="toggle(c.courseId)" [disabled]="saving()||loading()" [attr.aria-label]="'取消选择'+c.courseName">{{c.courseName}} <span aria-hidden="true">×</span></button>}</div>}
|
|
|
<div class="form-actions"><label>开课原因<input [(ngModel)]="reason" maxlength="500" [disabled]="saving()" /></label><button class="primary" type="button" (click)="submit()" [disabled]="saving()||loading()||!value.eligible||!selected().length">{{saving()?'正在开课…':'确认开课('+selected().length+'项)'}}</button></div>
|
|
<div class="form-actions"><label>开课原因<input [(ngModel)]="reason" maxlength="500" [disabled]="saving()" /></label><button class="primary" type="button" (click)="submit()" [disabled]="saving()||loading()||!value.eligible||!selected().length">{{saving()?'正在开课…':'确认开课('+selected().length+'项)'}}</button></div>
|
|
|
|
|
+<div class="scroll"><table><thead><tr><th>选择</th><th>课程内容</th><th>学阶</th><th>单词数量</th><th>状态</th></tr></thead><tbody>@for(c of filtered();track c.courseId){<tr [class.selectable]="c.eligible&&!c.alreadyOpened&&!saving()&&!loading()" [class.selected]="selected().includes(c.courseId)" (click)="toggle(c.courseId)"><td><input type="checkbox" [attr.aria-label]="'选择'+c.courseName" [checked]="selected().includes(c.courseId)" (click)="$event.stopPropagation()" (change)="toggle(c.courseId)" [disabled]="saving()||loading()||!c.eligible||c.alreadyOpened" /></td><td>{{c.courseName}}<small>ID {{c.courseId}}</small></td><td>{{c.stageName}}</td><td>{{c.wordCount}}</td><td>{{c.alreadyOpened?'该分类已开课':c.reason||'可开课'}}</td></tr>}@empty{<tr><td colspan="5">该分类下暂无匹配内容,请检查课程目录和学阶。</td></tr>}</tbody></table></div>
|
|
|
|
|
+
|
|
|
}</section>`,styleUrls:['./member-enrollment.scss']})
|
|
}</section>`,styleUrls:['./member-enrollment.scss']})
|
|
|
export class MemberEnrollmentComponent implements OnChanges{
|
|
export class MemberEnrollmentComponent implements OnChanges{
|
|
|
@Input({required:true}) studentId=0;@Input() refreshVersion=0;@Output() changed=new EventEmitter<void>();private operations=inject(OperationsService);
|
|
@Input({required:true}) studentId=0;@Input() refreshVersion=0;@Output() changed=new EventEmitter<void>();private operations=inject(OperationsService);
|
|
@@ -20,7 +23,9 @@ export class MemberEnrollmentComponent implements OnChanges{
|
|
|
categoryStage(){return ({primary_writing:'primary',middle_writing:'middle',high_writing:'high'} as Record<string,string>)[this.category]||'';}
|
|
categoryStage(){return ({primary_writing:'primary',middle_writing:'middle',high_writing:'high'} as Record<string,string>)[this.category]||'';}
|
|
|
filtered=computed(()=>(this.options()?.items||[]).filter(c=>(!this.stage()||c.stageKey===this.stage())&&`${c.courseName} ${c.courseId}`.toLowerCase().includes(this.search().trim().toLowerCase())));
|
|
filtered=computed(()=>(this.options()?.items||[]).filter(c=>(!this.stage()||c.stageKey===this.stage())&&`${c.courseName} ${c.courseId}`.toLowerCase().includes(this.search().trim().toLowerCase())));
|
|
|
ngOnChanges(){void this.load();}changeCategory(value:string){this.category=value;this.options.set(null);this.selected.set([]);this.stage.set(this.categoryStage());this.message.set('');void this.load();}
|
|
ngOnChanges(){void this.load();}changeCategory(value:string){this.category=value;this.options.set(null);this.selected.set([]);this.stage.set(this.categoryStage());this.message.set('');void this.load();}
|
|
|
- toggle(id:number){this.selected.update(ids=>ids.includes(id)?ids.filter(x=>x!==id):[...ids,id]);}
|
|
|
|
|
- async load(){if(!this.studentId)return;const serial=++this.serial;this.loading.set(true);try{const result=await this.operations.request<EnrollmentOptions>('ops/course-bindings/options',{studentId:this.studentId,categoryKey:this.category});if(serial===this.serial){this.options.set(result);this.error.set('');}}catch(e){if(serial===this.serial){this.options.set(null);this.error.set(e instanceof Error?e.message:'开课内容加载失败');}}finally{if(serial===this.serial)this.loading.set(false);}}
|
|
|
|
|
|
|
+ selectedCourses=computed(()=>(this.options()?.items||[]).filter(c=>this.selected().includes(c.courseId)));
|
|
|
|
|
+ clearSelection(){if(!this.saving())this.selected.set([]);}
|
|
|
|
|
+ toggle(id:number){const course=this.options()?.items.find(c=>c.courseId===id);if(this.saving()||this.loading()||!course?.eligible||course.alreadyOpened)return;this.selected.update(ids=>ids.includes(id)?ids.filter(x=>x!==id):[...ids,id]);}
|
|
|
|
|
+ async load(){if(!this.studentId)return;const serial=++this.serial;this.loading.set(true);try{const result=await this.operations.request<EnrollmentOptions>('ops/course-bindings/options',{studentId:this.studentId,categoryKey:this.category});if(serial===this.serial){this.options.set(result);this.selected.update(ids=>ids.filter(id=>result.items.some(c=>c.courseId===id&&c.eligible&&!c.alreadyOpened)));this.error.set('');}}catch(e){if(serial===this.serial){this.options.set(null);this.error.set(e instanceof Error?e.message:'开课内容加载失败');}}finally{if(serial===this.serial)this.loading.set(false);}}
|
|
|
async submit(){if(this.saving()||this.loading()||!this.selected().length||!this.options()?.eligible||this.options()?.categoryKey!==this.category)return;if(this.reason.trim().length<2){this.error.set('请填写操作原因');return;}const payload={studentId:this.studentId,categoryKey:this.category,courseIds:[...this.selected()].sort((a,b)=>a-b)},f=JSON.stringify([payload,this.reason]);if(f!==this.fingerprint){this.fingerprint=f;this.key=crypto.randomUUID();}this.saving.set(true);this.error.set('');try{await this.operations.mutate('ops/course-bindings/save','',this.reason,payload,'',this.key);this.selected.set([]);this.fingerprint='';this.message.set('开课成功,可前往排课');await this.load();this.changed.emit();}catch(e){this.error.set(e instanceof Error?e.message:'开课失败');}finally{this.saving.set(false);}}
|
|
async submit(){if(this.saving()||this.loading()||!this.selected().length||!this.options()?.eligible||this.options()?.categoryKey!==this.category)return;if(this.reason.trim().length<2){this.error.set('请填写操作原因');return;}const payload={studentId:this.studentId,categoryKey:this.category,courseIds:[...this.selected()].sort((a,b)=>a-b)},f=JSON.stringify([payload,this.reason]);if(f!==this.fingerprint){this.fingerprint=f;this.key=crypto.randomUUID();}this.saving.set(true);this.error.set('');try{await this.operations.mutate('ops/course-bindings/save','',this.reason,payload,'',this.key);this.selected.set([]);this.fingerprint='';this.message.set('开课成功,可前往排课');await this.load();this.changed.emit();}catch(e){this.error.set(e instanceof Error?e.message:'开课失败');}finally{this.saving.set(false);}}
|
|
|
}
|
|
}
|