Selaa lähdekoodia

refactor(admin): show training records in compact numbered rows

彭峰 58 minuuttia sitten
vanhempi
commit
e1619e6470

+ 54 - 4
projects/xiaoshu-admin/src/app/pages/lesson-records.component.html

@@ -6,10 +6,60 @@
 <div class="filter-actions"><button type="submit" class="ui-button" [disabled]="busy()">查询</button><button type="button" class="ui-button secondary" [disabled]="busy()" (click)="reset()">重置</button></div></form>
 @if(error()){<p role="alert" class="notice error">{{error()}}</p>}@if(busy()){<p class="notice" role="status">正在查询训练记录…</p>}
 @if(data();as d){<div class="record-count"><strong>累计已训练 {{d.cumulativeTotal}} 节</strong><span>当前筛选 {{d.total}} 节 · 时间均为北京时间</span><label>每页<select [(ngModel)]="pageSize" (ngModelChange)="query()" [disabled]="busy()"><option [ngValue]="20">20 条</option><option [ngValue]="50">50 条</option><option [ngValue]="100">100 条</option></select></label>@if(studentId||memberId){<button class="ui-button secondary" [disabled]="busy()" (click)="showAll()">查看该学员全部结果</button>}</div>
-<section class="content-card"><div class="table-scroll"><table class="data-table lesson-table"><thead><tr><th>学员 / 课程</th><th>训练时间 ↓</th><th>授课老师 / 课型</th><th>反馈摘要</th><th>当前复习陪练</th><th>操作</th></tr></thead><tbody>
-@for(row of d.items;track row.lessonId){<tr><td><strong>{{row.studentName}}</strong><small>ID {{row.studentId}} · {{row.storeName}}</small><p>{{row.courseName}}</p></td><td class="time-cell">{{row.lessonAt | date:'yyyy-MM-dd HH:mm':'+0800'}}</td><td>{{row.coachName}}<small>{{row.classTypeLabel}}</small></td><td><p class="feedback-excerpt">{{row.feedbackSummary||'未记录'}}</p>@if(row.duplicateCount>1){<small class="exception">同课次有重复来源,已合并计数</small>}</td><td>{{row.reviewCoachLabel}}<small>待复习 {{row.pendingReviews}} 次</small></td><td><button class="ui-button secondary compact" (click)="detailId.set(row.lessonId)">查看反馈</button></td></tr>}
-@empty{<tr><td colspan="6"><div class="empty">没有符合条件的训练记录,可调整姓名或日期后查询。</div></td></tr>}
-</tbody></table></div></section>
+<section class="content-card">
+  <div class="table-scroll" role="region" aria-label="训练记录列表" tabindex="0">
+    <table class="data-table lesson-table">
+      <colgroup>
+        <col class="number-col"><col class="student-col"><col class="id-col"><col class="store-col">
+        <col class="course-col"><col class="time-col"><col class="coach-col"><col class="class-col">
+        <col><col class="review-col"><col class="pending-col"><col class="action-col">
+      </colgroup>
+      <thead>
+        <tr>
+          <th scope="col" class="number-cell">序号</th>
+          <th scope="col">学员</th>
+          <th scope="col">会员 ID</th>
+          <th scope="col">门店</th>
+          <th scope="col">课程</th>
+          <th scope="col" aria-sort="descending">训练时间 <span aria-hidden="true">↓</span></th>
+          <th scope="col">授课老师</th>
+          <th scope="col">课型</th>
+          <th scope="col">反馈摘要</th>
+          <th scope="col">当前复习陪练</th>
+          <th scope="col" class="number-cell">待复习</th>
+          <th scope="col">操作</th>
+        </tr>
+      </thead>
+      <tbody>
+        @for (row of d.items; track row.lessonId; let index = $index) {
+          <tr>
+            <td class="number-cell">{{ (d.page - 1) * d.pageSize + index + 1 }}</td>
+            <td [attr.title]="row.studentName"><strong>{{ row.studentName }}</strong></td>
+            <td class="id-cell">{{ row.studentId }}</td>
+            <td [attr.title]="row.storeName">{{ row.storeName }}</td>
+            <td [attr.title]="row.courseName">{{ row.courseName }}</td>
+            <td class="time-cell">{{ row.lessonAt | date:'yyyy-MM-dd HH:mm':'+0800' }}</td>
+            <td [attr.title]="row.coachName">{{ row.coachName }}</td>
+            <td [attr.title]="row.classTypeLabel">{{ row.classTypeLabel }}</td>
+            <td>
+              <div class="feedback-cell">
+                <span class="feedback-excerpt" [attr.title]="row.feedbackSummary || null">{{ row.feedbackSummary || '未记录' }}</span>
+                @if (row.duplicateCount > 1) {
+                  <span class="source-warning" title="同课次有重复来源,已合并计数">已合并</span>
+                }
+              </div>
+            </td>
+            <td [attr.title]="row.reviewCoachLabel">{{ row.reviewCoachLabel }}</td>
+            <td class="number-cell">{{ row.pendingReviews }} 次</td>
+            <td><button class="ui-button secondary compact" (click)="detailId.set(row.lessonId)">查看反馈</button></td>
+          </tr>
+        } @empty {
+          <tr><td colspan="12"><div class="empty">没有符合条件的训练记录,可调整姓名或日期后查询。</div></td></tr>
+        }
+      </tbody>
+    </table>
+  </div>
+</section>
 <div class="lesson-pagination"><span>已显示 {{d.items.length}} 条 / 共 {{d.total}} 条</span>@if(!all){<div><button class="ui-button secondary" [disabled]="busy()||page<=1" (click)="turn(-1)">上一页</button><span>第 {{page}} 页</span><button class="ui-button secondary" [disabled]="busy()||page*pageSize>=d.total" (click)="turn(1)">下一页</button></div>}</div>}
 @if(detailId()){<app-lesson-record-detail [lessonId]="detailId()" (closed)="detailId.set('')" (changed)="load()" />}
 </section>

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
projects/xiaoshu-admin/src/app/pages/lesson-records.component.scss


