course-center-page.component.html 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. <section class="center-page">
  2. @if (loading() || syncNoticeVisible()) {
  3. <div class="center-sync" [class.delayed]="liveSyncStatus().state !== 'current'" role="status"><lucide-icon [class.spin]="loading() || liveChecking()" [img]="icons.LoaderCircle" [size]="18" /><span>{{ liveStatusText() }}</span></div>
  4. }
  5. @if (!isCoach) {
  6. <section class="course-band">
  7. <header><lucide-icon [img]="icons.BookOpen" [size]="21" /><h2>我的学习内容</h2><span>{{ (loading()||error())&&!courses().length ? '—' : courses().length }} 项</span></header>
  8. @if (courses().length) {
  9. <ul class="course-list" id="learning-course-list">@for (item of visibleCourses(); track courseBindingId(item) || courseId(item) || $index) { <li><button type="button" [class.readonly]="!canStartPointReading" [disabled]="!canOpenCourse(item)" (click)="openCourse(item)"><span><strong>{{ courseCategory(item) ? courseCategory(item)+' · ' : '' }}{{ courseTitle(item) }}</strong><small>{{ courseHint(item) }}</small></span><lucide-icon [img]="icons.ChevronRight" [size]="18" /></button></li> }</ul>
  10. @if (courses().length > 3) {
  11. <button class="course-toggle" type="button" (click)="toggleCourses()" [attr.aria-expanded]="coursesExpanded()" aria-controls="learning-course-list"><span>{{ coursesExpanded() ? '收起' : '展开其余 ' + (courses().length - 3) + ' 项' }}</span><lucide-icon [img]="icons.ChevronDown" [size]="18" [class.expanded]="coursesExpanded()" /></button>
  12. }
  13. } @else { <p class="band-empty">{{ error() ? '学习内容暂时无法加载,请重试' : loading() ? '正在读取学习内容…' : '暂未分配学习内容,请联系老师' }}</p> }
  14. </section>
  15. }
  16. <section class="schedule-band">
  17. <header><lucide-icon [img]="isCoach ? icons.GraduationCap : icons.CalendarDays" [size]="21" /><h2>{{ isHistory ? '陪练记录' : isCoach ? '陪练排课' : '伴学列表' }}</h2><span>{{ (loading()||error())&&!schedules().length ? '—' : isHistory || hasScheduleCalendar ? visibleSchedules().length + ' / ' + schedules().length : schedules().length }} 节</span></header>
  18. <nav class="range-tabs schedule-actions" aria-label="课表操作"><button type="button" (click)="load(true)" [disabled]="loading()">刷新课表</button><button type="button" (click)="clearScheduleFilters()">清除筛选</button>@if (nextScheduleDate() && !isHistory) { <button type="button" (click)="showNextSchedule()">最近一节 · {{nextScheduleDate()}}</button> }</nav>
  19. @if (isHistory) {
  20. <div class="history-filter-panel" aria-live="polite"><strong>{{historyMonth()==='all'?'全部月份':historyMonth()}} · 陪练费用合计:{{loading() || error() || !earningsSummary() ? '—' : '¥' + (earningsSummary()!.amount | number:'1.2-2')}}</strong><p>按当前筛选全部 {{listTotal()}} 条记录汇总,包含所有分页;已取消记录不计费用。</p>@if(earningsSummary()?.pendingCount){<small>另有 {{earningsSummary()!.pendingCount}} 条费用待核对,未计入合计。</small>}</div>
  21. <section class="history-filter-panel" aria-label="陪练记录筛选">
  22. <div class="history-filter-grid">
  23. <label>
  24. <span>月份</span>
  25. <div class="history-filter-control month-control">
  26. <lucide-icon [img]="icons.CalendarDays" [size]="18" />
  27. <select [ngModel]="historyMonth()" (ngModelChange)="setHistoryMonth($event)" aria-label="按月份筛选陪练记录">
  28. <option value="all">全部月份</option>
  29. @for (month of historyMonthOptions(); track month.value) { <option [value]="month.value">{{ month.label }}({{ month.count }})</option> }
  30. </select>
  31. </div>
  32. </label>
  33. <label>
  34. <span>学生姓名</span>
  35. <div class="history-filter-control name-control">
  36. <lucide-icon [img]="icons.Search" [size]="18" />
  37. <input type="search" [ngModel]="historyStudentQuery()" (ngModelChange)="setHistoryStudentQuery($event)" placeholder="输入姓名查找" autocomplete="off" aria-label="按学生姓名筛选陪练记录" />
  38. </div>
  39. </label>
  40. </div>
  41. <footer>
  42. <span>当前显示 <strong>{{ visibleSchedules().length }}</strong> 节,共 {{ listTotal() }} 节</span>
  43. @if (historyFilterActive()) { <button type="button" (click)="clearHistoryFilters()"><lucide-icon [img]="icons.X" [size]="15" />清除筛选</button> }
  44. </footer>
  45. </section>
  46. }
  47. @if (hasScheduleCalendar) {
  48. <div class="schedule-tools">
  49. <div class="schedule-toolbar">
  50. <nav class="range-tabs" aria-label="课表日期范围">
  51. <button type="button" [class.active]="scheduleRange() === 'day' && selectedDate() === today()" (click)="showToday()">今天</button>
  52. <button type="button" [class.active]="scheduleRange() === 'day' && selectedDate() === tomorrow()" (click)="showTomorrow()">明天</button>
  53. <button type="button" [class.active]="scheduleRange() === 'week'" (click)="showWeek()">本周</button>
  54. <button type="button" [class.active]="scheduleRange() === 'all'" (click)="showAllSchedules()">全部</button>
  55. </nav>
  56. @if (isCoach && studentOptions().length) {
  57. <label class="student-filter"><span>学员</span>
  58. <select aria-label="筛选排课学员" [ngModel]="selectedStudent()" (ngModelChange)="setStudentFilter($event)">
  59. <option value="all">全部学员({{ allStudentCount() }})</option>
  60. @for (student of studentOptions(); track student.id) { <option [value]="student.id">{{ student.name }}({{ student.count }})</option> }
  61. </select>
  62. </label>
  63. }
  64. </div>
  65. <nav class="schedule-filters" [attr.aria-label]="isCoach ? '陪练排课状态筛选' : '伴学记录筛选'">
  66. <button type="button" [class.active]="scheduleFilter() === 'all'" (click)="setScheduleFilter('all')">全部 <span>{{ (loading()||error())&&!schedules().length ? '—' : scopedSchedules().length }}</span></button>
  67. <button type="button" [class.active]="scheduleFilter() === 'pending'" (click)="setScheduleFilter('pending')">计划未完成 <span>{{ (loading()||error())&&!schedules().length ? '—' : pendingCount() }}</span></button>
  68. <button type="button" [class.active]="scheduleFilter() === 'finished'" (click)="setScheduleFilter('finished')">已结束 <span>{{ (loading()||error())&&!schedules().length ? '—' : finishedCount() }}</span></button>
  69. </nav>
  70. <div class="calendar-panel">
  71. <div class="calendar-heading">
  72. <button type="button" (click)="shiftCalendarMonth(-1)" aria-label="上个月">‹</button>
  73. <strong>{{ calendarLabel() }}</strong>
  74. <button type="button" (click)="shiftCalendarMonth(1)" aria-label="下个月">›</button>
  75. </div>
  76. <div class="calendar-weekdays" aria-hidden="true">
  77. @for (week of weekLabels; track week) { <span>{{ week }}</span> }
  78. </div>
  79. <div class="calendar-grid">
  80. @for (day of calendarDays(); track day.date) {
  81. <button
  82. type="button"
  83. class="calendar-day"
  84. [class.outside]="!day.inMonth"
  85. [class.today]="day.isToday"
  86. [class.selected]="scheduleRange() === 'day' && selectedDate() === day.date"
  87. [class.has-course]="day.count > 0"
  88. [class.finished-course]="day.finishedCount > 0"
  89. [class.planned-course]="day.plannedCount > 0"
  90. (click)="selectCalendarDay(day)"
  91. [attr.aria-label]="day.date + ',已学完 ' + day.finishedCount + ' 节,计划学 ' + day.plannedCount + ' 节'"
  92. >
  93. <span>{{ day.day }}</span>
  94. @if (day.count > 0) { <em>{{ day.count }}</em> }
  95. </button>
  96. }
  97. </div>
  98. </div>
  99. <p class="schedule-summary">{{ scheduleRangeLabel() }} · {{(loading()||error())&&!schedules().length?'—':visibleSchedules().length}} 节课</p>
  100. </div>
  101. } @else if (!isCoach && schedules().length) {
  102. <nav class="schedule-filters" aria-label="伴学记录筛选">
  103. <button type="button" [class.active]="scheduleFilter() === 'all'" (click)="setScheduleFilter('all')">全部 <span>{{ listTotal() }}</span></button>
  104. <button type="button" [class.active]="scheduleFilter() === 'pending'" (click)="setScheduleFilter('pending')">计划未完成 <span>{{ (loading()||error())&&!schedules().length ? '—' : pendingCount() }}</span></button>
  105. <button type="button" [class.active]="scheduleFilter() === 'finished'" (click)="setScheduleFilter('finished')">已结束 <span>{{ (loading()||error())&&!schedules().length ? '—' : finishedCount() }}</span></button>
  106. </nav>
  107. }
  108. @if (visibleSchedules().length) {
  109. @for (group of groups(); track group.date) {
  110. <h3>{{ group.date }}</h3>
  111. <div class="schedule-list">
  112. @for (item of group.items; track $index) {
  113. <article [class.has-lesson-pay]="isHistory && isCoach && !item['cancelled']" [attr.data-schedule-id]="scheduleId(item)" (click)="open(item, $event)">
  114. <time>{{ scheduleTime(item) }}</time>
  115. <div class="schedule-copy"><strong>{{ scheduleTitle(item) }}</strong>@if (scheduleSubtitle(item)) { <span>{{ scheduleSubtitle(item) }}</span> }</div>
  116. @if (isHistory && isCoach && !item['cancelled']) { <div class="lesson-pay"><strong>{{ item['payStatus'] === 'earned' ? '陪练费用 ¥' + ($any(item['teacherPay']) | number:'1.2-2') : '费用待核对' }}</strong><small [class.feedback-pending]="item['feedbackStatus'] !== 'submitted'">{{item['feedbackStatus']==='submitted'?'反馈已提交':'反馈待补'}}</small></div> }
  117. <span class="status" [class]="'status ' + statusClass(item)">{{ statusText(item) }}</span>
  118. @if (isCoach) { <button type="button" class="report-button" (click)="report(item, $event)" aria-label="学习报表" title="学习报表"><lucide-icon [img]="icons.BarChart3" [size]="18" /></button> }
  119. <lucide-icon class="chevron" [img]="icons.ChevronRight" [size]="18" />
  120. </article>
  121. }
  122. </div>
  123. }
  124. } @else { <p class="band-empty">{{ error() ? '课表暂时无法加载,请重试' : loading() ? '正在读取排课数据…' : (isHistory && schedules().length && historyFilterActive() ? '没有符合筛选条件的陪练记录' : (hasScheduleCalendar ? scheduleEmptyText() : (schedules().length ? '该分类暂无伴学记录' : (isCoach ? '暂无陪练记录' : '暂无伴学记录')))) }}</p> }
  125. </section>
  126. @if (error()) {
  127. <div class="center-state error"><lucide-icon [img]="icons.AlertCircle" [size]="25" /><span>{{ error() }}{{schedules().length?',当前保留上次加载结果':''}}</span><button type="button" class="secondary-button" (click)="load(true)">重新加载</button></div>
  128. }
  129. <nav class="mobile-pagination"><button (click)="turnPage(-1)" [disabled]="loading()||listPage()<=1">上一页</button><span>共 {{(loading()||error())&&!schedules().length?'—':listTotal()}} 条 · {{listPage()}} / {{listPages()}}</span><button (click)="turnPage(1)" [disabled]="loading()||listPage()>=listPages()">下一页</button></nav></section>