Pārlūkot izejas kodu

fix: 同步教师端统计与预约详情功能

彭峰 2 mēneši atpakaļ
vecāks
revīzija
7e7673311c

+ 2 - 2
src/app/features/appointment/appointment-page.component.html

@@ -20,10 +20,10 @@
 
     @if (!isStudentView && status() <= 10 && !isWriting) {
       <section class="course-section">
-        <label><span>本次课程</span><select [(ngModel)]="selectedCourse" (change)="chooseCourse()"><option value="">请选择课程</option>@for (course of courses(); track course['kcid'] ?? $index) { <option [value]="course['kcid']">{{ course['Title'] }}</option> }</select></label>
+        <label><span>本次课程</span><select [(ngModel)]="selectedCourse" (change)="chooseCourse()"><option value="">请选择课程</option>@for (course of courses(); track $index) { <option [value]="course['kcid']">{{ course['Title'] }}</option> }</select></label>
         <div class="unit-heading"><strong>选择学习单元</strong><span>可多选,长按标记功能在 Web 中改为右侧按钮</span></div>
         <div class="unit-grid">
-          @for (unit of units(); track unit['NodeID'] ?? $index) {
+          @for (unit of units(); track $index) {
             <article [class.selected]="selectedUnits().has(unitId(unit))" [class.completed]="completedUnits().has(unitId(unit))">
               <button type="button" class="unit-select" (click)="toggleUnit(unit)"><span class="check">@if (selectedUnits().has(unitId(unit))) { <lucide-icon [img]="icons.Check" [size]="16" /> }</span><strong>{{ unitTitle(unit) }}</strong></button>
               <button type="button" class="mark-complete" title="标记已学完" aria-label="标记已学完" (click)="markUnitComplete(unit)"><lucide-icon [img]="icons.BookCheck" [size]="17" /></button>

+ 17 - 7
src/app/features/home/account-page.component.html

@@ -15,18 +15,28 @@
     </div>
     <div class="companion-summary">
       <span>累计陪练</span>
-      @if (companionLoading()) { <strong>正在同步...</strong> }
+      @if (session.groupId() === 3 && coachPanelLoading()) { <strong>正在同步...</strong> }
+      @else if (session.groupId() === 3) { <strong>{{ coachPanel().totalHours | number:'1.0-1' }} 小时</strong><em>上课 {{ coachPanel().totalLessons }} 次</em> }
+      @else if (companionLoading()) { <strong>正在同步...</strong> }
       @else { <strong>{{ companionSummary().durationLabel }}</strong><em>已结束 {{ companionSummary().completedSessions }} 次</em> }
     </div>
-    <div class="learning-rights">
-      <div><strong>{{ membership().remainingCompanionHours }}</strong><span>剩余陪练时长</span></div>
-      <div><strong>{{ membership().software30 }}</strong><span>剩余30分钟软件</span></div>
-      <div><strong>{{ membership().software60 }}</strong><span>剩余60分钟软件</span></div>
-    </div>
+    @if (session.groupId() === 3) {
+      <div class="learning-rights">
+        <div><strong>{{ coachPanel().course30 }}</strong><span>30分钟课程</span></div>
+        <div><strong>{{ coachPanel().course60 }}</strong><span>60分钟课程</span></div>
+        <div><strong>{{ coachPanel().trialLessons }}</strong><span>体验课</span></div>
+      </div>
+    } @else {
+      <div class="learning-rights">
+        <div><strong>{{ membership().remainingCompanionHours }}</strong><span>剩余陪练时长</span></div>
+        <div><strong>{{ membership().software30 }}</strong><span>剩余30分钟软件</span></div>
+        <div><strong>{{ membership().software60 }}</strong><span>剩余60分钟软件</span></div>
+      </div>
+    }
   </section>
   <section class="wallet-strip"><a routerLink="/pages/member/point"><strong>{{ session.user()?.point || 0 }}</strong><span>学习令</span></a><a routerLink="/pages/member/coin"><strong>{{ session.user()?.silverCoin || 0 }}</strong><span>学习豆</span></a><a routerLink="/pages/member/purse"><strong>{{ session.user()?.purse || 0 }}</strong><span>钱包余额</span></a></section>
   @if (session.groupId() === 3) {
-    <section class="role-panel"><header><h2>陪练专用</h2><a [routerLink]="orderCounts()['all'] ? '/pages/member/myorder' : '/pages/member/feedback'">{{ orderCounts()['all'] ? '全部' + orderCounts()['all'] + '条订单' : '休息时间申请' }}</a></header><div class="order-strip two"><a routerLink="/pages/member/ke_xiao"><strong>{{ orderCounts()['unpaid'] }}</strong><span>销课记录</span></a><a routerLink="/pages/member/ke_fuxi"><strong>{{ orderCounts()['paid'] }}</strong><span>复习收入</span></a></div></section>
+    <section class="role-panel"><header><h2>陪练专用</h2><a routerLink="/pages/member/feedback">休息时间申请</a></header><div class="order-strip coach-strip"><a routerLink="/pages/home/shop"><strong>{{ coachPanel().todaySchedules }}</strong><span>今日排课</span></a><a routerLink="/pages/member/ke_xiao"><strong>{{ coachPanel().completedLessons }}</strong><span>销课记录</span></a><a routerLink="/pages/member/ke_fuxi"><strong>¥{{ coachPanel().reviewIncome | number:'1.0-2' }}</strong><span>复习收入</span></a></div></section>
   } @else {
     <section class="role-panel course-memory">
       <header><h2>我的课程</h2><a routerLink="/pages/stu/stu_calendar">科学抗遗忘</a></header>

+ 1 - 0
src/app/features/home/account-page.component.scss

@@ -62,6 +62,7 @@
 .memory-state.empty span { color: var(--ink-soft); grid-column: 1; }
 .memory-state.empty lucide-icon { grid-column: 2; grid-row: 1 / 3; }
 .order-strip.two { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.order-strip.coach-strip { grid-template-columns: repeat(3, minmax(0, 1fr)); }
 .order-strip a { display: flex; min-width: 0; min-height: 66px; align-items: center; justify-content: center; color: var(--ink); text-decoration: none; flex-direction: column; gap: 4px; }
 .order-strip strong { color: var(--brand-strong); font-size: 15px; }
 .order-strip span { max-width: 100%; overflow: hidden; color: var(--ink-soft); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }

+ 84 - 7
src/app/features/home/account-page.component.ts

@@ -12,6 +12,18 @@ import { buildAccountMembership, summarizeCompanionLearning } from './account-me
 
 type SourceItem = Record<string, unknown>;
 
+interface CoachPanelSummary {
+  todaySchedules: number;
+  completedLessons: number;
+  reviewIncome: number;
+  reviewRecords: number;
+  totalLessons: number;
+  totalHours: number;
+  course30: number;
+  course60: number;
+  trialLessons: number;
+}
+
 @Component({
   selector: 'app-account-page',
   standalone: true,
@@ -23,7 +35,8 @@ type SourceItem = Record<string, unknown>;
 export class AccountPageComponent implements OnInit {
   readonly session = inject(SessionService);
   readonly version = APP_VERSION;
-  readonly orderCounts = signal<Record<string, number>>({ all: 0, unpaid: 0, paid: 0, issued: 0, signed: 0 });
+  readonly coachPanel = signal<CoachPanelSummary>({ todaySchedules: 0, completedLessons: 0, reviewIncome: 0, reviewRecords: 0, totalLessons: 0, totalHours: 0, course30: 0, course60: 0, trialLessons: 0 });
+  readonly coachPanelLoading = signal(false);
   readonly antiForgetItems = signal<SourceItem[]>([]);
   readonly antiForgetLoading = signal(false);
   readonly antiForgetError = signal('');
@@ -53,12 +66,7 @@ export class AccountPageComponent implements OnInit {
       this.loadAntiForget();
       return;
     }
-    for (const fast of ['all', 'unpaid', 'paid', 'issued', 'signed']) {
-      this.api.get<unknown>('order_list', { uid, fast, psize: 1, norc: 1 }).subscribe({
-        next: ({ page }) => this.orderCounts.update((counts) => ({ ...counts, [fast]: Number(page?.itemCount ?? 0) })),
-        error: () => undefined,
-      });
-    }
+    this.loadCoachPanel(uid);
   }
 
   private loadCompanionSummary(uid: number): void {
@@ -107,7 +115,76 @@ export class AccountPageComponent implements OnInit {
     });
   }
 
+  private loadCoachPanel(uid: number): void {
+    this.coachPanelLoading.set(true);
+    forkJoin({
+      stats: this.api.post<Record<string, number>>('e_order_tongji', { uid }).pipe(catchError(() => of(null))),
+      today: this.api.get<unknown>('content_list', {
+        uid,
+        nid: 29,
+        myfield2: `pl=${uid}`,
+        dateFilter: this.today(),
+        psize: 200,
+      }).pipe(catchError(() => of(null))),
+      completed: this.api.get<unknown>('content_list', {
+        uid,
+        nid: 29,
+        myfield2: `pl=${uid}`,
+        orders: 'B.yysj=DESC',
+        dszt: 30,
+        xiaokejilu: 1,
+        cpage: 1,
+        psize: 1,
+      }).pipe(catchError(() => of(null))),
+      incomeList: this.api.post<unknown>('e_get_21list', {
+        status: 1,
+        shouru: 1,
+        uid,
+        cpage: 1,
+        psize: 1,
+      }).pipe(catchError(() => of(null))),
+      incomeTotal: this.api.post<unknown>('e_get_21list_tj', { uid }).pipe(catchError(() => of(null))),
+    }).pipe(finalize(() => this.coachPanelLoading.set(false))).subscribe(({ stats, today, completed, incomeList, incomeTotal }) => {
+      const statValues = stats?.result ?? {};
+      this.coachPanel.set({
+        todaySchedules: this.itemCount(today),
+        completedLessons: this.itemCount(completed),
+        reviewIncome: this.incomeAmount(incomeTotal),
+        reviewRecords: this.itemCount(incomeList),
+        totalLessons: Number(statValues['t_total'] ?? 0),
+        totalHours: Number(statValues['t_shichang'] ?? 0),
+        course30: Number(statValues['t30'] ?? 0),
+        course60: Number(statValues['t60'] ?? 0),
+        trialLessons: Number(statValues['t_tiyan'] ?? 0),
+      });
+    });
+  }
+
   logout(): void { this.session.logout(); void this.router.navigateByUrl('/pages/member/wxauth'); }
   avatarUrl(): string { return this.api.assetUrl(this.session.user()?.userFace); }
 
+  private itemCount(envelope: unknown): number {
+    const value = envelope as { page?: { itemCount?: unknown }; result?: unknown } | null;
+    const pageTotal = Number(value?.page?.itemCount);
+    if (Number.isFinite(pageTotal)) return pageTotal;
+    return this.toList(value?.result).length;
+  }
+
+  private incomeAmount(envelope: unknown): number {
+    const value = envelope as { result?: unknown } | null;
+    return Number(this.toList(value?.result)[0]?.['num'] ?? 0);
+  }
+
+  private toList(value: unknown): SourceItem[] {
+    if (Array.isArray(value)) return value.filter((item): item is SourceItem => Boolean(item) && typeof item === 'object');
+    if (value && typeof value === 'object') {
+      for (const nested of Object.values(value)) if (Array.isArray(nested)) return nested as SourceItem[];
+    }
+    return [];
+  }
+
+  private today(): string {
+    const now = new Date();
+    return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
+  }
 }