Browse Source

update time

0235713 21 hours ago
parent
commit
9b370c056e

+ 5 - 1
interview-web/src/modules/interview/mobile/mobile.routes.ts

@@ -34,5 +34,9 @@ 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)
+}
 ];

+ 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 

+ 3 - 3
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>
   

+ 14 - 6
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,11 +99,11 @@ 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,
@@ -104,7 +111,8 @@ export class PageHome {
       description: '查看历史表现',
       gradient: 'linear-gradient(135deg, #48BB78, #38A169)',
       RouterLink:'/interview/mobile/page-mine'
-    }
+    },
+    
   ];
 
   // Swiper配置

+ 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);
-  }
-}
+}

+ 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);
+  }
+}