| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- <section class="learning-page">
- @if (loading()) {
- <div class="empty-state"><p>正在加载学习内容...</p></div>
- } @else if (error()) {
- <div class="empty-state error">
- <div>
- <lucide-icon [img]="icons.CircleHelp" [size]="32" />
- <p>{{ error() }}</p>
- <button type="button" class="secondary-button" (click)="load()">重新加载</button>
- </div>
- </div>
- } @else if (isNodePage) {
- <div class="node-intro">
- <lucide-icon [img]="icons.Library" [size]="27" />
- <div>
- <h2>{{ isCatalogPage ? '选择词库教材' : canStartPointReading ? '选择学习单元' : '学习单元' }}</h2>
- <p>{{ isCatalogPage ? '先选择学习阶段,再选择对应教材' : canStartPointReading ? '可多选单元,下一步精筛本次新词' : '点击单元查看老师分配的单词列表' }}</p>
- </div>
- </div>
- @if (isCatalogPage) {
- <nav class="catalog-tabs" aria-label="学习阶段">
- @for (category of catalogCategories; track category.pid; let index = $index) {
- <button type="button" [class.active]="catalogCategoryIndex() === index" (click)="selectCatalogCategory(index)">{{ category.label }}</button>
- }
- </nav>
- } @else if (canStartPointReading) {
- <div class="unit-selection-summary">
- <span>已选 <strong>{{ selectedNodes().size }}</strong> 个单元</span>
- <small>单击单元可选择或取消</small>
- </div>
- }
- <div class="node-list">
- @for (node of nodes(); track $index) {
- <button type="button" [class.unit-node]="isUnitListPage" [class.readonly]="isUnitListPage && !canStartPointReading" [class.selected]="isUnitListPage && selectedNodes().has(nodeId(node))" (click)="openNode(node)">
- @if (isUnitListPage && canStartPointReading) {
- <span class="node-check">@if (selectedNodes().has(nodeId(node))) { <lucide-icon [img]="icons.Check" [size]="16" /> }</span>
- }
- <span class="node-copy">
- <strong>{{ nodeTitle(node) }}</strong>
- @if (nodeProgress(node)) { <small>{{ nodeProgress(node) }}</small> }
- @else if (nodeCount(node)) { <small>{{ nodeCount(node) }} 个词</small> }
- </span>
- @if (isUnitListPage) {
- <span class="unit-progress-ring" [class.complete]="nodeProgressPercent(node) === 100" [style.--progress]="nodeProgressPercent(node) + '%'" [attr.aria-label]="nodeTitle(node) + '学习进度' + nodeProgressPercent(node) + '%'"><strong>{{ nodeProgressPercent(node) }}%</strong></span>
- }
- @if (isCatalogPage || (isUnitListPage && !canStartPointReading)) { <lucide-icon [img]="icons.ChevronRight" [size]="19" /> }
- </button>
- } @empty {
- <div class="empty-state"><p>{{ isCatalogPage ? '该阶段暂无可用词库' : '该教材暂无可选单元' }}</p></div>
- }
- </div>
- @if (isUnitListPage && canStartPointReading) {
- <div class="sticky-actions unit-actions">
- <span>已选 {{ selectedNodes().size }} 个单元</span>
- <button type="button" class="primary-button" (click)="startNodeSelection()"><lucide-icon [img]="icons.Play" [size]="18" />选词提升</button>
- </div>
- }
- } @else if (isReadOnlyWordView) {
- <section class="readonly-word-card" aria-labelledby="readonly-word-title">
- <header class="readonly-word-header">
- <div><strong id="readonly-word-title">单词列表</strong><span>第 {{ wordPreviewRangeStart() }}–{{ wordPreviewRangeEnd() }} 条 · 共 {{ wordPreviewTotal() }} 个单词</span></div>
- <em>{{ isHistoryWordView && canStartPointReading ? '历史记录' : '学生只读' }}</em>
- </header>
- <div class="readonly-word-list" [class.is-updating]="wordPreviewLoading()" [attr.aria-busy]="wordPreviewLoading()" aria-live="polite">
- @for (word of words(); track word.id) {
- <article>
- <div><strong>{{ word.word }}</strong>@if (word.phonetic) { <small>{{ word.phonetic }}</small> }</div>
- <p>{{ word.meaning || '暂无释义' }}</p>
- </article>
- } @empty {
- <div class="empty-state"><p>{{ isHistoryWordView ? '该次学习记录暂无可显示的单词' : '该单元暂无单词' }}</p></div>
- }
- </div>
- @if (wordPreviewPageCount() > 1) {
- <nav class="word-preview-pagination" aria-label="单词列表分页">
- <button type="button" class="page-direction" [disabled]="wordPreviewPage() === 1 || wordPreviewLoading()" (click)="openWordPreviewPage(wordPreviewPage() - 1)"><lucide-icon [img]="icons.ChevronLeft" [size]="17" />上一页</button>
- <div class="page-numbers">
- @for (page of wordPreviewPages(); track page) {
- <button type="button" [class.active]="page === wordPreviewPage()" [attr.aria-current]="page === wordPreviewPage() ? 'page' : null" [attr.aria-label]="'第 ' + page + ' 页'" [disabled]="wordPreviewLoading()" (click)="openWordPreviewPage(page)">{{ page }}</button>
- }
- </div>
- <button type="button" class="page-direction" [disabled]="wordPreviewPage() === wordPreviewPageCount() || wordPreviewLoading()" (click)="openWordPreviewPage(wordPreviewPage() + 1)">下一页<lucide-icon [img]="icons.ChevronRight" [size]="17" /></button>
- <span>{{ wordPreviewPage() }} / {{ wordPreviewPageCount() }} 页</span>
- </nav>
- }
- <p class="readonly-word-note">{{ isHistoryWordView ? '这里展示本次学习记录中的单词,不会改动原学习结果' : '学生可查看老师分配的单词,点读、跟读和学习记录由陪练老师在上课时操作' }}</p>
- </section>
- } @else if (isSelectionPage) {
- <header class="selection-header" [class.refine-selection-header]="isRefineSelectionPage">
- <div>
- @if (isRefineSelectionPage) {
- <strong>{{ isReadOnlySelectionPage ? '词库学习进度' : '本次待学 ' + selected().size + ' / ' + selectionLimit }}</strong>
- <span>已学完 {{ completedSelectionWords().length }} · 还没学 {{ pendingSelectionWords().length }}</span>
- } @else {
- <strong>已选 {{ selected().size }} / {{ selectionLimit }}</strong>
- <span>选择本次要学习的单词</span>
- }
- </div>
- <div class="header-actions">
- @if (!isRefineSelectionPage) { <button type="button" class="quiet-button" (click)="toggleAllWords()">{{ selected().size ? '取消全选' : '全选' }}</button> }
- @if (!isReadOnlySelectionPage) {
- <button type="button" class="primary-button" [disabled]="submitting()" (click)="startSelected()">
- <lucide-icon [img]="icons.Play" [size]="17" />{{ isRefineSelectionPage ? selectionActionLabel() : '开始学习' }}
- </button>
- }
- </div>
- </header>
- @if (isRefineSelectionPage) {
- <div class="word-selection-panel">
- <nav class="word-selection-tabs" role="tablist" aria-label="单词学习状态">
- <button type="button" role="tab" [class.active]="activeSelectionBucket() === 'pending'" [attr.aria-selected]="activeSelectionBucket() === 'pending'" (click)="activeSelectionBucket.set('pending')">
- <span>还没学</span><b>{{ pendingSelectionWords().length }}</b>
- </button>
- <button type="button" role="tab" [class.active]="activeSelectionBucket() === 'completed'" [attr.aria-selected]="activeSelectionBucket() === 'completed'" (click)="activeSelectionBucket.set('completed')">
- <span>已学完</span><b>{{ completedSelectionWords().length }}</b>
- </button>
- </nav>
- @if (activeSelectionBucket() === 'pending') {
- <section class="word-selection-list pending-column" role="tabpanel" aria-label="还没学的单词">
- <p class="word-column-help">{{ isReadOnlySelectionPage ? '这些单词尚未完成学习,可点击单词查看释义和读音。' : '先回忆中文,核对后选择“认识”或“加入学习”。' }}</p>
- <div class="word-select-list">
- @for (word of pendingSelectionWords(); track word.id) {
- <article class="word-refine-item" [class.selected]="selected().has(word.id)">
- <div class="word-refine-main">
- @if (isReadOnlySelectionPage) {
- <span class="word-pending-mark" aria-hidden="true"><lucide-icon [img]="icons.CircleHelp" [size]="16" /></span>
- } @else {
- <button type="button" class="word-select-check" [attr.aria-label]="(selected().has(word.id) ? '移出学习:' : '加入学习:') + word.word" [attr.aria-pressed]="selected().has(word.id)" (click)="toggleWord(word)">
- @if (selected().has(word.id)) { <lucide-icon [img]="icons.Check" [size]="16" /> }
- </button>
- }
- <button type="button" class="word-reveal" [attr.aria-expanded]="selectionWordRevealed(word)" (click)="revealSelectionWord(word)">
- <strong>{{ word.word }}</strong>
- <small [class.revealed]="selectionWordRevealed(word)">{{ selectionWordRevealed(word) ? (word.meaning || '暂无释义') : '先说出中文意思,再点击核对' }}</small>
- </button>
- <span class="audio" aria-hidden="true"><lucide-icon [img]="icons.Volume2" [size]="18" /></span>
- </div>
- @if (selectionWordRevealed(word) && !isReadOnlySelectionPage) {
- <div class="word-assessment">
- <span>和你刚才说的一样吗?</span>
- <button type="button" class="known-button" [class.active]="selectionAssessment()[word.id] === 'known'" (click)="markWordKnown(word)"><lucide-icon [img]="icons.Check" [size]="15" />认识</button>
- <button type="button" class="unknown-button" [class.active]="selectionAssessment()[word.id] === 'unknown'" (click)="markWordUnknown(word)"><lucide-icon [img]="icons.CircleHelp" [size]="15" />不认识,加入学习</button>
- </div>
- }
- </article>
- } @empty {
- <div class="word-column-empty"><lucide-icon [img]="icons.CheckCheck" [size]="24" /><span>这个词库已经全部学完</span></div>
- }
- </div>
- </section>
- } @else {
- <section class="word-selection-list completed-column" role="tabpanel" aria-label="已学完的单词">
- <p class="word-column-help">包含真正完成学习,以及本次判断为“认识”的词。</p>
- <div class="word-select-list">
- @for (word of completedSelectionWords(); track word.id) {
- <article class="word-refine-item completed-word">
- <div class="word-refine-main">
- <span class="word-complete-check" aria-hidden="true"><lucide-icon [img]="icons.Check" [size]="16" /></span>
- <button type="button" class="word-reveal" [attr.aria-expanded]="selectionWordRevealed(word)" (click)="revealSelectionWord(word)">
- <strong>{{ word.word }}</strong>
- <small [class.revealed]="selectionWordRevealed(word)">{{ selectionWordRevealed(word) ? (word.meaning || '暂无释义') : selectionCompletionLabel(word) + ' · 点击查看释义' }}</small>
- </button>
- <span class="audio" aria-hidden="true"><lucide-icon [img]="icons.Volume2" [size]="18" /></span>
- </div>
- @if (!wordHistoricallyLearned(word)) {
- <div class="completed-word-action">
- <span>本次已标记为认识</span>
- <button type="button" (click)="markWordUnknown(word)">改为还没学</button>
- </div>
- }
- </article>
- } @empty {
- <div class="word-column-empty"><lucide-icon [img]="icons.CheckCheck" [size]="24" /><span>还没有学完的词</span></div>
- }
- </div>
- </section>
- }
- </div>
- } @else {
- <div class="word-select-list">
- @for (word of words(); track word.id) {
- <button type="button" [class.selected]="selected().has(word.id)" (click)="toggleWord(word)">
- <span class="check">@if (selected().has(word.id)) { <lucide-icon [img]="icons.Check" [size]="16" /> }</span>
- <span><strong>{{ word.word }}</strong><small>{{ word.meaning || '暂无释义' }}</small></span>
- <span class="audio" (click)="$event.stopPropagation(); play(word)"><lucide-icon [img]="icons.Volume2" [size]="18" /></span>
- </button>
- } @empty {
- <div class="empty-state"><p>暂无可选词汇</p></div>
- }
- </div>
- }
- @if (meta.path === 'pages/member/starshop') {
- <div class="sticky-actions single">
- <button type="button" class="danger-button" (click)="deleteSelectedNewWords()"><lucide-icon [img]="icons.Trash2" [size]="18" />删除所选</button>
- </div>
- }
- } @else if (isStudyListPage) {
- <div class="stage-banner">
- <div>
- <span>第 {{ studyStep() }} / 3 轮</span>
- <strong>{{ studyStep() === 1 ? '顺序学习' : studyStep() === 2 ? '乱序学习' : '中文挑战' }}</strong>
- </div>
- <button type="button" title="重新乱序" aria-label="重新乱序" (click)="shuffleStudyWords()"><lucide-icon [img]="icons.RefreshCw" [size]="19" /></button>
- </div>
- <div class="repeat-list">
- @for (word of words(); track word.id; let index = $index) {
- <button type="button" (click)="repeatWord(word, index)">
- <span>
- <strong>{{ studyStep() === 3 ? word.meaning : word.word }}</strong>
- @if ((repetitions()[word.id] || 0) >= 2) { <small>{{ studyStep() === 3 ? word.word : word.meaning }}</small> }
- </span>
- <span class="repeat-dots" aria-label="跟读次数">
- @for (dot of [1, 2, 3]; track dot) { <i [class.done]="(repetitions()[word.id] || 0) >= dot"></i> }
- </span>
- </button>
- }
- </div>
- <div class="sticky-actions">
- <button type="button" class="secondary-button" (click)="shuffleStudyWords()"><lucide-icon [img]="icons.RefreshCw" [size]="18" />再次乱序</button>
- <button type="button" class="primary-button" (click)="skipStudyList()">跳到下一关<lucide-icon [img]="icons.ChevronRight" [size]="18" /></button>
- </div>
- } @else if (isMatchingPage) {
- @if (completed()) {
- <ng-container *ngTemplateOutlet="stageComplete; context: { title: '配对完成', text: '听音识意已经准备好了' }" />
- } @else {
- <div class="game-heading"><span>单词对对碰</span><strong>剩余 {{ matchLeft().length }} 组</strong></div>
- <div class="match-grid">
- <div>
- @for (word of matchLeft(); track word.id) {
- <button type="button" [class.active]="leftChoice() === word.id" (click)="chooseMatch('left', word)"><lucide-icon [img]="icons.Volume2" [size]="17" />{{ word.word }}</button>
- }
- </div>
- <div>
- @for (word of matchRight(); track word.id) {
- <button type="button" [class.active]="rightChoice() === word.id" (click)="chooseMatch('right', word)">{{ word.meaning || '暂无释义' }}</button>
- }
- </div>
- </div>
- }
- } @else if (isListeningPage) {
- @if (completed()) {
- <ng-container *ngTemplateOutlet="stageComplete; context: { title: '听音识意完成', text: '下一关是组合拼写' }" />
- } @else {
- @if (current(); as word) {
- <div class="study-progress"><div><span [style.width.%]="progress()"></span></div><strong>{{ currentIndex() + 1 }} / {{ words().length }}</strong></div>
- <div class="listen-panel">
- <button type="button" class="listen-button" aria-label="播放单词" (click)="play(word)"><lucide-icon [img]="icons.Headphones" [size]="34" /></button>
- <p>点击喇叭,选择正确释义</p>
- </div>
- <div class="option-list">
- @for (option of choiceOptions(); track option.id) {
- <button type="button" (click)="chooseMeaning(option)">{{ option.meaning || option.word }}</button>
- }
- </div>
- }
- }
- } @else if (isSpellingPage) {
- @if (completed()) {
- <ng-container *ngTemplateOutlet="stageComplete; context: { title: '拼写完成', text: '本轮单词已全部完成' }" />
- } @else {
- @if (current(); as word) {
- <div class="study-progress"><div><span [style.width.%]="progress()"></span></div><strong>{{ currentIndex() + 1 }} / {{ words().length }}</strong></div>
- <article class="spelling-card">
- <button type="button" class="audio-main" aria-label="播放读音" title="播放读音" (click)="play(word)"><lucide-icon [img]="icons.Headphones" [size]="25" /></button>
- <p>{{ word.meaning || '请根据读音拼写单词' }}</p>
- <div class="assembled" [class.correct]="spellingCorrect()">{{ assembled() || ' ' }}</div>
- <div class="letter-grid">
- @for (letter of letters(); track $index) {
- <button type="button" [disabled]="letter.used" (click)="spellLetter($index)">{{ letter.value }}</button>
- }
- </div>
- </article>
- <div class="sticky-actions" [class.teacher-spelling-actions]="meta.path === 'pages/stu/pl_stu5'">
- <button type="button" class="secondary-button" (click)="resetSpelling()"><lucide-icon [img]="icons.RotateCcw" [size]="18" />重拼</button>
- @if (meta.path === 'pages/stu/pl_stu5') { <button type="button" class="secondary-button" (click)="skipSpellingWord()">{{ currentIndex() === words().length - 1 ? '跳过并完成' : '跳过此词' }}</button> }
- <button type="button" class="primary-button" [disabled]="!spellingCorrect()" (click)="nextSpellingWord()">{{ currentIndex() === words().length - 1 ? '完成本关' : '下一个' }}<lucide-icon [img]="icons.ChevronRight" [size]="18" /></button>
- </div>
- }
- }
- } @else if (isReviewPage) {
- <div class="review-summary">
- <div><strong>{{ words().length }}</strong><span>学习词数</span></div>
- <div><strong>{{ reviewCount(1) }}</strong><span>已掌握</span></div>
- <div><strong>{{ reviewCount(2) }}</strong><span>待加强</span></div>
- </div>
- <div class="review-list">
- @for (word of words(); track word.id) {
- <article>
- <button type="button" class="word-audio" (click)="play(word)"><span><strong>{{ word.word }}</strong><small>{{ word.meaning }}</small></span><lucide-icon [img]="icons.Volume2" [size]="18" /></button>
- <div class="review-buttons">
- <button type="button" [class.active]="reviewValue(word) === 1" (click)="markReview(word, 1)"><lucide-icon [img]="icons.Check" [size]="18" />已掌握</button>
- <button type="button" [class.active]="reviewValue(word) === 2" (click)="markReview(word, 2)"><lucide-icon [img]="icons.X" [size]="18" />待加强</button>
- </div>
- </article>
- }
- </div>
- <div class="sticky-actions">
- <button type="button" class="secondary-button" (click)="markAllKnown()">{{ reviewCount(1) === words().length ? '取消全选' : '全部掌握' }}</button>
- <button type="button" class="primary-button" [disabled]="submitting()" (click)="submitReview()">{{ meta.path === 'pages/stu/pl_end' ? '结束课程' : '提交检测' }}</button>
- </div>
- } @else if (isCalendarPage) {
- <section class="memory-overview" aria-label="抗遗忘计划概览">
- <lucide-icon class="memory-watermark" [img]="icons.BrainCircuit" [size]="156" aria-hidden="true" />
- <header>
- <div class="memory-copy">
- <span class="memory-kicker"><lucide-icon [img]="icons.Activity" [size]="14" />{{ canStartPointReading ? '关联学员抗遗忘任务' : '21 天智能复习引擎' }}</span>
- <strong>{{ canStartPointReading ? '带学生及时复习' : '记忆巩固进行中' }}</strong>
- <small>{{ canStartPointReading ? '查看学生姓名、复习日期和完成情况,并从任务中直接开始带练' : '在遗忘临界点及时复习,让记忆一次次回升' }}</small>
- </div>
- <div class="memory-score" role="progressbar" aria-label="总体完成度" aria-valuemin="0" aria-valuemax="100" [attr.aria-valuenow]="memoryProgress()"><b>{{ memoryProgress() }}%</b><small>总体完成</small></div>
- </header>
- <figure class="memory-curve" aria-labelledby="forgetting-curve-title">
- <figcaption><span id="forgetting-curve-title">21 天抗遗忘原理</span><strong><i></i>临界点复习</strong></figcaption>
- <div class="memory-curve-chart">
- <svg viewBox="0 0 320 128" role="img" aria-label="自然遗忘曲线逐渐下降,长期记忆曲线逐渐上升;在五个交点及时复习后,遗忘速度越来越慢">
- <line class="memory-chart-axis" x1="26" y1="10" x2="26" y2="98" />
- <line class="memory-chart-axis" x1="26" y1="98" x2="304" y2="98" />
- <path class="memory-forgetting-line" [attr.d]="antiForgetChart.forgettingPath" />
- <path class="memory-learning-line" [attr.d]="antiForgetChart.learningPath" />
- @for (event of antiForgetChart.reviewEvents; track event.day) {
- <path class="memory-review-decay" [attr.d]="event.path" />
- <line class="memory-review-lift" [style.--pulse-delay]="(((event.day - 1) * 0.45) + 's')" [attr.x1]="event.x" [attr.y1]="event.y" [attr.x2]="event.x" y2="12" />
- <circle class="memory-review-ripple" [style.--pulse-delay]="(((event.day - 1) * 0.45) + 's')" [attr.cx]="event.x" [attr.cy]="event.y" r="4" />
- <circle class="memory-review-dot" [attr.cx]="event.x" [attr.cy]="event.y" r="3" />
- <circle class="memory-reset-dot" [attr.cx]="event.x" cy="12" r="2.5" />
- <text class="memory-review-label" [attr.x]="event.x + 4" [attr.y]="event.y - 4">复习{{ $index + 1 }}</text>
- }
- <path class="memory-review-decay" [attr.d]="antiForgetChart.continuationPath" />
- <text class="memory-line-label forgetting" x="238" y="88">自然遗忘</text>
- <text class="memory-line-label learning" x="226" y="27">长期记忆</text>
- @for (point of antiForgetChart.days; track point.day) {
- <g class="memory-day-point" [style.--pulse-delay]="(($index * 0.45) + 's')">
- <circle class="memory-day-ripple" [attr.cx]="point.x" cy="106" r="2.8" />
- <circle class="memory-day-dot" [attr.cx]="point.x" cy="106" r="1.6" />
- </g>
- }
- <text x="26" y="122">第 1 天</text>
- <text x="109.4" y="122" text-anchor="middle">第 7 天</text>
- <text x="206.7" y="122" text-anchor="middle">第 14 天</text>
- <text x="304" y="122" text-anchor="end">第 21 天</text>
- </svg>
- </div>
- </figure>
- <div class="memory-stats">
- <span><strong>{{ memoryCompletedCount() }} / {{ memoryFilteredItems().length }}</strong><small>任务完成</small></span>
- <span><strong>{{ memoryDueCount() }}</strong><small>到期待复习</small></span>
- <span><strong>{{ memoryCalendar().scheduledDays }}</strong><small>本月安排天数</small></span>
- </div>
- </section>
- @if (canStartPointReading) {
- <section class="memory-student-filter" aria-label="筛选抗遗忘学员">
- <div><span>当前查看</span><strong>{{ memorySelectedStudentName() }}</strong></div>
- <label><span>学员</span><select aria-label="筛选抗遗忘学员" [value]="memoryStudentId()" (change)="selectMemoryStudent($event)"><option value="">全部学员({{ memoryStudents().length }}人)</option>@for (student of memoryStudents(); track student.id) { <option [value]="student.id">{{ student.name }}({{ student.taskCount }}项)</option> }</select></label>
- </section>
- }
- <section class="memory-calendar" aria-label="复习日历">
- <header>
- <div><span>复习日历</span><strong>{{ memoryCalendar().monthLabel }}</strong></div>
- <nav aria-label="切换月份">
- <button type="button" aria-label="上个月" (click)="shiftMemoryMonth(-1)"><lucide-icon [img]="icons.ChevronLeft" [size]="18" /></button>
- <button type="button" class="current-month" (click)="showCurrentMemoryMonth()">本月</button>
- <button type="button" aria-label="下个月" (click)="shiftMemoryMonth(1)"><lucide-icon [img]="icons.ChevronRight" [size]="18" /></button>
- </nav>
- </header>
- <div class="memory-week" aria-hidden="true"><span>一</span><span>二</span><span>三</span><span>四</span><span>五</span><span>六</span><span>日</span></div>
- <div class="memory-month-grid" role="grid" [attr.aria-label]="memoryCalendar().monthLabel + '复习安排'">
- @for (day of memoryCalendar().days; track day.date) {
- <button type="button" role="gridcell" class="memory-date" [class.outside]="!day.inCurrentMonth" [class.today]="day.isToday" [class.selected]="day.isSelected" [class.completed]="day.state === 'completed'" [class.due]="day.state === 'due'" [class.scheduled]="day.state === 'scheduled'" [attr.aria-label]="day.date + (day.total ? ',' + day.total + '项复习任务' : ',无复习任务')" (click)="selectMemoryDay(day)">
- <span>{{ day.day }}</span>
- @if (day.total) { <small>{{ day.completed }}/{{ day.total }}</small> }
- </button>
- }
- </div>
- <footer><span><i class="completed"></i>已完成</span><span><i class="due"></i>待复习</span><span><i class="scheduled"></i>未到期</span><span><i></i>无任务</span></footer>
- </section>
- <section class="memory-date-tasks" aria-live="polite">
- <header><div><span>当日复习</span><strong>{{ memorySelectedLabel() }}</strong></div><b>{{ memorySelectedTasks().length }} 项</b></header>
- <div class="calendar-list">
- @for (item of memorySelectedTasks(); track $index) {
- <article [class.ready]="calendarReady(item)" [class.completed]="calendarCompleted(item)">
- <span class="task-state"><i></i>{{ memoryTaskStatus(item) }}</span>
- <div class="task-copy"><span class="task-heading"><em class="review-sequence">第 {{ calendarReviewNumber(item) }} 次复习</em><strong>{{ calendarTitle(item) }}</strong></span><small class="task-context">{{ calendarStudent(item) }}<em>复习日期 {{ calendarDate(item) }}</em></small><small class="first-learned-at">{{ calendarFirstLearnedAt(item) }}</small></div>
- <button type="button" [disabled]="!calendarReady(item) && !calendarCompleted(item)" (click)="openCalendar(item)">{{ calendarCompleted(item) ? '查看' : calendarReady(item) ? (canStartPointReading ? '带学生复习' : '开始复习') : '未到期' }}</button>
- </article>
- } @empty {
- <div class="memory-empty"><lucide-icon [img]="icons.Clock3" [size]="26" /><strong>当天没有复习任务</strong><span>{{ canStartPointReading ? '选择有颜色标记的日期查看关联学员任务' : '可以选择有颜色标记的日期查看计划' }}</span></div>
- }
- </div>
- </section>
- } @else if (isRecordPage) {
- <section class="record-overview">
- <header><div><span>单词复习</span><strong>先回忆,再核对</strong></div><b>{{ seen().size }} / {{ words().length }}</b></header>
- <div><span><strong>{{ seen().size }}</strong><small>已核对</small></span><span><strong>{{ selected().size }}</strong><small>遗忘词</small></span></div>
- </section>
- <div class="record-tip">{{ orderInfo()['if_writing_tips'] || '复习单词并将遗忘词加入生词本' }}</div>
- <div class="record-list">
- @for (word of words(); track word.id; let index = $index) {
- <article [class.selected]="selected().has(word.id)" [class.revealed]="recordRevealed().has(word.id)">
- <button type="button" class="record-check" [attr.aria-label]="(selected().has(word.id) ? '取消标记 ' : '标记遗忘词 ') + word.word" (click)="toggleWord(word)">@if (selected().has(word.id)) { <lucide-icon [img]="icons.Check" [size]="17" /> }</button>
- <button type="button" class="record-word" [attr.aria-label]="recordRevealed().has(word.id) ? '隐藏中文释义' : '播放并查看中文释义'" (click)="toggleRecordAnswer(word)">
- <span class="record-index">{{ index + 1 }}</span>
- <span class="record-copy"><small>{{ recordRevealed().has(word.id) ? '中文释义' : '英文' }}</small><strong>{{ recordRevealed().has(word.id) ? (word.meaning || '暂无中文释义') : word.word }}</strong></span>
- <lucide-icon [img]="icons.Volume2" [size]="18" />
- </button>
- </article>
- }
- </div>
- <div class="sticky-actions">
- @if (!isWritingRecord()) { <button type="button" class="secondary-button" (click)="addRecordWords()">加入生词本</button> }
- <button type="button" class="primary-button" [disabled]="submitting()" (click)="endRecord()">结束本次复习</button>
- </div>
- } @else if (isAntiForgetPage) {
- <div class="anti-heading"><div><strong>今日复习任务</strong><span>已完成 {{ seen().size }} / {{ words().length }}</span></div><span>勾选遗忘词可加入生词本</span></div>
- <div class="anti-grid">
- @for (word of words(); track word.id) {
- <article [class.seen]="seen().has(word.id)">
- <button type="button" class="anti-check" [class.active]="selected().has(word.id)" aria-label="标记遗忘" (click)="toggleWord(word)">@if (selected().has(word.id)) { <lucide-icon [img]="icons.Check" [size]="16" /> }</button>
- <button type="button" class="anti-word" (click)="toggleSeen(word)">{{ seen().has(word.id) ? (word.meaning || word.word) : word.word }}</button>
- </article>
- }
- </div>
- <div class="sticky-actions single"><button type="button" class="primary-button" (click)="joinAntiWords()">加入生词本</button></div>
- } @else if (isExamPage) {
- @if (examSubmitted()) {
- <div class="report">
- <div class="report-icon"><lucide-icon [img]="icons.Flag" [size]="34" /></div>
- <h2>测试结果已保存</h2>
- <p>本次答对 {{ examScore() }} / {{ questions().length }} 题</p>
- <div class="report-numbers">
- <div><strong>{{ examScore() }}</strong><span>认识</span></div>
- <div><strong>{{ questions().length - examScore() }}</strong><span>待复习</span></div>
- <div><strong>{{ questions().length }}</strong><span>总题数</span></div>
- </div>
- <button type="button" class="primary-button" (click)="restart()"><lucide-icon [img]="icons.RotateCcw" [size]="18" />换一套再测</button>
- </div>
- } @else if (!examStarted()) {
- <div class="exam-intro">
- <div class="exam-mark"><lucide-icon [img]="icons.Flag" [size]="34" /></div>
- <h2>{{ askInfo()['Title'] || askInfo()['AskName'] || '词汇测试' }}</h2>
- <p>共 {{ questions().length }} 题,每题限时 10 秒</p>
- @if (previousExam()['GeneralID']) {
- <section class="previous-exam">
- <header><span>上次测试结果</span><strong>{{ examScoreValue(previousExam()) }} / {{ previousExam()['totalScore'] || questions().length }}</strong></header>
- <div class="previous-stats">
- <span><strong>{{ previousExam()['totalScore'] || questions().length }}</strong><small>测试</small></span>
- <span><strong>{{ examScoreValue(previousExam()) }}</strong><small>认识</small></span>
- <span><strong>{{ previousExam()['dontKnow'] || 0 }}</strong><small>不认识</small></span>
- <span><strong>{{ previousExam()['wrong'] || 0 }}</strong><small>选错</small></span>
- </div>
- <div class="exam-guidance"><strong>结果分析</strong><p>{{ examAnalysis(previousExam()) }}</p><strong>学习建议</strong><p>{{ examAdvice(previousExam()) }}</p></div>
- </section>
- }
- <button type="button" class="primary-button" [disabled]="!questions().length" (click)="startExam()"><lucide-icon [img]="icons.Play" [size]="18" />开始测试</button>
- </div>
- } @else {
- @if (currentQuestion(); as question) {
- <div class="exam-status"><span>第 {{ currentIndex() + 1 }} / {{ questions().length }} 题</span><strong><lucide-icon [img]="icons.Clock3" [size]="17" />{{ timer() }}s</strong></div>
- <article class="question-card">
- @if (question.QTitle) { <p class="question-word">{{ question.QTitle }}</p> }
- <div class="exam-options">
- @for (option of question.QOption; track $index) { <button type="button" (click)="answerExam(option, $index)">{{ option.text }}</button> }
- </div>
- </article>
- <div class="sticky-actions single"><button type="button" class="secondary-button" (click)="skipExamQuestion()">不认识,下一题</button></div>
- }
- }
- } @else if (isReportPage || completed()) {
- <div class="report">
- <div class="report-icon"><lucide-icon [img]="icons.Star" [size]="34" /></div>
- <h2>本次学习完成</h2>
- <p>坚持是最稳的进步方式</p>
- <div class="report-numbers">
- <div><strong>{{ words().length }}</strong><span>学习词数</span></div>
- <div><strong>{{ correct() }}</strong><span>已掌握</span></div>
- <div><strong>{{ unknown().length }}</strong><span>待复习</span></div>
- </div>
- <button type="button" class="primary-button" (click)="restart()"><lucide-icon [img]="icons.RotateCcw" [size]="18" />再学一次</button>
- <button type="button" class="secondary-button" (click)="leave()">返回首页</button>
- </div>
- } @else {
- <div class="empty-state"><div><lucide-icon [img]="icons.BookOpen" [size]="34" /><p>暂无学习内容</p></div></div>
- }
- </section>
- <ng-template #stageComplete let-title="title" let-text="text">
- <div class="report compact-report">
- <div class="report-icon"><lucide-icon [img]="icons.CheckCheck" [size]="34" /></div>
- <h2>{{ title }}</h2>
- <p>{{ text }}</p>
- <button type="button" class="primary-button" (click)="continueAfterStage()">继续下一关<lucide-icon [img]="icons.ChevronRight" [size]="18" /></button>
- </div>
- </ng-template>
|