Parcourir la source

feat(mobile): allow word pronunciation in learning history

彭峰 il y a 2 semaines
Parent
commit
f358d25962

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

@@ -23,6 +23,7 @@ class LearningModulesApiService {
   lastMemoryParams: Record<string, unknown> = {};
   lastStandaloneLearningRecord: Record<string, unknown> = {};
   appointmentStatus = 0;
+  historyWordAudio = '';
   completionRequests: Record<string, unknown>[] = [];
   completionResponse = new Subject<ApiEnvelope<unknown>>();
   lastAppointmentRecordParams: Record<string, unknown> = {};
@@ -252,7 +253,7 @@ class LearningModulesApiService {
     if (action === 'e_record_detail') {
       return of({
         retcode: 1,
-        result: [{ GeneralID: 701, Title: 'apple', detail: [{ GeneralID: 701, Title: 'apple', sy: '苹果', yp: '' }] }],
+        result: [{ GeneralID: 701, Title: 'apple', detail: [{ GeneralID: 701, Title: 'apple', sy: '苹果', yp: this.historyWordAudio }] }],
         addon: [{ if_writing: '0', if_writing_tips: '先回忆单词,再查看释义' }],
       } as unknown as ApiEnvelope<T>);
     }
@@ -325,6 +326,48 @@ describe('home learning modules', () => {
     expect(play).not.toHaveBeenCalled();
   });
 
+  it('plays student history words on click without changing learning records or enabling unit preview audio', async () => {
+    const api = TestBed.inject(ApiService) as unknown as LearningModulesApiService;
+    api.historyWordAudio = 'apple.mp3';
+    const post = spyOn(api, 'post').and.callThrough();
+    const platform = TestBed.inject(PlatformService);
+    const play = spyOn(platform, 'play');
+    const stopAudio = spyOn(platform, 'stopAudio');
+    const harness = await RouterTestingHarness.create();
+    await harness.navigateByUrl('/pages/his/his');
+    (harness.routeNativeElement?.querySelector('.learning-record-card .learning-item') as HTMLElement).click();
+    await harness.fixture.whenStable();
+    harness.fixture.detectChanges();
+    expect(TestBed.inject(Router).url).toBe('/pages/his/his_words?id=201');
+    expect(play).not.toHaveBeenCalled();
+    post.calls.reset();
+    const button = harness.routeNativeElement?.querySelector('.history-word-audio') as HTMLButtonElement;
+    expect(button.disabled).toBeFalse();
+    expect(button.getAttribute('aria-label')).toBe('播放单词读音:apple');
+    button.querySelector('strong')!.click();
+    (button.querySelector('lucide-icon') as HTMLElement).click();
+    expect(play).toHaveBeenCalledTimes(2);
+    expect(play.calls.mostRecent().args[0]).toContain('apple.mp3');
+    expect(post).not.toHaveBeenCalled();
+
+    const learning = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
+    learning.words.update((words) => words.map((word) => ({ ...word, audio: '' })));
+    harness.fixture.detectChanges();
+    expect(button.disabled).toBeTrue();
+    expect(harness.routeNativeElement?.textContent).toContain('暂无音频');
+    button.click();
+    expect(play).toHaveBeenCalledTimes(2);
+
+    stopAudio.calls.reset();
+    await harness.navigateByUrl('/pages/home/index');
+    expect(stopAudio).toHaveBeenCalled();
+    await harness.navigateByUrl('/pages/stu/stu_con?nid=811&readonly=1');
+    expect(harness.routeNativeElement?.querySelector('.history-word-audio')).toBeNull();
+    const preview = harness.fixture.debugElement.query(By.directive(LearningPageComponent)).componentInstance as LearningPageComponent;
+    preview.words().forEach((word) => preview.play(word));
+    expect(play).toHaveBeenCalledTimes(2);
+  });
+
   it('still pronounces pending and completed words when a coach reveals them', async () => {
     user.set({ userId: 300, userName: 'coach', honeyName: '张老师', groupId: 3 });
     session.groupId.set(3);

+ 12 - 2
projects/xiaoshu-mobile/src/app/features/learning/learning-page.component.html

@@ -65,7 +65,17 @@
       <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>
+            <div>
+              @if (isHistoryWordView) {
+                <button type="button" class="history-word-audio" [disabled]="!word.audio" [attr.aria-label]="word.audio ? '播放单词读音:' + word.word : word.word + '暂无音频'" (click)="play(word)">
+                  <span><strong>{{ word.word }}</strong>@if (word.phonetic) { <small>{{ word.phonetic }}</small> }</span>
+                  <lucide-icon [img]="icons.Volume2" [size]="20" aria-hidden="true" />
+                </button>
+                @if (!word.audio) { <small>暂无音频</small> }
+              } @else {
+                <strong>{{ word.word }}</strong>@if (word.phonetic) { <small>{{ word.phonetic }}</small> }
+              }
+            </div>
             <p>{{ word.meaning || '暂无释义' }}</p>
           </article>
         } @empty {
@@ -84,7 +94,7 @@
           <span>{{ wordPreviewPage() }} / {{ wordPreviewPageCount() }} 页</span>
         </nav>
       }
-      <p class="readonly-word-note">{{ isHistoryWordView ? '这里展示本次学习记录中的单词,不会改动原学习结果' : '学生可查看老师分配的单词,点读、跟读和学习记录由陪练老师在上课时操作' }}</p>
+      <p class="readonly-word-note">{{ isHistoryWordView ? '点击单词或喇叭可听读音,复习不会改动原学习结果' : '学生可查看老师分配的单词,点读、跟读和学习记录由陪练老师在上课时操作' }}</p>
     </section>
   } @else if (isSelectionPage) {
     <header class="selection-header" [class.refine-selection-header]="isRefineSelectionPage">

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

@@ -625,7 +625,7 @@ export class LearningPageComponent implements OnInit, OnDestroy {
   }
 
   play(word = this.current()): void {
-    if (this.isReadOnlySelectionPage || this.isReadOnlyWordView) return;
+    if (this.isReadOnlySelectionPage || (this.isReadOnlyWordView && !this.isHistoryWordView)) return;
     if (!word?.audio) {
       this.platform.toast('该单词暂无音频', 'error');
       return;

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

@@ -752,6 +752,29 @@ app-learning-page .readonly-word-list small { color: var(--ink-soft); font-size:
 app-learning-page .readonly-word-list p { margin: 0; color: var(--ink); font-size: 14px; line-height: 1.5; }
 app-learning-page .readonly-word-note { margin: 12px 14px 0; padding: 10px 12px; color: var(--ink-soft); background: var(--brand-wash); border: 1px solid var(--line); border-radius: var(--radius-md); font-size: 11px; line-height: 1.5; text-align: center; }
 
+app-learning-page .history-word-audio {
+  display: flex;
+  min-width: 0;
+  min-height: 44px;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0;
+  color: var(--brand-strong);
+  background: transparent;
+  border: 0;
+  border-radius: 6px;
+  text-align: left;
+  gap: 8px;
+  cursor: pointer;
+
+  > span { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
+  strong { white-space: normal; overflow-wrap: anywhere; }
+  lucide-icon { flex-shrink: 0; }
+  &:focus-visible { outline: 2px solid var(--brand-strong); outline-offset: 3px; }
+  &:disabled { cursor: default; }
+  &:disabled lucide-icon { opacity: .35; }
+}
+
 app-study-records-page .current-learning-toggle {
   display: flex;
   width: 100%;