Forráskód Böngészése

渲染面试问题

yangg 2 hónapja
szülő
commit
7e0b921c7d

+ 12 - 0
api/user.js

@@ -114,3 +114,15 @@ export const fillUserInfo = (params) => {
   return http.post('/api/system/wechat/save_user_info', params);
 };
 
+/* 获取面试列表 */
+export const getInterviewList = (params) => {
+  return http.get('/system/interview_question/list', params);
+};
+
+/* 获取面试详情 */
+export const getInterviewDetail = (params) => {
+  return http.get('/system/interview_question/detail', params);
+};
+
+
+

+ 276 - 70
pages/camera/camera.vue

@@ -41,18 +41,27 @@
 				<!-- 问题和选项区域 -->
 				<view class="question-area">
 					<view class="question-importance">
-						<text v-if="currentQuestion.isImportant">重点题</text> {{currentQuestion.text}}
+						<text v-if="currentQuestion.isImportant">重点题</text> 
+						<text class="question-type">[{{currentQuestion.questionTypeName}}]</text> 
+						{{currentQuestion.text}}
 					</view>
 
 					<view class="options">
 						<view v-for="(option, index) in currentQuestion.options" :key="index" class="option-item"
 							:class="{
-                'option-selected': selectedOption === index,
-                'option-correct': showResult && index === currentQuestion.correctAnswer,
-                'option-wrong': showResult && selectedOption === index && index !== currentQuestion.correctAnswer
-              }" 
-              @click="selectOption(index)">
-							<text class="option-text">{{option}}</text>
+								'option-selected': currentQuestion.questionType === 1 ? selectedOption === index : selectedOptions.includes(index),
+								'option-correct': showResult && (
+									currentQuestion.questionType === 1 ? index === currentQuestion.correctAnswer : currentQuestion.correctAnswers.includes(index)
+								),
+								'option-wrong': showResult && (
+									currentQuestion.questionType === 1 ? 
+									(selectedOption === index && index !== currentQuestion.correctAnswer) : 
+									(selectedOptions.includes(index) && !currentQuestion.correctAnswers.includes(index))
+								)
+							}" 
+							@click="selectOption(index)">
+							<!-- {{JSON.parse(option) }} -->
+							<text class="option-text">{{ option.option_text || (typeof option === 'string' ? option : JSON.stringify(option)) }}</text>
 						</view>
 					</view>
 
@@ -60,8 +69,9 @@
 						<text class="timer-text">本题剩余时间 {{remainingTime}}</text>
 					</view>
 					
-					<button class="next-button" @click="nextQuestion" :disabled="selectedOption === null">
-						下一题
+					<button class="next-button" @click="nextQuestion" 
+						:disabled="currentQuestion.questionType === 1 ? selectedOption === null : selectedOptions.length === 0">
+						{{showResult ? '下一题' : '提交答案'}}
 					</button>
 				</view>
 			</view>
@@ -98,10 +108,26 @@
 
 		<!-- 在模板中添加一个测试按钮(开发时使用,发布前删除) -->
 		<!-- <button @click="testEndScreen" style="position: absolute; top: 10px; right: 10px; z-index: 9999;">测试结束页</button> -->
+
+		<!-- 添加加载状态 -->
+		<view class="loading-container" v-if="loading">
+			<uni-load-more status="loading" :contentText="{contentdown: '加载中...'}" />
+			<text class="loading-text">正在加载面试题目...</text>
+		</view>
+
+		<!-- 添加加载错误提示 -->
+		<view class="loading-container" v-if="!loading && loadError">
+			<view class="error-container">
+				<text class="error-message">加载面试题目失败</text>
+				<button class="retry-button" @click="fetchInterviewData">重试</button>
+			</view>
+		</view>
 	</view>
 </template>
 
 <script>
+	import { getInterviewList, getInterviewDetail } from '@/api/user.js';
+	
 	export default {
 		data() {
 			return {
@@ -115,61 +141,38 @@
 				progressWidth: 50,
 				remainingTime: '00:27',
 				selectedOption: null,
+				selectedOptions: [],
 				showResult: false,
 				isAnswerCorrect: false,
-				questions: [{
-						id: 6,
-						text: '以下不属于中国传统节日的是( )。',
-						options: [
-							'A. 春节',
-							'B. 端午节',
-							'C. 重阳节',
-							'D. 元旦'
-						],
-						correctAnswer: 3,
-						isImportant: true,
-						explanation: '元旦是公历新年,属于现代节日,而春节、端午节和重阳节都是中国传统节日。'
-					},
-					{
-						id: 7,
-						text: '下列哪个是中国四大名著之一( )。',
-						options: [
-							'A. 聊斋志异',
-							'B. 西游记',
-							'C. 世说新语',
-							'D. 聊斋志异'
-						],
-						correctAnswer: 1,
-						isImportant: false,
-						explanation: '中国四大名著是《红楼梦》、《西游记》、《水浒传》和《三国演义》。'
-					},
-					{
-						id: 8,
-						text: '中国传统文化中"仁义礼智信"五常不包括( )。',
-						options: [
-							'A. 忠',
-							'B. 孝',
-							'C. 礼',
-							'D. 信'
-						],
-						correctAnswer: 1,
-						isImportant: true,
-						explanation: '儒家的五常是"仁、义、礼、智、信",不包括"孝"。'
-					}
-				],
+				questions: [], // 改为空数组,将通过API获取
+				interviewId: null, // 存储当前面试ID
 				useVideo: false,
 				timerInterval: null,
 				score: 0,
 				totalQuestions: 0,
 				interviewCompleted: false,
-				digitalHumanUrl: '' // 数字人URL
+				digitalHumanUrl: '', // 数字人URL
+				loading: true, // 添加加载状态
+				loadError: false, // 添加加载错误状态
+				errorMessage: '' // 添加错误消息
 			}
 		},
 		computed: {
 			currentQuestion() {
+				console.log(this.questions[this.currentQuestionIndex]);
 				return this.questions[this.currentQuestionIndex];
 			}
 		},
+		onLoad(options) {
+			// 从路由参数中获取面试ID
+			if (options && options.id) {
+				this.interviewId = options.id;
+				this.fetchInterviewData();
+			} else {
+				// 如果没有ID,可以获取面试列表并使用第一个
+				this.fetchInterviewList();
+			}
+		},
 		onReady() {
 			// 创建相机上下文
 			this.cameraContext = uni.createCameraContext();
@@ -177,16 +180,120 @@
 			if (this.useVideo) {
 				this.aiVideoContext = uni.createVideoContext('aiInterviewer');
 			}
-			// 启动倒计时
-			this.startTimer();
-			// 设置总题目数
-			this.totalQuestions = this.questions.length;
 			
 			// 初始化数字人
 			this.initDigitalHuman();
 		},
 		methods: {
+			// 获取面试列表
+			async fetchInterviewList() {
+				try {
+					this.loading = true;
+					const res = await getInterviewList();
+					console.log(res);
+										// 使用第一个面试
+					this.interviewId = res.items[0].id;
+					this.fetchInterviewData();
+				} catch (error) {
+					console.error('获取面试列表失败:', error);
+					this.handleLoadError('获取面试列表失败');
+				}
+			},
+			
+			// 获取面试详情数据
+			async fetchInterviewData() {
+				try {
+					this.loading = true;
+					const res = await getInterviewDetail({ id: this.interviewId });
+					console.log('API返回数据:', res);
+					
+					// 如果返回的是问题列表
+					if (res && Array.isArray(res.items)) {
+						// 处理多个问题
+						this.questions = res.items.map((q, index) => ({
+							id: q.id || index + 1,
+							text: q.question || '未知问题',
+							options:q.options || [],
+							correctAnswer: q.correctAnswer || 0,
+							isImportant: q.is_system || false,
+							explanation: q.explanation || '',
+							questionType: q.question_form || 1,
+							questionTypeName: q.question_form_name || '单选题',
+							correctAnswers: q.correct_answers || [],
+							difficulty: q.difficulty || 1,
+							difficultyName: q.difficulty_name || '初级'
+						}));
+					} else {
+						// 处理单个问题
+						this.processInterviewData(res);
+					}
+					console.log(this.questions);
+					// 设置总题目数
+					this.totalQuestions = this.questions.length;
+					
+					// 启动倒计时
+					if (this.questions.length > 0) {
+						this.startTimer();
+					}
+				} catch (error) {
+					console.error('获取面试详情失败:', error);
+					this.handleLoadError('获取面试详情失败');
+				} finally {
+					this.loading = false;
+				}
+			},
+			
+			// 处理面试数据
+			processInterviewData(data) {
+				// 清空现有问题列表
+				this.questions = [];
+				
+				// 检查数据格式并处理
+				if (data) {
+					// 创建一个格式化的问题对象
+					const formattedQuestion = {
+						id: data.id || 1,
+						text: data.question || '未知问题',
+						options: data.options || [],
+						correctAnswer: data.correctAnswer || 0,
+						isImportant: data.is_system || false,
+						explanation: data.explanation || '',
+						questionType: data.question_form || 1, // 1-单选题,2-多选题
+						questionTypeName: data.question_form_name || '单选题',
+						correctAnswers: data.correct_answers || [],
+						difficulty: data.difficulty || 1,
+						difficultyName: data.difficulty_name || '初级'
+					};
+					
+					// 添加到问题列表
+					this.questions.push(formattedQuestion);
+					
+					// 设置总题目数
+					this.totalQuestions = this.questions.length;
+					
+					// 启动倒计时
+					this.startTimer();
+				} else {
+					this.handleLoadError('面试中没有问题');
+				}
+			},
+			
+			// 处理加载错误
+			handleLoadError(message) {
+				this.loadError = true;
+				this.loading = false;
+				this.errorMessage = message || '加载失败';
+				uni.showToast({
+					title: message || '加载失败',
+					icon: 'none',
+					duration: 2000
+				});
+			},
+			
 			startTimer() {
+				// 确保问题已加载
+				if (this.questions.length === 0) return;
+				
 				// 模拟倒计时
 				let seconds = 30; // 每题30秒
 				this.timerInterval = setInterval(() => {
@@ -212,7 +319,20 @@
 			selectOption(index) {
 				if (this.showResult) return; // 已显示结果时不能再选择
 				
-				this.selectedOption = index;
+				// 判断当前题目类型
+				if (this.currentQuestion.questionType === 2) { // 多选题
+					// 如果已经选中,则取消选中
+					const optionIndex = this.selectedOptions.indexOf(index);
+					if (optionIndex > -1) {
+						this.selectedOptions.splice(optionIndex, 1);
+					} else {
+						// 否则添加到已选中数组
+						this.selectedOptions.push(index);
+					}
+				} else { // 单选题
+					this.selectedOption = index;
+				}
+				
 				this.playAiSpeaking();
 			},
 			
@@ -225,31 +345,61 @@
 					return;
 				}
 				
-				// 检查答案是否正确
-				this.isAnswerCorrect = this.selectedOption === this.currentQuestion.correctAnswer;
+				// 根据题目类型检查答案是否正确
+				if (this.currentQuestion.questionType === 2) { // 多选题
+					// 对于多选题,需要比较选中的选项数组和正确答案数组
+					// 先排序以确保顺序一致
+					const sortedSelected = [...this.selectedOptions].sort();
+					const sortedCorrect = [...this.currentQuestion.correctAnswers].sort();
+					
+					// 检查长度是否相同
+					if (sortedSelected.length !== sortedCorrect.length) {
+						this.isAnswerCorrect = false;
+					} else {
+						// 检查每个元素是否相同
+						this.isAnswerCorrect = sortedSelected.every((value, index) => value === sortedCorrect[index]);
+					}
+				} else { // 单选题
+					this.isAnswerCorrect = this.selectedOption === this.currentQuestion.correctAnswer;
+				}
 				
 				// 更新分数
 				if (this.isAnswerCorrect) {
 					this.score++;
 				}
 				
+				// 显示结果
+				this.showResult = true;
+				
 				// 检查是否是最后一题
 				if (this.currentQuestionIndex === this.questions.length - 1) {
 					// 显示AI面试结束页面
-					this.showEndModal = false;
-					this.interviewCompleted = true;
+					setTimeout(() => {
+						// this.showEndModal = true;
+						this.interviewCompleted = false;
+					}, 1500);
 					return;
 				}
 				
-				// 不是最后一题,进入下一题
-				this.goToNextQuestion();
+				// 不是最后一题,延迟后进入下一题
+				setTimeout(() => {
+					this.goToNextQuestion();
+				}, 1500);
 			},
 
 			nextQuestion() {
 				// 如果还没有显示结果,先检查答案
-				if (this.selectedOption !== null) {
-					this.checkAnswer();
-					return;
+				if (this.currentQuestion.questionType === 2) { // 多选题
+					if (this.selectedOptions.length > 0 && !this.showResult) {
+						this.checkAnswer();
+					}
+				} else { // 单选题
+					if (this.selectedOption !== null && !this.showResult) {
+						this.checkAnswer();
+					} else if (this.showResult) {
+						// 如果已经显示结果,进入下一题
+						this.goToNextQuestion();
+					}
 				}
 			},
 
@@ -258,6 +408,7 @@
 				// 重置状态
 				this.showResult = false;
 				this.selectedOption = null;
+				this.selectedOptions = []; // 重置多选题选项
 				
 				// 前往下一题
 				this.currentQuestionIndex++;
@@ -265,8 +416,8 @@
 				// 检查是否已完成所有题目
 				if (this.currentQuestionIndex >= this.questions.length) {
 					// 显示AI面试结束页面,确保弹窗不显示
-					this.showEndModal = false;
-					this.interviewCompleted = true;
+					this.showEndModal = true;
+					this.interviewCompleted = false;
 					
 					// 确保清除计时器
 					if (this.timerInterval) {
@@ -360,6 +511,7 @@
 				this.showEndModal = false;
 				this.showResult = false;
 				this.selectedOption = null;
+				this.selectedOptions = [];
 				this.resetTimer();
 			},
 
@@ -371,11 +523,16 @@
 
 			// 初始化数字人
 			initDigitalHuman() {
-				// 这里可以根据实际情况设置数字人的URL
-				// 例如,可以是一个第三方数字人服务的URL,或者是本地的HTML页面
-				this.digitalHumanUrl = 'https://your-digital-human-service.com/avatar?id=123';
+				// 移除不可用的示例URL
+				// this.digitalHumanUrl = 'https://your-digital-human-service.com/avatar?id=123';
 				
-				// 如果使用本地HTML,可以这样设置
+				// 暂时不使用数字人服务,直接使用本地图片
+				this.digitalHumanUrl = '';
+				
+				// 如果您有实际可用的数字人服务,可以在这里设置
+				// 例如:this.digitalHumanUrl = 'https://your-actual-service.com/avatar';
+				
+				// 或者使用本地HTML文件(如果有的话)
 				// this.digitalHumanUrl = '/hybrid/html/digital-human.html';
 			},
 			
@@ -799,4 +956,53 @@
 		padding: 20rpx 60rpx;
 		margin-top: 40rpx;
 	}
+
+	/* 添加加载状态样式 */
+	.loading-container {
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		background-color: rgba(255, 255, 255, 0.9);
+		z-index: 100;
+	}
+	
+	.loading-text {
+		margin-top: 20rpx;
+		font-size: 28rpx;
+		color: #666;
+	}
+	
+	.error-container {
+		text-align: center;
+	}
+	
+	.error-message {
+		font-size: 28rpx;
+		color: #ff4d4f;
+		margin-bottom: 30rpx;
+	}
+	
+	.retry-button {
+		background-color: #6c5ce7;
+		color: #ffffff;
+		border-radius: 10rpx;
+		font-size: 28rpx;
+		padding: 10rpx 30rpx;
+	}
+
+	.question-type {
+		display: inline-block;
+		background-color: #6c5ce7;
+		color: white;
+		font-size: 24rpx;
+		padding: 4rpx 10rpx;
+		border-radius: 6rpx;
+		margin-right: 10rpx;
+	}
 </style>

+ 204 - 23
pages/face-photo/face-photo.vue

@@ -6,18 +6,37 @@
       <text class="photo-subtitle">我们将用于身份核验,请正对摄像头</text>
     </view>
     
-    <!-- 照片预览区域 -->
+    <!-- 模式选择 -->
+    <view class="mode-selector">
+      <view class="mode-option" :class="{'active': mode === 'photo'}" @click="switchMode('photo')">拍照</view>
+      <view class="mode-option" :class="{'active': mode === 'video'}" @click="switchMode('video')">录制视频</view>
+    </view>
+    
+    <!-- 照片/视频预览区域 -->
     <view class="photo-preview">
       <!-- 使用camera组件替代静态图片 -->
-      <camera v-if="!photoSrc" device-position="front" flash="auto" class="camera" @error="handleCameraError"></camera>
-      <image v-else class="preview-image" :src="photoSrc" mode="aspectFit"></image>
+      <camera v-if="!mediaSource" device-position="front" flash="auto" class="camera" 
+              :mode="mode" @error="handleCameraError"></camera>
+      <image v-else-if="mode === 'photo'" class="preview-image" :src="mediaSource" mode="aspectFit"></image>
+      <video v-else-if="mode === 'video'" class="preview-video" :src="mediaSource" 
+             controls autoplay></video>
       <view class="face-outline"></view>
+      
+      <!-- 视频录制时间显示 -->
+      <view v-if="mode === 'video' && isRecording" class="recording-indicator">
+        <view class="recording-dot"></view>
+        <text class="recording-time">{{formatTime(recordingTime)}}</text>
+      </view>
     </view>
     
-    <!-- 拍照按钮 -->
-    <button v-if="!photoSrc" class="capture-btn" @click="takePhoto">拍照</button>
+    <!-- 操作按钮 -->
+    <view v-if="!mediaSource" class="capture-btn-container">
+      <button v-if="mode === 'photo'" class="capture-btn" @click="takePhoto">拍照</button>
+      <button v-else-if="mode === 'video' && !isRecording" class="capture-btn" @click="startRecording">开始录制</button>
+      <button v-else-if="mode === 'video' && isRecording" class="stop-btn" @click="stopRecording">停止录制</button>
+    </view>
     <view v-else class="btn-group">
-      <button class="retry-btn" @click="retakePhoto">重新拍照</button>
+      <button class="retry-btn" @click="retakeMedia">重新{{mode === 'photo' ? '拍照' : '录制'}}</button>
       <button class="start-btn" @click="startInterview">开始面试</button>
     </view>
   </view>
@@ -27,10 +46,14 @@
 export default {
   data() {
     return {
-      photoSrc: '', // 拍摄的照片路径
+      mediaSource: '', // 拍摄的照片或视频路径
       isCameraReady: false,
       cameraContext: null,
-      isPageLoaded: false // 添加页面加载状态标记
+      isPageLoaded: false, // 添加页面加载状态标记
+      mode: 'photo', // 默认为拍照模式,可选值:photo, video
+      isRecording: false, // 是否正在录制视频
+      recordingTime: 0, // 录制时间(秒)
+      recordingTimer: null, // 录制计时器
     }
   },
   onReady() {
@@ -42,6 +65,15 @@ export default {
     }, 100);
   },
   methods: {
+    // 切换拍照/录制模式
+    switchMode(newMode) {
+      if (this.mediaSource) {
+        // 如果已经有拍摄内容,需要先清除
+        this.retakeMedia();
+      }
+      this.mode = newMode;
+    },
+    
     // 处理相机错误
     handleCameraError(e) {
       console.error('相机错误:', e);
@@ -68,7 +100,7 @@ export default {
       this.cameraContext.takePhoto({
         quality: 'high',
         success: (res) => {
-          this.photoSrc = res.tempImagePath;
+          this.mediaSource = res.tempImagePath;
           uni.hideLoading();
         },
         fail: (err) => {
@@ -82,15 +114,89 @@ export default {
       });
     },
     
-    // 重新拍照
-    retakePhoto() {
-      this.photoSrc = '';
+    // 开始录制视频
+    startRecording() {
+      if (!this.cameraContext) {
+        uni.showToast({
+          title: '相机未就绪',
+          icon: 'none'
+        });
+        return;
+      }
+      
+      this.isRecording = true;
+      this.recordingTime = 0;
+      
+      // 开始计时
+      this.recordingTimer = setInterval(() => {
+        this.recordingTime++;
+        
+        // 限制最长录制时间为60秒
+        if (this.recordingTime >= 60) {
+          this.stopRecording();
+        }
+      }, 1000);
+      
+      this.cameraContext.startRecord({
+        success: () => {
+          console.log('开始录制视频');
+        },
+        fail: (err) => {
+          console.error('开始录制失败:', err);
+          this.isRecording = false;
+          clearInterval(this.recordingTimer);
+          uni.showToast({
+            title: '录制失败,请重试',
+            icon: 'none'
+          });
+        }
+      });
+    },
+    
+    // 停止录制视频
+    stopRecording() {
+      if (!this.isRecording) return;
+      
+      clearInterval(this.recordingTimer);
+      this.isRecording = false;
+      
+      uni.showLoading({
+        title: '处理中...'
+      });
+      
+      this.cameraContext.stopRecord({
+        success: (res) => {
+          this.mediaSource = res.tempVideoPath;
+          uni.hideLoading();
+        },
+        fail: (err) => {
+          console.error('停止录制失败:', err);
+          uni.hideLoading();
+          uni.showToast({
+            title: '视频保存失败,请重试',
+            icon: 'none'
+          });
+        }
+      });
+    },
+    
+    // 格式化时间显示 (秒 -> MM:SS)
+    formatTime(seconds) {
+      const minutes = Math.floor(seconds / 60);
+      const remainingSeconds = seconds % 60;
+      return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
+    },
+    
+    // 重新拍照或录制
+    retakeMedia() {
+      this.mediaSource = '';
+      this.recordingTime = 0;
     },
     
     startInterview() {
-      if (!this.photoSrc) {
+      if (!this.mediaSource) {
         uni.showToast({
-          title: '请先完成拍照',
+          title: `请先完成${this.mode === 'photo' ? '拍照' : '视频录制'}`,
           icon: 'none'
         });
         return;
@@ -100,8 +206,8 @@ export default {
         title: '验证中...'
       });
       
-      // 这里可以添加将照片上传到服务器进行身份验证的代码
-      // 例如:this.uploadPhoto();
+      // 这里可以添加将照片或视频上传到服务器进行身份验证的代码
+      // 例如:this.uploadMedia();
       
       setTimeout(() => {
         uni.hideLoading();
@@ -118,13 +224,13 @@ export default {
       }, 1500);
     }
     
-    // 上传照片方法(示例)
+    // 上传媒体文件方法(示例)
     /* 
-    uploadPhoto() {
+    uploadMedia() {
       uni.uploadFile({
         url: 'https://your-api-endpoint.com/upload',
-        filePath: this.photoSrc,
-        name: 'photo',
+        filePath: this.mediaSource,
+        name: this.mode === 'photo' ? 'photo' : 'video',
         success: (res) => {
           const data = JSON.parse(res.data);
           if (data.success) {
@@ -170,7 +276,7 @@ export default {
   display: flex;
   flex-direction: column;
   align-items: center;
-  margin-bottom: 40rpx;
+  margin-bottom: 20rpx;
 }
 
 .photo-title {
@@ -185,6 +291,33 @@ export default {
   color: #666;
 }
 
+/* 模式选择器样式 */
+.mode-selector {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 30rpx;
+  background-color: #eaeaea;
+  border-radius: 40rpx;
+  padding: 6rpx;
+  width: 80%;
+  align-self: center;
+}
+
+.mode-option {
+  flex: 1;
+  text-align: center;
+  padding: 16rpx 0;
+  font-size: 28rpx;
+  color: #666;
+  border-radius: 36rpx;
+  transition: all 0.3s;
+}
+
+.mode-option.active {
+  background-color: #6c5ce7;
+  color: #fff;
+}
+
 .photo-preview {
   position: relative;
   width: 100%;
@@ -198,7 +331,7 @@ export default {
   align-items: center;
 }
 
-.preview-image {
+.preview-image, .preview-video {
   width: 100%;
   height: 100%;
   object-fit: cover;
@@ -221,6 +354,45 @@ export default {
   height: 100%;
 }
 
+/* 录制指示器 */
+.recording-indicator {
+  position: absolute;
+  top: 30rpx;
+  right: 30rpx;
+  display: flex;
+  align-items: center;
+  background-color: rgba(0, 0, 0, 0.5);
+  padding: 10rpx 20rpx;
+  border-radius: 30rpx;
+}
+
+.recording-dot {
+  width: 20rpx;
+  height: 20rpx;
+  background-color: #ff0000;
+  border-radius: 50%;
+  margin-right: 10rpx;
+  animation: blink 1s infinite;
+}
+
+.recording-time {
+  color: #fff;
+  font-size: 28rpx;
+}
+
+@keyframes blink {
+  0% { opacity: 1; }
+  50% { opacity: 0.3; }
+  100% { opacity: 1; }
+}
+
+.capture-btn-container {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  margin-top: 60rpx;
+}
+
 .capture-btn {
   width: 100%;
   height: 90rpx;
@@ -229,7 +401,16 @@ export default {
   color: #fff;
   border-radius: 45rpx;
   font-size: 32rpx;
-  margin-top: 60rpx;
+}
+
+.stop-btn {
+  width: 100%;
+  height: 90rpx;
+  line-height: 90rpx;
+  background-color: #e74c3c;
+  color: #fff;
+  border-radius: 45rpx;
+  font-size: 32rpx;
 }
 
 .btn-group {

+ 8 - 0
unpackage/dist/dev/mp-weixin/api/user.js

@@ -51,7 +51,15 @@ const getJobList = (params = {}) => {
 const fillUserInfo = (params) => {
   return utils_request.http.post("/api/system/wechat/save_user_info", params);
 };
+const getInterviewList = (params) => {
+  return utils_request.http.get("/system/interview_question/list", params);
+};
+const getInterviewDetail = (params) => {
+  return utils_request.http.get("/system/interview_question/detail", params);
+};
 exports.fillUserInfo = fillUserInfo;
+exports.getInterviewDetail = getInterviewDetail;
+exports.getInterviewList = getInterviewList;
 exports.getJobList = getJobList;
 exports.getUserInfo = getUserInfo;
 exports.getUserPhoneNumber = getUserPhoneNumber;

+ 64 - 6
unpackage/dist/dev/mp-weixin/common/vendor.js

@@ -732,8 +732,8 @@ function promisify$1(name, fn) {
     if (hasCallback(args)) {
       return wrapperReturnValue(name, invokeApi(name, fn, args, rest));
     }
-    return wrapperReturnValue(name, handlePromise(new Promise((resolve, reject) => {
-      invokeApi(name, fn, extend(args, { success: resolve, fail: reject }), rest);
+    return wrapperReturnValue(name, handlePromise(new Promise((resolve2, reject) => {
+      invokeApi(name, fn, extend(args, { success: resolve2, fail: reject }), rest);
     })));
   };
 }
@@ -1031,7 +1031,7 @@ function invokeGetPushCidCallbacks(cid2, errMsg) {
   getPushCidCallbacks.length = 0;
 }
 const API_GET_PUSH_CLIENT_ID = "getPushClientId";
-const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve, reject }) => {
+const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve: resolve2, reject }) => {
   Promise.resolve().then(() => {
     if (typeof enabled === "undefined") {
       enabled = false;
@@ -1040,7 +1040,7 @@ const getPushClientId = defineAsyncApi(API_GET_PUSH_CLIENT_ID, (_, { resolve, re
     }
     getPushCidCallbacks.push((cid2, errMsg) => {
       if (cid2) {
-        resolve({ cid: cid2 });
+        resolve2({ cid: cid2 });
       } else {
         reject(errMsg);
       }
@@ -1105,9 +1105,9 @@ function promisify(name, api) {
     if (isFunction(options.success) || isFunction(options.fail) || isFunction(options.complete)) {
       return wrapperReturnValue(name, invokeApi(name, api, options, rest));
     }
-    return wrapperReturnValue(name, handlePromise(new Promise((resolve, reject) => {
+    return wrapperReturnValue(name, handlePromise(new Promise((resolve2, reject) => {
       invokeApi(name, api, extend({}, options, {
-        success: resolve,
+        success: resolve2,
         fail: reject
       }), rest);
     })));
@@ -2963,6 +2963,9 @@ function isReadonly(value) {
 function isShallow(value) {
   return !!(value && value["__v_isShallow"]);
 }
+function isProxy(value) {
+  return isReactive(value) || isReadonly(value);
+}
 function toRaw(observed) {
   const raw = observed && observed["__v_raw"];
   return raw ? toRaw(raw) : observed;
@@ -3757,6 +3760,47 @@ function setCurrentRenderingInstance(instance) {
   instance && instance.type.__scopeId || null;
   return prev;
 }
+const COMPONENTS = "components";
+function resolveComponent(name, maybeSelfReference) {
+  return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name;
+}
+function resolveAsset(type, name, warnMissing = true, maybeSelfReference = false) {
+  const instance = currentRenderingInstance || currentInstance;
+  if (instance) {
+    const Component2 = instance.type;
+    if (type === COMPONENTS) {
+      const selfName = getComponentName(
+        Component2,
+        false
+      );
+      if (selfName && (selfName === name || selfName === camelize(name) || selfName === capitalize(camelize(name)))) {
+        return Component2;
+      }
+    }
+    const res = (
+      // local registration
+      // check instance[type] first which is resolved for options API
+      resolve(instance[type] || Component2[type], name) || // global registration
+      resolve(instance.appContext[type], name)
+    );
+    if (!res && maybeSelfReference) {
+      return Component2;
+    }
+    if (warnMissing && !res) {
+      const extra = type === COMPONENTS ? `
+If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.` : ``;
+      warn$1(`Failed to resolve ${type.slice(0, -1)}: ${name}${extra}`);
+    }
+    return res;
+  } else {
+    warn$1(
+      `resolve${capitalize(type.slice(0, -1))} can only be used in render() or setup().`
+    );
+  }
+}
+function resolve(registry, name) {
+  return registry && (registry[name] || registry[camelize(name)] || registry[capitalize(camelize(name))]);
+}
 const INITIAL_WATCHER_VALUE = {};
 function watch(source, cb, options) {
   if (!isFunction(cb)) {
@@ -5364,6 +5408,12 @@ const Static = Symbol.for("v-stc");
 function isVNode(value) {
   return value ? value.__v_isVNode === true : false;
 }
+const InternalObjectKey = `__vInternal`;
+function guardReactiveProps(props) {
+  if (!props)
+    return null;
+  return isProxy(props) || InternalObjectKey in props ? extend({}, props) : props;
+}
 const emptyAppContext = createAppContext();
 let uid = 0;
 function createComponentInstance(vnode, parent, suspense) {
@@ -6548,6 +6598,11 @@ function initApp(app) {
   }
 }
 const propsCaches = /* @__PURE__ */ Object.create(null);
+function renderProps(props) {
+  const { uid: uid2, __counter } = getCurrentInstance();
+  const propsId = (propsCaches[uid2] || (propsCaches[uid2] = [])).push(guardReactiveProps(props)) - 1;
+  return uid2 + "," + propsId + "," + __counter;
+}
 function pruneComponentPropsCache(uid2) {
   delete propsCaches[uid2];
 }
@@ -6714,6 +6769,7 @@ const f = (source, renderItem) => vFor(source, renderItem);
 const e = (target, ...sources) => extend(target, ...sources);
 const n = (value) => normalizeClass(value);
 const t = (val) => toDisplayString(val);
+const p = (props) => renderProps(props);
 function createApp$1(rootComponent, rootProps = null) {
   rootComponent && (rootComponent.mpType = "app");
   return createVueApp(rootComponent, rootProps).use(plugin);
@@ -7552,5 +7608,7 @@ exports.f = f;
 exports.index = index;
 exports.n = n;
 exports.o = o;
+exports.p = p;
+exports.resolveComponent = resolveComponent;
 exports.t = t;
 exports.wx$1 = wx$1;

+ 187 - 72
unpackage/dist/dev/mp-weixin/pages/camera/camera.js

@@ -1,5 +1,6 @@
 "use strict";
 const common_vendor = require("../../common/vendor.js");
+const api_user = require("../../api/user.js");
 const common_assets = require("../../common/assets.js");
 const _sfc_main = {
   data() {
@@ -14,74 +15,137 @@ const _sfc_main = {
       progressWidth: 50,
       remainingTime: "00:27",
       selectedOption: null,
+      selectedOptions: [],
       showResult: false,
       isAnswerCorrect: false,
-      questions: [
-        {
-          id: 6,
-          text: "以下不属于中国传统节日的是( )。",
-          options: [
-            "A. 春节",
-            "B. 端午节",
-            "C. 重阳节",
-            "D. 元旦"
-          ],
-          correctAnswer: 3,
-          isImportant: true,
-          explanation: "元旦是公历新年,属于现代节日,而春节、端午节和重阳节都是中国传统节日。"
-        },
-        {
-          id: 7,
-          text: "下列哪个是中国四大名著之一( )。",
-          options: [
-            "A. 聊斋志异",
-            "B. 西游记",
-            "C. 世说新语",
-            "D. 聊斋志异"
-          ],
-          correctAnswer: 1,
-          isImportant: false,
-          explanation: "中国四大名著是《红楼梦》、《西游记》、《水浒传》和《三国演义》。"
-        },
-        {
-          id: 8,
-          text: '中国传统文化中"仁义礼智信"五常不包括( )。',
-          options: [
-            "A. 忠",
-            "B. 孝",
-            "C. 礼",
-            "D. 信"
-          ],
-          correctAnswer: 1,
-          isImportant: true,
-          explanation: '儒家的五常是"仁、义、礼、智、信",不包括"孝"。'
-        }
-      ],
+      questions: [],
+      // 改为空数组,将通过API获取
+      interviewId: null,
+      // 存储当前面试ID
       useVideo: false,
       timerInterval: null,
       score: 0,
       totalQuestions: 0,
       interviewCompleted: false,
-      digitalHumanUrl: ""
+      digitalHumanUrl: "",
       // 数字人URL
+      loading: true,
+      // 添加加载状态
+      loadError: false,
+      // 添加加载错误状态
+      errorMessage: ""
+      // 添加错误消息
     };
   },
   computed: {
     currentQuestion() {
+      console.log(this.questions[this.currentQuestionIndex]);
       return this.questions[this.currentQuestionIndex];
     }
   },
+  onLoad(options) {
+    if (options && options.id) {
+      this.interviewId = options.id;
+      this.fetchInterviewData();
+    } else {
+      this.fetchInterviewList();
+    }
+  },
   onReady() {
     this.cameraContext = common_vendor.index.createCameraContext();
     if (this.useVideo) {
       this.aiVideoContext = common_vendor.index.createVideoContext("aiInterviewer");
     }
-    this.startTimer();
-    this.totalQuestions = this.questions.length;
     this.initDigitalHuman();
   },
   methods: {
+    // 获取面试列表
+    async fetchInterviewList() {
+      try {
+        this.loading = true;
+        const res = await api_user.getInterviewList();
+        console.log(res);
+        this.interviewId = res.items[0].id;
+        this.fetchInterviewData();
+      } catch (error) {
+        console.error("获取面试列表失败:", error);
+        this.handleLoadError("获取面试列表失败");
+      }
+    },
+    // 获取面试详情数据
+    async fetchInterviewData() {
+      try {
+        this.loading = true;
+        const res = await api_user.getInterviewDetail({ id: this.interviewId });
+        console.log("API返回数据:", res);
+        if (res && Array.isArray(res.items)) {
+          this.questions = res.items.map((q, index) => ({
+            id: q.id || index + 1,
+            text: q.question || "未知问题",
+            options: q.options || [],
+            correctAnswer: q.correctAnswer || 0,
+            isImportant: q.is_system || false,
+            explanation: q.explanation || "",
+            questionType: q.question_form || 1,
+            questionTypeName: q.question_form_name || "单选题",
+            correctAnswers: q.correct_answers || [],
+            difficulty: q.difficulty || 1,
+            difficultyName: q.difficulty_name || "初级"
+          }));
+        } else {
+          this.processInterviewData(res);
+        }
+        console.log(this.questions);
+        this.totalQuestions = this.questions.length;
+        if (this.questions.length > 0) {
+          this.startTimer();
+        }
+      } catch (error) {
+        console.error("获取面试详情失败:", error);
+        this.handleLoadError("获取面试详情失败");
+      } finally {
+        this.loading = false;
+      }
+    },
+    // 处理面试数据
+    processInterviewData(data) {
+      this.questions = [];
+      if (data) {
+        const formattedQuestion = {
+          id: data.id || 1,
+          text: data.question || "未知问题",
+          options: data.options || [],
+          correctAnswer: data.correctAnswer || 0,
+          isImportant: data.is_system || false,
+          explanation: data.explanation || "",
+          questionType: data.question_form || 1,
+          // 1-单选题,2-多选题
+          questionTypeName: data.question_form_name || "单选题",
+          correctAnswers: data.correct_answers || [],
+          difficulty: data.difficulty || 1,
+          difficultyName: data.difficulty_name || "初级"
+        };
+        this.questions.push(formattedQuestion);
+        this.totalQuestions = this.questions.length;
+        this.startTimer();
+      } else {
+        this.handleLoadError("面试中没有问题");
+      }
+    },
+    // 处理加载错误
+    handleLoadError(message) {
+      this.loadError = true;
+      this.loading = false;
+      this.errorMessage = message || "加载失败";
+      common_vendor.index.showToast({
+        title: message || "加载失败",
+        icon: "none",
+        duration: 2e3
+      });
+    },
     startTimer() {
+      if (this.questions.length === 0)
+        return;
       let seconds = 30;
       this.timerInterval = setInterval(() => {
         seconds--;
@@ -103,7 +167,16 @@ const _sfc_main = {
     selectOption(index) {
       if (this.showResult)
         return;
-      this.selectedOption = index;
+      if (this.currentQuestion.questionType === 2) {
+        const optionIndex = this.selectedOptions.indexOf(index);
+        if (optionIndex > -1) {
+          this.selectedOptions.splice(optionIndex, 1);
+        } else {
+          this.selectedOptions.push(index);
+        }
+      } else {
+        this.selectedOption = index;
+      }
       this.playAiSpeaking();
     },
     checkAnswer() {
@@ -112,31 +185,53 @@ const _sfc_main = {
         console.error("当前问题不存在");
         return;
       }
-      this.isAnswerCorrect = this.selectedOption === this.currentQuestion.correctAnswer;
+      if (this.currentQuestion.questionType === 2) {
+        const sortedSelected = [...this.selectedOptions].sort();
+        const sortedCorrect = [...this.currentQuestion.correctAnswers].sort();
+        if (sortedSelected.length !== sortedCorrect.length) {
+          this.isAnswerCorrect = false;
+        } else {
+          this.isAnswerCorrect = sortedSelected.every((value, index) => value === sortedCorrect[index]);
+        }
+      } else {
+        this.isAnswerCorrect = this.selectedOption === this.currentQuestion.correctAnswer;
+      }
       if (this.isAnswerCorrect) {
         this.score++;
       }
+      this.showResult = true;
       if (this.currentQuestionIndex === this.questions.length - 1) {
-        this.showEndModal = false;
-        this.interviewCompleted = true;
+        setTimeout(() => {
+          this.interviewCompleted = false;
+        }, 1500);
         return;
       }
-      this.goToNextQuestion();
+      setTimeout(() => {
+        this.goToNextQuestion();
+      }, 1500);
     },
     nextQuestion() {
-      if (this.selectedOption !== null) {
-        this.checkAnswer();
-        return;
+      if (this.currentQuestion.questionType === 2) {
+        if (this.selectedOptions.length > 0 && !this.showResult) {
+          this.checkAnswer();
+        }
+      } else {
+        if (this.selectedOption !== null && !this.showResult) {
+          this.checkAnswer();
+        } else if (this.showResult) {
+          this.goToNextQuestion();
+        }
       }
     },
     // 新增方法,处理进入下一题的逻辑
     goToNextQuestion() {
       this.showResult = false;
       this.selectedOption = null;
+      this.selectedOptions = [];
       this.currentQuestionIndex++;
       if (this.currentQuestionIndex >= this.questions.length) {
-        this.showEndModal = false;
-        this.interviewCompleted = true;
+        this.showEndModal = true;
+        this.interviewCompleted = false;
         if (this.timerInterval) {
           clearInterval(this.timerInterval);
         }
@@ -205,6 +300,7 @@ const _sfc_main = {
       this.showEndModal = false;
       this.showResult = false;
       this.selectedOption = null;
+      this.selectedOptions = [];
       this.resetTimer();
     },
     // 在methods中添加测试方法
@@ -214,7 +310,7 @@ const _sfc_main = {
     },
     // 初始化数字人
     initDigitalHuman() {
-      this.digitalHumanUrl = "https://your-digital-human-service.com/avatar?id=123";
+      this.digitalHumanUrl = "";
     },
     // 与数字人交互的方法
     interactWithDigitalHuman(message) {
@@ -231,6 +327,10 @@ const _sfc_main = {
     }
   }
 };
+if (!Array) {
+  const _component_uni_load_more = common_vendor.resolveComponent("uni-load-more");
+  _component_uni_load_more();
+}
 function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
   return common_vendor.e({
     a: $data.digitalHumanUrl
@@ -244,33 +344,48 @@ function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
     f: common_vendor.t($data.questions.length),
     g: $options.currentQuestion.isImportant
   }, $options.currentQuestion.isImportant ? {} : {}, {
-    h: common_vendor.t($options.currentQuestion.text),
-    i: common_vendor.f($options.currentQuestion.options, (option, index, i0) => {
+    h: common_vendor.t($options.currentQuestion.questionTypeName),
+    i: common_vendor.t($options.currentQuestion.text),
+    j: common_vendor.f($options.currentQuestion.options, (option, index, i0) => {
       return {
-        a: common_vendor.t(option),
+        a: common_vendor.t(option.option_text || (typeof option === "string" ? option : JSON.stringify(option))),
         b: index,
-        c: $data.selectedOption === index ? 1 : "",
-        d: $data.showResult && index === $options.currentQuestion.correctAnswer ? 1 : "",
-        e: $data.showResult && $data.selectedOption === index && index !== $options.currentQuestion.correctAnswer ? 1 : "",
+        c: ($options.currentQuestion.questionType === 1 ? $data.selectedOption === index : $data.selectedOptions.includes(index)) ? 1 : "",
+        d: $data.showResult && ($options.currentQuestion.questionType === 1 ? index === $options.currentQuestion.correctAnswer : $options.currentQuestion.correctAnswers.includes(index)) ? 1 : "",
+        e: $data.showResult && ($options.currentQuestion.questionType === 1 ? $data.selectedOption === index && index !== $options.currentQuestion.correctAnswer : $data.selectedOptions.includes(index) && !$options.currentQuestion.correctAnswers.includes(index)) ? 1 : "",
         f: common_vendor.o(($event) => $options.selectOption(index), index)
       };
     }),
-    j: common_vendor.t($data.remainingTime),
-    k: common_vendor.o((...args) => $options.nextQuestion && $options.nextQuestion(...args)),
-    l: $data.selectedOption === null,
-    m: $data.showEndModal
+    k: common_vendor.t($data.remainingTime),
+    l: common_vendor.t($data.showResult ? "下一题" : "提交答案"),
+    m: common_vendor.o((...args) => $options.nextQuestion && $options.nextQuestion(...args)),
+    n: $options.currentQuestion.questionType === 1 ? $data.selectedOption === null : $data.selectedOptions.length === 0,
+    o: $data.showEndModal
   }, $data.showEndModal ? {
-    n: common_vendor.t($data.score),
-    o: common_vendor.t($data.totalQuestions),
     p: common_vendor.t($data.score),
     q: common_vendor.t($data.totalQuestions),
-    r: common_vendor.o((...args) => $options.restartTest && $options.restartTest(...args)),
-    s: common_vendor.o((...args) => $options.back && $options.back(...args))
+    r: common_vendor.t($data.score),
+    s: common_vendor.t($data.totalQuestions),
+    t: common_vendor.o((...args) => $options.restartTest && $options.restartTest(...args)),
+    v: common_vendor.o((...args) => $options.back && $options.back(...args))
   } : {}, {
-    t: $data.interviewCompleted
+    w: $data.interviewCompleted
   }, $data.interviewCompleted ? {
-    v: common_assets._imports_0,
-    w: common_vendor.o((...args) => $options.back && $options.back(...args))
+    x: common_assets._imports_0,
+    y: common_vendor.o((...args) => $options.back && $options.back(...args))
+  } : {}, {
+    z: $data.loading
+  }, $data.loading ? {
+    A: common_vendor.p({
+      status: "loading",
+      contentText: {
+        contentdown: "加载中..."
+      }
+    })
+  } : {}, {
+    B: !$data.loading && $data.loadError
+  }, !$data.loading && $data.loadError ? {
+    C: common_vendor.o((...args) => $options.fetchInterviewData && $options.fetchInterviewData(...args))
   } : {});
 }
 const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 0 - 1
unpackage/dist/dev/mp-weixin/pages/camera/camera.wxml


+ 44 - 0
unpackage/dist/dev/mp-weixin/pages/camera/camera.wxss

@@ -343,3 +343,47 @@
 		padding: 20rpx 60rpx;
 		margin-top: 40rpx;
 }
+
+	/* 添加加载状态样式 */
+.loading-container {
+		position: absolute;
+		top: 0;
+		left: 0;
+		right: 0;
+		bottom: 0;
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+		background-color: rgba(255, 255, 255, 0.9);
+		z-index: 100;
+}
+.loading-text {
+		margin-top: 20rpx;
+		font-size: 28rpx;
+		color: #666;
+}
+.error-container {
+		text-align: center;
+}
+.error-message {
+		font-size: 28rpx;
+		color: #ff4d4f;
+		margin-bottom: 30rpx;
+}
+.retry-button {
+		background-color: #6c5ce7;
+		color: #ffffff;
+		border-radius: 10rpx;
+		font-size: 28rpx;
+		padding: 10rpx 30rpx;
+}
+.question-type {
+		display: inline-block;
+		background-color: #6c5ce7;
+		color: white;
+		font-size: 24rpx;
+		padding: 4rpx 10rpx;
+		border-radius: 6rpx;
+		margin-right: 10rpx;
+}

+ 127 - 26
unpackage/dist/dev/mp-weixin/pages/face-photo/face-photo.js

@@ -3,12 +3,20 @@ const common_vendor = require("../../common/vendor.js");
 const _sfc_main = {
   data() {
     return {
-      photoSrc: "",
-      // 拍摄的照片路径
+      mediaSource: "",
+      // 拍摄的照片或视频路径
       isCameraReady: false,
       cameraContext: null,
-      isPageLoaded: false
+      isPageLoaded: false,
       // 添加页面加载状态标记
+      mode: "photo",
+      // 默认为拍照模式,可选值:photo, video
+      isRecording: false,
+      // 是否正在录制视频
+      recordingTime: 0,
+      // 录制时间(秒)
+      recordingTimer: null
+      // 录制计时器
     };
   },
   onReady() {
@@ -18,6 +26,13 @@ const _sfc_main = {
     }, 100);
   },
   methods: {
+    // 切换拍照/录制模式
+    switchMode(newMode) {
+      if (this.mediaSource) {
+        this.retakeMedia();
+      }
+      this.mode = newMode;
+    },
     // 处理相机错误
     handleCameraError(e) {
       console.error("相机错误:", e);
@@ -41,7 +56,7 @@ const _sfc_main = {
       this.cameraContext.takePhoto({
         quality: "high",
         success: (res) => {
-          this.photoSrc = res.tempImagePath;
+          this.mediaSource = res.tempImagePath;
           common_vendor.index.hideLoading();
         },
         fail: (err) => {
@@ -54,14 +69,77 @@ const _sfc_main = {
         }
       });
     },
-    // 重新拍照
-    retakePhoto() {
-      this.photoSrc = "";
+    // 开始录制视频
+    startRecording() {
+      if (!this.cameraContext) {
+        common_vendor.index.showToast({
+          title: "相机未就绪",
+          icon: "none"
+        });
+        return;
+      }
+      this.isRecording = true;
+      this.recordingTime = 0;
+      this.recordingTimer = setInterval(() => {
+        this.recordingTime++;
+        if (this.recordingTime >= 60) {
+          this.stopRecording();
+        }
+      }, 1e3);
+      this.cameraContext.startRecord({
+        success: () => {
+          console.log("开始录制视频");
+        },
+        fail: (err) => {
+          console.error("开始录制失败:", err);
+          this.isRecording = false;
+          clearInterval(this.recordingTimer);
+          common_vendor.index.showToast({
+            title: "录制失败,请重试",
+            icon: "none"
+          });
+        }
+      });
+    },
+    // 停止录制视频
+    stopRecording() {
+      if (!this.isRecording)
+        return;
+      clearInterval(this.recordingTimer);
+      this.isRecording = false;
+      common_vendor.index.showLoading({
+        title: "处理中..."
+      });
+      this.cameraContext.stopRecord({
+        success: (res) => {
+          this.mediaSource = res.tempVideoPath;
+          common_vendor.index.hideLoading();
+        },
+        fail: (err) => {
+          console.error("停止录制失败:", err);
+          common_vendor.index.hideLoading();
+          common_vendor.index.showToast({
+            title: "视频保存失败,请重试",
+            icon: "none"
+          });
+        }
+      });
+    },
+    // 格式化时间显示 (秒 -> MM:SS)
+    formatTime(seconds) {
+      const minutes = Math.floor(seconds / 60);
+      const remainingSeconds = seconds % 60;
+      return `${minutes.toString().padStart(2, "0")}:${remainingSeconds.toString().padStart(2, "0")}`;
+    },
+    // 重新拍照或录制
+    retakeMedia() {
+      this.mediaSource = "";
+      this.recordingTime = 0;
     },
     startInterview() {
-      if (!this.photoSrc) {
+      if (!this.mediaSource) {
         common_vendor.index.showToast({
-          title: "请先完成拍照",
+          title: `请先完成${this.mode === "photo" ? "拍照" : "视频录制"}`,
           icon: "none"
         });
         return;
@@ -83,13 +161,13 @@ const _sfc_main = {
         });
       }, 1500);
     }
-    // 上传照片方法(示例)
+    // 上传媒体文件方法(示例)
     /* 
-    uploadPhoto() {
+    uploadMedia() {
       uni.uploadFile({
         url: 'https://your-api-endpoint.com/upload',
-        filePath: this.photoSrc,
-        name: 'photo',
+        filePath: this.mediaSource,
+        name: this.mode === 'photo' ? 'photo' : 'video',
         success: (res) => {
           const data = JSON.parse(res.data);
           if (data.success) {
@@ -116,20 +194,43 @@ const _sfc_main = {
 };
 function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
   return common_vendor.e({
-    a: !$data.photoSrc
-  }, !$data.photoSrc ? {
-    b: common_vendor.o((...args) => $options.handleCameraError && $options.handleCameraError(...args))
-  } : {
-    c: $data.photoSrc
-  }, {
-    d: !$data.photoSrc
-  }, !$data.photoSrc ? {
-    e: common_vendor.o((...args) => $options.takePhoto && $options.takePhoto(...args))
-  } : {
-    f: common_vendor.o((...args) => $options.retakePhoto && $options.retakePhoto(...args)),
-    g: common_vendor.o((...args) => $options.startInterview && $options.startInterview(...args))
+    a: $data.mode === "photo" ? 1 : "",
+    b: common_vendor.o(($event) => $options.switchMode("photo")),
+    c: $data.mode === "video" ? 1 : "",
+    d: common_vendor.o(($event) => $options.switchMode("video")),
+    e: !$data.mediaSource
+  }, !$data.mediaSource ? {
+    f: $data.mode,
+    g: common_vendor.o((...args) => $options.handleCameraError && $options.handleCameraError(...args))
+  } : $data.mode === "photo" ? {
+    i: $data.mediaSource
+  } : $data.mode === "video" ? {
+    k: $data.mediaSource
+  } : {}, {
+    h: $data.mode === "photo",
+    j: $data.mode === "video",
+    l: $data.mode === "video" && $data.isRecording
+  }, $data.mode === "video" && $data.isRecording ? {
+    m: common_vendor.t($options.formatTime($data.recordingTime))
+  } : {}, {
+    n: !$data.mediaSource
+  }, !$data.mediaSource ? common_vendor.e({
+    o: $data.mode === "photo"
+  }, $data.mode === "photo" ? {
+    p: common_vendor.o((...args) => $options.takePhoto && $options.takePhoto(...args))
+  } : $data.mode === "video" && !$data.isRecording ? {
+    r: common_vendor.o((...args) => $options.startRecording && $options.startRecording(...args))
+  } : $data.mode === "video" && $data.isRecording ? {
+    t: common_vendor.o((...args) => $options.stopRecording && $options.stopRecording(...args))
+  } : {}, {
+    q: $data.mode === "video" && !$data.isRecording,
+    s: $data.mode === "video" && $data.isRecording
+  }) : {
+    v: common_vendor.t($data.mode === "photo" ? "拍照" : "录制"),
+    w: common_vendor.o((...args) => $options.retakeMedia && $options.retakeMedia(...args)),
+    x: common_vendor.o((...args) => $options.startInterview && $options.startInterview(...args))
   }, {
-    h: $data.isPageLoaded ? 1 : ""
+    y: $data.isPageLoaded ? 1 : ""
   });
 }
 const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render]]);

+ 1 - 1
unpackage/dist/dev/mp-weixin/pages/face-photo/face-photo.wxml

@@ -1 +1 @@
-<view class="{{['photo-container', h && 'loaded']}}"><view class="photo-header"><text class="photo-title">拍摄照片</text><text class="photo-subtitle">我们将用于身份核验,请正对摄像头</text></view><view class="photo-preview"><camera wx:if="{{a}}" device-position="front" flash="auto" class="camera" binderror="{{b}}"></camera><image wx:else class="preview-image" src="{{c}}" mode="aspectFit"></image><view class="face-outline"></view></view><button wx:if="{{d}}" class="capture-btn" bindtap="{{e}}">拍照</button><view wx:else class="btn-group"><button class="retry-btn" bindtap="{{f}}">重新拍照</button><button class="start-btn" bindtap="{{g}}">开始面试</button></view></view>
+<view class="{{['photo-container', y && 'loaded']}}"><view class="photo-header"><text class="photo-title">拍摄照片</text><text class="photo-subtitle">我们将用于身份核验,请正对摄像头</text></view><view class="mode-selector"><view class="{{['mode-option', a && 'active']}}" bindtap="{{b}}">拍照</view><view class="{{['mode-option', c && 'active']}}" bindtap="{{d}}">录制视频</view></view><view class="photo-preview"><camera wx:if="{{e}}" device-position="front" flash="auto" class="camera" mode="{{f}}" binderror="{{g}}"></camera><image wx:elif="{{h}}" class="preview-image" src="{{i}}" mode="aspectFit"></image><video wx:elif="{{j}}" class="preview-video" src="{{k}}" controls autoplay></video><view class="face-outline"></view><view wx:if="{{l}}" class="recording-indicator"><view class="recording-dot"></view><text class="recording-time">{{m}}</text></view></view><view wx:if="{{n}}" class="capture-btn-container"><button wx:if="{{o}}" class="capture-btn" bindtap="{{p}}">拍照</button><button wx:elif="{{q}}" class="capture-btn" bindtap="{{r}}">开始录制</button><button wx:elif="{{s}}" class="stop-btn" bindtap="{{t}}">停止录制</button></view><view wx:else class="btn-group"><button class="retry-btn" bindtap="{{w}}">重新{{v}}</button><button class="start-btn" bindtap="{{x}}">开始面试</button></view></view>

+ 74 - 3
unpackage/dist/dev/mp-weixin/pages/face-photo/face-photo.wxss

@@ -15,7 +15,7 @@
   display: flex;
   flex-direction: column;
   align-items: center;
-  margin-bottom: 40rpx;
+  margin-bottom: 20rpx;
 }
 .photo-title {
   font-size: 36rpx;
@@ -27,6 +27,31 @@
   font-size: 28rpx;
   color: #666;
 }
+
+/* 模式选择器样式 */
+.mode-selector {
+  display: flex;
+  justify-content: center;
+  margin-bottom: 30rpx;
+  background-color: #eaeaea;
+  border-radius: 40rpx;
+  padding: 6rpx;
+  width: 80%;
+  align-self: center;
+}
+.mode-option {
+  flex: 1;
+  text-align: center;
+  padding: 16rpx 0;
+  font-size: 28rpx;
+  color: #666;
+  border-radius: 36rpx;
+  transition: all 0.3s;
+}
+.mode-option.active {
+  background-color: #6c5ce7;
+  color: #fff;
+}
 .photo-preview {
   position: relative;
   width: 100%;
@@ -39,7 +64,7 @@
   justify-content: center;
   align-items: center;
 }
-.preview-image {
+.preview-image, .preview-video {
   width: 100%;
   height: 100%;
   object-fit: cover;
@@ -59,6 +84,44 @@
   width: 100%;
   height: 100%;
 }
+
+/* 录制指示器 */
+.recording-indicator {
+  position: absolute;
+  top: 30rpx;
+  right: 30rpx;
+  display: flex;
+  align-items: center;
+  background-color: rgba(0, 0, 0, 0.5);
+  padding: 10rpx 20rpx;
+  border-radius: 30rpx;
+}
+.recording-dot {
+  width: 20rpx;
+  height: 20rpx;
+  background-color: #ff0000;
+  border-radius: 50%;
+  margin-right: 10rpx;
+  animation: blink 1s infinite;
+}
+.recording-time {
+  color: #fff;
+  font-size: 28rpx;
+}
+@keyframes blink {
+0% { opacity: 1;
+}
+50% { opacity: 0.3;
+}
+100% { opacity: 1;
+}
+}
+.capture-btn-container {
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  margin-top: 60rpx;
+}
 .capture-btn {
   width: 100%;
   height: 90rpx;
@@ -67,7 +130,15 @@
   color: #fff;
   border-radius: 45rpx;
   font-size: 32rpx;
-  margin-top: 60rpx;
+}
+.stop-btn {
+  width: 100%;
+  height: 90rpx;
+  line-height: 90rpx;
+  background-color: #e74c3c;
+  color: #fff;
+  border-radius: 45rpx;
+  font-size: 32rpx;
 }
 .btn-group {
   display: flex;

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott