<template>
<div class="voice-interview-page">
<!-- 顶部状态栏 -->
<div class="interview-header">
<h1 class="interview-title">智能语音面试</h1>
<div class="progress-bar">
<span class="progress-text">问题 {{ currentQuestionIndex }}/{{ totalQuestions }}</span>
<el-progress
:percentage="(currentQuestionIndex / totalQuestions) * 100"
:show-text="false"
stroke-width="8"
/>
</div>
</div>
<!-- 主要内容区域 -->
<div class="interview-main">
<el-row :gutter="20">
<!-- 左侧:视频录制部分 -->
<el-col :span="8">
<div class="video-section">
<div class="video-card">
<h2 class="video-title">
<i class="el-icon-video-camera"></i>
视频录制
</h2>
<p class="video-tip">系统将自动录制您回答问题的过程</p>
<VideoRecorder
ref="videoRecorderRef"
:session-id="sessionId"
:question-index="currentQuestionIndex"
:auto-start="true"
@video-submitted="handleVideoSubmitted"
@video-upload-failed="handleVideoUploadFailed"
/>
</div>
</div>
</el-col>
<!-- 中间:数字人区域 -->
<el-col :span="8">
<div class="avatar-section">
<div class="avatar-card">
<!-- <h2 class="avatar-title">
<i class="el-icon-user"></i>
数字人面试官
</h2>-->
<div class="avatar-container">
<div class="weather rain" id="wrapper"></div>
<!-- <div class="avatar-controls">
<span>透明度</span>
<input type="range" id="opacityRange" min="0" max="1" step="0.1" value="1">
</div>-->
</div>
</div>
</div>
</el-col>
<!-- 右侧:语音交互部分 -->
<el-col :span="8">
<!-- 状态指示器 -->
<div class="status-indicator">
<div v-if="isReadingQuestion" class="status-item reading">
<div class="status-icon">
<i class="el-icon-s-promotion"></i>
</div>
<div class="status-content">
<h3>正在播报题目</h3>
<p>请仔细聆听面试问题...</p>
</div>
</div>
<div v-else-if="isRecording" class="status-item recording">
<div class="status-icon recording-pulse">
<i class="el-icon-microphone"></i>
</div>
<div class="status-content">
<h3>请开始回答</h3>
<p>录音时间: {{ formatTime(recordingTime) }}</p>
</div>
</div>
<div v-else-if="isProcessing" class="status-item processing">
<div class="status-icon">
<i class="el-icon-loading"></i>
</div>
<div class="status-content">
<h3>正在处理</h3>
<p>请稍候,正在提交您的回答...</p>
</div>
</div>
<div v-else class="status-item ready">
<div class="status-icon">
<i class="el-icon-check"></i>
</div>
<div class="status-content">
<h3>准备就绪</h3>
<p>点击开始按钮开始面试</p>
</div>
</div>
</div>
<!-- 题目显示区域 -->
<div class="question-display">
<div class="question-card">
<h2 class="question-title">面试问题</h2>
<div class="question-content">
<p>{{ currentQuestion }}</p>
</div>
</div>
</div>
<!-- 实时转录显示区域 -->
<div class="transcription-display" v-if="isRecording || transcriptionResult">
<div class="transcription-card">
<h3 class="transcription-title">
<i class="el-icon-chat-line-round"></i>
{{ isRecording ? '实时转录' : '您的回答' }}
</h3>
<div class="transcription-content">
<div class="transcription-text" v-if="transcriptionResult">
{{ transcriptionResult }}
</div>
<div class="transcription-placeholder" v-else>
<i class="el-icon-microphone"></i>
<p>请开始说话,系统将实时转录您的回答...</p>
</div>
</div>
<!-- 录音控制按钮 -->
<div class="transcription-actions" v-if="isRecording">
<el-button
type="success"
size="large"
@click="finishAnswer"
:disabled="!transcriptionResult">
<i class="el-icon-check"></i>
完成回答
</el-button>
<el-button
type="danger"
size="large"
@click="cancelRecording">
<i class="el-icon-close"></i>
取消录音
</el-button>
</div>
<!-- 提交按钮 -->
<div class="transcription-actions" v-if="!isRecording && transcriptionResult && !isProcessing">
<el-button
type="primary"
size="large"
@click="submitAnswer">
<i class="el-icon-right"></i>
{{ currentQuestionIndex < totalQuestions ? '下一题' : '完成面试' }}
</el-button>
<el-button
size="large"
@click="retryAnswer">
<i class="el-icon-refresh"></i>
重新回答
</el-button>
</div>
</div>
</div>
</el-col>
</el-row>
<!-- 评估结果显示 -->
<!-- 移除整个评估结果显示区域 -->
<!-- 删除以下代码块:
<div class="evaluation-display" v-if="currentEvaluation">
<div class="evaluation-card">
<h3 class="evaluation-title">
<i class="el-icon-trophy"></i>
本题评估
</h3>
<div class="evaluation-content">
<div class="evaluation-score">
<el-progress
type="circle"
:percentage="evaluationScore"
:color="scoreColor"
:width="80">
<template #default="{ percentage }">
<span class="score-text">{{ percentage }}</span>
</template>
</el-progress>
</div>
<div class="evaluation-details">
<div class="evaluation-section" v-if="currentEvaluation.strengths?.length">
<h4>优点</h4>
<ul>
<li v-for="(strength, index) in currentEvaluation.strengths" :key="index">
{{ strength }}
</li>
</ul>
</div>
<div class="evaluation-section" v-if="currentEvaluation.suggestions">
<h4>建议</h4>
<p>{{ currentEvaluation.suggestions }}</p>
</div>
</div>
</div>
</div>
</div>
-->
</div>
<!-- 隐藏的录音组件 -->
<AudioRecorder
ref="audioRecorderRef"
:session-id="sessionId"
@transcription-update="handleTranscriptionUpdate"
@transcription-complete="handleTranscriptionComplete"
style="display: none;"
/>
<!-- 面试完成对话框 -->
<el-dialog
v-model="showCompletionDialog"
title="面试完成"
width="500px"
:close-on-click-modal="false"
:close-on-press-escape="false">
<div class="completion-content">
<div class="completion-icon">
<i class="el-icon-success"></i>
</div>
<h3>恭喜您完成了本次面试!</h3>
<p>系统正在生成您的面试报告,请稍候...</p>
</div>
<template #footer>
<el-button type="primary" @click="viewResults">
查看面试结果
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive,computed, onMounted, onUnmounted, watch ,onBeforeUnmount} from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { ElMessage, ElMessageBox } from 'element-plus';
import { interviewAPI } from '../services/api';
import AudioRecorder from '../components/AudioRecorder.vue';
import VideoRecorder from '../components/VideoRecorder.vue';
import XfyunTTSService from '../services/xfyunTTS.js';
import { TTS_CONFIG } from '../config/tts.js';
import AvatarPlatform, {PlayerEvents, SDKEvents,} from "../vm-sdk/avatar-sdk-web_3.1.1.1011/index.js";
const route = useRoute();
const router = useRouter();
const sessionId = route.params.sessionId;
// 基础状态
const currentQuestion = ref('正在加载面试问题...');
const currentQuestionIndex = ref(1);
const totalQuestions = ref(5);
const transcriptionResult = ref('');
const currentEvaluation = ref(null);
const showCompletionDialog = ref(false);
// 流程状态
const isReadingQuestion = ref(false);
const isRecording = ref(false);
const isProcessing = ref(false);
const recordingTime = ref(0);
const recordingTimer = ref(null);
// 组件引用
const audioRecorderRef = ref(null);
const videoRecorderRef = ref(null);
const ttsService = new XfyunTTSService();
// 全局变量
let avatarPlatform2 = null
let recorder = null
// 响应式数据
const SetApiInfodialog = ref(false)
const SetGlobalParamsdialog = ref(false)
const textarea = ref("")
const vc = ref("")
const recorderbutton = ref(false)
const nlp = ref(false)
const emotion = ref(0)
const action = ref("A_RH_hello_O")
const volume = ref(100)
const formLabelWidth = ref("120px")
// 表单数据
const form = reactive({
appid: "2e96547f", // 到交互平台-接口服务中获取
apikey: "98e4b25f7fe16f861124b62789d4cb2a", // 到交互平台-接口服务中获取
apisecret: "YjM2Nzc1NjM2ZDMwNTdhZDZjY2UzNWVm", // 到交互平台-接口服务中获取
sceneid: "193755375642742784", // 到交互平台-接口服务中获取,即"接口服务ID"
serverurl: "wss://avatar.cn-huadong-1.xf-yun.com/v1/interact", // 接口地址,无需更改
})
// 全局参数表单数据
const setglobalparamsform = reactive({
stream: {
protocol: "xrtc", // (必传)实时视频协议,支持webrtc/xrtc/rtmp,其中只有xrtc支持透明背景,需参数alpha传1
fps: 25, // (非必传)视频刷新率,值越大,越流畅,取值范围0-25,默认25即可
bitrate: 1000000, // (非必传)视频码率,值越大,越清晰,对网络要求越高,默认1000000即可
alpha: true, // (非必传)是否开启透明背景,0关闭1开始,需配合protocol=xrtc使用
},
avatar: {
avatar_id: "110332017", // (必传)授权的形象资源id,请到交互平台-接口服务-形象列表中获取
width: 1080, // (非必传)视频分辨率宽(不是画布的宽,调整画布大小需调整名为wrapper的div宽)
height: 1920, // (非必传)视频分辨率高(不是画布的高,调整画布大小需调整名为wrapper的div高)
mask_region: "[0,0,1080,1920]", // (非必传)形象裁剪参数,[从左到右,从上到下,从右到左,从下到上]
scale: 1, // (非必传)形象缩放比例,取值范围0.1-1
move_h: 0, // (非必传)形象左右移动
move_v: 0, // (非必传)形象上下移动
audio_format: 1, // (非必传)音频采样率,传1即可
},
tts: {
vcn: "x4_lingxiaoying_assist", // (必传)授权的声音资源id,请到交互平台-接口服务-声音列表中获取
speed: 50, // (非必传)语速
pitch: 50, // (非必传)语调
volume: 100, // (非必传)音量
emotion: 13, // (非必传)情感系数,仅带有情感能力的超拟人音色支持该能力,普通音色不支持
},
avatar_dispatch: {
interactive_mode: 1, // (非必传)0追加模式,1打断模式
},
subtitle: {
subtitle: 1, // (非必传)开启字幕,2D形象支持字幕,透明背景不支持字幕,3D形象不支持字幕(3D形象多为卡通形象,2D多为真人形象)
font_color: "#FFFFFF", // (非必传)字体颜色
font_name: "Sanji.Suxian.Simple", // (非必传)不支持自定义字体,若不想使用默认提供的
// 字体,那么可以设置asr和nlp监听事件,去获取语音识别和语义理解的文本,自己前端贴字体。
// 支持一下字体:'Sanji.Suxian.Simple','Honglei.Runninghand.Sim','Hunyuan.Gothic.Bold',
// 'Huayuan.Gothic.Regular','mainTitle'
position_x: 100, // (非必传)设置字幕水平位置,必须配置width、height一起使用,否则字幕不显示
position_y: 0, // (非必传)设置字幕竖向位置,必须配置width、height一起使用,否则字幕不显示
font_size: 10, // (非必传)设置字幕字体大小,取值范围:1-10
width: 100, // (非必传)设置字幕宽
height: 100, // (非必传)设置字幕高
},
enable: false, // demo中用来控制是否开启背景的参数,与虚拟人参数无关
background: {
type: "res_key", // (非必传)上传图片的类型,支持url以及res_key。(res_key请到交互平台-素材管理-背景中上传获取)
data: "22SLM2teIw+aqR6Xsm2JbH6Ng310kDam2NiCY/RQ9n6dw47gMO+7gGUJfWWfkqD3IxsU/HMK1uJTTxxF2llcKSM4dlSdBy0Piag/DndHocqs32kTOwXUw6lkyggYQBXF0uwTv9jVFm1ZjZgSehV3kpx5RTvizZ9MqEI8lotCRvokC9HLI0pGfKtSmlKgCKL+OUoc9QI5HW3wLtYbLersumd4UCKEPk/uWAdKEh4ntSJiW2km8waGFsg/VSNFj5vaDK3LC4PxfsRvi1a2veZW7JUs/VOleE9wwgTH+A/oqPPcyksBY7aQ4TxYjvS9Qj9LtXkvOwttQMgPGwoxlqBEBhR/xLUwmecHkHzgjACFtxE=", // (非必传)图片的值,当type='url'时,data='http://xxx/xxx.png',当type='res_key'时,data='res_key值'(res_key请到交互平台-素材管理-背景中上传获取)
}
})
// 计算属性
const evaluationScore = computed(() => {
if (!currentEvaluation.value?.score) return 0;
return Math.min(Math.round(currentEvaluation.value.score * 10), 100);
});
const scoreColor = computed(() => {
const score = evaluationScore.value;
if (score >= 80) return '#67C23A';
if (score >= 60) return '#E6A23C';
return '#F56C6C';
});
// TTS 服务配置
ttsService.onPlay(() => {
isReadingQuestion.value = true;
});
ttsService.onStop(() => {
isReadingQuestion.value = false;
// 播报完成后自动开始录音
setTimeout(() => {
startRecording();
}, 1000);
});
ttsService.onError((error) => {
console.error('TTS错误:', error);
isReadingQuestion.value = false;
ElMessage.error('语音播报失败,请手动开始录音');
});
// 格式化时间
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
};
// 开始问题流程(播报题目)
const startQuestionFlow = async () => {
if (!currentQuestion.value || currentQuestion.value === '正在加载面试问题...') {
ElMessage.warning('题目尚未加载完成');
return;
}
try {
// 清空之前的转录结果
transcriptionResult.value = '';
currentEvaluation.value = null;
// 开始播报题目
/*await ttsService.speak(currentQuestion.value, {
...TTS_CONFIG.DEFAULT_OPTIONS,
vcn: 'xiaoyan',
speed: 45,
volume: 80
});*/
writeText();
} catch (error) {
console.error('播报题目失败:', error);
ElMessage.error('语音播报失败,请手动开始录音');
isReadingQuestion.value = false;
}
};
// 开始录音
const startRecording = async () => {
try {
isRecording.value = true;
recordingTime.value = 0;
// 启动录音计时器
recordingTimer.value = setInterval(() => {
recordingTime.value += 1;
}, 1000);
// 启动录音组件
await audioRecorderRef.value?.startRealTimeTranscription();
ElMessage.success('开始录音,请回答问题');
} catch (error) {
console.error('启动录音失败:', error);
ElMessage.error('启动录音失败,请检查麦克风权限');
isRecording.value = false;
clearInterval(recordingTimer.value);
}
};
// 完成回答
const finishAnswer = () => {
if (!transcriptionResult.value) {
ElMessage.warning('请先回答问题');
return;
}
// 停止录音
audioRecorderRef.value?.stopRealTimeTranscription();
isRecording.value = false;
clearInterval(recordingTimer.value);
ElMessage.success('录音完成,请检查您的回答');
};
// 取消录音
const cancelRecording = async () => {
try {
const result = await ElMessageBox.confirm(
'确定要取消当前录音吗?录音内容将丢失。',
'确认取消',
{
confirmButtonText: '确定',
cancelButtonText: '继续录音',
type: 'warning'
}
);
if (result === 'confirm') {
audioRecorderRef.value?.stopRealTimeTranscription();
isRecording.value = false;
transcriptionResult.value = '';
clearInterval(recordingTimer.value);
ElMessage.info('已取消录音');
}
} catch {
// 用户点击了取消,继续录音
}
};
// 重新回答
const retryAnswer = () => {
transcriptionResult.value = '';
currentEvaluation.value = null;
startRecording();
};
// 提交答案
// 修改 submitAnswer 方法
const submitAnswer = async () => {
if (!transcriptionResult.value?.trim()) {
ElMessage.warning('请先录音回答问题');
return;
}
isProcessing.value = true;
try {
// 停止录音
if (isRecording.value) {
await stopVoiceRecognition();
}
// 处理视频录制
if (videoRecorderRef.value && videoRecorderRef.value.isRecording) {
try {
await videoRecorderRef.value.stopRecording();
await videoRecorderRef.value.submitVideo();
} catch (videoError) {
console.error('处理视频录制时出错:', videoError);
// 继续提交答案,不阻止流程
}
}
const response = await interviewAPI.answerQuestion(sessionId, transcriptionResult.value);
// 不处理评估结果显示,直接处理下一题逻辑
// 移除评估相关代码
// 更新问题计数
if (response.data.questionCount) {
currentQuestionIndex.value = response.data.questionCount;
}
// 检查面试是否完成
if (response.data.isComplete) {
showCompletionDialog.value = true;
// 如果面试完成,关闭摄像头
if (videoRecorderRef.value) {
videoRecorderRef.value.stopCamera();
}
} else {
// 自动准备下一题
currentQuestion.value = response.data.nextQuestion || response.data.question;
// 清空转录结果
transcriptionResult.value = '';
// clearTranscription(); // 删除这一行
// 延迟一下再自动开始下一题,给用户一个缓冲
setTimeout(() => {
startQuestionFlow();
}, 1000);
}
} catch (error) {
console.error('提交答案失败:', error);
ElMessage.error('提交答案失败,请重试');
} finally {
isProcessing.value = false;
}
};
// 处理视频提交成功事件
const handleVideoSubmitted = (videoUrl) => {
console.log('视频提交成功:', videoUrl);
};
// 处理视频提交失败事件
const handleVideoUploadFailed = (error) => {
console.error('视频提交失败:', error);
};
// 处理转录更新
const handleTranscriptionUpdate = (data) => {
transcriptionResult.value = data.text;
};
// 处理转录完成
const handleTranscriptionComplete = (text) => {
transcriptionResult.value = text;
};
// 查看结果
const viewResults = () => {
router.push(`/results/${sessionId}`);
};
// 初始化面试
const initializeInterview = async () => {
try {
if (route.query.initialQuestion) {
currentQuestion.value = route.query.initialQuestion;
if (route.query.questionCount) {
totalQuestions.value = parseInt(route.query.questionCount);
}
} else {
const response = await interviewAPI.getInterviewResults(sessionId);
if (response.data.current_question) {
currentQuestion.value = response.data.current_question;
} else if (response.data.questions && response.data.questions.length > 0) {
currentQuestion.value = response.data.questions[0];
} else {
ElMessage.warning('无法获取面试问题,请从设置页面开始面试');
setTimeout(() => router.push('/setup'), 2000);
return;
}
if (response.data.question_count_limit) {
totalQuestions.value = response.data.question_count_limit;
}
if (response.data.isComplete) {
ElMessage.info('此面试会话已完成');
showCompletionDialog.value = true;
return;
}
}
// 自动开始第一题
setTimeout(() => {
startQuestionFlow();
}, 2000);
} catch (error) {
console.error('获取面试问题失败:', error);
ElMessage.warning('无法获取面试问题,请从设置页面开始面试');
setTimeout(() => router.push('/setup'), 2000);
}
};
//数字人部分
const initSDK = () => {
// 必须先实例化SDK,再去调用其挂载的方法
avatarPlatform2 = new AvatarPlatform()
if (avatarPlatform2 != null) {
open2("实例化SDK成功")
}
}
const createRecoder = () => {
if (avatarPlatform2 != null) {
recorder = avatarPlatform2.createRecorder()
open2("创建录音器成功")
} else {
alert("请实例化SDK实例")
}
}
const setSDKEvenet = () => {
// 绑定SDK事件
if (avatarPlatform2 != null) {
avatarPlatform2
.on(SDKEvents.connected, function (initResp) {
console.log("SDKEvent.connect:initResp:", initResp)
})
.on(SDKEvents.stream_start, function () {
console.log("stream_start")
})
.on(SDKEvents.disconnected, function (err) {
console.log("SDKEvent.disconnected:", err)
if (err) {
// 因为异常 而导致的断开! 此处可以进行 提示通知等
console.error("ws link disconnected because of Error")
console.error(err.code, err.message, err.name, err.stack)
}
})
.on(SDKEvents.nlp, function (nlpData) {
console.log("语义理解内容nlp:", nlpData)
})
.on(SDKEvents.frame_start, function (frame_start) {
console.log(
"推流开始(可以看作一段文本开始播报时间点)frame_start:",
frame_start
)
})
.on(SDKEvents.frame_stop, function (frame_stop) {
console.log(
"推流结束(可以看作一段文本结束播报时间点)frame_stop:",
frame_stop
)
if (isReadingQuestion.value) {
isReadingQuestion.value = false;
setTimeout(() => {
startRecording();
}, 1000);
}
})
.on(SDKEvents.error, function (error) {
console.log("错误信息error:", error)
})
.on(SDKEvents.connected, function () {
console.log("connected")
})
.on(SDKEvents.asr, function (asrData) {
console.log("语音识别数据asr:", asrData)
})
.on(SDKEvents.tts_duration, function (ttsData) {
console.log("语音合成用时tts:", ttsData)
})
.on(SDKEvents.subtitle_info, function (subtitleData) {
console.log("subtitleData:", subtitleData)
})
.on(SDKEvents.action_start, function (action_start) {
console.log(
"动作推流开始(可以看作动作开始时间节点)action_start:",
action_start
)
})
.on(SDKEvents.action_stop, function (action_stop) {
console.log(
"动作推流结束(可以看作动作结束时间点)action_stop:",
action_stop
)
})
open2("监听SDK事件成功")
} else {
alert("请先实例化SDK")
}
}
const setPlayerEvenet = () => {
if (avatarPlatform2 != null) {
// 绑定播放器事件
const player = avatarPlatform2.createPlayer()
player
.on(PlayerEvents.play, function () {
console.log("paly")
})
.on(PlayerEvents.playing, function () {
console.log("playing")
})
.on(PlayerEvents.waiting, function () {
console.log("waiting")
})
.on(PlayerEvents.stop, function () {
console.log("stop")
})
.on(PlayerEvents.playNotAllowed, function () {
console.log(
"playNotAllowed:触发了游览器限制自动播放策略,播放前必须与游览器产生交互(例如点击页面或者dom组件),触发该事件后调用avatarPlatform2.player.resume()方法来接触限制"
)
player.resume()
})
open2("监听播放器事件成功")
} else {
alert("请先实例化SDK")
}
}
const SetApiInfo2 = () => {
if (avatarPlatform2 == null) {
alert("请先实例化SDK")
} else {
console.log("设置setApiInfo")
const params = {
appId: form.appid,
apiKey: form.apikey,
apiSecret: form.apisecret,
serverUrl: form.serverurl,
sceneId: form.sceneid,
}
console.log("初始化SDK信息:", params)
// 初始化SDK
avatarPlatform2.setApiInfo(params)
open2("初始化SDK成功")
}
}
const SetGlobalParams = () => {
if (avatarPlatform2 != null) {
let params = Object.assign({}, setglobalparamsform)
console.log("setglobalparamsform.stream.alpha", setglobalparamsform.stream.alpha)
if (setglobalparamsform.enable == false) {
delete params.background
delete params.enable
}
console.log("setglobalparamsform", setglobalparamsform)
if (setglobalparamsform.stream.alpha == true) {
console.log("设置alpha=1")
params.stream.alpha = 1
} else {
console.log("设置alpha=0")
params.stream.alpha = 0
}
console.log("设置的全局变量为:", params)
avatarPlatform2.setGlobalParams(params)
open2("设置全局变量成功")
} else {
alert("请先实例化SDK")
}
}
const start = () => {
if (avatarPlatform2 != null) {
avatarPlatform2
.start({ wrapper: document.querySelector("#wrapper") })
.catch((e) => {
console.error(e.code, e.message, e.name, e.stack)
})
} else {
alert("请先实例化SDK")
}
}
const writeText = () => {
if (avatarPlatform2 != null) {
isReadingQuestion.value = true;
const text = currentQuestion.value;
if (text != "" && vc.value == "") {
avatarPlatform2.writeText(text, {
nlp: nlp.value, // 是否开启语义理解
tts: {
volume: 100,
},
});
} else if (text != "" && vc.value != "") {
avatarPlatform2.writeText(text, {
nlp: nlp.value, // 是否开启语义理解
tts: {
vcn: vc.value, // 变声
volume: 100,
emotion: emotion.value,
},
});
} else {
alert("内容不许为空");
}
} else {
isReadingQuestion.value = false;
alert("请先实例化SDK");
}
};
const writeCmd = () => {
avatarPlatform2.writeCmd("action", action.value)
}
const interrupt = () => {
if (avatarPlatform2 != null) {
avatarPlatform2.interrupt()
} else {
alert("请先实例化SDK")
}
}
const startRecord = () => {
if (avatarPlatform2 != null) {
avatarPlatform2.recorder.startRecord(
0,
() => {
console.warn('STOPED RECORDER')
},
{
nlp: true,
avatar_dispatch: {
interactive_mode: 0 // 交互模式(追加或打断)
}
}
)
// 关闭录音按钮显示
recorderbutton.value = true
} else {
alert("请先实例化SDK")
}
}
const stopRecord = () => {
if (avatarPlatform2 != null) {
avatarPlatform2.recorder.stopRecord()
// 开启录音按钮显示
recorderbutton.value = false
} else {
alert("请先实例化SDK")
}
}
const stop = () => {
if (avatarPlatform2 != null) {
avatarPlatform2.stop()
} else {
alert("请先实例化SDK")
}
}
const destroy = () => {
if (avatarPlatform2 != null) {
// 销毁SDK示例,内部包含stop协议,重启需重新示例化avatarPlatform实例
avatarPlatform2.destroy()
avatarPlatform2 = null
} else {
alert("请先实例化SDK")
}
}
const open2 = (text) => {
/*ElMessage({
message: text,
type: "success",
})*/
}
// 生命周期
onMounted(() => {
const div = document.getElementById('wrapper')
const range = document.getElementById('opacityRange')
if (div && range) {
range.addEventListener('input', function () {
div.style.opacity = this.value
})
}
initSDK();
createRecoder();
setSDKEvenet();
setPlayerEvenet();
SetApiInfo2();
SetGlobalParams();
start();
initializeInterview();
});
onUnmounted(() => {
// 清理资源
ttsService.stop();
clearInterval(recordingTimer.value);
if (audioRecorderRef.value) {
audioRecorderRef.value.stopRealTimeTranscription();
}
// 确保关闭摄像头
if (videoRecorderRef.value) {
videoRecorderRef.value.stopCamera();
}
});
// 组件卸载前清理资源
onBeforeUnmount(() => {
// 关闭页面时调用stop协议,确保链接断开,释放资源
if (avatarPlatform2) {
avatarPlatform2.stop()
}
});
</script>
<style scoped>
.voice-interview-page {
min-height: 100vh;
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%); /* 浅蓝色渐变 */
padding: 20px;
}
.interview-header {
text-align: center;
margin-bottom: 30px;
}
.interview-title {
color: white;
font-size: 2.5rem;
margin-bottom: 20px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.progress-bar {
max-width: 400px;
margin: 0 auto;
background: rgba(255,255,255,0.1);
padding: 15px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
.progress-text {
color: white;
font-size: 1.1rem;
display: block;
margin-bottom: 10px;
}
.interview-main {
max-width: 1400px;
margin: 0 auto;
}
/* 视频录制区域样式 */
.video-section {
margin-bottom: 20px;
}
.video-card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
height: 100%;
}
.video-title {
color: #303133;
font-size: 1.4rem;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
text-align: center;
justify-content: center;
}
.video-tip {
color: #666;
margin-bottom: 15px;
text-align: center;
font-size: 0.9rem;
}
/* 数字人区域样式 */
.avatar-section {
margin-bottom: 20px;
}
.avatar-card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
height: 100%;
}
.avatar-title {
color: #303133;
font-size: 1.4rem;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
text-align: center;
justify-content: center;
}
.avatar-container {
display: flex;
flex-direction: column;
align-items: center;
height: 600px;
}
#wrapper {
width: 400px;
height: 500px;
background: #f5f7fa;
border: 2px solid #dcdfe6;
border-radius: 10px;
margin-bottom: 20px;
position: relative;
overflow: hidden;
}
.avatar-controls {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
.avatar-controls span {
color: #606266;
font-size: 0.9rem;
}
.avatar-controls input[type="range"] {
width: 150px;
}
.status-indicator {
margin-bottom: 20px;
}
.status-item {
display: flex;
align-items: center;
gap: 20px;
background: white;
padding: 20px;
border-radius: 15px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}
.status-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
}
.status-item.reading .status-icon {
background: #409EFF;
}
.status-item.recording .status-icon {
background: #E6A23C;
}
.status-item.processing .status-icon {
background: #909399;
}
.status-item.ready .status-icon {
background: #67C23A;
}
.recording-pulse {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
.status-content h3 {
margin: 0 0 5px 0;
color: #303133;
font-size: 1.3rem;
}
.status-content p {
margin: 0;
color: #606266;
font-size: 1rem;
}
.question-display,
.transcription-display,
.evaluation-display {
margin-bottom: 20px;
}
.question-card,
.transcription-card,
.evaluation-card {
background: white;
border-radius: 15px;
padding: 25px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
}
.question-title,
.transcription-title,
.evaluation-title {
color: #303133;
font-size: 1.4rem;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 10px;
}
.question-content {
background: #f5f7fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
border-left: 4px solid #409EFF;
}
.question-content p {
font-size: 1.2rem;
line-height: 1.6;
margin: 0;
color: #303133;
}
.question-actions {
text-align: center;
}
.transcription-content {
min-height: 120px;
margin-bottom: 20px;
}
.transcription-text {
background: #f0f9ff;
border: 2px solid #409EFF;
border-radius: 10px;
padding: 20px;
font-size: 1.1rem;
line-height: 1.6;
color: #303133;
}
.transcription-placeholder {
text-align: center;
color: #909399;
padding: 40px 20px;
}
.transcription-placeholder i {
font-size: 48px;
margin-bottom: 15px;
display: block;
}
.transcription-actions {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.evaluation-content {
display: flex;
gap: 30px;
align-items: flex-start;
}
.evaluation-score {
flex-shrink: 0;
}
.score-text {
font-size: 18px;
font-weight: bold;
}
.evaluation-details {
flex: 1;
}
.evaluation-section {
margin-bottom: 20px;
}
.evaluation-section h4 {
color: #409EFF;
margin-bottom: 10px;
font-size: 1.1rem;
}
.evaluation-section ul {
margin: 0;
padding-left: 20px;
}
.evaluation-section li {
margin-bottom: 5px;
color: #606266;
}
.completion-content {
text-align: center;
padding: 20px;
}
.completion-icon {
font-size: 64px;
color: #67C23A;
margin-bottom: 20px;
}
.completion-content h3 {
color: #303133;
margin-bottom: 15px;
}
.completion-content p {
color: #606266;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.interview-main {
max-width: 100%;
}
.avatar-container {
height: 500px;
}
#wrapper {
width: 320px;
height: 420px;
}
}
@media (max-width: 768px) {
.voice-interview-page {
padding: 10px;
}
.interview-title {
font-size: 2rem;
}
.status-item {
flex-direction: column;
text-align: center;
gap: 15px;
}
.evaluation-content {
flex-direction: column;
align-items: center;
}
.transcription-actions {
flex-direction: column;
}
.interview-main .el-col {
margin-bottom: 20px;
}
.avatar-container {
height: 350px;
}
#wrapper {
width: 250px;
height: 300px;
}
}
</style>