+ 21 - 0
projects/xiaoshu-admin/src/app/pages/lesson-records.component.spec.ts

@@ -7,6 +7,27 @@ describe('训练记录与复习转交',()=>{
  let ops:jasmine.SpyObj<OperationsService>;
  beforeEach(()=>{ops=jasmine.createSpyObj<OperationsService>('OperationsService',['request']);TestBed.configureTestingModule({providers:[{provide:OperationsService,useValue:ops},{provide:ActivatedRoute,useValue:{snapshot:{queryParamMap:convertToParamMap({})}}}]});});
  const page=(items:unknown[],total=2)=>({items,total,cumulativeTotal:51,page:1,pageSize:100,version:'snapshot1'});
+ it('训练记录逐列显示,翻页按返回的页码连续编号',async()=>{
+  const row={lessonId:'lesson-a',studentName:'灵灵',studentId:1582,storeName:'南昌总店',courseName:'人教版2024年9月录',
+   lessonAt:'2026-11-10T08:33:00Z',coachName:'明慧陪练',classTypeLabel:'单词课',feedbackSummary:'本节学习反馈',
+   reviewCoachLabel:'明慧陪练',pendingReviews:10,duplicateCount:2};
+  ops.request.and.resolveTo({...page([row],23),pageSize:20});
+  const fixture=TestBed.createComponent(LessonRecordsComponent);
+  fixture.detectChanges();
+  await fixture.whenStable();
+  fixture.detectChanges();
+  const cells=()=>Array.from(fixture.nativeElement.querySelectorAll('.lesson-table tbody tr:first-child td') as NodeListOf<HTMLTableCellElement>,cell=>cell.textContent!.trim());
+  expect(cells().slice(0,8)).toEqual(['1','灵灵','1582','南昌总店','人教版2024年9月录','2026-11-10 16:33','明慧陪练','单词课']);
+  expect(cells().slice(9)).toEqual(['明慧陪练','10 次','查看反馈']);
+  expect(fixture.nativeElement.querySelector('.source-warning').title).toBe('同课次有重复来源,已合并计数');
+  ops.request.and.resolveTo({...page([{...row,lessonId:'lesson-b'}],23),page:2,pageSize:20});
+  fixture.componentInstance.turn(1);
+  fixture.detectChanges();
+  expect(cells()[0]).toBe('1');
+  await fixture.whenStable();
+  fixture.detectChanges();
+  expect(cells()[0]).toBe('21');
+ });
  it('菜单返回保留筛选和已读数据,30 秒后后台更新失败仍可查看',async()=>{
   let now=100_000;spyOn(Date,'now').and.callFake(()=>now);
   const c=TestBed.runInInjectionContext(()=>new LessonRecordsComponent());c.coachName='钱老师';ops.request.and.resolveTo(page([{lessonId:'a'}]));await c.load();

+ 5 - 5
scripts/check-lesson-records-browser.mjs

@@ -7,14 +7,14 @@ export async function checkLessonRecordsBrowser({auth,output,studentId=1857,memb
  try{const context=await browser.newContext({viewport:{width:1440,height:1050}});await context.addInitScript(auth=>{localStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify(auth));sessionStorage.setItem('xiaoshu_admin_session_v1',JSON.stringify(auth));},auth);
  const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message));
  await page.goto('http://127.0.0.1:4300/admin/lessons?studentId='+studentId);await page.getByRole('button',{name:'查看反馈',exact:true}).first().waitFor({timeout:60000});
- assert.match(await page.locator('.record-count').innerText(),/累计已上课/);checks.push('集中查询入口、真实数据与累计课次');
+ assert.match(await page.locator('.record-count').innerText(),/累计已训练/);checks.push('集中查询入口、真实数据与累计课次');
  const total=Number((await page.locator('.record-count').innerText()).match(/当前筛选 (\d+) 节/)[1]);
  if(total>20){assert.equal(await page.locator('.lesson-table tbody tr').count(),20);const first=await page.locator('.lesson-table tbody tr').first().innerText();await page.getByRole('button',{name:'下一页',exact:true}).click();await page.waitForFunction(()=>document.querySelector('.lesson-pagination')?.textContent.includes('第 2 页')&&!document.querySelector('[role=status]'));assert.notEqual(await page.locator('.lesson-table tbody tr').first().innerText(),first);checks.push('超过一页的真实记录可以翻页');}
  await page.getByRole('button',{name:'查看该学员全部结果'}).click();await page.waitForFunction(n=>document.querySelectorAll('.lesson-table tbody tr').length===n&&!document.querySelector('[role=status]'),total,{timeout:60000});
- const dates=await page.locator('.lesson-table tbody tr td:nth-child(2)').allTextContents();assert.deepEqual(dates,[...dates].sort().reverse());checks.push('全部历史行数与累计一致、北京时间倒序');
+ const dates=await page.locator('.lesson-table tbody .time-cell').allTextContents();assert.deepEqual(dates,[...dates].sort().reverse());checks.push('全部历史行数与累计一致、北京时间倒序');
  await page.screenshot({path:output+'/history-desktop.png',fullPage:true});
- const downloadPromise=page.waitForEvent('download');await page.getByRole('button',{name:'导出 Excel',exact:true}).click();const download=await downloadPromise;assert.match(download.suggestedFilename(),/上课记录.*xlsx/);await download.saveAs(output+'/lesson-records.xlsx');const book=XLSX.read(readFileSync(output+'/lesson-records.xlsx'));assert.equal(XLSX.utils.sheet_to_json(book.Sheets[book.SheetNames[0]]).length,total);checks.push('真实筛选结果导出 Excel,行数与课次一致');
- await page.getByRole('button',{name:'查看反馈',exact:true}).first().click();await page.getByRole('dialog',{name:'上课记录详情'}).waitFor();await page.getByRole('heading',{name:'老师点评',exact:true}).waitFor({timeout:60000}).catch(async e=>{await page.screenshot({path:output+'/detail-error.png',fullPage:true});throw Error((await page.getByRole('dialog').innerText())+' '+e.message);});
+ const downloadPromise=page.waitForEvent('download');await page.getByRole('button',{name:'导出 Excel',exact:true}).click();const download=await downloadPromise;assert.equal(download.suggestedFilename(),'训练记录.xlsx');await download.saveAs(output+'/lesson-records.xlsx');const book=XLSX.read(readFileSync(output+'/lesson-records.xlsx'));assert.equal(XLSX.utils.sheet_to_json(book.Sheets[book.SheetNames[0]]).length,total);checks.push('真实筛选结果导出 Excel,行数与课次一致');
+ await page.getByRole('button',{name:'查看反馈',exact:true}).first().click();await page.getByRole('dialog',{name:'训练记录详情'}).waitFor();await page.getByRole('heading',{name:'老师点评',exact:true}).waitFor({timeout:60000}).catch(async e=>{await page.screenshot({path:output+'/detail-error.png',fullPage:true});throw Error((await page.getByRole('dialog').innerText())+' '+e.message);});
  const picker=page.getByRole('combobox',{name:'目标陪练'});
  for(const viewport of [{width:1440,height:720},{width:390,height:640}]){
   await page.setViewportSize(viewport);await page.getByRole('button',{name:'更换抗遗忘陪练'}).click();
@@ -30,7 +30,7 @@ export async function checkLessonRecordsBrowser({auth,output,studentId=1857,memb
  assert.equal(await page.locator('#transfer-coach-results').getByRole('option').first().evaluate(e=>getComputedStyle(e).backgroundColor),'rgb(255, 255, 255)');await page.locator('.transfer-panel').screenshot({path:output+'/coach-search.png'});checks.push('陪练姓名模糊搜索、键盘选择、无结果提示及白底列表;未提交真实转交');
  await page.screenshot({path:output+'/feedback-desktop.png',fullPage:true});await page.getByRole('button',{name:'关闭',exact:true}).click();
  await page.setViewportSize({width:390,height:844});await page.screenshot({path:output+'/history-mobile-width.png',fullPage:true});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=window.innerWidth+1));checks.push('窄屏无整页横向溢出');
- if(memberObjectId){await page.setViewportSize({width:1440,height:1050});await page.goto('http://127.0.0.1:4300/admin/members/'+memberObjectId);await page.getByRole('button',{name:'上课记录',exact:true}).click();await page.locator('app-lesson-records .data-table tbody tr').first().waitFor();assert.equal(await page.locator('nav.pagination').count(),0);checks.push('会员详情复用同一列表且不重复分页');}
+ if(memberObjectId){await page.setViewportSize({width:1440,height:1050});await page.goto('http://127.0.0.1:4300/admin/members/'+memberObjectId);await page.getByRole('button',{name:'训练记录',exact:true}).click();await page.locator('app-lesson-records .data-table tbody tr').first().waitFor();assert.equal(await page.locator('nav.pagination').count(),0);checks.push('会员详情复用同一列表且不重复分页');}
  assert.deepEqual(errors,[]);writeFileSync(output+'/browser-result.json',JSON.stringify({passed:true,checks},null,2));return{passed:true,checks};
  }finally{await browser.close();}
 }

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä