camera.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  1. <template>
  2. <view class="camera-container">
  3. <!-- 顶部标题栏 -->
  4. <!-- <view class="header">
  5. <view class="back-button" @click="back">
  6. <text class="iconfont icon-back">&#xe679;</text>
  7. </view>
  8. <view class="title">中国传统文化测试</view>
  9. <view class="controls">
  10. <text class="iconfont icon-mic">&#xe677;</text>
  11. <text class="iconfont icon-settings" @click="toggleSettings">&#xe678;</text>
  12. </view>
  13. </view> -->
  14. <!-- 主要内容区域 -->
  15. <view class="content">
  16. <!-- 数字人头像 -->
  17. <view class="digital-avatar">
  18. <web-view v-if="digitalHumanUrl" :src="digitalHumanUrl" class="digital-human-webview"></web-view>
  19. <image v-else src="/static/avatar.png" mode="aspectFill" class="avatar-image"></image>
  20. </view>
  21. <!-- 进度指示器 -->
  22. <!-- <view class="progress-container">
  23. <view class="progress-step" v-for="(step, index) in 3" :key="index">
  24. <view class="step-circle" :class="{'active': currentStep >= index+1}">{{index+1}}</view>
  25. <view class="step-text">{{stepTexts[index]}}</view>
  26. </view>
  27. <view class="progress-bar">
  28. <view class="progress-fill" :style="{width: progressWidth + '%'}"></view>
  29. </view>
  30. </view> -->
  31. <!-- 面试内容区域 -->
  32. <view class="interview-content">
  33. <!-- 问题编号和进度 -->
  34. <view class="question-number">
  35. <text>{{currentQuestionIndex + 1}}-{{currentQuestion.id}}/{{questions.length}}</text>
  36. </view>
  37. <!-- 问题和选项区域 -->
  38. <view class="question-area">
  39. <view class="question-importance">
  40. <text v-if="currentQuestion.isImportant">重点题</text>
  41. <text class="question-type">[{{currentQuestion.questionTypeName}}]</text>
  42. {{currentQuestion.text}}
  43. </view>
  44. <view class="options">
  45. <view v-for="(option, index) in currentQuestion.options" :key="index" class="option-item"
  46. :class="{
  47. 'option-selected': currentQuestion.questionType === 1 ? selectedOption === index : selectedOptions.includes(index),
  48. 'option-correct': showResult && (
  49. currentQuestion.questionType === 1 ? index === currentQuestion.correctAnswer : currentQuestion.correctAnswers.includes(index)
  50. ),
  51. 'option-wrong': showResult && (
  52. currentQuestion.questionType === 1 ?
  53. (selectedOption === index && index !== currentQuestion.correctAnswer) :
  54. (selectedOptions.includes(index) && !currentQuestion.correctAnswers.includes(index))
  55. )
  56. }"
  57. @click="selectOption(index)">
  58. <!-- {{JSON.parse(option) }} -->
  59. <text class="option-text">{{ option.option_text || (typeof option === 'string' ? option : JSON.stringify(option)) }}</text>
  60. </view>
  61. </view>
  62. <view class="timer-container">
  63. <text class="timer-text">本题剩余时间 {{remainingTime}}</text>
  64. </view>
  65. <button class="next-button" @click="nextQuestion"
  66. :disabled="currentQuestion.questionType === 1 ? selectedOption === null : selectedOptions.length === 0">
  67. {{showResult ? '下一题' : '提交答案'}}
  68. </button>
  69. </view>
  70. </view>
  71. </view>
  72. <!-- 底部控制栏 -->
  73. <!-- <view class="footer">
  74. <view class="timer">本题剩余 {{remainingTime}}</view>
  75. <view class="next-step">进入下一题</view>
  76. </view> -->
  77. <!-- 面试结束弹窗 -->
  78. <view class="interview-end-modal" v-if="showEndModal">
  79. <view class="modal-content">
  80. <view class="modal-title">测试已完成</view>
  81. <view class="score-display">{{score}}/{{totalQuestions}}</view>
  82. <view class="modal-message">您在本次中国传统文化测试中答对了{{score}}道题目,共{{totalQuestions}}道题目。</view>
  83. <view class="modal-buttons">
  84. <button type="default" class="modal-button" @click="restartTest">重新测试</button>
  85. <button type="primary" class="modal-button" @click="back">返回首页</button>
  86. </view>
  87. </view>
  88. </view>
  89. <!-- AI面试结束页面 -->
  90. <view class="interview-complete-screen" v-if="interviewCompleted">
  91. <view class="digital-avatar-large">
  92. <image src="/static/avatar.png" mode="aspectFill" class="avatar-image-large"></image>
  93. </view>
  94. <view class="complete-message">AI面试已结束</view>
  95. <view class="complete-description">本次面试的全部环节已结束。非常感谢您的参与。</view>
  96. <button class="complete-button" @click="back">我知道了</button>
  97. </view>
  98. <!-- 在模板中添加一个测试按钮(开发时使用,发布前删除) -->
  99. <!-- <button @click="testEndScreen" style="position: absolute; top: 10px; right: 10px; z-index: 9999;">测试结束页</button> -->
  100. <!-- 添加加载状态 -->
  101. <view class="loading-container" v-if="loading">
  102. <uni-load-more status="loading" :contentText="{contentdown: '加载中...'}" />
  103. <text class="loading-text">正在加载面试题目...</text>
  104. </view>
  105. <!-- 添加加载错误提示 -->
  106. <view class="loading-container" v-if="!loading && loadError">
  107. <view class="error-container">
  108. <text class="error-message">加载面试题目失败</text>
  109. <button class="retry-button" @click="fetchInterviewData">重试</button>
  110. </view>
  111. </view>
  112. </view>
  113. </template>
  114. <script>
  115. import { getInterviewList, getInterviewDetail } from '@/api/user.js';
  116. export default {
  117. data() {
  118. return {
  119. cameraContext: null,
  120. devicePosition: 'front',
  121. interviewStarted: true,
  122. showEndModal: false,
  123. currentQuestionIndex: 0,
  124. currentStep: 1,
  125. stepTexts: ['测试准备', '回答问题', '测试结束'],
  126. progressWidth: 50,
  127. remainingTime: '00:27',
  128. selectedOption: null,
  129. selectedOptions: [],
  130. showResult: false,
  131. isAnswerCorrect: false,
  132. questions: [], // 改为空数组,将通过API获取
  133. interviewId: null, // 存储当前面试ID
  134. useVideo: false,
  135. timerInterval: null,
  136. score: 0,
  137. totalQuestions: 0,
  138. interviewCompleted: false,
  139. digitalHumanUrl: '', // 数字人URL
  140. loading: true, // 添加加载状态
  141. loadError: false, // 添加加载错误状态
  142. errorMessage: '' // 添加错误消息
  143. }
  144. },
  145. computed: {
  146. currentQuestion() {
  147. console.log(this.questions[this.currentQuestionIndex]);
  148. return this.questions[this.currentQuestionIndex];
  149. }
  150. },
  151. onLoad(options) {
  152. // 从路由参数中获取面试ID
  153. if (options && options.id) {
  154. this.interviewId = options.id;
  155. this.fetchInterviewData();
  156. } else {
  157. // 如果没有ID,可以获取面试列表并使用第一个
  158. this.fetchInterviewList();
  159. }
  160. },
  161. onReady() {
  162. // 创建相机上下文
  163. this.cameraContext = uni.createCameraContext();
  164. // 创建AI视频上下文
  165. if (this.useVideo) {
  166. this.aiVideoContext = uni.createVideoContext('aiInterviewer');
  167. }
  168. // 初始化数字人
  169. this.initDigitalHuman();
  170. },
  171. methods: {
  172. // 获取面试列表
  173. async fetchInterviewList() {
  174. try {
  175. this.loading = true;
  176. const res = await getInterviewList();
  177. console.log(res);
  178. // 使用第一个面试
  179. this.interviewId = res.items[0].id;
  180. this.fetchInterviewData();
  181. } catch (error) {
  182. console.error('获取面试列表失败:', error);
  183. this.handleLoadError('获取面试列表失败');
  184. }
  185. },
  186. // 获取面试详情数据
  187. async fetchInterviewData() {
  188. try {
  189. this.loading = true;
  190. const res = await getInterviewDetail({ id: this.interviewId });
  191. console.log('API返回数据:', res);
  192. // 如果返回的是问题列表
  193. if (res && Array.isArray(res.items)) {
  194. // 处理多个问题
  195. this.questions = res.items.map((q, index) => ({
  196. id: q.id || index + 1,
  197. text: q.question || '未知问题',
  198. options:q.options || [],
  199. correctAnswer: q.correctAnswer || 0,
  200. isImportant: q.is_system || false,
  201. explanation: q.explanation || '',
  202. questionType: q.question_form || 1,
  203. questionTypeName: q.question_form_name || '单选题',
  204. correctAnswers: q.correct_answers || [],
  205. difficulty: q.difficulty || 1,
  206. difficultyName: q.difficulty_name || '初级'
  207. }));
  208. } else {
  209. // 处理单个问题
  210. this.processInterviewData(res);
  211. }
  212. console.log(this.questions);
  213. // 设置总题目数
  214. this.totalQuestions = this.questions.length;
  215. // 启动倒计时
  216. if (this.questions.length > 0) {
  217. this.startTimer();
  218. }
  219. } catch (error) {
  220. console.error('获取面试详情失败:', error);
  221. this.handleLoadError('获取面试详情失败');
  222. } finally {
  223. this.loading = false;
  224. }
  225. },
  226. // 处理面试数据
  227. processInterviewData(data) {
  228. // 清空现有问题列表
  229. this.questions = [];
  230. // 检查数据格式并处理
  231. if (data) {
  232. // 创建一个格式化的问题对象
  233. const formattedQuestion = {
  234. id: data.id || 1,
  235. text: data.question || '未知问题',
  236. options: data.options || [],
  237. correctAnswer: data.correctAnswer || 0,
  238. isImportant: data.is_system || false,
  239. explanation: data.explanation || '',
  240. questionType: data.question_form || 1, // 1-单选题,2-多选题
  241. questionTypeName: data.question_form_name || '单选题',
  242. correctAnswers: data.correct_answers || [],
  243. difficulty: data.difficulty || 1,
  244. difficultyName: data.difficulty_name || '初级'
  245. };
  246. // 添加到问题列表
  247. this.questions.push(formattedQuestion);
  248. // 设置总题目数
  249. this.totalQuestions = this.questions.length;
  250. // 启动倒计时
  251. this.startTimer();
  252. } else {
  253. this.handleLoadError('面试中没有问题');
  254. }
  255. },
  256. // 处理加载错误
  257. handleLoadError(message) {
  258. this.loadError = true;
  259. this.loading = false;
  260. this.errorMessage = message || '加载失败';
  261. uni.showToast({
  262. title: message || '加载失败',
  263. icon: 'none',
  264. duration: 2000
  265. });
  266. },
  267. startTimer() {
  268. // 确保问题已加载
  269. if (this.questions.length === 0) return;
  270. // 模拟倒计时
  271. let seconds = 30; // 每题30秒
  272. this.timerInterval = setInterval(() => {
  273. seconds--;
  274. if (seconds <= 0) {
  275. clearInterval(this.timerInterval);
  276. // 时间到,自动提交答案
  277. if (!this.showResult) {
  278. this.checkAnswer();
  279. }
  280. }
  281. const min = Math.floor(seconds / 60).toString().padStart(2, '0');
  282. const sec = (seconds % 60).toString().padStart(2, '0');
  283. this.remainingTime = `${min}:${sec}`;
  284. }, 1000);
  285. },
  286. resetTimer() {
  287. clearInterval(this.timerInterval);
  288. this.startTimer();
  289. },
  290. selectOption(index) {
  291. if (this.showResult) return; // 已显示结果时不能再选择
  292. // 判断当前题目类型
  293. if (this.currentQuestion.questionType === 2) { // 多选题
  294. // 如果已经选中,则取消选中
  295. const optionIndex = this.selectedOptions.indexOf(index);
  296. if (optionIndex > -1) {
  297. this.selectedOptions.splice(optionIndex, 1);
  298. } else {
  299. // 否则添加到已选中数组
  300. this.selectedOptions.push(index);
  301. }
  302. } else { // 单选题
  303. this.selectedOption = index;
  304. }
  305. this.playAiSpeaking();
  306. },
  307. checkAnswer() {
  308. clearInterval(this.timerInterval); // 停止计时
  309. // 确保当前问题存在
  310. if (!this.currentQuestion) {
  311. console.error('当前问题不存在');
  312. return;
  313. }
  314. // 根据题目类型检查答案是否正确
  315. if (this.currentQuestion.questionType === 2) { // 多选题
  316. // 对于多选题,需要比较选中的选项数组和正确答案数组
  317. // 先排序以确保顺序一致
  318. const sortedSelected = [...this.selectedOptions].sort();
  319. const sortedCorrect = [...this.currentQuestion.correctAnswers].sort();
  320. // 检查长度是否相同
  321. if (sortedSelected.length !== sortedCorrect.length) {
  322. this.isAnswerCorrect = false;
  323. } else {
  324. // 检查每个元素是否相同
  325. this.isAnswerCorrect = sortedSelected.every((value, index) => value === sortedCorrect[index]);
  326. }
  327. } else { // 单选题
  328. this.isAnswerCorrect = this.selectedOption === this.currentQuestion.correctAnswer;
  329. }
  330. // 更新分数
  331. if (this.isAnswerCorrect) {
  332. this.score++;
  333. }
  334. // 显示结果
  335. this.showResult = true;
  336. // 检查是否是最后一题
  337. if (this.currentQuestionIndex === this.questions.length - 1) {
  338. // 显示AI面试结束页面
  339. setTimeout(() => {
  340. // this.showEndModal = true;
  341. this.interviewCompleted = false;
  342. }, 1500);
  343. return;
  344. }
  345. // 不是最后一题,延迟后进入下一题
  346. setTimeout(() => {
  347. this.goToNextQuestion();
  348. }, 1500);
  349. },
  350. nextQuestion() {
  351. // 如果还没有显示结果,先检查答案
  352. if (this.currentQuestion.questionType === 2) { // 多选题
  353. if (this.selectedOptions.length > 0 && !this.showResult) {
  354. this.checkAnswer();
  355. }
  356. } else { // 单选题
  357. if (this.selectedOption !== null && !this.showResult) {
  358. this.checkAnswer();
  359. } else if (this.showResult) {
  360. // 如果已经显示结果,进入下一题
  361. this.goToNextQuestion();
  362. }
  363. }
  364. },
  365. // 新增方法,处理进入下一题的逻辑
  366. goToNextQuestion() {
  367. // 重置状态
  368. this.showResult = false;
  369. this.selectedOption = null;
  370. this.selectedOptions = []; // 重置多选题选项
  371. // 前往下一题
  372. this.currentQuestionIndex++;
  373. // 检查是否已完成所有题目
  374. if (this.currentQuestionIndex >= this.questions.length) {
  375. // 显示AI面试结束页面,确保弹窗不显示
  376. this.showEndModal = true;
  377. this.interviewCompleted = false;
  378. // 确保清除计时器
  379. if (this.timerInterval) {
  380. clearInterval(this.timerInterval);
  381. }
  382. return;
  383. }
  384. // 重置计时器
  385. this.resetTimer();
  386. // 更新进度
  387. this.progressWidth = (this.currentQuestionIndex + 1) / this.questions.length * 100;
  388. // 播放AI介绍下一题
  389. this.playAiSpeaking();
  390. setTimeout(() => {
  391. this.pauseAiSpeaking();
  392. }, 2000);
  393. },
  394. toggleSettings() {
  395. // 打开设置面板
  396. uni.showToast({
  397. title: '设置功能开发中',
  398. icon: 'none'
  399. });
  400. },
  401. back() {
  402. // 清除计时器
  403. if (this.timerInterval) {
  404. clearInterval(this.timerInterval);
  405. }
  406. // 尝试返回上一页,如果失败则跳转到首页
  407. try {
  408. const pages = getCurrentPages();
  409. if (pages.length > 1) {
  410. uni.reLaunch({
  411. url: '/pages/index/index'
  412. });
  413. } else {
  414. // 如果当前是第一页,则跳转到首页
  415. uni.reLaunch({
  416. url: '/pages/index/index'
  417. });
  418. }
  419. } catch (e) {
  420. console.error('导航错误:', e);
  421. // 出错时也跳转到首页
  422. uni.reLaunch({
  423. url: '/pages/index/index'
  424. });
  425. }
  426. },
  427. error(e) {
  428. // 相机错误事件
  429. console.error(e.detail);
  430. uni.showToast({
  431. title: '相机启动失败,请检查权限设置',
  432. icon: 'none'
  433. });
  434. },
  435. playAiSpeaking() {
  436. if (this.useVideo && this.aiVideoContext) {
  437. this.aiVideoContext.play();
  438. }
  439. // 触发数字人说话动画
  440. if (this.digitalHumanUrl) {
  441. // 假设当前问题的文本作为数字人要说的内容
  442. const speakText = this.currentQuestion ? this.currentQuestion.text : '';
  443. this.interactWithDigitalHuman(speakText);
  444. }
  445. },
  446. pauseAiSpeaking() {
  447. if (this.useVideo && this.aiVideoContext) {
  448. this.aiVideoContext.pause();
  449. }
  450. },
  451. // 重新开始测试
  452. restartTest() {
  453. this.currentQuestionIndex = 0;
  454. this.score = 0;
  455. this.showEndModal = false;
  456. this.showResult = false;
  457. this.selectedOption = null;
  458. this.selectedOptions = [];
  459. this.resetTimer();
  460. },
  461. // 在methods中添加测试方法
  462. testEndScreen() {
  463. this.interviewCompleted = true;
  464. this.showEndModal = false;
  465. },
  466. // 初始化数字人
  467. initDigitalHuman() {
  468. // 移除不可用的示例URL
  469. // this.digitalHumanUrl = 'https://your-digital-human-service.com/avatar?id=123';
  470. // 暂时不使用数字人服务,直接使用本地图片
  471. this.digitalHumanUrl = '';
  472. // 如果您有实际可用的数字人服务,可以在这里设置
  473. // 例如:this.digitalHumanUrl = 'https://your-actual-service.com/avatar';
  474. // 或者使用本地HTML文件(如果有的话)
  475. // this.digitalHumanUrl = '/hybrid/html/digital-human.html';
  476. },
  477. // 与数字人交互的方法
  478. interactWithDigitalHuman(message) {
  479. // 如果数字人是通过web-view加载的,可以使用postMessage与其通信
  480. const webview = this.$mp.page.$getAppWebview().children()[0];
  481. if (webview) {
  482. webview.evalJS(`receiveMessage('${message}')`);
  483. }
  484. }
  485. },
  486. // 添加生命周期钩子,确保在组件销毁时清除计时器
  487. beforeDestroy() {
  488. if (this.timerInterval) {
  489. clearInterval(this.timerInterval);
  490. }
  491. }
  492. }
  493. </script>
  494. <style>
  495. .camera-container {
  496. position: relative;
  497. width: 100%;
  498. height: 100vh;
  499. background-color: #ffffff;
  500. display: flex;
  501. flex-direction: column;
  502. }
  503. .header {
  504. height: 90rpx;
  505. /* background-color: #000000; */
  506. display: flex;
  507. align-items: center;
  508. padding: 0 30rpx;
  509. /* color: #ffffff; */
  510. }
  511. .back-button {
  512. width: 60rpx;
  513. height: 60rpx;
  514. display: flex;
  515. align-items: center;
  516. justify-content: center;
  517. }
  518. .title {
  519. flex: 1;
  520. text-align: center;
  521. font-size: 32rpx;
  522. font-weight: bold;
  523. }
  524. .controls {
  525. display: flex;
  526. gap: 30rpx;
  527. }
  528. .iconfont {
  529. font-size: 40rpx;
  530. }
  531. .content {
  532. flex: 1;
  533. display: flex;
  534. flex-direction: column;
  535. position: relative;
  536. padding: 50rpx 20rpx 0;
  537. }
  538. .progress-container {
  539. display: flex;
  540. flex-direction: column;
  541. justify-content: space-between;
  542. margin-bottom: 20rpx;
  543. }
  544. .progress-step {
  545. display: flex;
  546. align-items: center;
  547. margin-bottom: 10rpx;
  548. }
  549. .step-circle {
  550. width: 40rpx;
  551. height: 40rpx;
  552. border-radius: 50%;
  553. background-color: #e0e0e0;
  554. display: flex;
  555. align-items: center;
  556. justify-content: center;
  557. font-size: 24rpx;
  558. margin-right: 10rpx;
  559. }
  560. .step-circle.active {
  561. background-color: #ff9500;
  562. color: #ffffff;
  563. }
  564. .step-text {
  565. font-size: 24rpx;
  566. color: #666666;
  567. }
  568. .progress-bar {
  569. height: 10rpx;
  570. background-color: #e0e0e0;
  571. border-radius: 5rpx;
  572. overflow: hidden;
  573. margin-bottom: 20rpx;
  574. }
  575. .progress-fill {
  576. height: 100%;
  577. background-color: #00c853;
  578. transition: width 0.3s;
  579. }
  580. .interview-content {
  581. flex: 1;
  582. display: flex;
  583. flex-direction: column;
  584. }
  585. .video-area {
  586. display: flex;
  587. /* flex-direction: column; */
  588. justify-content: space-between;
  589. height: auto;
  590. margin-bottom: 20rpx;
  591. }
  592. .video-camera {
  593. display: flex;
  594. flex-direction: row;
  595. align-items: center;
  596. justify-content: space-between;
  597. width: 65%;
  598. }
  599. .interviewer {
  600. background-color: #f5f5f5;
  601. display: flex;
  602. align-items: center;
  603. justify-content: center;
  604. position: relative;
  605. width: 48% !important;
  606. height: 300rpx;
  607. }
  608. .interviewer-video {
  609. width: 100%;
  610. height: 100%;
  611. object-fit: cover;
  612. }
  613. .interviewer-avatar {
  614. width: 80%;
  615. height: 80%;
  616. object-fit: contain;
  617. }
  618. .user-camera-container {
  619. width: 48%;
  620. height: 300rpx;
  621. border-radius: 10rpx;
  622. overflow: hidden;
  623. border: 4rpx solid #ffffff;
  624. box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.2);
  625. }
  626. .question-area {
  627. flex: 1;
  628. display: flex;
  629. flex-direction: column;
  630. }
  631. .question-number {
  632. font-size: 28rpx;
  633. color: #666;
  634. margin-bottom: 20rpx;
  635. }
  636. .question-importance {
  637. background-color: #f9f9f9;
  638. padding: 20rpx;
  639. border-radius: 10rpx;
  640. margin-bottom: 20rpx;
  641. font-size: 28rpx;
  642. color: #333;
  643. }
  644. .question-importance text {
  645. color: #ff0000;
  646. margin-right: 10rpx;
  647. }
  648. .options {
  649. display: flex;
  650. flex-direction: column;
  651. gap: 20rpx;
  652. margin-bottom: 30rpx;
  653. }
  654. .option-item {
  655. padding: 20rpx;
  656. background-color: #f5f5f5;
  657. border-radius: 10rpx;
  658. font-size: 28rpx;
  659. transition: all 0.3s;
  660. border: 1px solid #e0e0e0;
  661. }
  662. .option-selected {
  663. background-color: #e6f7ff;
  664. border: 1px solid #1890ff;
  665. }
  666. .option-correct {
  667. background-color: #d4f7d4;
  668. border: 1px solid #52c41a;
  669. }
  670. .option-wrong {
  671. background-color: #fff1f0;
  672. border: 1px solid #ff4d4f;
  673. }
  674. .option-text {
  675. color: #333;
  676. }
  677. .timer-container {
  678. text-align: center;
  679. margin: 20rpx 0;
  680. }
  681. .timer-text {
  682. font-size: 24rpx;
  683. color: #666;
  684. }
  685. .next-button {
  686. background-color: #6c5ce7;
  687. color: #ffffff;
  688. border-radius: 10rpx;
  689. font-size: 30rpx;
  690. margin: 20rpx auto;
  691. height: 80rpx;
  692. line-height: 80rpx;
  693. width: 90%;
  694. text-align: center;
  695. }
  696. .next-button[disabled] {
  697. background-color: #cccccc;
  698. color: #999999;
  699. }
  700. .footer {
  701. height: 100rpx;
  702. background-color: #000000;
  703. display: flex;
  704. justify-content: space-between;
  705. align-items: center;
  706. padding: 0 30rpx;
  707. color: #ffffff;
  708. }
  709. .timer {
  710. font-size: 28rpx;
  711. }
  712. .next-step {
  713. font-size: 28rpx;
  714. }
  715. .interview-end-modal {
  716. position: fixed;
  717. top: 0;
  718. left: 0;
  719. right: 0;
  720. bottom: 0;
  721. background-color: rgba(0, 0, 0, 0.5);
  722. display: flex;
  723. align-items: center;
  724. justify-content: center;
  725. z-index: 999;
  726. }
  727. .modal-content {
  728. width: 80%;
  729. background-color: #ffffff;
  730. border-radius: 20rpx;
  731. padding: 40rpx;
  732. display: flex;
  733. flex-direction: column;
  734. align-items: center;
  735. }
  736. .modal-title {
  737. font-size: 36rpx;
  738. font-weight: bold;
  739. margin-bottom: 30rpx;
  740. }
  741. .score-display {
  742. font-size: 48rpx;
  743. font-weight: bold;
  744. color: #6c5ce7;
  745. margin: 20rpx 0;
  746. }
  747. .modal-message {
  748. font-size: 28rpx;
  749. color: #666;
  750. text-align: center;
  751. margin-bottom: 40rpx;
  752. }
  753. .modal-buttons {
  754. display: flex;
  755. justify-content: space-between;
  756. width: 100%;
  757. margin-top: 20rpx;
  758. }
  759. .modal-button {
  760. width: 45%;
  761. }
  762. @keyframes speaking {
  763. 0% {
  764. opacity: 0.8;
  765. }
  766. 50% {
  767. opacity: 1;
  768. }
  769. 100% {
  770. opacity: 0.8;
  771. }
  772. }
  773. .speaking {
  774. animation: speaking 1.5s infinite;
  775. }
  776. .digital-avatar {
  777. width: 120rpx;
  778. height: 120rpx;
  779. z-index: 10;
  780. overflow: hidden;
  781. }
  782. .avatar-image {
  783. width: 120rpx;
  784. height: 120rpx;
  785. border-radius: 20rpx;
  786. border: 2rpx solid #e0e0e0;
  787. }
  788. .digital-human-webview {
  789. width: 120rpx;
  790. height: 120rpx;
  791. border-radius: 20rpx;
  792. border: 2rpx solid #e0e0e0;
  793. }
  794. .interview-complete-screen {
  795. position: fixed;
  796. top: 0;
  797. left: 0;
  798. right: 0;
  799. bottom: 0;
  800. background-color: #f0f5ff;
  801. display: flex;
  802. flex-direction: column;
  803. align-items: center;
  804. justify-content: center;
  805. z-index: 1000;
  806. padding: 40rpx;
  807. }
  808. .digital-avatar-large {
  809. width: 200rpx;
  810. height: 200rpx;
  811. margin-bottom: 60rpx;
  812. }
  813. .avatar-image-large {
  814. width: 100%;
  815. height: 100%;
  816. border-radius: 30rpx;
  817. }
  818. .complete-message {
  819. font-size: 40rpx;
  820. font-weight: bold;
  821. color: #333;
  822. margin-bottom: 30rpx;
  823. }
  824. .complete-description {
  825. font-size: 28rpx;
  826. color: #666;
  827. text-align: center;
  828. margin-bottom: 80rpx;
  829. line-height: 1.5;
  830. }
  831. .complete-button {
  832. background-color: #6c5ce7;
  833. color: #ffffff;
  834. border-radius: 10rpx;
  835. font-size: 32rpx;
  836. padding: 20rpx 60rpx;
  837. margin-top: 40rpx;
  838. }
  839. /* 添加加载状态样式 */
  840. .loading-container {
  841. position: absolute;
  842. top: 0;
  843. left: 0;
  844. right: 0;
  845. bottom: 0;
  846. display: flex;
  847. flex-direction: column;
  848. align-items: center;
  849. justify-content: center;
  850. background-color: rgba(255, 255, 255, 0.9);
  851. z-index: 100;
  852. }
  853. .loading-text {
  854. margin-top: 20rpx;
  855. font-size: 28rpx;
  856. color: #666;
  857. }
  858. .error-container {
  859. text-align: center;
  860. }
  861. .error-message {
  862. font-size: 28rpx;
  863. color: #ff4d4f;
  864. margin-bottom: 30rpx;
  865. }
  866. .retry-button {
  867. background-color: #6c5ce7;
  868. color: #ffffff;
  869. border-radius: 10rpx;
  870. font-size: 28rpx;
  871. padding: 10rpx 30rpx;
  872. }
  873. .question-type {
  874. display: inline-block;
  875. background-color: #6c5ce7;
  876. color: white;
  877. font-size: 24rpx;
  878. padding: 4rpx 10rpx;
  879. border-radius: 6rpx;
  880. margin-right: 10rpx;
  881. }
  882. </style>