appointment-page.component.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. <section class="appointment-page">
  2. @if (loading()) {
  3. <div class="empty-state"><p>正在加载预约...</p></div>
  4. } @else if (error()) {
  5. <div class="empty-state error"><div><lucide-icon [img]="icons.CircleHelp" [size]="30" /><p>{{ error() }}</p><button type="button" class="secondary-button" (click)="load()">重新加载</button></div></div>
  6. } @else {
  7. <section class="appointment-overview" aria-label="预约信息">
  8. <header class="appointment-status">
  9. <div><span>{{ status() > 20 ? '课程陪练报告' : '预约课程' }}</span><strong>{{ statusText() }}</strong></div>
  10. <button type="button" class="refresh-detail" (click)="load()" [disabled]="submitting()"><lucide-icon [img]="icons.RefreshCw" [size]="16" aria-hidden="true" />刷新详情</button>
  11. </header>
  12. @if (detailWarning()) { <p class="detail-warning" role="alert">{{ detailWarning() }}</p> }
  13. @if(actionError()){<p class="detail-warning" role="alert">{{actionError()}}</p>}
  14. @if(order()['blockedReason']){<p class="detail-warning">{{order()['blockedReason']}}</p>}
  15. @if (can('start')) { <p class="schedule-hint"><lucide-icon [img]="icons.Clock3" [size]="16" aria-hidden="true" /><span>预约当天(北京时间)均可上课,可提前开始,也可在计划时间之后开始。</span></p> }
  16. <dl class="appointment-info">
  17. <div><dt>学生姓名</dt><dd>{{ field('student') || '-' }}</dd></div>
  18. <div><dt>预约课程</dt><dd>{{ field('course') || '-' }}</dd></div>
  19. <div><dt>课程类型</dt><dd>{{ field('type') || '-' }}</dd></div>
  20. <div><dt>预约时间</dt><dd>{{ field('date') }} {{ field('time') }}@if (field('end')) { ~ {{ field('end') }} }</dd></div>
  21. <div><dt>交付方式</dt><dd>{{ field('delivery') || '-' }}</dd></div>
  22. @if (status() > 20) { <div><dt>陪练老师</dt><dd>{{ field('coach') || '-' }}</dd></div><div><dt>学习成果</dt><dd>{{ order()['dsxueshi'] || 0 }} 学时,累计识词 {{ order()['learned'] || 0 }}</dd></div> }
  23. </dl>
  24. </section>
  25. @if (!order()['cancelled'] && !isStudentView && status() <= 10) {
  26. <section class="course-section">
  27. <label><span>本次学习课本 / 词库</span>
  28. <select aria-label="本次学习课本或词库" [(ngModel)]="selectedBindingId" (ngModelChange)="chooseCourse()" [disabled]="coursesLoading() || submitting()">
  29. <option value="">{{coursesLoading() ? '正在加载学员课程…' : '请选择已开通课程'}}</option>
  30. @for (course of courses(); track course['bindingId']) { <option [value]="course['bindingId']">{{course['Title']}} · {{course['categoryName'] || '已开通'}}{{course['scheduled'] ? '(排课课程)' : ''}}</option> }
  31. </select>
  32. <small>可选择该学员所有已开通的课程,默认使用排课课程。</small>
  33. </label>
  34. @if(coursesError()){<p class="detail-warning" role="alert">{{coursesError()}}</p><button type="button" class="secondary-button" (click)="load()">重新加载课程</button>}
  35. @else if(!coursesLoading() && !courses().length){<p>该学员暂无已开通课程,请联系管理员开通。</p>}
  36. @if(isWriting){<p class="schedule-hint">当前选择语法写作课程,可开始上课并在结束时填写学习内容。</p>}
  37. @else {
  38. <div class="unit-heading"><strong>选择学习单元</strong><span>可多选,右侧按钮可标记已学完</span></div>
  39. <div class="unit-grid">
  40. @for (unit of units(); track $index) {
  41. <article [class.selected]="selectedUnits().has(unitId(unit))" [class.completed]="completedUnits().has(unitId(unit))">
  42. <button type="button" class="unit-select" [attr.aria-pressed]="selectedUnits().has(unitId(unit))" (click)="toggleUnit(unit)">
  43. <span class="check">@if (selectedUnits().has(unitId(unit))) { <lucide-icon [img]="icons.Check" [size]="16" /> }</span>
  44. <span class="unit-copy">
  45. <strong>{{ unitTitle(unit) }}</strong>
  46. @if (unitProgress(unit); as progress) {
  47. <span class="unit-progress-summary"><b>已学 {{ progress.percent }}%</b><span>{{ progress.total > 0 ? progress.learned + '/' + progress.total + ' 词' : '暂无单词' }}</span></span>
  48. <span class="unit-progress-track" aria-hidden="true"><span [style.width.%]="progress.percent"></span></span>
  49. } @else {
  50. <span class="unit-progress-unavailable">暂无进度数据</span>
  51. }
  52. @if (completedUnits().has(unitId(unit))) { <span class="unit-completion-note">已标记学完</span> }
  53. </span>
  54. </button>
  55. <button type="button" class="mark-complete" [disabled]="!can('start') && !can('continue')" title="标记已学完" aria-label="标记已学完" (click)="markUnitComplete(unit)"><lucide-icon [img]="icons.BookCheck" [size]="17" /></button>
  56. </article>
  57. } @empty { <div class="empty-state"><p>{{unitsLoading() ? '正在加载学习单元…' : selectedBindingId ? '暂无可用学习单元' : '请选择课程后加载单元'}}</p></div> }
  58. </div>
  59. }
  60. </section>
  61. }
  62. @if (!isStudentView && can('complete')) {
  63. <dialog #completionDialog class="completion-dialog" aria-labelledby="completion-title" aria-describedby="completion-description" (cancel)="closeCompletionFeedback($event)">
  64. <section class="completion-feedback">
  65. <header>
  66. <div><h2 id="completion-title">完课反馈</h2><p id="completion-description">填写反馈并提交后,结束本次课程。</p></div>
  67. <button type="button" class="feedback-close" aria-label="关闭完课反馈" [disabled]="submitting()" (click)="closeCompletionFeedback()"><lucide-icon [img]="icons.X" [size]="20" /></button>
  68. </header>
  69. <p class="feedback-hint">日期、时间、课时扣减、生词和抗遗忘数据由系统自动记录</p>
  70. <fieldset class="feedback-fields" [disabled]="submitting()">
  71. <label><span>本次学习的课本或强化内容 <b>*</b></span><textarea autofocus [(ngModel)]="contentSummary" maxlength="500" aria-required="true" placeholder="例:人教版三年级 Unit 3,重点词汇与句型"></textarea></label>
  72. <div class="feedback-grid">
  73. <label><span>复习 / 生词本情况</span><input [(ngModel)]="reviewStatus" maxlength="200" placeholder="可填写复习或生词本情况(选填)" /></label>
  74. <label><span>复习说明</span><input [(ngModel)]="reviewSummary" maxlength="200" placeholder="可补充复习情况" /></label>
  75. </div>
  76. <div class="feedback-grid">
  77. <label><span>训练进程和状态</span><input [(ngModel)]="trainingStatus" maxlength="200" placeholder="可填写训练进程和状态(选填)" /></label>
  78. <label><span>进度说明 <b>*</b></span><input [(ngModel)]="trainingSummary" maxlength="200" aria-required="true" placeholder="说明当前进度与下次起点" /></label>
  79. </div>
  80. <div class="feedback-homework">
  81. <label><span>作业安排 @if (!noHomework) { <b>*</b> }</span><textarea [(ngModel)]="homework" [disabled]="noHomework" [attr.aria-required]="!noHomework" maxlength="300" placeholder="填写作业内容和要求"></textarea></label>
  82. <label class="checkbox-row"><input type="checkbox" [(ngModel)]="noHomework" /><span>本次无作业</span></label>
  83. </div>
  84. <label><span>陪练点评</span><textarea [(ngModel)]="coachComment" maxlength="1000" placeholder="本次学习表现与建议"></textarea></label>
  85. <label><span>备注 <small>选填</small></span><textarea [(ngModel)]="notes" maxlength="300" placeholder="记录需要运营或家长关注的情况"></textarea></label>
  86. </fieldset>
  87. @if (feedbackError()) { <p class="feedback-error" role="alert">{{ feedbackError() }}</p> }
  88. <footer class="feedback-actions">
  89. <button type="button" class="secondary-button" [disabled]="submitting()" (click)="closeCompletionFeedback()">返回课程</button>
  90. <button type="button" class="primary-button" [disabled]="submitting()" (click)="finishCoachLesson()">{{ submitting() ? '正在提交…' : '提交并结束课程' }}</button>
  91. </footer>
  92. </section>
  93. </dialog>
  94. }
  95. @if (isStudentView && can('review')) {
  96. <section class="comment-panel">
  97. <label><span>本次评分</span><select [(ngModel)]="studentScore">@for (score of [5,4,3,2,1]; track score) { <option [ngValue]="score">{{ score }} 分</option> }</select></label>
  98. <label><span>学习评价</span><textarea [(ngModel)]="studentComment" maxlength="200" placeholder="请输入评价内容"></textarea></label>
  99. </section>
  100. }
  101. @if (status() >= 30 || practiceRecord()['pldp'] || practiceRecord()['pjnr']) {
  102. <section class="report-panel">
  103. <h2><lucide-icon [img]="icons.Star" [size]="20" />课程评价</h2>
  104. <div><span>学员评分</span><strong>{{ $any(order()['studentReview'])?.score || practiceRecord()['pf'] || '-' }}@if ($any(order()['studentReview'])?.score || practiceRecord()['pf']) { 分 }</strong></div>
  105. <p>{{ $any(order()['studentReview'])?.comment || practiceRecord()['pjnr'] || '学员暂无文字评价' }}</p>
  106. <div><span>陪练点评</span><strong>{{ practiceRecord()['plpjsj'] || '' }}</strong></div>
  107. <p>{{ practiceRecord()['pldp'] || '陪练暂无文字点评' }}</p>
  108. </section>
  109. }
  110. <div class="appointment-actions">
  111. @if (can('start')) { <button type="button" class="primary-button" [disabled]="submitting() || coursesLoading() || unitsLoading()" (click)="startOrContinue()">{{submitting()?'正在开始…':'开始课程'}}</button> }
  112. @if (can('continue')) { <button type="button" [class.primary-button]="!can('complete')" [class.secondary-button]="can('complete')" [disabled]="submitting() || coursesLoading() || unitsLoading()" (click)="startOrContinue()">继续课程</button> }
  113. @if (can('complete')) { <button type="button" class="primary-button" [disabled]="submitting()" (click)="openCompletionFeedback()">{{status()===10?'结束课程':'补齐反馈并结课'}}</button> }
  114. @if (can('review')) { <button type="button" class="primary-button" [disabled]="submitting()" (click)="finishStudentLesson()">{{submitting()?'正在提交…':'提交评价'}}</button> }
  115. @if (status()>=30 && !order()['cancelled']) { <button type="button" class="secondary-button" (click)="openReport()">查看学习报告</button><span>{{order()['reviewStatus']==='submitted'?'学生已评价':'已结课 · 待学生评价'}}</span> }
  116. </div>
  117. }
  118. @if(feedback();as f){<section class="lesson-feedback card"><h3>完课反馈</h3><p>学习内容:{{f['contentSummary']}}</p><p>训练进度:{{f['trainingSummary']}}</p><p>作业:{{f['homework']||'无作业'}}</p></section>}</section>