Explorar el Código

Replace empty home recommendations with wordbook entry

彭峰 hace 1 semana
padre
commit
e8bc0ffe92

+ 7 - 9
projects/xiaoshu-mobile/src/app/features/home/home-page.component.html

@@ -118,14 +118,12 @@
     </section>
     </section>
   }
   }
 
 
-  @if (!isCoach()) { <section class="content-section">
-    <div class="section-heading"><div><span class="eyebrow"><lucide-icon [img]="icons.Sparkles" [size]="15" />精选</span><h2>推荐学习内容</h2></div><a routerLink="/pages/shop/shop_nodes">查看全部<lucide-icon [img]="icons.ChevronRight" [size]="17" /></a></div>
-    @if (recommendations().length) {
-      <div class="product-grid">@for (item of recommendations(); track item['ID'] ?? $index) { <button type="button" class="product" (click)="openProduct(item)"><img [src]="image(item)" [alt]="name(item)" loading="lazy" /><span>{{ name(item) }}</span><strong>¥{{ price(item) }}</strong></button> }</div>
-    } @else if (recommendationError()) {
-      <div class="recommend-empty error"><lucide-icon [img]="icons.AlertCircle" [size]="30" /><span>推荐内容暂时无法加载</span><button type="button" (click)="loadRecommendations()">重新加载</button></div>
-    } @else {
-      <div class="recommend-empty"><lucide-icon [img]="icons.Headphones" [size]="32" /><strong>暂无推荐内容</strong><span>完成更多学习,智能推荐更适合你的内容</span></div>
-    }
+  @if (!isCoach()) { <section class="content-section" aria-labelledby="wordbook-title">
+    <div class="section-heading"><div><span class="eyebrow"><lucide-icon [img]="icons.BookMarked" [size]="15" />自主学习</span><h2 id="wordbook-title">生词本</h2></div></div>
+    <a class="wordbook-entry" routerLink="/pages/member/starshop" aria-label="打开生词本,选择单词开始学习">
+      <span class="wordbook-entry-icon"><lucide-icon [img]="icons.BookMarked" [size]="30" /></span>
+      <span class="wordbook-entry-copy"><strong>选生词,开始学习</strong><small>勾选想学的单词,按自己的节奏练习</small></span>
+      <span class="wordbook-entry-action">进入生词本<lucide-icon [img]="icons.ChevronRight" [size]="18" /></span>
+    </a>
   </section> }
   </section> }
 </main>
 </main>

+ 7 - 11
projects/xiaoshu-mobile/src/app/features/home/home-page.component.scss

@@ -118,17 +118,13 @@
 .section-heading { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 16px; }
 .section-heading { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 16px; }
 .section-heading h2 { margin: 4px 0 0; font-size: 20px; letter-spacing: 0; }
 .section-heading h2 { margin: 4px 0 0; font-size: 20px; letter-spacing: 0; }
 .eyebrow { display: flex; align-items: center; color: var(--accent); font-size: 11px; gap: 4px; }
 .eyebrow { display: flex; align-items: center; color: var(--accent); font-size: 11px; gap: 4px; }
-.section-heading a { display: flex; align-items: center; color: var(--ink-soft); font-size: 12px; text-decoration: none; }
-.product-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
-.product { overflow: hidden; min-width: 0; padding: 0 0 11px; text-align: left; background: white; border: 1px solid var(--line); border-radius: var(--radius-md); box-shadow: var(--shadow-xs); }
-.product img { width: 100%; aspect-ratio: 4 / 3; background: var(--surface-muted); }
-.product span { display: -webkit-box; overflow: hidden; min-height: 42px; margin: 9px 10px 4px; font-size: 13px; line-height: 1.55; -webkit-box-orient: vertical; -webkit-line-clamp: 2; }
-.product strong { margin: 0 10px; color: var(--danger); font-size: 15px; }
-.recommend-empty { display: flex; min-height: 185px; align-items: center; justify-content: center; color: var(--ink-soft); text-align: center; flex-direction: column; gap: 8px; }
-.recommend-empty strong { color: var(--ink); font-size: 14px; }
-.recommend-empty span { max-width: 280px; font-size: 11px; }
-.recommend-empty button { min-height: 40px; padding: 0 16px; color: var(--brand-strong); background: white; border: 1px solid var(--brand); border-radius: var(--radius-sm); }
-.recommend-empty.error { color: var(--danger); }
+.wordbook-entry { display: grid; min-height: 140px; grid-template-columns: 58px minmax(0, 1fr); align-content: center; align-items: center; padding: 20px; color: var(--ink); text-decoration: none; background: linear-gradient(120deg, #edf8f4, #f7fbf9); border: 1px solid #c9e4d9; border-radius: var(--radius-md); column-gap: 16px; row-gap: 8px; }
+.wordbook-entry:focus-visible, .wordbook-entry:hover { border-color: var(--brand); outline-offset: 3px; }
+.wordbook-entry-icon { display: grid; width: 58px; height: 58px; grid-row: span 2; color: var(--brand-strong); background: white; border-radius: 16px; place-items: center; }
+.wordbook-entry-copy { display: grid; gap: 5px; }
+.wordbook-entry-copy strong { font-size: 17px; }
+.wordbook-entry-copy small { color: var(--ink-soft); font-size: 12px; line-height: 1.5; }
+.wordbook-entry-action { display: flex; align-items: center; color: var(--brand-strong); font-size: 13px; font-weight: 700; gap: 4px; }
 
 
 @keyframes spin { to { transform: rotate(360deg); } }
 @keyframes spin { to { transform: rotate(360deg); } }
 .spin { animation: spin .8s linear infinite; }
 .spin { animation: spin .8s linear infinite; }

+ 3 - 31
projects/xiaoshu-mobile/src/app/features/home/home-page.component.ts

@@ -2,10 +2,10 @@ import { ReviewPage, reviewOverview } from '../../core/review-page';
 import { CommonModule } from '@angular/common';
 import { CommonModule } from '@angular/common';
 import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core';
 import { ChangeDetectionStrategy, Component, computed, inject, OnDestroy, OnInit, signal } from '@angular/core';
 import { Router, RouterLink } from '@angular/router';
 import { Router, RouterLink } from '@angular/router';
-import { AlertCircle, BarChart3, BookOpen, CalendarDays, Check, ChevronRight, Clock3, Eye, EyeOff, GraduationCap, Headphones, Info, LucideAngularModule, RefreshCw, Search, Sparkles, UserRound, Zap } from 'lucide-angular';
+import { AlertCircle, BarChart3, BookMarked, BookOpen, CalendarDays, Check, ChevronRight, Clock3, Eye, EyeOff, GraduationCap, Info, LucideAngularModule, RefreshCw, Search, UserRound, Zap } from 'lucide-angular';
 import { finalize } from 'rxjs';
 import { finalize } from 'rxjs';
 import { antiForgetCompleted, antiForgetDate, buildAntiForgetView, findAntiForgetList } from '../../core/anti-forget';
 import { antiForgetCompleted, antiForgetDate, buildAntiForgetView, findAntiForgetList } from '../../core/anti-forget';
-import { ApiError, ApiService } from '../../core/api.service';
+import { ApiService } from '../../core/api.service';
 import { PlatformService } from '../../core/platform.service';
 import { PlatformService } from '../../core/platform.service';
 import { SessionService } from '../../core/session.service';
 import { SessionService } from '../../core/session.service';
 import { LiveDataRefreshService } from '../../core/live-data-refresh.service';
 import { LiveDataRefreshService } from '../../core/live-data-refresh.service';
@@ -25,8 +25,6 @@ export class HomePageComponent implements OnInit, OnDestroy {
   readonly stats = signal<Record<string, number>>({});
   readonly stats = signal<Record<string, number>>({});
   readonly statsLoading = signal(false);
   readonly statsLoading = signal(false);
   readonly coachStatsError = signal('');
   readonly coachStatsError = signal('');
-  readonly recommendations = signal<Item[]>([]);
-  readonly recommendationError = signal('');
   readonly commissionHidden = signal(true);
   readonly commissionHidden = signal(true);
   readonly antiForgetItems = signal<Item[]>([]);
   readonly antiForgetItems = signal<Item[]>([]);
   readonly antiForgetLoading = signal(false);
   readonly antiForgetLoading = signal(false);
@@ -42,7 +40,7 @@ export class HomePageComponent implements OnInit, OnDestroy {
     return stateOrder(left) === 1 ? dateDifference : -dateDifference;
     return stateOrder(left) === 1 ? dateDifference : -dateDifference;
   }));
   }));
   readonly coachReviewSummary=computed(()=>this.reviewData()?.summary||{today:0,due:0,completed:0});
   readonly coachReviewSummary=computed(()=>this.reviewData()?.summary||{today:0,due:0,completed:0});
