Selaa lähdekoodia

feat(mobile): show read-only word progress to students

彭峰 1 kuukausi sitten
vanhempi
commit
4bdea542bb

+ 12 - 8
projects/xiaoshu-mobile/src/app/core/home-learning-modules.integration.spec.ts

@@ -253,12 +253,16 @@ describe('home learning modules', () => {
     await harness.fixture.whenStable();
     harness.fixture.detectChanges();
     const readonlyRouter = TestBed.inject(Router);
-    expect(readonlyRouter.url).toBe('/pages/stu/stu_con?nid=811&pnid=801&readonly=1');
-    expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('学生只读');
-    expect(harness.routeNativeElement?.querySelectorAll('.readonly-word-list article').length).toBe(2);
-    expect(harness.routeNativeElement?.querySelector('.readonly-word-list')?.textContent).toContain('apple');
-    expect(harness.routeNativeElement?.querySelector('.readonly-word-list')?.textContent).toContain('n. 苹果');
-    expect(harness.routeNativeElement?.querySelector('.repeat-list')).toBeNull();
+    expect(readonlyRouter.url).toBe('/pages/stu/stu_abc?nids=811&pnid=801&readonly=1');
+    expect(harness.routeNativeElement?.querySelector('.selection-header')?.textContent).toContain('词库学习进度');
+    expect(harness.routeNativeElement?.querySelector('.selection-header .primary-button')).toBeNull();
+    expect(harness.routeNativeElement?.querySelector('.pending-column')?.textContent).toContain('banana');
+    expect(harness.routeNativeElement?.querySelector('.word-select-check')).toBeNull();
+    expect(harness.routeNativeElement?.querySelector('.word-assessment')).toBeNull();
+    (harness.routeNativeElement?.querySelectorAll('.word-selection-tabs button')[1] as HTMLButtonElement).click();
+    harness.fixture.detectChanges();
+    expect(harness.routeNativeElement?.querySelector('.completed-column')?.textContent).toContain('apple');
+    expect(harness.routeNativeElement?.querySelector('.pending-column')).toBeNull();
 
     await harness.navigateByUrl('/pages/home/index');
     await harness.navigateByUrl('/pages/stu/stu_con?nid=813&pnid=801&readonly=1');
@@ -281,8 +285,8 @@ describe('home learning modules', () => {
     await harness.navigateByUrl('/pages/stu/stu_list?nid=999&bindingId=799');
     await harness.fixture.whenStable();
     harness.fixture.detectChanges();
-    expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_con?nid=999&readonly=1');
-    expect(harness.routeNativeElement?.querySelector('.readonly-word-header')?.textContent).toContain('学生只读');
+    expect(TestBed.inject(Router).url).toBe('/pages/stu/stu_abc?nids=999&readonly=1');
+    expect(harness.routeNativeElement?.querySelector('.selection-header')?.textContent).toContain('词库学习进度');
 
     await harness.navigateByUrl('/pages/stu/stu_calendar');
     expect(harness.fixture.nativeElement.querySelector('.tabbar')).toBeNull();

+ 15 - 9
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.html

@@ -89,7 +89,7 @@
     <header class="selection-header" [class.refine-selection-header]="isRefineSelectionPage">
       <div>
         @if (isRefineSelectionPage) {
-          <strong>本次待学 {{ selected().size }} / {{ selectionLimit }}</strong>
+          <strong>{{ isReadOnlySelectionPage ? '词库学习进度' : '本次待学 ' + selected().size + ' / ' + selectionLimit }}</strong>
           <span>已学完 {{ completedSelectionWords().length }} · 还没学 {{ pendingSelectionWords().length }}</span>
         } @else {
           <strong>已选 {{ selected().size }} / {{ selectionLimit }}</strong>
@@ -98,9 +98,11 @@
       </div>
       <div class="header-actions">
         @if (!isRefineSelectionPage) { <button type="button" class="quiet-button" (click)="toggleAllWords()">{{ selected().size ? '取消全选' : '全选' }}</button> }
-        <button type="button" class="primary-button" [disabled]="submitting()" (click)="startSelected()">
-          <lucide-icon [img]="icons.Play" [size]="17" />{{ isRefineSelectionPage ? selectionActionLabel() : '开始学习' }}
-        </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) {
@@ -116,21 +118,25 @@
 
         @if (activeSelectionBucket() === 'pending') {
           <section class="word-selection-list pending-column" role="tabpanel" aria-label="还没学的单词">
-            <p class="word-column-help">先回忆中文,核对后选择“认识”或“加入学习”。</p>
+            <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">
-                    <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>
+                    @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)) {
+                  @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>

+ 9 - 5
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.ts

@@ -244,7 +244,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   private antiStartedAt = Date.now();
 
   ngOnInit(): void {
-    if (isTeacherLedLearningPath(this.meta.path) && !this.isReadOnlyWordView && !this.canStartPointReading) {
+    if (isTeacherLedLearningPath(this.meta.path) && !this.isReadOnlyWordView && !this.isReadOnlySelectionPage && !this.canStartPointReading) {
       this.platform.toast('点读学习由陪练老师在上课时发起', 'error');
       void this.router.navigateByUrl('/pages/home/cart');
       return;
@@ -289,6 +289,10 @@ export class LearningPageComponent implements OnInit, OnDestroy {
     return this.meta.path === 'pages/stu/stu_con';
   }
 
+  get isReadOnlySelectionPage(): boolean {
+    return !this.canStartPointReading && this.isRefineSelectionPage && this.query('readonly') === '1';
+  }
+
   get canStartPointReading(): boolean {
     return canStartTeacherLedLearning(this.session.groupId());
   }
@@ -389,8 +393,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
       return;
     }
     if (this.isUnitListPage && !this.canStartPointReading) {
-      void this.router.navigate(['/pages/stu/stu_con'], {
-        queryParams: { nid: id, pnid: this.query('nid'), readonly: 1 },
+      void this.router.navigate(['/pages/stu/stu_abc'], {
+        queryParams: { nids: id, pnid: this.query('nid'), readonly: 1 },
       });
       return;
     }
@@ -1021,8 +1025,8 @@ export class LearningPageComponent implements OnInit, OnDestroy {
       next: (nodes) => {
         const rows = this.findList(nodes.result);
         if (this.isUnitListPage && !this.canStartPointReading && !rows.length && pid) {
-          void this.router.navigate(['/pages/stu/stu_con'], {
-            queryParams: { nid: pid, readonly: 1 },
+          void this.router.navigate(['/pages/stu/stu_abc'], {
+            queryParams: { nids: pid, readonly: 1 },
             replaceUrl: true,
           });
           return;

+ 1 - 0
projects/xiaoshu-mobile/src/styles.scss

@@ -812,6 +812,7 @@ app-learning-page .word-selection-tabs b { min-width: 25px; padding: 3px 7px; co
 app-learning-page .word-selection-tabs button.active b { background: var(--brand-pale); }
 app-learning-page .word-column-help { min-height: 51px; margin: 0; padding: 10px 14px; color: var(--ink-soft); border-bottom: 1px solid var(--line); font-size: 11px; line-height: 1.45; }
 app-learning-page .word-complete-check { display: grid; width: 32px; height: 32px; color: white; background: var(--brand-strong); border-radius: 50%; place-items: center; }
+app-learning-page .word-pending-mark { display: grid; width: 32px; height: 32px; color: #9a6407; background: var(--accent-pale); border-radius: 50%; place-items: center; }
 app-learning-page .completed-word { background: color-mix(in srgb, var(--brand-pale) 58%, white); }
 app-learning-page .completed-word-action { display: flex; padding: 0 14px 12px 56px; align-items: center; justify-content: space-between; color: var(--ink-soft); font-size: 10px; gap: 8px; }
 app-learning-page .completed-word-action button { min-height: 30px; padding: 0 9px; color: var(--brand-strong); background: white; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 10px; white-space: nowrap; }