|
@@ -0,0 +1,5931 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <view class="custom-navbar" :style="{paddingTop: statusBarHeight + 'px'}">
|
|
|
|
|
+ <!-- <view class="navbar-title">我的标题</view>1 -->
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <div class="identity-verify-container" :class="{ 'page-warning': showPageWarning }">
|
|
|
|
|
+ <div class="digital-human-container">
|
|
|
|
|
+ <!-- 添加题号显示 -->
|
|
|
|
|
+ <!-- <div class="question-counter" v-if="currentVideoIndex > 0">
|
|
|
|
|
+ <span class="question-number">{{ currentVideoIndex }}/{{ totalQuestions }}</span>
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI数字人视频/图像显示区域 -->
|
|
|
|
|
+ <div class="digital-human-video">
|
|
|
|
|
+ <!-- 添加GIF图片显示区域 -->
|
|
|
|
|
+ <!-- <img v-if="showGif && gifUrl" :src="gifUrl" class="gif-player" alt="面试官GIF" /> -->
|
|
|
|
|
+ <!-- <video
|
|
|
|
|
+ v-if="showGif && gifUrl"
|
|
|
|
|
+ :src="gifUrl"
|
|
|
|
|
+ id="mpVideo"
|
|
|
|
|
+ autoplay
|
|
|
|
|
+ class="video-player"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ loop
|
|
|
|
|
+ muted
|
|
|
|
|
+ object-fit="contain"
|
|
|
|
|
+ ></video> v-if="!showGif"-->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 视频播放区域,当showGif为false时显示 -->
|
|
|
|
|
+ <video
|
|
|
|
|
+ :src="videoUrl"
|
|
|
|
|
+ id="myVideo"
|
|
|
|
|
+ ref="videoPlayer"
|
|
|
|
|
+ autoplay
|
|
|
|
|
+ playsinline
|
|
|
|
|
+ disablePictureInPicture
|
|
|
|
|
+ controlsList="nodownload nofullscreen noremoteplayback"
|
|
|
|
|
+ class="video-player"
|
|
|
|
|
+ :controls="false"
|
|
|
|
|
+ @error="handleVideoError"
|
|
|
|
|
+ @ended="handleVideoEnded"
|
|
|
|
|
+ @timeupdate="handleTimeUpdate"
|
|
|
|
|
+ @click.prevent="preventVideoControl"
|
|
|
|
|
+ @touchstart.prevent="preventVideoControl">
|
|
|
|
|
+ </video>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加透明蒙层 -->
|
|
|
|
|
+ <div class="video-overlay"></div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="answer-button-container" v-if="showAnswerButton">
|
|
|
|
|
+ <button class="answer-button" @click="handleAnswerButtonClick">
|
|
|
|
|
+ 开始回答
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 添加字幕覆盖层 -->
|
|
|
|
|
+ <div class="subtitle-overlay" v-if="currentSubtitle || (showSubtitleText && subtitleText)">
|
|
|
|
|
+ <!-- 添加题号显示 -->
|
|
|
|
|
+ <div class="subtitle-header" v-if="currentVideoIndex > 0">
|
|
|
|
|
+ <span class="question-tag">{{ currentVideoIndex }}/{{ totalQuestions }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- 显示父问题 -->
|
|
|
|
|
+ <div class="subtitle-main" v-if="!isFollowUpQuestion">{{ currentSubtitle }}</div>
|
|
|
|
|
+ <!-- 当是追问时显示父问题和追问问题 -->
|
|
|
|
|
+ <template v-else>
|
|
|
|
|
+ <div class="parent-question">{{ parentQuestion }}</div>
|
|
|
|
|
+ <div class="followup-container">
|
|
|
|
|
+ <div class="followup-content">
|
|
|
|
|
+ <span class="followup-label">追问:</span>
|
|
|
|
|
+ <span class="followup-text">{{ subtitleText || currentSubtitle }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加答题按钮 -->
|
|
|
|
|
+
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 用户摄像头视频显示区域 -->
|
|
|
|
|
+ <div class="user-camera-container" :class="{ 'camera-warning': showPageWarning }">
|
|
|
|
|
+ <!-- 在小程序环境中使用camera组件 -->
|
|
|
|
|
+ <camera v-if="useMiniProgramCameraComponent"
|
|
|
|
|
+ device-position="front"
|
|
|
|
|
+ flash="off"
|
|
|
|
|
+ class="user-camera-video"
|
|
|
|
|
+ @error="handleCameraError"
|
|
|
|
|
+ :record-audio="true"
|
|
|
|
|
+ frame-size="small"
|
|
|
|
|
+ resolution="low"
|
|
|
|
|
+ :disable-zoom="true"
|
|
|
|
|
+ :enable-camera="true"
|
|
|
|
|
+ :enable-record="true"
|
|
|
|
|
+ mode="normal">
|
|
|
|
|
+ </camera>
|
|
|
|
|
+ <!-- 在H5/App环境中使用video元素 -->
|
|
|
|
|
+ <video v-else
|
|
|
|
|
+ id="userCamera"
|
|
|
|
|
+ ref="userCameraVideo"
|
|
|
|
|
+ autoplay
|
|
|
|
|
+ playsinline
|
|
|
|
|
+ muted
|
|
|
|
|
+ class="user-camera-video"
|
|
|
|
|
+ :controls="false">
|
|
|
|
|
+ </video>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 字幕/文本覆盖区域 -->
|
|
|
|
|
+ <!-- <div class="subtitle-overlay" v-if="assistantResponse">
|
|
|
|
|
+ {{ assistantResponse }}
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 加载状态 -->
|
|
|
|
|
+ <div v-if="loading" class="loading">加载中...</div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 控制面板(可选,可以隐藏) -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 响应数据(可以设为隐藏,仅用于调试) -->
|
|
|
|
|
+ <div v-if="showDebugInfo" class="response-container">
|
|
|
|
|
+ <div v-if="assistantResponse" class="response-item">
|
|
|
|
|
+ <div class="response-content">
|
|
|
|
|
+ <span>助手回复: {{ assistantResponse }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="audioTranscript" class="response-item">
|
|
|
|
|
+ <div class="response-content">
|
|
|
|
|
+ <span>音频转写: {{ audioTranscript }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-for="(item, index) in processedResponses" :key="index" class="response-item">
|
|
|
|
|
+ <div class="response-content">
|
|
|
|
|
+ <span v-if="item.role">角色: {{ item.role }}</span>
|
|
|
|
|
+ <span v-if="item.transcript">文本: {{ item.transcript }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加停止录制按钮 -->
|
|
|
|
|
+ <div class="stop-recording-button-container" v-if="showStopRecordingButton">
|
|
|
|
|
+ <button class="stop-recording-button" @click="stopRecordingAnswer">
|
|
|
|
|
+ 完成作答
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加录制指示器 -->
|
|
|
|
|
+ <div class="recording-indicator" v-if="isRecording">
|
|
|
|
|
+ <!-- 替换原有的progress组件为自定义环形进度条 -->
|
|
|
|
|
+ <div class="circle-progress-container">
|
|
|
|
|
+ <div class="circle-progress" :style="{
|
|
|
|
|
+ background: `conic-gradient(${progressColor} ${progressPercent}%, ${progressBgColor} 0%)`
|
|
|
|
|
+ }">
|
|
|
|
|
+ <div class="circle-progress-inner">
|
|
|
|
|
+ <span class="progress-text">{{ recordingTimeDisplay }}</span><!-- -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <!-- <span class="timer-text">{{ recordingTimeDisplay || '00:00 / 05:00' }}</span> -->
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加开始回答按钮 -->
|
|
|
|
|
+ <div class="start-recording-button-container" v-if="showStartRecordingButton">
|
|
|
|
|
+ <button class="start-recording-button" @click="handleStartRecordingClick">
|
|
|
|
|
+ 开始作答
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加重试上传按钮 -->
|
|
|
|
|
+ <div class="retry-button-container" v-if="showRetryButton">
|
|
|
|
|
+ <button class="retry-button" @click="retryVideoUpload">
|
|
|
|
|
+ 重新上传
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 在模板部分添加录制时长显示 -->
|
|
|
|
|
+ <!-- <div class="recording-timer" v-if="isRecording">
|
|
|
|
|
+ <span class="timer-text">{{ recordingTimeDisplay || '00:00' }}</span>
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加上传状态指示器 -->
|
|
|
|
|
+ <!-- <div class="upload-status-indicator" v-if="showUploadStatus && uploadStatusText">
|
|
|
|
|
+ <div class="upload-status-content">
|
|
|
|
|
+ <div class="upload-status-icon" :class="{'uploading': isUploading}"></div>
|
|
|
|
|
+ <span class="upload-status-text">{{ uploadStatusText }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 修改录制时长显示 -->
|
|
|
|
|
+ <!-- <div class="recording-timer" v-if="isRecording">
|
|
|
|
|
+ <span class="timer-text">{{ recordingTimeDisplay || '00:00 / 05:00' }}</span>
|
|
|
|
|
+ <span class="remaining-time" :class="{'warning': remainingTime <= 10}">
|
|
|
|
|
+ 剩余: {{ formatTime(remainingTime) }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </div> -->
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 在模板部分添加倒计时蒙层 -->
|
|
|
|
|
+ <div class="countdown-overlay" v-if="showCountdown">
|
|
|
|
|
+ <div class="countdown-content">
|
|
|
|
|
+ <div class="countdown-number">{{ countdownValue }}</div>
|
|
|
|
|
+ <div class="countdown-text">准备开始回答</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 添加重新录制按钮 -->
|
|
|
|
|
+ <div class="rerecord-button-container" v-if="showRerecordButton">
|
|
|
|
|
+ <button class="rerecord-button" @click="handleRerecordButtonClick">
|
|
|
|
|
+ 重新作答
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script>
|
|
|
|
|
+import { apiBaseUrl, personDetectionWsUrl } from '@/common/config.js';
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'IdentityVerify',
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ followUpQuestion: '', // 追问问题
|
|
|
|
|
+ followUpAudioUrl: '', // 追问音频URL
|
|
|
|
|
+ audioContext: null, // 音频上下文
|
|
|
|
|
+ followUpQuestions: [], // 追问问题列表
|
|
|
|
|
+ isWaitingForAnswer: false, // 添加新的状态来控制是否在等待用户回答
|
|
|
|
|
+ currentFollowUpIndex: -1, // 当前追问问题索引
|
|
|
|
|
+ showSubtitleText: false, // 是否显示字幕
|
|
|
|
|
+ subtitleText: '', // 字幕文本
|
|
|
|
|
+ parentQuestion: '', // 父问题文本
|
|
|
|
|
+ isFollowUpQuestion: false, // 是否是追问问题
|
|
|
|
|
+ loading: false,
|
|
|
|
|
+ responses: [],
|
|
|
|
|
+ processedResponses: [],
|
|
|
|
|
+ assistantResponse: '',
|
|
|
|
|
+ audioTranscript: '',
|
|
|
|
|
+ videoPlaying: false,
|
|
|
|
|
+ showDebugInfo: false, // 设置为true可以显示调试信息
|
|
|
|
|
+ videoUrl: '', // 修改为空字符串,将通过API获取
|
|
|
|
|
+ showReplayButton: false,
|
|
|
|
|
+ cameraStream: null, // 存储摄像头流
|
|
|
|
|
+ cameraError: null, // 存储摄像头错误信息
|
|
|
|
|
+ useMiniProgramCameraComponent: false, // 添加小程序相机组件标志
|
|
|
|
|
+ cameraContext: null, // 添加相机上下文
|
|
|
|
|
+ currentSubtitle: '',
|
|
|
|
|
+ currentTranslation: '', // 添加翻译文本
|
|
|
|
|
+ subtitles: [], // 修改为空数组,将通过API获取
|
|
|
|
|
+ // 移除硬编码的字幕数组
|
|
|
|
|
+ // secondVideoSubtitles: [...],
|
|
|
|
|
+ // thirdVideoSubtitles: [...],
|
|
|
|
|
+ // 等等...
|
|
|
|
|
+ showAnswerButton: false, // 控制答题按钮显示
|
|
|
|
|
+ currentVideoIndex: 0, // 当前播放的视频索引
|
|
|
|
|
+ videoList: [], // 修改为空数组,将通过API获取
|
|
|
|
|
+ // 其他属性保持不变...
|
|
|
|
|
+ questions: [], // 添加新属性存储API返回的问题数据
|
|
|
|
|
+ introVideoUrl: (() => {
|
|
|
|
|
+ const DEFAULT_VIDEO_URL = 'https://data.qicai321.com/minlong/ee4d9cce-c3d5-4350-8c6e-684283827897.mp4';
|
|
|
|
|
+ try {
|
|
|
|
|
+ const configStr = uni.getStorageSync('configData');
|
|
|
|
|
+ if (!configStr) return DEFAULT_VIDEO_URL;
|
|
|
|
|
+
|
|
|
|
|
+ const config = JSON.parse(configStr);
|
|
|
|
|
+ return config.digital_human_opening_video_url || DEFAULT_VIDEO_URL;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.warn('解析配置数据失败:', error);
|
|
|
|
|
+ return DEFAULT_VIDEO_URL;
|
|
|
|
|
+ }
|
|
|
|
|
+ })(),//'https://data.qicai321.com/minlong/ee4d9cce-c3d5-4350-8c6e-684283827897.mp4', // 保留介绍视频
|
|
|
|
|
+ isRecording: false,
|
|
|
|
|
+ recordingTimer: null,
|
|
|
|
|
+ showStopRecordingButton: false,
|
|
|
|
|
+ mediaRecorder: null,
|
|
|
|
|
+ recordedChunks: [],
|
|
|
|
|
+ recorder: null,
|
|
|
|
|
+ lastUploadedVideoUrl: '',
|
|
|
|
|
+ showStartRecordingButton: false,
|
|
|
|
|
+ showRetryButton: false, // 控制重试按钮显示
|
|
|
|
|
+ lastVideoToRetry: null, // 存储上次失败的视频URL,用于重试
|
|
|
|
|
+ recordingStartTime: null, // 录制开始时间
|
|
|
|
|
+ recordingTimerCount: 0, // 录制计时器计数
|
|
|
|
|
+ recordingTimeDisplay: '00:00', // 格式化的录制时间显示
|
|
|
|
|
+
|
|
|
|
|
+ // 添加上传队列相关数据
|
|
|
|
|
+ uploadQueue: [], // 存储待上传的视频
|
|
|
|
|
+ backgroundUploadQueue: [], // 存储后台上传的视频
|
|
|
|
|
+ isUploading: false, // 标记是否正在上传
|
|
|
|
|
+ uploadProgress: {}, // 存储每个视频的上传进度
|
|
|
|
|
+ uploadStatus: {}, // 存储每个视频的上传状态
|
|
|
|
|
+ showUploadStatus: false, // 是否显示上传状态指示器
|
|
|
|
|
+ uploadStatusText: '', // 上传状态文本
|
|
|
|
|
+ mediaRecorderTimeout: null, // 用于存储MediaRecorder的超时机制
|
|
|
|
|
+ maxRecordingTime: 300, // 最大录制时间(秒)- 从60秒改为300秒(5分钟)
|
|
|
|
|
+ remainingTime: 300, // 剩余录制时间(秒)- 从60秒改为300秒
|
|
|
|
|
+ countdownValue: 10, // 倒计时数值
|
|
|
|
|
+ showCountdown: false, // 是否显示倒计时蒙层
|
|
|
|
|
+ countdownTimer: null,
|
|
|
|
|
+ showGif: false, // 控制是否显示GIF
|
|
|
|
|
+ gifUrl: '', // GIF图片的URL
|
|
|
|
|
+ globalSocketTask: null, // 添加全局 WebSocket 连接对象
|
|
|
|
|
+ lowScoreVideoUrl: (() => {
|
|
|
|
|
+ const DEFAULT_VIDEO_URL = 'https://data.qicai321.com/minlong/latentsync/0530e7f5-1957-422d-8f34-ba4a92608081_result.mp4';
|
|
|
|
|
+ try {
|
|
|
|
|
+ const configStr = uni.getStorageSync('configData');
|
|
|
|
|
+ if (!configStr) return DEFAULT_VIDEO_URL;
|
|
|
|
|
+
|
|
|
|
|
+ const config = JSON.parse(configStr);
|
|
|
|
|
+ return config.digital_human?.middle_video_url || DEFAULT_VIDEO_URL;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.warn('解析配置数据失败:', error);
|
|
|
|
|
+ return DEFAULT_VIDEO_URL;
|
|
|
|
|
+ }
|
|
|
|
|
+ })(),//'https://data.qicai321.com/minlong/latentsync/0530e7f5-1957-422d-8f34-ba4a92608081_result.mp4', // 低分提示视频URL
|
|
|
|
|
+ showRerecordButton: false, // 控制重新录制按钮显示
|
|
|
|
|
+ isPlayingLowScoreVideo: false, // 标记是否正在播放低分提示视频
|
|
|
|
|
+ lowScoreVideoSubtitles: [
|
|
|
|
|
+ {
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 10, // 假设视频长度为10秒,可根据实际情况调整
|
|
|
|
|
+ text: '我未听清楚您在说什么,请您再说一遍!'
|
|
|
|
|
+ }
|
|
|
|
|
+ ], // 用于存储低分提示视频的字幕
|
|
|
|
|
+ retryCount: 0, // 添加重试次数计数器
|
|
|
|
|
+ maxRetryAttempts: 2, // 最大重试次数限制
|
|
|
|
|
+ needPlayLowScoreVideo: false, // 是否需要播放低分视频
|
|
|
|
|
+ finalRecordingDuration: 0, // 最终录制时长
|
|
|
|
|
+ historyTime: 0, // 添加历史时间记录
|
|
|
|
|
+ followUpQuestions: [], // 存储追问问题数据
|
|
|
|
|
+ currentParentQuestionId: null, // 当前父问题ID
|
|
|
|
|
+ hasPlayedFollowUp: {}, // 记录已播放的追问,格式: {questionId: true}
|
|
|
|
|
+ isFollowUpQuestion: false, // 标记当前是否正在播放追问问题
|
|
|
|
|
+ currentFollowUpQuestion: null, // 添加当前追问问题的完整信息
|
|
|
|
|
+ lastQuestionWasFollowUp: false,
|
|
|
|
|
+ lastFollowUpQuestionId: null,
|
|
|
|
|
+ // 追问次数控制
|
|
|
|
|
+ defaultFollowUpLimit: 1, // 默认每个主题目最多2次追问
|
|
|
|
|
+ followUpLimitMap: {}, // { parentJobPositionQuestionId: limit }
|
|
|
|
|
+ followUpAskedCountMap: {}, // { parentJobPositionQuestionId: askedCount }
|
|
|
|
|
+ followUpRequestInFlight: {}, // { parentJobPositionQuestionId: boolean }
|
|
|
|
|
+ lastUpdateTime: Date.now(), // 添加最后更新时间戳
|
|
|
|
|
+ subtitleMap: {}, // 用于存储字幕和翻译的映射
|
|
|
|
|
+ progressPercent: 0, // 录制进度百分比
|
|
|
|
|
+ progressColor: '#05dc8b', // 进度条颜色
|
|
|
|
|
+ progressBgColor: 'rgba(0, 0, 0,0.3)', // 进度条背景色
|
|
|
|
|
+ parentQuestion: '', // 添加父问题存储
|
|
|
|
|
+ screenCaptureCount: 0, // 添加截屏次数记录
|
|
|
|
|
+ personDetectionSocket: null, // WebSocket对象
|
|
|
|
|
+ personDetectionInterval: null, // 定时器对象
|
|
|
|
|
+ showCameraWarning: false, // 添加新的数据属性
|
|
|
|
|
+ showPageWarning: false, // 添加新的数据属性
|
|
|
|
|
+ followUpQuestion: '', // 追问问题
|
|
|
|
|
+ followUpAudioUrl: '', // 追问音频URL
|
|
|
|
|
+ audioContext: null, // 音频上下文
|
|
|
|
|
+ followUpQuestions: [], // 追问问题列表
|
|
|
|
|
+ currentFollowUpIndex: -1, // 当前追问问题索引
|
|
|
|
|
+ showSubtitleText: false, // 是否显示字幕
|
|
|
|
|
+ subtitleText: '', // 字幕文本
|
|
|
|
|
+ isAudioPlaying: false,
|
|
|
|
|
+ isWaitingForAnswer: false, // 添加新的状态来控制是否在等待用户回答
|
|
|
|
|
+ currentRecordingDuration: 0, // 添加当前录制时长
|
|
|
|
|
+ parentJobPositionQuestionId: null, // 添加父问题的job_position_question_id
|
|
|
|
|
+ isFollowUpMode: false, // 是否处于追问模式
|
|
|
|
|
+ mainQuestionIndex: 0, // 当前主问题的索引
|
|
|
|
|
+ isVideoSwitching: false, // 添加视频切换状态锁
|
|
|
|
|
+ originalQuestionSubtitle: null, // 保存原始字幕信息
|
|
|
|
|
+ isThinking: false, // 面试官思考中状态
|
|
|
|
|
+ thinkingTimer: null, // 思考计时器
|
|
|
|
|
+ questionRetryMap: {}, // 用于跟踪每个问题的重试次数
|
|
|
|
|
+ maxQuestionRetries: 2, // 每个问题最大重试次数
|
|
|
|
|
+ statusBarHeight: 0
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ onLoad() {
|
|
|
|
|
+ // 获取状态栏高度
|
|
|
|
|
+ this.statusBarHeight = uni.getSystemInfoSync().statusBarHeight || 0
|
|
|
|
|
+ },
|
|
|
|
|
+ mounted() {
|
|
|
|
|
+ this.fetchQuestions();
|
|
|
|
|
+ //this.fetchFollowUpQuestions();
|
|
|
|
|
+ this.checkAudioPermission();
|
|
|
|
|
+ this.initCamera();
|
|
|
|
|
+ this.checkIOSCameraRecordPermission();
|
|
|
|
|
+ this.checkAndFixRenderingIssues();
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (this.cameraStream && !this.useMiniProgramCameraComponent) {
|
|
|
|
|
+ this.testAudioInput();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 3e3);
|
|
|
|
|
+ this.historyTime = 0;
|
|
|
|
|
+ uni.setKeepScreenOn({
|
|
|
|
|
+ keepScreenOn: true
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 添加截屏监听
|
|
|
|
|
+ uni.onUserCaptureScreen(() => {
|
|
|
|
|
+ console.log('User captured screen');
|
|
|
|
|
+ this.screenCaptureCount++;
|
|
|
|
|
+
|
|
|
|
|
+ if (this.screenCaptureCount === 1) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: 'Warning',
|
|
|
|
|
+ content: '检测到屏幕截图。如果你再次捕捉屏幕,你的面试将无效.',
|
|
|
|
|
+ showCancel: false,
|
|
|
|
|
+ confirmText: 'OK'
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (this.screenCaptureCount >= 2) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: 'Interview Invalid',
|
|
|
|
|
+ content: '由于多次屏幕截图,您的面试已无效。请联系招聘人员寻求帮助.',
|
|
|
|
|
+ showCancel: false,
|
|
|
|
|
+ confirmText: 'OK',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ this.invalidateInterview();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith('mp-');
|
|
|
|
|
+ console.log('isMiniProgram', isMiniProgram);
|
|
|
|
|
+ if (isMiniProgram) {
|
|
|
|
|
+ this.initPersonDetectionWebSocket();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ beforeDestroy() {
|
|
|
|
|
+ // 移除截屏监听
|
|
|
|
|
+ uni.offUserCaptureScreen();
|
|
|
|
|
+ this.cleanupPersonDetectionWebSocket();
|
|
|
|
|
+ },
|
|
|
|
|
+ methods: {
|
|
|
|
|
+ // ===== 追问次数控制:工具方法 =====
|
|
|
|
|
+ getFollowUpLimit(parentId) {
|
|
|
|
|
+ if (!parentId) return this.defaultFollowUpLimit;
|
|
|
|
|
+ return this.followUpLimitMap[parentId] || this.defaultFollowUpLimit;
|
|
|
|
|
+ },
|
|
|
|
|
+ setFollowUpLimit(parentId, limit) {
|
|
|
|
|
+ if (!parentId) return;
|
|
|
|
|
+ const parsed = parseInt(limit);
|
|
|
|
|
+ this.followUpLimitMap[parentId] = Number.isFinite(parsed) && parsed > 0 ? parsed : this.defaultFollowUpLimit;
|
|
|
|
|
+ },
|
|
|
|
|
+ resetFollowUpAskedCount(parentId) {
|
|
|
|
|
+ if (!parentId) return;
|
|
|
|
|
+ this.followUpAskedCountMap[parentId] = 0;
|
|
|
|
|
+ },
|
|
|
|
|
+ incrementFollowUpCount(parentId) {
|
|
|
|
|
+ if (!parentId) return;
|
|
|
|
|
+ const current = this.followUpAskedCountMap[parentId] || 0;
|
|
|
|
|
+ this.followUpAskedCountMap[parentId] = current + 1;
|
|
|
|
|
+ },
|
|
|
|
|
+ canAskMoreFollowUps(parentId) {
|
|
|
|
|
+ if (!parentId) return false;
|
|
|
|
|
+ const asked = this.followUpAskedCountMap[parentId] || 0;
|
|
|
|
|
+ const limit = this.getFollowUpLimit(parentId);
|
|
|
|
|
+ return asked < limit;
|
|
|
|
|
+ },
|
|
|
|
|
+ async maybeRequestNextFollowUp(parentId) {
|
|
|
|
|
+ if (!parentId) {
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (this.canAskMoreFollowUps(parentId)) {
|
|
|
|
|
+ // 避免重复触发并发请求
|
|
|
|
|
+ if (this.followUpRequestInFlight[parentId]) {
|
|
|
|
|
+ console.log('已有追问请求进行中,跳过重复触发');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 交由 callInterviewInteraction 统一处理并发标记
|
|
|
|
|
+ const ok = await this.callInterviewInteraction(parentId);
|
|
|
|
|
+ if (!ok) {
|
|
|
|
|
+ console.log('未获取到下一条追问或调用失败,进入下一题');
|
|
|
|
|
+ this.isFollowUpMode = false;
|
|
|
|
|
+ this.isFollowUpQuestion = false;
|
|
|
|
|
+ this.currentFollowUpQuestion = null;
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 已达上限,进入下一题
|
|
|
|
|
+ this.isFollowUpMode = false;
|
|
|
|
|
+ this.isFollowUpQuestion = false;
|
|
|
|
|
+ this.currentFollowUpQuestion = null;
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 处理音频播放完成
|
|
|
|
|
+ handleAudioEnd() {
|
|
|
|
|
+ console.log('音频播放完成');
|
|
|
|
|
+ // 设置等待回答状态
|
|
|
|
|
+ this.isWaitingForAnswer = true;
|
|
|
|
|
+ // 显示开始作答按钮
|
|
|
|
|
+ this.showStartRecordingButton = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟一下再开始倒计时,避免重复触发
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ // 开始倒计时
|
|
|
|
|
+ this.startCountdown();
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 开始倒计时
|
|
|
|
|
+ startCountdown() {
|
|
|
|
|
+ this.showCountdown = true;
|
|
|
|
|
+ this.countdownValue = 5; // 重置倒计时值
|
|
|
|
|
+
|
|
|
|
|
+ this.countdownTimer = setInterval(() => {
|
|
|
|
|
+ this.countdownValue--;
|
|
|
|
|
+
|
|
|
|
|
+ if (this.countdownValue <= 0) {
|
|
|
|
|
+ // 倒计时结束,清除定时器
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏开始作答按钮
|
|
|
|
|
+ this.showStartRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 开始录制
|
|
|
|
|
+ this.startRecordingAnswer();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 清除倒计时
|
|
|
|
|
+ clearCountdown() {
|
|
|
|
|
+ if (this.countdownTimer) {
|
|
|
|
|
+ clearInterval(this.countdownTimer);
|
|
|
|
|
+ this.countdownTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.showCountdown = false;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理追问问题
|
|
|
|
|
+ async handleFollowUpQuestion(questionData) {
|
|
|
|
|
+ console.log('处理追问:', questionData);
|
|
|
|
|
+ this.enterFollowUpMode();
|
|
|
|
|
+ console.log('处理追问问题数据:', questionData);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查数据完整性
|
|
|
|
|
+ if (!questionData || !questionData.follow_up_voice_url || !questionData.follow_up_question) {
|
|
|
|
|
+ console.error('追问数据不完整:', questionData);
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '获取追问数据失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存父问题
|
|
|
|
|
+ this.parentQuestion = questionData.original_question || '';
|
|
|
|
|
+
|
|
|
|
|
+ // 确保音频URL正确
|
|
|
|
|
+ let audioUrl = '';
|
|
|
|
|
+ if (questionData.follow_up_voice_url.direct_url) {
|
|
|
|
|
+ audioUrl = questionData.follow_up_voice_url.direct_url;
|
|
|
|
|
+ } else if (questionData.follow_up_voice_url.file_url) {
|
|
|
|
|
+ audioUrl = questionData.follow_up_voice_url.file_url;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确保音频URL是完整的
|
|
|
|
|
+ if (!audioUrl.startsWith('http')) {
|
|
|
|
|
+ audioUrl = apiBaseUrl + audioUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('处理后的音频URL:', audioUrl);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建追问问题对象
|
|
|
|
|
+ const followUpQuestionObj = {
|
|
|
|
|
+ id: Date.now(),
|
|
|
|
|
+ question: questionData.follow_up_question.trim(), // 去除可能的空白字符
|
|
|
|
|
+ audioUrl: audioUrl,
|
|
|
|
|
+ originalQuestion: questionData.original_question,
|
|
|
|
|
+ sessionId: questionData.session_id
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ console.log('创建的追问问题对象:', followUpQuestionObj);
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到追问问题列表
|
|
|
|
|
+ this.followUpQuestions.push(followUpQuestionObj);
|
|
|
|
|
+ this.currentFollowUpIndex = this.followUpQuestions.length - 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新当前追问问题
|
|
|
|
|
+ this.followUpQuestion = followUpQuestionObj.question;
|
|
|
|
|
+ this.followUpAudioUrl = followUpQuestionObj.audioUrl;
|
|
|
|
|
+
|
|
|
|
|
+ // 标记当前是追问问题
|
|
|
|
|
+ this.isFollowUpQuestion = true;
|
|
|
|
|
+ this.currentFollowUpQuestion = followUpQuestionObj;
|
|
|
|
|
+
|
|
|
|
|
+ // 显示字幕
|
|
|
|
|
+ this.showSubtitle(followUpQuestionObj.question, true);
|
|
|
|
|
+
|
|
|
|
|
+ // 等待音频播放完成
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.playFollowUpAudio();
|
|
|
|
|
+ console.log('音频播放完成,准备录制回答');
|
|
|
|
|
+ // 开始准备录制回答
|
|
|
|
|
+ this.prepareToAnswer(true, followUpQuestionObj);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('音频播放失败:', error);
|
|
|
|
|
+ // 即使音频播放失败,也继续准备录制回答
|
|
|
|
|
+ this.prepareToAnswer(true, followUpQuestionObj);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 准备回答问题
|
|
|
|
|
+ prepareToAnswer(isFollowUp = false, questionData = null) {
|
|
|
|
|
+ console.log('准备回答问题:', { isFollowUp, questionData });
|
|
|
|
|
+
|
|
|
|
|
+ // 重置录制状态
|
|
|
|
|
+ this.isRecording = false;
|
|
|
|
|
+ this.recordedTime = 0;
|
|
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置当前问题状态
|
|
|
|
|
+ if (isFollowUp && questionData) {
|
|
|
|
|
+ this.currentFollowUpQuestion = questionData;
|
|
|
|
|
+ this.isFollowUpQuestion = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置等待回答状态
|
|
|
|
|
+ this.isWaitingForAnswer = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 显示开始回答按钮和倒计时
|
|
|
|
|
+ this.handleAudioEnd();
|
|
|
|
|
+ },
|
|
|
|
|
+ // 调用面试互动接口
|
|
|
|
|
+ async callInterviewInteraction(questionId, retryCount = 0, maxRetries = 3) {
|
|
|
|
|
+ // 防重入:同一父问题并发保护
|
|
|
|
|
+ if (questionId && this.followUpRequestInFlight[questionId]) {
|
|
|
|
|
+ console.log('追问请求已在进行中,跳过本次调用');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (questionId) {
|
|
|
|
|
+ this.followUpRequestInFlight[questionId] = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ const userInfo = JSON.parse(uni.getStorageSync('userInfo'));
|
|
|
|
|
+ const appId = uni.getStorageSync('appId');
|
|
|
|
|
+ const positionConfigId = JSON.parse(uni.getStorageSync('configData')).id;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 显示思考中loading
|
|
|
|
|
+ this.showThinkingLoading();
|
|
|
|
|
+ console.log('开始调用面试互动接口', { questionId, appId });
|
|
|
|
|
+ const res = await uni.request({
|
|
|
|
|
+ url: `${apiBaseUrl}/api/voice_interview_interaction/`,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ tenant_id: userInfo.tenant_id || 1,
|
|
|
|
|
+ question_id: questionId,
|
|
|
|
|
+ position_config_id: positionConfigId,
|
|
|
|
|
+ application_id: appId
|
|
|
|
|
+ },
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'content-type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ console.log('面试互动接口返回数据:', res);
|
|
|
|
|
+
|
|
|
|
|
+ // 处理4000状态码(视频未转写完成)的情况
|
|
|
|
|
+ if (res.statusCode === 400) {
|
|
|
|
|
+ if (retryCount < maxRetries) {
|
|
|
|
|
+ console.log(`视频转写未完成,${retryCount + 1}次重试中...`);
|
|
|
|
|
+ // 等待3秒后重试
|
|
|
|
|
+ await new Promise(resolve => setTimeout(resolve, 3000));
|
|
|
|
|
+ return this.callInterviewInteraction(questionId, retryCount + 1, maxRetries);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('达到最大重试次数,视频转写仍未完成');
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '视频转写处理中,请稍后再试',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (res.data.success) {
|
|
|
|
|
+ // 成功获取到一条追问,计数 +1(表示“已请求”这条追问)
|
|
|
|
|
+ if (questionId) {
|
|
|
|
|
+ this.incrementFollowUpCount(questionId);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 处理本次追问(即便已达到上限,也应该播放本次追问)
|
|
|
|
|
+ await this.handleFollowUpQuestion(res.data);
|
|
|
|
|
+ this.hideThinkingLoading();
|
|
|
|
|
+ return true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('面试互动接口返回错误:', res.data);
|
|
|
|
|
+ this.hideThinkingLoading();
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('调用面试互动接口失败:', error);
|
|
|
|
|
+ this.hideThinkingLoading();
|
|
|
|
|
+ return false;
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ if (questionId) {
|
|
|
|
|
+ this.followUpRequestInFlight[questionId] = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 播放追问音频
|
|
|
|
|
+ async playFollowUpAudio() {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ console.log('开始播放追问音频, URL:', this.followUpAudioUrl);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查音频URL
|
|
|
|
|
+ if (!this.followUpAudioUrl) {
|
|
|
|
|
+ console.error('没有音频URL');
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '音频URL无效',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ }); */
|
|
|
|
|
+ reject(new Error('没有音频URL'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 显示加载提示
|
|
|
|
|
+ uni.showLoading({
|
|
|
|
|
+ title: '思考中...'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 停止并销毁之前的音频实例
|
|
|
|
|
+ this.stopAndDestroyAudio();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 创建新的音频上下文
|
|
|
|
|
+ const innerAudioContext = uni.createInnerAudioContext();
|
|
|
|
|
+ this.audioContext = innerAudioContext;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置音频属性
|
|
|
|
|
+ innerAudioContext.autoplay = true;
|
|
|
|
|
+ innerAudioContext.obeyMuteSwitch = false;
|
|
|
|
|
+ innerAudioContext.volume = 1.0;
|
|
|
|
|
+
|
|
|
|
|
+ // 监听音频加载状态
|
|
|
|
|
+ innerAudioContext.onCanplay(() => {
|
|
|
|
|
+ console.log('音频可以播放');
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ this.isAudioPlaying = true;
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '正在播放追问',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ }); */
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听音频播放完成
|
|
|
|
|
+ innerAudioContext.onEnded(() => {
|
|
|
|
|
+ console.log('追问音频播放完成');
|
|
|
|
|
+ this.isAudioPlaying = false;
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ // 延迟清理资源,确保不会影响播放完成的回调
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.stopAndDestroyAudio();
|
|
|
|
|
+ // 音频播放完成后,自动准备录制回答
|
|
|
|
|
+ this.prepareToAnswer(true, this.currentFollowUpQuestion);
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听音频播放错误
|
|
|
|
|
+ innerAudioContext.onError((res) => {
|
|
|
|
|
+ console.error('音频播放错误:', res);
|
|
|
|
|
+ this.isAudioPlaying = false;
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '音频播放失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ }); */
|
|
|
|
|
+ reject(res);
|
|
|
|
|
+ this.stopAndDestroyAudio();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 设置音频源
|
|
|
|
|
+ console.log('设置音频源:', this.followUpAudioUrl);
|
|
|
|
|
+ innerAudioContext.src = this.followUpAudioUrl;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('创建或播放音频失败:', error);
|
|
|
|
|
+ this.isAudioPlaying = false;
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '音频播放失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ }); */
|
|
|
|
|
+ this.stopAndDestroyAudio();
|
|
|
|
|
+ reject(error);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 停止并销毁音频
|
|
|
|
|
+ stopAndDestroyAudio() {
|
|
|
|
|
+ if (!this.audioContext) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const ctx = this.audioContext;
|
|
|
|
|
+ this.audioContext = null; // 先清除引用
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.isAudioPlaying) {
|
|
|
|
|
+ ctx.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('停止音频播放失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ctx.destroy();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('销毁音频实例失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.isAudioPlaying = false;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 绑定音频事件
|
|
|
|
|
+ bindAudioEvents(audioContext, resolve, reject) {
|
|
|
|
|
+ // 监听音频加载状态
|
|
|
|
|
+ audioContext.onCanplay(() => {
|
|
|
|
|
+ console.log('音频可以播放');
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '正在播放追问',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ }); */
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听播放进度
|
|
|
|
|
+ audioContext.onTimeUpdate(() => {
|
|
|
|
|
+ console.log('音频播放进度:', audioContext.currentTime);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听音频播放完成
|
|
|
|
|
+ audioContext.onEnded(() => {
|
|
|
|
|
+ console.log('追问音频播放完成');
|
|
|
|
|
+ this.cleanupAudioContext();
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听音频播放错误
|
|
|
|
|
+ audioContext.onError((res) => {
|
|
|
|
|
+ console.error('音频播放错误:', res);
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '音频播放失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ }); */
|
|
|
|
|
+ this.cleanupAudioContext();
|
|
|
|
|
+ reject(res);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改准备回答方法
|
|
|
|
|
+ prepareToAnswer(isFollowUp = false, questionData = null) {
|
|
|
|
|
+ // 重置录制状态
|
|
|
|
|
+ this.isRecording = false;
|
|
|
|
|
+ this.recordedTime = 0;
|
|
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置当前问题状态
|
|
|
|
|
+ if (isFollowUp && questionData) {
|
|
|
|
|
+ this.currentFollowUpQuestion = questionData;
|
|
|
|
|
+ this.isFollowUpQuestion = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置等待回答状态
|
|
|
|
|
+ this.isWaitingForAnswer = true;
|
|
|
|
|
+ // 显示开始回答按钮和倒计时
|
|
|
|
|
+ this.handleAudioEnd();
|
|
|
|
|
+ },
|
|
|
|
|
+ // 初始化相机
|
|
|
|
|
+ async initCamera() {
|
|
|
|
|
+ // 检查平台
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+
|
|
|
|
|
+ // 判断是否在小程序环境中
|
|
|
|
|
+ const isMiniProgram = systemInfo.uniPlatform === 'mp-weixin' ||
|
|
|
|
|
+ systemInfo.uniPlatform === 'mp-alipay' ||
|
|
|
|
|
+ systemInfo.uniPlatform === 'mp-baidu';
|
|
|
|
|
+
|
|
|
|
|
+ if (isMiniProgram) {
|
|
|
|
|
+ // 小程序环境使用小程序的相机API
|
|
|
|
|
+ this.useMiniProgramCameraComponent = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建相机上下文
|
|
|
|
|
+ this.cameraContext = uni.createCameraContext();
|
|
|
|
|
+
|
|
|
|
|
+ // 确保已获取录音和相机权限
|
|
|
|
|
+ uni.getSetting({
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ // 检查录音权限
|
|
|
|
|
+ if (!res.authSetting['scope.record']) {
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.record',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('录音权限已获取');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('录音权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('录音');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查相机权限
|
|
|
|
|
+ if (!res.authSetting['scope.camera']) {
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.camera',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('相机权限已获取');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('相机权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('相机');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 在iOS上,可能需要额外的权限检查
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ if (systemInfo.platform === 'ios') {
|
|
|
|
|
+ // 在iOS上,不要使用无效的权限范围
|
|
|
|
|
+ // 移除以下代码块
|
|
|
|
|
+ /*
|
|
|
|
|
+ if (!res.authSetting['scope.camera.record']) {
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.camera.record',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('相机录制权限已获取');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('相机录制权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('相机录制');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+ // 确保同时获取了相机和录音权限
|
|
|
|
|
+ if (!res.authSetting['scope.camera'] || !res.authSetting['scope.record']) {
|
|
|
|
|
+ console.log('iOS需要同时获取相机和录音权限');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // H5/App环境使用浏览器的API
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 请求摄像头和麦克风权限并获取媒体流,添加更多约束
|
|
|
|
|
+ const constraints = {
|
|
|
|
|
+ audio: {
|
|
|
|
|
+ echoCancellation: true,
|
|
|
|
|
+ noiseSuppression: true,
|
|
|
|
|
+ autoGainControl: true
|
|
|
|
|
+ },
|
|
|
|
|
+ video: {
|
|
|
|
|
+ width: { ideal: 640, max: 1280 }, // 控制视频宽度
|
|
|
|
|
+ height: { ideal: 480, max: 720 }, // 控制视频高度
|
|
|
|
|
+ frameRate: { ideal: 15, max: 24 }, // 控制帧率
|
|
|
|
|
+ facingMode: 'user'
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存流以便后续使用
|
|
|
|
|
+ this.cameraStream = stream;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查音频轨道
|
|
|
|
|
+ const audioTracks = stream.getAudioTracks();
|
|
|
|
|
+ console.log('音频轨道数量:', audioTracks.length);
|
|
|
|
|
+ if (audioTracks.length > 0) {
|
|
|
|
|
+ console.log('音频轨道已获取:', audioTracks[0].label);
|
|
|
|
|
+ // 确保音频轨道已启用
|
|
|
|
|
+ audioTracks[0].enabled = true;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('未检测到音频轨道,尝试单独获取音频');
|
|
|
|
|
+ this.tryGetAudioOnly();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 将流设置到视频元素
|
|
|
|
|
+ const videoElement = this.$refs.userCameraVideo;
|
|
|
|
|
+ if (videoElement) {
|
|
|
|
|
+ videoElement.srcObject = stream;
|
|
|
|
|
+ videoElement.muted = true; // 避免回声,但仍然录制声音
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取摄像头失败:', error);
|
|
|
|
|
+ this.cameraError = error.message || '无法访问摄像头';
|
|
|
|
|
+
|
|
|
|
|
+ // 显示错误提示
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '无法访问摄像头,请检查权限设置',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 停止用户摄像头
|
|
|
|
|
+ stopUserCamera() {
|
|
|
|
|
+ if (this.cameraStream) {
|
|
|
|
|
+ // 停止所有轨道
|
|
|
|
|
+ this.cameraStream.getTracks().forEach(track => {
|
|
|
|
|
+ track.stop();
|
|
|
|
|
+ });
|
|
|
|
|
+ this.cameraStream = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async fetchData() {
|
|
|
|
|
+ this.loading = true
|
|
|
|
|
+ this.assistantResponse = ''
|
|
|
|
|
+ this.audioTranscript = ''
|
|
|
|
|
+ this.processedResponses = []
|
|
|
|
|
+
|
|
|
|
|
+ // 显示加载提示
|
|
|
|
|
+ uni.showLoading({
|
|
|
|
|
+ title: '面试官思考中...',
|
|
|
|
|
+ mask: true // 添加遮罩,防止用户触摸界面
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 使用uni.request代替fetch
|
|
|
|
|
+ const requestTask = uni.request({
|
|
|
|
|
+ url: 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions',
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
|
|
+ 'Authorization': 'Bearer sk-9e1ec73a7d97493b8613c63f06b6110c'
|
|
|
|
|
+ },
|
|
|
|
|
+ data: {
|
|
|
|
|
+ "model": "qwen-omni-turbo",
|
|
|
|
|
+ "messages": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "role": "user",
|
|
|
|
|
+ "content": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "type": "input_audio",
|
|
|
|
|
+ "input_audio": {
|
|
|
|
|
+ "data": "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250211/tixcef/cherry.wav",
|
|
|
|
|
+ "format": "wav"
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ "type": "text",
|
|
|
|
|
+ "text": "这段音频在说什么"
|
|
|
|
|
+ }
|
|
|
|
|
+ ]
|
|
|
|
|
+ }
|
|
|
|
|
+ ],
|
|
|
|
|
+ "stream":true,
|
|
|
|
|
+ "stream_options":{
|
|
|
|
|
+ "include_usage":true
|
|
|
|
|
+ },
|
|
|
|
|
+ "modalities":["text","audio"],
|
|
|
|
|
+ "audio":{"voice":"Cherry","format":"wav"}
|
|
|
|
|
+ },
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ console.log('请求成功,响应数据:', res.data);
|
|
|
|
|
+ // 检查响应数据是否包含多个JSON对象
|
|
|
|
|
+ if (typeof res.data === 'string' && res.data.includes('data: {')) {
|
|
|
|
|
+ // 处理包含多个JSON对象的情况
|
|
|
|
|
+ const chunks = res.data.split('data: ').filter(chunk => chunk.trim() !== '');
|
|
|
|
|
+ chunks.forEach(chunk => {
|
|
|
|
|
+ this.handleStreamResponse(chunk);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 处理单个响应对象的情况
|
|
|
|
|
+ this.handleStreamResponse(res.data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 模拟获取到数字人视频并播放
|
|
|
|
|
+ this.playDigitalHumanVideo();
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('请求失败:', err);
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请求失败,请重试',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ complete: () => {
|
|
|
|
|
+ this.loading = false;
|
|
|
|
|
+ // 隐藏加载提示
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取数据失败:', error);
|
|
|
|
|
+ this.loading = false;
|
|
|
|
|
+ // 隐藏加载提示
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '系统错误,请重试',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ handleStreamResponse(data) {
|
|
|
|
|
+ // 处理流式响应数据
|
|
|
|
|
+ if (typeof data === 'string') {
|
|
|
|
|
+ // 处理字符串格式的响应
|
|
|
|
|
+ if (data === '[DONE]') return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 移除可能存在的换行符和多余空格
|
|
|
|
|
+ const cleanData = data.trim();
|
|
|
|
|
+ // 检查是否是有效的JSON字符串
|
|
|
|
|
+ if (cleanData.startsWith('{') && cleanData.endsWith('}')) {
|
|
|
|
|
+ const jsonData = JSON.parse(cleanData);
|
|
|
|
|
+ this.processStreamChunk(jsonData);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('解析JSON失败:', e, '原始数据:', data);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 处理对象格式的响应
|
|
|
|
|
+ this.processStreamChunk(data);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ processStreamChunk(chunk) {
|
|
|
|
|
+ if (chunk.choices && chunk.choices.length > 0) {
|
|
|
|
|
+ const choice = chunk.choices[0];
|
|
|
|
|
+
|
|
|
|
|
+ // 处理助手回复内容
|
|
|
|
|
+ if (choice.delta && choice.delta.content) {
|
|
|
|
|
+ this.assistantResponse += choice.delta.content;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理音频转写内容
|
|
|
|
|
+ if (choice.delta && choice.delta.audio && choice.delta.audio.transcript) {
|
|
|
|
|
+ this.audioTranscript += choice.delta.audio.transcript;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理角色和音频转写
|
|
|
|
|
+ if (choice.delta) {
|
|
|
|
|
+ const result = {};
|
|
|
|
|
+
|
|
|
|
|
+ if (choice.delta.role) {
|
|
|
|
|
+ result.role = choice.delta.role;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (choice.delta.audio && choice.delta.audio.transcript) {
|
|
|
|
|
+ result.transcript = choice.delta.audio.transcript;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (Object.keys(result).length > 0) {
|
|
|
|
|
+ this.processedResponses.push(result);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ processResponseData() {
|
|
|
|
|
+ // 处理返回的数据
|
|
|
|
|
+ this.processedResponses = this.responses.map(item => {
|
|
|
|
|
+ const result = {}
|
|
|
|
|
+
|
|
|
|
|
+ // 处理角色信息
|
|
|
|
|
+ if (item.delta && item.delta.role) {
|
|
|
|
|
+ result.role = item.delta.role
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 处理音频转写文本
|
|
|
|
|
+ if (item.delta && item.delta.audio && item.delta.audio.transcript) {
|
|
|
|
|
+ result.transcript = item.delta.audio.transcript
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result
|
|
|
|
|
+ }).filter(item => Object.keys(item).length > 0)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 播放数字人视频
|
|
|
|
|
+ playDigitalHumanVideo() {
|
|
|
|
|
+ // 如果还没有加载问题数据,先获取问题数据
|
|
|
|
|
+ if (this.videoList.length === 0) {
|
|
|
|
|
+ this.fetchQuestions();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置当前视频
|
|
|
|
|
+ this.videoUrl = this.videoList[this.currentVideoIndex];
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 uni.createVideoContext 来控制视频
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+
|
|
|
|
|
+ // 设置超时检查,确认视频是否真的在播放
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (this.videoPlaying && this.$refs.videoPlayer) {
|
|
|
|
|
+ console.log('视频应该正在播放');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('视频可能未成功播放,尝试替代方案');
|
|
|
|
|
+ this.tryAlternativeVideoPath();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('无法创建视频上下文');
|
|
|
|
|
+ this.tryAlternativeVideoPath();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 tryAlternativeVideoPath 方法
|
|
|
|
|
+ tryAlternativeVideoPath() {
|
|
|
|
|
+ console.log('尝试使用替代路径');
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试不同的路径格式
|
|
|
|
|
+ const alternativePaths = [
|
|
|
|
|
+ './static/demo.mp4',
|
|
|
|
|
+ '../static/demo.mp4',
|
|
|
|
|
+ 'static/demo.mp4',
|
|
|
|
|
+ '/static/demo.mp4',
|
|
|
|
|
+ // 添加绝对路径
|
|
|
|
|
+ `${window.location.origin}/static/demo.mp4`
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前路径索引
|
|
|
|
|
+ const currentPathIndex = alternativePaths.indexOf(this.videoUrl);
|
|
|
|
|
+ const nextPathIndex = (currentPathIndex + 1) % alternativePaths.length;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置下一个路径
|
|
|
|
|
+ this.videoUrl = alternativePaths[nextPathIndex];
|
|
|
|
|
+ console.log('尝试新路径:', this.videoUrl);
|
|
|
|
|
+
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.stop();
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否成功播放
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ if (nextPathIndex === alternativePaths.length - 1 && !this.videoPlaying) {
|
|
|
|
|
+ console.log('所有路径均失败,尝试使用uni.getVideoInfo检查视频');
|
|
|
|
|
+ this.checkVideoWithAPI();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用uni API检查视频
|
|
|
|
|
+ checkVideoWithAPI() {
|
|
|
|
|
+ // 尝试使用uni.getVideoInfo API检查视频是否可用
|
|
|
|
|
+ uni.getVideoInfo({
|
|
|
|
|
+ src: '/static/demo.mp4',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ console.log('视频信息获取成功:', res);
|
|
|
|
|
+ // 如果能获取到视频信息,再次尝试播放
|
|
|
|
|
+ this.videoUrl = '/static/demo.mp4';
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('视频信息获取失败:', err);
|
|
|
|
|
+ // 最后尝试使用uni.chooseVideo API
|
|
|
|
|
+ this.fallbackToLocalVideo();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:回退到本地视频
|
|
|
|
|
+ fallbackToLocalVideo() {
|
|
|
|
|
+ console.log('尝试使用本地视频资源');
|
|
|
|
|
+
|
|
|
|
|
+ // 检查平台
|
|
|
|
|
+ const platform = uni.getSystemInfoSync().platform;
|
|
|
|
|
+ if (platform === 'android' || platform === 'ios') {
|
|
|
|
|
+ // 移动端可以尝试使用本地资源
|
|
|
|
|
+ this.videoUrl = platform === 'android' ? 'android.resource://package_name/raw/demo' : 'file:///assets/demo.mp4';
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 最终回退到静态图片
|
|
|
|
|
+ this.videoPlaying = false;
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '视频加载失败,显示静态图片',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 handleVideoError 方法
|
|
|
|
|
+ handleVideoError(e) {
|
|
|
|
|
+ console.error('视频加载错误:', e);
|
|
|
|
|
+
|
|
|
|
|
+ // 记录更详细的错误信息
|
|
|
|
|
+ if (e && e.detail) {
|
|
|
|
|
+ console.error('详细错误信息:', e.detail);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查视频文件是否存在
|
|
|
|
|
+ uni.getFileInfo({
|
|
|
|
|
+ filePath: this.videoUrl.startsWith('/') ? this.videoUrl.substring(1) : this.videoUrl,
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ console.log('文件存在,大小:', res.size);
|
|
|
|
|
+ // 文件存在但播放失败,可能是格式问题
|
|
|
|
|
+ this.tryDifferentFormat();
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('文件不存在或无法访问:', err);
|
|
|
|
|
+ // 尝试不同路径
|
|
|
|
|
+ this.tryAlternativeVideoPath();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 如果多次尝试后仍然失败,显示错误信息
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '视频加载失败,请检查文件是否存在',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:尝试不同格式
|
|
|
|
|
+ tryDifferentFormat() {
|
|
|
|
|
+ console.log('尝试不同的视频格式');
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试不同的视频格式
|
|
|
|
|
+ const formats = [
|
|
|
|
|
+ { ext: 'mp4', mime: 'video/mp4' },
|
|
|
|
|
+ { ext: 'webm', mime: 'video/webm' },
|
|
|
|
|
+ { ext: 'ogg', mime: 'video/ogg' },
|
|
|
|
|
+ { ext: 'mov', mime: 'video/quicktime' }
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前文件名(不含扩展名)
|
|
|
|
|
+ const currentPath = this.videoUrl;
|
|
|
|
|
+ const basePath = currentPath.substring(0, currentPath.lastIndexOf('.')) || '/static/demo';
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试下一个格式
|
|
|
|
|
+ let nextFormat = formats.find(f => !currentPath.endsWith(f.ext));
|
|
|
|
|
+ if (nextFormat) {
|
|
|
|
|
+ this.videoUrl = `${basePath}.${nextFormat.ext}`;
|
|
|
|
|
+ console.log('尝试新格式:', this.videoUrl);
|
|
|
|
|
+
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.stop();
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 所有格式都尝试过了,使用内置资源
|
|
|
|
|
+ this.useBuiltInResource();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用内置资源
|
|
|
|
|
+ useBuiltInResource() {
|
|
|
|
|
+ console.log('尝试使用内置资源');
|
|
|
|
|
+
|
|
|
|
|
+ // 检查平台
|
|
|
|
|
+ const platform = uni.getSystemInfoSync().platform;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据平台选择合适的视频源
|
|
|
|
|
+ if (platform === 'windows') {
|
|
|
|
|
+ // Windows平台特定处理
|
|
|
|
|
+ // 尝试使用相对于应用根目录的路径
|
|
|
|
|
+ const appRoot = process.env.UNI_INPUT_DIR || '';
|
|
|
|
|
+ this.videoUrl = `./static/demo.mp4`;
|
|
|
|
|
+
|
|
|
|
|
+ // 或者尝试使用file://协议
|
|
|
|
|
+ // this.videoUrl = `file:///${appRoot.replace(/\\/g, '/')}/static/demo.mp4`;
|
|
|
|
|
+
|
|
|
|
|
+ console.log('Windows平台尝试路径:', this.videoUrl);
|
|
|
|
|
+ } else if (platform === 'android' || platform === 'ios') {
|
|
|
|
|
+ // 移动端
|
|
|
|
|
+ this.useNativeVideo();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Web平台
|
|
|
|
|
+ // 尝试使用完整URL
|
|
|
|
|
+ const baseUrl = window.location.origin;
|
|
|
|
|
+ this.videoUrl = `${baseUrl}/static/demo.mp4`;
|
|
|
|
|
+ console.log('Web平台尝试URL:', this.videoUrl);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用原生视频能力
|
|
|
|
|
+ useNativeVideo() {
|
|
|
|
|
+ console.log('尝试使用原生视频能力');
|
|
|
|
|
+
|
|
|
|
|
+ // 在移动端,可以尝试使用原生视频播放器
|
|
|
|
|
+ uni.chooseVideo({
|
|
|
|
|
+ sourceType: ['album'],
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ this.videoUrl = res.tempFilePath;
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ // 如果用户取消选择,回退到静态图片
|
|
|
|
|
+ this.videoPlaying = false;
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '无法加载视频,显示静态图片',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ }); */
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理视频结束事件
|
|
|
|
|
+ handleVideoEnded() {
|
|
|
|
|
+ console.log('视频播放结束事件触发');
|
|
|
|
|
+
|
|
|
|
|
+ // 获取视频元素和当前时间
|
|
|
|
|
+ const videoElement = this.$refs.videoPlayer;
|
|
|
|
|
+ if (videoElement) {
|
|
|
|
|
+ const currentTime = videoElement.currentTime || 0;
|
|
|
|
|
+ const duration = videoElement.duration || 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 更严格的检查:只有当前时间非常接近视频总时长时才认为是真正结束
|
|
|
|
|
+ if (duration > 0 && (duration - currentTime > 0.5)) {
|
|
|
|
|
+ console.log('检测到可能的误触发结束事件,继续播放', currentTime, duration);
|
|
|
|
|
+ // 继续播放视频
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ // 如果距离结束还很远,回到历史位置
|
|
|
|
|
+ if (duration - currentTime > 2) {
|
|
|
|
|
+ videoContext.seek(this.historyTime || 0);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果接近结束但还没完全结束,继续播放
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('视频确认结束,当前时间:', currentTime, '总时长:', duration);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.videoPlaying = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否是低分提示视频
|
|
|
|
|
+ if (this.isPlayingLowScoreVideo) {
|
|
|
|
|
+ console.log('低分提示视频播放完成');
|
|
|
|
|
+
|
|
|
|
|
+ // 检查重试次数
|
|
|
|
|
+ if (this.retryCount < this.maxRetryAttempts) {
|
|
|
|
|
+ console.log(`显示重新录制按钮 (重试次数: ${this.retryCount + 1}/${this.maxRetryAttempts})`);
|
|
|
|
|
+
|
|
|
|
|
+ // 显示重新录制按钮
|
|
|
|
|
+ this.showRerecordButton = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复原始问题的字幕
|
|
|
|
|
+ if (this.isFollowUpQuestion && this.currentFollowUpQuestion) {
|
|
|
|
|
+ this.currentSubtitle = this.currentFollowUpQuestion.question || this.currentFollowUpQuestion.question;
|
|
|
|
|
+ } else if (this.originalQuestionSubtitle) {
|
|
|
|
|
+ this.currentSubtitle = this.originalQuestionSubtitle;
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('已达到最大重试次数,继续下一题');
|
|
|
|
|
+
|
|
|
|
|
+ // 重置重试次数
|
|
|
|
|
+ this.retryCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 继续下一个问题
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置标记
|
|
|
|
|
+ this.isPlayingLowScoreVideo = false;
|
|
|
|
|
+ this.originalQuestionSubtitle = null;
|
|
|
|
|
+
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否是追问问题视频
|
|
|
|
|
+ if (this.isFollowUpQuestion) {
|
|
|
|
|
+ console.log('追问问题视频播放完成');
|
|
|
|
|
+
|
|
|
|
|
+ // 不重置追问状态,保持显示
|
|
|
|
|
+ // this.isFollowUpQuestion = false;
|
|
|
|
|
+ // this.currentFollowUpQuestion = null;
|
|
|
|
|
+
|
|
|
|
|
+ // 显示"开始回答"按钮,准备回答追问
|
|
|
|
|
+ this.showStartRecordingButton = true;
|
|
|
|
|
+ this.startCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 恢复原有的视频结束处理逻辑
|
|
|
|
|
+ // 对于所有视频,都显示相应的按钮
|
|
|
|
|
+ if (this.currentVideoIndex >= 1) {
|
|
|
|
|
+ // 对于问题视频,显示"开始回答"按钮
|
|
|
|
|
+ this.showStartRecordingButton = true;
|
|
|
|
|
+ this.startCountdown();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 对于介绍视频(第一个视频),显示"开始面试"按钮
|
|
|
|
|
+ this.showAnswerButton = true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置追问状态
|
|
|
|
|
+ if (this.isFollowUpQuestion) {
|
|
|
|
|
+ this.isFollowUpQuestion = false;
|
|
|
|
|
+ this.currentFollowUpQuestion = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:开始倒计时
|
|
|
|
|
+ startCountdown() {
|
|
|
|
|
+ // 先清除可能存在的倒计时
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 显示倒计时蒙层
|
|
|
|
|
+ this.showCountdown = true;
|
|
|
|
|
+ this.countdownValue = 10;
|
|
|
|
|
+
|
|
|
|
|
+ // 开始倒计时
|
|
|
|
|
+ this.countdownTimer = setInterval(() => {
|
|
|
|
|
+ if (!this.showStartRecordingButton) {
|
|
|
|
|
+ // 如果开始回答按钮已经被点击,清除倒计时
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.countdownValue--;
|
|
|
|
|
+
|
|
|
|
|
+ if (this.countdownValue <= 0) {
|
|
|
|
|
+ // 倒计时结束,清除定时器
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 自动开始录制
|
|
|
|
|
+ this.showStartRecordingButton = false;
|
|
|
|
|
+ this.startRecordingAnswer();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:清除倒计时
|
|
|
|
|
+ clearCountdown() {
|
|
|
|
|
+ if (this.countdownTimer) {
|
|
|
|
|
+ clearInterval(this.countdownTimer);
|
|
|
|
|
+ this.countdownTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.showCountdown = false;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 handleStartRecordingClick 方法,使其可以终止倒计时
|
|
|
|
|
+ handleStartRecordingClick() {
|
|
|
|
|
+ // 隐藏"开始回答"按钮
|
|
|
|
|
+ this.showStartRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果倒计时正在进行,清除倒计时
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 重置等待回答状态
|
|
|
|
|
+ this.isWaitingForAnswer = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 直接开始录制
|
|
|
|
|
+ this.startRecordingAnswer();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 stopRecordingAnswer 方法,添加录制时长检查
|
|
|
|
|
+ stopRecordingAnswer() {
|
|
|
|
|
+ // 如果倒计时正在进行,先清除倒计时
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 重置等待回答状态
|
|
|
|
|
+ this.isWaitingForAnswer = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查录制时长
|
|
|
|
|
+ const recordingDuration = this.getRecordingDuration();
|
|
|
|
|
+ const minimumDuration = 3; // 最小录制时长(秒),改为3秒
|
|
|
|
|
+ const lowScoreDuration = 7; // 低分阈值时长(秒),少于7秒视为低分
|
|
|
|
|
+
|
|
|
|
|
+ // 保存录制时长,用于上传
|
|
|
|
|
+ this.currentRecordingDuration = recordingDuration;
|
|
|
|
|
+
|
|
|
|
|
+ if (recordingDuration < minimumDuration) {
|
|
|
|
|
+ // 录制时间过短,显示提示
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '录制时间过短,请至少录制3秒',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ // 不执行停止录制逻辑,继续录制
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否需要播放低分视频(录制时间少于7秒)
|
|
|
|
|
+ if (recordingDuration < lowScoreDuration) {
|
|
|
|
|
+ console.log(`录制时间 ${recordingDuration} 秒,少于 ${lowScoreDuration} 秒,将播放低分提示视频`);
|
|
|
|
|
+ // 停止录制但不上传
|
|
|
|
|
+ this.completeRecordingStop(false);
|
|
|
|
|
+ // 直接播放低分视频
|
|
|
|
|
+ this.playLowScoreVideo();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 录制时长足够,正常停止录制并上传
|
|
|
|
|
+ this.completeRecordingStop(true);
|
|
|
|
|
+ this.needPlayLowScoreVideo = false;
|
|
|
|
|
+ this.recordingTimeDisplay='00:00'
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:开始录制用户回答
|
|
|
|
|
+ startRecordingAnswer() {
|
|
|
|
|
+ console.log('开始录制用户回答');
|
|
|
|
|
+ this.isRecording = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 记录录制开始时间
|
|
|
|
|
+ this.recordingStartTime = Date.now();
|
|
|
|
|
+ this.recordingTimerCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前问题的推荐录制时长
|
|
|
|
|
+ this.maxRecordingTime = this.getCurrentQuestionRecommendedDuration();
|
|
|
|
|
+ this.remainingTime = this.maxRecordingTime;
|
|
|
|
|
+
|
|
|
|
|
+ // 启动计时器,每秒更新计时
|
|
|
|
|
+ this.recordingTimer = setInterval(() => {
|
|
|
|
|
+ this.recordingTimerCount++;
|
|
|
|
|
+ this.remainingTime = Math.max(0, this.maxRecordingTime - this.recordingTimerCount);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新UI显示录制时长和剩余时间
|
|
|
|
|
+ this.recordingTimeDisplay = this.formatTime(this.recordingTimerCount)/* +
|
|
|
|
|
+ ' / ' + this.formatTime(this.maxRecordingTime) */;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新进度百分比 - 确保值在0-100之间
|
|
|
|
|
+ this.progressPercent = Math.min((this.recordingTimerCount / this.maxRecordingTime) * 100, 100);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否达到最大录制时间
|
|
|
|
|
+ if (this.recordingTimerCount >= this.maxRecordingTime) {
|
|
|
|
|
+ console.log(`已达到最大录制时间(${this.maxRecordingTime}秒),自动停止录制`);
|
|
|
|
|
+ this.stopRecordingAnswer();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+
|
|
|
|
|
+ // 显示录制中的提示
|
|
|
|
|
+ // uni.showLoading({
|
|
|
|
|
+ // title: '正在录制...',
|
|
|
|
|
+ // mask: true
|
|
|
|
|
+ // });
|
|
|
|
|
+
|
|
|
|
|
+ // 根据平台选择不同的录制方法
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith('mp-');
|
|
|
|
|
+
|
|
|
|
|
+ if (isMiniProgram) {
|
|
|
|
|
+ // 小程序环境使用小程序的录制API
|
|
|
|
|
+ this.startMiniProgramRecording();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // H5/App环境使用MediaRecorder API
|
|
|
|
|
+ this.startBrowserRecording();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 显示停止录制按钮
|
|
|
|
|
+ this.showStopRecordingButton = true;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加一个新方法:重置相机组件
|
|
|
|
|
+ resetCamera() {
|
|
|
|
|
+ console.log('重置相机组件');
|
|
|
|
|
+
|
|
|
|
|
+ // 先完全移除相机组件
|
|
|
|
|
+ this.useMiniProgramCameraComponent = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 释放相机上下文
|
|
|
|
|
+ if (this.cameraContext) {
|
|
|
|
|
+ this.cameraContext = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟后重新创建相机组件
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.useMiniProgramCameraComponent = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 再次延迟创建相机上下文,确保组件已完全渲染
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.cameraContext = uni.createCameraContext();
|
|
|
|
|
+ console.log('相机组件已重置');
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 startMiniProgramRecording 方法
|
|
|
|
|
+ startMiniProgramRecording() {
|
|
|
|
|
+ console.log('开始小程序录制方法');
|
|
|
|
|
+
|
|
|
|
|
+ // 获取平台信息
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ const isIOS = systemInfo.platform === 'ios';
|
|
|
|
|
+
|
|
|
|
|
+ // 在iOS上,先重置相机
|
|
|
|
|
+ if (isIOS) {
|
|
|
|
|
+ // 先重置相机组件
|
|
|
|
|
+ this.resetCamera();
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟执行录制,给相机组件足够的初始化时间
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.actualStartRecording(isIOS);
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Android直接开始
|
|
|
|
|
+ this.actualStartRecording(isIOS);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:实际开始录制
|
|
|
|
|
+ actualStartRecording(isIOS) {
|
|
|
|
|
+ if (!this.cameraContext) {
|
|
|
|
|
+ this.cameraContext = uni.createCameraContext();
|
|
|
|
|
+ console.log('创建新的相机上下文');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确保有录音权限
|
|
|
|
|
+ uni.getSetting({
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ const hasRecordAuth = res.authSetting['scope.record'];
|
|
|
|
|
+ const hasCameraAuth = res.authSetting['scope.camera'];
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasRecordAuth || !hasCameraAuth) {
|
|
|
|
|
+ console.warn('缺少必要权限,请求权限');
|
|
|
|
|
+ this.requestMiniProgramPermissions();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 在iOS上,先检查相机状态
|
|
|
|
|
+ if (isIOS) {
|
|
|
|
|
+ console.log('iOS: 检查相机状态');
|
|
|
|
|
+
|
|
|
|
|
+ // 使用最简单的选项,设置最大录制时间为当前问题的推荐时长
|
|
|
|
|
+ const maxDuration = this.getCurrentQuestionRecommendedDuration() * 1000; // 转换为毫秒
|
|
|
|
|
+ const options = {
|
|
|
|
|
+ timeout: maxDuration, // 使用当前问题的推荐时长
|
|
|
|
|
+ quality: 'low', // 降低质量
|
|
|
|
|
+ compressed: true,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('iOS录制开始成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('iOS录制失败:', err);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果失败,尝试使用不同的方法
|
|
|
|
|
+ this.useAlternativeRecordingMethod();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ console.log('尝试开始录制');
|
|
|
|
|
+ this.recorder = this.cameraContext.startRecord(options);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('开始录制异常:', e);
|
|
|
|
|
+ this.useAlternativeRecordingMethod();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Android使用标准选项,设置最大录制时间为当前问题的推荐时长
|
|
|
|
|
+ const maxDuration = this.getCurrentQuestionRecommendedDuration() * 1000; // 转换为毫秒
|
|
|
|
|
+ const options = {
|
|
|
|
|
+ timeout: maxDuration, // 使用当前问题的推荐时长
|
|
|
|
|
+ quality: 'medium',
|
|
|
|
|
+ compressed: true,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('Android录制开始成功');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('Android录制失败:', err);
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '录制失败,请检查相机权限',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ this.recorder = this.cameraContext.startRecord(options);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用替代录制方法
|
|
|
|
|
+ useAlternativeRecordingMethod() {
|
|
|
|
|
+ console.log('使用替代录制方法');
|
|
|
|
|
+
|
|
|
|
|
+ // 在iOS上,可以尝试使用chooseVideo API作为备选
|
|
|
|
|
+ uni.showActionSheet({
|
|
|
|
|
+ itemList: ['使用相册中的视频', '跳过此问题'],
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.tapIndex === 0) {
|
|
|
|
|
+ // 选择相册中的视频
|
|
|
|
|
+ uni.chooseVideo({
|
|
|
|
|
+ sourceType: ['album'],
|
|
|
|
|
+ maxDuration: this.getCurrentQuestionRecommendedDuration(), // 使用当前问题的推荐时长
|
|
|
|
|
+ camera: 'front',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ console.log('选择视频成功:', res.tempFilePath);
|
|
|
|
|
+ // 停止录制状态
|
|
|
|
|
+ this.isRecording = false;
|
|
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 上传选择的视频
|
|
|
|
|
+ this.uploadRecordedVideo(res.tempFilePath);
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ console.log('用户取消选择视频');
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 跳过此问题
|
|
|
|
|
+ console.log('用户选择跳过问题');
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: () => {
|
|
|
|
|
+ console.log('操作取消');
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:请求小程序权限
|
|
|
|
|
+ requestMiniProgramPermissions() {
|
|
|
|
|
+ // 请求录音权限
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.record',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('录音权限已获取');
|
|
|
|
|
+
|
|
|
|
|
+ // 请求相机权限
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.camera',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('相机权限已获取');
|
|
|
|
|
+ // 权限都获取到后,开始录制
|
|
|
|
|
+ this.startMiniProgramRecording();
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('相机权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('相机');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('录音权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('录音');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改浏览器环境下的录制方法
|
|
|
|
|
+ startBrowserRecording() {
|
|
|
|
|
+ if (!this.cameraStream) {
|
|
|
|
|
+ console.error('没有可用的摄像头流');
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '录制失败,摄像头未就绪',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ }); */
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 检查流中是否包含音频轨道
|
|
|
|
|
+ const hasAudio = this.cameraStream.getAudioTracks().length > 0;
|
|
|
|
|
+ if (!hasAudio) {
|
|
|
|
|
+ console.warn('警告:媒体流中没有音频轨道,尝试重新获取带音频的媒体流');
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试重新获取带音频的媒体流
|
|
|
|
|
+ navigator.mediaDevices.getUserMedia({
|
|
|
|
|
+ audio: {
|
|
|
|
|
+ echoCancellation: true,
|
|
|
|
|
+ noiseSuppression: true,
|
|
|
|
|
+ autoGainControl: true
|
|
|
|
|
+ },
|
|
|
|
|
+ video: true
|
|
|
|
|
+ })
|
|
|
|
|
+ .then(newStream => {
|
|
|
|
|
+ // 检查新流是否包含音频轨道
|
|
|
|
|
+ const audioTracks = newStream.getAudioTracks();
|
|
|
|
|
+ if (audioTracks.length > 0) {
|
|
|
|
|
+ console.log('成功获取音频轨道:', audioTracks[0].label);
|
|
|
|
|
+
|
|
|
|
|
+ // 合并视频和音频轨道
|
|
|
|
|
+ const videoTrack = this.cameraStream.getVideoTracks()[0];
|
|
|
|
|
+ const audioTrack = newStream.getAudioTracks()[0];
|
|
|
|
|
+
|
|
|
|
|
+ // 创建新的媒体流,包含视频和音频轨道
|
|
|
|
|
+ const combinedStream = new MediaStream();
|
|
|
|
|
+ if (videoTrack) combinedStream.addTrack(videoTrack);
|
|
|
|
|
+ if (audioTrack) combinedStream.addTrack(audioTrack);
|
|
|
|
|
+
|
|
|
|
|
+ this.cameraStream = combinedStream;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新视频元素的源
|
|
|
|
|
+ const videoElement = this.$refs.userCameraVideo;
|
|
|
|
|
+ if (videoElement) {
|
|
|
|
|
+ videoElement.srcObject = combinedStream;
|
|
|
|
|
+ videoElement.muted = true; // 避免回声,但仍然录制声音
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 继续录制过程
|
|
|
|
|
+ this.setupMediaRecorder(combinedStream);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('仍然无法获取音频轨道');
|
|
|
|
|
+ // 使用现有流继续
|
|
|
|
|
+ this.setupMediaRecorder(this.cameraStream);
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(err => {
|
|
|
|
|
+ console.error('获取音频失败:', err);
|
|
|
|
|
+ // 使用现有流继续
|
|
|
|
|
+ this.setupMediaRecorder(this.cameraStream);
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('检测到音频轨道,直接使用');
|
|
|
|
|
+ // 已有音频轨道,直接使用
|
|
|
|
|
+ this.setupMediaRecorder(this.cameraStream);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('浏览器录制失败:', error);
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '录制失败,浏览器可能不支持此功能',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 setupMediaRecorder 方法
|
|
|
|
|
+ setupMediaRecorder(stream) {
|
|
|
|
|
+ // 检查流中的轨道
|
|
|
|
|
+ const videoTracks = stream.getVideoTracks();
|
|
|
|
|
+ const audioTracks = stream.getAudioTracks();
|
|
|
|
|
+
|
|
|
|
|
+ console.log('设置MediaRecorder - 视频轨道:', videoTracks.length, '音频轨道:', audioTracks.length);
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试使用支持度更广的编码格式
|
|
|
|
|
+ let mimeType = '';
|
|
|
|
|
+ const supportedTypes = [
|
|
|
|
|
+ 'video/webm;codecs=vp9,opus',
|
|
|
|
|
+ 'video/webm;codecs=vp8,opus',
|
|
|
|
|
+ 'video/webm;codecs=h264,opus',
|
|
|
|
|
+ 'video/mp4;codecs=h264,aac',
|
|
|
|
|
+ 'video/webm',
|
|
|
|
|
+ 'video/mp4'
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ for (const type of supportedTypes) {
|
|
|
|
|
+ if (MediaRecorder.isTypeSupported(type)) {
|
|
|
|
|
+ mimeType = type;
|
|
|
|
|
+ console.log('使用支持的MIME类型:', mimeType);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建MediaRecorder实例,设置较低的比特率以减小文件大小
|
|
|
|
|
+ const options = {
|
|
|
|
|
+ mimeType: mimeType || '',
|
|
|
|
|
+ audioBitsPerSecond: 64000, // 降低音频比特率
|
|
|
|
|
+ videoBitsPerSecond: 1000000, // 降低视频比特率到1Mbps
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.mediaRecorder = new MediaRecorder(stream, options);
|
|
|
|
|
+ console.log('MediaRecorder创建成功,使用选项:', options);
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.warn('使用指定选项创建MediaRecorder失败,尝试使用默认选项');
|
|
|
|
|
+ this.mediaRecorder = new MediaRecorder(stream);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 存储录制的数据块
|
|
|
|
|
+ this.recordedChunks = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 监听数据可用事件
|
|
|
|
|
+ this.mediaRecorder.ondataavailable = (event) => {
|
|
|
|
|
+ if (event.data && event.data.size > 0) {
|
|
|
|
|
+ this.recordedChunks.push(event.data);
|
|
|
|
|
+ console.log(`收到数据块: ${event.data.size} 字节`);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 监听录制停止事件
|
|
|
|
|
+ this.mediaRecorder.onstop = async () => {
|
|
|
|
|
+ console.log('MediaRecorder停止,数据块数量:', this.recordedChunks.length);
|
|
|
|
|
+
|
|
|
|
|
+ if (this.recordedChunks.length === 0) {
|
|
|
|
|
+ console.error('没有录制到数据');
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '录制失败,未捕获到数据',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ }); */
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建Blob对象
|
|
|
|
|
+ const mimeType = this.mediaRecorder.mimeType || 'video/webm';
|
|
|
|
|
+ const blob = new Blob(this.recordedChunks, { type: mimeType });
|
|
|
|
|
+ console.log('创建Blob,原始大小:', blob.size, '类型:', mimeType);
|
|
|
|
|
+
|
|
|
|
|
+ // 显示压缩中提示
|
|
|
|
|
+ uni.showLoading({
|
|
|
|
|
+ title: '正在处理视频...',
|
|
|
|
|
+ mask: true
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 压缩视频
|
|
|
|
|
+ const compressedBlob = await this.compressVideo(blob);
|
|
|
|
|
+
|
|
|
|
|
+ // 将Blob转换为File对象
|
|
|
|
|
+ const fileName = `answer_${this.currentVideoIndex}_${Date.now()}.webm`;
|
|
|
|
|
+ const file = new File([compressedBlob], fileName, { type: mimeType });
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏压缩提示
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+
|
|
|
|
|
+ // 上传文件
|
|
|
|
|
+ this.uploadRecordedVideo(file);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('视频处理失败:', error);
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果压缩失败,使用原始视频
|
|
|
|
|
+ const fileName = `answer_${this.currentVideoIndex}_${Date.now()}.webm`;
|
|
|
|
|
+ const file = new File([blob], fileName, { type: mimeType });
|
|
|
|
|
+ this.uploadRecordedVideo(file);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 监听错误
|
|
|
|
|
+ this.mediaRecorder.onerror = (event) => {
|
|
|
|
|
+ console.error('MediaRecorder错误:', event.error);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 开始录制
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.mediaRecorder.start(1000); // 每秒触发一次dataavailable事件
|
|
|
|
|
+ console.log('MediaRecorder开始录制');
|
|
|
|
|
+
|
|
|
|
|
+ // 设置最大录制时间为60秒
|
|
|
|
|
+ // 注意:我们已经在startRecordingAnswer方法中设置了全局计时器
|
|
|
|
|
+ // 这里作为备份机制,确保即使全局计时器失效,也能停止录制
|
|
|
|
|
+ this.mediaRecorderTimeout = setTimeout(() => {
|
|
|
|
|
+ if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
|
|
|
|
|
+ console.log('MediaRecorder备份超时机制触发,停止录制');
|
|
|
|
|
+ this.mediaRecorder.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 300000); // 300秒 (5分钟)
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('开始录制失败:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:获取录制时长
|
|
|
|
|
+ getRecordingDuration() {
|
|
|
|
|
+ // 如果有明确的录制开始时间,计算实际录制时长
|
|
|
|
|
+ if (this.recordingStartTime) {
|
|
|
|
|
+ return (Date.now() - this.recordingStartTime) / 1000;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有明确记录开始时间,尝试从视频元素获取
|
|
|
|
|
+ if (this.mediaRecorder && this.$refs.userCameraVideo) {
|
|
|
|
|
+ return this.$refs.userCameraVideo.currentTime || 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果无法获取准确时长,返回估计值(如果有计时器)
|
|
|
|
|
+ if (this.recordingTimerCount) {
|
|
|
|
|
+ return this.recordingTimerCount;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 默认返回0
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:重置录制
|
|
|
|
|
+ resetRecording() {
|
|
|
|
|
+ // 保持录制状态,但重置计时器
|
|
|
|
|
+ if (this.recordingTimer) {
|
|
|
|
|
+ clearTimeout(this.recordingTimer);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置录制开始时间
|
|
|
|
|
+ this.recordingStartTime = Date.now();
|
|
|
|
|
+ this.recordingTimerCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是浏览器环境,需要重置MediaRecorder
|
|
|
|
|
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
|
|
|
+ // 停止当前录制
|
|
|
|
|
+ this.mediaRecorder.stop();
|
|
|
|
|
+
|
|
|
|
|
+ // 清空已录制的数据块
|
|
|
|
|
+ this.recordedChunks = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 短暂延迟后重新开始录制
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.startBrowserRecording();
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果是小程序环境,需要重置相机录制
|
|
|
|
|
+ else if (this.cameraContext) {
|
|
|
|
|
+ // 停止当前录制
|
|
|
|
|
+ this.cameraContext.stopRecord({
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('相机录制已停止');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('停止相机录制失败:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 显示提示
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '请重新开始回答',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ }); */
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 completeRecordingStop 方法,确保正确处理追问问题ID
|
|
|
|
|
+ completeRecordingStop(uploadVideo = true) {
|
|
|
|
|
+ console.log('完成录制停止');
|
|
|
|
|
+
|
|
|
|
|
+ // 停止录制计时器
|
|
|
|
|
+ if (this.recordingTimer) {
|
|
|
|
|
+ clearInterval(this.recordingTimer);
|
|
|
|
|
+ this.recordingTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 记录最终录制时长
|
|
|
|
|
+ this.finalRecordingDuration = this.recordingTimerCount;
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏停止录制按钮
|
|
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏录制指示器
|
|
|
|
|
+ this.isRecording = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据平台选择不同的停止录制方法
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith('mp-');
|
|
|
|
|
+
|
|
|
|
|
+ if (isMiniProgram) {
|
|
|
|
|
+ // 小程序环境停止录制
|
|
|
|
|
+ if (uploadVideo) {
|
|
|
|
|
+ this.stopMiniProgramRecording();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果不需要上传,直接停止录制
|
|
|
|
|
+ if (this.cameraContext) {
|
|
|
|
|
+ this.cameraContext.stopRecord({
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('相机录制已停止,不上传视频');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('停止相机录制失败:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // H5/App环境停止录制
|
|
|
|
|
+ if (uploadVideo) {
|
|
|
|
|
+ this.stopBrowserRecording();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果不需要上传,直接停止录制
|
|
|
|
|
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
|
|
|
+ this.mediaRecorder.stop();
|
|
|
|
|
+ this.recordedChunks = []; // 清空已录制的数据
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 记录当前问题ID和类型,确保追问问题使用正确的ID
|
|
|
|
|
+ if (this.isFollowUpQuestion && this.currentFollowUpQuestion) {
|
|
|
|
|
+ console.log('当前是追问问题,记录追问问题ID:', this.currentFollowUpQuestion.id);
|
|
|
|
|
+ this.lastQuestionWasFollowUp = true;
|
|
|
|
|
+ this.lastFollowUpQuestionId = this.currentFollowUpQuestion.id;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.lastQuestionWasFollowUp = false;
|
|
|
|
|
+ this.lastFollowUpQuestionId = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 stopMiniProgramRecording 方法,确保正确传递追问问题ID
|
|
|
|
|
+ stopMiniProgramRecording() {
|
|
|
|
|
+ console.log('停止小程序录制');
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有录制上下文
|
|
|
|
|
+ if (!this.cameraContext) {
|
|
|
|
|
+ console.error('相机上下文不存在');
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 停止录制
|
|
|
|
|
+ this.cameraContext.stopRecord({
|
|
|
|
|
+ success: async (res) => {
|
|
|
|
|
+ console.log('小程序录制停止成功:', res);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取录制的视频文件路径
|
|
|
|
|
+ const videoPath = res.tempVideoPath;
|
|
|
|
|
+
|
|
|
|
|
+ // 上传视频文件
|
|
|
|
|
+ if (videoPath) {
|
|
|
|
|
+ // 等待上传完成后再继续
|
|
|
|
|
+ await this.uploadRecordedVideo(videoPath);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('未获取到录制视频路径');
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('小程序录制停止失败:', err);
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:停止浏览器录制
|
|
|
|
|
+ stopBrowserRecording() {
|
|
|
|
|
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
|
|
|
+ this.mediaRecorder.stop();
|
|
|
|
|
+ console.log('浏览器录制停止成功');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('MediaRecorder不存在或已经停止');
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 uploadRecordedVideo 方法,添加记录父问题ID的逻辑
|
|
|
|
|
+ uploadRecordedVideo(fileOrPath) {
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ console.log('准备上传视频:', typeof fileOrPath === 'string' ? fileOrPath : fileOrPath.name);
|
|
|
|
|
+ console.log('当前问题ID:', this.currentParentQuestionId);
|
|
|
|
|
+ console.log('当前问题类型:', this.isFollowUpQuestion);
|
|
|
|
|
+ console.log('当前追问问题:', this.currentFollowUpQuestion);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前问题ID和相关信息
|
|
|
|
|
+ let questionId;
|
|
|
|
|
+ let isFollowUpQuestionUpload = false;
|
|
|
|
|
+ let questionText = '';
|
|
|
|
|
+ let questionForm = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 检查当前是否是追问问题
|
|
|
|
|
+ if (this.currentFollowUpQuestion &&
|
|
|
|
|
+ (this.currentFollowUpQuestion.question_form === 5 || this.isFollowUpQuestion)) {
|
|
|
|
|
+ // 使用追问问题的信息
|
|
|
|
|
+ questionId = this.currentFollowUpQuestion.id;
|
|
|
|
|
+ isFollowUpQuestionUpload = true;
|
|
|
|
|
+ questionText = this.currentFollowUpQuestion.question || '';
|
|
|
|
|
+ questionForm = 5; // 追问问题类型
|
|
|
|
|
+ console.log('正在上传追问问题的回答:', {
|
|
|
|
|
+ questionId,
|
|
|
|
|
+ questionText,
|
|
|
|
|
+ questionForm,
|
|
|
|
|
+ isFollowUp: true
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 显示思考中loading
|
|
|
|
|
+ this.showThinkingLoading();
|
|
|
|
|
+ // 使用常规问题的信息
|
|
|
|
|
+ const currentQuestion = this.getCurrentQuestionByIndex(this.currentVideoIndex);
|
|
|
|
|
+ if (currentQuestion && currentQuestion.id) {
|
|
|
|
|
+ questionId = currentQuestion.id;
|
|
|
|
|
+ questionText = currentQuestion.question || '';
|
|
|
|
|
+ console.log('正在上传常规问题的回答:', {
|
|
|
|
|
+ questionId,
|
|
|
|
|
+ questionText,
|
|
|
|
|
+ questionForm: 0
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ questionId = this.getDefaultQuestionId(this.currentVideoIndex);
|
|
|
|
|
+ console.log('使用默认问题ID:', questionId);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 创建上传任务对象
|
|
|
|
|
+ const uploadTask = {
|
|
|
|
|
+ id: Date.now().toString(),
|
|
|
|
|
+ file: fileOrPath,
|
|
|
|
|
+ questionId: questionId,
|
|
|
|
|
+ isFollowUp: isFollowUpQuestionUpload,
|
|
|
|
|
+ questionText: questionText,
|
|
|
|
|
+ questionForm: questionForm,
|
|
|
|
|
+ attempts: 0,
|
|
|
|
|
+ maxAttempts: 3,
|
|
|
|
|
+ parentQuestionId: this.currentParentQuestionId,
|
|
|
|
|
+ videoDuration: this.currentRecordingDuration || 0 // 添加视频时长
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 如果不是追问问题,记录当前问题ID作为父问题ID
|
|
|
|
|
+ if (!isFollowUpQuestionUpload) {
|
|
|
|
|
+ this.currentParentQuestionId = questionId;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到上传队列
|
|
|
|
|
+ this.uploadQueue.push(uploadTask);
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化上传进度和状态
|
|
|
|
|
+ this.uploadProgress[uploadTask.id] = 0;
|
|
|
|
|
+ this.uploadStatus[uploadTask.id] = 'pending';
|
|
|
|
|
+
|
|
|
|
|
+ // 显示上传状态提示
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '正在上传回答...',
|
|
|
|
|
+ icon: 'loading',
|
|
|
|
|
+ duration: 1500
|
|
|
|
|
+ }); */
|
|
|
|
|
+
|
|
|
|
|
+ // 更新上传状态文本
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是追问问题,使用后台上传,不阻塞流程
|
|
|
|
|
+ if (isFollowUpQuestionUpload) {
|
|
|
|
|
+ console.log('追问问题使用后台上传,不阻塞流程');
|
|
|
|
|
+
|
|
|
|
|
+ // 立即执行后续操作,不等待上传完成
|
|
|
|
|
+ this.handlePostUploadActions(uploadTask);
|
|
|
|
|
+
|
|
|
|
|
+ // 启动后台上传
|
|
|
|
|
+ this.startBackgroundUpload(uploadTask);
|
|
|
|
|
+
|
|
|
|
|
+ // 返回立即解决的Promise
|
|
|
|
|
+ return Promise.resolve();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 常规问题保持原有逻辑,等待上传完成
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ const checkUploadStatus = () => {
|
|
|
|
|
+ if (!this.isUploading && this.uploadQueue.length === 0) {
|
|
|
|
|
+ // 上传完成后执行后续操作
|
|
|
|
|
+ this.handlePostUploadActions(uploadTask);
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果还在上传,继续检查
|
|
|
|
|
+ setTimeout(checkUploadStatus, 100);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 如果当前没有上传任务在进行,开始处理队列
|
|
|
|
|
+ if (!this.isUploading) {
|
|
|
|
|
+ this.processUploadQueue();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 开始检查上传状态
|
|
|
|
|
+ checkUploadStatus();
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:启动后台上传
|
|
|
|
|
+ startBackgroundUpload(task) {
|
|
|
|
|
+ console.log('启动后台上传任务:', task.id);
|
|
|
|
|
+
|
|
|
|
|
+ // 标记为后台上传任务
|
|
|
|
|
+ task.isBackgroundUpload = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 从主上传队列中移除,避免与常规上传冲突
|
|
|
|
|
+ const taskIndex = this.uploadQueue.findIndex(t => t.id === task.id);
|
|
|
|
|
+ if (taskIndex !== -1) {
|
|
|
|
|
+ this.uploadQueue.splice(taskIndex, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到后台上传队列
|
|
|
|
|
+ if (!this.backgroundUploadQueue) {
|
|
|
|
|
+ this.backgroundUploadQueue = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ this.backgroundUploadQueue.push(task);
|
|
|
|
|
+
|
|
|
|
|
+ // 启动后台上传处理
|
|
|
|
|
+ this.processBackgroundUploadQueue();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:处理后台上传队列
|
|
|
|
|
+ processBackgroundUploadQueue() {
|
|
|
|
|
+ // 如果后台上传队列为空,结束处理
|
|
|
|
|
+ if (!this.backgroundUploadQueue || this.backgroundUploadQueue.length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取队列中的第一个任务
|
|
|
|
|
+ const task = this.backgroundUploadQueue[0];
|
|
|
|
|
+
|
|
|
|
|
+ console.log('开始后台上传:', task.id);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新任务状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'uploading';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 增加尝试次数
|
|
|
|
|
+ task.attempts++;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据文件类型选择上传方法
|
|
|
|
|
+ if (typeof task.file === 'string') {
|
|
|
|
|
+ // 小程序环境,使用uni.uploadFile上传
|
|
|
|
|
+ this.uploadFileWithUniBackground(task);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 浏览器环境,使用XMLHttpRequest上传
|
|
|
|
|
+ this.uploadFileWithXHRBackground(task);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用XMLHttpRequest后台上传文件
|
|
|
|
|
+ uploadFileWithXHRBackground(task) {
|
|
|
|
|
+ // 获取用户信息
|
|
|
|
|
+ const userInfo = uni.getStorageSync('userInfo');
|
|
|
|
|
+ const openid = userInfo ? (JSON.parse(userInfo).openid || '') : '';
|
|
|
|
|
+ const tenant_id = JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1';
|
|
|
|
|
+
|
|
|
|
|
+ // 创建FormData
|
|
|
|
|
+ const formData = new FormData();
|
|
|
|
|
+ formData.append('file', task.file);
|
|
|
|
|
+ formData.append('openid', openid);
|
|
|
|
|
+ formData.append('tenant_id', tenant_id);
|
|
|
|
|
+ formData.append('application_id', uni.getStorageSync('appId'));
|
|
|
|
|
+ formData.append('question_id', task.questionId);
|
|
|
|
|
+ formData.append('video_duration', task.videoDuration || 0);
|
|
|
|
|
+ formData.append('has_audio', 'true');
|
|
|
|
|
+
|
|
|
|
|
+ // 创建XMLHttpRequest
|
|
|
|
|
+ const xhr = new XMLHttpRequest();
|
|
|
|
|
+
|
|
|
|
|
+ // 监听上传进度
|
|
|
|
|
+ xhr.upload.onprogress = (event) => {
|
|
|
|
|
+ if (event.lengthComputable) {
|
|
|
|
|
+ const progress = Math.round((event.loaded / event.total) * 100);
|
|
|
|
|
+ this.uploadProgress[task.id] = progress;
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 监听请求完成
|
|
|
|
|
+ xhr.onload = () => {
|
|
|
|
|
+ if (xhr.status === 200) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = JSON.parse(xhr.responseText);
|
|
|
|
|
+ console.log('后台上传响应:', res);
|
|
|
|
|
+ if (res.code === 2000) {
|
|
|
|
|
+ // 获取上传后的视频URL
|
|
|
|
|
+ const videoUrl = res.data.url || res.data.photoUrl || '';
|
|
|
|
|
+ if (videoUrl) {
|
|
|
|
|
+ // 上传成功,更新状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'success';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 提交到面试接口
|
|
|
|
|
+ this.submitVideoToInterviewBackground(videoUrl, task);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, '视频URL获取失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, res.msg || '上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, '解析响应失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, 'HTTP状态: ' + xhr.status);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 监听错误
|
|
|
|
|
+ xhr.onerror = () => {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, '网络错误');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 监听超时
|
|
|
|
|
+ xhr.ontimeout = () => {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, '上传超时');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 发送请求
|
|
|
|
|
+ xhr.open('POST', `${apiBaseUrl}/api/upload/`);
|
|
|
|
|
+ xhr.send(formData);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用uni.uploadFile后台上传文件
|
|
|
|
|
+ uploadFileWithUniBackground(task) {
|
|
|
|
|
+ // 获取用户信息
|
|
|
|
|
+ const userInfo = uni.getStorageSync('userInfo');
|
|
|
|
|
+ const openid = userInfo ? (JSON.parse(userInfo).openid || '') : '';
|
|
|
|
|
+ const tenant_id = JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1';
|
|
|
|
|
+
|
|
|
|
|
+ // 创建上传任务
|
|
|
|
|
+ const uploadTask = uni.uploadFile({
|
|
|
|
|
+ url: `${apiBaseUrl}/api/upload/`,
|
|
|
|
|
+ filePath: task.file,
|
|
|
|
|
+ name: 'file',
|
|
|
|
|
+ formData: {
|
|
|
|
|
+ openid: openid,
|
|
|
|
|
+ tenant_id: tenant_id,
|
|
|
|
|
+ application_id: uni.getStorageSync('appId'),
|
|
|
|
|
+ question_id: task.questionId,
|
|
|
|
|
+ video_duration: task.videoDuration || 0,
|
|
|
|
|
+ has_audio: 'true'
|
|
|
|
|
+ },
|
|
|
|
|
+ success: (uploadRes) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = JSON.parse(uploadRes.data);
|
|
|
|
|
+ console.log('后台上传响应:', res);
|
|
|
|
|
+ if (res.code === 2000) {
|
|
|
|
|
+ // 获取上传后的视频URL
|
|
|
|
|
+ const videoUrl = res.data.permanent_link || res.data.url || '';
|
|
|
|
|
+ if (videoUrl) {
|
|
|
|
|
+ // 上传成功,更新状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'success';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 提交到面试接口
|
|
|
|
|
+ this.submitVideoToInterviewBackground(videoUrl, task);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, '视频URL获取失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, res.msg || '上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, '解析响应失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ this.handleBackgroundUploadFailure(task, err.errMsg || '上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听上传进度
|
|
|
|
|
+ uploadTask.onProgressUpdate((res) => {
|
|
|
|
|
+ this.uploadProgress[task.id] = res.progress;
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:处理后台上传失败
|
|
|
|
|
+ handleBackgroundUploadFailure(task, errorMsg) {
|
|
|
|
|
+ console.error('后台上传失败:', errorMsg);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新任务状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'failed';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否还有重试机会
|
|
|
|
|
+ if (task.attempts < task.maxAttempts) {
|
|
|
|
|
+ console.log(`将在5秒后重试后台上传,当前尝试次数: ${task.attempts}/${task.maxAttempts}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 5秒后重试
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ // 重置进度
|
|
|
|
|
+ this.uploadProgress[task.id] = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 重新开始上传
|
|
|
|
|
+ if (typeof task.file !== 'string') {
|
|
|
|
|
+ this.uploadFileWithXHRBackground(task);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.uploadFileWithUniBackground(task);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 5000);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 超过最大重试次数,移除任务
|
|
|
|
|
+ console.log('超过最大重试次数,放弃后台上传');
|
|
|
|
|
+
|
|
|
|
|
+ // 从后台上传队列中移除当前任务
|
|
|
|
|
+ if (this.backgroundUploadQueue) {
|
|
|
|
|
+ const taskIndex = this.backgroundUploadQueue.findIndex(t => t.id === task.id);
|
|
|
|
|
+ if (taskIndex !== -1) {
|
|
|
|
|
+ this.backgroundUploadQueue.splice(taskIndex, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 继续处理队列中的下一个任务
|
|
|
|
|
+ this.processBackgroundUploadQueue();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:后台上传提交到面试接口
|
|
|
|
|
+ submitVideoToInterviewBackground(videoUrl, task) {
|
|
|
|
|
+ // 准备请求参数
|
|
|
|
|
+ const followUpRequestData = {
|
|
|
|
|
+ application_id: uni.getStorageSync('appId'),
|
|
|
|
|
+ tenant_id: JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1',
|
|
|
|
|
+ video_url: videoUrl,
|
|
|
|
|
+ original_question_id: this.parentJobPositionQuestionId, // 使用保存的job_position_question_id
|
|
|
|
|
+ follow_up_question: task.questionText,
|
|
|
|
|
+ video_duration: task.videoDuration,
|
|
|
|
|
+ openid: JSON.parse(uni.getStorageSync('userInfo')).openid || ''
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ console.log('后台上传提交追问视频:', followUpRequestData);
|
|
|
|
|
+
|
|
|
|
|
+ uni.request({
|
|
|
|
|
+ url: `${apiBaseUrl}/voice_interview/upload_follow_up_video/`,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: followUpRequestData,
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'content-type': 'application/x-www-form-urlencoded'
|
|
|
|
|
+ },
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.data.code === 200 || res.data.code === 2000) {
|
|
|
|
|
+ console.log('后台上传追问视频提交成功');
|
|
|
|
|
+
|
|
|
|
|
+ // 从后台上传队列中移除任务
|
|
|
|
|
+ if (this.backgroundUploadQueue) {
|
|
|
|
|
+ const taskIndex = this.backgroundUploadQueue.findIndex(t => t.id === task.id);
|
|
|
|
|
+ if (taskIndex !== -1) {
|
|
|
|
|
+ this.backgroundUploadQueue.splice(taskIndex, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 继续处理下一个后台上传任务
|
|
|
|
|
+ this.processBackgroundUploadQueue();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleBackgroundSubmitFailure(task, '提交失败: ' + (res.data.msg || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('后台上传提交失败:', err);
|
|
|
|
|
+ this.handleBackgroundSubmitFailure(task, '提交失败: ' + err.errMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:处理后台上传提交失败
|
|
|
|
|
+ handleBackgroundSubmitFailure(task, errorMsg) {
|
|
|
|
|
+ console.error('后台上传提交失败:', errorMsg);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否还有重试机会
|
|
|
|
|
+ if (task.attempts < task.maxAttempts) {
|
|
|
|
|
+ console.log(`将在5秒后重试后台上传提交,当前尝试次数: ${task.attempts}/${task.maxAttempts}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 5秒后重试
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.submitVideoToInterviewBackground(task.videoUrl, task);
|
|
|
|
|
+ }, 5000);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 超过最大重试次数,移除任务
|
|
|
|
|
+ console.log('超过最大重试次数,放弃后台上传提交');
|
|
|
|
|
+
|
|
|
|
|
+ // 从后台上传队列中移除当前任务
|
|
|
|
|
+ if (this.backgroundUploadQueue) {
|
|
|
|
|
+ const taskIndex = this.backgroundUploadQueue.findIndex(t => t.id === task.id);
|
|
|
|
|
+ if (taskIndex !== -1) {
|
|
|
|
|
+ this.backgroundUploadQueue.splice(taskIndex, 1);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 继续处理队列中的下一个任务
|
|
|
|
|
+ this.processBackgroundUploadQueue();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:处理上传后的逻辑
|
|
|
|
|
+ handlePostUploadActions(task) {
|
|
|
|
|
+ // 隐藏思考中loading
|
|
|
|
|
+ this.hideThinkingLoading();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否需要播放低分视频
|
|
|
|
|
+ if (this.needPlayLowScoreVideo && this.retryCount < 1) {
|
|
|
|
|
+ this.playLowScoreVideo();
|
|
|
|
|
+ this.needPlayLowScoreVideo = false;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果是追问问题的回答,直接进入下一个问题
|
|
|
|
|
+ if (task.isFollowUp) {
|
|
|
|
|
+ console.log('追问问题回答完成,进入下一个问题');
|
|
|
|
|
+ this.retryCount = 0; // 重置重试次数
|
|
|
|
|
+ this.isFollowUpQuestion = false; // 重置追问标记
|
|
|
|
|
+ this.currentFollowUpQuestion = null; // 清除当前追问问题信息
|
|
|
|
|
+ this.videoUrl=''
|
|
|
|
|
+ // 若未达到上限则继续请求下一条追问,否则进入下一题
|
|
|
|
|
+ this.maybeRequestNextFollowUp(this.parentJobPositionQuestionId);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果是常规问题,记录父问题ID并检查追问问题
|
|
|
|
|
+ this.currentParentQuestionId = task.questionId;
|
|
|
|
|
+ console.log('检查常规问题的追问,父问题ID:', this.currentParentQuestionId);
|
|
|
|
|
+ // 从题目读取追问上限(若后端提供),否则使用默认
|
|
|
|
|
+ const currentQuestion = this.getCurrentQuestionByIndex(this.currentVideoIndex);
|
|
|
|
|
+ if (currentQuestion && typeof currentQuestion.follow_up_limit !== 'undefined') {
|
|
|
|
|
+ this.setFollowUpLimit(this.currentParentQuestionId, currentQuestion.follow_up_limit);
|
|
|
|
|
+ } else if (typeof this.defaultFollowUpLimit !== 'undefined') {
|
|
|
|
|
+ this.setFollowUpLimit(this.currentParentQuestionId, this.defaultFollowUpLimit);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.resetFollowUpAskedCount(this.currentParentQuestionId);
|
|
|
|
|
+ this.checkAndPlayFollowUpQuestion(this.currentParentQuestionId);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 processUploadQueue 方法
|
|
|
|
|
+ processUploadQueue() {
|
|
|
|
|
+ // 如果队列为空,结束处理
|
|
|
|
|
+ if (this.uploadQueue.length === 0) {
|
|
|
|
|
+ this.isUploading = false;
|
|
|
|
|
+ this.showUploadStatus = false;
|
|
|
|
|
+ this.hideThinkingLoading(); // 隐藏思考中loading
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 标记为正在上传
|
|
|
|
|
+ this.isUploading = true;
|
|
|
|
|
+ this.showUploadStatus = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取队列中的第一个任务
|
|
|
|
|
+ const task = this.uploadQueue[0];
|
|
|
|
|
+
|
|
|
|
|
+ // 更新任务状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'uploading';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 增加尝试次数
|
|
|
|
|
+ task.attempts++;
|
|
|
|
|
+
|
|
|
|
|
+ // 根据文件类型选择上传方法
|
|
|
|
|
+ if (typeof task.file === 'string') {
|
|
|
|
|
+ // 小程序环境,使用uni.uploadFile上传
|
|
|
|
|
+ this.uploadFileWithUni(task);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 浏览器环境,使用XMLHttpRequest上传
|
|
|
|
|
+ this.uploadFileWithXHR(task);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用XMLHttpRequest上传文件
|
|
|
|
|
+ uploadFileWithXHR(task) {
|
|
|
|
|
+ // 获取用户信息
|
|
|
|
|
+ const userInfo = uni.getStorageSync('userInfo');
|
|
|
|
|
+ const openid = userInfo ? (JSON.parse(userInfo).openid || '') : '';
|
|
|
|
|
+ const tenant_id = JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1';
|
|
|
|
|
+
|
|
|
|
|
+ // 创建FormData
|
|
|
|
|
+ const formData = new FormData();
|
|
|
|
|
+ formData.append('file', task.file);
|
|
|
|
|
+ formData.append('openid', openid);
|
|
|
|
|
+ formData.append('tenant_id', tenant_id);
|
|
|
|
|
+ formData.append('application_id', uni.getStorageSync('appId'));
|
|
|
|
|
+ formData.append('question_id', task.questionId);
|
|
|
|
|
+ formData.append('video_duration', task.videoDuration);
|
|
|
|
|
+ formData.append('has_audio', 'true');
|
|
|
|
|
+
|
|
|
|
|
+ // 创建XMLHttpRequest
|
|
|
|
|
+ const xhr = new XMLHttpRequest();
|
|
|
|
|
+ xhr.open('POST', `${apiBaseUrl}/api/upload/`, true);
|
|
|
|
|
+ xhr.timeout = 120000; // 2分钟超时
|
|
|
|
|
+
|
|
|
|
|
+ // 监听上传进度
|
|
|
|
|
+ xhr.upload.onprogress = (event) => {
|
|
|
|
|
+ if (event.lengthComputable) {
|
|
|
|
|
+ const percentComplete = Math.round((event.loaded / event.total) * 100);
|
|
|
|
|
+ this.uploadProgress[task.id] = percentComplete;
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 监听请求完成
|
|
|
|
|
+ xhr.onload = () => {
|
|
|
|
|
+ if (xhr.status === 200) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = JSON.parse(xhr.responseText);
|
|
|
|
|
+ console.log('上传响应:', res);
|
|
|
|
|
+ if (res.code === 2000) {
|
|
|
|
|
+ // 获取上传后的视频URL
|
|
|
|
|
+ const videoUrl = res.data.url || res.data.photoUrl || '';
|
|
|
|
|
+ if (videoUrl) {
|
|
|
|
|
+ // 上传成功,更新状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'success';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 提交到面试接口
|
|
|
|
|
+ this.submitVideoToInterview(videoUrl, task);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleUploadFailure(task, '视频URL获取失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleUploadFailure(task, res.msg || '上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.handleUploadFailure(task, '解析响应失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleUploadFailure(task, 'HTTP状态: ' + xhr.status);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 监听错误
|
|
|
|
|
+ xhr.onerror = () => {
|
|
|
|
|
+ this.handleUploadFailure(task, '网络错误');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 监听超时
|
|
|
|
|
+ xhr.ontimeout = () => {
|
|
|
|
|
+ this.handleUploadFailure(task, '上传超时');
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 发送请求
|
|
|
|
|
+ xhr.send(formData);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用uni.uploadFile上传文件
|
|
|
|
|
+ uploadFileWithUni(task) {
|
|
|
|
|
+ // 获取用户信息
|
|
|
|
|
+ const userInfo = uni.getStorageSync('userInfo');
|
|
|
|
|
+ const openid = userInfo ? (JSON.parse(userInfo).openid || '') : '';
|
|
|
|
|
+ const tenant_id = JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1';
|
|
|
|
|
+
|
|
|
|
|
+ // 创建上传任务
|
|
|
|
|
+ const uploadTask = uni.uploadFile({
|
|
|
|
|
+ url: `${apiBaseUrl}/api/upload/`,
|
|
|
|
|
+ filePath: task.file,
|
|
|
|
|
+ name: 'file',
|
|
|
|
|
+ formData: {
|
|
|
|
|
+ openid: openid,
|
|
|
|
|
+ tenant_id: tenant_id,
|
|
|
|
|
+ application_id: uni.getStorageSync('appId'),
|
|
|
|
|
+ question_id: task.questionId,
|
|
|
|
|
+ video_duration: 0,
|
|
|
|
|
+ has_audio: 'true'
|
|
|
|
|
+ },
|
|
|
|
|
+ success: (uploadRes) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = JSON.parse(uploadRes.data);
|
|
|
|
|
+ console.log('上传响应:', res);
|
|
|
|
|
+ if (res.code === 2000) {
|
|
|
|
|
+ // 获取上传后的视频URL
|
|
|
|
|
+ const videoUrl = res.data.permanent_link || res.data.url || '';
|
|
|
|
|
+ if (videoUrl) {
|
|
|
|
|
+ // 上传成功,更新状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'success';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 提交到面试接口
|
|
|
|
|
+ this.submitVideoToInterview(videoUrl, task);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleUploadFailure(task, '视频URL获取失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleUploadFailure(task, res.msg || '上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.handleUploadFailure(task, '解析响应失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ this.handleUploadFailure(task, err.errMsg || '上传失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听上传进度
|
|
|
|
|
+ uploadTask.onProgressUpdate((res) => {
|
|
|
|
|
+ this.uploadProgress[task.id] = res.progress;
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:处理上传失败
|
|
|
|
|
+ handleUploadFailure(task, errorMsg) {
|
|
|
|
|
+ console.error('上传失败:', errorMsg);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新任务状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'failed';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否还有重试机会
|
|
|
|
|
+ if (task.attempts < task.maxAttempts) {
|
|
|
|
|
+ console.log(`将在5秒后重试上传,当前尝试次数: ${task.attempts}/${task.maxAttempts}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 5秒后重试
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ // 重置进度
|
|
|
|
|
+ this.uploadProgress[task.id] = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 重新开始上传
|
|
|
|
|
+ if (typeof task.file !== 'string') {
|
|
|
|
|
+ this.uploadFileWithXHR(task);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.uploadFileWithUni(task);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 5000);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 超过最大重试次数,移除任务并继续处理队列
|
|
|
|
|
+ console.log('超过最大重试次数,放弃上传');
|
|
|
|
|
+
|
|
|
|
|
+ // 显示错误提示
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '视频上传失败,请稍后重试',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ }); */
|
|
|
|
|
+
|
|
|
|
|
+ // 从队列中移除当前任务
|
|
|
|
|
+ this.uploadQueue.shift();
|
|
|
|
|
+
|
|
|
|
|
+ // 继续处理队列中的下一个任务
|
|
|
|
|
+ this.processUploadQueue();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 submitVideoToInterview 方法
|
|
|
|
|
+ submitVideoToInterview(videoUrl, task) {
|
|
|
|
|
+ // 准备请求参数
|
|
|
|
|
+ const requestData = {
|
|
|
|
|
+ application_id: uni.getStorageSync('appId'),
|
|
|
|
|
+ question_id: task.questionId,
|
|
|
|
|
+ video_url: videoUrl,
|
|
|
|
|
+ tenant_id: JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1',
|
|
|
|
|
+ is_follow_up: task.isFollowUp ? 1 : 0,
|
|
|
|
|
+ question_form: task.questionForm,
|
|
|
|
|
+ question_text: task.questionText
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ console.log('提交视频到面试接口', requestData);
|
|
|
|
|
+
|
|
|
|
|
+ // 发送请求到面试接口
|
|
|
|
|
+ if (task.isFollowUp) {
|
|
|
|
|
+ // 追问视频上传
|
|
|
|
|
+ const followUpRequestData = {
|
|
|
|
|
+ application_id: uni.getStorageSync('appId'),
|
|
|
|
|
+ tenant_id: JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1',
|
|
|
|
|
+ video_url: videoUrl,
|
|
|
|
|
+ original_question_id: this.parentJobPositionQuestionId, // 使用保存的job_position_question_id
|
|
|
|
|
+ follow_up_question: task.questionText,
|
|
|
|
|
+ video_duration: task.videoDuration,
|
|
|
|
|
+ openid: JSON.parse(uni.getStorageSync('userInfo')).openid || ''
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ console.log('准备提交追问视频:', followUpRequestData);
|
|
|
|
|
+
|
|
|
|
|
+ uni.request({
|
|
|
|
|
+ url: `${apiBaseUrl}/voice_interview/upload_follow_up_video/`,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: followUpRequestData,
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'content-type': 'application/x-www-form-urlencoded'
|
|
|
|
|
+ },
|
|
|
|
|
+ success: async (res) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (res.data.code === 200 || res.data.code === 2000) {
|
|
|
|
|
+ if (res.data.data && res.data.data.transcription_status === 'pending') {
|
|
|
|
|
+ // 如果转写状态为pending,5秒后重试
|
|
|
|
|
+ console.log('视频转写进行中,5秒后重试');
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.checkTranscriptionStatus(task, followUpRequestData);
|
|
|
|
|
+ }, 5000);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('追问视频提交成功');
|
|
|
|
|
+ this.isFollowUpMode = false;
|
|
|
|
|
+ this.currentVideoIndex = this.mainQuestionIndex;
|
|
|
|
|
+
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '回答已提交',
|
|
|
|
|
+ icon: 'success',
|
|
|
|
|
+ duration: 1500
|
|
|
|
|
+ }); */
|
|
|
|
|
+
|
|
|
|
|
+ this.uploadQueue.shift();
|
|
|
|
|
+ this.processUploadQueue();
|
|
|
|
|
+
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ if (this.currentVideoIndex < this.videoList.length) {
|
|
|
|
|
+ this.videoUrl = this.videoList[this.currentVideoIndex];
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleSubmitFailure(task, '提交失败: ' + (res.data.msg || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ this.handleSubmitFailure(task, '解析响应失败: ' + e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('提交视频失败:', err);
|
|
|
|
|
+ this.handleSubmitFailure(task, '提交失败: ' + err.errMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 原有的视频上传逻辑
|
|
|
|
|
+ uni.request({
|
|
|
|
|
+ url: `${apiBaseUrl}/api/job/upload_video`,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: requestData,
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'content-type': 'application/x-www-form-urlencoded'
|
|
|
|
|
+ },
|
|
|
|
|
+ success: async (res) => {
|
|
|
|
|
+ if (res.data.code === 200 || res.data.code === 2000) {
|
|
|
|
|
+ console.log('视频提交成功,准备调用面试互动接口');
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 保存job_position_question_id用于后续追问
|
|
|
|
|
+ this.parentJobPositionQuestionId = res.data.data.job_position_question_id;
|
|
|
|
|
+ console.log('保存父问题ID:', this.parentJobPositionQuestionId);
|
|
|
|
|
+ // 初始化该题的追问上限与已问次数
|
|
|
|
|
+ const limitFromServer = res.data.data.follow_up_limit;
|
|
|
|
|
+ if (typeof limitFromServer !== 'undefined') {
|
|
|
|
|
+ this.setFollowUpLimit(this.parentJobPositionQuestionId, limitFromServer);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.setFollowUpLimit(this.parentJobPositionQuestionId, this.defaultFollowUpLimit);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.resetFollowUpAskedCount(this.parentJobPositionQuestionId);
|
|
|
|
|
+
|
|
|
|
|
+ await this.callInterviewInteraction(res.data.data.job_position_question_id);
|
|
|
|
|
+ console.log('面试互动接口调用成功');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('面试互动接口调用失败:', error);
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '回答已提交',
|
|
|
|
|
+ icon: 'success',
|
|
|
|
|
+ duration: 1500
|
|
|
|
|
+ }); */
|
|
|
|
|
+
|
|
|
|
|
+ this.uploadQueue.shift();
|
|
|
|
|
+ this.processUploadQueue();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleSubmitFailure(task, '提交失败: ' + (res.data.msg || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('提交视频失败:', err);
|
|
|
|
|
+ this.handleSubmitFailure(task, '提交失败: ' + err.errMsg);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:处理提交失败
|
|
|
|
|
+ handleSubmitFailure(task, errorMsg) {
|
|
|
|
|
+ console.error('提交失败:', errorMsg);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新任务状态
|
|
|
|
|
+ this.uploadStatus[task.id] = 'failed';
|
|
|
|
|
+ this.updateUploadStatusText();
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否还有重试机会
|
|
|
|
|
+ if (task.attempts < task.maxAttempts) {
|
|
|
|
|
+ console.log(`将在5秒后重试提交,当前尝试次数: ${task.attempts}/${task.maxAttempts}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 5秒后重试
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ // 重新提交
|
|
|
|
|
+ this.submitVideoToInterview(task.videoUrl, task);
|
|
|
|
|
+ }, 5000);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 超过最大重试次数,移除任务并继续处理队列
|
|
|
|
|
+ console.log('超过最大重试次数,放弃提交');
|
|
|
|
|
+
|
|
|
|
|
+ // 显示错误提示
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '视频提交失败,请稍后重试',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ }); */
|
|
|
|
|
+
|
|
|
|
|
+ // 从队列中移除当前任务
|
|
|
|
|
+ this.uploadQueue.shift();
|
|
|
|
|
+
|
|
|
|
|
+ // 继续处理队列中的下一个任务
|
|
|
|
|
+ this.processUploadQueue();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加检查转写状态的方法
|
|
|
|
|
+ checkTranscriptionStatus(task, requestData, retryCount = 0) {
|
|
|
|
|
+ const maxRetries = 24; // 最大重试次数(2分钟)
|
|
|
|
|
+ const retryInterval = 5000; // 重试间隔时间(5秒)
|
|
|
|
|
+
|
|
|
|
|
+ if (retryCount >= maxRetries) {
|
|
|
|
|
+ console.log('转写超时,继续下一个问题');
|
|
|
|
|
+ this.isFollowUpMode = false;
|
|
|
|
|
+ this.currentVideoIndex = this.mainQuestionIndex;
|
|
|
|
|
+
|
|
|
|
|
+ this.uploadQueue.shift();
|
|
|
|
|
+ this.processUploadQueue();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ uni.request({
|
|
|
|
|
+ url: `${apiBaseUrl}/voice_interview/check_transcription_status/`,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ application_id: requestData.application_id,
|
|
|
|
|
+ original_question_id: requestData.original_question_id,
|
|
|
|
|
+ video_url: requestData.video_url
|
|
|
|
|
+ },
|
|
|
|
|
+ header: {
|
|
|
|
|
+ 'content-type': 'application/x-www-form-urlencoded'
|
|
|
|
|
+ },
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.data.code === 200 || res.data.code === 2000) {
|
|
|
|
|
+ if (res.data.data && res.data.data.status === 'completed') {
|
|
|
|
|
+ console.log('视频转写完成');
|
|
|
|
|
+ this.isFollowUpMode = false;
|
|
|
|
|
+ this.currentVideoIndex = this.mainQuestionIndex;
|
|
|
|
|
+
|
|
|
|
|
+ this.uploadQueue.shift();
|
|
|
|
|
+ this.processUploadQueue();
|
|
|
|
|
+
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ if (this.currentVideoIndex < this.videoList.length) {
|
|
|
|
|
+ this.videoUrl = this.videoList[this.currentVideoIndex];
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 状态仍为pending,继续重试
|
|
|
|
|
+ console.log(`转写进行中,${retryInterval/1000}秒后重试 (${retryCount + 1}/${maxRetries})`);
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.checkTranscriptionStatus(task, requestData, retryCount + 1);
|
|
|
|
|
+ }, retryInterval);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (res.data.message === "面试视频尚未完成转写" || res.data.success === false) {
|
|
|
|
|
+ // 特殊处理转写未完成的情况
|
|
|
|
|
+ console.log(`转写未完成,${retryInterval/1000}秒后重试 (${retryCount + 1}/${maxRetries})`);
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.checkTranscriptionStatus(task, requestData, retryCount + 1);
|
|
|
|
|
+ }, retryInterval);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.handleSubmitFailure(task, '检查转写状态失败: ' + (res.data.msg || '未知错误'));
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('检查转写状态失败:', err);
|
|
|
|
|
+ // 网络错误时也进行重试
|
|
|
|
|
+ console.log(`网络请求失败,${retryInterval/1000}秒后重试 (${retryCount + 1}/${maxRetries})`);
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.checkTranscriptionStatus(task, requestData, retryCount + 1);
|
|
|
|
|
+ }, retryInterval);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:更新上传状态文本
|
|
|
|
|
+ updateUploadStatusText() {
|
|
|
|
|
+ // 检查主上传队列
|
|
|
|
|
+ let mainQueueText = '';
|
|
|
|
|
+ if (this.uploadQueue.length > 0) {
|
|
|
|
|
+ const currentTask = this.uploadQueue[0];
|
|
|
|
|
+ const progress = this.uploadProgress[currentTask.id] || 0;
|
|
|
|
|
+ const status = this.uploadStatus[currentTask.id] || 'pending';
|
|
|
|
|
+
|
|
|
|
|
+ let statusText = '';
|
|
|
|
|
+ switch (status) {
|
|
|
|
|
+ case 'pending':
|
|
|
|
|
+ statusText = '等待上传';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'uploading':
|
|
|
|
|
+ statusText = `上传中 ${progress}%`;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'success':
|
|
|
|
|
+ statusText = '上传成功,提交中...';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'failed':
|
|
|
|
|
+ statusText = `上传失败,${currentTask.attempts < currentTask.maxAttempts ? '即将重试' : '已放弃'}`;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 添加问题类型和文本的中文释义
|
|
|
|
|
+ const questionTypeText = currentTask.isFollowUp ? '追问' : '问题';
|
|
|
|
|
+ const questionShortText = currentTask.questionText ?
|
|
|
|
|
+ (currentTask.questionText.length > 10 ? currentTask.questionText.substring(0, 10) + '...' : currentTask.questionText) :
|
|
|
|
|
+ `${questionTypeText}${currentTask.questionId}`;
|
|
|
|
|
+
|
|
|
|
|
+ mainQueueText = `${questionTypeText}「${questionShortText}」:${statusText}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果队列中有多个任务,显示总数
|
|
|
|
|
+ if (this.uploadQueue.length > 1) {
|
|
|
|
|
+ mainQueueText += ` (${this.uploadQueue.length}个视频待处理)`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查后台上传队列
|
|
|
|
|
+ let backgroundQueueText = '';
|
|
|
|
|
+ if (this.backgroundUploadQueue && this.backgroundUploadQueue.length > 0) {
|
|
|
|
|
+ const currentTask = this.backgroundUploadQueue[0];
|
|
|
|
|
+ const progress = this.uploadProgress[currentTask.id] || 0;
|
|
|
|
|
+ const status = this.uploadStatus[currentTask.id] || 'pending';
|
|
|
|
|
+
|
|
|
|
|
+ let statusText = '';
|
|
|
|
|
+ switch (status) {
|
|
|
|
|
+ case 'pending':
|
|
|
|
|
+ statusText = '等待后台上传';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'uploading':
|
|
|
|
|
+ statusText = `后台上传中 ${progress}%`;
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'success':
|
|
|
|
|
+ statusText = '后台上传成功,提交中...';
|
|
|
|
|
+ break;
|
|
|
|
|
+ case 'failed':
|
|
|
|
|
+ statusText = `后台上传失败,${currentTask.attempts < currentTask.maxAttempts ? '即将重试' : '已放弃'}`;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const questionShortText = currentTask.questionText ?
|
|
|
|
|
+ (currentTask.questionText.length > 10 ? currentTask.questionText.substring(0, 10) + '...' : currentTask.questionText) :
|
|
|
|
|
+ `追问${currentTask.questionId}`;
|
|
|
|
|
+
|
|
|
|
|
+ backgroundQueueText = `追问「${questionShortText}」:${statusText}`;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果后台上传队列中有多个任务,显示总数
|
|
|
|
|
+ if (this.backgroundUploadQueue.length > 1) {
|
|
|
|
|
+ backgroundQueueText += ` (${this.backgroundUploadQueue.length}个追问待后台上传)`;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 合并状态文本
|
|
|
|
|
+ if (mainQueueText && backgroundQueueText) {
|
|
|
|
|
+ this.uploadStatusText = `${mainQueueText} | ${backgroundQueueText}`;
|
|
|
|
|
+ } else if (mainQueueText) {
|
|
|
|
|
+ this.uploadStatusText = mainQueueText;
|
|
|
|
|
+ } else if (backgroundQueueText) {
|
|
|
|
|
+ this.uploadStatusText = backgroundQueueText;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.uploadStatusText = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 proceedToNextQuestion 方法,确保在切换视频时重置历史时间
|
|
|
|
|
+ proceedToNextQuestion() {
|
|
|
|
|
+ // 如果正在切换视频,则返回
|
|
|
|
|
+ if (this.isVideoSwitching) {
|
|
|
|
|
+ console.log('正在切换视频中,请等待...');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('继续下一个问题');
|
|
|
|
|
+ this.isVideoSwitching = true; // 设置切换状态锁
|
|
|
|
|
+
|
|
|
|
|
+ // 停止当前视频播放
|
|
|
|
|
+ const currentVideo = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (currentVideo) {
|
|
|
|
|
+ currentVideo.stop();
|
|
|
|
|
+ this.videoPlaying = false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否已经是最后一个视频
|
|
|
|
|
+ if (this.currentVideoIndex + 1 >= this.videoList.length) {
|
|
|
|
|
+ // 所有视频都播放完毕,显示完成页面或返回
|
|
|
|
|
+ console.log('所有视频已播放完毕');
|
|
|
|
|
+
|
|
|
|
|
+ // 清理资源
|
|
|
|
|
+ this.stopUserCamera();
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前职位ID
|
|
|
|
|
+ const currentJobDetail = JSON.parse(uni.getStorageSync('selectedJob'));
|
|
|
|
|
+ console.log('当前职位ID:', currentJobDetail);
|
|
|
|
|
+ const jobId = currentJobDetail ? currentJobDetail.id : null;
|
|
|
|
|
+
|
|
|
|
|
+ this.handleVideoCompletion(jobId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 清理当前父题的追问中的并发锁
|
|
|
|
|
+ if (this.currentParentQuestionId) {
|
|
|
|
|
+ this.followUpRequestInFlight[this.currentParentQuestionId] = false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 增加当前视频索引,切换到下一个视频
|
|
|
|
|
+ this.currentVideoIndex++;
|
|
|
|
|
+
|
|
|
|
|
+ // 重置历史时间,避免触发异常跳转检测
|
|
|
|
|
+ this.historyTime = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果还有视频,播放下一个视频
|
|
|
|
|
+ if (this.currentVideoIndex < this.videoList.length) {
|
|
|
|
|
+ // 确保先更新视频URL
|
|
|
|
|
+ this.videoUrl = this.videoList[this.currentVideoIndex];
|
|
|
|
|
+
|
|
|
|
|
+ // 使用setTimeout确保DOM更新完成
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ this.isVideoSwitching = false; // 重置切换状态锁
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 所有视频都播放完毕,显示完成页面或返回
|
|
|
|
|
+ console.log('所有视频已播放完毕');
|
|
|
|
|
+
|
|
|
|
|
+ // 清理资源
|
|
|
|
|
+ this.stopUserCamera();
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前职位ID
|
|
|
|
|
+ const currentJobDetail = JSON.parse(uni.getStorageSync('selectedJob'));
|
|
|
|
|
+ console.log('当前职位ID:', currentJobDetail);
|
|
|
|
|
+ const jobId = currentJobDetail ? currentJobDetail.id : null;
|
|
|
|
|
+
|
|
|
|
|
+ console.log('当前职位ID:', jobId);
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟后根据职位ID跳转到不同页面
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.isVideoSwitching = false; // 重置切换状态锁
|
|
|
|
|
+ // 如果职位ID为9,跳转到interview-question页面
|
|
|
|
|
+ if (jobId === 9) {
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: '/pages/interview-question/interview-question',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('成功跳转到interview-question页面');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('跳转到interview-question页面失败:', err);
|
|
|
|
|
+ // 如果跳转失败,尝试使用redirectTo
|
|
|
|
|
+ uni.redirectTo({
|
|
|
|
|
+ url: '/pages/interview-question/interview-question',
|
|
|
|
|
+ fail: (redirectErr) => {
|
|
|
|
|
+ console.error('重定向到interview-question页面也失败:', redirectErr);
|
|
|
|
|
+ // 如果所有跳转方式都失败,尝试返回上一页
|
|
|
|
|
+ uni.navigateBack({
|
|
|
|
|
+ delta: 1
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 其他职位ID跳转到camera页面
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: '/pages/camera/camera',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('成功跳转到camera页面');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('跳转到camera页面失败:', err);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果跳转失败,尝试使用redirectTo
|
|
|
|
|
+ uni.redirectTo({
|
|
|
|
|
+ url: '/pages/camera/camera',
|
|
|
|
|
+ fail: (redirectErr) => {
|
|
|
|
|
+ console.error('重定向到camera页面也失败:', redirectErr);
|
|
|
|
|
+
|
|
|
|
|
+ // 最后尝试使用switchTab(如果camera是tabBar页面)
|
|
|
|
|
+ uni.switchTab({
|
|
|
|
|
+ url: '/pages/camera/camera',
|
|
|
|
|
+ fail: (switchErr) => {
|
|
|
|
|
+ console.error('所有跳转方式都失败:', switchErr);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果所有跳转方式都失败,尝试返回上一页
|
|
|
|
|
+ uni.navigateBack({
|
|
|
|
|
+ delta: 1
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 0);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 handleAnswerButtonClick 方法,确保在切换视频时重置历史时间
|
|
|
|
|
+ handleAnswerButtonClick() {
|
|
|
|
|
+ console.log('点击开始回答按钮');
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏开始回答按钮
|
|
|
|
|
+ this.showAnswerButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 增加当前视频索引,切换到下一个视频
|
|
|
|
|
+ this.currentVideoIndex++;
|
|
|
|
|
+
|
|
|
|
|
+ // 重置历史时间,避免触发异常跳转检测
|
|
|
|
|
+ this.historyTime = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果还有视频,播放下一个视频
|
|
|
|
|
+ if (this.currentVideoIndex < this.videoList.length) {
|
|
|
|
|
+ this.videoUrl = this.videoList[this.currentVideoIndex];
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理相机错误
|
|
|
|
|
+ handleCameraError(e) {
|
|
|
|
|
+ console.error('相机错误:', e);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取平台信息
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ const isIOS = systemInfo.platform === 'ios';
|
|
|
|
|
+
|
|
|
|
|
+ if (isIOS) {
|
|
|
|
|
+ console.log('iOS相机错误,尝试重新初始化');
|
|
|
|
|
+
|
|
|
|
|
+ // 显示提示
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '相机初始化中...',
|
|
|
|
|
+ icon: 'loading',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 重置相机
|
|
|
|
|
+ this.resetCamera();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果正在录制,停止录制
|
|
|
|
|
+ if (this.isRecording) {
|
|
|
|
|
+ this.isRecording = false;
|
|
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 提供替代选项
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.useAlternativeRecordingMethod();
|
|
|
|
|
+ }, 1000);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 显示错误提示
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '相机初始化失败,请检查权限设置',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试备用选项
|
|
|
|
|
+ this.tryFallbackOptions();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:尝试备用选项
|
|
|
|
|
+ tryFallbackOptions() {
|
|
|
|
|
+ // 检查环境
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+
|
|
|
|
|
+ // 在小程序环境中使用小程序API
|
|
|
|
|
+ if (systemInfo.uniPlatform === 'mp-weixin' || systemInfo.uniPlatform === 'mp-alipay') {
|
|
|
|
|
+ this.useMiniProgramCamera();
|
|
|
|
|
+ }
|
|
|
|
|
+ // 在H5环境中显示静态图像
|
|
|
|
|
+ else {
|
|
|
|
|
+ this.showStaticCameraPlaceholder();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:使用小程序相机API
|
|
|
|
|
+ useMiniProgramCamera() {
|
|
|
|
|
+ console.log('尝试使用小程序相机组件');
|
|
|
|
|
+ // 这里需要在模板中添加小程序相机组件
|
|
|
|
|
+ // 并设置一个标志来控制显示
|
|
|
|
|
+ this.useMiniProgramCameraComponent = true;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:显示静态图像
|
|
|
|
|
+ showStaticCameraPlaceholder() {
|
|
|
|
|
+ console.log('显示静态摄像头占位图');
|
|
|
|
|
+ // 创建一个图像元素
|
|
|
|
|
+ const img = document.createElement('img');
|
|
|
|
|
+ img.src = '/static/images/camera-placeholder.png'; // 确保有这个图片资源
|
|
|
|
|
+ img.className = 'static-camera-image';
|
|
|
|
|
+ img.style.width = '100%';
|
|
|
|
|
+ img.style.height = '100%';
|
|
|
|
|
+ img.style.objectFit = 'cover';
|
|
|
|
|
+
|
|
|
|
|
+ // 获取容器并添加图像
|
|
|
|
|
+ const container = this.$refs.userCameraVideo.parentNode;
|
|
|
|
|
+ container.appendChild(img);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 handleTimeUpdate 方法
|
|
|
|
|
+ handleTimeUpdate(e) {
|
|
|
|
|
+ // 获取当前视频播放时间
|
|
|
|
|
+ const currentTime = e.detail.currentTime || e.target.currentTime;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果正在录制,更新录制时间显示
|
|
|
|
|
+ if (this.isRecording && this.recordingTimerCount) {
|
|
|
|
|
+ this.recordingTimeDisplay = this.formatTime(this.recordingTimerCount);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果正在播放低分提示视频,使用专门的字幕数组
|
|
|
|
|
+ if (this.isPlayingLowScoreVideo) {
|
|
|
|
|
+ if (this.lowScoreVideoSubtitles && this.lowScoreVideoSubtitles.length > 0) {
|
|
|
|
|
+ const subtitle = this.lowScoreVideoSubtitles[0]; // 使用第一个字幕
|
|
|
|
|
+ this.currentSubtitle = subtitle.text;
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前视频对应的字幕数组
|
|
|
|
|
+ let currentSubtitles = null;
|
|
|
|
|
+
|
|
|
|
|
+ if (this.isFollowUpQuestion && this.currentFollowUpQuestion) {
|
|
|
|
|
+ // 追问问题字幕处理
|
|
|
|
|
+ const subtitleKey = `followUpSubtitles_${this.currentFollowUpQuestion.id}`;
|
|
|
|
|
+ currentSubtitles = this[subtitleKey] || [{
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 30,
|
|
|
|
|
+ text: this.currentFollowUpQuestion.question ||
|
|
|
|
|
+ this.currentFollowUpQuestion.question
|
|
|
|
|
+ }];
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 常规问题字幕处理
|
|
|
|
|
+ if (this.currentVideoIndex === 0) {
|
|
|
|
|
+ currentSubtitles = this.subtitles;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const subtitleArrayName = `question${this.currentVideoIndex}Subtitles`;
|
|
|
|
|
+ currentSubtitles = this[subtitleArrayName];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确保字幕持续显示
|
|
|
|
|
+ if (currentSubtitles && currentSubtitles.length > 0) {
|
|
|
|
|
+ // 找到当前时间对应的字幕
|
|
|
|
|
+ const subtitle = currentSubtitles.find(sub =>
|
|
|
|
|
+ currentTime >= sub.startTime && currentTime <= sub.endTime
|
|
|
|
|
+ ) || currentSubtitles[0]; // 如果找不到对应时间的字幕,使用第一个字幕
|
|
|
|
|
+
|
|
|
|
|
+ // 只显示字幕文本
|
|
|
|
|
+ if (subtitle) {
|
|
|
|
|
+ this.currentSubtitle = subtitle.text;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 更新历史时间和时间戳
|
|
|
|
|
+ this.historyTime = currentTime;
|
|
|
|
|
+ this.lastUpdateTime = Date.now();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // Add a new method to handle the "Start Recording" button click
|
|
|
|
|
+ handleStartRecordingClick() {
|
|
|
|
|
+ // 隐藏"开始回答"按钮
|
|
|
|
|
+ this.showStartRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果倒计时正在进行,清除倒计时
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 重置等待回答状态
|
|
|
|
|
+ this.isWaitingForAnswer = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 直接开始录制
|
|
|
|
|
+ this.startRecordingAnswer();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 checkAudioPermission 方法,确保在录制前获取音频权限
|
|
|
|
|
+ checkAudioPermission() {
|
|
|
|
|
+ // 在小程序环境中
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith('mp-');
|
|
|
|
|
+
|
|
|
|
|
+ if (isMiniProgram) {
|
|
|
|
|
+ // 小程序环境下的权限请求
|
|
|
|
|
+ uni.getSetting({
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (!res.authSetting['scope.record']) {
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.record',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('录音权限已获取');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('录音权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('录音');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!res.authSetting['scope.camera']) {
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.camera',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('相机权限已获取');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('相机权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('相机');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 浏览器环境下的权限请求
|
|
|
|
|
+ // ... 保持原有代码不变
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:测试音频输入
|
|
|
|
|
+ testAudioInput() {
|
|
|
|
|
+ if (!this.cameraStream) {
|
|
|
|
|
+ console.warn('没有可用的媒体流,无法测试音频');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const audioTracks = this.cameraStream.getAudioTracks();
|
|
|
|
|
+ if (audioTracks.length === 0) {
|
|
|
|
|
+ console.warn('没有检测到音频轨道,尝试重新获取');
|
|
|
|
|
+ this.tryGetAudioOnly();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('音频轨道信息:', audioTracks[0].getSettings());
|
|
|
|
|
+
|
|
|
|
|
+ // 创建音频上下文和分析器
|
|
|
|
|
+ try {
|
|
|
|
|
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
|
|
|
+ if (!AudioContext) {
|
|
|
|
|
+ console.warn('浏览器不支持AudioContext');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const audioContext = new AudioContext();
|
|
|
|
|
+ const analyser = audioContext.createAnalyser();
|
|
|
|
|
+ const microphone = audioContext.createMediaStreamSource(this.cameraStream);
|
|
|
|
|
+ microphone.connect(analyser);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置分析器
|
|
|
|
|
+ analyser.fftSize = 256;
|
|
|
|
|
+ const bufferLength = analyser.frequencyBinCount;
|
|
|
|
|
+ const dataArray = new Uint8Array(bufferLength);
|
|
|
|
|
+
|
|
|
|
|
+ // 检测音频输入
|
|
|
|
|
+ let silenceCounter = 0;
|
|
|
|
|
+ const checkAudio = () => {
|
|
|
|
|
+ if (this.isRecording) return; // 如果正在录制,停止检测
|
|
|
|
|
+
|
|
|
|
|
+ analyser.getByteFrequencyData(dataArray);
|
|
|
|
|
+
|
|
|
|
|
+ // 计算平均音量
|
|
|
|
|
+ let sum = 0;
|
|
|
|
|
+ for (let i = 0; i < bufferLength; i++) {
|
|
|
|
|
+ sum += dataArray[i];
|
|
|
|
|
+ }
|
|
|
|
|
+ const average = sum / bufferLength;
|
|
|
|
|
+
|
|
|
|
|
+ if (average > 10) {
|
|
|
|
|
+ console.log('检测到音频输入,音量:', average);
|
|
|
|
|
+ silenceCounter = 0;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ silenceCounter++;
|
|
|
|
|
+ if (silenceCounter > 10) {
|
|
|
|
|
+ console.warn('持续检测不到音频输入,可能麦克风未正常工作');
|
|
|
|
|
+ silenceCounter = 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 继续检测
|
|
|
|
|
+ requestAnimationFrame(checkAudio);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 开始检测
|
|
|
|
|
+ checkAudio();
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('音频测试失败:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:尝试单独获取音频
|
|
|
|
|
+ tryGetAudioOnly() {
|
|
|
|
|
+ navigator.mediaDevices.getUserMedia({ audio: true })
|
|
|
|
|
+ .then(audioStream => {
|
|
|
|
|
+ // 如果已有视频流,合并音频和视频轨道
|
|
|
|
|
+ if (this.cameraStream) {
|
|
|
|
|
+ const videoTrack = this.cameraStream.getVideoTracks()[0];
|
|
|
|
|
+ const audioTrack = audioStream.getAudioTracks()[0];
|
|
|
|
|
+
|
|
|
|
|
+ // 创建新的媒体流,包含视频和音频轨道
|
|
|
|
|
+ const combinedStream = new MediaStream();
|
|
|
|
|
+ if (videoTrack) combinedStream.addTrack(videoTrack);
|
|
|
|
|
+ if (audioTrack) combinedStream.addTrack(audioTrack);
|
|
|
|
|
+
|
|
|
|
|
+ // 更新摄像头流
|
|
|
|
|
+ this.cameraStream = combinedStream;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新视频元素的源
|
|
|
|
|
+ const videoElement = this.$refs.userCameraVideo;
|
|
|
|
|
+ if (videoElement) {
|
|
|
|
|
+ videoElement.srcObject = combinedStream;
|
|
|
|
|
+ videoElement.muted = true; // 避免回声,但仍然录制声音
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('成功合并音频和视频轨道');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('没有视频流可合并');
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(err => {
|
|
|
|
|
+ console.error('单独获取音频失败:', err);
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:显示权限对话框
|
|
|
|
|
+ showPermissionDialog(permissionType) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '需要权限',
|
|
|
|
|
+ content: `请允许使用${permissionType}权限,否则可能影响面试功能`,
|
|
|
|
|
+ confirmText: '去设置',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.confirm) {
|
|
|
|
|
+ uni.openSetting({
|
|
|
|
|
+ success: (settingRes) => {
|
|
|
|
|
+ console.log('设置页面打开成功', settingRes);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加重试上传方法
|
|
|
|
|
+ retryVideoUpload() {
|
|
|
|
|
+ if (this.lastVideoToRetry) {
|
|
|
|
|
+ // 隐藏重试按钮
|
|
|
|
|
+ this.showRetryButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 显示加载提示
|
|
|
|
|
+ uni.showLoading({
|
|
|
|
|
+ title: '正在重新提交...',
|
|
|
|
|
+ mask: true
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 重新提交视频
|
|
|
|
|
+ this.submitVideoToInterview(this.lastVideoToRetry);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '没有可重试的视频',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加一个新方法用于压缩视频
|
|
|
|
|
+ async compressVideo(videoBlob) {
|
|
|
|
|
+ // 如果视频大小小于某个阈值,直接返回原视频
|
|
|
|
|
+ if (videoBlob.size < 5 * 1024 * 1024) { // 小于5MB
|
|
|
|
|
+ return videoBlob;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('开始压缩视频,原始大小:', videoBlob.size);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建一个视频元素来加载视频
|
|
|
|
|
+ const videoElement = document.createElement('video');
|
|
|
|
|
+ videoElement.muted = true;
|
|
|
|
|
+ videoElement.autoplay = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建一个canvas元素用于绘制视频帧
|
|
|
|
|
+ const canvas = document.createElement('canvas');
|
|
|
|
|
+ const ctx = canvas.getContext('2d');
|
|
|
|
|
+
|
|
|
|
|
+ // 设置视频源
|
|
|
|
|
+ videoElement.src = URL.createObjectURL(videoBlob);
|
|
|
|
|
+
|
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
|
+ videoElement.onloadedmetadata = () => {
|
|
|
|
|
+ // 设置canvas尺寸为视频的一半
|
|
|
|
|
+ const width = Math.floor(videoElement.videoWidth / 2);
|
|
|
|
|
+ const height = Math.floor(videoElement.videoHeight / 2);
|
|
|
|
|
+ canvas.width = width;
|
|
|
|
|
+ canvas.height = height;
|
|
|
|
|
+
|
|
|
|
|
+ // 创建MediaRecorder来录制canvas
|
|
|
|
|
+ const stream = canvas.captureStream(15); // 15fps
|
|
|
|
|
+
|
|
|
|
|
+ // 如果原视频有音轨,添加到新流中
|
|
|
|
|
+ if (videoElement.captureStream) {
|
|
|
|
|
+ const originalStream = videoElement.captureStream();
|
|
|
|
|
+ const audioTracks = originalStream.getAudioTracks();
|
|
|
|
|
+ if (audioTracks.length > 0) {
|
|
|
|
|
+ stream.addTrack(audioTracks[0]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 设置较低的比特率
|
|
|
|
|
+ const options = {
|
|
|
|
|
+ mimeType: 'video/webm;codecs=vp8,opus',
|
|
|
|
|
+ audioBitsPerSecond: 64000,
|
|
|
|
|
+ videoBitsPerSecond: 800000 // 800kbps
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const mediaRecorder = new MediaRecorder(stream, options);
|
|
|
|
|
+ const chunks = [];
|
|
|
|
|
+
|
|
|
|
|
+ mediaRecorder.ondataavailable = (e) => {
|
|
|
|
|
+ if (e.data.size > 0) {
|
|
|
|
|
+ chunks.push(e.data);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ mediaRecorder.onstop = () => {
|
|
|
|
|
+ const compressedBlob = new Blob(chunks, { type: 'video/webm' });
|
|
|
|
|
+ console.log('视频压缩完成,压缩后大小:', compressedBlob.size);
|
|
|
|
|
+ resolve(compressedBlob);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ // 开始播放视频并录制
|
|
|
|
|
+ videoElement.onplay = () => {
|
|
|
|
|
+ mediaRecorder.start(10);
|
|
|
|
|
+
|
|
|
|
|
+ // 绘制视频帧到canvas
|
|
|
|
|
+ const drawFrame = () => {
|
|
|
|
|
+ if (videoElement.paused || videoElement.ended) {
|
|
|
|
|
+ mediaRecorder.stop();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ ctx.drawImage(videoElement, 0, 0, width, height);
|
|
|
|
|
+ requestAnimationFrame(drawFrame);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ drawFrame();
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ videoElement.play();
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 checkIOSCameraRecordPermission 方法
|
|
|
|
|
+ checkIOSCameraRecordPermission() {
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ if (systemInfo.platform !== 'ios') return;
|
|
|
|
|
+
|
|
|
|
|
+ // 在iOS上,需要同时请求相机和录音权限
|
|
|
|
|
+ uni.getSetting({
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ // 检查相机权限
|
|
|
|
|
+ if (!res.authSetting['scope.camera']) {
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.camera',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('iOS相机权限已获取');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('iOS相机权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('相机');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查录音权限
|
|
|
|
|
+ if (!res.authSetting['scope.record']) {
|
|
|
|
|
+ uni.authorize({
|
|
|
|
|
+ scope: 'scope.record',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('iOS录音权限已获取');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('iOS录音权限获取失败:', err);
|
|
|
|
|
+ this.showPermissionDialog('录音');
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:检查并修复渲染问题
|
|
|
|
|
+ checkAndFixRenderingIssues() {
|
|
|
|
|
+ // 检查全局对象,防止渲染错误
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 检查是否有全局变量u
|
|
|
|
|
+ if (typeof u !== 'undefined' && u) {
|
|
|
|
|
+ // 检查currentQuestion属性
|
|
|
|
|
+ if (!u.currentQuestion) {
|
|
|
|
|
+ console.log('修复: 创建缺失的currentQuestion对象');
|
|
|
|
|
+ u.currentQuestion = {};
|
|
|
|
|
+ }
|
|
|
|
|
+ // 确保isImportant属性存在
|
|
|
|
|
+ if (u.currentQuestion && typeof u.currentQuestion.isImportant === 'undefined') {
|
|
|
|
|
+ console.log('修复: 设置缺失的isImportant属性');
|
|
|
|
|
+ u.currentQuestion.isImportant = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.log('防御性检查异常:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加格式化时间的辅助方法
|
|
|
|
|
+ formatTime(seconds) {
|
|
|
|
|
+ if (!seconds && seconds !== 0) return '03:30'; // 默认显示03:30
|
|
|
|
|
+ const minutes = Math.floor(seconds / 60);
|
|
|
|
|
+ const remainingSeconds = seconds % 60;
|
|
|
|
|
+ return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加获取问题数据的方法
|
|
|
|
|
+ fetchQuestions() {
|
|
|
|
|
+ this.loading = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 获取tenant_id,如果没有则使用默认值1
|
|
|
|
|
+ const tenant_id = JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1';
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前选择的职位ID
|
|
|
|
|
+ let position_id = 1; // 默认值
|
|
|
|
|
+ try {
|
|
|
|
|
+ const selectedJob = JSON.parse(uni.getStorageSync('selectedJob') || '{}');
|
|
|
|
|
+ if (selectedJob && selectedJob.id) {
|
|
|
|
|
+ position_id = selectedJob.id;
|
|
|
|
|
+ console.log('使用选择的职位ID获取问题:', position_id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('未找到选择的职位信息,使用默认职位ID:', position_id);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('解析职位信息失败:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建完整的请求URL,便于调试
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const requestUrl = `${apiBaseUrl}/api/wechat/open_questions/?position_id=${position_id}&tenant_id=${tenant_id}&question_form=0&_t=${timestamp}`;
|
|
|
|
|
+ console.log('完整的请求URL:', requestUrl);
|
|
|
|
|
+
|
|
|
|
|
+ uni.request({
|
|
|
|
|
+ url: requestUrl,
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.data && res.data.code === 2000 && res.data.data && res.data.data.items) {
|
|
|
|
|
+ console.log('获取问题数据成功:', res.data.data.items);
|
|
|
|
|
+
|
|
|
|
|
+ // 存储问题数据
|
|
|
|
|
+ this.questions = res.data.data.items;
|
|
|
|
|
+
|
|
|
|
|
+ // 处理视频列表和字幕
|
|
|
|
|
+ this.processQuestionData();
|
|
|
|
|
+
|
|
|
|
|
+ // 播放介绍视频
|
|
|
|
|
+ this.playIntroVideo();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('获取问题数据失败:', res.data);
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '获取问题数据失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ /* // 使用默认视频和字幕
|
|
|
|
|
+ this.useDefaultVideosAndSubtitles(); */
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('请求问题数据失败:', err);
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '网络错误,请检查网络连接',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ /* // 使用默认视频和字幕
|
|
|
|
|
+ this.useDefaultVideosAndSubtitles(); */
|
|
|
|
|
+ },
|
|
|
|
|
+ complete: () => {
|
|
|
|
|
+ this.loading = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理问题数据,提取视频URL和字幕
|
|
|
|
|
+ processQuestionData() {
|
|
|
|
|
+ // 清空视频列表和字幕
|
|
|
|
|
+ this.videoList = [];
|
|
|
|
|
+
|
|
|
|
|
+ // 首先添加介绍视频
|
|
|
|
|
+ this.videoList.push(this.introVideoUrl);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取配置数据中的开场白
|
|
|
|
|
+ const configStr = uni.getStorageSync('configData');
|
|
|
|
|
+ let openingSpeech = [];
|
|
|
|
|
+
|
|
|
|
|
+ if (configStr) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const configData = JSON.parse(configStr);
|
|
|
|
|
+ if (configData && configData.digital_human_opening_speech) {
|
|
|
|
|
+ // 检查是否为数组格式
|
|
|
|
|
+ if (Array.isArray(configData.digital_human_opening_speech)) {
|
|
|
|
|
+ // 将配置数据转换为字幕格式
|
|
|
|
|
+ openingSpeech = configData.digital_human_opening_speech.map((item, index, arr) => {
|
|
|
|
|
+ const startTime = index === 0 ? 0 : arr[index - 1].end_time || index * 5;
|
|
|
|
|
+ const endTime = item.end_time || (index + 1) * 5;
|
|
|
|
|
+ return {
|
|
|
|
|
+ startTime,
|
|
|
|
|
+ endTime,
|
|
|
|
|
+ text: item.content
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ } else if (typeof configData.digital_human_opening_speech === 'string') {
|
|
|
|
|
+ // 使用新的方法处理字符串类型的开场白
|
|
|
|
|
+ openingSpeech = this.convertStringToSubtitles(configData.digital_human_opening_speech);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('解析configData失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果成功获取到开场白配置则使用配置的内容,否则使用默认字幕
|
|
|
|
|
+ this.subtitles = openingSpeech.length > 0 ? openingSpeech : [
|
|
|
|
|
+ {
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 5,
|
|
|
|
|
+ text: '你好,我是本次面试的面试官,欢迎参加本公司的线上面试!'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ startTime: 5,
|
|
|
|
|
+ endTime: 13,
|
|
|
|
|
+ text: '面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ startTime: 13,
|
|
|
|
|
+ endTime: 20,
|
|
|
|
|
+ text: '以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。'
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|
|
|
|
|
+ console.log('处理后的字幕数据:', this.subtitles);
|
|
|
|
|
+ // 按照sequence_number排序问题
|
|
|
|
|
+ const sortedQuestions = [...this.questions].sort((a, b) => a.sequence_number - b.sequence_number);
|
|
|
|
|
+
|
|
|
|
|
+ // 只处理前5个问题(或者更少,如果问题总数不足5个)
|
|
|
|
|
+ const questionsToUse = sortedQuestions.slice(0, 5);
|
|
|
|
|
+
|
|
|
|
|
+ // 为每个问题添加视频URL和字幕
|
|
|
|
|
+ questionsToUse.forEach((question, index) => {
|
|
|
|
|
+ if (question.digital_human_video_url) {
|
|
|
|
|
+ this.videoList.push(question.digital_human_video_url);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建字幕映射,只保留文本
|
|
|
|
|
+ const subtitleArray = [{
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 30, // 延长字幕显示时间到30秒
|
|
|
|
|
+ text: question.question || question.question
|
|
|
|
|
+ }];
|
|
|
|
|
+
|
|
|
|
|
+ const videoIndex = this.videoList.length - 1;
|
|
|
|
|
+ this[`question${videoIndex}Subtitles`] = subtitleArray;
|
|
|
|
|
+
|
|
|
|
|
+ // 存储字幕映射
|
|
|
|
|
+ this.subtitleMap[question.digital_human_video_url] = subtitleArray;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有获取到任何问题视频,使用默认视频
|
|
|
|
|
+ if (this.videoList.length <= 1) {
|
|
|
|
|
+ /* this.useDefaultVideosAndSubtitles(); */
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('处理后的视频列表:', this.videoList);
|
|
|
|
|
+ console.log('处理后的字幕:', this.subtitles);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 将字符串类型的文本转换为字幕格式
|
|
|
|
|
+ convertStringToSubtitles(text) {
|
|
|
|
|
+ if (!text || typeof text !== 'string') {
|
|
|
|
|
+ return [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 按照标点符号分割文本,保留标点符号
|
|
|
|
|
+ const sentences = text.split(/([。!?!?])/)
|
|
|
|
|
+ .filter(part => part.trim() !== '')
|
|
|
|
|
+ .reduce((acc, part, index, arr) => {
|
|
|
|
|
+ if (index % 2 === 0) {
|
|
|
|
|
+ // 文本部分
|
|
|
|
|
+ const nextPart = arr[index + 1] || '';
|
|
|
|
|
+ acc.push(part + nextPart);
|
|
|
|
|
+ }
|
|
|
|
|
+ return acc;
|
|
|
|
|
+ }, [])
|
|
|
|
|
+ .filter(sentence => sentence.trim().length > 0);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有明显的分句标点,按长度分割
|
|
|
|
|
+ if (sentences.length <= 1) {
|
|
|
|
|
+ const maxLength = 30; // 每段最大字符数
|
|
|
|
|
+ const textParts = [];
|
|
|
|
|
+ for (let i = 0; i < text.length; i += maxLength) {
|
|
|
|
|
+ textParts.push(text.substring(i, i + maxLength));
|
|
|
|
|
+ }
|
|
|
|
|
+ sentences.splice(0, sentences.length, ...textParts);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 为每个句子分配时间戳
|
|
|
|
|
+ let currentStartTime = 0;
|
|
|
|
|
+ return sentences.map((sentence) => {
|
|
|
|
|
+ const duration = Math.max(3, Math.ceil(sentence.length / 6)); // 根据文本长度计算时长
|
|
|
|
|
+ const startTime = currentStartTime;
|
|
|
|
|
+ const endTime = startTime + duration;
|
|
|
|
|
+ currentStartTime = endTime;
|
|
|
|
|
+ return {
|
|
|
|
|
+ startTime,
|
|
|
|
|
+ endTime,
|
|
|
|
|
+ text: sentence.trim()
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 使用默认视频和字幕(作为备用)
|
|
|
|
|
+ useDefaultVideosAndSubtitles() {
|
|
|
|
|
+ console.log('使用默认视频和字幕');
|
|
|
|
|
+
|
|
|
|
|
+ // 设置默认字幕
|
|
|
|
|
+ this.subtitles = [
|
|
|
|
|
+ {
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 5,
|
|
|
|
|
+ text: '你好,我是本次面试的面试官,欢迎参加本公司的线上面试!'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ startTime: 5,
|
|
|
|
|
+ endTime: 13,
|
|
|
|
|
+ text: '面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试'
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ startTime: 13,
|
|
|
|
|
+ endTime: 20,
|
|
|
|
|
+ text: '以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。'
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ // 设置默认问题字幕
|
|
|
|
|
+ this.question1Subtitles = [{
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 10,
|
|
|
|
|
+ text: '请结合您的基本信息与过往履历进行简单的自我介绍,并讲一讲您有哪些优势胜任本岗位:'
|
|
|
|
|
+ }];
|
|
|
|
|
+
|
|
|
|
|
+ this.question2Subtitles = [{
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 4,
|
|
|
|
|
+ text: '在工作中,你如何确保个人防护装备的正确使用?'
|
|
|
|
|
+ }];
|
|
|
|
|
+
|
|
|
|
|
+ this.question3Subtitles = [{
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 4,
|
|
|
|
|
+ text: '描述一次你与团队合作改善生产流程的经历。'
|
|
|
|
|
+ }];
|
|
|
|
|
+
|
|
|
|
|
+ this.question4Subtitles = [{
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 6,
|
|
|
|
|
+ text: '你在团队合作中曾遇到过哪些挑战?如何解决团队内部的分歧?'
|
|
|
|
|
+ }];
|
|
|
|
|
+
|
|
|
|
|
+ this.question5Subtitles = [{
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 5,
|
|
|
|
|
+ text: '您已完成本次面试全部题目,请问您对于这个岗位还有什么想要了解的吗?'
|
|
|
|
|
+ }];
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 播放介绍视频
|
|
|
|
|
+ playIntroVideo() {
|
|
|
|
|
+ // 设置第一个视频(介绍视频)
|
|
|
|
|
+ this.videoUrl = this.videoList[0];
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:获取默认问题ID
|
|
|
|
|
+ getDefaultQuestionId(videoIndex) {
|
|
|
|
|
+ // 根据视频索引映射到默认问题ID
|
|
|
|
|
+ switch(videoIndex) {
|
|
|
|
|
+ case 1: // 第一个问题
|
|
|
|
|
+ return 10;
|
|
|
|
|
+ case 2: // 第二个问题
|
|
|
|
|
+ return 11;
|
|
|
|
|
+ case 3: // 第三个问题
|
|
|
|
|
+ return 12;
|
|
|
|
|
+ case 4: // 第四个问题
|
|
|
|
|
+ return 13;
|
|
|
|
|
+ default:
|
|
|
|
|
+ return 10; // 默认值
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:根据索引获取当前问题
|
|
|
|
|
+ getCurrentQuestionByIndex(videoIndex) {
|
|
|
|
|
+ // 视频索引减1,因为第一个视频是介绍视频
|
|
|
|
|
+ const questionIndex = videoIndex - 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 按照sequence_number排序问题
|
|
|
|
|
+ const sortedQuestions = [...this.questions].sort((a, b) => a.sequence_number - b.sequence_number);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查索引是否有效
|
|
|
|
|
+ if (questionIndex >= 0 && questionIndex < sortedQuestions.length) {
|
|
|
|
|
+ return sortedQuestions[questionIndex];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return null;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:获取当前问题的推荐录制时长
|
|
|
|
|
+ getCurrentQuestionRecommendedDuration() {
|
|
|
|
|
+ const currentQuestion = this.getCurrentQuestionByIndex(this.currentVideoIndex);
|
|
|
|
|
+ if (currentQuestion && currentQuestion.recommended_duration) {
|
|
|
|
|
+ // 如果问题有推荐时长,使用推荐时长(转换为秒)
|
|
|
|
|
+ const recommendedDuration = parseInt(currentQuestion.recommended_duration);
|
|
|
|
|
+ console.log(`使用问题推荐时长: ${recommendedDuration}秒`);
|
|
|
|
|
+ return recommendedDuration;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果没有推荐时长,使用默认值300秒(5分钟)
|
|
|
|
|
+ console.log('使用默认录制时长: 300秒');
|
|
|
|
|
+ return 300;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:重置录制状态,准备重新回答
|
|
|
|
|
+ resetForRerecording() {
|
|
|
|
|
+ console.log('重置录制状态,准备重新回答');
|
|
|
|
|
+
|
|
|
|
|
+ // 重置录制状态
|
|
|
|
|
+ this.isRecording = false;
|
|
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 显示"开始回答"按钮
|
|
|
|
|
+ this.showStartRecordingButton = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 清除倒计时
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 重置录制计时器
|
|
|
|
|
+ if (this.recordingTimer) {
|
|
|
|
|
+ clearInterval(this.recordingTimer);
|
|
|
|
|
+ this.recordingTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置录制时间显示
|
|
|
|
|
+ this.recordingTimerCount = 0;
|
|
|
|
|
+ this.recordingTimeDisplay = '00:00 ';/* / 05:00 */
|
|
|
|
|
+
|
|
|
|
|
+ // 重新获取当前问题的推荐录制时长
|
|
|
|
|
+ this.maxRecordingTime = this.getCurrentQuestionRecommendedDuration();
|
|
|
|
|
+ this.remainingTime = this.maxRecordingTime;
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是浏览器环境,停止MediaRecorder
|
|
|
|
|
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
|
|
|
+ this.mediaRecorder.stop();
|
|
|
|
|
+ this.recordedChunks = [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果是小程序环境,停止相机录制
|
|
|
|
|
+ if (this.cameraContext) {
|
|
|
|
|
+ this.cameraContext.stopRecord({
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('相机录制已停止');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('停止相机录制失败:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置问题重试次数映射
|
|
|
|
|
+ this.questionRetryMap = {};
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 playLowScoreVideo 方法,优化字幕显示并添加重试次数限制
|
|
|
|
|
+ playLowScoreVideo() {
|
|
|
|
|
+ console.log('播放低分提示视频');
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前问题的重试次数
|
|
|
|
|
+ const currentQuestionId = this.getCurrentQuestionByIndex(this.currentVideoIndex)?.id;
|
|
|
|
|
+ if (!currentQuestionId) {
|
|
|
|
|
+ console.error('无法获取当前问题ID');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化或增加重试次数
|
|
|
|
|
+ if (!this.questionRetryMap[currentQuestionId]) {
|
|
|
|
|
+ this.questionRetryMap[currentQuestionId] = 1;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.questionRetryMap[currentQuestionId]++;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否超过最大重试次数
|
|
|
|
|
+ if (this.questionRetryMap[currentQuestionId] > this.maxQuestionRetries) {
|
|
|
|
|
+ console.log(`问题 ${currentQuestionId} 已超过最大重试次数,自动进入下一题`);
|
|
|
|
|
+ // 重置低分视频状态
|
|
|
|
|
+ this.isPlayingLowScoreVideo = false;
|
|
|
|
|
+ // 重置追问相关状态
|
|
|
|
|
+ this.isFollowUpQuestion = false;
|
|
|
|
|
+ this.currentFollowUpQuestion = null;
|
|
|
|
|
+ this.followUpQuestion = '';
|
|
|
|
|
+ this.parentQuestion = '';
|
|
|
|
|
+ // 增加视频索引,进入下一题
|
|
|
|
|
+ this.currentVideoIndex++;
|
|
|
|
|
+ // 检查是否还有下一题
|
|
|
|
|
+ if (this.currentVideoIndex < this.videoList.length) {
|
|
|
|
|
+ // 获取下一题的字幕
|
|
|
|
|
+ const nextQuestion = this.getCurrentQuestionByIndex(this.currentVideoIndex);
|
|
|
|
|
+ if (nextQuestion) {
|
|
|
|
|
+ this.currentSubtitle = nextQuestion.question || nextQuestion.question;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 播放下一题的视频
|
|
|
|
|
+ this.videoUrl = this.videoList[this.currentVideoIndex];
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 如果没有下一题,结束面试
|
|
|
|
|
+ this.finishInterview();
|
|
|
|
|
+ }
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 标记正在播放低分提示视频
|
|
|
|
|
+ this.isPlayingLowScoreVideo = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏其他按钮
|
|
|
|
|
+ this.showStartRecordingButton = false;
|
|
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
|
|
+ this.showRerecordButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置视频URL
|
|
|
|
|
+ this.videoUrl = this.lowScoreVideoUrl;
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 保存当前问题的字幕
|
|
|
|
|
+ const currentQuestion = this.getCurrentQuestionByIndex(this.currentVideoIndex);
|
|
|
|
|
+ const originalSubtitle = currentQuestion ? (currentQuestion.question || currentQuestion.question) : this.currentSubtitle;
|
|
|
|
|
+
|
|
|
|
|
+ // 清除现有字幕
|
|
|
|
|
+ this.currentSubtitle = '';
|
|
|
|
|
+
|
|
|
|
|
+ // 使用 nextTick 确保 DOM 更新后再播放视频
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+
|
|
|
|
|
+ // 视频开始播放后短暂延迟显示字幕,确保用户注意到
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ // 设置低分提示视频的字幕
|
|
|
|
|
+ this.currentSubtitle = '我未听清楚您在说什么,请您再说一遍!';
|
|
|
|
|
+
|
|
|
|
|
+ // 创建专门的字幕数组用于低分提示视频
|
|
|
|
|
+ this.lowScoreVideoSubtitles = [
|
|
|
|
|
+ {
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 30,
|
|
|
|
|
+ text: '我未听清楚您在说什么,请您再说一遍!',
|
|
|
|
|
+ translation: 'I didn\'t quite catch what you said, could you please repeat it?'
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 保存原始字幕信息到实例属性,以便在视频结束时恢复
|
|
|
|
|
+ this.originalQuestionSubtitle = originalSubtitle;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:处理重新录制按钮点击
|
|
|
|
|
+ handleRerecordButtonClick() {
|
|
|
|
|
+ console.log('点击重新录制按钮');
|
|
|
|
|
+
|
|
|
|
|
+ // 增加重试计数
|
|
|
|
|
+ this.retryCount++;
|
|
|
|
|
+ console.log(`当前重试次数: ${this.retryCount}/${this.maxRetryAttempts}`);
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏重新录制按钮
|
|
|
|
|
+ this.showRerecordButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 直接调用开始录制的方法
|
|
|
|
|
+ this.handleStartRecordingClick();
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 页面卸载时关闭WebSocket连接
|
|
|
|
|
+ onUnload() {
|
|
|
|
|
+ console.log('页面卸载,关闭WebSocket连接');
|
|
|
|
|
+ this.cleanupPersonDetectionWebSocket();
|
|
|
|
|
+ // 清理其他资源
|
|
|
|
|
+ this.stopUserCamera();
|
|
|
|
|
+ this.clearCountdown();
|
|
|
|
|
+
|
|
|
|
|
+ // 清理录制相关定时器
|
|
|
|
|
+ if (this.recordingTimer) {
|
|
|
|
|
+ clearInterval(this.recordingTimer);
|
|
|
|
|
+ this.recordingTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 停止MediaRecorder
|
|
|
|
|
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
|
|
|
+ this.mediaRecorder.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 关闭屏幕常亮
|
|
|
|
|
+ uni.setKeepScreenOn({
|
|
|
|
|
+ keepScreenOn: false
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 页面隐藏时关闭WebSocket连接
|
|
|
|
|
+ onHide() {
|
|
|
|
|
+ console.log('页面隐藏,关闭WebSocket连接');
|
|
|
|
|
+
|
|
|
|
|
+ // 完全关闭WebSocket连接和相关定时器
|
|
|
|
|
+ this.cleanupPersonDetectionWebSocket();
|
|
|
|
|
+
|
|
|
|
|
+ // 如果正在录制,给用户提示
|
|
|
|
|
+ if (this.isRecording) {
|
|
|
|
|
+ console.log('页面隐藏时正在录制,录制将继续');
|
|
|
|
|
+ // 可以选择暂停录制或者给用户提示
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请不要离开面试页面',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 页面显示时重新初始化WebSocket连接
|
|
|
|
|
+ onShow() {
|
|
|
|
|
+ console.log('identity-verify页面onShow');
|
|
|
|
|
+ console.log('当前本地存储中的职位信息:', uni.getStorageSync('selectedJob'));
|
|
|
|
|
+
|
|
|
|
|
+ // 每次页面显示时重新获取问题
|
|
|
|
|
+ this.fetchQuestions();
|
|
|
|
|
+ // this.fetchFollowUpQuestions(); // 添加获取追问问题的调用
|
|
|
|
|
+
|
|
|
|
|
+ // 重新初始化WebSocket连接(仅在小程序环境中)
|
|
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
|
|
+ const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith('mp-');
|
|
|
|
|
+ if (isMiniProgram) {
|
|
|
|
|
+ // 如果WebSocket未连接,重新初始化
|
|
|
|
|
+ if (!this.personDetectionSocket) {
|
|
|
|
|
+ console.log('重新初始化WebSocket连接');
|
|
|
|
|
+ this.initPersonDetectionWebSocket();
|
|
|
|
|
+ }
|
|
|
|
|
+ // 如果WebSocket已连接但检测定时器被停止,重新启动检测
|
|
|
|
|
+ else if (!this.personDetectionInterval) {
|
|
|
|
|
+ console.log('重新启动人脸检测定时器');
|
|
|
|
|
+ this.startPersonDetectionInterval();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.hideHomeButton(); // 隐藏左上角返回按钮
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加新方法:阻止视频控制
|
|
|
|
|
+ preventVideoControl(e) {
|
|
|
|
|
+ // 阻止默认行为和事件冒泡
|
|
|
|
|
+ e.preventDefault();
|
|
|
|
|
+ e.stopPropagation();
|
|
|
|
|
+ console.log('阻止视频控制操作');
|
|
|
|
|
+ return false;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加获取追问问题的方法
|
|
|
|
|
+ fetchFollowUpQuestions() {
|
|
|
|
|
+ // 获取tenant_id,如果没有则使用默认值1
|
|
|
|
|
+ const tenant_id = JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1';
|
|
|
|
|
+
|
|
|
|
|
+ // 获取当前选择的职位ID
|
|
|
|
|
+ let position_id = 1; // 默认值
|
|
|
|
|
+ try {
|
|
|
|
|
+ const selectedJob = JSON.parse(uni.getStorageSync('selectedJob') || '{}');
|
|
|
|
|
+ if (selectedJob && selectedJob.id) {
|
|
|
|
|
+ position_id = selectedJob.id;
|
|
|
|
|
+ console.log('使用选择的职位ID获取追问问题:', position_id);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.warn('未找到选择的职位信息,使用默认职位ID:', position_id);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('解析职位信息失败:', e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 构建完整的请求URL,便于调试
|
|
|
|
|
+ const timestamp = Date.now();
|
|
|
|
|
+ const requestUrl = `${apiBaseUrl}/api/wechat/open_questions/?position_id=${position_id}&tenant_id=${tenant_id}&question_form=5&_t=${timestamp}`;
|
|
|
|
|
+ console.log('获取追问问题的请求URL:', requestUrl);
|
|
|
|
|
+
|
|
|
|
|
+ uni.request({
|
|
|
|
|
+ url: requestUrl,
|
|
|
|
|
+ method: 'GET',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ if (res.data && res.data.code === 2000 && res.data.data && res.data.data.items) {
|
|
|
|
|
+ console.log('获取追问问题数据成功:', res.data.data.items);
|
|
|
|
|
+
|
|
|
|
|
+ // 存储追问问题数据,并确保每个问题都有question_form=5标记
|
|
|
|
|
+ this.followUpQuestions = res.data.data.items.map(item => ({
|
|
|
|
|
+ ...item,
|
|
|
|
|
+ question_form: 5 // 确保每个追问问题都有正确的question_form值
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // 处理追问问题数据,建立与父问题的关联
|
|
|
|
|
+ this.processFollowUpQuestions();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.error('获取追问问题数据失败:', res.data);
|
|
|
|
|
+ // 初始化为空数组,避免undefined错误
|
|
|
|
|
+ this.followUpQuestions = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('请求追问问题数据失败:', err);
|
|
|
|
|
+ // 初始化为空数组,避免undefined错误
|
|
|
|
|
+ this.followUpQuestions = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 改进处理追问问题数据的方法
|
|
|
|
|
+ processFollowUpQuestions() {
|
|
|
|
|
+ if (!this.followUpQuestions || this.followUpQuestions.length === 0) {
|
|
|
|
|
+ console.log('没有追问问题数据');
|
|
|
|
|
+ this.followUpQuestions = []; // 确保是数组
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('处理追问问题数据,总数:', this.followUpQuestions.length);
|
|
|
|
|
+
|
|
|
|
|
+ // 过滤掉没有parent_question_id的追问问题
|
|
|
|
|
+ this.followUpQuestions = this.followUpQuestions.filter(q => q.parent_question_id);
|
|
|
|
|
+
|
|
|
|
|
+ // 可以在这里进行额外的数据处理,如按照parent_question_id分组等
|
|
|
|
|
+ const parentQuestionIds = new Set();
|
|
|
|
|
+ this.followUpQuestions.forEach(question => {
|
|
|
|
|
+ if (question.parent_question_id) {
|
|
|
|
|
+ parentQuestionIds.add(question.parent_question_id);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ console.log('有追问的父问题ID:', Array.from(parentQuestionIds));
|
|
|
|
|
+
|
|
|
|
|
+ // 初始化已播放追问记录对象
|
|
|
|
|
+ this.hasPlayedFollowUp = {};
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改检查并播放追问问题的方法
|
|
|
|
|
+ checkAndPlayFollowUpQuestion(parentQuestionId) {
|
|
|
|
|
+ console.log('检查是否有追问问题,父问题ID:', parentQuestionId);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果正在等待用户回答,不进行任何操作
|
|
|
|
|
+ if (this.isWaitingForAnswer) {
|
|
|
|
|
+ console.log('正在等待用户回答,暂不处理追问');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 超过追问上限则直接进入下一题
|
|
|
|
|
+ if (!this.canAskMoreFollowUps(parentQuestionId)) {
|
|
|
|
|
+ console.log('追问次数已达上限,进入下一题');
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!parentQuestionId) {
|
|
|
|
|
+ console.warn('没有父问题ID,无法检查追问问题');
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果已经播放过这个问题的追问,则直接进入下一个问题
|
|
|
|
|
+ if (this.hasPlayedFollowUp[parentQuestionId]) {
|
|
|
|
|
+ console.log('已经播放过此问题的追问,直接进入下一个问题');
|
|
|
|
|
+ this.retryCount = 0;
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 查找对应的追问问题
|
|
|
|
|
+ const followUpQuestion = this.followUpQuestions.find(q =>
|
|
|
|
|
+ q.parent_question_id === parentQuestionId &&
|
|
|
|
|
+ q.digital_human_video_url &&
|
|
|
|
|
+ q.question_form === 5
|
|
|
|
|
+ );
|
|
|
|
|
+
|
|
|
|
|
+ if (followUpQuestion) {
|
|
|
|
|
+ console.log('找到追问问题:', followUpQuestion);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查录制时长
|
|
|
|
|
+ const recordingDuration = this.getRecordingDuration();
|
|
|
|
|
+ const lowScoreDuration = 7; // 低分阈值时长(秒),少于7秒视为低分
|
|
|
|
|
+ const minDuration = 3; // 最小录制时长阈值(秒)
|
|
|
|
|
+
|
|
|
|
|
+ // 如果录制时间少于最小时长或者在最小时长和低分阈值之间,播放"未听清楚"提示视频
|
|
|
|
|
+ if (recordingDuration < lowScoreDuration) {
|
|
|
|
|
+ let message = recordingDuration < minDuration ?
|
|
|
|
|
+ `录制时间 ${recordingDuration} 秒,少于最小时长 ${minDuration} 秒` :
|
|
|
|
|
+ `录制时间 ${recordingDuration} 秒,少于标准时长 ${lowScoreDuration} 秒`;
|
|
|
|
|
+ console.log(message + ',将播放低分提示视频');
|
|
|
|
|
+
|
|
|
|
|
+ // 增加重试次数
|
|
|
|
|
+ this.retryCount++;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置需要播放低分提示视频
|
|
|
|
|
+ this.needPlayLowScoreVideo = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 播放低分提示视频
|
|
|
|
|
+ this.playLowScoreVideo();
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果录制时长足够,重置重试次数
|
|
|
|
|
+ this.retryCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ // 标记此追问已播放
|
|
|
|
|
+ this.hasPlayedFollowUp[parentQuestionId] = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 标记当前是追问问题
|
|
|
|
|
+ this.isFollowUpQuestion = true;
|
|
|
|
|
+
|
|
|
|
|
+ // 记录当前父问题ID
|
|
|
|
|
+ this.currentParentQuestionId = parentQuestionId;
|
|
|
|
|
+
|
|
|
|
|
+ // 保存当前追问问题的完整信息
|
|
|
|
|
+ this.currentFollowUpQuestion = followUpQuestion;
|
|
|
|
|
+
|
|
|
|
|
+ // 播放追问问题视频
|
|
|
|
|
+ this.playFollowUpQuestionVideo(followUpQuestion);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ console.log('没有找到对应的追问问题,继续下一个问题');
|
|
|
|
|
+ this.retryCount = 0;
|
|
|
|
|
+ this.proceedToNextQuestion();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改播放追问问题视频的方法
|
|
|
|
|
+ playFollowUpQuestionVideo(followUpQuestion) {
|
|
|
|
|
+ console.log('播放追问问题视频:', followUpQuestion.digital_human_video_url);
|
|
|
|
|
+
|
|
|
|
|
+ // 保存父问题内容
|
|
|
|
|
+ this.parentQuestion = this.currentSubtitle;
|
|
|
|
|
+
|
|
|
|
|
+ this.videoUrl = followUpQuestion.digital_human_video_url;
|
|
|
|
|
+ this.videoPlaying = true;
|
|
|
|
|
+ this.isFollowUpQuestion = true;
|
|
|
|
|
+ this.currentFollowUpQuestion = followUpQuestion;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置追问问题的字幕
|
|
|
|
|
+ const followUpSubtitles = [{
|
|
|
|
|
+ startTime: 0,
|
|
|
|
|
+ endTime: 60, // 延长显示时间到60秒
|
|
|
|
|
+ text: followUpQuestion.question || followUpQuestion.question
|
|
|
|
|
+ }];
|
|
|
|
|
+
|
|
|
|
|
+ const subtitleKey = `followUpSubtitles_${followUpQuestion.id}`;
|
|
|
|
|
+ this[subtitleKey] = followUpSubtitles;
|
|
|
|
|
+
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
|
|
+ if (videoContext) {
|
|
|
|
|
+ videoContext.play();
|
|
|
|
|
+
|
|
|
|
|
+ // 更新当前字幕为追问内容
|
|
|
|
|
+ this.currentSubtitle = followUpSubtitles[0].text;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 添加作废面试的方法
|
|
|
|
|
+ invalidateInterview() {
|
|
|
|
|
+ // 停止录制(如果正在录制)
|
|
|
|
|
+ if (this.isRecording) {
|
|
|
|
|
+ this.stopRecordingAnswer();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 清除所有计时器
|
|
|
|
|
+ if (this.recordingTimer) {
|
|
|
|
|
+ clearInterval(this.recordingTimer);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 重置状态
|
|
|
|
|
+ this.isRecording = false;
|
|
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
|
|
+ this.showStartRecordingButton = false;
|
|
|
|
|
+
|
|
|
|
|
+ // 显示作废提示
|
|
|
|
|
+ // uni.showToast({
|
|
|
|
|
+ // title: 'Interview invalidated',
|
|
|
|
|
+ // icon: 'none',
|
|
|
|
|
+ // duration: 2000
|
|
|
|
|
+ // });
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟后返回上一页
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ uni.switchTab({
|
|
|
|
|
+ url: '/pages/index/index'
|
|
|
|
|
+ });
|
|
|
|
|
+ }, 500);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ initPersonDetectionWebSocket() {
|
|
|
|
|
+ if (this.personDetectionSocket) {
|
|
|
|
|
+ this.cleanupPersonDetectionWebSocket();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.personDetectionSocket = uni.connectSocket({
|
|
|
|
|
+ url: `${personDetectionWsUrl}/ws/interview-room/room_${uni.getStorageSync('appId')}/${uni.getStorageSync('appId')}/`,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('WebSocket connection initiated');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ console.error('WebSocket connection failed:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.personDetectionSocket.onOpen(() => {
|
|
|
|
|
+ console.log('WebSocket connection opened');
|
|
|
|
|
+ this.startPersonDetectionInterval();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.personDetectionSocket.onError((error) => {
|
|
|
|
|
+ console.error('WebSocket error:', error);
|
|
|
|
|
+ this.cleanupPersonDetectionWebSocket();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.personDetectionSocket.onClose(() => {
|
|
|
|
|
+ console.log('WebSocket connection closed');
|
|
|
|
|
+ this.cleanupPersonDetectionWebSocket();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ this.personDetectionSocket.onMessage((res) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = JSON.parse(res.data);
|
|
|
|
|
+ /* console.log('data1', data); */
|
|
|
|
|
+ if (data.type === 'person_detection_result') {
|
|
|
|
|
+ this.handlePersonDetectionResult(data);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error parsing WebSocket message:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error initializing WebSocket:', error);
|
|
|
|
|
+ this.cleanupPersonDetectionWebSocket();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ startPersonDetectionInterval() {
|
|
|
|
|
+ if (this.personDetectionInterval) {
|
|
|
|
|
+ clearInterval(this.personDetectionInterval);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.personDetectionInterval = setInterval(() => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!this.personDetectionSocket || !this.cameraContext) {
|
|
|
|
|
+ console.warn('人脸检测:相机上下文或WebSocket连接未就绪');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.cameraContext.takePhoto({
|
|
|
|
|
+ quality: 'low',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const tempFilePath = res.tempImagePath;
|
|
|
|
|
+ if (!tempFilePath) {
|
|
|
|
|
+ console.warn('人脸检测:未获取到有效的图片路径');
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+ uni.getFileSystemManager().readFile({
|
|
|
|
|
+ filePath: tempFilePath,
|
|
|
|
|
+ encoding: 'base64',
|
|
|
|
|
+ success: (res) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const base64Image = res.data;
|
|
|
|
|
+ if (!this.personDetectionSocket || this.personDetectionSocket.readyState !== 1) {
|
|
|
|
|
+ console.warn('人脸检测:WebSocket连接已断开或未就绪');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ this.personDetectionSocket.send({
|
|
|
|
|
+ data: JSON.stringify({
|
|
|
|
|
+ type: 'person_detection',
|
|
|
|
|
+ image_data: base64Image
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (wsError) {
|
|
|
|
|
+ console.error('人脸检测:发送WebSocket数据时出错:', wsError);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ console.error('人脸检测:读取图片文件失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (fileError) {
|
|
|
|
|
+ console.error('人脸检测:处理图片文件时出错:', fileError);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (error) => {
|
|
|
|
|
+ console.error('人脸检测:拍照失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (mainError) {
|
|
|
|
|
+ console.error('人脸检测:主流程执行出错:', mainError);
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 5000);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ cleanupPersonDetectionWebSocket() {
|
|
|
|
|
+ if (this.personDetectionInterval) {
|
|
|
|
|
+ clearInterval(this.personDetectionInterval);
|
|
|
|
|
+ this.personDetectionInterval = null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (this.personDetectionSocket) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ this.personDetectionSocket.close();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error closing WebSocket:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ this.personDetectionSocket = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ handlePersonDetectionResult(data) {
|
|
|
|
|
+ console.log('data', data);
|
|
|
|
|
+ /* console.log(data.data.detection.has_person);
|
|
|
|
|
+ console.log(data.data.identity.status);//identity_verified */
|
|
|
|
|
+
|
|
|
|
|
+ /* // 首先检查是否有人
|
|
|
|
|
+ if (!data.data.detection.has_person) {
|
|
|
|
|
+ this.showPageWarning = true;
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '请勿离开摄像头',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 3000
|
|
|
|
|
+ });
|
|
|
|
|
+ // 添加震动反馈
|
|
|
|
|
+ uni.vibrateLong({
|
|
|
|
|
+ success: function () {
|
|
|
|
|
+ console.log('Vibration successful');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: function (err) {
|
|
|
|
|
+ console.error('Vibration failed:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.showPageWarning = false;
|
|
|
|
|
+ }, 3000);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 然后检查身份验证状态
|
|
|
|
|
+ else */ if (data.data.identity.status == "no_face") {
|
|
|
|
|
+ this.showPageWarning = true;
|
|
|
|
|
+ uni.showToast({/* data.data.identity.message */
|
|
|
|
|
+ title: '请保持面部完整出现在镜头内',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 3000
|
|
|
|
|
+ });
|
|
|
|
|
+ uni.vibrateLong({
|
|
|
|
|
+ success: function () {
|
|
|
|
|
+ console.log('Vibration successful');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: function (err) {
|
|
|
|
|
+ console.error('Vibration failed:', err);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.showPageWarning = false;
|
|
|
|
|
+ }, 3000);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 进入追问模式
|
|
|
|
|
+ enterFollowUpMode() {
|
|
|
|
|
+ this.isFollowUpMode = true;
|
|
|
|
|
+ this.mainQuestionIndex = this.currentVideoIndex; // 保存当前主问题的索引
|
|
|
|
|
+ console.log('进入追问模式,保存主问题索引:', this.mainQuestionIndex);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 修改 handleFollowUpQuestion 方法
|
|
|
|
|
+ async handleFollowUpQuestion(questionData) {
|
|
|
|
|
+ console.log('处理追问:', questionData);
|
|
|
|
|
+ this.enterFollowUpMode();
|
|
|
|
|
+ console.log('处理追问问题数据:', questionData);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查数据完整性
|
|
|
|
|
+ if (!questionData || !questionData.follow_up_voice_url || !questionData.follow_up_question) {
|
|
|
|
|
+ console.error('追问数据不完整:', questionData);
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '获取追问数据失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存父问题
|
|
|
|
|
+ this.parentQuestion = questionData.original_question || '';
|
|
|
|
|
+
|
|
|
|
|
+ // 确保音频URL正确
|
|
|
|
|
+ let audioUrl = '';
|
|
|
|
|
+ if (questionData.follow_up_voice_url.direct_url) {
|
|
|
|
|
+ audioUrl = questionData.follow_up_voice_url.direct_url;
|
|
|
|
|
+ } else if (questionData.follow_up_voice_url.file_url) {
|
|
|
|
|
+ audioUrl = questionData.follow_up_voice_url.file_url;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 确保音频URL是完整的
|
|
|
|
|
+ if (!audioUrl.startsWith('http')) {
|
|
|
|
|
+ audioUrl = apiBaseUrl + audioUrl;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ console.log('处理后的音频URL:', audioUrl);
|
|
|
|
|
+
|
|
|
|
|
+ // 创建追问问题对象
|
|
|
|
|
+ const followUpQuestionObj = {
|
|
|
|
|
+ id: Date.now(),
|
|
|
|
|
+ question: questionData.follow_up_question.trim(), // 去除可能的空白字符
|
|
|
|
|
+ audioUrl: audioUrl,
|
|
|
|
|
+ originalQuestion: questionData.original_question,
|
|
|
|
|
+ sessionId: questionData.session_id
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ console.log('创建的追问问题对象:', followUpQuestionObj);
|
|
|
|
|
+
|
|
|
|
|
+ // 添加到追问问题列表
|
|
|
|
|
+ this.followUpQuestions.push(followUpQuestionObj);
|
|
|
|
|
+ this.currentFollowUpIndex = this.followUpQuestions.length - 1;
|
|
|
|
|
+
|
|
|
|
|
+ // 更新当前追问问题
|
|
|
|
|
+ this.followUpQuestion = followUpQuestionObj.question;
|
|
|
|
|
+ this.followUpAudioUrl = followUpQuestionObj.audioUrl;
|
|
|
|
|
+
|
|
|
|
|
+ // 标记当前是追问问题
|
|
|
|
|
+ this.isFollowUpQuestion = true;
|
|
|
|
|
+ this.currentFollowUpQuestion = followUpQuestionObj;
|
|
|
|
|
+
|
|
|
|
|
+ // 显示字幕
|
|
|
|
|
+ this.showSubtitle(followUpQuestionObj.question, true);
|
|
|
|
|
+
|
|
|
|
|
+ // 等待音频播放完成
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.playFollowUpAudio();
|
|
|
|
|
+ console.log('音频播放完成,准备录制回答');
|
|
|
|
|
+ // 开始准备录制回答
|
|
|
|
|
+ this.prepareToAnswer(true, followUpQuestionObj);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('音频播放失败:', error);
|
|
|
|
|
+ // 即使音频播放失败,也继续准备录制回答
|
|
|
|
|
+ this.prepareToAnswer(true, followUpQuestionObj);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 播放追问音频
|
|
|
|
|
+ async playFollowUpAudio() {
|
|
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
|
|
+ console.log('开始播放追问音频, URL:', this.followUpAudioUrl);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查音频URL
|
|
|
|
|
+ if (!this.followUpAudioUrl) {
|
|
|
|
|
+ console.error('没有音频URL');
|
|
|
|
|
+ reject(new Error('没有音频URL'));
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 显示加载提示
|
|
|
|
|
+ uni.showLoading({
|
|
|
|
|
+ title: '思考中...'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 停止并销毁之前的音频实例
|
|
|
|
|
+ this.stopAndDestroyAudio();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 创建新的音频上下文
|
|
|
|
|
+ const innerAudioContext = uni.createInnerAudioContext();
|
|
|
|
|
+ this.audioContext = innerAudioContext;
|
|
|
|
|
+
|
|
|
|
|
+ // 设置音频属性
|
|
|
|
|
+ innerAudioContext.autoplay = true;
|
|
|
|
|
+ innerAudioContext.obeyMuteSwitch = false;
|
|
|
|
|
+ innerAudioContext.volume = 1.0;
|
|
|
|
|
+
|
|
|
|
|
+ // 监听音频加载状态
|
|
|
|
|
+ innerAudioContext.onCanplay(() => {
|
|
|
|
|
+ console.log('音频可以播放');
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ this.isAudioPlaying = true;
|
|
|
|
|
+ /* uni.showToast({
|
|
|
|
|
+ title: '正在播放追问',
|
|
|
|
|
+ icon: 'none',
|
|
|
|
|
+ duration: 2000
|
|
|
|
|
+ }); */
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听音频播放完成
|
|
|
|
|
+ innerAudioContext.onEnded(() => {
|
|
|
|
|
+ console.log('追问音频播放完成');
|
|
|
|
|
+ this.isAudioPlaying = false;
|
|
|
|
|
+ resolve();
|
|
|
|
|
+ // 延迟清理资源,确保不会影响播放完成的回调
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.stopAndDestroyAudio();
|
|
|
|
|
+ // 音频播放完成后,自动准备录制回答
|
|
|
|
|
+ this.prepareToAnswer(true, this.currentFollowUpQuestion);
|
|
|
|
|
+ }, 100);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 监听音频播放错误
|
|
|
|
|
+ innerAudioContext.onError((res) => {
|
|
|
|
|
+ console.error('音频播放错误:', res);
|
|
|
|
|
+ this.isAudioPlaying = false;
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '音频播放失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ reject(res);
|
|
|
|
|
+ this.stopAndDestroyAudio();
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // 设置音频源
|
|
|
|
|
+ console.log('设置音频源:', this.followUpAudioUrl);
|
|
|
|
|
+ innerAudioContext.src = this.followUpAudioUrl;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('创建或播放音频失败:', error);
|
|
|
|
|
+ this.isAudioPlaying = false;
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ uni.showToast({
|
|
|
|
|
+ title: '音频播放失败',
|
|
|
|
|
+ icon: 'none'
|
|
|
|
|
+ });
|
|
|
|
|
+ this.stopAndDestroyAudio();
|
|
|
|
|
+ reject(error);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 停止并销毁音频
|
|
|
|
|
+ stopAndDestroyAudio() {
|
|
|
|
|
+ if (!this.audioContext) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const ctx = this.audioContext;
|
|
|
|
|
+ this.audioContext = null; // 先清除引用
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (this.isAudioPlaying) {
|
|
|
|
|
+ ctx.stop();
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('停止音频播放失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ ctx.destroy();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('销毁音频实例失败:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.isAudioPlaying = false;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 显示字幕
|
|
|
|
|
+ showSubtitle(text, isFollowUp = true) {
|
|
|
|
|
+ console.log('显示字幕:', text);
|
|
|
|
|
+ this.subtitleText = text;
|
|
|
|
|
+ this.showSubtitleText = true;
|
|
|
|
|
+ this.isFollowUpQuestion = isFollowUp;
|
|
|
|
|
+
|
|
|
|
|
+ // 确保字幕显示在界面上
|
|
|
|
|
+ this.$nextTick(() => {
|
|
|
|
|
+ // 如果需要,可以添加字幕动画效果
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ this.showSubtitleText = false;
|
|
|
|
|
+ }, 8000); // 8秒后隐藏字幕
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 显示问题对话框
|
|
|
|
|
+ showQuestionDialog() {
|
|
|
|
|
+ if (this.followUpQuestion) {
|
|
|
|
|
+ uni.showModal({
|
|
|
|
|
+ title: '追问问题',
|
|
|
|
|
+ content: this.followUpQuestion,
|
|
|
|
|
+ showCancel: false,
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ // 准备开始回答
|
|
|
|
|
+ this.prepareToAnswer(true, this.followUpQuestions[this.currentFollowUpIndex]);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 显示面试官思考中loading
|
|
|
|
|
+ showThinkingLoading() {
|
|
|
|
|
+ this.isThinking = true;
|
|
|
|
|
+ uni.showLoading({
|
|
|
|
|
+ title: '面试官正在思考中',
|
|
|
|
|
+ mask: false
|
|
|
|
|
+ });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 隐藏面试官思考中loading
|
|
|
|
|
+ hideThinkingLoading() {
|
|
|
|
|
+ this.isThinking = false;
|
|
|
|
|
+ uni.hideLoading();
|
|
|
|
|
+ if (this.thinkingTimer) {
|
|
|
|
|
+ clearTimeout(this.thinkingTimer);
|
|
|
|
|
+ this.thinkingTimer = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 处理视频完成
|
|
|
|
|
+ handleVideoCompletion(jobId) {
|
|
|
|
|
+ this.isVideoSwitching = false; // 重置切换状态锁
|
|
|
|
|
+
|
|
|
|
|
+ // 延迟执行跳转,确保资源清理完成
|
|
|
|
|
+ /* setTimeout(() => { */
|
|
|
|
|
+ // 如果职位ID为9,跳转到interview-question页面
|
|
|
|
|
+ if (jobId === 9) {
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: '/pages/interview-question/interview-question',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('成功跳转到interview-question页面');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('跳转到interview-question页面失败:', err);
|
|
|
|
|
+ // 如果跳转失败,尝试使用redirectTo
|
|
|
|
|
+ uni.redirectTo({
|
|
|
|
|
+ url: '/pages/interview-question/interview-question',
|
|
|
|
|
+ fail: (redirectErr) => {
|
|
|
|
|
+ console.error('重定向到interview-question页面也失败:', redirectErr);
|
|
|
|
|
+ // 如果所有跳转方式都失败,尝试返回上一页
|
|
|
|
|
+ uni.navigateBack({
|
|
|
|
|
+ delta: 1
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 其他职位ID跳转到camera页面
|
|
|
|
|
+ uni.navigateTo({
|
|
|
|
|
+ url: '/pages/camera/camera',
|
|
|
|
|
+ success: () => {
|
|
|
|
|
+ console.log('成功跳转到camera页面');
|
|
|
|
|
+ },
|
|
|
|
|
+ fail: (err) => {
|
|
|
|
|
+ console.error('跳转到camera页面失败:', err);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果跳转失败,尝试使用redirectTo
|
|
|
|
|
+ uni.redirectTo({
|
|
|
|
|
+ url: '/pages/camera/camera',
|
|
|
|
|
+ fail: (redirectErr) => {
|
|
|
|
|
+ console.error('重定向到camera页面也失败:', redirectErr);
|
|
|
|
|
+
|
|
|
|
|
+ // 最后尝试使用switchTab(如果camera是tabBar页面)
|
|
|
|
|
+ uni.switchTab({
|
|
|
|
|
+ url: '/pages/camera/camera',
|
|
|
|
|
+ fail: (switchErr) => {
|
|
|
|
|
+ console.error('所有跳转方式都失败:', switchErr);
|
|
|
|
|
+
|
|
|
|
|
+ // 如果所有跳转方式都失败,尝试返回上一页
|
|
|
|
|
+ uni.navigateBack({
|
|
|
|
|
+ delta: 1
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ /* }, 200); */
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ computed: {
|
|
|
|
|
+ // 计算进度比例
|
|
|
|
|
+ progressRatio() {
|
|
|
|
|
+ return this.recordingTimerCount / this.maxRecordingTime;
|
|
|
|
|
+ },
|
|
|
|
|
+ // 右半圆旋转角度
|
|
|
|
|
+ half1Turn() {
|
|
|
|
|
+ // 初始状态为0度,小于50%时,右半圆按比例旋转
|
|
|
|
|
+ if (this.progressRatio <= 0.5) {
|
|
|
|
|
+ return this.progressRatio * 2 * 180;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 大于50%时,右半圆固定在180度
|
|
|
|
|
+ return 180;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 左半圆旋转角度
|
|
|
|
|
+ half2Turn() {
|
|
|
|
|
+ // 小于50%时,左半圆固定在0度
|
|
|
|
|
+ if (this.progressRatio <= 0.5) {
|
|
|
|
|
+ return 0;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 大于50%时,左半圆按超出50%的比例旋转
|
|
|
|
|
+ return (this.progressRatio - 0.5) * 2 * 180;
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 修改进度百分比的计算方式
|
|
|
|
|
+ progressPercent() {
|
|
|
|
|
+ // 将时间进度转换为百分比(0-100)
|
|
|
|
|
+ return Math.min(Math.round((this.recordingTimerCount / this.maxRecordingTime) * 100), 100);
|
|
|
|
|
+ },
|
|
|
|
|
+ // 添加总题数计算属性
|
|
|
|
|
+ totalQuestions() {
|
|
|
|
|
+ // 获取问题总数,不包括介绍视频
|
|
|
|
|
+ return this.questions.length || 5; // 如果没有问题数据,默认显示5
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped>
|
|
|
|
|
+.subtitle-container {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ bottom: 20%;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ z-index: 1000;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.subtitle-text {
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.7);
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ padding: 10px 20px;
|
|
|
|
|
+ border-radius: 5px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ max-width: 80%;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ animation: fadeIn 0.5s ease-in-out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes fadeIn {
|
|
|
|
|
+ from {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transform: translateY(20px);
|
|
|
|
|
+ }
|
|
|
|
|
+ to {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.identity-verify-container {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ background-color: #f5f5f5;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.page-warning {
|
|
|
|
|
+ animation: page-warning-flash 1s ease-in-out infinite;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes page-warning-flash {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ background-color: rgba(255, 0, 0, 0.2);
|
|
|
|
|
+ box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
|
|
|
|
|
+ }
|
|
|
|
|
+ 50% {
|
|
|
|
|
+ background-color: rgba(255, 0, 0, 0.1);
|
|
|
|
|
+ box-shadow: 0 0 10px rgba(255, 0, 0, 0.2);
|
|
|
|
|
+ }
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ background-color: rgba(255, 0, 0, 0.2);
|
|
|
|
|
+ box-shadow: 0 0 20px rgba(255, 0, 0, 0.3);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.digital-human-container {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100vh;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background-color: #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.digital-human-video {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ margin-top: 32px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 用户摄像头容器样式 */
|
|
|
|
|
+.user-camera-container {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 100px;
|
|
|
|
|
+ right: 5px;
|
|
|
|
|
+ width: 110px;
|
|
|
|
|
+ height: 160px;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+ transition: all 0.3s ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.camera-warning {
|
|
|
|
|
+ animation: camera-warning-flash 1s ease-in-out infinite;
|
|
|
|
|
+ border: 4px solid #ff0000;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes camera-warning-flash {
|
|
|
|
|
+ 0% {
|
|
|
|
|
+ border-color: #ff0000;
|
|
|
|
|
+ box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
|
|
|
|
|
+ }
|
|
|
|
|
+ 50% {
|
|
|
|
|
+ border-color: rgba(255, 0, 0, 0.3);
|
|
|
|
|
+ box-shadow: 0 0 5px rgba(255, 0, 0, 0.4);
|
|
|
|
|
+ }
|
|
|
|
|
+ 100% {
|
|
|
|
|
+ border-color: #ff0000;
|
|
|
|
|
+ box-shadow: 0 0 15px rgba(255, 0, 0, 0.8);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 用户摄像头视频样式 */
|
|
|
|
|
+.user-camera-video {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ background-color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.video-player {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ outline: none; /* 移除视频获得焦点时的轮廓 */
|
|
|
|
|
+ -webkit-tap-highlight-color: transparent; /* 移除移动设备上的点击高亮 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 隐藏视频控制条 */
|
|
|
|
|
+video::-webkit-media-controls {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-enclosure {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-panel {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-play-button {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-timeline {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-current-time-display {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-time-remaining-display {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-mute-button {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-volume-slider {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+video::-webkit-media-controls-fullscreen-button {
|
|
|
|
|
+ display: none !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 修改字幕覆盖层样式,使其与图片中的样式一致 */
|
|
|
|
|
+.subtitle-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 180px;
|
|
|
|
|
+ left: 10%;
|
|
|
|
|
+ width: 80%;
|
|
|
|
|
+ padding: 0; /* 移除内边距,由内部元素控制 */
|
|
|
|
|
+ border-radius: 15px;
|
|
|
|
|
+ background-color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ text-align: left;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ z-index: 10;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ overflow: hidden; /* 确保圆角效果 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 添加题号header样式 */
|
|
|
|
|
+.subtitle-header {
|
|
|
|
|
+/* background-color: rgba(0, 0, 0, 0.05); */
|
|
|
|
|
+ padding: 8px 20px;
|
|
|
|
|
+ /* border-bottom: 1px solid rgba(0, 0, 0, 0.05); */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.question-tag {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ color: #666;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 调整主字幕样式 */
|
|
|
|
|
+.subtitle-main {
|
|
|
|
|
+ padding: 15px 20px ;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 移除之前的题号显示器 */
|
|
|
|
|
+.question-counter {
|
|
|
|
|
+ display: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.control-panel {
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.control-button {
|
|
|
|
|
+ padding: 10px 20px;
|
|
|
|
|
+ background-color: #4CAF50;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.loading {
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ margin: 20px 0;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.response-container {
|
|
|
|
|
+ margin-top: 20px;
|
|
|
|
|
+ padding: 0 20px;
|
|
|
|
|
+ display: none; /* 默认隐藏调试信息 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 当showDebugInfo为true时显示 */
|
|
|
|
|
+.showDebugInfo .response-container {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.response-item {
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ border: 1px solid #eee;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ margin-bottom: 10px;
|
|
|
|
|
+ background-color: #f9f9f9;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.response-content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.answer-button-container {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 50px; /* 将按钮放在底部而不是75%的位置 */
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%); /* 只在X轴上平移,保持水平居中 */
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 修改回答按钮样式 */
|
|
|
|
|
+.answer-button {
|
|
|
|
|
+ width: 120px;
|
|
|
|
|
+ height: 40px; /* 改为矩形按钮 */
|
|
|
|
|
+ border-radius: 20px; /* 圆角矩形 */
|
|
|
|
|
+ background-color: #ffffff; /* 白色背景 */
|
|
|
|
|
+ color: #333; /* 深色文字 */
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ animation: none; /* 移除脉动动画 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stop-recording-button-container {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 50px; /* 统一与其他按钮的位置 */
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.stop-recording-button {
|
|
|
|
|
+ width: 120px;
|
|
|
|
|
+ height: 40px; /* 改为矩形按钮 */
|
|
|
|
|
+ border-radius: 20px; /* 圆角矩形 */
|
|
|
|
|
+ background-color: #0039b3; /* 白色背景 */
|
|
|
|
|
+ color: #fff;/* 深色文字 */
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ animation: none; /* 移除脉动动画 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.recording-indicator {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ bottom: 20px;
|
|
|
|
|
+ left: 17%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ /* background-color: rgba(0, 0, 0, 0.6); */
|
|
|
|
|
+ padding: 10px 15px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.timer-text {
|
|
|
|
|
+ color: #05dc8b;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ margin-top: 5px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.start-recording-button-container {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 50px; /* 统一与其他按钮的位置 */
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ z-index: 101;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.start-recording-button {
|
|
|
|
|
+ width: 120px;
|
|
|
|
|
+ height: 40px; /* 改为矩形按钮 */
|
|
|
|
|
+ border-radius: 20px; /* 圆角矩形 */
|
|
|
|
|
+ background-color: #ffffff; /* 白色背景 */
|
|
|
|
|
+ color: #333; /* 深色文字 */
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ animation: none; /* 移除脉动动画 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.retry-button-container {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 30px; /* 统一与其他按钮的位置 */
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.retry-button {
|
|
|
|
|
+ padding: 10px 20px;
|
|
|
|
|
+ background-color: #ffffff; /* 白色背景 */
|
|
|
|
|
+ color: #333; /* 深色文字 */
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ border-radius: 20px; /* 圆角矩形 */
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.retry-button:hover {
|
|
|
|
|
+ background-color: #2980b9;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 录制时长显示 */
|
|
|
|
|
+.recording-timer {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ left: 130px; /* 放在录制指示器旁边 */
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.6);
|
|
|
|
|
+ padding: 5px 10px;
|
|
|
|
|
+ border-radius: 15px;
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.timer-text {
|
|
|
|
|
+ color: #000;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-family: monospace; /* 使用等宽字体,使时间显示更稳定 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.remaining-time {
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ margin-top: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.remaining-time.warning {
|
|
|
|
|
+ color: #ff6b6b; /* 剩余时间少时显示红色 */
|
|
|
|
|
+ animation: blink 1s infinite; /* 使用闪烁动画提醒用户 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 添加上传状态指示器 */
|
|
|
|
|
+.upload-status-indicator {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ left: 130px; /* 放在录制指示器旁边 */
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.6);
|
|
|
|
|
+ padding: 5px 10px;
|
|
|
|
|
+ border-radius: 15px;
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload-status-content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload-status-icon {
|
|
|
|
|
+ width: 12px;
|
|
|
|
|
+ height: 12px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ margin-right: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.upload-status-text {
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.uploading {
|
|
|
|
|
+ background-color: #e74c3c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 添加倒计时蒙层样式 */
|
|
|
|
|
+.countdown-overlay {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.7);
|
|
|
|
|
+ z-index: 100;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.countdown-content {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.countdown-number {
|
|
|
|
|
+ font-size: 80px;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.countdown-text {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: #ffffff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 添加GIF播放器样式 */
|
|
|
|
|
+.gif-player {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ z-index: 5; /* 确保GIF在视频上方但在字幕和按钮下方 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 重新录制按钮容器样式 */
|
|
|
|
|
+.rerecord-button-container {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ bottom: 50px; /* 统一与其他按钮的位置 */
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ transform: translateX(-50%);
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 重新录制按钮样式 */
|
|
|
|
|
+.rerecord-button {
|
|
|
|
|
+ width: 120px;
|
|
|
|
|
+ height: 40px;
|
|
|
|
|
+ border-radius: 20px;
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.rerecord-button:hover {
|
|
|
|
|
+ background-color: #f0f0f0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 添加视频蒙层样式 */
|
|
|
|
|
+.video-overlay {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background-color: transparent; /* 完全透明 */
|
|
|
|
|
+ z-index: 6; /* 确保在视频之上,但在按钮和字幕之下 */
|
|
|
|
|
+ pointer-events: auto; /* 允许捕获点击事件 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 调整其他元素的z-index以确保正确的层级关系 */
|
|
|
|
|
+.subtitle-overlay {
|
|
|
|
|
+ /* ... 现有样式 ... */
|
|
|
|
|
+ z-index: 10; /* 确保字幕在蒙层之上 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.answer-button-container,
|
|
|
|
|
+.stop-recording-button-container,
|
|
|
|
|
+.start-recording-button-container,
|
|
|
|
|
+.retry-button-container,
|
|
|
|
|
+.rerecord-button-container {
|
|
|
|
|
+ /* ... 现有样式 ... */
|
|
|
|
|
+ z-index: 999; /* 确保按钮在蒙层之上 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.user-camera-container {
|
|
|
|
|
+ /* ... 现有样式 ... */
|
|
|
|
|
+ z-index: 20; /* 确保摄像头窗口在蒙层之上 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.recording-indicator {
|
|
|
|
|
+ /* ... 现有样式 ... */
|
|
|
|
|
+ z-index: 20; /* 确保录制指示器在蒙层之上 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 环形进度条样式 */
|
|
|
|
|
+.circle-progress-container {
|
|
|
|
|
+ width: 60px;
|
|
|
|
|
+ height: 60px;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ margin: 0 auto 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.circle-progress {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ transform: rotate(-90deg); /* 从顶部开始进度 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.circle-progress-inner {
|
|
|
|
|
+ width: 80%;
|
|
|
|
|
+ height: 80%;
|
|
|
|
|
+ background-color: rgba(255, 255, 255);
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ transform: rotate(90deg); /* 修正文本方向 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.progress-text {
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+ color: #05dc8b;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 添加题号显示 */
|
|
|
|
|
+.question-counter {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 20px;
|
|
|
|
|
+ right: 130px; /* 调整位置,避免与用户摄像头重叠 */
|
|
|
|
|
+ background-color: rgba(255, 255, 255, 0.9);
|
|
|
|
|
+ padding: 6px 12px;
|
|
|
|
|
+ border-radius: 15px;
|
|
|
|
|
+ z-index: 20;
|
|
|
|
|
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.question-number {
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问问题容器样式 */
|
|
|
|
|
+.followup-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问分隔线 */
|
|
|
|
|
+.followup-divider {
|
|
|
|
|
+ height: 1px;
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.05);
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问内容样式 */
|
|
|
|
|
+.followup-content {
|
|
|
|
|
+ padding: 12px 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问标签样式 */
|
|
|
|
|
+.followup-label {
|
|
|
|
|
+ color: #fb752f;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问文本样式 */
|
|
|
|
|
+.followup-text {
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 调整主字幕样式,确保有足够空间 */
|
|
|
|
|
+.subtitle-main {
|
|
|
|
|
+ padding:15px 20px ;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ border-bottom: none; /* 移除底部边框,由追问分隔线处理 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 父问题样式 */
|
|
|
|
|
+.parent-question {
|
|
|
|
|
+ padding: 15px 20px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问容器样式调整 */
|
|
|
|
|
+.followup-container {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ /* background-color: rgba(0, 0, 0, 0.02); *//* 轻微的背景色区分 */
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 分隔线样式 */
|
|
|
|
|
+.followup-divider {
|
|
|
|
|
+ height: 1px;
|
|
|
|
|
+ background-color: rgba(0, 0, 0, 0.05);
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问内容样式 */
|
|
|
|
|
+.followup-content {
|
|
|
|
|
+ padding: 12px 20px;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问标签样式 */
|
|
|
|
|
+.followup-label {
|
|
|
|
|
+ color: #fb752f;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/* 追问文本样式 */
|
|
|
|
|
+.followup-text {
|
|
|
|
|
+ color: #333;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.5;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+.custom-navbar {
|
|
|
|
|
+ position: fixed;
|
|
|
|
|
+ top: 0;
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ right: 0;
|
|
|
|
|
+ height: 44px;
|
|
|
|
|
+ background-color: #ffffff;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ z-index: 999;
|
|
|
|
|
+}
|
|
|
|
|
+.navbar-title {
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ font-weight: bold;
|
|
|
|
|
+}
|
|
|
|
|
+.content {
|
|
|
|
|
+ margin-top: 44px;
|
|
|
|
|
+ padding-top: var(--status-bar-height);
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|