| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889 |
- <header class="page-header report-header"><div><p>LEARNING REPORTS</p><h1>单词学习报表</h1><span>集中查看学生每次学习结果,生成可下载的专业 PDF 报表</span></div><button class="ui-button refresh-action" type="button" (click)="load(true)" [disabled]="refreshing()"><lucide-icon [img]="icons.RefreshCw" [size]="17" [class.spin]="refreshing()" />{{ refreshing() ? '正在刷新' : '刷新最新数据' }}</button></header>
- @if (message()) { <p class="notice" role="status">{{ message() }}</p> }
- @if (error()) { <p class="notice error" role="alert">{{ error() }}</p> }
- @if (studentObjectId()) { <div class="member-scope"><span>当前仅显示该会员的学习报表</span><a routerLink="/admin/learning-reports">查看全部会员</a></div> }
- <section class="toolbar report-toolbar" aria-label="学习报表筛选">
- <div class="filter-field date-picker-field"><span>统计月份</span><button class="picker-trigger" type="button" aria-label="统计月份" aria-haspopup="dialog" [attr.aria-expanded]="activePicker()==='month'" [class.active]="activePicker()==='month'" (click)="toggleMonthPicker($event)"><lucide-icon [img]="icons.CalendarDays" [size]="17" /><strong>{{ monthLabel() }}</strong><lucide-icon class="picker-chevron" [img]="icons.ChevronDown" [size]="16" /></button>
- @if(activePicker()==='month'){<div class="calendar-popover month-popover" role="dialog" aria-label="选择统计月份" (click)="$event.stopPropagation()"><header><button type="button" aria-label="上一年" (click)="shiftMonthPickerYear(-1,$event)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" /></button><strong>{{ monthPickerYear() }}年</strong><button type="button" aria-label="下一年" (click)="shiftMonthPickerYear(1,$event)"><lucide-icon [img]="icons.ChevronRight" [size]="17" /></button></header><div class="month-grid">@for(name of monthNames;track $index){<button type="button" [class.selected]="month()===monthOptionValue($index)" (click)="selectMonth($index)">{{ name }}</button>}</div></div>}
- </div>
- <div class="filter-field date-picker-field"><span>开始日期</span><button class="picker-trigger" type="button" aria-label="开始日期" aria-haspopup="dialog" [attr.aria-expanded]="activePicker()==='from'" [class.active]="activePicker()==='from'" (click)="toggleDatePicker('from',$event)"><lucide-icon [img]="icons.CalendarDays" [size]="17" /><strong>{{ dateLabel(dateFrom()) }}</strong><lucide-icon class="picker-chevron" [img]="icons.ChevronDown" [size]="16" /></button>
- @if(activePicker()==='from'){<ng-container [ngTemplateOutlet]="dateCalendar" />}
- </div>
- <div class="filter-field date-picker-field"><span>结束日期</span><button class="picker-trigger" type="button" aria-label="结束日期" aria-haspopup="dialog" [attr.aria-expanded]="activePicker()==='to'" [class.active]="activePicker()==='to'" (click)="toggleDatePicker('to',$event)"><lucide-icon [img]="icons.CalendarDays" [size]="17" /><strong>{{ dateLabel(dateTo()) }}</strong><lucide-icon class="picker-chevron" [img]="icons.ChevronDown" [size]="16" /></button>
- @if(activePicker()==='to'){<ng-container [ngTemplateOutlet]="dateCalendar" />}
- </div>
- <label class="search-field"><span>学员</span><input [ngModel]="search()" (ngModelChange)="search.set($event)" placeholder="姓名、手机号或ID" (keyup.enter)="applyFilters()" /></label>
- <label><span>陪练</span><input [ngModel]="coachSearch()" (ngModelChange)="coachSearch.set($event)" placeholder="姓名或ID" /></label>
- <label><span>课程</span><input [ngModel]="courseSearch()" (ngModelChange)="courseSearch.set($event)" placeholder="课程名称" /></label>
- <label><span>报表状态</span><select [ngModel]="status()" (ngModelChange)="status.set($event)"><option value="">全部状态</option><option value="available">可生成</option><option value="historical">历史记录</option><option value="incomplete">学习未完成</option><option value="missing">明细缺失</option></select></label>
- <div class="filter-actions">
- <button class="ui-button secondary reset-action" type="button" (click)="resetFilters()" [disabled]="searching()"><lucide-icon [img]="icons.RotateCcw" [size]="17" />重置</button>
- <button class="ui-button primary query-action" type="button" (click)="applyFilters()" [disabled]="searching()"><lucide-icon [img]="searching() ? icons.LoaderCircle : icons.Search" [size]="17" [class.spin]="searching()" />{{ searching() ? '正在查找' : '查找' }}</button>
- </div>
- </section>
- <ng-template #dateCalendar><div class="calendar-popover day-popover" role="dialog" aria-label="选择日期" (click)="$event.stopPropagation()"><header><button type="button" aria-label="上一月" (click)="shiftCalendar(-1,$event)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" /></button><strong>{{ calendarTitle() }}</strong><button type="button" aria-label="下一月" (click)="shiftCalendar(1,$event)"><lucide-icon [img]="icons.ChevronRight" [size]="17" /></button></header><div class="week-grid" aria-hidden="true">@for(day of weekDays;track day){<span>{{ day }}</span>}</div><div class="day-grid">@for(day of calendarDays();track $index){@if(day){<button type="button" [class.today]="day.date===(today | date:'yyyy-MM-dd')" [class.selected]="day.date===selectedDate()" [attr.aria-label]="day.date" [attr.aria-current]="day.date===selectedDate()?'date':null" (click)="selectDate(day.date)">{{ day.day }}</button>}@else{<span></span>}}</div></div></ng-template>
- @if (data(); as pageData) {
- <section class="report-metrics" aria-label="学习报表摘要">
- <article><span>学习报表</span><strong>{{ pageData.summary.reportCount }}</strong><small>当前筛选范围</small></article>
- <article><span>学习学员</span><strong>{{ pageData.summary.studentCount }}</strong><small>去重学员人数</small></article>
- <article><span>学习单词</span><strong>{{ pageData.summary.learnedCount }}</strong><small>全部学习词次</small></article>
- <article><span>已掌握</span><strong>{{ pageData.summary.masteredCount }}</strong><small>学习结果为已掌握</small></article>
- <article><span>待复习</span><strong>{{ pageData.summary.reviewDueCount }}</strong><small>进入复习计划</small></article>
- <article><span>平均掌握率</span><strong>{{ pageData.summary.masteryRate | number:'1.0-1' }}%</strong><small>已掌握 / 学习单词</small></article>
- </section>
- <section class="content-card report-list-card">
- <header><div><strong>学习报表明细</strong><span>共 {{ pageData.total }} 份,点击查看完整单词与复习计划</span></div></header>
- <div class="table-scroll"><table class="data-table report-table"><thead><tr><th>学习日期</th><th>学员</th><th>陪练老师</th><th>课程</th><th>学习结果</th><th>掌握率</th><th>状态</th><th>操作</th></tr></thead><tbody>
- @for (row of pageData.items; track trackReport($index, row)) {
- <tr><td><strong>{{ row.studyDate || '日期缺失' }}</strong><small>@if(row.appointmentAt){ {{ row.appointmentAt | date:'HH:mm' }} } @else {记录 {{ row.generalId }}}</small></td><td><strong>{{ row.studentName }}</strong><small>ID {{ row.studentId }} · {{ row.studentMobileMasked || '无手机号' }}</small></td><td>{{ row.coachName }}<small>@if(row.coachId){ ID {{ row.coachId }} }</small></td><td>{{ row.courseName }}<small>{{ row.classTypeLabel }}</small></td><td><strong>{{ row.learnedCount }} 个</strong><small>掌握 {{ row.masteredCount }} · 复习 {{ row.reviewDueCount }} · 未标记 {{ row.unmarkedCount }}</small></td><td><div class="rate"><span [style.width.%]="row.masteryRate"></span></div><strong>{{ row.masteryRate | number:'1.0-1' }}%</strong></td><td><span class="report-status" [attr.data-status]="row.status">{{ row.statusLabel }}</span></td><td><div class="row-actions"><button type="button" title="查看报表" aria-label="查看报表" (click)="open(row.targetId)"><lucide-icon [img]="icons.Eye" [size]="16" /></button></div></td></tr>
- } @empty { <tr><td colspan="8"><div class="empty">当前筛选范围暂无学习报表</div></td></tr> }
- </tbody></table></div>
- <footer class="pagination"><span>第 {{ pageData.page }} / {{ totalPages() }} 页</span><div><button type="button" aria-label="上一页" [disabled]="pageData.page<=1" (click)="changePage(pageData.page-1)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" /></button><button type="button" aria-label="下一页" [disabled]="pageData.page>=totalPages()" (click)="changePage(pageData.page+1)"><lucide-icon [img]="icons.ChevronRight" [size]="17" /></button></div></footer>
- </section>
- } @else if (loading()) { <div class="state"><lucide-icon class="spin" [img]="icons.LoaderCircle" [size]="28" />正在加载学习报表…</div> }
- @if (detailLoading() && !detail()) { <div class="detail-loading"><lucide-icon class="spin" [img]="icons.LoaderCircle" [size]="28" />正在整理学习报表…</div> }
- @if (detail(); as report) {
- <div class="backdrop" (click)="close()"></div><aside class="drawer report-drawer" role="dialog" aria-modal="true" aria-label="单词学习报表预览"><header><div><p>REPORT PREVIEW</p><h2>{{ report.studentName }} · {{ report.studyDate }}</h2><span>学习记录 {{ report.generalId }}</span></div><button class="icon-action" type="button" aria-label="关闭" [disabled]="!!exportingId()" (click)="close()"><lucide-icon [img]="icons.X" [size]="20" /></button></header>
- @if (report.countMismatch) { <p class="notice error">历史汇总数量与单词明细不一致,报表已按实际单词明细重新计算。</p> }
- <div class="report-variants" role="group" aria-label="报表类型">
- <button type="button" [class.selected]="reportVariant()==='full'" [attr.aria-pressed]="reportVariant()==='full'" [disabled]="!!exportingId()" (click)="selectVariant('full')">完整学习报表</button>
- <button type="button" [class.selected]="reportVariant()==='english'" [attr.aria-pressed]="reportVariant()==='english'" [disabled]="!!exportingId()" (click)="selectVariant('english')">英文填中文</button>
- <button type="button" [class.selected]="reportVariant()==='chinese'" [attr.aria-pressed]="reportVariant()==='chinese'" [disabled]="!!exportingId()" (click)="selectVariant('chinese')">中文填英文</button>
- </div>
- @if(reportVariant()!=='full'){<p class="worksheet-hint">每次打开复习版都会打乱顺序,下载 PDF 与当前预览一致。</p>}
- @if (error()) { <p class="notice error" role="alert">{{ error() }}</p> }
- <div class="preview-scroll"><article #pdfContent class="pdf-sheet" [class.worksheet-sheet]="reportVariant()!=='full'">
- <section class="pdf-title"><div class="brand-mark"><lucide-icon [img]="icons.FileChartColumn" [size]="28" /></div><div><p>小树陪练</p><h1>{{ reportTitle() }}</h1></div><span>NO. {{ report.generalId }}</span></section>
- <dl class="report-meta"><div><dt>学员</dt><dd>{{ report.studentName }}</dd></div><div><dt>陪练老师</dt><dd>{{ report.coachName }}</dd></div><div><dt>课程</dt><dd>{{ report.courseName }}</dd></div><div><dt>学习日期</dt><dd>{{ report.studyDate }} @if(report.appointmentAt){ {{ report.appointmentAt | date:'HH:mm' }} }</dd></div><div><dt>课型</dt><dd>{{ report.classTypeLabel }}</dd></div><div><dt>预约编号</dt><dd>{{ report.appointmentId || '历史记录' }}</dd></div></dl>
- @if(reportVariant()==='full'){<section class="report-summary"><article><strong>{{ report.learnedCount }}</strong><span>学习单词</span></article><article><strong>{{ report.masteredCount }}</strong><span>已掌握</span></article><article><strong>{{ report.reviewDueCount }}</strong><span>待复习</span></article><article><strong>{{ report.unmarkedCount }}</strong><span>未标记</span></article><article><strong>{{ report.masteryRate | number:'1.0-1' }}%</strong><span>掌握率</span></article></section>}
- @if (reportVariant()==='full') {
- <section class="pdf-section"><h2>单词学习明细</h2><table class="word-table"><thead><tr><th>#</th><th>单词 / 音标</th><th>中文释义</th><th>学习结果</th></tr></thead><tbody>@for (word of report.words; track word.index) { <tr class="word-row"><td>{{ word.index }}</td><td><strong>{{ word.word }}</strong><small>{{ word.phonetic || '—' }}</small>@if(word.example){<em>{{ word.example }}</em>}</td><td>{{ word.meaning || '暂无释义' }}</td><td><span [attr.data-result]="word.result">{{ word.resultLabel }}</span></td></tr> } @empty { <tr><td colspan="4">没有可展示的单词明细</td></tr> }</tbody></table></section>
- } @else {
- <section class="pdf-section"><h2>{{ reportVariant()==='english' ? '看英文,填写中文释义' : '看中文,填写英文单词' }}</h2>
- <p class="worksheet-hint">共 {{ report.words.length }} 个单词,请独立填写;完成后对照完整学习报表核对。</p>
- <table class="word-table worksheet-table"><thead><tr><th>{{ reportVariant()==='english' ? '英文单词' : '中文释义' }}</th><th>{{ reportVariant()==='english' ? '填写中文' : '填写英文' }}</th><th>{{ reportVariant()==='english' ? '英文单词' : '中文释义' }}</th><th>{{ reportVariant()==='english' ? '填写中文' : '填写英文' }}</th></tr></thead><tbody>
- @for (pair of worksheetRows(); track $index) { <tr class="word-row"><td class="word-prompt">{{ reportVariant()==='english' ? pair[0].word : (pair[0].meaning || '释义缺失') }}</td><td class="answer-space"> </td>@if (pair[1]; as word) { <td class="word-prompt">{{ reportVariant()==='english' ? word.word : (word.meaning || '释义缺失') }}</td><td class="answer-space"> </td> } @else { <td colspan="2" class="worksheet-empty"></td> }</tr> }
- @empty { <tr><td colspan="4">没有可展示的单词明细</td></tr> }
- </tbody></table>
- </section>
- }
- <section class="pdf-section review-section"><h2>21天复习计划与记录</h2>
- <p class="review-words">本次学习 {{ report.words.length }} 个单词,词汇见上方列表。</p>
- <table class="review-table"><thead><tr><th scope="row">第几天</th>@for (item of reviewSchedule(); track item.day) { <th scope="col">{{ item.day }}</th> }</tr></thead><tbody>
- <tr><th scope="row">复习日期</th>@for (item of reviewSchedule(); track item.day) { <td>{{ item.date.replaceAll('-', '') }}</td> }</tr>
- <tr><th scope="row">遗忘词数</th>@for (item of reviewSchedule(); track item.day) { <td class="review-count"> </td> }</tr>
- </tbody></table>
- <p class="worksheet-hint">每次复习后填写遗忘的单词数量,未复习的格子留空。@if(hasInferredReviewDates()){ 未设置的复习日期按学习日期推算;日期缺失时请手写补充。 }</p>
- </section>
- @if (reportVariant()==='full' && (report.score || report.studentComment || report.coachComment)) { <section class="pdf-section"><h2>课程评价</h2><div class="comments"><article class="comment-card"><strong>学员评分</strong><p>{{ report.score ? report.score + ' 分' : '未评分' }}</p></article><article class="comment-card"><strong>学员评价</strong><p>{{ report.studentComment || '暂无评价' }}</p></article><article class="comment-card"><strong>陪练点评</strong><p>{{ report.coachComment || '暂无点评' }}</p></article></div></section> }
- @if(reportVariant()==='full'){<footer class="pdf-footer"><span>数据来源:学习记录 {{ report.generalId }}</span><span>生成时间:{{ report.generatedAt | date:'yyyy-MM-dd HH:mm:ss' }}</span></footer>}
- </article></div>
- <footer class="drawer-actions"><span class="report-status" [attr.data-status]="report.status">{{ report.statusLabel }}</span><button class="ui-button primary" type="button" [disabled]="!report.canExport || !!exportingId()" (click)="download(report, reportVariant())"><lucide-icon [img]="exportingId() ? icons.LoaderCircle : icons.Download" [size]="17" [class.spin]="!!exportingId()" />{{ exportingId() ? '正在生成' : '下载 PDF' }}</button></footer></aside>
- }
|