3 Commits 6113604854 ... 0d100b67d3

Author SHA1 Message Date
  0235713 0d100b67d3 update jianlixiangqin 19 hours ago
  0235713 223f9dd780 docs xiangxi 21 hours ago
  0235713 9b370c056e update time 1 day ago
23 changed files with 1862 additions and 217 deletions
  1. 7 5
      interview-web/src/app/app.config.ts
  2. 2 0
      interview-web/src/main.ts
  3. 13 0
      interview-web/src/modules/interview/mobile/mobile.routes.ts
  4. 0 1
      interview-web/src/modules/interview/mobile/nav-mobile-tabs/nav-mobile-tabs.html
  5. 82 0
      interview-web/src/modules/interview/mobile/page-ability-analysis/page-ability-analysis.html
  6. 212 0
      interview-web/src/modules/interview/mobile/page-ability-analysis/page-ability-analysis.scss
  7. 23 0
      interview-web/src/modules/interview/mobile/page-ability-analysis/page-ability-analysis.spec.ts
  8. 160 0
      interview-web/src/modules/interview/mobile/page-ability-analysis/page-ability-analysis.ts
  9. 4 4
      interview-web/src/modules/interview/mobile/page-home/page-home.html
  10. 32 20
      interview-web/src/modules/interview/mobile/page-home/page-home.ts
  11. 120 118
      interview-web/src/modules/interview/mobile/page-interview/page-interview.html
  12. 13 0
      interview-web/src/modules/interview/mobile/page-interview/page-interview.scss
  13. 63 69
      interview-web/src/modules/interview/mobile/page-interview/page-interview.ts
  14. 61 0
      interview-web/src/modules/interview/mobile/page-job-detail/page-job-detail.html
  15. 164 0
      interview-web/src/modules/interview/mobile/page-job-detail/page-job-detail.scss
  16. 23 0
      interview-web/src/modules/interview/mobile/page-job-detail/page-job-detail.spec.ts
  17. 109 0
      interview-web/src/modules/interview/mobile/page-job-detail/page-job-detail.ts
  18. 158 0
      interview-web/src/modules/interview/mobile/page-question-bank/page-question-bank.html
  19. 282 0
      interview-web/src/modules/interview/mobile/page-question-bank/page-question-bank.scss
  20. 23 0
      interview-web/src/modules/interview/mobile/page-question-bank/page-question-bank.spec.ts
  21. 126 0
      interview-web/src/modules/interview/mobile/page-question-bank/page-question-bank.ts
  22. 179 0
      package-lock.json
  23. 6 0
      package.json

+ 7 - 5
interview-web/src/app/app.config.ts

@@ -1,12 +1,14 @@
-import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
+import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
 import { provideRouter } from '@angular/router';
-
+import { provideClientHydration } from '@angular/platform-browser';
 import { routes } from './app.routes';
+import { provideCharts } from 'ng2-charts';
 
 export const appConfig: ApplicationConfig = {
   providers: [
-    provideBrowserGlobalErrorListeners(),
     provideZoneChangeDetection({ eventCoalescing: true }),
-    provideRouter(routes)
+    provideRouter(routes),
+    provideClientHydration(),
+    
   ]
-};
+};

+ 2 - 0
interview-web/src/main.ts

@@ -2,5 +2,7 @@ import { bootstrapApplication } from '@angular/platform-browser';
 import { appConfig } from './app/app.config';
 import { App } from './app/app';
 
+
 bootstrapApplication(App, appConfig)
   .catch((err) => console.error(err));
+  

+ 13 - 0
interview-web/src/modules/interview/mobile/mobile.routes.ts

@@ -34,5 +34,18 @@ export const MOBILE_ROUTES: Routes = [
   {
     path: 'interview/mobile/page-job-hunting',
     loadComponent: () => import('./page-job-hunting/page-job-hunting').then(m => m.PageJobHunting)
+  },
+  {
+  path: 'interview/mobile/page-question-bank',
+  loadComponent: () => import('./page-question-bank/page-question-bank').then(m => m.PageQuestionBank)
+  },
+  {
+  path: 'interview/mobile/page-job-detail/:id',
+  loadComponent: () => import('./page-job-detail/page-job-detail').then(m => m.PageJobDetail)
+  },
+  {
+  path: 'interview/mobile/page-ability-analysis',
+  loadComponent: () => import('./page-ability-analysis/page-ability-analysis').then(m => m.PageAbilityAnalysis)
   }
+
 ];

+ 0 - 1
interview-web/src/modules/interview/mobile/nav-mobile-tabs/nav-mobile-tabs.html

@@ -1,4 +1,3 @@
-<p>nav-mobile-tabs works!</p>
 <nav class="bottom-nav">
   <div class="nav-container">
     <div 

+ 82 - 0
interview-web/src/modules/interview/mobile/page-ability-analysis/page-ability-analysis.html

@@ -0,0 +1,82 @@
+<div class="ability-container">
+  <!-- 头部 -->
+  <div class="header">
+    <button class="back-btn" (click)="goBack()">
+      <fa-icon [icon]="icons.arrowLeft"></fa-icon>
+    </button>
+    <h1 class="title">个人能力分析</h1>
+    <div class="placeholder"></div>
+  </div>
+
+  <!-- 能力雷达图 -->
+  <div class="chart-section">
+    <h2 class="section-title">能力雷达图</h2>
+    <div class="chart-container">
+      <canvas baseChart
+        [data]="radarChartData"
+        [options]="radarChartOptions"
+        [labels]="radarChartLabels"
+        chartType="radar">
+      </canvas>
+    </div>
+    <div class="chart-legend">
+      <div class="legend-item">
+        <span class="legend-color" style="background-color: #2A5CAA;"></span>
+        <span>当前能力</span>
+      </div>
+      <div class="legend-item">
+        <span class="legend-color" style="background-color: #FF6B35;"></span>
+        <span>岗位要求</span>
+      </div>
+    </div>
+  </div>
+
+  <!-- 能力详情 -->
+  <div class="details-section">
+    <h2 class="section-title">能力详情</h2>
+    <div class="ability-cards">
+      <div class="ability-card" *ngFor="let ability of abilityDetails">
+        <div class="ability-header">
+          <div class="ability-icon" [style.background]="'rgba(42, 92, 170, 0.1)'">
+            <fa-icon [icon]="ability.icon"></fa-icon>
+          </div>
+          <h3 class="ability-title">{{ability.title}}</h3>
+        </div>
+        <div class="ability-score">
+          <span class="score-value">{{ability.score}}</span>
+          <span class="score-label">分</span>
+        </div>
+        <p class="ability-desc">{{ability.description}}</p>
+        <div class="progress-bar">
+          <div class="progress-fill" [style.width.%]="ability.score"></div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 历史趋势 -->
+  <div class="history-section">
+    <h2 class="section-title">能力成长趋势</h2>
+    <div class="history-chart">
+      <canvas baseChart
+        [data]="historyChartData"
+        [options]="historyChartOptions"
+        [labels]="historyChartLabels"
+        chartType="line">
+      </canvas>
+    </div>
+  </div>
+
+  <!-- 改进建议 -->
+  <div class="suggestions-section">
+    <h2 class="section-title">改进建议</h2>
+    <div class="suggestion-card">
+      <h3 class="suggestion-title">提升编码能力</h3>
+      <ul class="suggestion-list">
+        <li>每周完成3道LeetCode中等难度算法题</li>
+        <li>阅读《重构:改善既有代码的设计》</li>
+        <li>参与开源项目,学习优秀代码风格</li>
+      </ul>
+    </div>
+  </div>
+</div>

+ 212 - 0
interview-web/src/modules/interview/mobile/page-ability-analysis/page-ability-analysis.scss

@@ -0,0 +1,212 @@
+.ability-container {
+  padding: 20px;
+  background-color: #f5f7fa;
+  min-height: 100vh;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 25px;
+  
+  .back-btn {
+    background: none;
+    border: none;
+    font-size: 20px;
+    color: #2A5CAA;
+    cursor: pointer;
+  }
+  
+  .title {
+    font-size: 20px;
+    font-weight: 600;
+    color: #2D3748;
+  }
+  
+  .placeholder {
+    width: 24px; // 保持对称
+  }
+}
+
+.section-title {
+  font-size: 18px;
+  font-weight: 600;
+  color: #2D3748;
+  margin-bottom: 15px;
+  padding-left: 10px;
+  position: relative;
+  
+  &::before {
+    content: '';
+    position: absolute;
+    left: 0;
+    top: 5px;
+    height: 16px;
+    width: 4px;
+    background: #2A5CAA;
+    border-radius: 2px;
+  }
+}
+
+.chart-section {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 15px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+  
+  .chart-container {
+    height: 300px;
+    margin-bottom: 15px;
+  }
+  
+  .chart-legend {
+    display: flex;
+    justify-content: center;
+    gap: 20px;
+    
+    .legend-item {
+      display: flex;
+      align-items: center;
+      font-size: 14px;
+      color: #4A5568;
+      
+      .legend-color {
+        display: inline-block;
+        width: 12px;
+        height: 12px;
+        border-radius: 50%;
+        margin-right: 6px;
+      }
+    }
+  }
+}
+
+.details-section {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 15px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+  
+  .ability-cards {
+    display: grid;
+    grid-template-columns: 1fr;
+    gap: 15px;
+    
+    @media (min-width: 600px) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+  }
+  
+  .ability-card {
+    border: 1px solid #EDF2F7;
+    border-radius: 10px;
+    padding: 15px;
+    
+    .ability-header {
+      display: flex;
+      align-items: center;
+      margin-bottom: 10px;
+      
+      .ability-icon {
+        width: 36px;
+        height: 36px;
+        border-radius: 50%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        margin-right: 12px;
+        color: #2A5CAA;
+        font-size: 16px;
+      }
+      
+      .ability-title {
+        font-size: 16px;
+        font-weight: 500;
+        color: #2D3748;
+      }
+    }
+    
+    .ability-score {
+      margin-bottom: 10px;
+      
+      .score-value {
+        font-size: 24px;
+        font-weight: 600;
+        color: #2A5CAA;
+      }
+      
+      .score-label {
+        font-size: 14px;
+        color: #718096;
+        margin-left: 2px;
+      }
+    }
+    
+    .ability-desc {
+      font-size: 13px;
+      color: #718096;
+      margin-bottom: 15px;
+      line-height: 1.5;
+    }
+    
+    .progress-bar {
+      height: 6px;
+      background: #EDF2F7;
+      border-radius: 3px;
+      overflow: hidden;
+      
+      .progress-fill {
+        height: 100%;
+        background: linear-gradient(to right, #2A5CAA, #3A7BD5);
+        border-radius: 3px;
+      }
+    }
+  }
+}
+
+.history-section {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 15px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+  
+  .history-chart {
+    height: 250px;
+  }
+}
+
+.suggestions-section {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 30px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+  
+  .suggestion-card {
+    background: #F0F9FF;
+    border-radius: 10px;
+    padding: 15px;
+    
+    .suggestion-title {
+      font-size: 16px;
+      font-weight: 500;
+      color: #2A5CAA;
+      margin-bottom: 10px;
+    }
+    
+    .suggestion-list {
+      padding-left: 20px;
+      color: #4A5568;
+      font-size: 14px;
+      line-height: 1.6;
+      
+      li {
+        margin-bottom: 8px;
+      }
+    }
+  }
+}

+ 23 - 0
interview-web/src/modules/interview/mobile/page-ability-analysis/page-ability-analysis.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PageAbilityAnalysis } from './page-ability-analysis';
+
+describe('PageAbilityAnalysis', () => {
+  let component: PageAbilityAnalysis;
+  let fixture: ComponentFixture<PageAbilityAnalysis>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [PageAbilityAnalysis]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(PageAbilityAnalysis);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 160 - 0
interview-web/src/modules/interview/mobile/page-ability-analysis/page-ability-analysis.ts

@@ -0,0 +1,160 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { RouterModule } from '@angular/router';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { 
+  faArrowLeft,
+  faChartLine,
+  faLightbulb,
+  faCode,
+  faComments,
+  faUsers,
+  faClock
+} from '@fortawesome/free-solid-svg-icons';
+import { ChartConfiguration, ChartType } from 'chart.js';
+import { BaseChartDirective } from 'ng2-charts';
+import { Location } from '@angular/common';
+
+@Component({
+  selector: 'app-page-ability-analysis',
+  standalone: true,
+  imports: [CommonModule, FaIconComponent, RouterModule , BaseChartDirective],
+  templateUrl: './page-ability-analysis.html',
+  styleUrls: ['./page-ability-analysis.scss'],
+  
+})
+export class PageAbilityAnalysis {
+  
+
+  icons = {
+    arrowLeft: faArrowLeft,
+    chart: faChartLine,
+    knowledge: faLightbulb,
+    coding: faCode,
+    communication: faComments,
+    teamwork: faUsers,
+    efficiency: faClock
+  };
+
+  constructor(private location: Location) {}
+
+  goBack() {
+    this.location.back();
+  }
+
+  public radarChartOptions: ChartConfiguration<'radar'>['options'] = {
+  responsive: true,
+  scales: {
+    r: {
+      angleLines: { display: true },
+      suggestedMin: 0,
+      suggestedMax: 100,
+      ticks: { stepSize: 20 }
+    }
+  }
+};
+
+public radarChartLabels = ['专业知识', '编码能力', '沟通表达', '团队协作', '工作效率', '学习能力'];
+
+public radarChartData: ChartConfiguration<'radar'>['data'] = {
+  labels: this.radarChartLabels,
+  datasets: [
+    {
+      label: '当前能力',
+      data: [85, 78, 90, 82, 88, 92],
+      backgroundColor: 'rgba(42, 92, 170, 0.2)',
+      borderColor: 'rgba(42, 92, 170, 1)',
+      pointBackgroundColor: 'rgba(42, 92, 170, 1)',
+      pointBorderColor: '#fff',
+      pointHoverBackgroundColor: '#fff',
+      pointHoverBorderColor: 'rgba(42, 92, 170, 1)'
+    },
+    {
+      label: '岗位要求',
+      data: [90, 85, 85, 80, 85, 88],
+      backgroundColor: 'rgba(255, 107, 53, 0.2)',
+      borderColor: 'rgba(255, 107, 53, 1)',
+      pointBackgroundColor: 'rgba(255, 107, 53, 1)',
+      pointBorderColor: '#fff',
+      pointHoverBackgroundColor: '#fff',
+      pointHoverBorderColor: 'rgba(255, 107, 53, 1)'
+    }
+  ]
+};
+
+  // 能力详情
+  abilityDetails = [
+    {
+      title: '专业知识',
+      icon: this.icons.knowledge,
+      score: 85,
+      description: '掌握前端开发核心知识体系'
+    },
+    {
+      title: '编码能力',
+      icon: this.icons.coding,
+      score: 78,
+      description: '熟练使用多种编程语言和框架'
+    },
+    {
+      title: '沟通表达',
+      icon: this.icons.communication,
+      score: 90,
+      description: '能够清晰表达技术观点'
+    },
+    {
+      title: '团队协作',
+      icon: this.icons.teamwork,
+      score: 82,
+      description: '良好的团队合作意识'
+    },
+    {
+      title: '工作效率',
+      icon: this.icons.efficiency,
+      score: 88,
+      description: '高效完成任务的能力'
+    },
+    {
+      title: '学习能力',
+      icon: this.icons.chart,
+      score: 92,
+      description: '快速学习新技术的能力'
+    }
+  ];
+
+  // 历史趋势图数据
+  historyChartLabels = ['2023-01', '2023-04', '2023-07'];
+  historyChartOptions = {
+    responsive: true,
+    scales: {
+      y: {
+        min: 60,
+        max: 100
+      }
+    }
+  };
+  
+  historyChartData = {
+    labels: this.historyChartLabels,
+    datasets: [
+      {
+        label: '专业知识',
+        data: [70, 75, 80],
+        borderColor: '#4BC0C0',
+        backgroundColor: 'rgba(75, 192, 192, 0.2)'
+      },
+      {
+        label: '编码能力',
+        data: [65, 70, 75],
+        borderColor: '#FF9F40',
+        backgroundColor: 'rgba(255, 159, 64, 0.2)'
+      },
+      {
+        label: '沟通表达',
+        data: [75, 80, 85],
+        borderColor: '#9966FF',
+        backgroundColor: 'rgba(153, 102, 255, 0.2)'
+      }
+    ]
+  };
+}

+ 4 - 4
interview-web/src/modules/interview/mobile/page-home/page-home.html

@@ -12,9 +12,9 @@
     <div class="welcome-card">
       <h1 class="welcome-title">{{greeting}},{{user.name}}</h1>
       <p class="welcome-subtitle">您有{{user.pendingInterviews}}个匹配的岗位待面试,准备好了吗?</p>
-      <button class="start-btn" (click)="handleCardClick()">
-        开始模拟面试 <fa-icon [icon]="icons.arrowRight"></fa-icon>
-      </button>
+      <button class="start-btn" (click)="startInterview()">
+  开始模拟面试 <fa-icon [icon]="icons.arrowRight"></fa-icon>
+</button>
     </div>
   </div>
   
@@ -26,7 +26,7 @@
     </h2>
      <swiper-container [init]="false" [config]="swiperConfig" class="mySwiper">
       <swiper-slide *ngFor="let job of jobs">
-        <div class="job-card" (click)="handleCardClick()">
+        <div class="job-card" (click)="handleCardClick(job)">
           <div class="job-header">
             <h3 class="job-title">{{job.title}}</h3>
             <div class="job-salary">{{job.salary}}</div>

+ 32 - 20
interview-web/src/modules/interview/mobile/page-home/page-home.ts

@@ -47,6 +47,13 @@ export class PageHome {
     pendingInterviews: 3
   };
 
+  constructor(private router: Router) {} // 注入Router
+
+  // 添加开始面试方法
+  startInterview() {
+    this.router.navigate(['/interview/mobile/page-interview']);
+  }
+
   // 岗位数据
   jobs = [
     {
@@ -92,19 +99,21 @@ export class PageHome {
       RouterLink:'/interview/mobile/page-job-hunting'
     },
     {
-      icon: this.icons.book,
-      title: '面试题库',
-      description: '海量真题练习',
-      gradient: 'linear-gradient(135deg, #F6AD55, #DD6B20)',
-      RouterLink:'/interview/mobile/page-mine'
+    icon: this.icons.book,
+    title: '面试题库',
+    description: '海量真题练习',
+    gradient: 'linear-gradient(135deg, #F6AD55, #DD6B20)',
+    RouterLink: '/interview/mobile/page-question-bank'
     },
+    
     {
-      icon: this.icons.chartLine,
-      title: '成长报告',
-      description: '查看历史表现',
-      gradient: 'linear-gradient(135deg, #48BB78, #38A169)',
-      RouterLink:'/interview/mobile/page-mine'
+    icon: this.icons.chartLine,
+    title: '成长报告',
+    description: '查看历史表现',
+    gradient: 'linear-gradient(135deg, #48BB78, #38A169)',
+    RouterLink: '/interview/mobile/page-ability-analysis'
     }
+    
   ];
 
   // Swiper配置
@@ -120,16 +129,19 @@ export class PageHome {
     }
   };
 
-  // 处理卡片点击
-  handleCardClick() {
-    alert('即将跳转到对应功能页面');
+    // 处理卡片点击
+  handleCardClick(job: any) {
+    // 在实际应用中,这里应该使用job.id
+    // 现在我们使用数组索引+1作为ID模拟
+    const jobId = (this.jobs.indexOf(job) + 1).toString();
+    this.router.navigate(['/interview/mobile/page-job-detail', jobId]);
   }
 
-  // 获取问候语
-  get greeting() {
-    const hour = new Date().getHours();
-    if (hour < 12) return '上午好';
-    if (hour < 18) return '下午好';
-    return '晚上好';
-  }
+    // 获取问候语
+    get greeting() {
+      const hour = new Date().getHours();
+      if (hour < 12) return '上午好';
+      if (hour < 18) return '下午好';
+      return '晚上好';
+    }
 }

+ 120 - 118
interview-web/src/modules/interview/mobile/page-interview/page-interview.html

@@ -1,130 +1,132 @@
 <div class="interview-container">
-    <!-- 头部区域 -->
-    <div class="interview-header">
-        <div class="logo">AI面试官</div>
-        <div class="timer">
-            <i class="fas fa-clock"></i> 剩余时间: {{ remainingTime }}
-        </div>
+  <!-- 头部区域 -->
+  <div class="interview-header">
+    <div class="logo">AI面试官</div>
+    <div class="timer">
+      <i class="fas fa-clock"></i> 
+      剩余时间: {{ remainingTime }}
+      <span *ngIf="remainingMinutes === 0 && remainingSeconds <= 30" class="time-warning">
+        (即将结束)
+      </span>
     </div>
-    
-    <!-- 数字人区域 -->
-    <div class="avatar-section">
-        <div class="avatar-container">
-            <img [src]="avatarImage" alt="AI头像" class="avatar-img" 
-            (error)="avatarImage = 'assets/default-avatar.svg'">
-            <div class="voice-wave" [style.animation]="isListening ? 'wave 2s infinite' : 'none'"></div>
-        </div>
-        <div class="avatar-expression">
-            <span class="expression-dot"></span>
-            <span>{{ expressionText }}</span>
-        </div>
+  </div>
+  
+  <!-- 数字人区域 -->
+  <div class="avatar-section">
+    <div class="avatar-container">
+      <img [src]="avatarImage" alt="AI头像" class="avatar-img" 
+      (error)="avatarImage = 'assets/default-avatar.svg'">
+      <div class="voice-wave" [style.animation]="isListening ? 'wave 2s infinite' : 'none'"></div>
     </div>
-    
-    <!-- 对话区域 -->
-    <div class="dialog-container" #dialogContainer>
-        @for (message of messages; track message) {
-        <div class="message" [ngClass]="{'message-ai': message.sender === 'ai', 'message-user': message.sender === 'user'}">
-            <div class="message-bubble" [ngClass]="{'ai-bubble': message.sender === 'ai', 'user-bubble': message.sender === 'user'}">
-                {{ message.text }}
-            </div>
-            <div class="message-meta" [ngClass]="{'ai-meta': message.sender === 'ai', 'user-meta': message.sender === 'user'}">
-                <i [class]="message.sender === 'ai' ? 'fas fa-robot' : 'fas fa-user'"></i> 
-                {{ message.sender === 'ai' ? 'AI面试官' : '您' }}
-            </div>
-        </div>}
+    <div class="avatar-expression">
+      <span class="expression-dot"></span>
+      <span>{{ expressionText }}</span>
+    </div>
+  </div>
+  
+  <!-- 对话区域 -->
+  <div class="dialog-container" #dialogContainer>
+    @for (message of messages; track message) {
+    <div class="message" [ngClass]="{'message-ai': message.sender === 'ai', 'message-user': message.sender === 'user'}">
+      <div class="message-bubble" [ngClass]="{'ai-bubble': message.sender === 'ai', 'user-bubble': message.sender === 'user'}">
+        {{ message.text }}
+      </div>
+      <div class="message-meta" [ngClass]="{'ai-meta': message.sender === 'ai', 'user-meta': message.sender === 'user'}">
+        <i [class]="message.sender === 'ai' ? 'fas fa-robot' : 'fas fa-user'"></i> 
+        {{ message.sender === 'ai' ? 'AI面试官' : '您' }}
+      </div>
+    </div>}
+  </div>
+  
+  <!-- 问题卡片 -->
+  @if (showQuestionCard) {
+  <div class="question-card">
+    <div class="question-text">
+      {{ currentQuestion }}
+    </div>
+    <div class="question-progress">
+      <span>问题 {{ currentQuestionIndex + 1 }}/{{ questions.length }}</span>
+      <div class="progress-bar">
+        <div class="progress-fill" [style.width]="progress + '%'"></div>
+      </div>
+      <span>{{ progress }}%</span>
+    </div>
+  </div>}
+  
+  <!-- 语音输入区域 -->
+  <div class="voice-input-section">
+    <div class="voice-controls">
+      <button class="voice-btn" 
+              (mousedown)="startVoiceInput()" 
+              (mouseup)="stopVoiceInput()"
+              [class.active]="isListening">
+        <i class="fas fa-microphone"></i>
+        <div class="voice-wave"></div>
+      </button>
+      <button class="voice-btn" 
+              (click)="playQuestion()"
+              style="background: linear-gradient(135deg, #48BB78, #38A169);">
+        <i class="fas fa-play"></i>
+      </button>
+    </div>
+    @if (showSubmitButton) {
+    <button class="submit-btn" (click)="submitAnswer()" [disabled]="isAnalyzing">
+      <svg class="check-icon" viewBox="0 0 24 24" width="16" height="16">
+        <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" style="color: burlywood;"/>
+      </svg>
+      <div style="color: black;">确认上传并分析</div>
+    </button>
+    }
+    <div class="voice-hint">
+      点击播放按钮可重复听取问题<br>
+      按住麦克风按钮开始回答
+    </div>
+  </div>
+  
+  <!-- 分析仪表盘 -->
+  <div class="dashboard-section">
+    <div class="dashboard-title">
+      <h3>实时分析面板</h3>
+      <button class="toggle-btn" (click)="toggleRadarChart()">
+        <i [class]="showRadarChart ? 'fas fa-chart-bar' : 'fas fa-chart-radar'"></i> 
+        {{ showRadarChart ? '隐藏雷达图' : '显示雷达图' }}
+      </button>
     </div>
     
-    <!-- 问题卡片 (保留但隐藏) -->
-     @if (showQuestionCard) {
-    <div class="question-card">
-        <div class="question-text">
-            {{ currentQuestion }}
+    <div class="dashboard-content">
+      <div class="metric-item expressiveness">
+        <div class="metric-header">
+          <span class="metric-name">表达能力</span>
+          <span class="metric-value">{{ metrics.expressiveness }}/100</span>
         </div>
-        <div class="question-progress">
-            <span>问题 {{ currentQuestionIndex + 1 }}/{{ questions.length }}</span>
-            <div class="progress-bar">
-                <div class="progress-fill" [style.width]="progress + '%'"></div>
-            </div>
-            <span>{{ progress }}%</span>
+        <div class="metric-bar">
+          <div class="metric-fill" [style.width]="metrics.expressiveness + '%'"></div>
         </div>
-    </div>}
-    
-    <!-- 语音输入区域 -->
-    <div class="voice-input-section">
-        <div class="voice-controls">
-            <button class="voice-btn" 
-                    (mousedown)="startVoiceInput()" 
-                    (mouseup)="stopVoiceInput()"
-                    [class.active]="isListening">
-                <i class="fas fa-microphone"></i>
-                <div class="voice-wave"></div>
-            </button>
-            <button class="voice-btn" 
-                    (click)="playQuestion()"
-                    style="background: linear-gradient(135deg, #48BB78, #38A169);">
-                <i class="fas fa-play"></i>
-            </button>
+      </div>
+      
+      <div class="metric-item professionalism">
+        <div class="metric-header">
+          <span class="metric-name">专业度</span>
+          <span class="metric-value">{{ metrics.professionalism }}/100</span>
         </div>
-        @if (showSubmitButton) {
-        <button class="submit-btn" (click)="submitAnswer()" [disabled]="isAnalyzing">
-            <svg class="check-icon" viewBox="0 0 24 24" width="16" height="16">
-                <path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z" style="color: burlywood;"/>
-            </svg>
-                    <div style="color: black;">确认上传并分析</div>
-        </button>
-  }
-        <div class="voice-hint">
-            点击播放按钮可重复听取问题<br>
-            按住麦克风按钮开始回答
+        <div class="metric-bar">
+          <div class="metric-fill" [style.width]="metrics.professionalism + '%'"></div>
         </div>
-    </div>
-    
-    <!-- 分析仪表盘 -->
-    <div class="dashboard-section">
-        <div class="dashboard-title">
-            <h3>实时分析面板</h3>
-            <button class="toggle-btn" (click)="toggleRadarChart()">
-                <i [class]="showRadarChart ? 'fas fa-chart-bar' : 'fas fa-chart-radar'"></i> 
-                {{ showRadarChart ? '隐藏雷达图' : '显示雷达图' }}
-            </button>
+      </div>
+      
+      <div class="metric-item relevance">
+        <div class="metric-header">
+          <span class="metric-name">岗位匹配度</span>
+          <span class="metric-value">{{ metrics.relevance }}/100</span>
         </div>
-        
-        <div class="dashboard-content">
-            <div class="metric-item expressiveness">
-                <div class="metric-header">
-                    <span class="metric-name">表达能力</span>
-                    <span class="metric-value">{{ metrics.expressiveness }}/100</span>
-                </div>
-                <div class="metric-bar">
-                    <div class="metric-fill" [style.width]="metrics.expressiveness + '%'"></div>
-                </div>
-            </div>
-            
-            <div class="metric-item professionalism">
-                <div class="metric-header">
-                    <span class="metric-name">专业度</span>
-                    <span class="metric-value">{{ metrics.professionalism }}/100</span>
-                </div>
-                <div class="metric-bar">
-                    <div class="metric-fill" [style.width]="metrics.professionalism + '%'"></div>
-                </div>
-            </div>
-            
-            <div class="metric-item relevance">
-                <div class="metric-header">
-                    <span class="metric-name">岗位匹配度</span>
-                    <span class="metric-value">{{ metrics.relevance }}/100</span>
-                </div>
-                <div class="metric-bar">
-                    <div class="metric-fill" [style.width]="metrics.relevance + '%'"></div>
-                </div>
-            </div>
+        <div class="metric-bar">
+          <div class="metric-fill" [style.width]="metrics.relevance + '%'"></div>
         </div>
-        
-        <!-- 雷达图容器 -->
-         @if (showRadarChart){
-            <div id="radarChart" class="radar-chart" *ngIf="showRadarChart"></div>
-         }
-        
+      </div>
     </div>
-</div>
+    
+    @if (showRadarChart){
+      <div id="radarChart" class="radar-chart"></div>
+    }
+  </div>
+</div>

+ 13 - 0
interview-web/src/modules/interview/mobile/page-interview/page-interview.scss

@@ -44,6 +44,18 @@ body {
   box-shadow: 0 2px 10px rgba(0,0,0,0.1);
   font-weight: bold;
   color: var(--primary-blue);
+  
+  .time-warning {
+    color: #E53E3E;
+    font-weight: bold;
+    animation: blink 1s infinite;
+  }
+}
+
+@keyframes blink {
+  0% { opacity: 1; }
+  50% { opacity: 0.5; }
+  100% { opacity: 1; }
 }
 
 /* 数字人区域 */
@@ -386,6 +398,7 @@ body {
     max-width: 90%;
   }
 }
+
 /* 提交按钮样式 */
 .submit-btn {
   background: linear-gradient(135deg, var(--primary-blue), var(--accent-orange));

+ 63 - 69
interview-web/src/modules/interview/mobile/page-interview/page-interview.ts

@@ -1,4 +1,4 @@
-import { Component, OnInit, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
+import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import * as echarts from 'echarts';
 
@@ -9,41 +9,43 @@ import * as echarts from 'echarts';
   templateUrl: './page-interview.html',
   styleUrl: './page-interview.scss'
 })
-export class PageInterview implements OnInit, AfterViewInit {
-remainingTime = '04:32';
-isAnalyzing = false;
-showSubmitButton = false;
-userAnswer = '';
-// 在组件中定义Base64编码的SVG
-avatarImages = {
-  default: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQjkwMCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMThTMCAyNy45NDEgMCAxOCA4LjA1OSAwIDE4IDBzMTggOC4wNTkgMTggMTgiLz48cGF0aCBmaWxsPSIjNjYyMjEyIiBkPSJNMTguODQ1IDEzLjE0OGMuMTM0LS4yNDYuMzU2LS4zNjkuNjU3LS4zNjkuMzA5IDAgLjUyOS4xMjMuNjY0LjM2OS4xMzYuMjQ1LjIwNC41NzkuMjA0IDEuMDAzIDAgLjQxNS0uMDY4Ljc0LS4yMDQuOTg1LS4xMzUuMjQ0LS4zNTUuMzY2LS42NjQuMzY2LS4zMDEgMC0uNTIzLS4xMjItLjY1Ny0uMzY2LS4xMzUtLjI0NS0uMjAyLS41Ny0uMjAyLS45ODUgMC0uNDI0LjA2Ny0uNzU4LjIwMi0xLjAwM3ptLTUuNDQ4IDBjLjEzNS0uMjQ2LjM1Ni0uMzY5LjY1OC0uMzY5LjMwOSAwIC41MjkuMTIzLjY2NC4zNjkuMTM2LjI0NS4yMDQuNTc5LjIwNCAxLjAwMyAwIC40MTUtLjA2OC43NC0uMjA0Ljk4NS0uMTM1LjI0NC0uMzU1LjM2Ni0uNjY0LjM2Ni0uMzAyIDAtLjUyMy0uMTIyLS42NTgtLjM2Ni0uMTM0LS4yNDUtLjIwMS0uNTctLjIwMS0uOTg1IDAtLjQyNC4wNjctLjc1OC4yMDEtMS4wMDN6Ii8+PHBhdGggZmlsbD0iIzY2MjIxMiIgZD0iTTE4IDIxLjUzYy0yLjc2NCAwLTQuOTUxLS40MTktNC45NTEtMS4wNTQgMC0uNjM1IDIuMTg3LTEuMDU0IDQuOTUxLTEuMDU0IDIuNzYzIDAgNC45NTEuNDE5IDQuOTUxIDEuMDU0IDAgLjYzNS0yLjE4OCAxLjA1NC00Ljk1MSAxLjA1NHptMC0yLjEwOGMtMy4wMTcgMC01LjQ1MS0uNDk0LTUuNDUxLTEuMDU0czIuNDM0LTEuMDU0IDUuNDUxLTEuMDU0YzMuMDE4IDAgNS40NTEuNDk0IDUuNDUxIDEuMDU0cy0yLjQzMyAxLjA1NC01LjQ1MSAxLjA1NHoiLz48L3N2Zz4=',
-  speaking: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzNiAzNiI+PHBhdGggZmlsbD0iI0ZGQjkwMCIgZD0iTTM2IDE4YzAgOS45NDEtOC4wNTkgMTgtMTggMThTMCAyNy45NDEgMCAxOCA4LjA1OSAwIDE4IDBzMTggOC4wNTkgMTggMTgiLz48cGF0aCBmaWxsPSIjNjYyMjEyIiBkPSJNMTggMjEuNTNjLTIuNzY0IDAtNC45NTEtLjQxOS00Ljk1MS0xLjA1NCAwLS42MzUgMi4xODctMS4wNTQgNC45NTEtMS4wNTQgMi43NjMgMCA0Ljk1MS40MTkgNC45NTEgMS4wNTQgMCAuNjM1LTIuMTg4IDEuMDU0LTQuOTUxIDEuMDU0em0wLTIuMTA4Yy0zLjAxNyAwLTUuNDUxLS40OTQtNS40NTEtMS4wNTRzMi40MzQtMS4wNTQgNS40NTEtMS4wNTRjMy4wMTggMCA1LjQ1MS40OTQgNS40NTEgMS4wNTRzLTIuNDMzIDEuMDU0LTUuNDUxIDEuMDU0eiIvPjwvc3ZnPg==',
-  // 其他表情...
-};
-  // 问题相关
+export class PageInterview implements OnInit, AfterViewInit, OnDestroy {
+  // 倒计时相关属性
+  remainingTime: string = '30:00';
+  remainingMinutes: number = 30;
+  remainingSeconds: number = 0;
+  timerInterval: any;
+
+  // 其他现有属性保持不变
+  isAnalyzing = false;
+  showSubmitButton = false;
+  userAnswer = '';
+  
+  avatarImages = {
+    default: 'data:image/svg+xml;base64,...',
+    speaking: 'data:image/svg+xml;base64,...',
+  };
+
   questions = [
     "欢迎参加本次AI面试,我是您的面试官AI助手。",
     "首先,请简单介绍一下您自己。",
     "您在过去工作中遇到的最大技术挑战是什么?您是如何解决的?",
     "请描述一个您与团队意见不合时,您是如何处理的案例。"
   ];
+  
   currentQuestionIndex = 0;
   currentQuestion = this.questions[0];
   showQuestionCard = false;
   progress = 30;
   
-  // 对话相关
   messages: {sender: 'ai' | 'user', text: string}[] = [];
   
-  // 语音相关
   isListening = false;
   isSpeaking = false;
   
-  // 头像和表情相关
   avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f913.svg";
   expressionText = "正在聆听您的回答...";
   
-  // 仪表盘相关
   showRadarChart = false;
   metrics = {
     expressiveness: 82,
@@ -51,7 +53,6 @@ avatarImages = {
     relevance: 73
   };
   
-  // 预设回答
   presetAnswers = [
     "我是张伟,有5年Java开发经验,擅长分布式系统设计。",
     "我们曾遇到高并发下的数据库瓶颈,我通过引入Redis缓存和分库分表解决了问题。",
@@ -63,15 +64,51 @@ avatarImages = {
 
   ngOnInit(): void {
     this.initConversation();
-    this.startProgressTimer();
+    this.startCountdown(); // 启动倒计时
   }
   
   ngAfterViewInit(): void {
-    // 初始化雷达图
     this.initRadarChart();
   }
   
-  // 初始化对话
+  ngOnDestroy(): void {
+    if (this.timerInterval) {
+      clearInterval(this.timerInterval);
+    }
+  }
+
+  // 倒计时相关方法
+  private startCountdown() {
+    this.updateTimeDisplay();
+    
+    this.timerInterval = setInterval(() => {
+      if (this.remainingSeconds > 0) {
+        this.remainingSeconds--;
+      } else {
+        if (this.remainingMinutes > 0) {
+          this.remainingMinutes--;
+          this.remainingSeconds = 59;
+        } else {
+          clearInterval(this.timerInterval);
+          this.handleTimeUp();
+        }
+      }
+      this.updateTimeDisplay();
+    }, 1000);
+  }
+
+  private updateTimeDisplay() {
+    const mins = this.remainingMinutes.toString().padStart(2, '0');
+    const secs = this.remainingSeconds.toString().padStart(2, '0');
+    this.remainingTime = `${mins}:${secs}`;
+  }
+
+  private handleTimeUp() {
+    this.addAIMessage("面试时间已结束,系统将自动提交您的回答");
+    this.submitAnswer();
+  }
+
+  // 其他现有方法保持不变
   initConversation(): void {
     this.addAIMessage(this.questions[0]);
     setTimeout(() => {
@@ -79,51 +116,31 @@ avatarImages = {
     }, 1500);
   }
   
-  // 提问问题
   askQuestion(index: number): void {
     if (index >= this.questions.length) return;
     
     this.currentQuestionIndex = index;
     this.currentQuestion = this.questions[index];
-    
-    // 添加到对话框
     this.addAIMessage(this.currentQuestion);
-    
-    // 更新头像状态
     this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f4ac.svg";
     this.expressionText = "等待您的回答...";
   }
   
-  // 添加AI消息
   addAIMessage(text: string): void {
-    this.messages.push({
-      sender: 'ai',
-      text: text
-    });
-    
-    // 滚动到底部
+    this.messages.push({ sender: 'ai', text: text });
     setTimeout(() => {
       this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
     }, 0);
-    
-    // 自动语音播报
     this.speakText(text);
   }
   
-  // 添加用户消息
   addUserMessage(text: string): void {
-    this.messages.push({
-      sender: 'user',
-      text: text
-    });
-    
-    // 滚动到底部
+    this.messages.push({ sender: 'user', text: text });
     setTimeout(() => {
       this.dialogContainer.nativeElement.scrollTop = this.dialogContainer.nativeElement.scrollHeight;
     }, 0);
   }
   
-  // 语音播报
   speakText(text: string): void {
     if (this.isSpeaking) return;
     
@@ -131,7 +148,6 @@ avatarImages = {
     this.avatarImage = "https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/svg/1f5e3.svg";
     this.expressionText = "正在播报问题...";
     
-    // 使用Web Speech API
     const utterance = new SpeechSynthesisUtterance(text);
     utterance.lang = 'zh-CN';
     utterance.rate = 0.9;
@@ -146,39 +162,28 @@ avatarImages = {
     speechSynthesis.speak(utterance);
   }
   
-  // 开始语音输入
   startVoiceInput(): void {
     this.isListening = true;
     this.expressionText = "正在聆听您的回答...";
-    console.log('语音输入开始');
   }
   
-  // 停止语音输入
   stopVoiceInput(): void {
     this.isListening = false;
-    
-    // 模拟语音识别结果
     this.userAnswer = this.presetAnswers[this.currentQuestionIndex - 1] || "这是我的回答...";
-    this.showSubmitButton = true; // 显示提交按钮
-    
-    // 不再自动提交,等待用户点击
+    this.showSubmitButton = true;
     this.avatarImage = this.avatarImages.default;
     this.expressionText = "请确认您的回答";
   }
+  
   submitAnswer(): void {
     this.showSubmitButton = false;
     this.addUserMessage(this.userAnswer);
-    
-    // 开始分析
     this.isAnalyzing = true;
     this.avatarImage = this.avatarImages.default;
     this.expressionText = "正在分析您的回答...";
     
-    // 模拟分析过程 (2秒)
     setTimeout(() => {
       this.isAnalyzing = false;
-      
-      // 问下一题或结束
       if (this.currentQuestionIndex < this.questions.length - 1) {
         this.askQuestion(this.currentQuestionIndex + 1);
       } else {
@@ -189,14 +194,12 @@ avatarImages = {
     }, 2000);
   }
   
-  // 播放问题
   playQuestion(): void {
     if (!this.isSpeaking) {
       this.speakText(this.questions[this.currentQuestionIndex]);
     }
   }
   
-  // 切换雷达图
   toggleRadarChart(): void {
     this.showRadarChart = !this.showRadarChart;
     if (this.showRadarChart) {
@@ -206,7 +209,6 @@ avatarImages = {
     }
   }
   
-  // 初始化雷达图
   initRadarChart(): void {
     if (!this.showRadarChart) return;
     
@@ -289,16 +291,8 @@ avatarImages = {
     
     this.radarChart.setOption(option);
     
-    // 响应式调整
     window.addEventListener('resize', () => {
       this.radarChart?.resize();
     });
   }
-  
-  // 进度条计时器
-  startProgressTimer(): void {
-    setInterval(() => {
-      this.progress = Math.min(this.progress + 10, 100);
-    }, 5000);
-  }
-}
+}

+ 61 - 0
interview-web/src/modules/interview/mobile/page-job-detail/page-job-detail.html

@@ -0,0 +1,61 @@
+<div class="job-detail-container">
+  <!-- 头部 -->
+  <div class="job-header">
+    <button class="back-btn" (click)="goBack()">
+      <fa-icon [icon]="icons.arrowLeft"></fa-icon>
+    </button>
+    <div class="action-buttons">
+      <button class="icon-btn">
+        <fa-icon [icon]="icons.bookmark"></fa-icon>
+      </button>
+      <button class="icon-btn">
+        <fa-icon [icon]="icons.share"></fa-icon>
+      </button>
+    </div>
+  </div>
+
+  <!-- 职位基本信息 -->
+  <div class="job-basic-info">
+    <h1 class="job-title">{{job.title}}</h1>
+    <div class="job-salary">{{job.salary}}</div>
+    
+    <div class="job-meta">
+      <span>{{job.companyName}}</span>
+      <span>{{job.location}}</span>
+      <span>{{job.experience}}</span>
+      <span>{{job.education}}</span>
+    </div>
+    
+    <div class="job-tags">
+      <span class="tag" *ngFor="let tag of job.tags">{{tag}}</span>
+    </div>
+    
+    <div class="match-info">
+      <span>匹配度 {{job.match}}%</span>
+      <div class="progress-bar">
+        <div class="progress-fill" [style.width.%]="job.match"></div>
+      </div>
+    </div>
+  </div>
+
+  <!-- 职位详情 -->
+  <div class="job-content">
+    <h2>职位描述</h2>
+    <pre class="job-description">{{job.description}}</pre>
+    
+    <h2>公司福利</h2>
+    <div class="benefits">
+      <div class="benefit-item" *ngFor="let benefit of job.benefits">
+        {{benefit}}
+      </div>
+    </div>
+  </div>
+
+  <!-- 底部操作栏 -->
+  <div class="job-actions">
+    <button class="apply-btn" (click)="applyJob()">
+      <fa-icon [icon]="icons.apply"></fa-icon>
+      <span>立即申请</span>
+    </button>
+  </div>
+</div>

+ 164 - 0
interview-web/src/modules/interview/mobile/page-job-detail/page-job-detail.scss

@@ -0,0 +1,164 @@
+.job-detail-container {
+  padding: 20px;
+  padding-bottom: 80px;
+  background-color: #f5f7fa;
+  min-height: 100vh;
+}
+
+.job-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 20px;
+  
+  .back-btn {
+    background: none;
+    border: none;
+    font-size: 20px;
+    color: #2A5CAA;
+    cursor: pointer;
+  }
+  
+  .action-buttons {
+    display: flex;
+    gap: 15px;
+    
+    .icon-btn {
+      background: none;
+      border: none;
+      font-size: 18px;
+      color: #4A5568;
+      cursor: pointer;
+    }
+  }
+}
+
+.job-basic-info {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 15px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+  
+  .job-title {
+    font-size: 22px;
+    margin-bottom: 5px;
+    color: #2D3748;
+  }
+  
+  .job-salary {
+    color: #FF6B35;
+    font-size: 18px;
+    font-weight: bold;
+    margin-bottom: 15px;
+  }
+  
+  .job-meta {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+    margin-bottom: 15px;
+    color: #718096;
+    font-size: 14px;
+  }
+  
+  .job-tags {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 8px;
+    margin-bottom: 15px;
+    
+    .tag {
+      background: #EDF2F7;
+      padding: 4px 10px;
+      border-radius: 20px;
+      font-size: 12px;
+      color: #4A5568;
+    }
+  }
+  
+  .match-info {
+    font-size: 14px;
+    color: #4A5568;
+    
+    .progress-bar {
+      height: 6px;
+      background: #EDF2F7;
+      border-radius: 3px;
+      margin-top: 5px;
+      overflow: hidden;
+      
+      .progress-fill {
+        height: 100%;
+        background: linear-gradient(to right, #2A5CAA, #3A7BD5);
+        border-radius: 3px;
+      }
+    }
+  }
+}
+
+.job-content {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 15px;
+  box-shadow: 0 4px 12px rgba(0,0,0,0.05);
+  
+  h2 {
+    font-size: 18px;
+    margin-bottom: 15px;
+    color: #2D3748;
+  }
+  
+  .job-description {
+    white-space: pre-wrap;
+    font-size: 14px;
+    line-height: 1.6;
+    color: #4A5568;
+    margin-bottom: 20px;
+  }
+  
+  .benefits {
+    display: flex;
+    flex-wrap: wrap;
+    gap: 10px;
+    
+    .benefit-item {
+      background: #F0FFF4;
+      color: #38A169;
+      padding: 6px 12px;
+      border-radius: 20px;
+      font-size: 13px;
+    }
+  }
+}
+
+.job-actions {
+  position: fixed;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background: white;
+  padding: 15px 20px;
+  box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
+  
+  .apply-btn {
+    background: linear-gradient(to right, #2A5CAA, #3A7BD5);
+    color: white;
+    border: none;
+    width: 100%;
+    padding: 12px;
+    border-radius: 30px;
+    font-size: 16px;
+    font-weight: 500;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 8px;
+    cursor: pointer;
+    
+    &:hover {
+      opacity: 0.9;
+    }
+  }
+}

+ 23 - 0
interview-web/src/modules/interview/mobile/page-job-detail/page-job-detail.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PageJobDetail } from './page-job-detail';
+
+describe('PageJobDetail', () => {
+  let component: PageJobDetail;
+  let fixture: ComponentFixture<PageJobDetail>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [PageJobDetail]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(PageJobDetail);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 109 - 0
interview-web/src/modules/interview/mobile/page-job-detail/page-job-detail.ts

@@ -0,0 +1,109 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ActivatedRoute } from '@angular/router';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { 
+  faArrowLeft,
+  faBookmark,
+  faShareNodes,
+  faPaperPlane
+} from '@fortawesome/free-solid-svg-icons';
+
+// 定义职位详情接口
+interface JobDetail {
+  id: string;
+  title: string;
+  salary: string;
+  companyLogo: string;
+  companyName: string;
+  location: string;
+  experience: string;
+  education: string;
+  tags: string[];
+  match: number;
+  description: string;
+  benefits: string[];
+}
+
+@Component({
+  selector: 'app-page-job-detail',
+  standalone: true,
+  imports: [CommonModule, FaIconComponent],
+  templateUrl: './page-job-detail.html',
+  styleUrls: ['./page-job-detail.scss']
+})
+export class PageJobDetail {
+  icons = {
+    arrowLeft: faArrowLeft,
+    bookmark: faBookmark,
+    share: faShareNodes,
+    apply: faPaperPlane
+  };
+
+  job!: JobDetail;
+  
+  constructor(private route: ActivatedRoute) {
+    // 使用非空断言操作符(!)告诉TypeScript我们知道这个值不会是null
+    const jobId = this.route.snapshot.paramMap.get('id')!;
+    this.loadJobDetail(jobId);
+  }
+
+  loadJobDetail(jobId: string) {
+    // 定义更完整的模拟数据类型
+    const mockJobs: Record<string, JobDetail> = {
+      '1': {
+        id: '1',
+        title: '前端开发工程师',
+        salary: '25-40K·15薪',
+        companyLogo: 'TX',
+        companyName: '腾讯科技',
+        location: '深圳',
+        experience: '3-5年',
+        education: '本科及以上',
+        tags: ['Vue.js', 'React', 'TypeScript'],
+        match: 87,
+        description: '岗位职责:\n1. 负责公司前端项目开发与维护\n2. 参与产品需求讨论和技术方案设计\n3. 优化前端性能,提升用户体验\n\n任职要求:\n1. 精通HTML/CSS/JavaScript\n2. 熟练掌握Vue.js/React框架\n3. 有大型项目开发经验者优先',
+        benefits: ['五险一金', '年度体检', '带薪年假', '弹性工作制']
+      },
+      '2': {
+        id: '2',
+        title: 'Java高级工程师',
+        salary: '30-50K·16薪',
+        companyLogo: 'AL',
+        companyName: '阿里巴巴',
+        location: '杭州',
+        experience: '5-8年',
+        education: '本科及以上',
+        tags: ['Spring Cloud', '分布式', 'MySQL'],
+        match: 76,
+        description: '岗位职责:\n1. 负责核心系统架构设计与开发\n2. 解决高并发场景下的技术难题\n3. 参与技术方案评审\n\n任职要求:\n1. 精通Java及常用框架\n2. 熟悉分布式系统设计\n3. 有性能优化经验',
+        benefits: ['六险一金', '股票期权', '免费三餐', '年度旅游']
+      },
+      '3': {
+        id: '3',
+        title: '产品经理',
+        salary: '20-35K·14薪',
+        companyLogo: 'BD',
+        companyName: '百度',
+        location: '北京',
+        experience: '3-5年',
+        education: '本科及以上',
+        tags: ['Axure', '用户研究', 'PRD'],
+        match: 92,
+        description: '岗位职责:\n1. 负责产品规划与设计\n2. 进行市场调研和竞品分析\n3. 协调研发团队推进产品落地\n\n任职要求:\n1. 熟悉产品设计流程\n2. 优秀的沟通协调能力\n3. 有成功产品案例',
+        benefits: ['五险一金', '弹性工作', '学习基金', '健身房']
+      }
+    };
+    
+    // 使用类型断言确保jobId是mockJobs的合法键
+    this.job = mockJobs[jobId as keyof typeof mockJobs] || mockJobs['1'];
+  }
+
+  goBack() {
+    window.history.back();
+  }
+
+  applyJob() {
+    alert(`已申请 ${this.job.title} 职位`);
+  }
+}

+ 158 - 0
interview-web/src/modules/interview/mobile/page-question-bank/page-question-bank.html

@@ -0,0 +1,158 @@
+<div class="question-bank-container">
+  <!-- 分类列表视图 -->
+  <div class="categories-view" *ngIf="!selectedCategory && !showAnalysis">
+    <div class="search-box">
+      <input 
+        type="text" 
+        [(ngModel)]="searchTerm" 
+        placeholder="搜索面试问题...">
+      <fa-icon [icon]="icons.search"></fa-icon>
+    </div>
+    
+    <div class="categories-list">
+      <div 
+        class="category-card" 
+        *ngFor="let category of filteredCategories"
+        (click)="selectCategory(category)">
+        <div class="category-header">
+          <fa-icon [icon]="icons.book"></fa-icon>
+          <h3>{{category.name}}</h3>
+        </div>
+        <div class="question-count">
+          {{category.questions.length}}个问题
+        </div>
+        <fa-icon [icon]="icons.arrowRight"></fa-icon>
+      </div>
+    </div>
+  </div>
+  
+  <!-- 问题列表视图 -->
+  <div class="questions-view" *ngIf="selectedCategory && !showAnalysis">
+    <div class="back-btn" (click)="backToCategories()">
+      <fa-icon [icon]="icons.arrowRight" class="back-icon"></fa-icon>
+      返回分类
+    </div>
+    
+    <h2>{{selectedCategory.name}}问题</h2>
+    
+    <div class="questions-list">
+      <div 
+        class="question-item" 
+        *ngFor="let question of selectedCategory.questions"
+        (click)="currentQuestion = question">
+        {{question}}
+      </div>
+    </div>
+  </div>
+  
+  <!-- 问题回答视图 -->
+  <div class="answer-view" *ngIf="currentQuestion && !showAnalysis">
+    <div class="back-btn" (click)="currentQuestion = ''">
+      <fa-icon [icon]="icons.arrowRight" class="back-icon"></fa-icon>
+      返回问题列表
+    </div>
+    
+    <div class="question-card">
+      <h3>问题:</h3>
+      <p>{{currentQuestion}}</p>
+    </div>
+    
+    <div class="answer-section">
+      <h3>您的回答:</h3>
+      <textarea [(ngModel)]="userAnswer" placeholder="输入或录制您的回答..."></textarea>
+      
+      <div class="voice-controls">
+        <button 
+          class="voice-btn" 
+          (mousedown)="startRecording()" 
+          (mouseup)="isRecording = false"
+          [class.active]="isRecording">
+          <fa-icon [icon]="icons.mic"></fa-icon>
+          按住录音
+        </button>
+        
+        <button class="play-btn" (click)="speakQuestion()">
+          <fa-icon [icon]="icons.play"></fa-icon>
+          播放问题
+        </button>
+      </div>
+      
+      <button class="submit-btn" (click)="submitAnswer()">
+        <fa-icon [icon]="icons.check"></fa-icon>
+        提交分析
+      </button>
+    </div>
+  </div>
+  
+  <!-- 分析报告视图 -->
+  <div class="analysis-view" *ngIf="showAnalysis">
+    <div class="back-btn" (click)="showAnalysis = false">
+      <fa-icon [icon]="icons.arrowRight" class="back-icon"></fa-icon>
+      返回
+    </div>
+    
+    <h2>回答分析报告</h2>
+    
+    <div class="analysis-card">
+      <div class="question-section">
+        <h3>问题:</h3>
+        <p>{{analysisResult.question}}</p>
+      </div>
+      
+      <div class="answer-section">
+        <h3>您的回答:</h3>
+        <p>{{analysisResult.answer}}</p>
+      </div>
+      
+      <div class="score-section">
+        <h3>评估分数:</h3>
+        <div class="score-metrics">
+          <div class="metric">
+            <div class="metric-name">相关性</div>
+            <div class="metric-bar">
+              <div 
+                class="metric-fill" 
+                [style.width.%]="analysisResult.scores.relevance">
+                {{analysisResult.scores.relevance}}%
+              </div>
+            </div>
+          </div>
+          
+          <div class="metric">
+            <div class="metric-name">清晰度</div>
+            <div class="metric-bar">
+              <div 
+                class="metric-fill" 
+                [style.width.%]="analysisResult.scores.clarity">
+                {{analysisResult.scores.clarity}}%
+              </div>
+            </div>
+          </div>
+          
+          <div class="metric">
+            <div class="metric-name">深度</div>
+            <div class="metric-bar">
+              <div 
+                class="metric-fill" 
+                [style.width.%]="analysisResult.scores.depth">
+                {{analysisResult.scores.depth}}%
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      
+      <div class="feedback-section">
+        <h3>反馈建议:</h3>
+        <p>{{analysisResult.feedback}}</p>
+        
+        <div class="suggestions">
+          <h4>改进建议:</h4>
+          <ul>
+            <li *ngFor="let suggestion of analysisResult.suggestions">{{suggestion}}</li>
+          </ul>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

+ 282 - 0
interview-web/src/modules/interview/mobile/page-question-bank/page-question-bank.scss

@@ -0,0 +1,282 @@
+.question-bank-container {
+  max-width: 800px;
+  margin: 0 auto;
+  padding: 20px;
+  min-height: 100vh;
+  background-color: #f5f7fa;
+}
+
+.search-box {
+  position: relative;
+  margin-bottom: 20px;
+  
+  input {
+    width: 100%;
+    padding: 12px 15px;
+    padding-left: 40px;
+    border-radius: 8px;
+    border: 1px solid #e2e8f0;
+    font-size: 16px;
+    
+    &:focus {
+      outline: none;
+      border-color: #2A5CAA;
+    }
+  }
+  
+  fa-icon {
+    position: absolute;
+    left: 15px;
+    top: 50%;
+    transform: translateY(-50%);
+    color: #718096;
+  }
+}
+
+.categories-list {
+  display: grid;
+  gap: 15px;
+}
+
+.category-card {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.05);
+  cursor: pointer;
+  transition: all 0.3s ease;
+  
+  &:hover {
+    transform: translateY(-3px);
+    box-shadow: 0 5px 15px rgba(0,0,0,0.1);
+  }
+  
+  .category-header {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+    
+    h3 {
+      font-size: 18px;
+      color: #2D3748;
+      margin: 0;
+    }
+    
+    fa-icon {
+      color: #2A5CAA;
+      font-size: 20px;
+    }
+  }
+  
+  .question-count {
+    font-size: 14px;
+    color: #718096;
+  }
+}
+
+.back-btn {
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  margin-bottom: 20px;
+  color: #2A5CAA;
+  cursor: pointer;
+  
+  .back-icon {
+    transform: rotate(180deg);
+  }
+}
+
+.questions-list {
+  background: white;
+  border-radius: 12px;
+  overflow: hidden;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.05);
+}
+
+.question-item {
+  padding: 15px 20px;
+  border-bottom: 1px solid #edf2f7;
+  cursor: pointer;
+  transition: background-color 0.3s;
+  
+  &:hover {
+    background-color: #f8fafc;
+  }
+  
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.question-card {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  margin-bottom: 20px;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.05);
+  
+  h3 {
+    color: #2A5CAA;
+    margin-top: 0;
+  }
+}
+
+.answer-section {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.05);
+  
+  textarea {
+    width: 100%;
+    min-height: 150px;
+    padding: 15px;
+    border: 1px solid #e2e8f0;
+    border-radius: 8px;
+    font-size: 16px;
+    margin-bottom: 15px;
+    
+    &:focus {
+      outline: none;
+      border-color: #2A5CAA;
+    }
+  }
+}
+
+.voice-controls {
+  display: flex;
+  gap: 15px;
+  margin-bottom: 20px;
+}
+
+.voice-btn, .play-btn, .submit-btn {
+  padding: 12px 20px;
+  border-radius: 8px;
+  border: none;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 16px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+
+.voice-btn {
+  background-color: #2A5CAA;
+  color: white;
+  
+  &.active {
+    background-color: #E53E3E;
+    animation: pulse 1.5s infinite;
+  }
+}
+
+.play-btn {
+  background-color: #38B2AC;
+  color: white;
+}
+
+.submit-btn {
+  background-color: #48BB78;
+  color: white;
+  margin-top: 10px;
+}
+
+@keyframes pulse {
+  0% { transform: scale(1); }
+  50% { transform: scale(1.05); }
+  100% { transform: scale(1); }
+}
+
+.analysis-card {
+  background: white;
+  border-radius: 12px;
+  padding: 20px;
+  box-shadow: 0 2px 10px rgba(0,0,0,0.05);
+  
+  .question-section, .answer-section, .score-section, .feedback-section {
+    margin-bottom: 25px;
+    padding-bottom: 15px;
+    border-bottom: 1px solid #edf2f7;
+    
+    &:last-child {
+      margin-bottom: 0;
+      padding-bottom: 0;
+      border-bottom: none;
+    }
+  }
+  
+  h3 {
+    color: #2A5CAA;
+    margin-top: 0;
+  }
+}
+
+.score-metrics {
+  display: grid;
+  gap: 15px;
+}
+
+.metric {
+  .metric-name {
+    font-weight: 500;
+    margin-bottom: 5px;
+  }
+  
+  .metric-bar {
+    height: 10px;
+    background-color: #edf2f7;
+    border-radius: 5px;
+    overflow: hidden;
+  }
+  
+  .metric-fill {
+    height: 100%;
+    background: linear-gradient(to right, #2A5CAA, #3A7BD5);
+    border-radius: 5px;
+    color: white;
+    font-size: 10px;
+    text-align: right;
+    padding-right: 5px;
+    line-height: 10px;
+  }
+}
+
+.suggestions {
+  background-color: #f8fafc;
+  padding: 15px;
+  border-radius: 8px;
+  margin-top: 15px;
+  
+  h4 {
+    margin-top: 0;
+    color: #4A5568;
+  }
+  
+  ul {
+    padding-left: 20px;
+    margin-bottom: 0;
+  }
+  
+  li {
+    margin-bottom: 8px;
+    
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}
+
+@media (max-width: 600px) {
+  .question-bank-container {
+    padding: 15px;
+  }
+  
+  .voice-controls {
+    flex-direction: column;
+  }
+}

+ 23 - 0
interview-web/src/modules/interview/mobile/page-question-bank/page-question-bank.spec.ts

@@ -0,0 +1,23 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { PageQuestionBank } from './page-question-bank';
+
+describe('PageQuestionBank', () => {
+  let component: PageQuestionBank;
+  let fixture: ComponentFixture<PageQuestionBank>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      imports: [PageQuestionBank]
+    })
+    .compileComponents();
+
+    fixture = TestBed.createComponent(PageQuestionBank);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 126 - 0
interview-web/src/modules/interview/mobile/page-question-bank/page-question-bank.ts

@@ -0,0 +1,126 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+import { Router } from '@angular/router';
+import { FaIconComponent } from '@fortawesome/angular-fontawesome';
+import { 
+  faBook, faSearch, faChevronRight, 
+  faMicrophone, faPlay, faCheck
+} from '@fortawesome/free-solid-svg-icons';
+
+@Component({
+  selector: 'app-page-question-bank',
+  standalone: true,
+  imports: [CommonModule, FormsModule, FaIconComponent],
+  templateUrl: './page-question-bank.html',
+  styleUrls: ['./page-question-bank.scss']
+})
+export class PageQuestionBank {
+  icons = {
+    book: faBook,
+    search: faSearch,
+    arrowRight: faChevronRight,
+    mic: faMicrophone,
+    play: faPlay,
+    check: faCheck
+  };
+
+  categories = [
+    {
+      id: 1,
+      name: '技术类',
+      questions: [
+        "请解释一下闭包的概念及其应用场景",
+        "谈谈你对React/Vue框架的理解",
+        "如何优化前端性能",
+        "解释一下HTTP和HTTPS的区别"
+      ]
+    },
+    {
+      id: 2,
+      name: '行为类',
+      questions: [
+        "请描述一个你遇到的技术难题及解决方法",
+        "你如何与意见不合的同事合作",
+        "描述一个你领导项目的经历"
+      ]
+    },
+    {
+      id: 3,
+      name: '情景类',
+      questions: [
+        "如果项目截止日期提前,你会如何处理",
+        "当客户需求频繁变更时你会怎么做"
+      ]
+    }
+  ];
+
+  selectedCategory: any = null;
+  searchTerm = '';
+  currentQuestion = '';
+  userAnswer = '';
+  isRecording = false;
+  showAnalysis = false;
+  analysisResult: any = null;
+
+  constructor(private router: Router) {}
+
+  selectCategory(category: any) {
+    this.selectedCategory = category;
+    this.showAnalysis = false;
+  }
+
+  backToCategories() {
+    this.selectedCategory = null;
+    this.showAnalysis = false;
+  }
+
+  startRecording() {
+    this.isRecording = true;
+    // 实际项目中这里会调用语音识别API
+    setTimeout(() => {
+      this.userAnswer = "这是我的模拟回答...";
+      this.isRecording = false;
+    }, 2000);
+  }
+
+  speakQuestion() {
+    // 使用浏览器语音合成API朗读问题
+    if ('speechSynthesis' in window) {
+      const utterance = new SpeechSynthesisUtterance(this.currentQuestion);
+      window.speechSynthesis.speak(utterance);
+    } else {
+      console.warn('您的浏览器不支持语音合成功能');
+    }
+  }
+
+  submitAnswer() {
+    // 模拟分析过程
+    this.analysisResult = {
+      question: this.currentQuestion,
+      answer: this.userAnswer,
+      scores: {
+        relevance: Math.floor(Math.random() * 30) + 70,
+        clarity: Math.floor(Math.random() * 30) + 70,
+        depth: Math.floor(Math.random() * 30) + 70
+      },
+      feedback: "您的回答结构清晰,但可以增加更多具体案例来增强说服力。",
+      suggestions: [
+        "尝试使用STAR法则(Situation, Task, Action, Result)来组织回答",
+        "加入具体数据或成果来量化您的工作",
+        "保持回答在1-2分钟内"
+      ]
+    };
+    this.showAnalysis = true;
+  }
+
+  get filteredCategories() {
+    if (!this.searchTerm) return this.categories;
+    
+    return this.categories.map(category => ({
+      ...category,
+      questions: category.questions.filter(q => 
+        q.toLowerCase().includes(this.searchTerm.toLowerCase()))
+    })).filter(category => category.questions.length > 0);
+  }
+}

+ 179 - 0
package-lock.json

@@ -0,0 +1,179 @@
+{
+  "name": "Works",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "dependencies": {
+        "chart.js": "^4.5.0",
+        "ng2-charts": "^8.0.0"
+      }
+    },
+    "node_modules/@angular/cdk": {
+      "version": "20.0.5",
+      "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.0.5.tgz",
+      "integrity": "sha512-WhJ1I/ib/Za0qjWkSzMYV0gM8NOWrtOcZ2TYZ4aYFsjd8E13rGhxOez0DWt2sN3vfjAc1iWMmGGbNZrkp98adg==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "parse5": "^7.1.2",
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "^20.0.0 || ^21.0.0",
+        "@angular/core": "^20.0.0 || ^21.0.0",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@angular/common": {
+      "version": "20.0.6",
+      "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.0.6.tgz",
+      "integrity": "sha512-NRsq2gI4CH8nEy8yEZFySEmZ4U+1Y1yGzdIFubrKmtE2NXxR4KFGvQCkBLCLh6hNQXQx+Soe126bqByA6oIaFw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/core": "20.0.6",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@angular/core": {
+      "version": "20.0.6",
+      "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.0.6.tgz",
+      "integrity": "sha512-PLSRl8vM8I+HOlAJFiTcRMNbRj2Clb7lpQqUfkeBSk8bBWOy9fLlscoY3JOk0tXoUTnW6lbRB1LmAFuYAQZzAA==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/compiler": "20.0.6",
+        "rxjs": "^6.5.3 || ^7.4.0",
+        "zone.js": "~0.15.0"
+      },
+      "peerDependenciesMeta": {
+        "@angular/compiler": {
+          "optional": true
+        },
+        "zone.js": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@angular/platform-browser": {
+      "version": "20.0.6",
+      "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.0.6.tgz",
+      "integrity": "sha512-EZC6ILD0nXOddNuwqQqwTzvRgXs/1kZoRGzdG8zpHhRREBf6VFMZ+g7IN3EKnYN4hDL5EMxIKIsIcQjmCDsu2A==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "engines": {
+        "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+      },
+      "peerDependencies": {
+        "@angular/animations": "20.0.6",
+        "@angular/common": "20.0.6",
+        "@angular/core": "20.0.6"
+      },
+      "peerDependenciesMeta": {
+        "@angular/animations": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@kurkle/color": {
+      "version": "0.3.4",
+      "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+      "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+      "license": "MIT"
+    },
+    "node_modules/chart.js": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.0.tgz",
+      "integrity": "sha512-aYeC/jDgSEx8SHWZvANYMioYMZ2KX02W6f6uVfyteuCGcadDLcYVHdfdygsTQkQ4TKn5lghoojAsPj5pu0SnvQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@kurkle/color": "^0.3.0"
+      },
+      "engines": {
+        "pnpm": ">=8"
+      }
+    },
+    "node_modules/entities": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+      "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+      "license": "BSD-2-Clause",
+      "peer": true,
+      "engines": {
+        "node": ">=0.12"
+      },
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/lodash-es": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+      "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+      "license": "MIT"
+    },
+    "node_modules/ng2-charts": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/ng2-charts/-/ng2-charts-8.0.0.tgz",
+      "integrity": "sha512-nofsNHI2Zt+EAwT+BJBVg0kgOhNo9ukO4CxULlaIi7VwZSr7I1km38kWSoU41Oq6os6qqIh5srnL+CcV+RFPFA==",
+      "license": "MIT",
+      "dependencies": {
+        "lodash-es": "^4.17.15",
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/cdk": ">=19.0.0",
+        "@angular/common": ">=19.0.0",
+        "@angular/core": ">=19.0.0",
+        "@angular/platform-browser": ">=19.0.0",
+        "chart.js": "^3.4.0 || ^4.0.0",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/parse5": {
+      "version": "7.3.0",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+      "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+      "license": "MIT",
+      "peer": true,
+      "dependencies": {
+        "entities": "^6.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/inikulin/parse5?sponsor=1"
+      }
+    },
+    "node_modules/rxjs": {
+      "version": "7.8.2",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+      "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+      "license": "Apache-2.0",
+      "peer": true,
+      "dependencies": {
+        "tslib": "^2.1.0"
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    }
+  }
+}

+ 6 - 0
package.json

@@ -0,0 +1,6 @@
+{
+  "dependencies": {
+    "chart.js": "^4.5.0",
+    "ng2-charts": "^8.0.0"
+  }
+}