|
@@ -0,0 +1,3292 @@
|
|
|
+<template>
|
|
|
+ <div class="identity-verify-container">
|
|
|
+ <div class="digital-human-container">
|
|
|
+ <!-- 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"
|
|
|
+ @play="handleVideoPlay"
|
|
|
+ @timeupdate="handleTimeUpdate">
|
|
|
+ </video>
|
|
|
+ <div class="answer-button-container" v-if="showAnswerButton">
|
|
|
+ <button class="answer-button" @click="handleAnswerButtonClick">
|
|
|
+ 开始回答
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ <!-- 添加字幕覆盖层 :data-time="formatTime(recordingTimerCount) || '03:30'"-->
|
|
|
+ <div class="subtitle-overlay" v-if="currentSubtitle" >
|
|
|
+ {{ currentSubtitle }}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 添加答题按钮 -->
|
|
|
+
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 用户摄像头视频显示区域 -->
|
|
|
+ <div class="user-camera-container">
|
|
|
+ <!-- 在小程序环境中使用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">
|
|
|
+ <div class="recording-dot"></div>
|
|
|
+ <span class="recording-text">正在录制...</span>
|
|
|
+ <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>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { apiBaseUrl } from '@/common/config.js';
|
|
|
+export default {
|
|
|
+ name: 'IdentityVerify',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loading: false,
|
|
|
+ responses: [],
|
|
|
+ processedResponses: [],
|
|
|
+ assistantResponse: '',
|
|
|
+ audioTranscript: '',
|
|
|
+ videoPlaying: false,
|
|
|
+ showDebugInfo: false, // 设置为true可以显示调试信息
|
|
|
+ videoUrl: '', // 修改为空字符串,将通过API获取
|
|
|
+ showReplayButton: false,
|
|
|
+ cameraStream: null, // 存储摄像头流
|
|
|
+ cameraError: null, // 存储摄像头错误信息
|
|
|
+ useMiniProgramCameraComponent: false, // 添加小程序相机组件标志
|
|
|
+ cameraContext: null, // 添加相机上下文
|
|
|
+ currentSubtitle: '',
|
|
|
+ subtitles: [], // 修改为空数组,将通过API获取
|
|
|
+ // 移除硬编码的字幕数组
|
|
|
+ // secondVideoSubtitles: [...],
|
|
|
+ // thirdVideoSubtitles: [...],
|
|
|
+ // 等等...
|
|
|
+ showAnswerButton: false, // 控制答题按钮显示
|
|
|
+ currentVideoIndex: 0, // 当前播放的视频索引
|
|
|
+ videoList: [], // 修改为空数组,将通过API获取
|
|
|
+ // 其他属性保持不变...
|
|
|
+ questions: [], // 添加新属性存储API返回的问题数据
|
|
|
+ introVideoUrl: 'http://121.36.251.245:9000/minlong/tenant_1/general_uploads/880a117c8fcc4196b0d8c0750608d373.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: [], // 存储待上传的视频
|
|
|
+ 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
|
|
|
+ additionalQuestions: [], // 存储追加问题
|
|
|
+ currentQuestionIndex: 0, // 当前问题索引
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ /* this.fetchData() */
|
|
|
+ // this.fetchQuestions(); // 移除获取问题数据的方法调用
|
|
|
+ this.checkAudioPermission();
|
|
|
+ this.initCamera();
|
|
|
+ this.checkIOSCameraRecordPermission();
|
|
|
+
|
|
|
+ // 添加防御性检查,避免渲染错误
|
|
|
+ this.checkAndFixRenderingIssues();
|
|
|
+
|
|
|
+ // 添加音频测试
|
|
|
+ setTimeout(() => {
|
|
|
+ if (this.cameraStream && !this.useMiniProgramCameraComponent) {
|
|
|
+ this.testAudioInput();
|
|
|
+ }
|
|
|
+ }, 3000);
|
|
|
+ uni.setKeepScreenOn({
|
|
|
+ keepScreenOn: true
|
|
|
+ });
|
|
|
+
|
|
|
+ // 获取追加问题
|
|
|
+ this.fetchAdditionalQuestions();
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ // 组件销毁前停止摄像头
|
|
|
+ this.stopUserCamera();
|
|
|
+
|
|
|
+ // 清除倒计时定时器
|
|
|
+ this.clearCountdown();
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 初始化相机
|
|
|
+ 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 = []
|
|
|
+ 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);
|
|
|
+ },
|
|
|
+ complete: () => {
|
|
|
+ this.loading = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取数据失败:', error);
|
|
|
+ this.loading = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ 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('视频播放结束');
|
|
|
+ this.videoPlaying = false;
|
|
|
+
|
|
|
+ // 对于所有视频,都显示相应的按钮
|
|
|
+ this.showStartRecordingButton = true;
|
|
|
+ this.startCountdown();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:开始倒计时
|
|
|
+ startCountdown() {
|
|
|
+ // 显示倒计时蒙层
|
|
|
+ this.showCountdown = true;
|
|
|
+ this.countdownValue = 10;
|
|
|
+
|
|
|
+ // 开始倒计时
|
|
|
+ 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;
|
|
|
+ },
|
|
|
+
|
|
|
+ // 修改 handleStartRecordingClick 方法,使其可以终止倒计时
|
|
|
+ handleStartRecordingClick() {
|
|
|
+ // 隐藏"开始回答"按钮
|
|
|
+ this.showStartRecordingButton = false;
|
|
|
+
|
|
|
+ // 如果倒计时正在进行,清除倒计时
|
|
|
+ this.clearCountdown();
|
|
|
+
|
|
|
+ // 直接开始录制
|
|
|
+ this.startRecordingAnswer();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 修改 stopRecordingAnswer 方法,确保清除倒计时
|
|
|
+ stopRecordingAnswer() {
|
|
|
+ // 如果倒计时正在进行,先清除倒计时
|
|
|
+ this.clearCountdown();
|
|
|
+
|
|
|
+ // 检查录制时长
|
|
|
+ const recordingDuration = this.getRecordingDuration();
|
|
|
+ const minimumDuration = 5; // 最小录制时长(秒)
|
|
|
+
|
|
|
+ if (recordingDuration < minimumDuration) {
|
|
|
+ // 录制时间过短,显示提示
|
|
|
+ uni.showToast({
|
|
|
+ title: '录制时间过短,请至少录制5秒',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+ // 不执行停止录制逻辑,继续录制
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 录制时长足够,直接停止
|
|
|
+ this.completeRecordingStop();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:开始录制用户回答
|
|
|
+ startRecordingAnswer() {
|
|
|
+ console.log('开始录制用户回答');
|
|
|
+ this.isRecording = true;
|
|
|
+
|
|
|
+ // 记录录制开始时间
|
|
|
+ this.recordingStartTime = Date.now();
|
|
|
+ this.recordingTimerCount = 0;
|
|
|
+ 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);
|
|
|
+
|
|
|
+ // 检查是否达到最大录制时间
|
|
|
+ if (this.recordingTimerCount >= this.maxRecordingTime) {
|
|
|
+ console.log('已达到最大录制时间(60秒),自动停止录制');
|
|
|
+ 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: 检查相机状态');
|
|
|
+
|
|
|
+ // 使用最简单的选项,设置最大录制时间为5分钟
|
|
|
+ const options = {
|
|
|
+ timeout: 300000, // 300秒超时 (5分钟)
|
|
|
+ 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使用标准选项,设置最大录制时间为5分钟
|
|
|
+ const options = {
|
|
|
+ timeout: 300000, // 300秒超时 (5分钟)
|
|
|
+ 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: 300, // 从60秒改为300秒
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:停止录制用户回答
|
|
|
+ stopRecordingAnswer() {
|
|
|
+ console.log('停止录制用户回答');
|
|
|
+
|
|
|
+ // 如果倒计时正在进行,先清除倒计时
|
|
|
+ if (this.countdownTimer) {
|
|
|
+ clearInterval(this.countdownTimer);
|
|
|
+ this.countdownTimer = null;
|
|
|
+ this.showCountdown = false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查录制时长
|
|
|
+ const recordingDuration = this.getRecordingDuration();
|
|
|
+ const minimumDuration = 5; // 最小录制时长(秒),从3秒改为5秒
|
|
|
+
|
|
|
+ if (recordingDuration < minimumDuration) {
|
|
|
+ // 录制时间过短,显示提示
|
|
|
+ uni.showToast({
|
|
|
+ title: '录制时间过短,请至少录制5秒',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+ // 不执行停止录制逻辑,继续录制
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 录制时长足够,直接停止
|
|
|
+ this.completeRecordingStop();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:获取录制时长
|
|
|
+ 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('重置录制:停止当前录制成功');
|
|
|
+ // 短暂延迟后重新开始录制
|
|
|
+ setTimeout(() => {
|
|
|
+ this.startMiniProgramRecording();
|
|
|
+ }, 500);
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('重置录制:停止当前录制失败', err);
|
|
|
+ // 尝试直接重新开始录制
|
|
|
+ this.startMiniProgramRecording();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 显示提示
|
|
|
+ uni.showToast({
|
|
|
+ title: '请重新开始回答',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:完成录制停止流程
|
|
|
+ completeRecordingStop() {
|
|
|
+ this.isRecording = false;
|
|
|
+
|
|
|
+ // 清除定时器
|
|
|
+ if (this.recordingTimer) {
|
|
|
+ clearInterval(this.recordingTimer);
|
|
|
+ this.recordingTimer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除MediaRecorder超时定时器
|
|
|
+ if (this.mediaRecorderTimeout) {
|
|
|
+ clearTimeout(this.mediaRecorderTimeout);
|
|
|
+ this.mediaRecorderTimeout = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 隐藏录制中提示
|
|
|
+ uni.hideLoading();
|
|
|
+
|
|
|
+ // 隐藏停止录制按钮
|
|
|
+ this.showStopRecordingButton = false;
|
|
|
+
|
|
|
+ // 根据平台选择不同的停止录制方法
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
+ const isMiniProgram = systemInfo.uniPlatform && systemInfo.uniPlatform.startsWith('mp-');
|
|
|
+
|
|
|
+ if (isMiniProgram) {
|
|
|
+ // 小程序环境停止录制
|
|
|
+ this.stopMiniProgramRecording();
|
|
|
+ } else {
|
|
|
+ // H5/App环境停止录制
|
|
|
+ this.stopBrowserRecording();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 修改 stopMiniProgramRecording 方法
|
|
|
+ stopMiniProgramRecording() {
|
|
|
+ if (!this.cameraContext) {
|
|
|
+ console.error('相机上下文不存在');
|
|
|
+ this.proceedToNextQuestion();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const systemInfo = uni.getSystemInfoSync();
|
|
|
+ const isIOS = systemInfo.platform === 'ios';
|
|
|
+
|
|
|
+ // 在iOS上添加额外的错误处理
|
|
|
+ const stopOptions = {
|
|
|
+ success: (res) => {
|
|
|
+ console.log('小程序录像停止成功:', res);
|
|
|
+ // 获取临时文件路径
|
|
|
+ const tempFilePath = res.tempVideoPath;
|
|
|
+ if (!tempFilePath) {
|
|
|
+ console.error('未获取到视频文件路径');
|
|
|
+ uni.showToast({
|
|
|
+ title: '录制失败,未获取到视频文件',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ this.proceedToNextQuestion();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 在iOS上,检查文件是否存在
|
|
|
+ if (isIOS) {
|
|
|
+ uni.getFileInfo({
|
|
|
+ filePath: tempFilePath,
|
|
|
+ success: () => {
|
|
|
+ // 文件存在,上传视频
|
|
|
+ this.uploadRecordedVideo(tempFilePath);
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('视频文件不存在:', err);
|
|
|
+ uni.showToast({
|
|
|
+ title: '录制失败,视频文件不存在',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ this.proceedToNextQuestion();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // Android直接上传
|
|
|
+ this.uploadRecordedVideo(tempFilePath);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('小程序录像停止失败:', err);
|
|
|
+ uni.showToast({
|
|
|
+ title: '录制失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ this.proceedToNextQuestion();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ // 停止录制
|
|
|
+ this.cameraContext.stopRecord(stopOptions);
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:停止浏览器录制
|
|
|
+ stopBrowserRecording() {
|
|
|
+ if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
|
+ this.mediaRecorder.stop();
|
|
|
+ console.log('浏览器录制停止成功');
|
|
|
+ } else {
|
|
|
+ console.error('MediaRecorder不存在或已经停止');
|
|
|
+ this.proceedToNextQuestion();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 修改上传录制的视频方法
|
|
|
+ uploadRecordedVideo(fileOrPath) {
|
|
|
+ console.log('准备上传视频:', typeof fileOrPath === 'string' ? fileOrPath : fileOrPath.name);
|
|
|
+
|
|
|
+ // 获取当前问题ID
|
|
|
+ let questionId;
|
|
|
+
|
|
|
+ // 获取当前正在回答的问题
|
|
|
+ const currentQuestion = this.additionalQuestions[this.currentVideoIndex];
|
|
|
+
|
|
|
+ if (currentQuestion && currentQuestion.id) {
|
|
|
+ // 直接使用API返回的问题ID
|
|
|
+ questionId = currentQuestion.id;
|
|
|
+ console.log(`使用API返回的问题ID: ${questionId}, 问题: ${currentQuestion.question}`);
|
|
|
+ } else {
|
|
|
+ // 如果没有找到对应的问题,使用默认ID
|
|
|
+ questionId = this.getDefaultQuestionId(this.currentVideoIndex);
|
|
|
+ console.log(`未找到当前问题,使用默认ID: ${questionId}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('上传视频对应的问题ID:', questionId);
|
|
|
+
|
|
|
+ // 创建上传任务对象
|
|
|
+ const uploadTask = {
|
|
|
+ id: Date.now().toString(), // 生成唯一ID
|
|
|
+ file: fileOrPath,
|
|
|
+ questionId: questionId,
|
|
|
+ attempts: 0, // 上传尝试次数
|
|
|
+ maxAttempts: 3, // 最大尝试次数
|
|
|
+ };
|
|
|
+
|
|
|
+ // 添加到上传队列
|
|
|
+ this.uploadQueue.push(uploadTask);
|
|
|
+
|
|
|
+ // 初始化上传进度和状态
|
|
|
+ this.uploadProgress[uploadTask.id] = 0;
|
|
|
+ this.uploadStatus[uploadTask.id] = 'pending'; // pending, uploading, success, failed
|
|
|
+
|
|
|
+ // 显示简短的上传状态提示
|
|
|
+ uni.showToast({
|
|
|
+ title: '已完成回答',
|
|
|
+ icon: 'none',
|
|
|
+ duration: 1500
|
|
|
+ });
|
|
|
+
|
|
|
+ // 更新上传状态文本
|
|
|
+ this.updateUploadStatusText();
|
|
|
+
|
|
|
+ // 如果当前没有上传任务在进行,开始处理队列
|
|
|
+ if (!this.isUploading) {
|
|
|
+ this.processUploadQueue();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 立即进入下一个问题,不等待上传完成
|
|
|
+ this.proceedToNextQuestion();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:处理上传队列
|
|
|
+ processUploadQueue() {
|
|
|
+ // 如果队列为空,结束处理
|
|
|
+ if (this.uploadQueue.length === 0) {
|
|
|
+ this.isUploading = false;
|
|
|
+ this.showUploadStatus = false;
|
|
|
+ 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') {
|
|
|
+ // 浏览器环境,使用XMLHttpRequest上传
|
|
|
+ this.uploadFileWithXHR(task);
|
|
|
+ } else {
|
|
|
+ // 小程序环境,使用uni.uploadFile上传
|
|
|
+ this.uploadFileWithUni(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', 0);
|
|
|
+ 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 = null) {
|
|
|
+ console.log('提交视频URL到面试接口:', videoUrl);
|
|
|
+
|
|
|
+ // 确定问题ID
|
|
|
+ let questionId;
|
|
|
+ if (task) {
|
|
|
+ questionId = task.questionId;
|
|
|
+ } else {
|
|
|
+ // 根据当前视频索引获取问题ID
|
|
|
+ if (this.currentVideoIndex >= 1 && this.questions.length > 0) {
|
|
|
+ // 获取当前正在回答的问题
|
|
|
+ const currentQuestion = this.getCurrentQuestionByIndex(this.currentVideoIndex);
|
|
|
+
|
|
|
+ if (currentQuestion && currentQuestion.id) {
|
|
|
+ // 直接使用API返回的问题ID
|
|
|
+ questionId = currentQuestion.id;
|
|
|
+ console.log(`使用API返回的问题ID: ${questionId}, 问题: ${currentQuestion.question}`);
|
|
|
+ } else {
|
|
|
+ // 如果没有找到对应的问题,使用默认ID
|
|
|
+ questionId = this.getDefaultQuestionId(this.currentVideoIndex);
|
|
|
+ console.log(`未找到当前问题,使用默认ID: ${questionId}`);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 使用旧的映射逻辑作为备用
|
|
|
+ questionId = this.getDefaultQuestionId(this.currentVideoIndex);
|
|
|
+ console.log(`使用备用问题ID: ${questionId} (问题索引: ${this.currentVideoIndex})`);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 准备请求参数
|
|
|
+ const requestData = {
|
|
|
+ application_id: uni.getStorageSync('appId'),
|
|
|
+ question_id: questionId,
|
|
|
+ video_url: videoUrl,
|
|
|
+ tenant_id: JSON.parse(uni.getStorageSync('userInfo')).tenant_id || '1'
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('提交面试请求参数:', requestData);
|
|
|
+
|
|
|
+ // 发送请求到面试接口
|
|
|
+ uni.request({
|
|
|
+ url: `${apiBaseUrl}/api/job/upload_video`,
|
|
|
+ method: 'POST',
|
|
|
+ data: requestData,
|
|
|
+ header: {
|
|
|
+ 'content-type': 'application/x-www-form-urlencoded'
|
|
|
+ },
|
|
|
+ success: (res) => {
|
|
|
+ console.log('面试接口提交成功:', res);
|
|
|
+
|
|
|
+ if (res.data.code === 0 || res.data.code === 2000) {
|
|
|
+ // 提交成功
|
|
|
+ if (task) {
|
|
|
+ // 从队列中移除当前任务
|
|
|
+ this.uploadQueue.shift();
|
|
|
+
|
|
|
+ // 显示简短的成功提示
|
|
|
+ // uni.showToast({
|
|
|
+ // title: '视频提交成功',
|
|
|
+ // icon: 'success',
|
|
|
+ // duration: 1500
|
|
|
+ // });
|
|
|
+
|
|
|
+ // 继续处理队列中的下一个任务
|
|
|
+ this.processUploadQueue();
|
|
|
+ } else {
|
|
|
+ // 兼容旧代码的处理逻辑
|
|
|
+ uni.showToast({
|
|
|
+ title: '回答已提交',
|
|
|
+ icon: 'success'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存最后上传的视频URL
|
|
|
+ this.lastUploadedVideoUrl = videoUrl;
|
|
|
+
|
|
|
+ // 隐藏重试按钮(如果之前显示了)
|
|
|
+ this.showRetryButton = false;
|
|
|
+ this.lastVideoToRetry = null;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 提交失败
|
|
|
+ if (task) {
|
|
|
+ this.handleSubmitFailure(task, res.data.msg || '提交失败');
|
|
|
+ } else {
|
|
|
+ // 兼容旧代码的处理逻辑
|
|
|
+ uni.showToast({
|
|
|
+ title: res.data.msg || '提交失败,请重试',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存失败的视频URL,用于重试
|
|
|
+ this.lastVideoToRetry = videoUrl;
|
|
|
+
|
|
|
+ // 显示重试按钮
|
|
|
+ this.showRetryButton = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('面试接口提交失败:', err);
|
|
|
+
|
|
|
+ if (task) {
|
|
|
+ this.handleSubmitFailure(task, err.errMsg || '网络错误');
|
|
|
+ } else {
|
|
|
+ // 兼容旧代码的处理逻辑
|
|
|
+ uni.hideLoading();
|
|
|
+ uni.showToast({
|
|
|
+ title: '网络错误,请重试',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+
|
|
|
+ // 保存失败的视频URL,用于重试
|
|
|
+ this.lastVideoToRetry = videoUrl;
|
|
|
+
|
|
|
+ // 显示重试按钮
|
|
|
+ this.showRetryButton = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:处理提交失败
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加新方法:更新上传状态文本
|
|
|
+ updateUploadStatusText() {
|
|
|
+ if (this.uploadQueue.length === 0) {
|
|
|
+ this.uploadStatusText = '';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ this.uploadStatusText = `问题${currentTask.questionId - 9}:${statusText}`;
|
|
|
+
|
|
|
+ // 如果队列中有多个任务,显示总数
|
|
|
+ if (this.uploadQueue.length > 1) {
|
|
|
+ this.uploadStatusText += ` (${this.uploadQueue.length}个视频待处理)`;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 修改 proceedToNextQuestion 方法
|
|
|
+ proceedToNextQuestion() {
|
|
|
+ // 检查是否已完成所有追加问题
|
|
|
+ if (this.currentVideoIndex >= this.additionalQuestions.length - 1) {
|
|
|
+ // 完成所有追加问题后,跳转到相机页面
|
|
|
+ uni.navigateTo({
|
|
|
+ url: '/pages/camera/camera'
|
|
|
+ });
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 切换到下一个视频
|
|
|
+ this.currentVideoIndex++;
|
|
|
+ if (this.currentVideoIndex < this.videoList.length) {
|
|
|
+ // 还有下一段视频,播放它
|
|
|
+ this.videoUrl = this.videoList[this.currentVideoIndex];
|
|
|
+ this.videoPlaying = true;
|
|
|
+
|
|
|
+ // 重置当前字幕
|
|
|
+ this.currentSubtitle = '';
|
|
|
+
|
|
|
+ // 使用 nextTick 确保 DOM 更新后再播放视频
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
+ if (videoContext) {
|
|
|
+ videoContext.play();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 所有视频都播放完毕,可以进行下一步操作
|
|
|
+ uni.showToast({
|
|
|
+ title: '追加问题已完成',
|
|
|
+ icon: 'success',
|
|
|
+ duration: 2000
|
|
|
+ });
|
|
|
+
|
|
|
+ // 跳转到相机页面
|
|
|
+ setTimeout(() => {
|
|
|
+ uni.navigateTo({
|
|
|
+ url: '/pages/camera/camera'
|
|
|
+ });
|
|
|
+ }, 2000);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 修改 handleAnswerButtonClick 方法
|
|
|
+ handleAnswerButtonClick() {
|
|
|
+ // 隐藏答题按钮
|
|
|
+ this.showAnswerButton = false;
|
|
|
+
|
|
|
+ /* // 重置GIF状态
|
|
|
+ this.showGif = false;
|
|
|
+ this.gifUrl = ''; */
|
|
|
+
|
|
|
+ // 直接进入下一个问题
|
|
|
+ this.proceedToNextQuestion();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 处理相机错误
|
|
|
+ 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.target.currentTime;
|
|
|
+
|
|
|
+ // 如果正在录制,更新录制时间显示
|
|
|
+ if (this.isRecording && this.recordingTimerCount) {
|
|
|
+ this.recordingTimeDisplay = this.formatTime(this.recordingTimerCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从当前追加问题中获取字幕
|
|
|
+ if (this.currentVideoIndex >= 0 && this.currentVideoIndex < this.additionalQuestions.length) {
|
|
|
+ const currentQuestion = this.additionalQuestions[this.currentVideoIndex];
|
|
|
+
|
|
|
+ if (currentQuestion && currentQuestion.subtitles) {
|
|
|
+ // 查找当前时间应该显示的字幕
|
|
|
+ const subtitle = currentQuestion.subtitles.find(
|
|
|
+ sub => currentTime >= sub.startTime && currentTime < sub.endTime
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新当前字幕
|
|
|
+ this.currentSubtitle = subtitle ? subtitle.text : '';
|
|
|
+ } else {
|
|
|
+ // 如果没有字幕数据,直接显示问题文本
|
|
|
+ this.currentSubtitle = currentQuestion.question || '';
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 清空字幕
|
|
|
+ this.currentSubtitle = '';
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // Add a new method to handle the "Start Recording" button click
|
|
|
+ handleStartRecordingClick() {
|
|
|
+ // 隐藏"开始回答"按钮
|
|
|
+ this.showStartRecordingButton = false;
|
|
|
+
|
|
|
+ // 如果倒计时正在进行,清除倒计时
|
|
|
+ this.clearCountdown();
|
|
|
+
|
|
|
+ // 直接开始录制
|
|
|
+ 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(100);
|
|
|
+
|
|
|
+ // 绘制视频帧到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);
|
|
|
+
|
|
|
+ // 为介绍视频设置字幕
|
|
|
+ this.subtitles = [
|
|
|
+ {
|
|
|
+ startTime: 0,
|
|
|
+ endTime: 5,
|
|
|
+ text: '你好,我是本次面试的面试官,欢迎参加本公司的线上面试!'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ startTime: 5,
|
|
|
+ endTime: 13,
|
|
|
+ text: '面试预计需要15分钟,请你提前安排在网络良好、光线亮度合适、且相对安静的环境参加这次面试'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ startTime: 13,
|
|
|
+ endTime: 20,
|
|
|
+ text: '以免影响本次面试的结果。如果你在面试过程中遇到问题,请与我们的招聘人员联系。'
|
|
|
+ }
|
|
|
+ ];
|
|
|
+
|
|
|
+ // 按照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 => {
|
|
|
+ // 如果有视频URL,添加到视频列表
|
|
|
+ if (question.digital_human_video_url) {
|
|
|
+ this.videoList.push(question.digital_human_video_url);
|
|
|
+
|
|
|
+ // 为这个问题创建字幕数组
|
|
|
+ const subtitleArray = [{
|
|
|
+ startTime: 0,
|
|
|
+ endTime: 10, // 默认10秒,可以根据实际情况调整
|
|
|
+ text: question.digital_human_video_subtitle || question.question
|
|
|
+ }];
|
|
|
+
|
|
|
+ // 根据问题的序号存储字幕
|
|
|
+ const index = this.videoList.length - 1; // 减1是因为我们已经添加了视频URL
|
|
|
+
|
|
|
+ // 动态创建字幕数组属性
|
|
|
+ this[`question${index}Subtitles`] = subtitleArray;
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果没有获取到任何问题视频,使用默认视频
|
|
|
+ if (this.videoList.length <= 1) {
|
|
|
+ /* this.useDefaultVideosAndSubtitles(); */
|
|
|
+ } else {
|
|
|
+ console.log('处理后的视频列表:', this.videoList);
|
|
|
+ console.log('处理后的字幕:', this.subtitles);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 使用默认视频和字幕(作为备用)
|
|
|
+ useDefaultVideosAndSubtitles() {
|
|
|
+ console.log('使用默认视频和字幕');
|
|
|
+
|
|
|
+ // 设置默认视频列表
|
|
|
+ /* this.videoList = [
|
|
|
+ this.introVideoUrl, // 介绍视频
|
|
|
+ 'http://121.36.251.245:9000/minlong/tenant_1/general_uploads/e465e23d377b4456bbb3b755d3ad9500.mp4',
|
|
|
+ 'http://121.36.251.245:9000/minlong/tenant_1/general_uploads/9bc84230d2a14978b4ea0a97e4102a15.mp4',
|
|
|
+ 'http://121.36.251.245:9000/minlong/tenant_1/general_uploads/9b48e824432f451d9e27e12b884d9074.mp4',
|
|
|
+ 'http://121.36.251.245:9000/minlong/tenant_1/general_uploads/52f1445b400345e1a673b3c7f05e5cc1.mp4',
|
|
|
+ 'http://121.36.251.245:9000/minlong/tenant_1/general_uploads/abdaa6fda8494e3a8613304743ed0433.mp4'
|
|
|
+ ]; */
|
|
|
+
|
|
|
+ // 设置默认字幕
|
|
|
+ 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) {
|
|
|
+ // 检查索引是否有效
|
|
|
+ if (videoIndex >= 0 && videoIndex < this.additionalQuestions.length) {
|
|
|
+ return this.additionalQuestions[videoIndex];
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ },
|
|
|
+
|
|
|
+ onShow() {
|
|
|
+ console.log('followUp页面onShow');
|
|
|
+ console.log('当前本地存储中的职位信息:', uni.getStorageSync('selectedJob'));
|
|
|
+
|
|
|
+ // 每次页面显示时重新获取追加问题
|
|
|
+ this.fetchAdditionalQuestions();
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取追加问题
|
|
|
+ fetchAdditionalQuestions() {
|
|
|
+ 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 requestUrl = `${apiBaseUrl}/api/job/additional_questions?position_id=${position_id}&tenant_id=${tenant_id}&count=2`;
|
|
|
+
|
|
|
+ uni.request({
|
|
|
+ url: requestUrl,
|
|
|
+ method: 'GET',
|
|
|
+ success: (res) => {
|
|
|
+ if (res.data && res.data.code === 2000 && Array.isArray(res.data.data)) {
|
|
|
+ console.log('获取追加问题成功:', res.data.data);
|
|
|
+
|
|
|
+ // 处理问题数据,添加视频URL和字幕
|
|
|
+ this.additionalQuestions = res.data.data.map(question => ({
|
|
|
+ ...question,
|
|
|
+ videoUrl: question.digital_human_video_url,
|
|
|
+ subtitles: [{
|
|
|
+ startTime: 0,
|
|
|
+ endTime: 60, // 增加字幕显示时间到60秒,确保覆盖整个视频
|
|
|
+ text: question.digital_human_video_subtitle || question.question || '请回答问题'
|
|
|
+ }]
|
|
|
+ }));
|
|
|
+
|
|
|
+ // 初始化视频列表
|
|
|
+ this.initializeVideoList();
|
|
|
+ } else {
|
|
|
+ console.error('获取追加问题失败:', res.data);
|
|
|
+ uni.showToast({
|
|
|
+ title: '获取追加问题失败',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('请求追加问题失败:', err);
|
|
|
+ uni.showToast({
|
|
|
+ title: '网络错误,请检查网络连接',
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ },
|
|
|
+ complete: () => {
|
|
|
+ this.loading = false;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 初始化视频列表
|
|
|
+ initializeVideoList() {
|
|
|
+ // 清空现有视频列表
|
|
|
+ this.videoList = [];
|
|
|
+
|
|
|
+ // 添加追加问题的视频
|
|
|
+ this.additionalQuestions.forEach(question => {
|
|
|
+ if (question.videoUrl) {
|
|
|
+ this.videoList.push(question.videoUrl);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 如果有视频,开始播放第一个
|
|
|
+ if (this.videoList.length > 0) {
|
|
|
+ this.videoUrl = this.videoList[0];
|
|
|
+ this.currentVideoIndex = 0;
|
|
|
+ this.playCurrentVideo();
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 播放当前视频
|
|
|
+ playCurrentVideo() {
|
|
|
+ if (this.currentVideoIndex < this.videoList.length) {
|
|
|
+ this.videoUrl = this.videoList[this.currentVideoIndex];
|
|
|
+ this.videoPlaying = true;
|
|
|
+
|
|
|
+ // 获取当前问题
|
|
|
+ const currentQuestion = this.additionalQuestions[this.currentVideoIndex];
|
|
|
+
|
|
|
+ // 设置当前字幕 - 直接设置问题文本作为字幕
|
|
|
+ if (currentQuestion) {
|
|
|
+ this.currentSubtitle = currentQuestion.question ||
|
|
|
+ currentQuestion.digital_human_video_subtitle ||
|
|
|
+ '请回答问题';
|
|
|
+ }
|
|
|
+
|
|
|
+ this.$nextTick(() => {
|
|
|
+ const videoContext = uni.createVideoContext('myVideo', this);
|
|
|
+ if (videoContext) {
|
|
|
+ videoContext.play();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 添加 handleVideoPlay 方法
|
|
|
+ handleVideoPlay() {
|
|
|
+ console.log('视频开始播放');
|
|
|
+
|
|
|
+ // 设置当前问题的字幕
|
|
|
+ if (this.currentVideoIndex >= 0 && this.currentVideoIndex < this.additionalQuestions.length) {
|
|
|
+ const currentQuestion = this.additionalQuestions[this.currentVideoIndex];
|
|
|
+ if (currentQuestion) {
|
|
|
+ this.currentSubtitle = currentQuestion.question ||
|
|
|
+ currentQuestion.digital_human_video_subtitle ||
|
|
|
+ '请回答问题';
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.identity-verify-container {
|
|
|
+ padding: 0;
|
|
|
+ max-width: 100%;
|
|
|
+ margin: 0 auto;
|
|
|
+ height: 100vh;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+}
|
|
|
+
|
|
|
+.digital-human-container {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh;
|
|
|
+ overflow: hidden;
|
|
|
+ background-color: #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.digital-human-video {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+/* 用户摄像头容器样式 */
|
|
|
+.user-camera-container {
|
|
|
+ position: absolute;
|
|
|
+ top: 50px;
|
|
|
+ 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;
|
|
|
+
|
|
|
+}
|
|
|
+
|
|
|
+/* 用户摄像头视频样式 */
|
|
|
+.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: 5%; /* 从左侧留出5%的空间 */
|
|
|
+ width: 80%; /* 宽度设为90%,两侧各留5%实现居中 */
|
|
|
+ padding: 15px 20px; /* 增加左右内边距 */
|
|
|
+ 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); /* 添加轻微阴影 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 添加计时器样式,右侧显示橙色圆点和时间 */
|
|
|
+.subtitle-overlay::after {
|
|
|
+ content: attr(data-time);
|
|
|
+ position: absolute;
|
|
|
+ right: 20px;
|
|
|
+ bottom: 15px;
|
|
|
+ color: #333;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.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: #e74c3c; /* 白色背景 */
|
|
|
+ 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; /* 移除脉动动画 */
|
|
|
+}
|
|
|
+
|
|
|
+.recording-indicator {
|
|
|
+ position: absolute;
|
|
|
+ top: 20px;
|
|
|
+ left: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ background-color: rgba(255, 255, 255, 0.9); /* 改为白色半透明背景 */
|
|
|
+ padding: 5px 10px;
|
|
|
+ border-radius: 15px;
|
|
|
+ z-index: 20;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.recording-dot {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ background-color: #ff6b00; /* 改为橙色,与图片中的颜色一致 */
|
|
|
+ border-radius: 50%;
|
|
|
+ margin-right: 8px;
|
|
|
+ animation: blink 1s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.recording-text {
|
|
|
+ color: #333; /* 改为深色文字 */
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.timer-text {
|
|
|
+ color: #333; /* 改为深色文字 */
|
|
|
+ font-size: 14px;
|
|
|
+ margin-left: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.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在视频上方但在字幕和按钮下方 */
|
|
|
+}
|
|
|
+</style>
|