appointment-page.component.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. import { LessonFeedbackComponent } from './lesson-feedback.component';
  2. import { lessonDateLabel } from './lesson-display';
  3. import { DropdownDirective } from '../../shared/dropdown.directive';
  4. import { CommonModule } from '@angular/common';
  5. import { ChangeDetectionStrategy, Component, computed, effect, ElementRef, inject, OnInit, OnDestroy, signal, viewChild } from '@angular/core';
  6. import { FormsModule } from '@angular/forms';
  7. import { ActivatedRoute, Router } from '@angular/router';
  8. import { BookCheck, Check, ChevronRight, CircleHelp, Clock3, Flag, GraduationCap, LucideAngularModule, Play, RefreshCw, Save, Star, X } from 'lucide-angular';
  9. import { finalize, Subscription } from 'rxjs';
  10. import { LiveDataRefreshService } from '../../core/live-data-refresh.service';
  11. import { ApiError, ApiService } from '../../core/api.service';
  12. import { PageMeta } from '../../core/models';
  13. import { PlatformService } from '../../core/platform.service';
  14. import { SessionService } from '../../core/session.service';
  15. import { appointmentDate, beijingDate } from '../../core/appointment-time';
  16. type SourceItem = Record<string, unknown>;
  17. @Component({
  18. selector: 'app-appointment-page',
  19. standalone: true,
  20. imports: [LessonFeedbackComponent, DropdownDirective, CommonModule, FormsModule, LucideAngularModule],
  21. templateUrl: './appointment-page.component.html',
  22. styleUrl: './appointment-page.component.scss',
  23. changeDetection: ChangeDetectionStrategy.OnPush,
  24. })
  25. export class AppointmentPageComponent implements OnInit, OnDestroy {
  26. readonly meta = inject(ActivatedRoute).snapshot.data['meta'] as PageMeta;
  27. readonly order = signal<SourceItem>({});
  28. readonly practiceRecord = signal<SourceItem>({});
  29. readonly learningRecord = signal<SourceItem>({});
  30. readonly courses = signal<SourceItem[]>([]);
  31. readonly coursesLoading = signal(false);
  32. readonly coursesError = signal('');
  33. readonly unitsLoading = signal(false);
  34. private courseRequest?: Subscription;
  35. private unitRequest?: Subscription;
  36. readonly units = signal<SourceItem[]>([]);
  37. readonly selectedUnits = signal<Set<string>>(new Set());
  38. readonly completedUnits = signal<Set<string>>(new Set());
  39. readonly bindingRecord = signal<SourceItem>({});
  40. readonly loading = signal(true);
  41. readonly submitting = signal(false);
  42. readonly error = signal('');
  43. readonly detailWarning = signal('');
  44. readonly feedbackError = signal('');
  45. readonly editingFeedback = signal(false);
  46. readonly feedbackSaved = signal(false);
  47. private feedbackRevision = '';
  48. private autoEditOpened = false;
  49. readonly dateLabel = lessonDateLabel;
  50. readonly actionError = signal('');
  51. readonly detailVerified = signal(false);
  52. private loadRequest?: Subscription;
  53. private stopLive?: () => void;
  54. private readonly liveData = inject(LiveDataRefreshService);
  55. readonly completionDialog = viewChild<ElementRef<HTMLDialogElement>>('completionDialog');
  56. readonly icons = { BookCheck, Check, ChevronRight, CircleHelp, Clock3, Flag, GraduationCap, Play, RefreshCw, Save, Star, X };
  57. readonly status = computed(() => Number(this.order()['dszt'] ?? this.order()['status'] ?? 0));
  58. readonly feedback=computed(()=>this.order()['feedback'] as Record<string,unknown>|null);
  59. selectedCourse = '';
  60. selectedBindingId = '';
  61. coachComment = '';
  62. contentSummary = '';
  63. reviewStatus = '';
  64. reviewSummary = '';
  65. trainingStatus = '';
  66. trainingSummary = '';
  67. homework = '';
  68. noHomework = false;
  69. notes = '';
  70. studentScore = 5;
  71. studentComment = '';
  72. private readonly route = inject(ActivatedRoute);
  73. private readonly router = inject(Router);
  74. private readonly api = inject(ApiService);
  75. private readonly session = inject(SessionService);
  76. private readonly platform = inject(PlatformService);
  77. constructor() {
  78. effect(() => {
  79. const dialog = this.completionDialog()?.nativeElement;
  80. if (dialog && !this.autoEditOpened && this.can('editFeedback') && this.feedback() && this.route.snapshot.queryParamMap.get('editReport') === '1') { this.autoEditOpened = true; this.openFeedbackEditor(); }
  81. if (dialog && !this.isStudentView && !this.order()['cancelled'] && this.can('complete') && this.route.snapshot.queryParamMap.get('feedback') === '1') dialog.showModal();
  82. }, { allowSignalWrites: true });
  83. }
  84. ngOnInit(): void {
  85. this.load();
  86. this.stopLive = this.liveData.watch({scopes:['appointments'],studentId:()=>undefined,appointmentPolling:true,onRefresh:()=>{if(!this.submitting() && !this.completionDialog()?.nativeElement.open)this.load(true);}});
  87. }
  88. ngOnDestroy(): void { this.stopLive?.(); this.loadRequest?.unsubscribe(); this.courseRequest?.unsubscribe(); this.unitRequest?.unsubscribe(); }
  89. can(action: string): boolean { return this.detailVerified() && !this.order()['cancelled'] && Array.isArray(this.order()['allowedActions']) && (this.order()['allowedActions'] as string[]).includes(action); }
  90. openReport(): void { void this.router.navigate(['/pages/shop/baobiao'], {queryParams:{id:this.orderId}}); }
  91. get isStudentView(): boolean { return this.meta.path === 'pages/shop/stu_get_order'; }
  92. get orderId(): string { return this.route.snapshot.queryParamMap.get('id') ?? this.route.snapshot.queryParamMap.get('cart') ?? ''; }
  93. get selectedCourseItem(): SourceItem | undefined { return this.courses().find(row => String(row['bindingId']) === this.selectedBindingId); }
  94. get isWriting(): boolean { return ['primary_writing','middle_writing','high_writing'].includes(String(this.selectedCourseItem?.['categoryKey'] || this.order()['courseCategoryKey'] || '')); }
  95. load(background = false): void {
  96. this.loadRequest?.unsubscribe();
  97. if (!this.orderId) {
  98. this.loading.set(false);
  99. this.error.set('缺少预约编号');
  100. return;
  101. }
  102. if (!background) { this.loading.set(true); this.detailVerified.set(false); }
  103. this.error.set('');
  104. this.detailWarning.set('');
  105. const fallback = this.appointmentSnapshot();
  106. this.loadRequest = this.api.get<unknown>('e_order_detail', { id: this.orderId }).pipe(finalize(() => this.loading.set(false))).subscribe({
  107. next: ({ result }) => {
  108. const detail = this.findList(result)[0] ?? this.asRecord(result);
  109. this.detailVerified.set(String(detail['GeneralID'] ?? '') === this.orderId);
  110. if (!this.detailVerified()) this.detailWarning.set('未能核实预约详情,请刷新后重试。当前课程仅供查看。');
  111. // Older servers already support starting lessons. Missing new workflow metadata
  112. // must not disable that endpoint; never derive write access from the cached row.
  113. if (!Array.isArray(detail['allowedActions'])) Object.assign(detail, this.legacyStartActions(detail));
  114. if (background) { const changed = this.order()['bindingId'] !== detail['bindingId']; this.order.set({...this.order(),...detail}); if (changed && !this.isStudentView) this.loadCourses(); return; }
  115. this.applyOrder({ ...fallback, ...detail });
  116. },
  117. error: (error: ApiError) => {
  118. this.detailVerified.set(false);
  119. if (background) { this.detailWarning.set('详情更新失败,已暂停操作,请刷新后重试:'+error.message); return; }
  120. if (Object.keys(fallback).length) {
  121. this.applyOrder(fallback);
  122. this.detailWarning.set('详情接口暂时返回异常,当前显示排课列表里的课程信息。你可以返回列表后重试。');
  123. this.loading.set(false);
  124. return;
  125. }
  126. this.error.set(this.friendlyDetailError(error.message));
  127. },
  128. });
  129. }
  130. private legacyStartActions(detail: SourceItem): SourceItem {
  131. const result: SourceItem = { allowedActions: [], blockedReason: '' };
  132. if (!this.detailVerified() || this.isStudentView || detail['cancelled']) return result;
  133. const teacherId = Number(detail['pl']);
  134. if (!teacherId || teacherId !== this.session.user()?.userId) {
  135. return { ...result, blockedReason: '仅本节课的老师可以开始或继续课程' };
  136. }
  137. const status = Number(detail['dszt'] ?? detail['status']);
  138. if (status === 10) return { ...result, allowedActions: ['continue'] };
  139. if (status !== 0) return result;
  140. const date = appointmentDate(detail);
  141. if (!date) return { ...result, blockedReason: '预约日期不完整,请联系管理员核对排课' };
  142. if (date !== beijingDate()) return { ...result, blockedReason: '课程仅在预约当天(北京时间)可开始,其他日期请联系管理员改期' };
  143. return { ...result, allowedActions: ['start'] };
  144. }
  145. field(label: string): string {
  146. const aliases: Record<string, string[]> = {
  147. student: ['HoneyName', 'honeyName', 'honeyname', 'xymz_t'],
  148. course: ['yykcmc', 'kcmc', 'Subtitle', 'ctitle', 'Title'],
  149. type: ['dslx', 'dslx_t', 'kclx'],
  150. date: ['bxrq', 'yydate'],
  151. time: ['sdsd', 'startTime'],
  152. end: ['jieshu', 'endTime'],
  153. delivery: ['jffs'],
  154. coach: ['plxm', 'plName'],
  155. };
  156. if (label === 'type') return String(this.order()['courseCategoryName'] || ({1:'30分钟课',2:'60分钟课',3:'体验课',4:'课型4'} as Record<string,string>)[String(this.order()['dslx'])] || this.order()['dslx_t'] || '课程');
  157. for (const key of aliases[label] ?? []) if (this.order()[key] !== undefined) return String(this.order()[key]);
  158. return '';
  159. }
  160. statusText(): string {
  161. if(this.order()['cancelled'])return '已取消';
  162. return String(this.order()['status_txt'] ?? ({ 0: '待开始', 10: '学习中', 11: '等待学员确认', 20: '学员已确认', 30: '已完成', 40: '已完成' } as Record<number, string>)[this.status()] ?? `状态 ${this.status()}`);
  163. }
  164. private applyOrder(order: SourceItem): void {
  165. this.order.set(order);
  166. this.syncAppointmentStudent(order);
  167. if (!this.selectedBindingId) { this.selectedCourse = String(order['kcid'] ?? ''); this.selectedBindingId = String(order['bindingId'] || ''); }
  168. if (!this.coachComment) this.coachComment = String(order['pldp'] ?? '');
  169. this.loadPracticeRecord();
  170. if (!this.isStudentView) {
  171. if (this.status() <= 10) this.loadCourses();
  172. this.loadLearningRecord();
  173. }
  174. }
  175. chooseCourse(): void {
  176. this.unitRequest?.unsubscribe();
  177. this.units.set([]); this.selectedUnits.set(new Set()); this.completedUnits.set(new Set()); this.bindingRecord.set({});
  178. this.actionError.set('');
  179. const course = this.selectedCourseItem;
  180. this.selectedCourse = String(course?.['kcid'] || '');
  181. if (!course) return;
  182. this.bindingRecord.set(course);
  183. this.completedUnits.set(new Set(Object.keys(this.parseRecord(course['syjd']))));
  184. if (!this.isWriting) this.loadUnits();
  185. }
  186. toggleUnit(unit: SourceItem): void {
  187. const id = String(unit['NodeID'] ?? unit['ID'] ?? '');
  188. this.selectedUnits.update((current) => {
  189. const next = new Set(current);
  190. if (next.has(id)) next.delete(id); else next.add(id);
  191. return next;
  192. });
  193. }
  194. async markUnitComplete(unit: SourceItem): Promise<void> {
  195. if (!this.detailVerified() || (!this.can('start') && !this.can('continue'))) return;
  196. const selectedBindingId = this.selectedBindingId;
  197. if (!(await this.platform.confirm(`确认将“${this.unitTitle(unit)}”标记为已学完?`))) return;
  198. if (selectedBindingId !== this.selectedBindingId) return;
  199. const id = String(unit['NodeID'] ?? unit['ID'] ?? '');
  200. const binding = this.bindingRecord();
  201. if (!binding['GeneralID']) {
  202. this.platform.toast('未找到课程进度记录', 'error');
  203. return;
  204. }
  205. const progress = this.parseRecord(binding['syjd']);
  206. progress[id] = `${id}@${this.unitTitle(unit)}@1`;
  207. this.api.post('content_update', {
  208. content: JSON.stringify({ GeneralID: binding['GeneralID'], ModelID: 58 }),
  209. addon: JSON.stringify({ syjd: JSON.stringify(progress) }),
  210. }).subscribe({
  211. 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)); } },
  212. error: (error: ApiError) => this.platform.toast(error.message, 'error'),
  213. });
  214. }
  215. unitTitle(unit: SourceItem): string {
  216. return String(unit['NodeName'] ?? unit['Title'] ?? '学习单元');
  217. }
  218. unitId(unit: SourceItem): string {
  219. return String(unit['NodeID'] ?? unit['ID'] ?? '');
  220. }
  221. unitProgress(unit: SourceItem): { learned: number; total: number; percent: number } | null {
  222. const rawTotal = unit['total'];
  223. const rawLearned = unit['yx_word'];
  224. if (rawTotal == null || rawTotal === '' || rawLearned == null || rawLearned === '') return null;
  225. const total = Number(rawTotal);
  226. const learned = Number(rawLearned);
  227. if (!Number.isFinite(total) || !Number.isFinite(learned) || total < 0 || learned < 0) return null;
  228. return {
  229. learned: Math.min(learned, total),
  230. total,
  231. percent: total > 0 ? Math.min(100, Math.round(learned / total * 100)) : 0,
  232. };
  233. }
  234. startOrContinue(): void {
  235. this.actionError.set('');
  236. if (!this.can('start') && !this.can('continue')) { this.actionError.set(String(this.order()['blockedReason'] || '详情未核实,请刷新后重试')); return; }
  237. if(this.order()['cancelled']===true){this.platform.toast('此预约已取消,不能开始课程','error');return;}
  238. if (this.coursesLoading() || this.unitsLoading() || !this.selectedCourseItem || this.coursesError()) { this.actionError.set('请先加载并选择该学员已开通的课程'); return; }
  239. if (!this.isWriting && !this.selectedUnits().size) {
  240. this.actionError.set(this.units().length ? '请选择本次学习单元' : '暂无可用学习单元,请刷新重试或联系管理员核对课程内容');
  241. return;
  242. }
  243. this.updateOrderStatus(10, () => {
  244. if (this.isWriting) {
  245. this.load();
  246. return;
  247. }
  248. void this.router.navigate(['/pages/stu/stu_abc'], {
  249. replaceUrl: true,
  250. queryParams: { nids: [...this.selectedUnits()].join(','), pnid: this.selectedCourse, orderId: this.orderId, bindingId:this.selectedBindingId||undefined },
  251. });
  252. });
  253. }
  254. openCompletionFeedback(): void {
  255. if(this.order()['cancelled']===true)return;
  256. if (this.isStudentView || !this.can('complete') || this.submitting()) return;
  257. this.editingFeedback.set(false);
  258. this.feedbackError.set('');
  259. this.completionDialog()?.nativeElement.showModal();
  260. }
  261. openFeedbackEditor(): void {
  262. if (this.isStudentView || !this.can('editFeedback') || !this.feedback() || this.submitting()) return;
  263. const feedback = this.feedback()!;
  264. this.contentSummary = String(feedback['contentSummary'] || '');
  265. this.reviewStatus = String(feedback['reviewStatus'] || '');
  266. this.reviewSummary = String(feedback['reviewSummary'] || '');
  267. this.trainingStatus = String(feedback['trainingStatus'] || '');
  268. this.trainingSummary = String(feedback['trainingSummary'] || '');
  269. this.homework = String(feedback['homework'] || '');
  270. this.noHomework = feedback['noHomework'] === true;
  271. this.coachComment = String(feedback['coachComment'] ?? this.practiceRecord()['pldp'] ?? '');
  272. this.notes = String(feedback['notes'] || '');
  273. this.feedbackRevision = String(feedback['revision'] || feedback['updatedAt'] || '');
  274. this.editingFeedback.set(true);
  275. this.feedbackError.set('');
  276. this.feedbackSaved.set(false);
  277. this.completionDialog()?.nativeElement.showModal();
  278. }
  279. closeCompletionFeedback(event?: Event): void {
  280. event?.preventDefault();
  281. if (!this.submitting()) this.completionDialog()?.nativeElement.close();
  282. }
  283. finishCoachLesson(): void {
  284. if (this.submitting() || !this.can(this.editingFeedback() ? 'editFeedback' : 'complete') || !this.completionDialog()?.nativeElement.open) return;
  285. this.feedbackError.set('');
  286. if (!this.contentSummary.trim()) { this.feedbackError.set('请填写本次学习内容'); return; }
  287. if (!this.trainingSummary.trim()) { this.feedbackError.set('请填写进度说明'); return; }
  288. if (!this.noHomework && !this.homework.trim()) { this.feedbackError.set('请填写作业安排,或选择“无作业”'); return; }
  289. this.submitting.set(true);
  290. const editing = this.editingFeedback();
  291. this.api.post(editing ? 'app_lesson_feedback_update' : 'app_lesson_complete', {
  292. orderId: this.orderId,
  293. expectedUpdatedAt: editing ? this.feedbackRevision : this.order()['updatedAt'],
  294. feedback: JSON.stringify({
  295. contentSummary: this.contentSummary.trim(),
  296. reviewStatus: this.reviewStatus.trim(),
  297. reviewSummary: this.reviewSummary.trim(),
  298. trainingStatus: this.trainingStatus.trim(),
  299. trainingSummary: this.trainingSummary.trim(),
  300. homework: this.noHomework ? '无作业' : this.homework.trim(),
  301. noHomework: this.noHomework,
  302. notes: this.notes.trim(),
  303. coachComment: this.coachComment.trim(),
  304. }),
  305. }).pipe(finalize(() => this.submitting.set(false))).subscribe({
  306. next: () => { this.completionDialog()?.nativeElement.close(); this.feedbackSaved.set(editing); this.platform.toast(editing ? '报告修改已保存' : '课程已结课,学习报告已生成'); this.load(); },
  307. error: (error: ApiError) => this.feedbackError.set(error.message),
  308. });
  309. }
  310. finishStudentLesson(): void {
  311. if(this.submitting() || !this.can('review')) return;
  312. this.actionError.set('');
  313. if (!Number.isInteger(this.studentScore) || this.studentScore < 1 || this.studentScore > 5) { this.actionError.set('评分应为1至5分'); return; }
  314. this.submitting.set(true);
  315. this.api.post('app_lesson_review',{orderId:this.orderId,score:this.studentScore,comment:this.studentComment.trim()}).pipe(finalize(()=>this.submitting.set(false))).subscribe({
  316. next:()=>{this.platform.toast('评价已提交');this.load();}, error:(error:ApiError)=>this.actionError.set(error.message),
  317. });
  318. }
  319. private updateOrderStatus(status:number,next:()=>void):void{
  320. if(this.submitting())return;this.submitting.set(true);
  321. 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)});
  322. }
  323. private loadPracticeRecord(): void {
  324. const user = this.session.user();
  325. const targetField = this.isStudentView ? `yyds=${this.orderId}|xymz=${user?.userId}` : `yyds=${this.orderId}|jsmz=${user?.userId}`;
  326. this.api.get<unknown>('content_list', { ModelID: 59, psize: 2, myfield2: targetField }).subscribe({
  327. next: ({ result }) => {
  328. const records=this.findList(result),bindingId=String(this.order()['bindingId']||'');
  329. const record = records.find(row=>String(row['yyds']||'')===this.orderId)|| (records.length===1?records[0]:undefined) || {};
  330. this.practiceRecord.set(record);
  331. if (!this.coachComment) this.coachComment = String(record['pldp'] ?? '');
  332. if (Number(record['pf']) > 0) { this.studentScore = Number(record['pf']); this.studentComment = String(record['pjnr'] ?? ''); }
  333. },
  334. error: () => undefined,
  335. });
  336. }
  337. private loadCourses(): void {
  338. this.courseRequest?.unsubscribe(); this.unitRequest?.unsubscribe();
  339. this.coursesLoading.set(true); this.coursesError.set(''); this.units.set([]); this.selectedUnits.set(new Set());
  340. this.courseRequest = this.api.get<unknown>('app_lesson_courses', { orderId: this.orderId }).pipe(finalize(() => this.coursesLoading.set(false))).subscribe({
  341. next: ({result}) => {
  342. const courses = this.findList(result);
  343. this.courses.set(courses);
  344. const selected = courses.find(row => String(row['bindingId']) === this.selectedBindingId)
  345. || courses.find(row => String(row['bindingId']) === String(this.order()['bindingId'] || ''));
  346. this.selectedBindingId = String(selected?.['bindingId'] || '');
  347. this.chooseCourse();
  348. },
  349. error: (error: ApiError) => { this.courses.set([]); this.selectedBindingId = ''; this.coursesError.set('学员课程加载失败:' + error.message); },
  350. });
  351. }
  352. private loadUnits(): void {
  353. const studentId = this.appointmentStudentId(), bindingId = this.selectedBindingId;
  354. if (!studentId || !bindingId) return;
  355. this.unitsLoading.set(true);
  356. 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({
  357. next: ({result}) => { if (bindingId === this.selectedBindingId) this.units.set(this.findList(result)); },
  358. error: (error: ApiError) => { if (bindingId === this.selectedBindingId) this.actionError.set('学习单元加载失败:' + error.message); },
  359. });
  360. }
  361. private loadLearningRecord(): void {
  362. const studentId = this.appointmentStudentId();
  363. if (!studentId) return;
  364. this.api.get<unknown>('content_list', { ModelID: 56, psize: 1, myfield: `dsid=${this.orderId}`, myfield2: `UserId=${studentId}|pl=${this.session.user()?.userId}` }).subscribe({
  365. next: ({ result }) => this.learningRecord.set(this.findList(result)[0] ?? {}),
  366. error: () => undefined,
  367. });
  368. }
  369. private parseRecord(value: unknown): Record<string, string> {
  370. if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, string>;
  371. if (typeof value !== 'string' || !value) return {};
  372. try { return JSON.parse(value) as Record<string, string>; } catch { return {}; }
  373. }
  374. private findList(value: unknown): SourceItem[] {
  375. if (Array.isArray(value)) return value as SourceItem[];
  376. if (value && typeof value === 'object') for (const child of Object.values(value)) if (Array.isArray(child)) return child as SourceItem[];
  377. return [];
  378. }
  379. private asRecord(value: unknown): SourceItem {
  380. return value && typeof value === 'object' && !Array.isArray(value) ? value as SourceItem : {};
  381. }
  382. private appointmentStudentId(): number {
  383. return Number(this.order()['szyh'] ?? this.order()['yhid'] ?? this.session.student()?.userId ?? 0);
  384. }
  385. private syncAppointmentStudent(order: SourceItem): void {
  386. const studentId = Number(order['szyh'] ?? order['yhid'] ?? 0);
  387. if (!studentId || this.session.student()?.userId === studentId) return;
  388. this.session.setStudent({
  389. userId: studentId,
  390. honeyName: String(order['HoneyName'] ?? order['honeyName'] ?? order['honeyname'] ?? order['xymz_t'] ?? '学员'),
  391. groupId: 1,
  392. });
  393. }
  394. private appointmentSnapshot(): SourceItem {
  395. const state = history.state as { appointmentSnapshot?: unknown } | null;
  396. if (state?.appointmentSnapshot && typeof state.appointmentSnapshot === 'object' && !Array.isArray(state.appointmentSnapshot)) {
  397. return state.appointmentSnapshot as SourceItem;
  398. }
  399. try {
  400. const stored = sessionStorage.getItem(`xiaoshu.appointment.snapshot.${this.orderId}`);
  401. const parsed = stored ? JSON.parse(stored) as unknown : null;
  402. return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as SourceItem : {};
  403. } catch {
  404. return {};
  405. }
  406. }
  407. private friendlyDetailError(message: string): string {
  408. return message.includes('Object reference not set to an instance of an object')
  409. ? '课程详情暂时无法加载,请返回排课列表重试。'
  410. : message;
  411. }
  412. private dateOnly(): string {
  413. const now = new Date();
  414. return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
  415. }
  416. private dateTime(): string {
  417. const now = new Date();
  418. return `${this.dateOnly()} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
  419. }
  420. }