-  readonly icons = { AlertCircle, BarChart3, BookOpen, CalendarDays, Check, ChevronRight, Clock3, Eye, EyeOff, GraduationCap, Headphones, Info, RefreshCw, Search, Sparkles, UserRound, Zap };
+  readonly icons = { AlertCircle, BarChart3, BookMarked, BookOpen, CalendarDays, Check, ChevronRight, Clock3, Eye, EyeOff, GraduationCap, Info, RefreshCw, Search, UserRound, Zap };
 
 
   private readonly api = inject(ApiService);
   private readonly api = inject(ApiService);
   private readonly router = inject(Router);
   private readonly router = inject(Router);
@@ -66,8 +64,6 @@ export class HomePageComponent implements OnInit, OnDestroy {
         },
         },
       });
       });
     }
     }
-    if (this.isCoach()) return;
-    this.loadRecommendations();
   }
   }
 
 
   ngOnDestroy(): void {
   ngOnDestroy(): void {
@@ -79,23 +75,6 @@ export class HomePageComponent implements OnInit, OnDestroy {
     if (uid) this.loadStats(uid);
     if (uid) this.loadStats(uid);
   }
   }
 
 
-  loadRecommendations(): void {
-    this.recommendationError.set('');
-    this.api.get<unknown>('product_list', { nodes: '15,16', hot: 1, cpage: 1, psize: 8 }).subscribe({
-      next: ({ result }) => this.recommendations.set(this.toList(result)),
-      error: (error: ApiError) => this.recommendationError.set(error.message),
-    });
-  }
-
-  openProduct(item: Item): void {
-    const id = item['ID'] ?? item['id'] ?? item['GeneralID'];
-    if (id) void this.router.navigate(['/pages/shop/shop_item'], { queryParams: { id } });
-  }
-
-  image(item: Item): string { return this.api.assetUrl(item['Thumbnails'] ?? item['Pic']); }
-  name(item: Item): string { return String(item['Proname'] ?? item['Title'] ?? '学习内容'); }
-  price(item: Item): string { return Number(item['LinPrice'] ?? item['Price'] ?? 0).toFixed(2); }
-
   retryAntiForget(): void {
   retryAntiForget(): void {
     const uid = this.session.user()?.userId;
     const uid = this.session.user()?.userId;
     if (uid) this.loadAntiForget(uid);
     if (uid) this.loadAntiForget(uid);
@@ -189,11 +168,4 @@ export class HomePageComponent implements OnInit, OnDestroy {
     return `${value('year')}-${value('month')}-${value('day')}`;
     return `${value('year')}-${value('month')}-${value('day')}`;
   }
   }
 
 
-  private toList(value: unknown): Item[] {
-    if (Array.isArray(value)) return value as Item[];
-    if (value && typeof value === 'object') {
-      for (const nested of Object.values(value)) if (Array.isArray(nested)) return nested as Item[];
-    }
-    return [];
-  }
 }
 }