|
|
@@ -28,6 +28,11 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
readonly practiceRecord = signal<SourceItem>({});
|
|
|
readonly learningRecord = signal<SourceItem>({});
|
|
|
readonly courses = signal<SourceItem[]>([]);
|
|
|
+ readonly coursesLoading = signal(false);
|
|
|
+ readonly coursesError = signal('');
|
|
|
+ readonly unitsLoading = signal(false);
|
|
|
+ private courseRequest?: Subscription;
|
|
|
+ private unitRequest?: Subscription;
|
|
|
readonly units = signal<SourceItem[]>([]);
|
|
|
readonly selectedUnits = signal<Set<string>>(new Set());
|
|
|
readonly completedUnits = signal<Set<string>>(new Set());
|
|
|
@@ -48,6 +53,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
readonly feedback=computed(()=>this.order()['feedback'] as Record<string,unknown>|null);
|
|
|
selectedCourse = '';
|
|
|
+ selectedBindingId = '';
|
|
|
coachComment = '';
|
|
|
contentSummary = '';
|
|
|
reviewStatus = '';
|
|
|
@@ -78,7 +84,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
this.stopLive = this.liveData.watch({scopes:['appointments'],studentId:()=>undefined,appointmentPolling:true,onRefresh:()=>{if(!this.submitting())this.load(true);}});
|
|
|
}
|
|
|
|
|
|
- ngOnDestroy(): void { this.stopLive?.(); this.loadRequest?.unsubscribe(); }
|
|
|
+ ngOnDestroy(): void { this.stopLive?.(); this.loadRequest?.unsubscribe(); this.courseRequest?.unsubscribe(); this.unitRequest?.unsubscribe(); }
|
|
|
|
|
|
can(action: string): boolean { return this.detailVerified() && !this.order()['cancelled'] && Array.isArray(this.order()['allowedActions']) && (this.order()['allowedActions'] as string[]).includes(action); }
|
|
|
|
|
|
@@ -86,7 +92,8 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
get isStudentView(): boolean { return this.meta.path === 'pages/shop/stu_get_order'; }
|
|
|
get orderId(): string { return this.route.snapshot.queryParamMap.get('id') ?? this.route.snapshot.queryParamMap.get('cart') ?? ''; }
|
|
|
- get isWriting(): boolean { return ['primary_writing','middle_writing','high_writing'].includes(String(this.order()['courseCategoryKey'] || '')) || String(this.order()['if_writing'] ?? '') === '1'; }
|
|
|
+ get selectedCourseItem(): SourceItem | undefined { return this.courses().find(row => String(row['bindingId']) === this.selectedBindingId); }
|
|
|
+ get isWriting(): boolean { return ['primary_writing','middle_writing','high_writing'].includes(String(this.selectedCourseItem?.['categoryKey'] || this.order()['courseCategoryKey'] || '')); }
|
|
|
|
|
|
load(background = false): void {
|
|
|
this.loadRequest?.unsubscribe();
|
|
|
@@ -107,7 +114,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
// Older servers already support starting lessons. Missing new workflow metadata
|
|
|
// must not disable that endpoint; never derive write access from the cached row.
|
|
|
if (!Array.isArray(detail['allowedActions'])) Object.assign(detail, this.legacyStartActions(detail));
|
|
|
- if (background) { this.order.set({...this.order(),...detail}); return; }
|
|
|
+ if (background) { const changed = this.order()['bindingId'] !== detail['bindingId']; this.order.set({...this.order(),...detail}); if (changed && !this.isStudentView) this.loadCourses(); return; }
|
|
|
this.applyOrder({ ...fallback, ...detail });
|
|
|
},
|
|
|
error: (error: ApiError) => {
|
|
|
@@ -163,17 +170,26 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
private applyOrder(order: SourceItem): void {
|
|
|
this.order.set(order);
|
|
|
this.syncAppointmentStudent(order);
|
|
|
- this.selectedCourse = String(order['kcid'] ?? '');
|
|
|
+ if (!this.selectedBindingId) { this.selectedCourse = String(order['kcid'] ?? ''); this.selectedBindingId = String(order['bindingId'] || ''); }
|
|
|
if (!this.coachComment) this.coachComment = String(order['pldp'] ?? '');
|
|
|
this.loadPracticeRecord();
|
|
|
if (!this.isStudentView) {
|
|
|
- this.loadCourses();
|
|
|
+ if (this.status() <= 10) this.loadCourses();
|
|
|
this.loadLearningRecord();
|
|
|
- if (this.status() <= 10 && this.selectedCourse) this.loadUnits();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- chooseCourse(): void { this.platform.toast('如需改课,请由管理员调整排课', 'error'); }
|
|
|
+ chooseCourse(): void {
|
|
|
+ this.unitRequest?.unsubscribe();
|
|
|
+ this.units.set([]); this.selectedUnits.set(new Set()); this.completedUnits.set(new Set()); this.bindingRecord.set({});
|
|
|
+ this.actionError.set('');
|
|
|
+ const course = this.selectedCourseItem;
|
|
|
+ this.selectedCourse = String(course?.['kcid'] || '');
|
|
|
+ if (!course) return;
|
|
|
+ this.bindingRecord.set(course);
|
|
|
+ this.completedUnits.set(new Set(Object.keys(this.parseRecord(course['syjd']))));
|
|
|
+ if (!this.isWriting) this.loadUnits();
|
|
|
+ }
|
|
|
|
|
|
toggleUnit(unit: SourceItem): void {
|
|
|
const id = String(unit['NodeID'] ?? unit['ID'] ?? '');
|
|
|
@@ -186,7 +202,9 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
|
|
|
async markUnitComplete(unit: SourceItem): Promise<void> {
|
|
|
if (!this.detailVerified() || (!this.can('start') && !this.can('continue'))) return;
|
|
|
+ const selectedBindingId = this.selectedBindingId;
|
|
|
if (!(await this.platform.confirm(`确认将“${this.unitTitle(unit)}”标记为已学完?`))) return;
|
|
|
+ if (selectedBindingId !== this.selectedBindingId) return;
|
|
|
const id = String(unit['NodeID'] ?? unit['ID'] ?? '');
|
|
|
const binding = this.bindingRecord();
|
|
|
if (!binding['GeneralID']) {
|
|
|
@@ -199,7 +217,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
content: JSON.stringify({ GeneralID: binding['GeneralID'], ModelID: 58 }),
|
|
|
addon: JSON.stringify({ syjd: JSON.stringify(progress) }),
|
|
|
}).subscribe({
|
|
|
- next: () => this.completedUnits.update((current) => new Set(current).add(id)),
|
|
|
+ next: () => { this.courses.update(rows => rows.map(row => String(row['bindingId']) === selectedBindingId ? {...row,syjd:JSON.stringify(progress)} : row)); if (selectedBindingId === this.selectedBindingId) { this.bindingRecord.set({...binding,syjd:JSON.stringify(progress)}); this.completedUnits.update(current => new Set(current).add(id)); } },
|
|
|
error: (error: ApiError) => this.platform.toast(error.message, 'error'),
|
|
|
});
|
|
|
}
|
|
|
@@ -230,6 +248,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
this.actionError.set('');
|
|
|
if (!this.can('start') && !this.can('continue')) { this.actionError.set(String(this.order()['blockedReason'] || '详情未核实,请刷新后重试')); return; }
|
|
|
if(this.order()['cancelled']===true){this.platform.toast('此预约已取消,不能开始课程','error');return;}
|
|
|
+ if (this.coursesLoading() || this.unitsLoading() || !this.selectedCourseItem || this.coursesError()) { this.actionError.set('请先加载并选择该学员已开通的课程'); return; }
|
|
|
if (!this.isWriting && !this.selectedUnits().size) {
|
|
|
this.actionError.set(this.units().length ? '请选择本次学习单元' : '暂无可用学习单元,请刷新重试或联系管理员核对课程内容');
|
|
|
return;
|
|
|
@@ -241,7 +260,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
void this.router.navigate(['/pages/stu/stu_abc'], {
|
|
|
replaceUrl: true,
|
|
|
- queryParams: { nids: [...this.selectedUnits()].join(','), pnid: this.selectedCourse, orderId: this.orderId, bindingId:this.order()['bindingId']||undefined },
|
|
|
+ queryParams: { nids: [...this.selectedUnits()].join(','), pnid: this.selectedCourse, orderId: this.orderId, bindingId:this.selectedBindingId||undefined },
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
@@ -297,7 +316,7 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
private updateOrderStatus(status:number,next:()=>void):void{
|
|
|
if(this.submitting())return;this.submitting.set(true);
|
|
|
- this.api.post('e_order_update_v2',{status,uid:this.session.user()?.userId,content:JSON.stringify({GeneralID:this.orderId})}).pipe(finalize(()=>this.submitting.set(false))).subscribe({next:()=>next(),error:(error:ApiError)=>this.actionError.set(error.message)});
|
|
|
+ this.api.post('e_order_update_v2',{status,uid:this.session.user()?.userId,learningBindingId:this.selectedBindingId,expectedUpdatedAt:this.order()['updatedAt'],content:JSON.stringify({GeneralID:this.orderId})}).pipe(finalize(()=>this.submitting.set(false))).subscribe({next:()=>next(),error:(error:ApiError)=>this.actionError.set(error.message)});
|
|
|
}
|
|
|
|
|
|
private loadPracticeRecord(): void {
|
|
|
@@ -316,54 +335,28 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
}
|
|
|
|
|
|
private loadCourses(): void {
|
|
|
- const studentId = this.appointmentStudentId();
|
|
|
- const currentCourse = this.currentCourseOption();
|
|
|
- if (!studentId) {
|
|
|
- this.courses.set(currentCourse ? [currentCourse] : []);
|
|
|
- return;
|
|
|
- }
|
|
|
- this.api.get<unknown>('content_list', { inputer: '', nid: 28, myfield: `yhid=${studentId}`, orders: 'id=DESC', psize: 200 }).subscribe({
|
|
|
- next: ({ result }) => {
|
|
|
- const courses: SourceItem[] = this.findList(result).map((item): SourceItem => {
|
|
|
- const parts = String(item['Title'] ?? '').split('|');
|
|
|
- return { ...item, Title: parts[parts.length - 1] || '-', kcId: item['kcid'] };
|
|
|
- });
|
|
|
- if (currentCourse && !courses.some((course) => String(course['kcid'] ?? course['kcId'] ?? '') === this.selectedCourse)) {
|
|
|
- courses.unshift(currentCourse);
|
|
|
- }
|
|
|
+ this.courseRequest?.unsubscribe(); this.unitRequest?.unsubscribe();
|
|
|
+ this.coursesLoading.set(true); this.coursesError.set(''); this.units.set([]); this.selectedUnits.set(new Set());
|
|
|
+ this.courseRequest = this.api.get<unknown>('app_lesson_courses', { orderId: this.orderId }).pipe(finalize(() => this.coursesLoading.set(false))).subscribe({
|
|
|
+ next: ({result}) => {
|
|
|
+ const courses = this.findList(result);
|
|
|
this.courses.set(courses);
|
|
|
+ const selected = courses.find(row => String(row['bindingId']) === this.selectedBindingId)
|
|
|
+ || courses.find(row => String(row['bindingId']) === String(this.order()['bindingId'] || ''));
|
|
|
+ this.selectedBindingId = String(selected?.['bindingId'] || '');
|
|
|
+ this.chooseCourse();
|
|
|
},
|
|
|
- error: () => this.courses.set(currentCourse ? [currentCourse] : []),
|
|
|
+ error: (error: ApiError) => { this.courses.set([]); this.selectedBindingId = ''; this.coursesError.set('学员课程加载失败:' + error.message); },
|
|
|
});
|
|
|
}
|
|
|
|
|
|
private loadUnits(): void {
|
|
|
- const studentId = this.appointmentStudentId();
|
|
|
- if (!studentId) {
|
|
|
- this.units.set([]);
|
|
|
- this.platform.toast('预约记录缺少学员 ID,暂时无法加载学习单元', 'error');
|
|
|
- return;
|
|
|
- }
|
|
|
- this.api.get<unknown>('node_list', { pid: this.selectedCourse, bindingId: this.order()['bindingId'] || '', ifunit: 1, userId: studentId }).subscribe({
|
|
|
- next: ({ result }) => {
|
|
|
- this.units.set(this.findList(result));
|
|
|
- this.loadUnitProgress();
|
|
|
- },
|
|
|
- error: (error: ApiError) => this.actionError.set('学习单元加载失败:'+error.message),
|
|
|
- });
|
|
|
- }
|
|
|
-
|
|
|
- private loadUnitProgress(): void {
|
|
|
- const studentId = this.appointmentStudentId();
|
|
|
- if (!studentId) return;
|
|
|
- this.api.post<unknown>('content_list', { ModelID: 58, myfield2: `yhid=${studentId}|kcid=${this.selectedCourse}` }).subscribe({
|
|
|
- next: ({ result }) => {
|
|
|
- const records=this.findList(result),bindingId=String(this.order()['bindingId']||'');
|
|
|
- const record = (bindingId?records.find(row=>String(row['bindingId']||row['id'])===bindingId):records.length===1?records[0]:undefined) ?? {};
|
|
|
- this.bindingRecord.set(record);
|
|
|
- this.completedUnits.set(new Set(Object.keys(this.parseRecord(record['syjd']))));
|
|
|
- },
|
|
|
- error: () => undefined,
|
|
|
+ const studentId = this.appointmentStudentId(), bindingId = this.selectedBindingId;
|
|
|
+ if (!studentId || !bindingId) return;
|
|
|
+ this.unitsLoading.set(true);
|
|
|
+ this.unitRequest = this.api.get<unknown>('node_list', {pid:this.selectedCourse,bindingId,ifunit:1,userId:studentId,psize:1000}).pipe(finalize(() => this.unitsLoading.set(false))).subscribe({
|
|
|
+ next: ({result}) => { if (bindingId === this.selectedBindingId) this.units.set(this.findList(result)); },
|
|
|
+ error: (error: ApiError) => { if (bindingId === this.selectedBindingId) this.actionError.set('学习单元加载失败:' + error.message); },
|
|
|
});
|
|
|
}
|
|
|
|
|
|
@@ -406,15 +399,6 @@ export class AppointmentPageComponent implements OnInit, OnDestroy {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- private currentCourseOption(): SourceItem | null {
|
|
|
- if (!this.selectedCourse) return null;
|
|
|
- return {
|
|
|
- kcid: this.selectedCourse,
|
|
|
- kcId: this.selectedCourse,
|
|
|
- Title: this.field('course') || `课程 ${this.selectedCourse}`,
|
|
|
- };
|
|
|
- }
|
|
|
-
|
|
|
private appointmentSnapshot(): SourceItem {
|
|
|
const state = history.state as { appointmentSnapshot?: unknown } | null;
|
|
|
if (state?.appointmentSnapshot && typeof state.appointmentSnapshot === 'object' && !Array.isArray(state.appointmentSnapshot)) {
|